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/EmacsService.java | 384 +++++++++++++++++++++++++++++++++++ 1 file changed, 384 insertions(+) create mode 100644 java/org/gnu/emacs/EmacsService.java (limited to 'java/org/gnu/emacs/EmacsService.java') 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); + } +}; -- cgit v1.3 From a32963e11f9f8e5d22b0d754d34a867f3b178ed2 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 2 Jan 2023 21:38:19 +0800 Subject: Update Android port * Makefile.in (java): Depend on info. (MAKEFILE_NAME): (config.status): Remove unneeded changes. * configure.ac (BUILD_DETAILS, ANDROID_STUBIFY): Don't require a C++ compiler on Android. * java/AndroidManifest.xml: : Set launchMode appropriately. : New activity. * java/Makefile.in (CROSS_BINS): Add EmacsClient. * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity) (onCreate): Use the window attachment manager. * java/org/gnu/emacs/EmacsCopyArea.java (EmacsCopyArea) (paintTo): Implement clip masks correctly. * java/org/gnu/emacs/EmacsDrawRectangle.java (getRect, paintTo): Fix damage tracking rectangles. * java/org/gnu/emacs/EmacsFontDriver.java (FontSpec, toString): New function. (FontMetrics, EmacsFontDriver): Fix signature of textExtents. * java/org/gnu/emacs/EmacsMultitaskActivity.java (EmacsMultitaskActivity): New file. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New functions sendFocusIn, sendFocusOut, sendWindowAction. * java/org/gnu/emacs/EmacsPaintQueue.java (run): Fix clipping handling. * java/org/gnu/emacs/EmacsPixmap.java (EmacsPixmap): Add constructor for mutable pixmaps. * java/org/gnu/emacs/EmacsSdk23FontDriver.java (EmacsSdk23FontDriver): New file. * java/org/gnu/emacs/EmacsSdk7FontDriver.java (EmacsSdk7FontDriver, Sdk7Typeface, Sdk7FontEntity, Sdk7FontObject) (checkMatch, hasChar, encodeChar): Implement text display and fix font metrics semantics. * java/org/gnu/emacs/EmacsService.java (EmacsService): Remove availableChildren. (getLibraryDirectory, onCreate): Pass pixel density to Emacs. (clearArea): Fix arguments. Switch to using the window attachment manager. * java/org/gnu/emacs/EmacsSurfaceView.java (surfaceChanged) (surfaceCreated): Flip buffers on surface attachment. * java/org/gnu/emacs/EmacsView.java (EmacsView, swapBuffers): New argument FORCE. Always swap if it is true. (onKeyMultiple, onFocusChanged): New functions. * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, destroyHandle) (run): Switch to using the window attachment manager. * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager): New file. * lisp/cus-edit.el (custom-button, custom-button-mouse) (custom-button-pressed): * lisp/faces.el (tool-bar): Define faces correctly on Android. * src/android.c (struct android_emacs_pixmap): Add mutable constructor. (struct android_emacs_drawable): New structure. (android_write_event): Check if event queue hasn't yet been initialized. (android_select): Set errno to EINTR if pselect fails. (android_close): Remove unused debugging code. (android_get_home_directory): New function. (Java_org_gnu_emacs_EmacsNative_setEmacsParams): Set pixel density and compute game path. (android_init_emacs_drawable): New function. (Java_org_gnu_emacs_EmacsNative_sendKeyPress): New argument `unicode_char'. Pass it in events. (Java_org_gnu_emacs_EmacsNative_sendKeyRelease): Likewise. (Java_org_gnu_emacs_EmacsNative_sendFocusIn) (Java_org_gnu_emacs_EmacsNative_sendFocusOut) (Java_org_gnu_emacs_EmacsNative_sendWindowAction): New functions. (android_resolve_handle): Export function. (android_change_gc): Clear clip rects under the right circumstances. Set right clip mask field. (android_create_pixmap_from_bitmap_data): Use correct alpha channels. (android_create_pixmap): Create mutable pixmap and avoid redundant color array allocation. (android_create_bitmap_from_data, android_create_image) (android_destroy_image, android_put_pixel, android_get_pixel) (android_get_image, android_put_image, faccessat): New functions. * src/android.h: Update prototypes. * src/androidfns.c (android_default_font_parameter): Prefer monospace to Droid Sans Mono. * src/androidfont.c (struct android_emacs_font_driver): New method `draw'. (struct android_emacs_font_spec): New field `dpi'. (struct androidfont_info): Add font metrics cache. (android_init_font_driver, android_init_font_spec): Adjust accordingly. (androidfont_from_lisp, androidfont_from_java): Handle new fields. (androidfont_draw): Implement function. (androidfont_open_font): Set pixel size correctly. (androidfont_close_font): Free metrics cache. (androidfont_cache_text_extents) (androidfont_check_cached_extents): New functions. (androidfont_text_extents): Cache glyph metrics somewhere for future use. (androidfont_list_family): Implement function. * src/androidgui.h (enum android_event_type): New focus and window action events. (enum android_modifier_mask): New masks. (struct android_key_event): New field `unicode_char'. (ANDROID_IS_MODIFIER_KEY): Newmacro. (struct android_focus_event, struct android_window_action_event): New structs. (union android_event): Add new fields. (enum android_image_format, struct android_image): New enums and structs. * src/androidterm.c (android_android_to_emacs_modifiers) (android_emacs_to_android_modifiers, android_lower_frame) (android_raise_frame, android_new_focus_frame) (android_focus_changed, android_detect_focus_change): New functions. (handle_one_android_event): Implement focus and key event handling. (android_frame_rehighlight): New function. (android_frame_raise_lower): Implement accordingly. (android_make_frame_invisible): Clear highlight_frame if required. (android_free_frame_resources): Clear x_focus_event_frame if required. (android_draw_fringe_bitmap, android_draw_image_foreground) (android_draw_image_foreground_1) (android_draw_image_glyph_string): Remove unnecessary code. (android_create_terminal, android_term_init): Set the baud rate to something sensible. * src/androidterm.h (struct android_bitmap_record): Make structure the same as on X. (struct android_display_info): New focus tracking fields. (struct android_output): Likewise. * src/dispextern.h (struct image): Add ximg and mask_img on Android. * src/emacs.c (android_emacs_init): Fix argc sorting iteration. * src/fileio.c (user_homedir): (get_homedir): Implement correctly on Android. * src/font.h (PT_PER_INCH): Define correctly on Android. * src/fringe.c (X, swap_nibble, init_fringe_bitmap): Swap fringe bitmaps correctly on Android. * src/image.c (GET_PIXEL, image_create_bitmap_from_data) (image_create_bitmap_from_file, free_bitmap_record) (image_unget_x_image_or_dc, struct image_type) (prepare_image_for_display, image_clear_image_1) (image_size_in_bytes, x_check_image_size) (x_create_x_image_and_pixmap, x_destroy_x_image) (image_check_image_size, image_create_x_image_and_pixmap_1) (image_destroy_x_image, gui_put_x_image, image_put_x_image) (image_get_x_image, image_unget_x_image) (Create_Pixmap_From_Bitmap_Data, image_pixmap_draw_cross) (MaskForeground, image_types, syms_of_image): Implement all of the above on Android in terms of an API very similar to X. * src/keyboard.c (FUNCTION_KEY_OFFSET, lispy_function_keys): Define on Android to something sensible. * src/lread.c (build_load_history): Fix problem. --- Makefile.in | 17 +- configure.ac | 20 +- java/AndroidManifest.xml | 9 +- java/Makefile.in | 3 +- java/org/gnu/emacs/EmacsActivity.java | 153 +++-- java/org/gnu/emacs/EmacsCopyArea.java | 135 ++++- java/org/gnu/emacs/EmacsDrawRectangle.java | 8 +- java/org/gnu/emacs/EmacsFontDriver.java | 26 +- java/org/gnu/emacs/EmacsMultitaskActivity.java | 25 + java/org/gnu/emacs/EmacsNative.java | 18 +- java/org/gnu/emacs/EmacsPaintQueue.java | 24 +- java/org/gnu/emacs/EmacsPixmap.java | 29 + java/org/gnu/emacs/EmacsSdk23FontDriver.java | 43 ++ java/org/gnu/emacs/EmacsSdk7FontDriver.java | 188 +++++- java/org/gnu/emacs/EmacsService.java | 83 +-- java/org/gnu/emacs/EmacsSurfaceView.java | 37 +- java/org/gnu/emacs/EmacsView.java | 45 +- java/org/gnu/emacs/EmacsWindow.java | 143 +++-- .../gnu/emacs/EmacsWindowAttachmentManager.java | 166 ++++++ lisp/cus-edit.el | 6 +- lisp/faces.el | 2 +- src/android.c | 627 +++++++++++++++++++-- src/android.h | 15 + src/androidfns.c | 5 +- src/androidfont.c | 270 ++++++--- src/androidgui.h | 81 +++ src/androidterm.c | 544 ++++++++++-------- src/androidterm.h | 30 +- src/dispextern.h | 11 +- src/emacs.c | 5 +- src/fileio.c | 9 + src/font.h | 6 + src/fringe.c | 23 +- src/image.c | 249 +++++--- src/keyboard.c | 53 +- src/lread.c | 20 +- 36 files changed, 2435 insertions(+), 693 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsMultitaskActivity.java create mode 100644 java/org/gnu/emacs/EmacsSdk23FontDriver.java create mode 100644 java/org/gnu/emacs/EmacsWindowAttachmentManager.java (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/Makefile.in b/Makefile.in index d8c5ba555d3..6ca1b1dc382 100644 --- a/Makefile.in +++ b/Makefile.in @@ -536,7 +536,7 @@ lisp: src lib lib-src lisp nt: Makefile $(MAKE) -C $@ all -java: lisp +java: lisp info $(MAKE) -C $@ all xcompile: src @@ -571,20 +571,10 @@ blessmail: Makefile src # then attempts to build that file. This forces 'Makefile', 'lib/Makefile', # etc. to be built without running into similar recursion problems. MAKEFILE_NAME = Makefile -ifeq ($(ANDROID),) $(MAKEFILE_NAME): config.status $(srcdir)/configure \ $(srcdir)/lib/gnulib.mk.in \ $(srcdir)/Makefile.in $(SUBDIR_MAKEFILES_IN) $(CONFIG_STATUS_FILES_IN) MAKE='$(MAKE)' ./config.status -else -# Note that calling config.status is insufficient on Android due to -# the recursive calls to configure. - -$(MAKEFILE_NAME): $(srcdir)/configure \ - $(srcdir)/lib/gnulib.mk.in \ - $(srcdir)/Makefile.in $(SUBDIR_MAKEFILES_IN) $(CONFIG_STATUS_FILES_IN) - $(CFG) $(srcdir)/configure $(CONFIGURE_FLAGS); -endif # Don't erase these files if make is interrupted while refreshing them. .PRECIOUS: Makefile config.status @@ -592,17 +582,12 @@ endif # Note that calling config.status --recheck is insufficient on Android # due to the recursive calls to configure. -ifneq ($(ANDROID),) -config.status: ${srcdir}/configure - $(CFG) $(srcdir)/configure $(CONFIGURE_FLAGS) -else config.status: ${srcdir}/configure if [ -x ./config.status ]; then \ $(CFG) ./config.status --recheck; \ else \ $(CFG) $(srcdir)/configure $(CONFIGURE_FLAGS); \ fi -endif $(srcdir)/configure: $(srcdir)/configure.ac $(srcdir)/m4/*.m4 cd $(srcdir) && ./autogen.sh autoconf diff --git a/configure.ac b/configure.ac index da8d753afca..70dbfa10b35 100644 --- a/configure.ac +++ b/configure.ac @@ -31,9 +31,8 @@ if test "$XCONFIGURE" = "android"; then # Android! AC_MSG_NOTICE([called to recursively configure Emacs \ for Android.]) - # Set CC to ANDROID_CC, and CXX to ANDROID_CXX. + # Set CC to ANDROID_CC. CC=$ANDROID_CC - CXX=$ANDROID_CXX fi dnl Set emacs_config_options to the options of 'configure', quoted for the shell, @@ -147,7 +146,7 @@ if test "$XCONFIGURE" = "android"; then ;; *) AC_MSG_ERROR([The cross compiler does not compile for Android. Please verify that you specified the correct compiler in the ANDROID_CC -and ANDROID_CXX variables when you ran configure.]) +variable when you ran configure.]) ;; esac AC_MSG_RESULT([$host_alias]) @@ -759,8 +758,7 @@ tools such as aapt, dx, and aidl): The cross-compiler should then be specified: - ANDROID_CC=/path/to/armv7a-linux-androideabi19-clang - ANDROID_CXX=/path/to/armv7a-linux-androideabi19-clang++]) + ANDROID_CC=/path/to/armv7a-linux-androideabi19-clang]) elif test "$with_android" = "no" || test "$with_android" = ""; then ANDROID=no else @@ -833,12 +831,11 @@ Please verify that the path to the SDK build tools you specified is correct]); dnl Now configure Emacs to generate binaries for Android. After the dnl configuration completes, move the generated Makefiles. - if test "$ANDROID_CC" = "" || test "$ANDROID_CXX" = ""; then + if test "$ANDROID_CC" = ""; then AC_MSG_ERROR([Please specify the path to the Android cross-compiler for your machine. For example: ANDROID_CC=/path/to/armv7a-linux-androideabi19-clang \\ - ANDROID_CXX=/path/to/armv7a-linux-androideabi19-clang++ \\ ./configure --with-android]) fi @@ -863,7 +860,7 @@ for your machine. For example: *) AC_MSG_ERROR([configure could not determine the type of Android \ binary Emacs is being configured for. Please port this configure script \ to your Android system, or verify that you specified the correct compiler \ -in the ANDROID_CC and ANDROID_CXX variables when you ran configure.]) +in the ANDROID_CC variable when you ran configure.]) ;; esac AC_MSG_RESULT([$android_abi]) @@ -874,9 +871,8 @@ in the ANDROID_CC and ANDROID_CXX variables when you ran configure.]) mv -f confdefs.h _confdefs.h mv -f config.log _config.log - AS_IF([XCONFIGURE=android ANDROID_CC="$ANDROID_CC" \ - ANDROID_CXX="$ANDROID_CXX" $0], [], [AC_MSG_ERROR([Failed to cross-\ -configure Emacs for android.])]) + AS_IF([XCONFIGURE=android ANDROID_CC="$ANDROID_CC" $0], [], + [AC_MSG_ERROR([Failed to cross-configure Emacs for android.])]) # Now set ANDROID to yes. ANDROID=yes @@ -2254,7 +2250,7 @@ for Android, but all API calls need to be stubbed out]) ANDROID_CFLAGS="-fPIC -fvisibility=hidden" # Link with libraries required for Android support. - ANDROID_LIBS="-landroid -llog" + ANDROID_LIBS="-landroid -llog -ljnigraphics" fi fi diff --git a/java/AndroidManifest.xml b/java/AndroidManifest.xml index 75aa5bdf409..06417017ae3 100644 --- a/java/AndroidManifest.xml +++ b/java/AndroidManifest.xml @@ -31,13 +31,15 @@ android:targetSdkVersion="28"/> - + @@ -45,6 +47,9 @@ + + availableActivities; - /* The currently attached EmacsWindow, or null if none. */ private EmacsWindow window; /* The frame layout associated with the activity. */ private FrameLayout layout; + /* List of activities with focus. */ + private static List focusedActivities; + + /* The currently focused window. */ + public static EmacsWindow focusedWindow; + static { - /* Set up the list of available activities. */ - availableActivities = new ArrayList (); + focusedActivities = new ArrayList (); }; + public static void + invalidateFocus1 (EmacsWindow window) + { + if (window.view.isFocused ()) + focusedWindow = window; + + for (EmacsWindow child : window.children) + invalidateFocus1 (window); + } + + public static void + invalidateFocus () + { + EmacsWindow oldFocus; + + /* Walk through each focused activity and assign the window focus + to the bottom-most focused window within. Record the old focus + as well. */ + oldFocus = focusedWindow; + focusedWindow = null; + + for (EmacsActivity activity : focusedActivities) + { + if (activity.window != null) + invalidateFocus1 (activity.window); + } + + /* Send focus in- and out- events to the previous and current + focus. */ + + if (oldFocus != null) + EmacsNative.sendFocusOut (oldFocus.handle, + System.currentTimeMillis ()); + + if (focusedWindow != null) + EmacsNative.sendFocusIn (focusedWindow.handle, + System.currentTimeMillis ()); + } + + @Override public void - attachChild (EmacsWindow child) + detachWindow () + { + if (window == null) + Log.w (TAG, "detachWindow called, but there is no window"); + else + { + /* Clear the window's pointer to this activity and remove the + window's view. */ + window.setConsumer (null); + layout.removeView (window.view); + window = null; + + invalidateFocus (); + } + } + + @Override + public void + attachWindow (EmacsWindow child) { if (window != null) throw new IllegalStateException ("trying to attach window when one" + " already exists"); /* Record and attach the view. */ + window = child; layout.addView (window.view); + child.setConsumer (this); - /* Remove the objects from the lists of what is available. */ - EmacsService.availableChildren.remove (child); - availableActivities.remove (this); - - /* Now set child->activity. */ - child.setActivity (this); + /* Invalidate the focus. */ + invalidateFocus (); } - /* Make this activity available for future windows to attach - again. */ - + @Override public void - makeAvailable () + destroy () { - window = null; - - for (EmacsWindow iterWindow - : EmacsService.availableChildren) - { - synchronized (iterWindow) - { - if (!iterWindow.isDestroyed ()) - attachChild (iterWindow); - - return; - } - } + finish (); + } - availableActivities.add (this); + @Override + public EmacsWindow + getAttachedWindow () + { + return window; } @Override @@ -109,38 +158,38 @@ public class EmacsActivity extends Activity /* Set it as the content view. */ setContentView (layout); - /* Make the activity available before starting the - service. */ - makeAvailable (); - if (EmacsService.SERVICE == null) /* Start the Emacs service now. */ startService (new Intent (this, EmacsService.class)); + /* Add this activity to the list of available activities. */ + EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this); + super.onCreate (savedInstanceState); } @Override public void - onStop () + onDestroy () { - /* The activity is no longer visible. If there is a window - attached, detach it. */ - - if (window != null) - { - layout.removeView (window.view); + /* The activity will die shortly hereafter. If there is a window + attached, close it now. */ + Log.d (TAG, "onDestroy " + this); + EmacsWindowAttachmentManager.MANAGER.removeWindowConsumer (this); + focusedActivities.remove (this); + invalidateFocus (); + super.onDestroy (); + } - /* Notice that the window is already available too. But do - not call noticeAvailableChild; that might assign it to some - other activity, which behaves badly. */ - EmacsService.availableChildren.add (window); - window = null; - } + @Override + public void + onWindowFocusChanged (boolean isFocused) + { + if (isFocused && !focusedActivities.contains (this)) + focusedActivities.add (this); + else + focusedActivities.remove (this); - /* Finally, remove this activity from the list of available - activities. */ - availableActivities.remove (this); - super.onStop (); + invalidateFocus (); } }; diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index f34d1ecde01..0dd5b2c1fb6 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -32,19 +32,22 @@ public class EmacsCopyArea implements EmacsPaintReq private int src_x, src_y, dest_x, dest_y, width, height; private EmacsDrawable destination, source; private EmacsGC immutableGC; - private static Xfermode xorAlu, srcInAlu; + private static Xfermode xorAlu, srcInAlu, overAlu; static { + overAlu = new PorterDuffXfermode (Mode.SRC_OVER); xorAlu = new PorterDuffXfermode (Mode.XOR); srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); }; public - EmacsCopyArea (EmacsDrawable destination, EmacsDrawable source, + EmacsCopyArea (EmacsDrawable source, EmacsDrawable destination, int src_x, int src_y, int width, int height, int dest_x, int dest_y, EmacsGC immutableGC) { + Bitmap bitmap; + this.destination = destination; this.source = source; this.src_x = src_x; @@ -71,6 +74,16 @@ public class EmacsCopyArea implements EmacsPaintReq return destination; } + private void + insetRectBy (Rect rect, int left, int top, int right, + int bottom) + { + rect.left += left; + rect.top += top; + rect.right -= right; + rect.bottom -= bottom; + } + @Override public EmacsGC getGC () @@ -86,16 +99,45 @@ public class EmacsCopyArea implements EmacsPaintReq Bitmap bitmap; Paint maskPaint; Canvas maskCanvas; - Bitmap maskBitmap; - Rect rect, srcRect; + Bitmap srcBitmap, maskBitmap, clipBitmap; + Rect rect, maskRect, srcRect, dstRect, maskDestRect; + boolean needFill; /* TODO implement stippling. */ if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) return; alu = immutableGC.function; + + /* A copy must be created or drawBitmap could end up overwriting + itself. */ + srcBitmap = source.getBitmap (); + + /* If srcBitmap is out of bounds, then adjust the source rectangle + to be within bounds. Note that tiling on windows with + backgrounds is unimplemented. */ + + if (src_x < 0) + { + width += src_x; + dest_x -= src_x; + src_x = 0; + } + + if (src_y < 0) + { + height += src_y; + dest_y -= src_y; + src_y = 0; + } + + if (src_x + width > srcBitmap.getWidth ()) + width = srcBitmap.getWidth () - src_x; + + if (src_y + height > srcBitmap.getHeight ()) + height = srcBitmap.getHeight () - src_y; + rect = getRect (); - bitmap = source.getBitmap (); if (alu == EmacsGC.GC_COPY) paint.setXfermode (null); @@ -103,29 +145,76 @@ public class EmacsCopyArea implements EmacsPaintReq paint.setXfermode (xorAlu); if (immutableGC.clip_mask == null) - canvas.drawBitmap (bitmap, new Rect (src_x, src_y, - src_x + width, - src_y + height), - rect, paint); + { + bitmap = Bitmap.createBitmap (srcBitmap, + src_x, src_y, width, + height); + canvas.drawBitmap (bitmap, null, rect, paint); + } else { - maskPaint = new Paint (); - srcRect = new Rect (0, 0, rect.width (), - rect.height ()); - maskBitmap - = immutableGC.clip_mask.bitmap.copy (Bitmap.Config.ARGB_8888, - true); - - if (maskBitmap == null) + /* Drawing with a clip mask involves calculating the + intersection of the clip mask with the dst rect, and + extrapolating the corresponding part of the src rect. */ + clipBitmap = immutableGC.clip_mask.bitmap; + dstRect = new Rect (dest_x, dest_y, + dest_x + width, + dest_y + height); + maskRect = new Rect (immutableGC.clip_x_origin, + immutableGC.clip_y_origin, + (immutableGC.clip_x_origin + + clipBitmap.getWidth ()), + (immutableGC.clip_y_origin + + clipBitmap.getHeight ())); + clipBitmap = immutableGC.clip_mask.bitmap; + + if (!maskRect.setIntersect (dstRect, maskRect)) + /* There is no intersection between the clip mask and the + dest rect. */ return; - maskPaint.setXfermode (srcInAlu); + /* Now figure out which part of the source corresponds to + maskRect and return it relative to srcBitmap. */ + srcRect = new Rect (src_x, src_y, src_x + width, + src_y + height); + insetRectBy (srcRect, maskRect.left - dstRect.left, + maskRect.top - dstRect.top, + maskRect.right - dstRect.right, + maskRect.bottom - dstRect.bottom); + + /* Finally, create a temporary bitmap that is the size of + maskRect. */ + + maskBitmap + = Bitmap.createBitmap (maskRect.width (), maskRect.height (), + Bitmap.Config.ARGB_8888); + + /* Draw the mask onto the maskBitmap. */ maskCanvas = new Canvas (maskBitmap); - maskCanvas.drawBitmap (bitmap, new Rect (src_x, src_y, - src_x + width, - src_y + height), - srcRect, maskPaint); - canvas.drawBitmap (maskBitmap, srcRect, rect, paint); + maskRect.offset (-immutableGC.clip_x_origin, + -immutableGC.clip_y_origin); + maskCanvas.drawBitmap (immutableGC.clip_mask.bitmap, + maskRect, new Rect (0, 0, + maskRect.width (), + maskRect.height ()), + paint); + maskRect.offset (immutableGC.clip_x_origin, + immutableGC.clip_y_origin); + + /* Set the transfer mode to SRC_IN to preserve only the parts + of the source that overlap with the mask. */ + maskPaint = new Paint (); + maskPaint.setXfermode (srcInAlu); + + /* Draw the source. */ + maskDestRect = new Rect (0, 0, srcRect.width (), + srcRect.height ()); + maskCanvas.drawBitmap (srcBitmap, srcRect, maskDestRect, + maskPaint); + + /* Finally, draw the mask bitmap to the destination. */ + paint.setXfermode (overAlu); + canvas.drawBitmap (maskBitmap, null, maskRect, paint); } } } diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index 462bf7c85b5..e3f28227146 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -57,7 +57,10 @@ public class EmacsDrawRectangle implements EmacsPaintReq public Rect getRect () { - return new Rect (x, y, x + width, y + height); + /* Canvas.drawRect actually behaves exactly like PolyRectangle wrt + to where the lines are placed, so extend the width and height + by 1 in the damage rectangle. */ + return new Rect (x, y, x + width + 1, y + height + 1); } @Override @@ -89,9 +92,10 @@ public class EmacsDrawRectangle implements EmacsPaintReq return; alu = immutableGC.function; - rect = getRect (); + rect = new Rect (x, y, x + width, y + height); paint.setStyle (Paint.Style.STROKE); + paint.setStrokeWidth (1); if (alu == EmacsGC.GC_COPY) paint.setXfermode (null); diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java index f419e71059d..9f40aa04c44 100644 --- a/java/org/gnu/emacs/EmacsFontDriver.java +++ b/java/org/gnu/emacs/EmacsFontDriver.java @@ -21,6 +21,8 @@ package org.gnu.emacs; import java.util.List; +import android.os.Build; + public abstract class EmacsFontDriver { /* Font weights. */ @@ -75,6 +77,7 @@ public abstract class EmacsFontDriver public Integer size; public Integer spacing; public Integer avgwidth; + public Integer dpi; @Override public String @@ -88,7 +91,8 @@ public abstract class EmacsFontDriver + " weight: " + weight + " slant: " + slant + " spacing: " + spacing - + " avgwidth: " + avgwidth); + + " avgwidth: " + avgwidth + + " dpi: " + dpi); } }; @@ -99,6 +103,17 @@ public abstract class EmacsFontDriver public short width; public short ascent; public short descent; + + @Override + public String + toString () + { + return ("lbearing " + lbearing + + " rbearing " + rbearing + + " width " + width + + " ascent " + ascent + + " descent " + descent); + } } public class FontEntity extends FontSpec @@ -139,12 +154,19 @@ public abstract class EmacsFontDriver public abstract FontObject openFont (FontEntity fontEntity, int pixelSize); public abstract int hasChar (FontSpec font, char charCode); public abstract void textExtents (FontObject font, int code[], - FontMetrics fontMetrics[]); + FontMetrics fontMetrics); public abstract int encodeChar (FontObject fontObject, char charCode); + public abstract int draw (FontObject fontObject, EmacsGC gc, + EmacsDrawable drawable, int[] chars, + int x, int y, int backgroundWidth, + boolean withBackground); public static EmacsFontDriver createFontDriver () { + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) + return new EmacsSdk23FontDriver (); + return new EmacsSdk7FontDriver (); } }; diff --git a/java/org/gnu/emacs/EmacsMultitaskActivity.java b/java/org/gnu/emacs/EmacsMultitaskActivity.java new file mode 100644 index 00000000000..dbdc99a3559 --- /dev/null +++ b/java/org/gnu/emacs/EmacsMultitaskActivity.java @@ -0,0 +1,25 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +public class EmacsMultitaskActivity extends EmacsActivity +{ + +} diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 6550e6fa2a1..c80339031a8 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -38,10 +38,15 @@ public class EmacsNative libDir must be the package's data storage location for native libraries. It is used as PATH. + pixelDensityX and pixelDensityY are the DPI values that will be + used by Emacs. + emacsService must be the emacsService singleton. */ public static native void setEmacsParams (AssetManager assetManager, String filesDir, String libDir, + float pixelDensityX, + float pixelDensityY, EmacsService emacsService); /* Initialize Emacs with the argument array ARGV. Each argument @@ -59,11 +64,20 @@ public class EmacsNative /* Send an ANDROID_KEY_PRESS event. */ public static native void sendKeyPress (short window, long time, int state, - int keyCode); + int keyCode, int unicodeChar); /* Send an ANDROID_KEY_RELEASE event. */ public static native void sendKeyRelease (short window, long time, int state, - int keyRelease); + int keyCode, int unicodeChar); + + /* Send an ANDROID_FOCUS_IN event. */ + public static native void sendFocusIn (short window, long time); + + /* Send an ANDROID_FOCUS_OUT event. */ + public static native void sendFocusOut (short window, long time); + + /* Send an ANDROID_WINDOW_ACTION event. */ + public static native void sendWindowAction (short window, int action); static { diff --git a/java/org/gnu/emacs/EmacsPaintQueue.java b/java/org/gnu/emacs/EmacsPaintQueue.java index 5af5868d3b9..f4840dbf5ae 100644 --- a/java/org/gnu/emacs/EmacsPaintQueue.java +++ b/java/org/gnu/emacs/EmacsPaintQueue.java @@ -47,7 +47,7 @@ public class EmacsPaintQueue { EmacsDrawable drawable, last; Canvas canvas; - EmacsGC gc, lastGC; + EmacsGC gc; int i; Paint paint; Rect rect, offsetRect, copyRect; @@ -60,45 +60,34 @@ public class EmacsPaintQueue for (EmacsPaintReq req : paintOperations) { drawable = req.getDrawable (); - - synchronized (req) - { - /* Ignore graphics requests for drawables that have been - destroyed. */ - if (drawable.isDestroyed ()) - continue; - } - canvas = drawable.lockCanvas (); if (canvas == null) /* No canvas is currently available. */ continue; - lastGC = gc; gc = req.getGC (); rect = req.getRect (); + drawable.damageRect (rect); + if (gc.clip_rects == null) { /* No clipping is applied. Just draw and continue. */ - canvas.save (); req.paintTo (canvas, paint, gc); - canvas.restore (); - drawable.damageRect (rect); continue; } if (gc.clip_rects != null && gc.clip_rects.length > 0) { - canvas.save (); - if (gc.clip_rects.length == 1) { /* There is only a single clip rect, which is simple enough. */ + canvas.save (); canvas.clipRect (gc.clip_rects[0]); req.paintTo (canvas, paint, gc); + canvas.restore (); } else { @@ -122,9 +111,6 @@ public class EmacsPaintQueue } } } - - drawable.damageRect (rect); - canvas.restore (); } } } diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java index ccd5f1e0043..897902c5b57 100644 --- a/java/org/gnu/emacs/EmacsPixmap.java +++ b/java/org/gnu/emacs/EmacsPixmap.java @@ -69,6 +69,35 @@ public class EmacsPixmap extends EmacsHandleObject this.depth = depth; } + public + EmacsPixmap (short handle, int width, int height, int depth) + { + super (handle); + + if (depth != 1 && depth != 24) + throw new IllegalArgumentException ("Invalid depth specified" + + " for pixmap: " + depth); + + switch (depth) + { + case 1: + bitmap = Bitmap.createBitmap (width, height, + Bitmap.Config.ALPHA_8, + false); + break; + + case 24: + bitmap = Bitmap.createBitmap (width, height, + Bitmap.Config.ARGB_8888, + false); + break; + } + + this.width = width; + this.height = height; + this.depth = depth; + } + @Override public Canvas lockCanvas () diff --git a/java/org/gnu/emacs/EmacsSdk23FontDriver.java b/java/org/gnu/emacs/EmacsSdk23FontDriver.java new file mode 100644 index 00000000000..34e2b1803a2 --- /dev/null +++ b/java/org/gnu/emacs/EmacsSdk23FontDriver.java @@ -0,0 +1,43 @@ +/* Font backend for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.graphics.Paint; + +public class EmacsSdk23FontDriver extends EmacsSdk7FontDriver +{ + @Override + public int + hasChar (FontSpec font, char charCode) + { + Sdk7FontObject fontObject; + Paint paint; + + if (font instanceof Sdk7FontObject) + { + fontObject = (Sdk7FontObject) font; + paint = fontObject.typeface.typefacePaint; + } + else + paint = ((Sdk7FontEntity) font).typeface.typefacePaint; + + return paint.hasGlyph (String.valueOf (charCode)) ? 1 : 0; + } +}; diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index 5a8cdbfc75b..437f38e62db 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -28,16 +28,19 @@ import java.util.List; import android.graphics.Paint; import android.graphics.Rect; import android.graphics.Typeface; +import android.graphics.Canvas; import android.util.Log; +import android.os.Build; + public class EmacsSdk7FontDriver extends EmacsFontDriver { private static final String TOFU_STRING = "\uDB3F\uDFFD"; private static final String EM_STRING = "m"; private static final String TAG = "EmacsSdk7FontDriver"; - private class Sdk7Typeface + protected class Sdk7Typeface { /* The typeface and paint. */ public Typeface typeface; @@ -57,7 +60,10 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver width = UNSPECIFIED; spacing = PROPORTIONAL; + this.typeface = typeface; + typefacePaint = new Paint (); + typefacePaint.setAntiAlias (true); typefacePaint.setTypeface (typeface); /* For the calls to measureText below. */ @@ -160,7 +166,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver } }; - private class Sdk7FontEntity extends FontEntity + protected class Sdk7FontEntity extends FontEntity { /* The typeface. */ public Sdk7Typeface typeface; @@ -177,19 +183,17 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver slant = typeface.slant; spacing = typeface.spacing; width = typeface.width; + dpi = Math.round (EmacsService.SERVICE.metrics.scaledDensity * 160f); this.typeface = typeface; } }; - private class Sdk7FontObject extends FontObject + protected class Sdk7FontObject extends FontObject { /* The typeface. */ public Sdk7Typeface typeface; - /* The text size. */ - public int pixelSize; - public Sdk7FontObject (Sdk7Typeface typeface, int pixelSize) { @@ -205,6 +209,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver slant = typeface.slant; spacing = typeface.spacing; width = typeface.width; + dpi = Math.round (EmacsService.SERVICE.metrics.scaledDensity * 160f); /* Compute the ascent and descent. */ typeface.typefacePaint.setTextSize (pixelSize); @@ -238,6 +243,93 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver } }; + private class Sdk7DrawString implements EmacsPaintReq + { + private boolean drawBackground; + private Sdk7FontObject fontObject; + private char[] chars; + private EmacsGC immutableGC; + private EmacsDrawable drawable; + private Rect rect; + private int originX, originY; + + public + Sdk7DrawString (Sdk7FontObject fontObject, char[] chars, + EmacsGC immutableGC, EmacsDrawable drawable, + boolean drawBackground, Rect rect, + int originX, int originY) + { + this.fontObject = fontObject; + this.chars = chars; + this.immutableGC = immutableGC; + this.drawable = drawable; + this.drawBackground = drawBackground; + this.rect = rect; + this.originX = originX; + this.originY = originY; + } + + @Override + public EmacsDrawable + getDrawable () + { + return drawable; + } + + @Override + public EmacsGC + getGC () + { + return immutableGC; + } + + @Override + public void + paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) + { + int scratch; + + paint.setStyle (Paint.Style.FILL); + + if (drawBackground) + { + paint.setColor (immutableGC.background | 0xff000000); + canvas.drawRect (rect, paint); + } + + paint.setTextSize (fontObject.pixelSize); + paint.setColor (immutableGC.foreground | 0xff000000); + paint.setTypeface (fontObject.typeface.typeface); + paint.setAntiAlias (true); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + /* Disable hinting as that leads to displayed text not + matching the computed metrics. */ + paint.setHinting (Paint.HINTING_OFF); + + canvas.drawText (chars, 0, chars.length, originX, originY, paint); + paint.setAntiAlias (false); + } + + @Override + public Rect + getRect () + { + Rect rect; + + rect = new Rect (); + + fontObject.typeface.typefacePaint.setTextSize (fontObject.pixelSize); + fontObject.typeface.typefacePaint.getTextBounds (chars, 0, chars.length, + rect); + + /* Add the background rect to the damage as well. */ + rect.union (this.rect); + + return rect; + } + }; + private String[] fontFamilyList; private Sdk7Typeface[] typefaceList; private Sdk7Typeface fallbackTypeface; @@ -252,7 +344,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver systemFontsDirectory = new File ("/system/fonts"); fontFamilyList = systemFontsDirectory.list (); - typefaceList = new Sdk7Typeface[fontFamilyList.length]; + typefaceList = new Sdk7Typeface[fontFamilyList.length + 3]; /* It would be nice to avoid opening each and every font upon startup. But that doesn't seem to be possible on @@ -267,8 +359,18 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver typeface); } + /* Initialize the default monospace and serif typefaces. */ fallbackTypeface = new Sdk7Typeface ("monospace", Typeface.MONOSPACE); + typefaceList[fontFamilyList.length] = fallbackTypeface; + + fallbackTypeface = new Sdk7Typeface ("Monospace", + Typeface.MONOSPACE); + typefaceList[fontFamilyList.length + 1] = fallbackTypeface; + + fallbackTypeface = new Sdk7Typeface ("Sans Serif", + Typeface.DEFAULT); + typefaceList[fontFamilyList.length + 2] = fallbackTypeface; } private boolean @@ -278,11 +380,6 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver && !fontSpec.family.equals (typeface.familyName)) return false; - - if (fontSpec.adstyle != null - && !fontSpec.adstyle.isEmpty ()) - /* return false; */; - if (fontSpec.slant != null && !fontSpec.weight.equals (typeface.weight)) return false; @@ -393,7 +490,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver paint.getTextBounds (TOFU_STRING, 0, TOFU_STRING.length (), rect1); paint.getTextBounds ("" + charCode, 0, 1, rect2); - return rect1.equals (rect2) ? 1 : 0; + return rect1.equals (rect2) ? 0 : 1; } private void @@ -434,21 +531,47 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver @Override public void - textExtents (FontObject font, int code[], FontMetrics fontMetrics[]) + textExtents (FontObject font, int code[], FontMetrics fontMetrics) { int i; Paint paintCache; Rect boundsCache; Sdk7FontObject fontObject; + char[] text; + float width; fontObject = (Sdk7FontObject) font; paintCache = fontObject.typeface.typefacePaint; paintCache.setTextSize (fontObject.pixelSize); boundsCache = new Rect (); - for (i = 0; i < code.length; ++i) - textExtents1 ((Sdk7FontObject) font, code[i], fontMetrics[i], + if (code.length == 0) + { + fontMetrics.lbearing = 0; + fontMetrics.rbearing = 0; + fontMetrics.ascent = 0; + fontMetrics.descent = 0; + fontMetrics.width = 0; + } + else if (code.length == 1) + textExtents1 ((Sdk7FontObject) font, code[0], fontMetrics, paintCache, boundsCache); + else + { + text = new char[code.length]; + + for (i = 0; i < code.length; ++i) + text[i] = (char) code[i]; + + paintCache.getTextBounds (text, 0, 1, boundsCache); + width = paintCache.measureText (text, 0, code.length); + + fontMetrics.lbearing = (short) boundsCache.left; + fontMetrics.rbearing = (short) boundsCache.right; + fontMetrics.ascent = (short) -boundsCache.top; + fontMetrics.descent = (short) boundsCache.bottom; + fontMetrics.width = (short) Math.round (width); + } } @Override @@ -457,4 +580,37 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver { return charCode; } + + @Override + public int + draw (FontObject fontObject, EmacsGC gc, EmacsDrawable drawable, + int[] chars, int x, int y, int backgroundWidth, + boolean withBackground) + { + Rect backgroundRect; + Sdk7FontObject sdk7FontObject; + Sdk7DrawString op; + char[] charsArray; + int i; + + sdk7FontObject = (Sdk7FontObject) fontObject; + charsArray = new char[chars.length]; + + for (i = 0; i < chars.length; ++i) + charsArray[i] = (char) chars[i]; + + backgroundRect = new Rect (); + backgroundRect.top = y - sdk7FontObject.ascent; + backgroundRect.left = x; + backgroundRect.right = x + backgroundWidth; + backgroundRect.bottom = y + sdk7FontObject.descent; + + op = new Sdk7DrawString (sdk7FontObject, charsArray, + gc.immutableGC (), drawable, + withBackground, + backgroundRect, x, y); + + EmacsService.SERVICE.appendPaintOperation (op); + return 1; + } }; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 311226e6f7e..41a45b0bd85 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -28,6 +28,8 @@ import android.graphics.Canvas; import android.graphics.Bitmap; import android.graphics.Point; +import android.view.View; + import android.annotation.TargetApi; import android.app.Service; import android.content.Context; @@ -37,7 +39,9 @@ import android.os.Build; import android.os.Looper; import android.os.IBinder; import android.os.Handler; + import android.util.Log; +import android.util.DisplayMetrics; class Holder { @@ -57,14 +61,8 @@ public class EmacsService extends Service private Handler handler; private EmacsPaintQueue paintQueue; - /* List of all EmacsWindows that are available to attach to an - activity. */ - public static List availableChildren; - - static - { - availableChildren = new ArrayList (); - }; + /* Display metrics used by font backends. */ + public DisplayMetrics metrics; @Override public int @@ -88,7 +86,7 @@ public class EmacsService extends Service Context context; context = getApplicationContext (); - apiLevel = android.os.Build.VERSION.SDK_INT; + apiLevel = Build.VERSION.SDK_INT; if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) return context.getApplicationInfo().nativeLibraryDir; @@ -105,11 +103,16 @@ public class EmacsService extends Service AssetManager manager; Context app_context; String filesDir, libDir; + double pixelDensityX; + double pixelDensityY; SERVICE = this; handler = new Handler (Looper.getMainLooper ()); manager = getAssets (); app_context = getApplicationContext (); + metrics = getResources ().getDisplayMetrics (); + pixelDensityX = metrics.xdpi; + pixelDensityY = metrics.ydpi; try { @@ -122,6 +125,8 @@ public class EmacsService extends Service + " and libDir = " + libDir); EmacsNative.setEmacsParams (manager, filesDir, libDir, + (float) pixelDensityX, + (float) pixelDensityY, this); /* Start the thread that runs Emacs. */ @@ -147,7 +152,8 @@ public class EmacsService extends Service } EmacsView - getEmacsView (final EmacsWindow window) + getEmacsView (final EmacsWindow window, final int visibility, + final boolean isFocusedByDefault) { Runnable runnable; final Holder view; @@ -161,6 +167,8 @@ public class EmacsService extends Service synchronized (this) { view.thing = new EmacsView (window); + view.thing.setVisibility (visibility); + view.thing.setFocusedByDefault (isFocusedByDefault); notify (); } } @@ -183,48 +191,6 @@ public class EmacsService extends Service return view.thing; } - /* Notice that a child of the root window named WINDOW is now - available for attachment to a specific activity. */ - - public void - noticeAvailableChild (final EmacsWindow window) - { - Log.d (TAG, "A new child is available: " + window); - - handler.post (new Runnable () { - public void - run () - { - for (EmacsActivity activity - : EmacsActivity.availableActivities) - { - /* TODO: check if the activity matches. */ - activity.attachChild (window); - break; - } - - /* Nope, wait for an activity to become available. */ - availableChildren.add (window); - } - }); - } - - /* Notice that a child of the root window named WINDOW has been - destroyed. */ - - public void - noticeChildDestroyed (final EmacsWindow child) - { - handler.post (new Runnable () { - @Override - public void - run () - { - availableChildren.remove (child); - } - }); - } - /* X drawing operations. These are quite primitive operations. The drawing queue is kept on the Emacs thread, but is periodically flushed to the application thread, upon buffers swaps and once it @@ -311,11 +277,6 @@ public class EmacsService extends Service ensurePaintQueue (); - if (gc.clip_rects != null && gc.clip_rects.length >= 1) - android.util.Log.d ("drawRectangle", - gc.clip_rects[0].toString () - + " " + gc.toString ()); - req = new EmacsDrawRectangle (drawable, x, y, width, height, gc.immutableGC ()); @@ -381,4 +342,12 @@ public class EmacsService extends Service { window.clearArea (x, y, width, height); } + + public void + appendPaintOperation (EmacsPaintReq op) + { + ensurePaintQueue (); + paintQueue.appendPaintOperation (op); + checkFlush (); + } }; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 194f6ad37a3..b8b828e4820 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -22,6 +22,8 @@ package org.gnu.emacs; import android.view.SurfaceView; import android.view.SurfaceHolder; +import android.os.Build; + import android.graphics.Canvas; import android.graphics.Rect; @@ -40,7 +42,9 @@ public class EmacsSurfaceView extends SurfaceView surfaceChanged (SurfaceHolder holder, int format, int width, int height) { - + /* Force a buffer swap now to get the contents of the Emacs + view on screen. */ + view.swapBuffers (true); } @Override @@ -51,7 +55,7 @@ public class EmacsSurfaceView extends SurfaceView /* Force a buffer swap now to get the contents of the Emacs view on screen. */ - view.swapBuffers (); + view.swapBuffers (true); } @Override @@ -72,12 +76,37 @@ public class EmacsSurfaceView extends SurfaceView public Canvas lockCanvas (Rect damage) { - return getHolder ().lockCanvas (damage); + SurfaceHolder holder; + + holder = getHolder (); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + damage.setEmpty (); + return holder.lockHardwareCanvas (); + } + + return holder.lockCanvas (damage); + } + + /* This method is only used during debugging when it seems damage + isn't working correctly. */ + + public Canvas + lockCanvas () + { + SurfaceHolder holder; + + holder = getHolder (); + return holder.lockCanvas (); } public void unlockCanvasAndPost (Canvas canvas) { - getHolder ().unlockCanvasAndPost (canvas); + SurfaceHolder holder; + + holder = getHolder (); + holder.unlockCanvasAndPost (canvas); } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 237946d6366..7b48eaf0aa6 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -19,17 +19,20 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import android.content.res.ColorStateList; + import android.view.View; import android.view.KeyEvent; import android.view.ViewGroup; + import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; import android.graphics.Region; import android.graphics.Paint; -import android.util.Log; import android.os.Build; +import android.util.Log; /* This is an Android view which has a back and front buffer. When swapBuffers is called, the back buffer is swapped to the front @@ -70,10 +73,11 @@ public class EmacsView extends ViewGroup this.damageRegion = new Region (); this.paint = new Paint (); + setFocusable (true); + setFocusableInTouchMode (true); + /* Create the surface view. */ this.surfaceView = new EmacsSurfaceView (this); - - setFocusable (FOCUSABLE); addView (this.surfaceView); } @@ -162,7 +166,7 @@ public class EmacsView extends ViewGroup } public void - swapBuffers () + swapBuffers (boolean force) { Bitmap back; Canvas canvas; @@ -185,14 +189,25 @@ public class EmacsView extends ViewGroup if (canvas == null) return; - /* Copy from the back buffer to the canvas. */ - canvas.drawBitmap (bitmap, damageRect, damageRect, paint); + /* Copy from the back buffer to the canvas. If damageRect was + made empty, then draw the entire back buffer. */ + + if (damageRect.isEmpty ()) + canvas.drawBitmap (bitmap, 0f, 0f, paint); + else + canvas.drawBitmap (bitmap, damageRect, damageRect, paint); /* Unlock the canvas and clear the damage. */ surfaceView.unlockCanvasAndPost (canvas); damageRegion.setEmpty (); } + public void + swapBuffers () + { + swapBuffers (false); + } + @Override public boolean onKeyDown (int keyCode, KeyEvent event) @@ -201,6 +216,14 @@ public class EmacsView extends ViewGroup return true; } + @Override + public boolean + onKeyMultiple (int keyCode, int repeatCount, KeyEvent event) + { + window.onKeyDown (keyCode, event); + return true; + } + @Override public boolean onKeyUp (int keyCode, KeyEvent event) @@ -208,4 +231,14 @@ public class EmacsView extends ViewGroup window.onKeyUp (keyCode, event); return true; } + + @Override + public void + onFocusChanged (boolean gainFocus, int direction, + Rect previouslyFocusedRect) + { + window.onFocusChanged (gainFocus); + super.onFocusChanged (gainFocus, direction, + previouslyFocusedRect); + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 28db04a261d..26e788a20a8 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -32,6 +32,8 @@ import android.view.View; import android.view.ViewGroup; import android.view.KeyEvent; +import android.content.Intent; + /* This defines a window, which is a handle. Windows represent a rectangular subset of the screen with their own contents. @@ -57,10 +59,10 @@ public class EmacsWindow extends EmacsHandleObject /* List of all children in stacking order. This must be kept consistent! */ - private ArrayList children; + public ArrayList children; - /* The EmacsActivity currently attached, if it exists. */ - private EmacsActivity attached; + /* The window consumer currently attached, if it exists. */ + private EmacsWindowAttachmentManager.WindowConsumer attached; /* The window background scratch GC. foreground is always the window background. */ @@ -74,35 +76,44 @@ public class EmacsWindow extends EmacsHandleObject rect = new Rect (x, y, x + width, y + height); - /* Create the view from the context's UI thread. */ - view = EmacsService.SERVICE.getEmacsView (this); + /* Create the view from the context's UI thread. The window is + unmapped, so the view is GONE. */ + view = EmacsService.SERVICE.getEmacsView (this, View.GONE, + parent == null); this.parent = parent; - children = new ArrayList (); - /* The window is unmapped by default. */ - view.setVisibility (View.GONE); + /* Create the list of children. */ + children = new ArrayList (); - /* If parent is the root window, notice that there are new - children available for interested activites to pick up. */ - if (parent == null) - EmacsService.SERVICE.noticeAvailableChild (this); - else + if (parent != null) { - /* Otherwise, directly add this window as a child of that - window's view. */ - synchronized (parent) + parent.children.add (this); + parent.view.post (new Runnable () { + @Override + public void + run () + { + parent.view.addView (view); + } + }); + } + else + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () { - parent.children.add (this); - parent.view.post (new Runnable () { - @Override - public void - run () - { - parent.view.addView (view); - } - }); + EmacsWindowAttachmentManager manager; + + manager = EmacsWindowAttachmentManager.MANAGER; + + /* If parent is the root window, notice that there are new + children available for interested activites to pick + up. */ + + manager.registerWindow (EmacsWindow.this); } - } + }); scratchGC = new EmacsGC ((short) 0); } @@ -129,28 +140,35 @@ public class EmacsWindow extends EmacsHandleObject public void destroyHandle () throws IllegalStateException { - synchronized (this) - { - if (!children.isEmpty ()) - throw new IllegalStateException ("Trying to destroy window with " - + "children!"); - } + if (parent != null) + parent.children.remove (this); + + EmacsActivity.invalidateFocus (); - /* Notice that the child has been destroyed. */ - EmacsService.SERVICE.noticeChildDestroyed (this); + if (!children.isEmpty ()) + throw new IllegalStateException ("Trying to destroy window with " + + "children!"); /* Remove the view from its parent and make it invisible. */ view.post (new Runnable () { public void run () { + View parent; + EmacsWindowAttachmentManager manager; + + if (EmacsActivity.focusedWindow == EmacsWindow.this) + EmacsActivity.focusedWindow = null; + + manager = EmacsWindowAttachmentManager.MANAGER; view.setVisibility (View.GONE); - if (view.getParent () != null) - ((ViewGroup) view.getParent ()).removeView (view); + parent = (View) view.getParent (); - if (attached != null) - attached.makeAvailable (); + if (parent != null && attached == null) + ((ViewGroup) parent).removeView (view); + + manager.detachWindow (EmacsWindow.this); } }); @@ -158,12 +176,15 @@ public class EmacsWindow extends EmacsHandleObject } public void - setActivity (EmacsActivity activity) + setConsumer (EmacsWindowAttachmentManager.WindowConsumer consumer) { - synchronized (this) - { - activity = activity; - } + attached = consumer; + } + + public EmacsWindowAttachmentManager.WindowConsumer + getAttachedConsumer () + { + return attached; } public void @@ -233,7 +254,10 @@ public class EmacsWindow extends EmacsHandleObject public void run () { + view.setVisibility (View.VISIBLE); + /* Eventually this should check no-focus-on-map. */ + view.requestFocus (); } }); } @@ -319,18 +343,47 @@ public class EmacsWindow extends EmacsHandleObject public void onKeyDown (int keyCode, KeyEvent event) { + int state; + + state = event.getModifiers (); + state &= ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); + EmacsNative.sendKeyPress (this.handle, event.getEventTime (), event.getModifiers (), - keyCode); + keyCode, + /* Ignore meta-state understood by Emacs + for now, or Ctrl+C will not be + recognized as an ASCII key press + event. */ + event.getUnicodeChar (state)); } public void onKeyUp (int keyCode, KeyEvent event) { + int state; + + state = event.getModifiers (); + state &= ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK); + EmacsNative.sendKeyRelease (this.handle, event.getEventTime (), event.getModifiers (), - keyCode); + keyCode, + event.getUnicodeChar (state)); + } + + public void + onFocusChanged (boolean gainFocus) + { + EmacsActivity.invalidateFocus (); + } + + public void + onActivityDetached () + { + /* Destroy the associated frame when the activity is detached. */ + EmacsNative.sendWindowAction (this.handle, 0); } }; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java new file mode 100644 index 00000000000..34be2ab8789 --- /dev/null +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -0,0 +1,166 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import java.util.LinkedList; +import java.util.List; + +import android.content.Intent; +import android.util.Log; + +/* Code to paper over the differences in lifecycles between + "activities" and windows. There are four interfaces to an instance + of this class: + + registerWindowConsumer (WindowConsumer) + registerWindow (EmacsWindow) + removeWindowConsumer (WindowConsumer) + removeWindow (EmacsWindow) + + A WindowConsumer is expected to allow an EmacsWindow to be attached + to it, and be created or destroyed. + + Every time a window is created, registerWindow checks the list of + window consumers. If a consumer exists and does not currently have + a window of its own attached, it gets the new window. Otherwise, + the window attachment manager starts a new consumer. + + Every time a consumer is registered, registerWindowConsumer checks + the list of available windows. If a window exists and is not + currently attached to a consumer, then the consumer gets it. + + Finally, every time a window is removed, the consumer is + destroyed. */ + +public class EmacsWindowAttachmentManager +{ + public static EmacsWindowAttachmentManager MANAGER; + private final static String TAG = "EmacsWindowAttachmentManager"; + + static + { + MANAGER = new EmacsWindowAttachmentManager (); + }; + + public interface WindowConsumer + { + public void attachWindow (EmacsWindow window); + public EmacsWindow getAttachedWindow (); + public void detachWindow (); + public void destroy (); + }; + + private List consumers; + private List windows; + + public + EmacsWindowAttachmentManager () + { + consumers = new LinkedList (); + windows = new LinkedList (); + } + + public void + registerWindowConsumer (WindowConsumer consumer) + { + Log.d (TAG, "registerWindowConsumer " + consumer); + + consumers.add (consumer); + + for (EmacsWindow window : windows) + { + if (window.getAttachedConsumer () == null) + { + Log.d (TAG, "registerWindowConsumer: attaching " + window); + consumer.attachWindow (window); + return; + } + } + + Log.d (TAG, "registerWindowConsumer: sendWindowAction 0, 0"); + EmacsNative.sendWindowAction ((short) 0, 0); + } + + public void + registerWindow (EmacsWindow window) + { + Intent intent; + + Log.d (TAG, "registerWindow " + window); + windows.add (window); + + for (WindowConsumer consumer : consumers) + { + if (consumer.getAttachedWindow () == null) + { + Log.d (TAG, "registerWindow: attaching " + consumer); + consumer.attachWindow (window); + return; + } + } + + intent = new Intent (EmacsService.SERVICE, + EmacsMultitaskActivity.class); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_DOCUMENT + | Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); + EmacsService.SERVICE.startActivity (intent); + Log.d (TAG, "registerWindow: startActivity"); + } + + public void + removeWindowConsumer (WindowConsumer consumer) + { + EmacsWindow window; + + Log.d (TAG, "removeWindowConsumer " + consumer); + + window = consumer.getAttachedWindow (); + + if (window != null) + { + Log.d (TAG, "removeWindowConsumer: detaching " + window); + + consumer.detachWindow (); + window.onActivityDetached (); + } + + Log.d (TAG, "removeWindowConsumer: removing " + consumer); + consumers.remove (consumer); + } + + public void + detachWindow (EmacsWindow window) + { + WindowConsumer consumer; + + Log.d (TAG, "detachWindow " + window); + + if (window.getAttachedConsumer () != null) + { + consumer = window.getAttachedConsumer (); + + Log.d (TAG, "detachWindow: removing" + consumer); + + consumers.remove (consumer); + consumer.destroy (); + } + } +}; diff --git a/lisp/cus-edit.el b/lisp/cus-edit.el index 65eb066a554..6e71bfa6db6 100644 --- a/lisp/cus-edit.el +++ b/lisp/cus-edit.el @@ -2209,7 +2209,7 @@ and `face'." ;;; The `custom' Widget. (defface custom-button - '((((type x w32 ns haiku pgtk) (class color)) ; Like default mode line + '((((type x w32 ns haiku pgtk android) (class color)) ; Like default mode line :box (:line-width 2 :style released-button) :background "lightgrey" :foreground "black")) "Face for custom buffer buttons if `custom-raised-buttons' is non-nil." @@ -2217,7 +2217,7 @@ and `face'." :group 'custom-faces) (defface custom-button-mouse - '((((type x w32 ns haiku pgtk) (class color)) + '((((type x w32 ns haiku pgtk android) (class color)) :box (:line-width 2 :style released-button) :background "grey90" :foreground "black") (t @@ -2242,7 +2242,7 @@ and `face'." (if custom-raised-buttons 'custom-button-mouse 'highlight)) (defface custom-button-pressed - '((((type x w32 ns haiku pgtk) (class color)) + '((((type x w32 ns haiku pgtk android) (class color)) :box (:line-width 2 :style pressed-button) :background "lightgrey" :foreground "black") (t :inverse-video t)) diff --git a/lisp/faces.el b/lisp/faces.el index 0dd89be4738..2779bfc295f 100644 --- a/lisp/faces.el +++ b/lisp/faces.el @@ -2911,7 +2911,7 @@ Note: Other faces cannot inherit from the cursor face." (((type haiku)) :foreground "B_MENU_ITEM_TEXT_COLOR" :background "B_MENU_BACKGROUND_COLOR") - (((type x w32 ns pgtk) (class color)) + (((type x w32 ns pgtk android) (class color)) :background "grey75") (((type x) (class mono)) :background "grey")) diff --git a/src/android.c b/src/android.c index dd841cf383a..db12e244275 100644 --- a/src/android.c +++ b/src/android.c @@ -24,6 +24,7 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include #include #include @@ -46,6 +47,7 @@ bool android_init_gui; #include #include +#include #include #include @@ -86,6 +88,7 @@ struct android_emacs_pixmap { jclass class; jmethodID constructor; + jmethodID constructor_mutable; }; struct android_graphics_point @@ -94,6 +97,12 @@ struct android_graphics_point jmethodID constructor; }; +struct android_emacs_drawable +{ + jclass class; + jmethodID get_bitmap; +}; + /* The asset manager being used. */ static AAssetManager *asset_manager; @@ -106,6 +115,12 @@ char *android_site_load_path; /* The path used to store native libraries. */ char *android_lib_dir; +/* The path used to store game files. */ +char *android_game_path; + +/* The display's pixel densities. */ +double android_pixel_density_x, android_pixel_density_y; + /* The Android application data directory. */ static char *android_files_dir; @@ -149,6 +164,9 @@ static struct android_emacs_pixmap pixmap_class; /* Various methods associated with the Point class. */ static struct android_graphics_point point_class; +/* Various methods associated with the EmacsDrawable class. */ +static struct android_emacs_drawable drawable_class; + /* Event handling functions. Events are stored on a (circular) queue @@ -383,6 +401,10 @@ android_write_event (union android_event *event) if (!container) return; + /* If the event queue hasn't been initialized yet, return false. */ + if (!event_queue.events.next) + return; + pthread_mutex_lock (&event_queue.mutex); /* The event queue is full, wait for events to be read. */ @@ -451,6 +473,10 @@ android_select (int nfds, fd_set *readfds, fd_set *writefds, /* Unlock the event queue mutex. */ pthread_mutex_unlock (&event_queue.mutex); + /* This is to shut up process.c when pselect gets EINTR. */ + if (nfds_return < 0) + errno = EINTR; + return nfds_return; } @@ -793,16 +819,20 @@ android_close (int fd) { if (fd < ANDROID_MAX_ASSET_FD && (android_table[fd].flags & ANDROID_FD_TABLE_ENTRY_IS_VALID)) - { - __android_log_print (ANDROID_LOG_INFO, __func__, - "closing android file descriptor %d", - fd); - android_table[fd].flags = 0; - } + android_table[fd].flags = 0; return close (fd); } +/* Return the current user's ``home'' directory, which is actually the + app data directory on Android. */ + +const char * +android_get_home_directory (void) +{ + return android_files_dir; +} + /* JNI functions called by Java. */ @@ -814,6 +844,8 @@ JNIEXPORT void JNICALL NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jobject local_asset_manager, jobject files_dir, jobject libs_dir, + jfloat pixel_density_x, + jfloat pixel_density_y, jobject emacs_service_object) { int pipefd[2]; @@ -829,6 +861,9 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, return; } + android_pixel_density_x = pixel_density_x; + android_pixel_density_y = pixel_density_y; + __android_log_print (ANDROID_LOG_INFO, __func__, "Initializing "PACKAGE_STRING"...\nPlease report bugs to " PACKAGE_BUGREPORT". Thanks.\n"); @@ -891,15 +926,23 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, if (!android_site_load_path) emacs_abort (); + android_game_path = malloc (PATH_MAX + 1); + + if (!android_game_path) + emacs_abort (); + snprintf (android_site_load_path, PATH_MAX, "%s/site-lisp", android_files_dir); + snprintf (android_game_path, PATH_MAX, "%s/scores", android_files_dir); + __android_log_print (ANDROID_LOG_INFO, __func__, "Site-lisp directory: %s\n" "Files directory: %s\n" - "Native code directory: %s", + "Native code directory: %s\n" + "Game score path: %s\n", android_site_load_path, android_files_dir, - android_lib_dir); + android_lib_dir, android_game_path); /* Make a reference to the Emacs service. */ emacs_service = (*env)->NewGlobalRef (env, emacs_service_object); @@ -997,6 +1040,7 @@ android_init_emacs_pixmap (void) assert (pixmap_class.c_name); FIND_METHOD (constructor, "", "(S[IIII)V"); + FIND_METHOD (constructor_mutable, "", "(SIII)V"); #undef FIND_METHOD } @@ -1031,6 +1075,36 @@ android_init_graphics_point (void) #undef FIND_METHOD } +static void +android_init_emacs_drawable (void) +{ + jclass old; + + drawable_class.class + = (*android_java_env)->FindClass (android_java_env, + "org/gnu/emacs/EmacsDrawable"); + eassert (drawable_class.class); + + old = drawable_class.class; + drawable_class.class + = (jclass) (*android_java_env)->NewGlobalRef (android_java_env, + (jobject) old); + ANDROID_DELETE_LOCAL_REF (old); + + if (!drawable_class.class) + emacs_abort (); + +#define FIND_METHOD(c_name, name, signature) \ + drawable_class.c_name \ + = (*android_java_env)->GetMethodID (android_java_env, \ + drawable_class.class, \ + name, signature); \ + assert (drawable_class.c_name); + + FIND_METHOD (get_bitmap, "getBitmap", "()Landroid/graphics/Bitmap;"); +#undef FIND_METHOD +} + extern JNIEXPORT void JNICALL NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) { @@ -1063,6 +1137,15 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) android_init_emacs_service (); android_init_emacs_pixmap (); android_init_graphics_point (); + android_init_emacs_drawable (); + + /* Set HOME to the app data directory. */ + setenv ("HOME", android_files_dir, 1); + + /* Set the cwd to that directory as well. */ + if (chdir (android_files_dir)) + __android_log_print (ANDROID_LOG_WARN, __func__, + "chdir: %s", strerror (errno)); /* Initialize the Android GUI. */ android_init_gui = true; @@ -1099,7 +1182,8 @@ NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, extern JNIEXPORT void JNICALL NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, jshort window, jlong time, - jint state, jint keycode) + jint state, jint keycode, + jint unicode_char) { union android_event event; @@ -1108,6 +1192,7 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, event.xkey.time = time; event.xkey.state = state; event.xkey.keycode = keycode; + event.xkey.unicode_char = unicode_char; android_write_event (&event); } @@ -1115,7 +1200,8 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, extern JNIEXPORT void JNICALL NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, jshort window, jlong time, - jint state, jint keycode) + jint state, jint keycode, + jint unicode_char) { union android_event event; @@ -1124,6 +1210,46 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, event.xkey.time = time; event.xkey.state = state; event.xkey.keycode = keycode; + event.xkey.unicode_char = unicode_char; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendFocusIn) (JNIEnv *env, jobject object, + jshort window, jlong time) +{ + union android_event event; + + event.xkey.type = ANDROID_FOCUS_IN; + event.xkey.window = window; + event.xkey.time = time; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendFocusOut) (JNIEnv *env, jobject object, + jshort window, jlong time) +{ + union android_event event; + + event.xkey.type = ANDROID_FOCUS_OUT; + event.xkey.window = window; + event.xkey.time = time; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendWindowAction) (JNIEnv *env, jobject object, + jshort window, jint action) +{ + union android_event event; + + event.xaction.type = ANDROID_WINDOW_ACTION; + event.xaction.window = window; + event.xaction.action = action; android_write_event (&event); } @@ -1142,13 +1268,6 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, #define MAX_HANDLE 65535 -enum android_handle_type - { - ANDROID_HANDLE_WINDOW, - ANDROID_HANDLE_GCONTEXT, - ANDROID_HANDLE_PIXMAP, - }; - struct android_handle_entry { /* The type. */ @@ -1245,7 +1364,7 @@ android_destroy_handle (android_handle handle) android_handles[handle].handle = NULL; } -static jobject +jobject android_resolve_handle (android_handle handle, enum android_handle_type type) { @@ -1625,14 +1744,15 @@ android_change_gc (struct android_gc *gc, ANDROID_HANDLE_PIXMAP); (*android_java_env)->SetObjectField (android_java_env, gcontext, - emacs_gc_stipple, + emacs_gc_clip_mask, what); /* Clearing GCClipMask also clears the clip rectangles. */ - (*android_java_env)->SetObjectField (android_java_env, - gcontext, - emacs_gc_clip_rects, - NULL); + if (!what) + (*android_java_env)->SetObjectField (android_java_env, + gcontext, + emacs_gc_clip_rects, + NULL); } if (mask & ANDROID_GC_STIPPLE) @@ -2008,10 +2128,23 @@ android_create_pixmap_from_bitmap_data (char *data, unsigned int width, { for (x = 0; x < width; ++x) { - if (data[y / 8] & (1 << (x % 8))) - region[x] = foreground; + if (depth == 24) + { + /* The alpha channels must be set, or otherwise, the + pixmap will be created entirely transparent. */ + + if (data[y * (width + 7) / 8 + x / 8] & (1 << (x % 8))) + region[x] = foreground | 0xff000000; + else + region[x] = background | 0xff000000; + } else - region[x] = background; + { + if (data[y * (width + 7) / 8 + x / 8] & (1 << (x % 8))) + region[x] = foreground; + else + region[x] = background; + } } (*android_java_env)->SetIntArrayRegion (android_java_env, @@ -2236,36 +2369,21 @@ android_create_pixmap (unsigned int width, unsigned int height, { android_handle prev_max_handle; jobject object; - jintArray colors; android_pixmap pixmap; - /* Create the color array holding the data. */ - colors = (*android_java_env)->NewIntArray (android_java_env, - width * height); - - if (!colors) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } - /* First, allocate the pixmap handle. */ prev_max_handle = max_handle; pixmap = android_alloc_id (); if (!pixmap) - { - ANDROID_DELETE_LOCAL_REF ((jobject) colors); - error ("Out of pixmap handles!"); - } + error ("Out of pixmap handles!"); object = (*android_java_env)->NewObject (android_java_env, pixmap_class.class, - pixmap_class.constructor, - (jshort) pixmap, colors, + pixmap_class.constructor_mutable, + (jshort) pixmap, (jint) width, (jint) height, (jint) depth); - ANDROID_DELETE_LOCAL_REF ((jobject) colors); if (!object) { @@ -2313,6 +2431,387 @@ android_clear_area (android_window handle, int x, int y, (jint) width, (jint) height); } +android_pixmap +android_create_bitmap_from_data (char *bits, unsigned int width, + unsigned int height) +{ + return android_create_pixmap_from_bitmap_data (bits, 1, 0, + width, height, 1); +} + +struct android_image * +android_create_image (unsigned int depth, enum android_image_format format, + char *data, unsigned int width, unsigned int height) +{ + struct android_image *image; + + image = xmalloc (sizeof *image); + + /* Fill in the fields required by image.c. N.B. that + android_destroy_image ostensibly will free data, but image.c + mostly sets and frees data itself. */ + image->width = width; + image->height = height; + image->data = data; + image->depth = depth; + image->format = format; + + /* Now fill in the image dimensions. There are only two depths + supported by this function. */ + + if (depth == 1) + { + image->bytes_per_line = (width + 7) / 8; + image->bits_per_pixel = 1; + } + else if (depth == 24) + { + image->bytes_per_line = width * 4; + image->bits_per_pixel = 32; + } + else + emacs_abort (); + + return image; +} + +void +android_destroy_image (struct android_image *ximg) +{ + /* If XIMG->data is NULL, then it has already been freed by + image.c. */ + + if (ximg->data) + xfree (ximg->data); + xfree (ximg); +} + +void +android_put_pixel (struct android_image *ximg, int x, int y, + unsigned long pixel) +{ + char *byte, *word; + unsigned int r, g, b; + + /* Ignore out-of-bounds accesses. */ + + if (x >= ximg->width || y >= ximg->height || x < 0 || y < 0) + return; + + switch (ximg->depth) + { + case 1: + byte = ximg->data + y * ximg->bytes_per_line + x / 8; + + if (pixel) + *byte |= (1 << x % 8); + else + *byte &= ~(1 << x % 8); + break; + + case 24: + /* Unaligned accesses are problematic on Android devices. */ + word = ximg->data + y * ximg->bytes_per_line + x * 4; + + /* Swizzle the pixel into ABGR format. Android uses Skia's + ``native color type'', which is ABGR. This is despite the + format being named ``ARGB'', and more confusingly + `ANDROID_BITMAP_FORMAT_RGBA_8888' in bitmap.h. */ + r = pixel & 0x00ff0000; + g = pixel & 0x0000ff00; + b = pixel & 0x000000ff; + pixel = (r >> 16) | g | (b << 16) | 0xff000000; + + memcpy (word, &pixel, sizeof pixel); + break; + } +} + +unsigned long +android_get_pixel (struct android_image *ximg, int x, int y) +{ + char *byte, *word; + unsigned int pixel, r, g, b; + + if (x >= ximg->width || y >= ximg->height + || x < 0 || y < 0) + return 0; + + switch (ximg->depth) + { + case 1: + byte = ximg->data + y * ximg->bytes_per_line + x / 8; + return (*byte & (1 << x % 8)) ? 1 : 0; + + case 24: + word = ximg->data + y * ximg->bytes_per_line + x * 4; + memcpy (&pixel, word, sizeof pixel); + + /* Convert the pixel back to RGB. */ + b = pixel & 0x00ff0000; + g = pixel & 0x0000ff00; + r = pixel & 0x000000ff; + pixel = ((r << 16) | g | (b >> 16)) & ~0xff000000; + + return pixel; + } + + emacs_abort (); +} + +struct android_image * +android_get_image (android_drawable handle, + enum android_image_format format) +{ + jobject drawable, bitmap; + AndroidBitmapInfo bitmap_info; + size_t byte_size; + void *data; + struct android_image *image; + unsigned char *data1, *data2; + int i, x; + + /* N.B. that supporting windows requires some more work to make + EmacsDrawable.getBitmap thread safe. */ + drawable = android_resolve_handle2 (handle, ANDROID_HANDLE_WINDOW, + ANDROID_HANDLE_PIXMAP); + + /* Look up the drawable and get the bitmap corresponding to it. + Then, lock the bitmap's bits. */ + bitmap = (*android_java_env)->CallObjectMethod (android_java_env, + drawable, + drawable_class.get_bitmap); + if (!bitmap) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + memset (&bitmap_info, 0, sizeof bitmap_info); + + /* The NDK doc seems to imply this function can fail but doesn't say + what value it gives when it does! */ + AndroidBitmap_getInfo (android_java_env, bitmap, &bitmap_info); + + if (!bitmap_info.stride) + { + ANDROID_DELETE_LOCAL_REF (bitmap); + memory_full (0); + } + + /* Compute how big the image data will be. Fail if it would be too + big. */ + + if (bitmap_info.format != ANDROID_BITMAP_FORMAT_A_8) + { + if (INT_MULTIPLY_WRAPV ((size_t) bitmap_info.stride, + (size_t) bitmap_info.height, + &byte_size)) + { + ANDROID_DELETE_LOCAL_REF (bitmap); + memory_full (0); + } + } + else + /* This A8 image will be packed into A1 later on. */ + byte_size = (bitmap_info.width + 7) / 8; + + /* Lock the image data. Once again, the NDK documentation says the + call can fail, but does not say how to determine whether or not + it has failed, nor how the address is aligned. */ + data = NULL; + AndroidBitmap_lockPixels (android_java_env, bitmap, &data); + + if (!data) + { + /* Take a NULL pointer to mean that AndroidBitmap_lockPixels + failed. */ + ANDROID_DELETE_LOCAL_REF (bitmap); + memory_full (0); + } + + /* Copy the data into a new struct android_image. */ + image = xmalloc (sizeof *image); + image->width = bitmap_info.width; + image->height = bitmap_info.height; + image->data = malloc (byte_size); + + if (!image->data) + { + ANDROID_DELETE_LOCAL_REF (bitmap); + xfree (image); + memory_full (byte_size); + } + + /* Use the format of the bitmap to determine the image depth. */ + switch (bitmap_info.format) + { + case ANDROID_BITMAP_FORMAT_RGBA_8888: + image->depth = 24; + image->bits_per_pixel = 32; + break; + + /* A8 images are used by Emacs to represent bitmaps. They have + to be packed manually. */ + case ANDROID_BITMAP_FORMAT_A_8: + image->depth = 1; + image->bits_per_pixel = 1; + break; + + /* Other formats are currently not supported. */ + default: + emacs_abort (); + } + + image->format = format; + + if (image->depth == 24) + { + image->bytes_per_line = bitmap_info.stride; + + /* Copy the bitmap data over. */ + memcpy (image->data, data, byte_size); + } + else + { + /* Pack the A8 image data into bits manually. */ + image->bytes_per_line = (image->width + 7) / 8; + + data1 = (unsigned char *) image->data; + data2 = data; + + for (i = 0; i < image->height; ++i) + { + for (x = 0; x < image->width; ++x) + /* Some bits in data1 might be initialized at this point, + but they will all be set properly later. */ + data1[x / 8] = (data2[x] + ? (data1[x / 8] | (1 << (x % 8))) + : (data1[x / 8] & ~(1 << (x % 8)))); + + data1 += image->bytes_per_line; + data2 += bitmap_info.stride; + } + } + + /* Unlock the bitmap pixels. */ + AndroidBitmap_unlockPixels (android_java_env, bitmap); + + /* Delete the bitmap reference. */ + ANDROID_DELETE_LOCAL_REF (bitmap); + return image; +} + +void +android_put_image (android_pixmap handle, struct android_image *image) +{ + jobject drawable, bitmap; + AndroidBitmapInfo bitmap_info; + void *data; + unsigned char *data_1, *data_2; + int i, x; + + drawable = android_resolve_handle (handle, ANDROID_HANDLE_PIXMAP); + + /* Look up the drawable and get the bitmap corresponding to it. + Then, lock the bitmap's bits. */ + bitmap = (*android_java_env)->CallObjectMethod (android_java_env, + drawable, + drawable_class.get_bitmap); + if (!bitmap) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + memset (&bitmap_info, 0, sizeof bitmap_info); + + /* The NDK doc seems to imply this function can fail but doesn't say + what value it gives when it does! */ + AndroidBitmap_getInfo (android_java_env, bitmap, &bitmap_info); + + if (!bitmap_info.stride) + { + ANDROID_DELETE_LOCAL_REF (bitmap); + memory_full (0); + } + + if (bitmap_info.width != image->width + || bitmap_info.height != image->height) + /* This is not yet supported. */ + emacs_abort (); + + /* Make sure the bitmap formats are compatible with each other. */ + + if ((image->depth == 24 + && bitmap_info.format != ANDROID_BITMAP_FORMAT_RGBA_8888) + || (image->depth == 1 + && bitmap_info.format != ANDROID_BITMAP_FORMAT_A_8)) + emacs_abort (); + + /* Lock the image data. Once again, the NDK documentation says the + call can fail, but does not say how to determine whether or not + it has failed, nor how the address is aligned. */ + data = NULL; + AndroidBitmap_lockPixels (android_java_env, bitmap, &data); + + if (!data) + { + /* Take a NULL pointer to mean that AndroidBitmap_lockPixels + failed. */ + ANDROID_DELETE_LOCAL_REF (bitmap); + memory_full (0); + } + + data_1 = data; + data_2 = (unsigned char *) image->data; + + /* Copy the bitmap data over scanline-by-scanline. */ + for (i = 0; i < image->height; ++i) + { + if (image->depth != 1) + memcpy (data_1, data_2, + image->width * (image->bits_per_pixel / 8)); + else + { + /* Android internally uses a 1 byte-per-pixel format for + ALPHA_8 images. Expand the image from the 1 + bit-per-pixel X format correctly. */ + + for (x = 0; x < image->width; ++x) + data_1[x] = (data_2[x / 8] & (1 << x % 8)) ? 0xff : 0; + } + + data_1 += bitmap_info.stride; + data_2 += image->bytes_per_line; + } + + /* Unlock the bitmap pixels. */ + AndroidBitmap_unlockPixels (android_java_env, bitmap); + + /* Delete the bitmap reference. */ + ANDROID_DELETE_LOCAL_REF (bitmap); +} + + + +#undef faccessat + +/* Replace the system faccessat with one which understands AT_EACCESS. + Android's faccessat simply fails upon using AT_EACCESS, so repalce + it with zero here. This isn't caught during configuration. */ + +int +faccessat (int dirfd, const char *pathname, int mode, int flags) +{ + static int (*real_faccessat) (int, const char *, int, int); + + if (!real_faccessat) + real_faccessat = dlsym (RTLD_NEXT, "faccessat"); + + return real_faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); +} + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ @@ -2332,4 +2831,44 @@ android_free_gc (struct android_gc *gc) emacs_abort (); } +struct android_image * +android_create_image (unsigned int depth, enum android_image_format format, + char *data, unsigned int width, unsigned int height) +{ + emacs_abort (); +} + +void +android_destroy_image (struct android_image *ximg) +{ + emacs_abort (); +} + +void +android_put_pixel (struct android_image *ximg, int x, int y, + unsigned long pixel) +{ + emacs_abort (); +} + +unsigned long +android_get_pixel (struct android_image *ximg, int x, int y) +{ + emacs_abort (); +} + +struct android_image * +android_get_image (android_drawable drawable, + enum android_image_format format) +{ + emacs_abort (); +} + +void +android_put_image (android_pixmap pixmap, + struct android_image *image) +{ + emacs_abort (); +} + #endif diff --git a/src/android.h b/src/android.h index 6bdcd38ed68..4d702fe2079 100644 --- a/src/android.h +++ b/src/android.h @@ -28,6 +28,8 @@ along with GNU Emacs. If not, see . */ #include #include #include + +#include "androidgui.h" #endif /* This must be used in every symbol declaration to export it to the @@ -49,6 +51,19 @@ extern int android_fstat (int, struct stat *); extern int android_fstatat (int, const char *restrict, struct stat *restrict, int); extern int android_close (int); +extern const char *android_get_home_directory (void); + +extern double android_pixel_density_x, android_pixel_density_y; + +enum android_handle_type + { + ANDROID_HANDLE_WINDOW, + ANDROID_HANDLE_GCONTEXT, + ANDROID_HANDLE_PIXMAP, + }; + +extern jobject android_resolve_handle (android_handle, + enum android_handle_type); #endif diff --git a/src/androidfns.c b/src/androidfns.c index e9e1ae3d52e..a78b35fc8ff 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -531,9 +531,10 @@ android_default_font_parameter (struct frame *f, Lisp_Object parms) if (! FONTP (font) && ! STRINGP (font)) { const char *names[] = { - /* This will find the normal font. */ - "DroidSansMono", + /* This will find the normal font. The default font size on + Android is 8. */ "monospace", + "DroidSansMono", NULL }; int i; diff --git a/src/androidfont.c b/src/androidfont.c index e312e55c54a..b2c83201b06 100644 --- a/src/androidfont.c +++ b/src/androidfont.c @@ -49,6 +49,7 @@ struct android_emacs_font_driver jmethodID has_char; jmethodID text_extents; jmethodID encode_char; + jmethodID draw; /* Static methods. */ jmethodID create_font_driver; @@ -67,6 +68,7 @@ struct android_emacs_font_spec jfieldID size; jfieldID spacing; jfieldID avgwidth; + jfieldID dpi; }; struct android_emacs_font_metrics @@ -113,6 +115,9 @@ struct androidfont_info /* The Java-side font. */ jobject object; + + /* Cached glyph metrics arranged in a two dimensional array. */ + struct font_metrics **metrics; }; struct androidfont_entity @@ -197,9 +202,11 @@ android_init_font_driver (void) FIND_METHOD (has_char, "hasChar", "(Lorg/gnu/emacs/EmacsFontDriver$Font" "Spec;C)I"); FIND_METHOD (text_extents, "textExtents", "(Lorg/gnu/emacs/EmacsFontDriver" - "$FontObject;[I[Lorg/gnu/emacs/EmacsFontDriver$FontMetrics;)V"); + "$FontObject;[ILorg/gnu/emacs/EmacsFontDriver$FontMetrics;)V"); FIND_METHOD (encode_char, "encodeChar", "(Lorg/gnu/emacs/EmacsFontDriver" "$FontObject;C)I"); + FIND_METHOD (draw, "draw", "(Lorg/gnu/emacs/EmacsFontDriver$FontObject;" + "Lorg/gnu/emacs/EmacsGC;Lorg/gnu/emacs/EmacsDrawable;[IIIIZ)I"); font_driver_class.create_font_driver = (*android_java_env)->GetStaticMethodID (android_java_env, @@ -252,6 +259,7 @@ android_init_font_spec (void) FIND_FIELD (size, "size", "Ljava/lang/Integer;"); FIND_FIELD (spacing, "spacing", "Ljava/lang/Integer;"); FIND_FIELD (avgwidth, "avgwidth", "Ljava/lang/Integer;"); + FIND_FIELD (dpi, "dpi", "Ljava/lang/Integer;"); #undef FIND_FIELD } @@ -449,6 +457,9 @@ androidfont_from_lisp (Lisp_Object font) DO_CARDINAL_FIELD (avgwidth, (FIXNUMP (AREF (font, FONT_AVGWIDTH_INDEX)) ? XFIXNUM (AREF (font, FONT_AVGWIDTH_INDEX)) : -1)); + DO_CARDINAL_FIELD (dpi, (FIXNUMP (AREF (font, FONT_DPI_INDEX)) + ? XFIXNUM (AREF (font, FONT_DPI_INDEX)) + : -1)); #undef DO_CARDINAL_FIELD @@ -507,6 +518,8 @@ androidfont_from_java (jobject spec, Lisp_Object entity) DO_CARDINAL_FIELD (size, FONT_SIZE_INDEX, false); DO_CARDINAL_FIELD (spacing, FONT_SPACING_INDEX, false); DO_CARDINAL_FIELD (avgwidth, FONT_AVGWIDTH_INDEX, false); + DO_CARDINAL_FIELD (dpi, FONT_DPI_INDEX, false); + #undef DO_CARDINAL_FIELD } @@ -613,49 +626,98 @@ static int androidfont_draw (struct glyph_string *s, int from, int to, int x, int y, bool with_background) { - return 0; + struct androidfont_info *info; + jarray chars; + int rc; + jobject gcontext, drawable; + + verify (sizeof (unsigned int) == sizeof (jint)); + info = (struct androidfont_info *) s->font; + + gcontext = android_resolve_handle (s->gc->gcontext, + ANDROID_HANDLE_GCONTEXT); + drawable = android_resolve_handle (FRAME_ANDROID_WINDOW (s->f), + ANDROID_HANDLE_WINDOW); + chars = (*android_java_env)->NewIntArray (android_java_env, + to - from); + + if (!chars) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + (*android_java_env)->SetIntArrayRegion (android_java_env, chars, + 0, to - from, + (jint *) s->char2b + from); + + info = (struct androidfont_info *) s->font; + prepare_face_for_display (s->f, s->face); + + rc = (*android_java_env)->CallIntMethod (android_java_env, + font_driver, + font_driver_class.draw, + info->object, + gcontext, drawable, + chars, (jint) x, (jint) y, + (jint) s->width, + (jboolean) with_background); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (chars); + + return rc; } static Lisp_Object -androidfont_open_font (struct frame *f, Lisp_Object font_entity, int x) +androidfont_open_font (struct frame *f, Lisp_Object font_entity, + int pixel_size) { struct androidfont_info *font_info; struct androidfont_entity *entity; struct font *font; - Lisp_Object font_object, tem; + Lisp_Object font_object; jobject old; jint value; - if (x <= 0) + if (XFIXNUM (AREF (font_entity, FONT_SIZE_INDEX)) != 0) + pixel_size = XFIXNUM (AREF (font_entity, FONT_SIZE_INDEX)); + else if (pixel_size == 0) { - /* Get pixel size from frame instead. */ - tem = get_frame_param (f, Qfontsize); - x = NILP (tem) ? 0 : XFIXNAT (tem); + /* This bit was copied from xfont.c. The values might need + adjustment. */ + + if (FRAME_FONT (f)) + pixel_size = FRAME_FONT (f)->pixel_size; + else + pixel_size = 12; } __android_log_print (ANDROID_LOG_DEBUG, __func__, "opening font entity %"pI"x:%d", - (EMACS_INT) font_entity, x); + (EMACS_INT) font_entity, pixel_size); entity = (struct androidfont_entity *) XFONT_ENTITY (font_entity); block_input (); font_object = font_make_object (VECSIZE (struct androidfont_info), - font_entity, x); + font_entity, pixel_size); ASET (font_object, FONT_TYPE_INDEX, Qandroid); font_info = (struct androidfont_info *) XFONT_OBJECT (font_object); font = &font_info->font; font->driver = &androidfont_driver; - /* Clear font_info->object early in case GC happens later on! */ + /* Clear font_info->object and font_info->metrics early in case GC + happens later on! */ font_info->object = NULL; + font_info->metrics = NULL; unblock_input (); font_info->object = (*android_java_env)->CallObjectMethod (android_java_env, font_driver, font_driver_class.open_font, - entity->object, (jint) x); + entity->object, + (jint) pixel_size); if (!font_info->object) { (*android_java_env)->ExceptionClear (android_java_env); @@ -710,9 +772,19 @@ static void androidfont_close_font (struct font *font) { struct androidfont_info *info; + int i; info = (struct androidfont_info *) font; + /* Free the font metrics cache if it exists. */ + + if (info->metrics) + { + for (i = 0; i < 256; ++i) + xfree (info->metrics[i]); + xfree (info->metrics); + } + /* If info->object is NULL, then FONT was unsuccessfully created, and there is no global reference that has to be deleted. */ @@ -762,18 +834,65 @@ androidfont_encode_char (struct font *font, int c) info->object, (jchar) c); } +static void +androidfont_cache_text_extents (struct androidfont_info *info, + unsigned int glyph, + struct font_metrics *metrics) +{ + int i; + + /* Glyphs larger than 65535 can't be cached. */ + if (glyph >= 256 * 256) + return; + + if (!info->metrics) + info->metrics = xzalloc (256 * sizeof *info->metrics); + + if (!info->metrics[glyph / 256]) + { + info->metrics[glyph / 256] + = xnmalloc (256, sizeof **info->metrics); + + /* Now, all the metrics in that array as invalid by setting + lbearing to SHRT_MAX. */ + for (i = 0; i < 256; ++i) + info->metrics[glyph / 256][i].lbearing = SHRT_MAX; + } + + /* Finally, cache the glyph. */ + info->metrics[glyph / 256][glyph % 256] = *metrics; +} + +static bool +androidfont_check_cached_extents (struct androidfont_info *info, + unsigned int glyph, + struct font_metrics *metrics) +{ + if (info->metrics && info->metrics[glyph / 256] + && info->metrics[glyph / 256][glyph % 256].lbearing != SHRT_MAX) + { + *metrics = info->metrics[glyph / 256][glyph % 256]; + return true; + } + + return false; +} + static void androidfont_text_extents (struct font *font, const unsigned int *code, int nglyphs, struct font_metrics *metrics) { struct androidfont_info *info; - jarray codepoint_array, metrics_array; + jarray codepoint_array; jobject metrics_object; - int i; short value; info = (struct androidfont_info *) font; + if (nglyphs == 1 + && androidfont_check_cached_extents (info, *code, metrics)) + return; + /* Allocate the arrays of code points and font metrics. */ codepoint_array = (*android_java_env)->NewIntArray (android_java_env, @@ -784,92 +903,103 @@ androidfont_text_extents (struct font *font, const unsigned int *code, memory_full (0); } - metrics_array - = (*android_java_env)->NewObjectArray (android_java_env, - nglyphs, - font_metrics_class.class, - NULL); - if (!metrics_array) - { - (*android_java_env)->ExceptionClear (android_java_env); - ANDROID_DELETE_LOCAL_REF (metrics_array); - memory_full (0); - } - - if (sizeof (unsigned int) == sizeof (jint)) - /* Always true on every Android device. */ - (*android_java_env)->SetIntArrayRegion (android_java_env, - codepoint_array, - 0, nglyphs, - (jint *) code); - else - emacs_abort (); - - for (i = 0; i < nglyphs; ++i) - { - metrics_object - = (*android_java_env)->AllocObject (android_java_env, - font_metrics_class.class); + verify (sizeof (unsigned int) == sizeof (jint)); - if (!metrics_object) - { - (*android_java_env)->ExceptionClear (android_java_env); - ANDROID_DELETE_LOCAL_REF (metrics_array); - ANDROID_DELETE_LOCAL_REF (codepoint_array); - memory_full (0); - } + /* Always true on every Android device. */ + (*android_java_env)->SetIntArrayRegion (android_java_env, + codepoint_array, + 0, nglyphs, + (jint *) code); - (*android_java_env)->SetObjectArrayElement (android_java_env, - metrics_array, i, - metrics_object); - ANDROID_DELETE_LOCAL_REF (metrics_object); - } + metrics_object + = (*android_java_env)->AllocObject (android_java_env, + font_metrics_class.class); (*android_java_env)->CallVoidMethod (android_java_env, font_driver, font_driver_class.text_extents, info->object, codepoint_array, - metrics_array); + metrics_object); if ((*android_java_env)->ExceptionCheck (android_java_env)) { (*android_java_env)->ExceptionClear (android_java_env); - ANDROID_DELETE_LOCAL_REF (metrics_array); + ANDROID_DELETE_LOCAL_REF (metrics_object); ANDROID_DELETE_LOCAL_REF (codepoint_array); memory_full (0); } - for (i = 0; i < nglyphs; ++i) - { - metrics_object - = (*android_java_env)->GetObjectArrayElement (android_java_env, - metrics_array, i); #define DO_CARDINAL_FIELD(field) \ value \ = (*android_java_env)->GetShortField (android_java_env, \ metrics_object, \ font_metrics_class.field); \ - metrics[i].field = value; + metrics->field = value; - DO_CARDINAL_FIELD (lbearing); - DO_CARDINAL_FIELD (rbearing); - DO_CARDINAL_FIELD (width); - DO_CARDINAL_FIELD (ascent); - DO_CARDINAL_FIELD (descent); + DO_CARDINAL_FIELD (lbearing); + DO_CARDINAL_FIELD (rbearing); + DO_CARDINAL_FIELD (width); + DO_CARDINAL_FIELD (ascent); + DO_CARDINAL_FIELD (descent); #undef DO_CARDINAL_FIELD - ANDROID_DELETE_LOCAL_REF (metrics_object); - } - - ANDROID_DELETE_LOCAL_REF (metrics_array); + ANDROID_DELETE_LOCAL_REF (metrics_object); ANDROID_DELETE_LOCAL_REF (codepoint_array); + + /* Emacs spends a lot of time in androidfont_text_extents, which + makes calling JNI too slow. Cache the metrics for this single + glyph. */ + + if (nglyphs == 1) + androidfont_cache_text_extents (info, *code, metrics); } static Lisp_Object androidfont_list_family (struct frame *f) { - return Qnil; + Lisp_Object families; + jarray family_array; + jobject string; + jsize i, length; + const char *family; + + family_array + = (*android_java_env)->CallObjectMethod (android_java_env, + font_driver, + font_driver_class.list_families); + if (!family_array) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + length = (*android_java_env)->GetArrayLength (android_java_env, + family_array); + families = Qnil; + + for (i = 0; i < length; ++i) + { + string = (*android_java_env)->GetObjectArrayElement (android_java_env, + family_array, i); + family = (*android_java_env)->GetStringUTFChars (android_java_env, + (jstring) string, NULL); + + if (!family) + { + ANDROID_DELETE_LOCAL_REF (string); + ANDROID_DELETE_LOCAL_REF (family_array); + } + + families = Fcons (build_string_from_utf8 (string), families); + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + (jstring) string, + family); + ANDROID_DELETE_LOCAL_REF (string); + } + + ANDROID_DELETE_LOCAL_REF (family_array); + return Fnreverse (families); } struct font_driver androidfont_driver = diff --git a/src/androidgui.h b/src/androidgui.h index 43ccc86e5c7..b0ea820cfdf 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -201,6 +201,9 @@ enum android_event_type ANDROID_KEY_PRESS, ANDROID_KEY_RELEASE, ANDROID_CONFIGURE_NOTIFY, + ANDROID_FOCUS_IN, + ANDROID_FOCUS_OUT, + ANDROID_WINDOW_ACTION, }; struct android_any_event @@ -209,6 +212,13 @@ struct android_any_event android_window window; }; +enum android_modifier_mask + { + ANDROID_SHIFT_MASK = 193, + ANDROID_CONTROL_MASK = 4096, + ANDROID_ALT_MASK = 2, + }; + struct android_key_event { enum android_event_type type; @@ -216,8 +226,18 @@ struct android_key_event android_time time; unsigned int state; unsigned int keycode; + unsigned int unicode_char; }; +/* These hard coded values are Android modifier keycodes derived + through experimentation. */ + +#define ANDROID_IS_MODIFIER_KEY(key) \ + ((key) == 57 || (key) == 58 || (key) == 113 || (key) == 114 \ + || (key) == 119 || (key) == 117 || (key) == 118 || (key) == 78 \ + || (key) == 94 || (key) == 59 || (key) == 60 || (key) == 95 \ + || (key) == 63) + struct android_configure_event { enum android_event_type type; @@ -227,12 +247,35 @@ struct android_configure_event int width, height; }; +struct android_focus_event +{ + enum android_event_type type; + android_window window; + android_time time; +}; + +struct android_window_action_event +{ + enum android_event_type type; + + /* The window handle. This can be ANDROID_NONE. */ + android_window window; + + /* Numerical identifier for this action. If 0 and WINDOW is set, + then it means the frame associated with that window has been + destroyed. Otherwise, it means Emacs should create a new + frame. */ + unsigned int action; +}; + union android_event { enum android_event_type type; struct android_any_event xany; struct android_key_event xkey; struct android_configure_event xconfigure; + struct android_focus_event xfocus; + struct android_window_action_event xaction; }; extern int android_pending (void); @@ -303,9 +346,47 @@ extern android_pixmap android_create_pixmap (unsigned int, unsigned int, extern void android_set_ts_origin (struct android_gc *, int, int); extern void android_clear_area (android_window, int, int, unsigned int, unsigned int); +extern android_pixmap android_create_bitmap_from_data (char *, unsigned int, + unsigned int); #endif + + +/* Image support. Keep the API as similar to XImage as possible. To + avoid leaving a huge mess of "#ifndef ANDROID_STUBIFY" in image.c, + stubs should be defined for all functions. */ + +enum android_image_format + { + ANDROID_Z_PIXMAP, + }; + +struct android_image +{ + int width, height; + enum android_image_format format; + char *data; + int depth; + int bytes_per_line; + int bits_per_pixel; +}; + +extern struct android_image *android_create_image (unsigned int, + enum android_image_format, + char *, unsigned int, + unsigned int); +extern void android_destroy_image (struct android_image *); + +extern void android_put_pixel (struct android_image *, int, int, + unsigned long); +extern unsigned long android_get_pixel (struct android_image *, int, int); +extern struct android_image *android_get_image (android_drawable, + enum android_image_format); +extern void android_put_image (android_pixmap, struct android_image *); + + + /* X emulation stuff also needed while building stubs. */ extern struct android_gc *android_create_gc (enum android_gc_value_mask, diff --git a/src/androidterm.c b/src/androidterm.c index bb3c2a29737..0279da4f58d 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -39,6 +39,8 @@ struct android_display_info *x_display_list; #ifndef ANDROID_STUBIFY +#include + enum { ANDROID_EVENT_NORMAL, @@ -141,6 +143,124 @@ android_flush_dirty_back_buffer_on (struct frame *f) show_back_buffer (f); } +/* Convert between the modifier bits Android uses and the modifier + bits Emacs uses. */ + +static int +android_android_to_emacs_modifiers (struct android_display_info *dpyinfo, + int state) +{ + return ((state & ANDROID_CONTROL_MASK) ? ctrl_modifier : 0 + | (state & ANDROID_SHIFT_MASK) ? shift_modifier : 0 + | (state & ANDROID_ALT_MASK) ? meta_modifier : 0); +} + +static int +android_emacs_to_android_modifiers (struct android_display_info *dpyinfo, + intmax_t state) +{ + return ((state & ctrl_modifier) ? ANDROID_CONTROL_MASK : 0 + | (state & shift_modifier) ? ANDROID_SHIFT_MASK : 0 + | (state & meta_modifier) ? ANDROID_ALT_MASK : 0); +} + +static void android_frame_rehighlight (struct android_display_info *); + +static void +android_lower_frame (struct frame *f) +{ + /* TODO. */ +} + +static void +android_raise_frame (struct frame *f) +{ + /* TODO. */ +} + +static void +android_new_focus_frame (struct android_display_info *dpyinfo, + struct frame *frame) +{ + struct frame *old_focus; + + old_focus = dpyinfo->focus_frame; + + if (frame != dpyinfo->focus_frame) + { + /* Set this before calling other routines, so that they see + the correct value of x_focus_frame. */ + dpyinfo->focus_frame = frame; + + if (old_focus && old_focus->auto_lower) + android_lower_frame (old_focus); + + if (dpyinfo->focus_frame && dpyinfo->focus_frame->auto_raise) + dpyinfo->pending_autoraise_frame = dpyinfo->focus_frame; + else + dpyinfo->pending_autoraise_frame = NULL; + } + + android_frame_rehighlight (dpyinfo); +} + +static void +android_focus_changed (int type, int state, + struct android_display_info *dpyinfo, + struct frame *frame, struct input_event *bufp) +{ + if (type == ANDROID_FOCUS_IN) + { + if (dpyinfo->x_focus_event_frame != frame) + { + android_new_focus_frame (dpyinfo, frame); + dpyinfo->x_focus_event_frame = frame; + bufp->kind = FOCUS_IN_EVENT; + XSETFRAME (bufp->frame_or_window, frame); + } + + frame->output_data.android->focus_state |= state; + } + else if (type == ANDROID_FOCUS_OUT) + { + frame->output_data.android->focus_state &= ~state; + + if (dpyinfo->x_focus_event_frame == frame) + { + dpyinfo->x_focus_event_frame = 0; + android_new_focus_frame (dpyinfo, 0); + + bufp->kind = FOCUS_OUT_EVENT; + XSETFRAME (bufp->frame_or_window, frame); + } + + if (frame->pointer_invisible) + android_toggle_invisible_pointer (frame, false); + } +} + +static void +android_detect_focus_change (struct android_display_info *dpyinfo, + struct frame *frame, + union android_event *event, + struct input_event *bufp) +{ + if (!frame) + return; + + switch (event->type) + { + case ANDROID_FOCUS_IN: + case ANDROID_FOCUS_OUT: + android_focus_changed (event->type, FOCUS_EXPLICIT, + dpyinfo, frame, bufp); + break; + + default: + break; + } +} + static int handle_one_android_event (struct android_display_info *dpyinfo, union android_event *event, int *finish, @@ -149,11 +269,20 @@ handle_one_android_event (struct android_display_info *dpyinfo, union android_event configureEvent; struct frame *f, *any, *mouse_frame; Mouse_HLInfo *hlinfo; + union buffered_input_event inev; + int modifiers, count; + + /* It is okay for this to not resemble handle_one_xevent so much. + Differences in event handling code are much less nasty than + stuble differences in the graphics code. */ + count = 0; hlinfo = &dpyinfo->mouse_highlight; *finish = ANDROID_EVENT_NORMAL; any = android_window_to_frame (dpyinfo, event->xany.window); + EVENT_INIT (inev.ie); + switch (event->type) { case ANDROID_CONFIGURE_NOTIFY: @@ -192,12 +321,15 @@ handle_one_android_event (struct android_display_info *dpyinfo, case ANDROID_KEY_PRESS: + /* Set f to any. There are no ``outer windows'' on Android. */ + f = any; + /* If mouse-highlight is an integer, input clears out mouse highlighting. */ if (!hlinfo->mouse_face_hidden && FIXNUMP (Vmouse_highlight) && (any == 0 - || !EQ (f->tool_bar_window, hlinfo->mouse_face_window) - || !EQ (f->tab_bar_window, hlinfo->mouse_face_window))) + || !EQ (any->tool_bar_window, hlinfo->mouse_face_window) + || !EQ (any->tab_bar_window, hlinfo->mouse_face_window))) { mouse_frame = hlinfo->mouse_face_mouse_frame; @@ -208,13 +340,90 @@ handle_one_android_event (struct android_display_info *dpyinfo, android_flush_dirty_back_buffer_on (mouse_frame); } + event->xkey.state + |= android_emacs_to_android_modifiers (dpyinfo, + extra_keyboard_modifiers); + modifiers = event->xkey.state; + + /* Common for all keysym input events. */ + XSETFRAME (inev.ie.frame_or_window, any); + inev.ie.modifiers + = android_android_to_emacs_modifiers (dpyinfo, modifiers); + inev.ie.timestamp = event->xkey.time; + + /* First deal with keysyms which have defined translations to + characters. */ + + if (event->xkey.unicode_char >= 32 + && event->xkey.unicode_char < 128) + { + inev.ie.kind = ASCII_KEYSTROKE_EVENT; + inev.ie.code = event->xkey.unicode_char; + } + else if (event->xkey.unicode_char < 32) + { + /* If the key is a modifier key, just return. */ + if (ANDROID_IS_MODIFIER_KEY (event->xkey.keycode)) + goto done_keysym; + + /* Next, deal with special ``characters'' by giving the + keycode to keyboard.c. */ + inev.ie.kind = NON_ASCII_KEYSTROKE_EVENT; + inev.ie.code = event->xkey.keycode; + } + else + { + /* Finally, deal with Unicode characters. */ + inev.ie.kind = MULTIBYTE_CHAR_KEYSTROKE_EVENT; + inev.ie.code = event->xkey.unicode_char; + } + + goto done_keysym; + + done_keysym: + goto OTHER; + + case ANDROID_FOCUS_IN: + case ANDROID_FOCUS_OUT: + android_detect_focus_change (dpyinfo, any, event, &inev.ie); + goto OTHER; + + case ANDROID_WINDOW_ACTION: + + f = any; + + if (event->xaction.action == 0) + { + /* Action 0 either means to destroy a frame or to create a + new frame, depending on whether or not + event->xaction.window exists. */ + + if (event->xaction.window) + { + if (!f) + goto OTHER; + + inev.ie.kind = DELETE_WINDOW_EVENT; + XSETFRAME (inev.ie.frame_or_window, f); + } + else + /* A new frame must be created. */; + } + + goto OTHER; + default: goto OTHER; } OTHER: + if (inev.ie.kind != NO_EVENT) + { + kbd_buffer_store_buffered_event (&inev, hold_quit); + count++; + } - return 0; + return count; } static int @@ -378,15 +587,52 @@ android_focus_frame (struct frame *f, bool noactivate) } static void -android_frame_rehighlight (struct frame *f) +android_frame_rehighlight (struct android_display_info *dpyinfo) { - /* TODO */ + struct frame *old_highlight; + + old_highlight = dpyinfo->highlight_frame; + + if (dpyinfo->focus_frame) + { + dpyinfo->highlight_frame + = ((FRAMEP (FRAME_FOCUS_FRAME (dpyinfo->focus_frame))) + ? XFRAME (FRAME_FOCUS_FRAME (dpyinfo->focus_frame)) + : dpyinfo->focus_frame); + if (!FRAME_LIVE_P (dpyinfo->highlight_frame)) + { + fset_focus_frame (dpyinfo->focus_frame, Qnil); + dpyinfo->highlight_frame = dpyinfo->focus_frame; + } + } + else + dpyinfo->highlight_frame = 0; + + if (dpyinfo->highlight_frame != old_highlight) + { + /* This is not yet required on Android. */ +#if 0 + if (old_highlight) + android_frame_unhighlight (old_highlight); + if (dpyinfo->highlight_frame) + android_frame_highlight (dpyinfo->highlight_frame); +#endif + } +} + +static void +android_frame_rehighlight_hook (struct frame *f) +{ + android_frame_rehighlight (FRAME_DISPLAY_INFO (f)); } static void android_frame_raise_lower (struct frame *f, bool raise_flag) { - /* TODO */ + if (raise_flag) + android_raise_frame (f); + else + android_lower_frame (f); } void @@ -401,6 +647,10 @@ android_make_frame_visible (struct frame *f) void android_make_frame_invisible (struct frame *f) { + /* Don't keep the highlight on an invisible frame. */ + if (FRAME_DISPLAY_INFO (f)->highlight_frame == f) + FRAME_DISPLAY_INFO (f)->highlight_frame = 0; + android_unmap_window (FRAME_ANDROID_WINDOW (f)); SET_FRAME_VISIBLE (f, false); @@ -584,6 +834,8 @@ android_free_frame_resources (struct frame *f) if (f == dpyinfo->focus_frame) dpyinfo->focus_frame = 0; + if (f == dpyinfo->x_focus_event_frame) + dpyinfo->x_focus_event_frame = 0; if (f == dpyinfo->highlight_frame) dpyinfo->highlight_frame = 0; if (f == hlinfo->mouse_face_mouse_frame) @@ -783,7 +1035,7 @@ android_draw_fringe_bitmap (struct window *w, struct glyph_row *row, if (p->overlay_p) { clipmask = android_create_pixmap_from_bitmap_data (bits, p->wd, p->h, - 1, 0, 0); + 1, 0, 1); gcv.clip_mask = clipmask; gcv.clip_x_origin = p->x; @@ -1606,158 +1858,47 @@ android_draw_image_foreground (struct glyph_string *s) if (s->img->pixmap) { - if (s->img->mask) - { - unsigned long mask = (ANDROID_GC_CLIP_MASK - | ANDROID_GC_CLIP_X_ORIGIN - | ANDROID_GC_CLIP_Y_ORIGIN - | ANDROID_GC_FUNCTION); - struct android_gc_values xgcv; - struct android_rectangle clip_rect, image_rect, r; - - xgcv.clip_mask = s->img->mask; - xgcv.clip_x_origin = x - s->slice.x; - xgcv.clip_y_origin = y - s->slice.y; - xgcv.function = ANDROID_GC_COPY; - android_change_gc (s->gc, mask, &xgcv); - - get_glyph_string_clip_rect (s, &clip_rect); - image_rect.x = x; - image_rect.y = y; - image_rect.width = s->slice.width; - image_rect.height = s->slice.height; - - if (gui_intersect_rectangles (&clip_rect, &image_rect, &r)) - android_copy_area (s->img->pixmap, FRAME_ANDROID_WINDOW (s->f), - s->gc, s->slice.x + r.x - x, - s->slice.y + r.y - y, - r.x, r.y, r.width, r.height); - } - else + unsigned long mask = (ANDROID_GC_CLIP_MASK + | ANDROID_GC_CLIP_X_ORIGIN + | ANDROID_GC_CLIP_Y_ORIGIN + | ANDROID_GC_FUNCTION); + struct android_gc_values xgcv; + struct android_rectangle clip_rect, image_rect, r; + + xgcv.clip_mask = s->img->mask; + xgcv.clip_x_origin = x - s->slice.x; + xgcv.clip_y_origin = y - s->slice.y; + xgcv.function = ANDROID_GC_COPY; + android_change_gc (s->gc, mask, &xgcv); + + get_glyph_string_clip_rect (s, &clip_rect); + image_rect.x = x; + image_rect.y = y; + image_rect.width = s->slice.width; + image_rect.height = s->slice.height; + + if (gui_intersect_rectangles (&clip_rect, &image_rect, &r)) + android_copy_area (s->img->pixmap, + FRAME_ANDROID_WINDOW (s->f), + s->gc, s->slice.x + r.x - x, + s->slice.y + r.y - y, + r.width, r.height, r.x, r.y); + + /* When the image has a mask, we can expect that at least part + of a mouse highlight or a block cursor will be visible. If + the image doesn't have a mask, make a block cursor visible by + drawing a rectangle around the image. I believe it's looking + better if we do nothing here for mouse-face. */ + if (s->hl == DRAW_CURSOR && !s->img->mask) { - unsigned long mask = (ANDROID_GC_CLIP_MASK - | ANDROID_GC_CLIP_X_ORIGIN - | ANDROID_GC_CLIP_Y_ORIGIN - | ANDROID_GC_FUNCTION); - struct android_gc_values xgcv; - struct android_rectangle clip_rect, image_rect, r; - - xgcv.clip_mask = s->img->mask; - xgcv.clip_x_origin = x - s->slice.x; - xgcv.clip_y_origin = y - s->slice.y; - xgcv.function = ANDROID_GC_COPY; - android_change_gc (s->gc, mask, &xgcv); - - get_glyph_string_clip_rect (s, &clip_rect); - image_rect.x = x; - image_rect.y = y; - image_rect.width = s->slice.width; - image_rect.height = s->slice.height; - - if (gui_intersect_rectangles (&clip_rect, &image_rect, &r)) - android_copy_area (s->img->pixmap, - FRAME_ANDROID_WINDOW (s->f), - s->gc, s->slice.x + r.x - x, - s->slice.y + r.y - y, - r.x, r.y, r.width, r.height); - - /* When the image has a mask, we can expect that at - least part of a mouse highlight or a block cursor will - be visible. If the image doesn't have a mask, make - a block cursor visible by drawing a rectangle around - the image. I believe it's looking better if we do - nothing here for mouse-face. */ - if (s->hl == DRAW_CURSOR) - { - int relief = eabs (s->img->relief); - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, - x - relief, y - relief, - s->slice.width + relief*2 - 1, - s->slice.height + relief*2 - 1); - } + int relief = eabs (s->img->relief); + android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + x - relief, y - relief, + s->slice.width + relief*2 - 1, + s->slice.height + relief*2 - 1); } - } - else - /* Draw a rectangle if image could not be loaded. */ - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, x, y, - s->slice.width - 1, s->slice.height - 1); -} - -/* Draw the foreground of image glyph string S to PIXMAP. */ - -static void -android_draw_image_foreground_1 (struct glyph_string *s, - android_pixmap pixmap) -{ - int x = 0; - int y = s->ybase - s->y - image_ascent (s->img, s->face, &s->slice); - - /* If first glyph of S has a left box line, start drawing it to the - right of that line. */ - if (s->face->box != FACE_NO_BOX - && s->first_glyph->left_box_line_p - && s->slice.x == 0) - x += max (s->face->box_vertical_line_width, 0); - - /* If there is a margin around the image, adjust x- and y-position - by that margin. */ - if (s->slice.x == 0) - x += s->img->hmargin; - if (s->slice.y == 0) - y += s->img->vmargin; - if (s->img->pixmap) - { - if (s->img->mask) - { - unsigned long mask = (ANDROID_GC_CLIP_MASK - | ANDROID_GC_CLIP_X_ORIGIN - | ANDROID_GC_CLIP_Y_ORIGIN - | ANDROID_GC_FUNCTION); - struct android_gc_values xgcv; - struct android_rectangle clip_rect, image_rect, r; - - xgcv.clip_mask = s->img->mask; - xgcv.clip_x_origin = x - s->slice.x; - xgcv.clip_y_origin = y - s->slice.y; - xgcv.function = ANDROID_GC_COPY; - android_change_gc (s->gc, mask, &xgcv); - - get_glyph_string_clip_rect (s, &clip_rect); - image_rect.x = x; - image_rect.y = y; - image_rect.width = s->slice.width; - image_rect.height = s->slice.height; - - if (gui_intersect_rectangles (&clip_rect, &image_rect, &r)) - android_copy_area (s->img->pixmap, pixmap, s->gc, - s->slice.x + r.x - x, - s->slice.y + r.y - y, - r.x, r.y, r.width, r.height); - - android_set_clip_mask (s->gc, ANDROID_NONE); - } - else - { - android_copy_area (s->img->pixmap, pixmap, s->gc, - s->slice.x, s->slice.y, - s->slice.width, s->slice.height, x, y); - - /* When the image has a mask, we can expect that at - least part of a mouse highlight or a block cursor will - be visible. If the image doesn't have a mask, make - a block cursor visible by drawing a rectangle around - the image. I believe it's looking better if we do - nothing here for mouse-face. */ - if (s->hl == DRAW_CURSOR) - { - int r = eabs (s->img->relief); - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), - s->gc, x - r, y - r, - s->slice.width + r*2 - 1, - s->slice.height + r*2 - 1); - } - } + android_set_clip_mask (s->gc, ANDROID_NONE); } else /* Draw a rectangle if image could not be loaded. */ @@ -1771,7 +1912,6 @@ android_draw_image_glyph_string (struct glyph_string *s) int box_line_hwidth = max (s->face->box_vertical_line_width, 0); int box_line_vwidth = max (s->face->box_horizontal_line_width, 0); int height; - android_pixmap pixmap = ANDROID_NONE; height = s->height; if (s->slice.y == 0) @@ -1793,79 +1933,27 @@ android_draw_image_glyph_string (struct glyph_string *s) if (s->stippled_p) s->row->stipple_p = true; - if (s->img->mask) - { - /* Create a pixmap as large as the glyph string. Fill it - with the background color. Copy the image to it, using - its mask. Copy the temporary pixmap to the display. */ - int depth = FRAME_DISPLAY_INFO (s->f)->n_planes; - - /* Create a pixmap as large as the glyph string. */ - pixmap = android_create_pixmap (s->background_width, - s->height, depth); - - /* Don't clip in the following because we're working on the - pixmap. */ - android_set_clip_mask (s->gc, ANDROID_NONE); - - /* Fill the pixmap with the background color/stipple. */ - if (s->stippled_p) - { - /* Fill background with a stipple pattern. */ - android_set_fill_style (s->gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_set_ts_origin (s->gc, - s->x, - s->y); - android_fill_rectangle (pixmap, s->gc, - 0, 0, s->background_width, s->height); - android_set_fill_style (s->gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_set_ts_origin (s->gc, 0, 0); - } - else - { - struct android_gc_values xgcv; + int x = s->x; + int y = s->y; + int width = s->background_width; - android_get_gc_values (s->gc, (ANDROID_GC_FOREGROUND - | ANDROID_GC_BACKGROUND), - &xgcv); - android_set_foreground (s->gc, xgcv.background); - android_fill_rectangle (pixmap, s->gc, - 0, 0, s->background_width, s->height); - android_set_background (s->gc, xgcv.foreground); - } - } - else + if (s->first_glyph->left_box_line_p + && s->slice.x == 0) { - int x = s->x; - int y = s->y; - int width = s->background_width; + x += box_line_hwidth; + width -= box_line_hwidth; + } - if (s->first_glyph->left_box_line_p - && s->slice.x == 0) - { - x += box_line_hwidth; - width -= box_line_hwidth; - } + if (s->slice.y == 0) + y += box_line_vwidth; - if (s->slice.y == 0) - y += box_line_vwidth; - - android_draw_glyph_string_bg_rect (s, x, y, width, height); - } + android_draw_glyph_string_bg_rect (s, x, y, width, height); s->background_filled_p = true; } /* Draw the foreground. */ - if (pixmap != ANDROID_NONE) - { - android_draw_image_foreground_1 (s, pixmap); - android_set_glyph_string_clipping (s); - android_copy_area (pixmap, FRAME_ANDROID_WINDOW (s->f), s->gc, - 0, 0, s->background_width, s->height, s->x, - s->y); - android_free_pixmap (pixmap); - } - else - android_draw_image_foreground (s); + android_draw_image_foreground (s); /* If we must draw a relief around the image, do it. */ if (s->img->relief @@ -3047,7 +3135,7 @@ android_create_terminal (struct android_display_info *dpyinfo) terminal->mouse_position_hook = android_mouse_position; terminal->get_focus_frame = android_get_focus_frame; terminal->focus_frame_hook = android_focus_frame; - terminal->frame_rehighlight_hook = android_frame_rehighlight; + terminal->frame_rehighlight_hook = android_frame_rehighlight_hook; terminal->frame_raise_lower_hook = android_frame_raise_lower; terminal->frame_visible_invisible_hook = android_make_frame_visible_invisible; @@ -3117,9 +3205,12 @@ android_term_init (void) dpyinfo->color_map = color_map; - /* TODO! */ - dpyinfo->resx = 96.0; - dpyinfo->resy = 96.0; +#ifndef ANDROID_STUBIFY + + dpyinfo->resx = android_pixel_density_x; + dpyinfo->resy = android_pixel_density_y; + +#endif /* https://lists.gnu.org/r/emacs-devel/2015-11/msg00194.html */ dpyinfo->smallest_font_height = 1; @@ -3130,6 +3221,9 @@ android_term_init (void) /* The display "connection" is now set up, and it must never go away. */ terminal->reference_count = 30000; + + /* Set the baud rate to the same value it gets set to on X. */ + baud_rate = 19200; } diff --git a/src/androidterm.h b/src/androidterm.h index 3a0c9f60555..c834ffb70e5 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -28,8 +28,8 @@ along with GNU Emacs. If not, see . */ struct android_bitmap_record { - /* The image backing the bitmap. */ - Emacs_Pixmap img; + /* The image backing the bitmap and its mask. */ + android_pixmap pixmap, mask; /* The file from which it comes. */ char *file; @@ -37,8 +37,11 @@ struct android_bitmap_record /* The number of references to it. */ int refcount; - /* The height and width. */ - int height, width; + /* The height and width and the depth. */ + int height, width, depth; + + /* Whether or not there is a mask. */ + bool have_mask; }; struct android_display_info @@ -95,6 +98,9 @@ struct android_display_info /* The frame currently with the input focus. */ struct frame *focus_frame; + /* The last frame mentioned in a focus event. */ + struct frame *x_focus_event_frame; + /* The frame which currently has the visual highlight, and should get keyboard input. It points to the focus frame's selected window's frame, but can differ. */ @@ -206,8 +212,24 @@ struct android_output /* The background for which the above relief GCs were set up. They are changed only when a different background is involved. */ unsigned long relief_background; + + /* Focus state. Only present for consistency with X; it is actually + a boolean. */ + int focus_state; }; +enum + { + /* Values for focus_state, used as bit mask. EXPLICIT means we + received a FocusIn for the frame and know it has the focus. + IMPLICIT means we received an EnterNotify and the frame may + have the focus if no window manager is running. FocusOut and + LeaveNotify clears EXPLICIT/IMPLICIT. */ + FOCUS_NONE = 0, + FOCUS_IMPLICIT = 1, + FOCUS_EXPLICIT = 2 + }; + /* Return the Android output data for frame F. */ #define FRAME_ANDROID_OUTPUT(f) ((f)->output_data.android) #define FRAME_OUTPUT_DATA(f) ((f)->output_data.android) diff --git a/src/dispextern.h b/src/dispextern.h index e521dffc37c..c9b3495934b 100644 --- a/src/dispextern.h +++ b/src/dispextern.h @@ -160,8 +160,8 @@ typedef Emacs_Pixmap Emacs_Pix_Context; #ifdef HAVE_ANDROID #include "androidgui.h" typedef struct android_display_info Display_Info; -typedef Emacs_Pixmap Emacs_Pix_Container; -typedef Emacs_Pixmap Emacs_Pix_Context; +typedef struct android_image *Emacs_Pix_Container; +typedef struct android_image *Emacs_Pix_Context; #endif #ifdef HAVE_WINDOW_SYSTEM @@ -3119,6 +3119,13 @@ struct image int original_width, original_height; # endif #endif /* HAVE_X_WINDOWS */ +#ifdef HAVE_ANDROID + /* Android images of the image, corresponding to the above Pixmaps. + Non-NULL means it and its Pixmap counterpart may be out of sync + and the latter is outdated. NULL means the X image has been + synchronized to Pixmap. */ + struct android_image *ximg, *mask_img; +#endif /* HAVE_ANDROID */ #ifdef HAVE_NTGUI XFORM xform; #endif diff --git a/src/emacs.c b/src/emacs.c index 9ef58ca412e..d3c53dd8051 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -1440,9 +1440,8 @@ main (int argc, char **argv) bool only_version = false; sort_args (argc, argv); old_argc = argc, argc = 0; - while (argv[argc] - /* Don't allow going past argv. */ - && argc < old_argc) argc++; + /* Don't allow going past argv. */ + while (argc < old_argc && argv[argc]) argc++; skip_args = 0; if (argmatch (argv, argc, "-version", "--version", 3, NULL, &skip_args)) diff --git a/src/fileio.c b/src/fileio.c index 1d22c51ebaa..0f205283d7e 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -898,6 +898,10 @@ user_homedir (char const *name) p[length] = 0; struct passwd *pw = getpwnam (p); SAFE_FREE (); +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + if (pw && !pw->pw_dir && pw->pw_uid == getuid ()) + return (char *) android_get_home_directory (); +#endif if (!pw || (pw->pw_dir && !IS_ABSOLUTE_FILE_NAME (pw->pw_dir))) return NULL; return pw->pw_dir; @@ -1888,6 +1892,11 @@ get_homedir (void) pw = getpwuid (getuid ()); if (pw) home = pw->pw_dir; + +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + if (!home && pw && pw->pw_uid == getuid ()) + return android_get_home_directory (); +#endif if (!home) return ""; } diff --git a/src/font.h b/src/font.h index c1ab26b87cb..1db94a9f3f8 100644 --- a/src/font.h +++ b/src/font.h @@ -547,8 +547,14 @@ CHECK_FONT_GET_OBJECT (Lisp_Object x) return XFONT_OBJECT (x); } +#ifndef HAVE_ANDROID /* Number of pt per inch (from the TeXbook). */ #define PT_PER_INCH 72.27 +#else +/* Android uses this value instead to compensate for different device + dimensions. */ +#define PT_PER_INCH 160.00 +#endif /* Return a pixel size (integer) corresponding to POINT size (double) on resolution DPI. */ diff --git a/src/fringe.c b/src/fringe.c index 5d7c8dca998..f10bc424239 100644 --- a/src/fringe.c +++ b/src/fringe.c @@ -1422,25 +1422,30 @@ If BITMAP overrides a standard fringe bitmap, the original bitmap is restored. On X, we bit-swap the built-in bitmaps and reduce bitmap from short to char array if width is <= 8 bits. + The Android port tries to follow X as closely as possible, so do + that there too. + On MAC with big-endian CPU, we need to byte-swap each short. On W32 and MAC (little endian), there's no need to do this. */ -#if defined (HAVE_X_WINDOWS) || defined (HAVE_PGTK) -static const unsigned char swap_nibble[16] = { - 0x0, 0x8, 0x4, 0xc, /* 0000 1000 0100 1100 */ - 0x2, 0xa, 0x6, 0xe, /* 0010 1010 0110 1110 */ - 0x1, 0x9, 0x5, 0xd, /* 0001 1001 0101 1101 */ - 0x3, 0xb, 0x7, 0xf}; /* 0011 1011 0111 1111 */ -#endif /* HAVE_X_WINDOWS */ +#if defined (HAVE_X_WINDOWS) || defined (HAVE_PGTK) || defined (HAVE_ANDROID) +static const unsigned char swap_nibble[16] = + { + 0x0, 0x8, 0x4, 0xc, /* 0000 1000 0100 1100 */ + 0x2, 0xa, 0x6, 0xe, /* 0010 1010 0110 1110 */ + 0x1, 0x9, 0x5, 0xd, /* 0001 1001 0101 1101 */ + 0x3, 0xb, 0x7, 0xf, /* 0011 1011 0111 1111 */ + }; +#endif static void init_fringe_bitmap (int which, struct fringe_bitmap *fb, int once_p) { if (once_p || fb->dynamic) { -#if defined (HAVE_X_WINDOWS) +#if defined (HAVE_X_WINDOWS) || defined (HAVE_ANDROID) unsigned short *bits = fb->bits; int j; @@ -1488,7 +1493,7 @@ init_fringe_bitmap (int which, struct fringe_bitmap *fb, int once_p) } } #endif /* not USE_CAIRO */ -#endif /* HAVE_X_WINDOWS */ +#endif /* HAVE_X_WINDOWS || HAVE_ANDROID */ #if !defined(HAVE_X_WINDOWS) && defined (HAVE_PGTK) unsigned short *bits = fb->bits; diff --git a/src/image.c b/src/image.c index 986dd7aada4..08d092e2c6e 100644 --- a/src/image.c +++ b/src/image.c @@ -177,11 +177,14 @@ typedef struct haiku_bitmap_record Bitmap_Record; #ifdef HAVE_ANDROID #include "androidterm.h" + typedef struct android_bitmap_record Bitmap_Record; -/* TODO: implement images on Android. */ -#define GET_PIXEL(ximg, x, y) 0 -#define PUT_PIXEL(ximg, x, y, pixel) ((void) (pixel)) +typedef struct android_image XImage; +typedef android_pixmap Pixmap; + +#define GET_PIXEL(ximg, x, y) android_get_pixel (ximg, x, y) +#define PUT_PIXEL(ximg, x, y, pixel) android_put_pixel (ximg, x, y, pixel) #define NO_PIXMAP 0 #define PIX_MASK_RETAIN 0 @@ -507,6 +510,18 @@ image_create_bitmap_from_data (struct frame *f, char *bits, return -1; #endif /* HAVE_X_WINDOWS */ +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + android_pixmap bitmap; + + bitmap = android_create_bitmap_from_data (bits, width, height); + + if (!bitmap) + return -1; +#else + ((void) dpyinfo); + emacs_abort (); +#endif + #ifdef HAVE_NTGUI Lisp_Object frame UNINIT; /* The value is not used. */ Emacs_Pixmap bitmap; @@ -619,14 +634,14 @@ image_create_bitmap_from_data (struct frame *f, char *bits, dpyinfo->bitmaps[id - 1].width = width; dpyinfo->bitmaps[id - 1].refcount = 1; -#ifdef HAVE_X_WINDOWS +#if defined HAVE_X_WINDOWS && defined HAVE_ANDROID dpyinfo->bitmaps[id - 1].pixmap = bitmap; dpyinfo->bitmaps[id - 1].have_mask = false; dpyinfo->bitmaps[id - 1].depth = 1; #ifdef USE_CAIRO dpyinfo->bitmaps[id - 1].stipple = NULL; #endif /* USE_CAIRO */ -#endif /* HAVE_X_WINDOWS */ +#endif /* HAVE_X_WINDOWS || HAVE_ANDROID */ #ifdef HAVE_NTGUI dpyinfo->bitmaps[id - 1].pixmap = bitmap; @@ -637,7 +652,7 @@ image_create_bitmap_from_data (struct frame *f, char *bits, return id; } -#if defined HAVE_HAIKU || defined HAVE_NS +#if defined HAVE_HAIKU || defined HAVE_NS || defined HAVE_ANDROID static char *slurp_file (int, ptrdiff_t *); static Lisp_Object image_find_image_fd (Lisp_Object, int *); static bool xbm_read_bitmap_data (struct frame *, char *, char *, @@ -861,8 +876,62 @@ image_create_bitmap_from_file (struct frame *f, Lisp_Object file) /* This function should never be called when building stubs. */ emacs_abort (); #else - /* you lose, not yet implemented TODO */ - return 0; + ptrdiff_t id, size; + int fd, width, height, rc; + char *contents, *data; + Lisp_Object found; + android_pixmap bitmap; + + /* Look for an existing bitmap with the same name. */ + for (id = 0; id < dpyinfo->bitmaps_last; ++id) + { + if (dpyinfo->bitmaps[id].refcount + && dpyinfo->bitmaps[id].file + && !strcmp (dpyinfo->bitmaps[id].file, SSDATA (file))) + { + ++dpyinfo->bitmaps[id].refcount; + return id + 1; + } + } + + /* Search bitmap-file-path for the file, if appropriate. */ + if (openp (Vx_bitmap_file_path, file, Qnil, &found, + make_fixnum (R_OK), false, false) + < 0) + return -1; + + if (!STRINGP (image_find_image_fd (file, &fd)) + && !STRINGP (image_find_image_fd (found, &fd))) + return -1; + + contents = slurp_file (fd, &size); + + if (!contents) + return -1; + + rc = xbm_read_bitmap_data (f, contents, contents + size, + &width, &height, &data, 0); + + if (!rc) + { + xfree (contents); + return -1; + } + + xfree (contents); + bitmap = android_create_bitmap_from_data (data, width, height); + xfree (data); + + id = image_allocate_bitmap_record (f); + dpyinfo->bitmaps[id - 1].pixmap = bitmap; + dpyinfo->bitmaps[id - 1].have_mask = false; + dpyinfo->bitmaps[id - 1].refcount = 1; + dpyinfo->bitmaps[id - 1].file = xlispstrdup (file); + dpyinfo->bitmaps[id - 1].depth = 1; + dpyinfo->bitmaps[id - 1].height = height; + dpyinfo->bitmaps[id - 1].width = width; + + return id; #endif #endif } @@ -882,6 +951,13 @@ free_bitmap_record (Display_Info *dpyinfo, Bitmap_Record *bm) #endif /* USE_CAIRO */ #endif /* HAVE_X_WINDOWS */ +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + android_free_pixmap (bm->pixmap); + + if (bm->have_mask) + android_free_pixmap (bm->pixmap); +#endif + #ifdef HAVE_NTGUI DeleteObject (bm->pixmap); #endif /* HAVE_NTGUI */ @@ -969,7 +1045,7 @@ static void image_unget_x_image (struct image *, bool, Emacs_Pix_Container); image_unget_x_image (img, mask_p, ximg) #endif -#ifdef HAVE_X_WINDOWS +#if defined HAVE_X_WINDOWS || defined HAVE_ANDROID #ifndef USE_CAIRO static void image_sync_to_pixmaps (struct frame *, struct image *); @@ -983,6 +1059,8 @@ static bool x_create_x_image_and_pixmap (struct frame *, int, int, int, XImage **, Pixmap *); static void x_destroy_x_image (XImage *); +#if defined HAVE_X_WINDOWS + /* Create a mask of a bitmap. Note is this not a perfect mask. It's nicer with some borders in this context */ @@ -1079,7 +1157,9 @@ x_create_bitmap_mask (struct frame *f, ptrdiff_t id) x_destroy_x_image (mask_img); } -#endif /* HAVE_X_WINDOWS */ +#endif + +#endif /* HAVE_X_WINDOWS || defined HAVE_ANDROID*/ /*********************************************************************** Image types @@ -1113,7 +1193,7 @@ struct image_type #if defined HAVE_RSVG || defined HAVE_PNG || defined HAVE_GIF || \ defined HAVE_TIFF || defined HAVE_JPEG || defined HAVE_XPM || \ defined HAVE_NS || defined HAVE_HAIKU || defined HAVE_PGTK || \ - defined HAVE_WEBP + defined HAVE_WEBP || defined HAVE_ANDROID # ifdef WINDOWSNT # define IMAGE_TYPE_INIT(f) f # else @@ -1615,7 +1695,7 @@ prepare_image_for_display (struct frame *f, struct image *img) } unblock_input (); } -#elif defined HAVE_X_WINDOWS +#elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID if (!img->load_failed_p) { block_input (); @@ -1822,7 +1902,7 @@ image_clear_image_1 (struct frame *f, struct image *img, int flags) /* NOTE (HAVE_NS): background color is NOT an indexed color! */ img->background_valid = 0; } -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO if (img->ximg) { image_destroy_x_image (img->ximg); @@ -1840,7 +1920,7 @@ image_clear_image_1 (struct frame *f, struct image *img, int flags) img->mask = NO_PIXMAP; img->background_transparent_valid = 0; } -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO if (img->mask_img) { image_destroy_x_image (img->mask_img); @@ -2212,11 +2292,11 @@ image_size_in_bytes (struct image *img) if (msk) size += msk->height * msk->bytes_per_line; -#elif defined HAVE_X_WINDOWS - /* Use a nominal depth of 24 bpp for pixmap and 1 bpp for mask, - to avoid having to query the server. */ +#elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID + /* Use a nominal depth of 24 and a bpp of 32 for pixmap and 1 bpp + for mask, to avoid having to query the server. */ if (img->pixmap != NO_PIXMAP) - size += img->width * img->height * 3; + size += img->width * img->height * 4; if (img->mask != NO_PIXMAP) size += img->width * img->height / 8; @@ -3195,9 +3275,12 @@ mark_image_cache (struct image_cache *c) /*********************************************************************** X / NS / W32 support code + Most of this code is shared with Android to make + it easier to maintain. ***********************************************************************/ -#ifdef HAVE_X_WINDOWS +#if defined HAVE_X_WINDOWS || defined HAVE_ANDROID + static bool x_check_image_size (XImage *ximg, int width, int height) { @@ -3213,7 +3296,11 @@ x_check_image_size (XImage *ximg, int width, int height) int bitmap_pad, depth, bytes_per_line; if (ximg) { +#ifndef HAVE_ANDROID bitmap_pad = ximg->bitmap_pad; +#else + bitmap_pad = (ximg->depth == 1 ? 8 : 32); +#endif depth = ximg->depth; bytes_per_line = ximg->bytes_per_line; } @@ -3231,16 +3318,23 @@ static bool x_create_x_image_and_pixmap (struct frame *f, int width, int height, int depth, XImage **ximg, Pixmap *pixmap) { +#ifndef HAVE_ANDROID Display *display = FRAME_X_DISPLAY (f); Drawable drawable = FRAME_X_DRAWABLE (f); +#endif eassert (input_blocked_p ()); if (depth <= 0) depth = FRAME_DISPLAY_INFO (f)->n_planes; +#ifndef HAVE_ANDROID *ximg = XCreateImage (display, FRAME_X_VISUAL (f), depth, ZPixmap, 0, NULL, width, height, depth > 16 ? 32 : depth > 8 ? 16 : 8, 0); +#else + *ximg = android_create_image (depth, ANDROID_Z_PIXMAP, NULL, width, + height); +#endif if (*ximg == NULL) { image_error ("Unable to allocate X image"); @@ -3260,7 +3354,15 @@ x_create_x_image_and_pixmap (struct frame *f, int width, int height, int depth, (*ximg)->data = xmalloc ((*ximg)->bytes_per_line * height); /* Allocate a pixmap of the same size. */ +#ifndef HAVE_ANDROID *pixmap = XCreatePixmap (display, drawable, width, height, depth); +#else +#ifndef ANDROID_STUBIFY + *pixmap = android_create_pixmap (width, height, depth); +#else + emacs_abort (); +#endif +#endif if (*pixmap == NO_PIXMAP) { x_destroy_x_image (*ximg); @@ -3281,7 +3383,11 @@ x_destroy_x_image (XImage *ximg) ximg->data = NULL; } +#ifndef HAVE_ANDROID XDestroyImage (ximg); +#else + android_destroy_image (ximg); +#endif } # if !defined USE_CAIRO && defined HAVE_XRENDER @@ -3348,7 +3454,7 @@ x_create_xrender_picture (struct frame *f, Emacs_Pixmap pixmap, int depth) static bool image_check_image_size (Emacs_Pix_Container ximg, int width, int height) { -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO return x_check_image_size (ximg, width, height); #else /* FIXME: Implement this check for the HAVE_NS and HAVE_NTGUI cases. @@ -3372,16 +3478,6 @@ image_create_x_image_and_pixmap_1 (struct frame *f, int width, int height, int d Emacs_Pix_Container *pimg, Emacs_Pixmap *pixmap, Picture *picture) { -#ifdef HAVE_ANDROID -#ifdef ANDROID_STUBIFY - /* This function should never be called when building stubs. */ - emacs_abort (); -#else - /* you lose, not yet implemented TODO */ - return false; -#endif -#endif - #ifdef USE_CAIRO eassert (input_blocked_p ()); @@ -3396,7 +3492,7 @@ image_create_x_image_and_pixmap_1 (struct frame *f, int width, int height, int d *pimg = *pixmap; return 1; -#elif defined HAVE_X_WINDOWS +#elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID if (!x_create_x_image_and_pixmap (f, width, height, depth, pimg, pixmap)) return 0; # ifdef HAVE_XRENDER @@ -3542,7 +3638,7 @@ image_create_x_image_and_pixmap_1 (struct frame *f, int width, int height, int d static void image_destroy_x_image (Emacs_Pix_Container pimg) { -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO x_destroy_x_image (pimg); #else eassert (input_blocked_p ()); @@ -3581,7 +3677,9 @@ gui_put_x_image (struct frame *f, Emacs_Pix_Container pimg, XPutImage (FRAME_X_DISPLAY (f), pixmap, gc, pimg, 0, 0, 0, 0, pimg->width, pimg->height); XFreeGC (FRAME_X_DISPLAY (f), gc); -#endif /* HAVE_X_WINDOWS */ +#elif defined HAVE_ANDROID + android_put_image (pixmap, pimg); +#endif #ifdef HAVE_NS eassert (pimg == pixmap); @@ -3618,7 +3716,7 @@ static void image_put_x_image (struct frame *f, struct image *img, Emacs_Pix_Container ximg, bool mask_p) { -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO if (!mask_p) { eassert (img->ximg == NULL); @@ -3636,7 +3734,7 @@ image_put_x_image (struct frame *f, struct image *img, Emacs_Pix_Container ximg, #endif } -#if defined HAVE_X_WINDOWS && !defined USE_CAIRO +#if (defined HAVE_X_WINDOWS || defined HAVE_ANDROID) && !defined USE_CAIRO /* Put the X images recorded in IMG on frame F into pixmaps, then free the X images and their buffers. */ @@ -3690,19 +3788,9 @@ image_unget_x_image_or_dc (struct image *img, bool mask_p, static Emacs_Pix_Container image_get_x_image (struct frame *f, struct image *img, bool mask_p) { -#ifdef HAVE_ANDROID -#ifdef ANDROID_STUBIFY - /* This function should never be called when building stubs. */ - emacs_abort (); -#else - /* you lose, not yet implemented TODO */ - return 0; -#endif -#endif - #if defined USE_CAIRO || defined (HAVE_HAIKU) return !mask_p ? img->pixmap : img->mask; -#elif defined HAVE_X_WINDOWS +#elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID XImage *ximg_in_img = !mask_p ? img->ximg : img->mask_img; if (ximg_in_img) @@ -3712,9 +3800,15 @@ image_get_x_image (struct frame *f, struct image *img, bool mask_p) return XGetImage (FRAME_X_DISPLAY (f), !mask_p ? img->pixmap : img->mask, 0, 0, img->original_width, img->original_height, ~0, ZPixmap); #endif +#ifndef HAVE_ANDROID else return XGetImage (FRAME_X_DISPLAY (f), !mask_p ? img->pixmap : img->mask, 0, 0, img->width, img->height, ~0, ZPixmap); +#else + else + return android_get_image (!mask_p ? img->pixmap : img->mask, + ANDROID_Z_PIXMAP); +#endif #elif defined (HAVE_NS) Emacs_Pix_Container pixmap = !mask_p ? img->pixmap : img->mask; @@ -3727,13 +3821,18 @@ static void image_unget_x_image (struct image *img, bool mask_p, Emacs_Pix_Container ximg) { #ifdef USE_CAIRO -#elif defined HAVE_X_WINDOWS +#elif defined HAVE_X_WINDOWS || defined HAVE_ANDROID XImage *ximg_in_img = !mask_p ? img->ximg : img->mask_img; if (ximg_in_img) eassert (ximg == ximg_in_img); +#ifdef HAVE_ANDROID + else + android_destroy_image (ximg); +#else else XDestroyImage (ximg); +#endif #elif defined (HAVE_NS) ns_release_object (ximg); #endif @@ -4256,6 +4355,15 @@ Create_Pixmap_From_Bitmap_Data (struct frame *f, struct image *img, char *data, img->picture = x_create_xrender_picture (f, img->pixmap, 0); # endif +#elif defined HAVE_ANDROID +#ifndef ANDROID_STUBIFY + img->pixmap + = android_create_pixmap_from_bitmap_data (data, img->width, img->height, + fg, bg, + FRAME_DISPLAY_INFO (f)->n_planes); +#else + emacs_abort (); +#endif #elif defined HAVE_NTGUI img->pixmap = w32_create_pixmap_from_bitmap_data (img->width, img->height, data); @@ -4686,7 +4794,8 @@ xbm_load (struct frame *f, struct image *img) XPM images ***********************************************************************/ -#if defined (HAVE_XPM) || defined (HAVE_NS) || defined (HAVE_PGTK) +#if defined (HAVE_XPM) || defined (HAVE_NS) || defined (HAVE_PGTK) \ + || defined (HAVE_ANDROID) static bool xpm_image_p (Lisp_Object object); static bool xpm_load (struct frame *f, struct image *img); @@ -4716,7 +4825,8 @@ static bool xpm_load (struct frame *f, struct image *img); #endif /* not HAVE_NTGUI */ #endif /* HAVE_XPM */ -#if defined HAVE_XPM || defined USE_CAIRO || defined HAVE_NS || defined HAVE_HAIKU +#if defined HAVE_XPM || defined USE_CAIRO || defined HAVE_NS \ + || defined HAVE_HAIKU || defined HAVE_ANDROID /* Indices of image specification fields in xpm_format, below. */ @@ -4736,7 +4846,8 @@ enum xpm_keyword_index XPM_LAST }; -#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU || defined HAVE_PGTK +#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU \ + || defined HAVE_PGTK || defined HAVE_ANDROID /* Vector of image_keyword structures describing the format of valid XPM image specifications. */ @@ -4978,7 +5089,8 @@ init_xpm_functions (void) #endif /* WINDOWSNT */ -#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU || defined HAVE_PGTK +#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU \ + || defined HAVE_PGTK || defined HAVE_ANDROID /* Value is true if COLOR_SYMBOLS is a valid color symbols list for XPM images. Such a list must consist of conses whose car and cdr are strings. */ @@ -5014,9 +5126,9 @@ xpm_image_p (Lisp_Object object) && (! fmt[XPM_COLOR_SYMBOLS].count || xpm_valid_color_symbols_p (fmt[XPM_COLOR_SYMBOLS].value))); } -#endif /* HAVE_XPM || HAVE_NS || HAVE_HAIKU || HAVE_PGTK */ +#endif /* HAVE_XPM || HAVE_NS || HAVE_HAIKU || HAVE_PGTK || HAVE_ANDROID */ -#endif /* HAVE_XPM || USE_CAIRO || HAVE_NS || HAVE_HAIKU */ +#endif /* HAVE_XPM || USE_CAIRO || HAVE_NS || HAVE_HAIKU || HAVE_ANDROID */ #if defined HAVE_XPM && defined HAVE_X_WINDOWS && !defined USE_GTK ptrdiff_t @@ -5389,10 +5501,12 @@ xpm_load (struct frame *f, struct image *img) #if (defined USE_CAIRO && defined HAVE_XPM) \ || (defined HAVE_NS && !defined HAVE_XPM) \ || (defined HAVE_HAIKU && !defined HAVE_XPM) \ - || (defined HAVE_PGTK && !defined HAVE_XPM) + || (defined HAVE_PGTK && !defined HAVE_XPM) \ + || (defined HAVE_ANDROID && !defined HAVE_XPM) -/* XPM support functions for NS and Haiku where libxpm is not available, and for - Cairo. Only XPM version 3 (without any extensions) is supported. */ +/* XPM support functions for NS, Haiku and Android where libxpm is not + available, and for Cairo. Only XPM version 3 (without any + extensions) is supported. */ static void xpm_put_color_table_v (Lisp_Object, const char *, int, Lisp_Object); @@ -6492,11 +6606,16 @@ image_pixmap_draw_cross (struct frame *f, Emacs_Pixmap pixmap, #elif HAVE_HAIKU be_draw_cross_on_pixmap (pixmap, x, y, width, height, color); #elif HAVE_ANDROID -#ifdef ANDROID_STUBIFY - /* This function should never be called when building stubs. */ - emacs_abort (); +#ifndef ANDROID_STUBIFY + struct android_gc *gc; + + gc = android_create_gc (0, NULL); + android_set_foreground (gc, color); + android_draw_line (pixmap, gc, x, y, x + width - 1, y + height - 1); + android_draw_line (pixmap, gc, x, y + height - 1, x + width - 1, y); + android_free_gc (gc); #else - /* you lose, not yet implemented TODO */ + emacs_abort (); #endif #endif } @@ -6552,7 +6671,7 @@ image_disable_image (struct frame *f, struct image *img) #define MaskForeground(f) PIX_MASK_DRAW #endif /* USE_CAIRO || HAVE_HAIKU */ -#if !defined USE_CAIRO && !defined HAVE_HAIKU && !defined HAVE_ANDROID +#if !defined USE_CAIRO && !defined HAVE_HAIKU image_sync_to_pixmaps (f, img); #endif /* !USE_CAIRO && !HAVE_HAIKU */ image_pixmap_draw_cross (f, img->pixmap, 0, 0, img->width, img->height, @@ -12079,7 +12198,8 @@ static struct image_type const image_types[] = { SYMBOL_INDEX (Qjpeg), jpeg_image_p, jpeg_load, image_clear_image, IMAGE_TYPE_INIT (init_jpeg_functions) }, #endif -#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU || defined HAVE_PGTK +#if defined HAVE_XPM || defined HAVE_NS || defined HAVE_HAIKU \ + || defined HAVE_PGTK || defined HAVE_ANDROID { SYMBOL_INDEX (Qxpm), xpm_image_p, xpm_load, image_clear_image, IMAGE_TYPE_INIT (init_xpm_functions) }, #endif @@ -12138,7 +12258,7 @@ syms_of_image (void) DEFVAR_LISP ("image-types", Vimage_types, doc: /* List of potentially supported image types. Each element of the list is a symbol for an image type, like `jpeg' or `png'. -To check whether it is really supported, use `image-type-available-p'. */); + check whether it is really supported, use `image-type-available-p'. */); Vimage_types = Qnil; DEFVAR_LISP ("max-image-size", Vmax_image_size, @@ -12241,7 +12361,8 @@ non-numeric, there is no explicit limit on the size of images. */); add_image_type (Qxbm); #if defined (HAVE_XPM) || defined (HAVE_NS) \ - || defined (HAVE_HAIKU) || defined (HAVE_PGTK) + || defined (HAVE_HAIKU) || defined (HAVE_PGTK) \ + || defined (HAVE_ANDROID) DEFSYM (Qxpm, "xpm"); add_image_type (Qxpm); #endif diff --git a/src/keyboard.c b/src/keyboard.c index 7bf89ac7d4b..8eec833a9ca 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -4909,7 +4909,58 @@ static const char *const lispy_accent_keys[] = "dead-horn", }; -#ifdef HAVE_NTGUI +#ifdef HAVE_ANDROID +#define FUNCTION_KEY_OFFSET 0 + +const char *const lispy_function_keys[] = + { + /* All elements in this array default to 0, except for the few + function keys that Emacs recognizes. */ + [111] = "escape", + [121] = "break", + [122] = "home", + [123] = "end", + [124] = "insert", + [131] = "f1", + [132] = "f2", + [133] = "f3", + [134] = "f4", + [135] = "f5", + [136] = "f6", + [137] = "f7", + [138] = "f8", + [139] = "f9", + [140] = "f10", + [141] = "f11", + [142] = "f12", + [160] = "kp-ret", + [19] = "up", + [20] = "down", + [213] = "muhenkan", + [214] = "henkan", + [215] = "hiragana-katakana", + [218] = "kana", + [21] = "left", + [22] = "right", + [259] = "help", + [268] = "kp-up-left", + [269] = "kp-down-left", + [270] = "kp-up-right", + [271] = "kp-down-right", + [277] = "cut", + [278] = "copy", + [279] = "paste", + [28] = "clear", + [4] = "back", + [61] = "tab", + [66] = "return", + [67] = "backspace", + [82] = "menu", + [92] = "page-up", + [93] = "page-down", + }; + +#elif defined HAVE_NTGUI #define FUNCTION_KEY_OFFSET 0x0 const char *const lispy_function_keys[] = diff --git a/src/lread.c b/src/lread.c index eb029f01623..15273ad34fa 100644 --- a/src/lread.c +++ b/src/lread.c @@ -2073,7 +2073,7 @@ static void build_load_history (Lisp_Object filename, bool entire) { Lisp_Object tail, prev, newelt; - Lisp_Object tem, tem2; + Lisp_Object tem, tem2, association; bool foundit = 0; tail = Vload_history; @@ -2088,7 +2088,7 @@ build_load_history (Lisp_Object filename, bool entire) { foundit = 1; - /* If we're loading the entire file, remove old data. */ + /* If we're loading the entire file, remove old data. */ if (entire) { if (NILP (prev)) @@ -2096,8 +2096,8 @@ build_load_history (Lisp_Object filename, bool entire) else Fsetcdr (prev, XCDR (tail)); } - - /* Otherwise, cons on new symbols that are not already members. */ + /* Otherwise, cons on new symbols that are not already + members. */ else { tem2 = Vcurrent_load_list; @@ -2122,8 +2122,16 @@ build_load_history (Lisp_Object filename, bool entire) front of load-history, the most-recently-loaded position. Also do this if we didn't find an existing member for the file. */ if (entire || !foundit) - Vload_history = Fcons (Fnreverse (Vcurrent_load_list), - Vload_history); + { + association = Fnreverse (Vcurrent_load_list); + + if (!NILP (association) && STRINGP (XCAR (association))) + /* readevalloop can be called with SOURCENAME set to some + nonsense value, meaning the car of ASSOCIATION will be nil + (or worse something else), leading to an invalid + Vload_history. Ignore such invalid entries. */ + Vload_history = Fcons (association, Vload_history); + } } static void -- cgit v1.3 From 695e26079eb60d10ffe25bb8ae91ebc6131fb27d Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 8 Jan 2023 15:39:02 +0800 Subject: Update Java part of Android port * java/org/gnu/emacs/EmacsCopyArea.java (EmacsCopyArea, perform) (paintTo): * java/org/gnu/emacs/EmacsDrawLine.java (EmacsDrawLine): * java/org/gnu/emacs/EmacsDrawPoint.java (EmacsDrawPoint): * java/org/gnu/emacs/EmacsDrawRectangle.java (EmacsDrawRectangle) (paintTo): * java/org/gnu/emacs/EmacsDrawable.java (EmacsDrawable): * java/org/gnu/emacs/EmacsFillPolygon.java (EmacsFillPolygon): * java/org/gnu/emacs/EmacsFillRectangle.java (EmacsFillRectangle): * java/org/gnu/emacs/EmacsFontDriver.java (EmacsFontDriver): * java/org/gnu/emacs/EmacsGC.java (EmacsGC): * java/org/gnu/emacs/EmacsNative.java (EmacsNative): * java/org/gnu/emacs/EmacsPixmap.java (EmacsPixmap): * java/org/gnu/emacs/EmacsSdk23FontDriver.java (EmacsSdk23FontDriver): * java/org/gnu/emacs/EmacsSdk7FontDriver.java (EmacsSdk7FontDriver, textExtents1, textExtents, draw): * java/org/gnu/emacs/EmacsService.java (EmacsService, copyArea): * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView): * java/org/gnu/emacs/EmacsView.java (EmacsView, onLayout) (onFocusChanged): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, run) (resizeWindow, lockCanvas, getBitmap, onKeyDown, onKeyUp) (onActivityDetached): Move rendering to main thread. Make drawing operations completely static. --- java/org/gnu/emacs/EmacsCopyArea.java | 131 +++++++++------------- java/org/gnu/emacs/EmacsDrawLine.java | 120 +++++--------------- java/org/gnu/emacs/EmacsDrawPoint.java | 11 +- java/org/gnu/emacs/EmacsDrawRectangle.java | 160 +++++++++++++-------------- java/org/gnu/emacs/EmacsDrawable.java | 1 - java/org/gnu/emacs/EmacsFillPolygon.java | 128 ++++++--------------- java/org/gnu/emacs/EmacsFillRectangle.java | 152 ++++++++++++------------- java/org/gnu/emacs/EmacsFontDriver.java | 2 +- java/org/gnu/emacs/EmacsGC.java | 77 ++++++------- java/org/gnu/emacs/EmacsNative.java | 22 ++++ java/org/gnu/emacs/EmacsPixmap.java | 9 +- java/org/gnu/emacs/EmacsSdk23FontDriver.java | 71 ++++++++++++ java/org/gnu/emacs/EmacsSdk7FontDriver.java | 139 +++++++---------------- java/org/gnu/emacs/EmacsService.java | 122 ++------------------ java/org/gnu/emacs/EmacsSurfaceView.java | 22 ++-- java/org/gnu/emacs/EmacsView.java | 133 ++++++++++++++++------ java/org/gnu/emacs/EmacsWindow.java | 141 +++++++++++++++++++---- 17 files changed, 685 insertions(+), 756 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index 0dd5b2c1fb6..00e817bb97d 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -27,54 +27,16 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Xfermode; -public class EmacsCopyArea implements EmacsPaintReq +public class EmacsCopyArea { - private int src_x, src_y, dest_x, dest_y, width, height; - private EmacsDrawable destination, source; - private EmacsGC immutableGC; - private static Xfermode xorAlu, srcInAlu, overAlu; + private static Xfermode overAlu; static { overAlu = new PorterDuffXfermode (Mode.SRC_OVER); - xorAlu = new PorterDuffXfermode (Mode.XOR); - srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); }; - public - EmacsCopyArea (EmacsDrawable source, EmacsDrawable destination, - int src_x, int src_y, int width, int height, - int dest_x, int dest_y, EmacsGC immutableGC) - { - Bitmap bitmap; - - this.destination = destination; - this.source = source; - this.src_x = src_x; - this.src_y = src_y; - this.width = width; - this.height = height; - this.dest_x = dest_x; - this.dest_y = dest_y; - this.immutableGC = immutableGC; - } - - @Override - public Rect - getRect () - { - return new Rect (dest_x, dest_y, dest_x + width, - dest_y + height); - } - - @Override - public EmacsDrawable - getDrawable () - { - return destination; - } - - private void + private static void insetRectBy (Rect rect, int left, int top, int right, int bottom) { @@ -84,30 +46,38 @@ public class EmacsCopyArea implements EmacsPaintReq rect.bottom -= bottom; } - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) + public static void + perform (EmacsDrawable source, EmacsGC gc, + EmacsDrawable destination, + int src_x, int src_y, int width, int height, + int dest_x, int dest_y) { - int alu; + int i; Bitmap bitmap; - Paint maskPaint; - Canvas maskCanvas; + Paint maskPaint, paint; + Canvas maskCanvas, canvas; Bitmap srcBitmap, maskBitmap, clipBitmap; Rect rect, maskRect, srcRect, dstRect, maskDestRect; boolean needFill; /* TODO implement stippling. */ - if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) return; - alu = immutableGC.function; + paint = gc.gcPaint; + + canvas = destination.lockCanvas (); + + if (canvas == null) + return; + + canvas.save (); + + if (gc.real_clip_rects != null) + { + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); + } /* A copy must be created or drawBitmap could end up overwriting itself. */ @@ -137,14 +107,10 @@ public class EmacsCopyArea implements EmacsPaintReq if (src_y + height > srcBitmap.getHeight ()) height = srcBitmap.getHeight () - src_y; - rect = getRect (); - - if (alu == EmacsGC.GC_COPY) - paint.setXfermode (null); - else - paint.setXfermode (xorAlu); + rect = new Rect (dest_x, dest_y, dest_x + width, + dest_y + height); - if (immutableGC.clip_mask == null) + if (gc.clip_mask == null) { bitmap = Bitmap.createBitmap (srcBitmap, src_x, src_y, width, @@ -156,17 +122,17 @@ public class EmacsCopyArea implements EmacsPaintReq /* Drawing with a clip mask involves calculating the intersection of the clip mask with the dst rect, and extrapolating the corresponding part of the src rect. */ - clipBitmap = immutableGC.clip_mask.bitmap; + clipBitmap = gc.clip_mask.bitmap; dstRect = new Rect (dest_x, dest_y, dest_x + width, dest_y + height); - maskRect = new Rect (immutableGC.clip_x_origin, - immutableGC.clip_y_origin, - (immutableGC.clip_x_origin + maskRect = new Rect (gc.clip_x_origin, + gc.clip_y_origin, + (gc.clip_x_origin + clipBitmap.getWidth ()), - (immutableGC.clip_y_origin + (gc.clip_y_origin + clipBitmap.getHeight ())); - clipBitmap = immutableGC.clip_mask.bitmap; + clipBitmap = gc.clip_mask.bitmap; if (!maskRect.setIntersect (dstRect, maskRect)) /* There is no intersection between the clip mask and the @@ -191,20 +157,21 @@ public class EmacsCopyArea implements EmacsPaintReq /* Draw the mask onto the maskBitmap. */ maskCanvas = new Canvas (maskBitmap); - maskRect.offset (-immutableGC.clip_x_origin, - -immutableGC.clip_y_origin); - maskCanvas.drawBitmap (immutableGC.clip_mask.bitmap, - maskRect, new Rect (0, 0, - maskRect.width (), - maskRect.height ()), - paint); - maskRect.offset (immutableGC.clip_x_origin, - immutableGC.clip_y_origin); + maskPaint = new Paint (); + maskRect.offset (-gc.clip_x_origin, + -gc.clip_y_origin); + maskCanvas.drawBitmap (gc.clip_mask.bitmap, + maskRect, + new Rect (0, 0, + maskRect.width (), + maskRect.height ()), + maskPaint); + maskRect.offset (gc.clip_x_origin, + gc.clip_y_origin); /* Set the transfer mode to SRC_IN to preserve only the parts of the source that overlap with the mask. */ - maskPaint = new Paint (); - maskPaint.setXfermode (srcInAlu); + maskPaint.setXfermode (EmacsGC.srcInAlu); /* Draw the source. */ maskDestRect = new Rect (0, 0, srcRect.width (), @@ -215,6 +182,10 @@ public class EmacsCopyArea implements EmacsPaintReq /* Finally, draw the mask bitmap to the destination. */ paint.setXfermode (overAlu); canvas.drawBitmap (maskBitmap, null, maskRect, paint); + gc.resetXfermode (); } + + canvas.restore (); + destination.damageRect (rect); } } diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index 6389031bbfa..8941d4c217f 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -29,109 +29,49 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Xfermode; -public class EmacsDrawLine implements EmacsPaintReq +public class EmacsDrawLine { - private int x, y, x2, y2; - private EmacsDrawable drawable; - private EmacsGC immutableGC; - private static Xfermode xorAlu, srcInAlu; - - static + public static void + perform (EmacsDrawable drawable, EmacsGC gc, + int x, int y, int x2, int y2) { - xorAlu = new PorterDuffXfermode (Mode.XOR); - srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); - }; + Rect rect; + Canvas canvas; + Paint paint; + int i; - public - EmacsDrawLine (EmacsDrawable drawable, int x, int y, - int x2, int y2, EmacsGC immutableGC) - { - this.drawable = drawable; - this.x = x; - this.y = y; - this.x2 = x2; - this.y2 = y2; - this.immutableGC = immutableGC; - } + /* TODO implement stippling. */ + if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + return; - @Override - public Rect - getRect () - { - return new Rect (Math.min (x, x2 + 1), + paint = gc.gcPaint; + rect = new Rect (Math.min (x, x2 + 1), Math.min (y, y2 + 1), Math.max (x2 + 1, x), Math.max (y2 + 1, y)); - } - - @Override - public EmacsDrawable - getDrawable () - { - return drawable; - } - - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) - { - int alu; - Paint maskPaint; - Canvas maskCanvas; - Bitmap maskBitmap; - Rect rect, srcRect; - int width, height; + canvas = drawable.lockCanvas (); - /* TODO implement stippling. */ - if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + if (canvas == null) return; - alu = immutableGC.function; - rect = getRect (); - width = rect.width (); - height = rect.height (); - - paint.setStyle (Paint.Style.STROKE); - - if (alu == EmacsGC.GC_COPY) - paint.setXfermode (null); - else - paint.setXfermode (xorAlu); + canvas.save (); - if (immutableGC.clip_mask == null) - { - paint.setColor (immutableGC.foreground | 0xff000000); - canvas.drawLine ((float) x, (float) y, - (float) x2, (float) y2, - paint); - } - else + if (gc.real_clip_rects != null) { - maskPaint = new Paint (); - maskBitmap - = immutableGC.clip_mask.bitmap.copy (Bitmap.Config.ARGB_8888, - true); - - if (maskBitmap == null) - return; - - maskPaint.setXfermode (srcInAlu); - maskPaint.setColor (immutableGC.foreground | 0xff000000); - maskCanvas = new Canvas (maskBitmap); - srcRect = new Rect (0, 0, maskBitmap.getWidth (), - maskBitmap.getHeight ()); - maskCanvas.drawLine (0.0f, 0.0f, (float) Math.abs (x - x2), - (float) Math.abs (y - y2), maskPaint); - canvas.drawBitmap (maskBitmap, srcRect, rect, paint); + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); } - paint.setXfermode (null); + paint.setStyle (Paint.Style.STROKE); + + if (gc.clip_mask == null) + canvas.drawLine ((float) x, (float) y, + (float) x2, (float) y2, + paint); + + /* DrawLine with clip mask not implemented; it is not used by + Emacs. */ + canvas.restore (); + drawable.damageRect (rect); } } diff --git a/java/org/gnu/emacs/EmacsDrawPoint.java b/java/org/gnu/emacs/EmacsDrawPoint.java index 772757ff424..3bc7be17961 100644 --- a/java/org/gnu/emacs/EmacsDrawPoint.java +++ b/java/org/gnu/emacs/EmacsDrawPoint.java @@ -19,12 +19,13 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -public class EmacsDrawPoint extends EmacsDrawRectangle +public class EmacsDrawPoint { - public - EmacsDrawPoint (EmacsDrawable drawable, int x, int y, - EmacsGC immutableGC) + public static void + perform (EmacsDrawable drawable, + EmacsGC immutableGC, int x, int y) { - super (drawable, x, y, 1, 1, immutableGC); + EmacsDrawRectangle.perform (drawable, immutableGC, + x, y, 1, 1); } } diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index e3f28227146..b42e9556e8c 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -22,110 +22,104 @@ package org.gnu.emacs; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.Xfermode; -public class EmacsDrawRectangle implements EmacsPaintReq -{ - private int x, y, width, height; - private EmacsDrawable drawable; - private EmacsGC immutableGC; - private static Xfermode xorAlu, srcInAlu; - - static - { - xorAlu = new PorterDuffXfermode (Mode.XOR); - srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); - }; - - public - EmacsDrawRectangle (EmacsDrawable drawable, int x, int y, - int width, int height, - EmacsGC immutableGC) - { - this.drawable = drawable; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.immutableGC = immutableGC; - } +import android.util.Log; - @Override - public Rect - getRect () - { - /* Canvas.drawRect actually behaves exactly like PolyRectangle wrt - to where the lines are placed, so extend the width and height - by 1 in the damage rectangle. */ - return new Rect (x, y, x + width + 1, y + height + 1); - } - - @Override - public EmacsDrawable - getDrawable () - { - return drawable; - } - - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) +public class EmacsDrawRectangle +{ + public static void + perform (EmacsDrawable drawable, EmacsGC gc, + int x, int y, int width, int height) { - int alu; - Paint maskPaint; + int i; + Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; - Rect rect, srcRect; + Rect rect; + Rect maskRect, dstRect; + Canvas canvas; + Bitmap clipBitmap; /* TODO implement stippling. */ - if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) return; - alu = immutableGC.function; - rect = new Rect (x, y, x + width, y + height); + canvas = drawable.lockCanvas (); - paint.setStyle (Paint.Style.STROKE); - paint.setStrokeWidth (1); + if (canvas == null) + return; - if (alu == EmacsGC.GC_COPY) - paint.setXfermode (null); - else - paint.setXfermode (xorAlu); + canvas.save (); - if (immutableGC.clip_mask == null) + if (gc.real_clip_rects != null) { - paint.setColor (immutableGC.foreground | 0xff000000); - canvas.drawRect (rect, paint); + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); } + + paint = gc.gcPaint; + rect = new Rect (x, y, x + width, y + height); + + paint.setStyle (Paint.Style.STROKE); + + if (gc.clip_mask == null) + canvas.drawRect (rect, paint); else { - maskPaint = new Paint (); - maskBitmap - = immutableGC.clip_mask.bitmap.copy (Bitmap.Config.ARGB_8888, - true); - - if (maskBitmap == null) + /* Drawing with a clip mask involves calculating the + intersection of the clip mask with the dst rect, and + extrapolating the corresponding part of the src rect. */ + clipBitmap = gc.clip_mask.bitmap; + dstRect = new Rect (x, y, x + width, y + height); + maskRect = new Rect (gc.clip_x_origin, + gc.clip_y_origin, + (gc.clip_x_origin + + clipBitmap.getWidth ()), + (gc.clip_y_origin + + clipBitmap.getHeight ())); + clipBitmap = gc.clip_mask.bitmap; + + if (!maskRect.setIntersect (dstRect, maskRect)) + /* There is no intersection between the clip mask and the + dest rect. */ return; - maskPaint.setXfermode (srcInAlu); - maskPaint.setColor (immutableGC.foreground | 0xff000000); + /* Finally, create a temporary bitmap that is the size of + maskRect. */ + + maskBitmap + = Bitmap.createBitmap (maskRect.width (), maskRect.height (), + Bitmap.Config.ARGB_8888); + + /* Draw the mask onto the maskBitmap. */ maskCanvas = new Canvas (maskBitmap); - srcRect = new Rect (0, 0, maskBitmap.getWidth (), - maskBitmap.getHeight ()); - maskCanvas.drawRect (srcRect, maskPaint); - canvas.drawBitmap (maskBitmap, srcRect, rect, paint); + maskRect.offset (-gc.clip_x_origin, + -gc.clip_y_origin); + maskCanvas.drawBitmap (gc.clip_mask.bitmap, + maskRect, new Rect (0, 0, + maskRect.width (), + maskRect.height ()), + paint); + maskRect.offset (gc.clip_x_origin, + gc.clip_y_origin); + + /* Set the transfer mode to SRC_IN to preserve only the parts + of the source that overlap with the mask. */ + maskPaint = new Paint (); + maskPaint.setXfermode (EmacsGC.srcInAlu); + maskPaint.setStyle (Paint.Style.STROKE); + + /* Draw the source. */ + maskCanvas.drawRect (maskRect, maskPaint); + + /* Finally, draw the mask bitmap to the destination. */ + paint.setXfermode (null); + canvas.drawBitmap (maskBitmap, null, maskRect, paint); } - paint.setXfermode (null); + canvas.restore (); + drawable.damageRect (new Rect (x, y, x + width + 1, + y + height + 1)); } } diff --git a/java/org/gnu/emacs/EmacsDrawable.java b/java/org/gnu/emacs/EmacsDrawable.java index 19062137213..6a6199ff214 100644 --- a/java/org/gnu/emacs/EmacsDrawable.java +++ b/java/org/gnu/emacs/EmacsDrawable.java @@ -26,7 +26,6 @@ import android.graphics.Canvas; public interface EmacsDrawable { public Canvas lockCanvas (); - public void unlockCanvas (); public void damageRect (Rect damageRect); public Bitmap getBitmap (); public boolean isDestroyed (); diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java index 3198c7f07c4..42b73886dff 100644 --- a/java/org/gnu/emacs/EmacsFillPolygon.java +++ b/java/org/gnu/emacs/EmacsFillPolygon.java @@ -26,34 +26,35 @@ import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.RectF; -import android.graphics.Xfermode; -public class EmacsFillPolygon implements EmacsPaintReq +public class EmacsFillPolygon { - private EmacsDrawable drawable; - private EmacsGC immutableGC; - private Path path; + public static void + perform (EmacsDrawable drawable, EmacsGC gc, Point points[]) + { + Canvas canvas; + Path path; + Paint paint; + Rect rect; + RectF rectF; + int i; - private static Xfermode xorAlu, srcInAlu; + canvas = drawable.lockCanvas (); - static - { - xorAlu = new PorterDuffXfermode (Mode.XOR); - srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); - }; + if (canvas == null) + return; - public - EmacsFillPolygon (EmacsDrawable drawable, Point points[], - EmacsGC immutableGC) - { - int i; + paint = gc.gcPaint; + + canvas.save (); - this.drawable = drawable; - this.immutableGC = immutableGC; + if (gc.real_clip_rects != null) + { + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); + } /* Build the path from the given array of points. */ path = new Path (); @@ -67,84 +68,25 @@ public class EmacsFillPolygon implements EmacsPaintReq path.close (); } - } - - @Override - public Rect - getRect () - { - RectF rect; - rect = new RectF (0, 0, 0, 0); - path.computeBounds (rect, true); + /* Compute the damage rectangle. */ + rectF = new RectF (0, 0, 0, 0); + path.computeBounds (rectF, true); - return new Rect ((int) Math.floor (rect.left), - (int) Math.floor (rect.top), - (int) Math.ceil (rect.right), - (int) Math.ceil (rect.bottom)); - } + rect = new Rect ((int) Math.floor (rectF.left), + (int) Math.floor (rectF.top), + (int) Math.ceil (rectF.right), + (int) Math.ceil (rectF.bottom)); - @Override - public EmacsDrawable - getDrawable () - { - return drawable; - } - - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) - { - int alu; - Paint maskPaint; - Canvas maskCanvas; - Bitmap maskBitmap; - Rect rect; - - /* TODO implement stippling. */ - if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) - return; - - alu = immutableGC.function; - rect = getRect (); + paint.setStyle (Paint.Style.FILL); - if (alu == EmacsGC.GC_COPY) - paint.setXfermode (null); - else - paint.setXfermode (xorAlu); + if (gc.clip_mask == null) + canvas.drawPath (path, paint); - paint.setStyle (Paint.Style.FILL); + canvas.restore (); + drawable.damageRect (rect); - if (immutableGC.clip_mask == null) - { - paint.setColor (immutableGC.foreground | 0xff000000); - canvas.drawPath (path, paint); - } - else - { - maskPaint = new Paint (); - maskBitmap = immutableGC.clip_mask.bitmap; - maskBitmap = maskBitmap.copy (Bitmap.Config.ARGB_8888, - true); - - if (maskBitmap == null) - return; - - maskPaint.setXfermode (srcInAlu); - maskPaint.setColor (immutableGC.foreground | 0xff000000); - maskCanvas = new Canvas (maskBitmap); - path.offset (-rect.left, -rect.top, null); - maskCanvas.drawPath (path, maskPaint); - canvas.drawBitmap (maskBitmap, new Rect (0, 0, rect.width (), - rect.height ()), - rect, paint); - } + /* FillPolygon with clip mask not implemented; it is not used by + Emacs. */ } } diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java index 7246c13de7f..b733b417d6b 100644 --- a/java/org/gnu/emacs/EmacsFillRectangle.java +++ b/java/org/gnu/emacs/EmacsFillRectangle.java @@ -22,108 +22,102 @@ package org.gnu.emacs; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.Xfermode; import android.util.Log; -public class EmacsFillRectangle implements EmacsPaintReq +public class EmacsFillRectangle { - private int x, y, width, height; - private EmacsDrawable drawable; - private EmacsGC immutableGC; - private static Xfermode xorAlu, srcInAlu; - - static - { - xorAlu = new PorterDuffXfermode (Mode.XOR); - srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); - }; - - public - EmacsFillRectangle (EmacsDrawable drawable, int x, int y, - int width, int height, - EmacsGC immutableGC) - { - this.drawable = drawable; - this.x = x; - this.y = y; - this.width = width; - this.height = height; - this.immutableGC = immutableGC; - } - - @Override - public Rect - getRect () - { - return new Rect (x, y, x + width, y + height); - } - - @Override - public EmacsDrawable - getDrawable () + public static void + perform (EmacsDrawable drawable, EmacsGC gc, + int x, int y, int width, int height) { - return drawable; - } - - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) - { - int alu; - Paint maskPaint; + int i; + Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; - Rect rect, srcRect; + Rect rect; + Rect maskRect, dstRect; + Canvas canvas; + Bitmap clipBitmap; /* TODO implement stippling. */ - if (immutableGC.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) + if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) return; - alu = immutableGC.function; - rect = getRect (); + canvas = drawable.lockCanvas (); - paint.setStyle (Paint.Style.FILL); + if (canvas == null) + return; - if (alu == EmacsGC.GC_COPY) - paint.setXfermode (null); - else - paint.setXfermode (xorAlu); + canvas.save (); - if (immutableGC.clip_mask == null) + if (gc.real_clip_rects != null) { - paint.setColor (immutableGC.foreground | 0xff000000); - canvas.drawRect (rect, paint); + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); } + + paint = gc.gcPaint; + rect = new Rect (x, y, x + width, y + height); + + paint.setStyle (Paint.Style.FILL); + + if (gc.clip_mask == null) + canvas.drawRect (rect, paint); else { - maskPaint = new Paint (); - maskBitmap - = immutableGC.clip_mask.bitmap.copy (Bitmap.Config.ARGB_8888, - true); - - if (maskBitmap == null) + /* Drawing with a clip mask involves calculating the + intersection of the clip mask with the dst rect, and + extrapolating the corresponding part of the src rect. */ + clipBitmap = gc.clip_mask.bitmap; + dstRect = new Rect (x, y, x + width, y + height); + maskRect = new Rect (gc.clip_x_origin, + gc.clip_y_origin, + (gc.clip_x_origin + + clipBitmap.getWidth ()), + (gc.clip_y_origin + + clipBitmap.getHeight ())); + clipBitmap = gc.clip_mask.bitmap; + + if (!maskRect.setIntersect (dstRect, maskRect)) + /* There is no intersection between the clip mask and the + dest rect. */ return; - maskPaint.setXfermode (srcInAlu); - maskPaint.setColor (immutableGC.foreground | 0xff000000); + /* Finally, create a temporary bitmap that is the size of + maskRect. */ + + maskBitmap + = Bitmap.createBitmap (maskRect.width (), maskRect.height (), + Bitmap.Config.ARGB_8888); + + /* Draw the mask onto the maskBitmap. */ maskCanvas = new Canvas (maskBitmap); - srcRect = new Rect (0, 0, maskBitmap.getWidth (), - maskBitmap.getHeight ()); - maskCanvas.drawRect (srcRect, maskPaint); - canvas.drawBitmap (maskBitmap, srcRect, rect, paint); + maskRect.offset (-gc.clip_x_origin, + -gc.clip_y_origin); + maskCanvas.drawBitmap (gc.clip_mask.bitmap, + maskRect, new Rect (0, 0, + maskRect.width (), + maskRect.height ()), + paint); + maskRect.offset (gc.clip_x_origin, + gc.clip_y_origin); + + /* Set the transfer mode to SRC_IN to preserve only the parts + of the source that overlap with the mask. */ + maskPaint = new Paint (); + maskPaint.setXfermode (EmacsGC.srcInAlu); + + /* Draw the source. */ + maskCanvas.drawRect (maskRect, maskPaint); + + /* Finally, draw the mask bitmap to the destination. */ + paint.setXfermode (null); + canvas.drawBitmap (maskBitmap, null, maskRect, paint); } - paint.setXfermode (null); + canvas.restore (); + drawable.damageRect (rect); } } diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java index 9f40aa04c44..1d1e6f7b33f 100644 --- a/java/org/gnu/emacs/EmacsFontDriver.java +++ b/java/org/gnu/emacs/EmacsFontDriver.java @@ -164,7 +164,7 @@ public abstract class EmacsFontDriver public static EmacsFontDriver createFontDriver () { - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) return new EmacsSdk23FontDriver (); return new EmacsSdk7FontDriver (); diff --git a/java/org/gnu/emacs/EmacsGC.java b/java/org/gnu/emacs/EmacsGC.java index 0becb04519d..caa5c91edd4 100644 --- a/java/org/gnu/emacs/EmacsGC.java +++ b/java/org/gnu/emacs/EmacsGC.java @@ -20,6 +20,11 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import android.graphics.Rect; +import android.graphics.Paint; + +import android.graphics.PorterDuff.Mode; +import android.graphics.PorterDuffXfermode; +import android.graphics.Xfermode; /* X like graphics context structures. Keep the enums in synch with androidgui.h! */ @@ -32,14 +37,21 @@ public class EmacsGC extends EmacsHandleObject public static final int GC_FILL_SOLID = 0; public static final int GC_FILL_OPAQUE_STIPPLED = 1; + public static final Xfermode xorAlu, srcInAlu; + public int function, fill_style; public int foreground, background; public int clip_x_origin, clip_y_origin; public int ts_origin_x, ts_origin_y; - public Rect clip_rects[]; + public Rect clip_rects[], real_clip_rects[]; public EmacsPixmap clip_mask, stipple; - private boolean dirty; - private EmacsGC immutableGC; + public Paint gcPaint; + + static + { + xorAlu = new PorterDuffXfermode (Mode.XOR); + srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); + } /* The following fields are only set on immutable GCs. */ @@ -55,62 +67,41 @@ public class EmacsGC extends EmacsHandleObject fill_style = GC_FILL_SOLID; function = GC_COPY; foreground = 0; - background = 0xffffffff; + background = 0xffffff; + gcPaint = new Paint (); } - public - EmacsGC (EmacsGC source) - { - super ((short) 0); + /* Mark this GC as dirty. Apply parameters to the paint and + recompute real_clip_rects. */ + public void + markDirty () + { int i; - function = source.function; - fill_style = source.fill_style; - foreground = source.foreground; - background = source.background; - clip_x_origin = source.clip_x_origin; - clip_y_origin = source.clip_y_origin; - clip_rects = source.clip_rects; - clip_mask = source.clip_mask; - stipple = source.stipple; - ts_origin_x = source.ts_origin_x; - ts_origin_y = source.ts_origin_y; - - /* Offset all the clip rects by ts_origin_x and ts_origin_y. */ - if ((ts_origin_x != 0 || ts_origin_y != 0) && clip_rects != null) { - clip_rects = new Rect[clip_rects.length]; + real_clip_rects = new Rect[clip_rects.length]; for (i = 0; i < clip_rects.length; ++i) { - clip_rects[i] = new Rect (source.clip_rects[i]); - clip_rects[i].offset (ts_origin_x, - ts_origin_y); + real_clip_rects[i] = new Rect (clip_rects[i]); + real_clip_rects[i].offset (ts_origin_x, ts_origin_y); } } - } + else + real_clip_rects = clip_rects; - /* Mark this GC as dirty. This means immutableGC will return a new - copy of this GC the next time it is called. */ + gcPaint.setColor (foreground | 0xff000000); + gcPaint.setXfermode (function == GC_XOR + ? xorAlu : srcInAlu); + } public void - markDirty () + resetXfermode () { - dirty = true; + gcPaint.setXfermode (function == GC_XOR + ? xorAlu : srcInAlu); } - - public EmacsGC - immutableGC () - { - if (immutableGC == null || dirty) - { - immutableGC = new EmacsGC (this); - dirty = false; - } - - return immutableGC; - }; }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index c80339031a8..ae48ce30408 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -79,6 +79,28 @@ public class EmacsNative /* Send an ANDROID_WINDOW_ACTION event. */ public static native void sendWindowAction (short window, int action); + /* Send an ANDROID_ENTER_NOTIFY event. */ + public static native void sendEnterNotify (short window, int x, int y, + long time); + + /* Send an ANDROID_LEAVE_NOTIFY event. */ + public static native void sendLeaveNotify (short window, int x, int y, + long time); + + /* Send an ANDROID_MOTION_NOTIFY event. */ + public static native void sendMotionNotify (short window, int x, int y, + long time); + + /* Send an ANDROID_BUTTON_PRESS event. */ + public static native void sendButtonPress (short window, int x, int y, + long time, int state, + int button); + + /* Send an ANDROID_BUTTON_RELEASE event. */ + public static native void sendButtonRelease (short window, int x, int y, + long time, int state, + int button); + static { System.loadLibrary ("emacs"); diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java index 897902c5b57..15452f007c4 100644 --- a/java/org/gnu/emacs/EmacsPixmap.java +++ b/java/org/gnu/emacs/EmacsPixmap.java @@ -93,6 +93,8 @@ public class EmacsPixmap extends EmacsHandleObject break; } + bitmap.eraseColor (0xff000000); + this.width = width; this.height = height; this.depth = depth; @@ -108,13 +110,6 @@ public class EmacsPixmap extends EmacsHandleObject return canvas; } - @Override - public void - unlockCanvas () - { - - } - @Override public void damageRect (Rect damageRect) diff --git a/java/org/gnu/emacs/EmacsSdk23FontDriver.java b/java/org/gnu/emacs/EmacsSdk23FontDriver.java index 34e2b1803a2..11e128d5769 100644 --- a/java/org/gnu/emacs/EmacsSdk23FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk23FontDriver.java @@ -20,9 +20,80 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import android.graphics.Paint; +import android.graphics.Rect; public class EmacsSdk23FontDriver extends EmacsSdk7FontDriver { + private void + textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics, + Paint paint, Rect bounds) + { + char[] text; + + text = new char[2]; + text[0] = (char) code; + text[1] = 'c'; + + paint.getTextBounds (text, 0, 1, bounds); + + metrics.lbearing = (short) bounds.left; + metrics.rbearing = (short) bounds.right; + metrics.ascent = (short) -bounds.top; + metrics.descent = (short) bounds.bottom; + metrics.width + = (short) paint.getRunAdvance (text, 0, 1, 0, 1, false, 1); + } + + @Override + public void + textExtents (FontObject font, int code[], FontMetrics fontMetrics) + { + int i; + Paint paintCache; + Rect boundsCache; + Sdk7FontObject fontObject; + char[] text; + float width; + + fontObject = (Sdk7FontObject) font; + paintCache = fontObject.typeface.typefacePaint; + paintCache.setTextSize (fontObject.pixelSize); + boundsCache = new Rect (); + + if (code.length == 0) + { + fontMetrics.lbearing = 0; + fontMetrics.rbearing = 0; + fontMetrics.ascent = 0; + fontMetrics.descent = 0; + fontMetrics.width = 0; + } + else if (code.length == 1) + textExtents1 ((Sdk7FontObject) font, code[0], fontMetrics, + paintCache, boundsCache); + else + { + text = new char[code.length + 1]; + + for (i = 0; i < code.length; ++i) + text[i] = (char) code[i]; + + text[code.length] = 'c'; + + paintCache.getTextBounds (text, 0, code.length, + boundsCache); + width = paintCache.getRunAdvance (text, 0, code.length, 0, + code.length, + false, code.length); + + fontMetrics.lbearing = (short) boundsCache.left; + fontMetrics.rbearing = (short) boundsCache.right; + fontMetrics.ascent = (short) -boundsCache.top; + fontMetrics.descent = (short) boundsCache.bottom; + fontMetrics.width = (short) width; + } + } + @Override public int hasChar (FontSpec font, char charCode) diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index 437f38e62db..8a9426050ae 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -243,93 +243,6 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver } }; - private class Sdk7DrawString implements EmacsPaintReq - { - private boolean drawBackground; - private Sdk7FontObject fontObject; - private char[] chars; - private EmacsGC immutableGC; - private EmacsDrawable drawable; - private Rect rect; - private int originX, originY; - - public - Sdk7DrawString (Sdk7FontObject fontObject, char[] chars, - EmacsGC immutableGC, EmacsDrawable drawable, - boolean drawBackground, Rect rect, - int originX, int originY) - { - this.fontObject = fontObject; - this.chars = chars; - this.immutableGC = immutableGC; - this.drawable = drawable; - this.drawBackground = drawBackground; - this.rect = rect; - this.originX = originX; - this.originY = originY; - } - - @Override - public EmacsDrawable - getDrawable () - { - return drawable; - } - - @Override - public EmacsGC - getGC () - { - return immutableGC; - } - - @Override - public void - paintTo (Canvas canvas, Paint paint, EmacsGC immutableGC) - { - int scratch; - - paint.setStyle (Paint.Style.FILL); - - if (drawBackground) - { - paint.setColor (immutableGC.background | 0xff000000); - canvas.drawRect (rect, paint); - } - - paint.setTextSize (fontObject.pixelSize); - paint.setColor (immutableGC.foreground | 0xff000000); - paint.setTypeface (fontObject.typeface.typeface); - paint.setAntiAlias (true); - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) - /* Disable hinting as that leads to displayed text not - matching the computed metrics. */ - paint.setHinting (Paint.HINTING_OFF); - - canvas.drawText (chars, 0, chars.length, originX, originY, paint); - paint.setAntiAlias (false); - } - - @Override - public Rect - getRect () - { - Rect rect; - - rect = new Rect (); - - fontObject.typeface.typefacePaint.setTextSize (fontObject.pixelSize); - fontObject.typeface.typefacePaint.getTextBounds (chars, 0, chars.length, - rect); - - /* Add the background rect to the damage as well. */ - rect.union (this.rect); - - return rect; - } - }; - private String[] fontFamilyList; private Sdk7Typeface[] typefaceList; private Sdk7Typeface fallbackTypeface; @@ -498,14 +411,11 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver Paint paint, Rect bounds) { char[] text; - float[] width; text = new char[1]; text[0] = (char) code; paint.getTextBounds (text, 0, 1, bounds); - width = new float[1]; - paint.getTextWidths (text, 0, 1, width); /* bounds is the bounding box of the glyph corresponding to CODE. Translate these into XCharStruct values. @@ -526,7 +436,7 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver metrics.rbearing = (short) bounds.right; metrics.ascent = (short) -bounds.top; metrics.descent = (short) bounds.bottom; - metrics.width = (short) Math.round (width[0]); + metrics.width = (short) paint.measureText ("" + text[0]); } @Override @@ -563,7 +473,8 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver for (i = 0; i < code.length; ++i) text[i] = (char) code[i]; - paintCache.getTextBounds (text, 0, 1, boundsCache); + paintCache.getTextBounds (text, 0, code.length, + boundsCache); width = paintCache.measureText (text, 0, code.length); fontMetrics.lbearing = (short) boundsCache.left; @@ -587,11 +498,12 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver int[] chars, int x, int y, int backgroundWidth, boolean withBackground) { - Rect backgroundRect; + Rect backgroundRect, bounds; Sdk7FontObject sdk7FontObject; - Sdk7DrawString op; char[] charsArray; int i; + Canvas canvas; + Paint paint; sdk7FontObject = (Sdk7FontObject) fontObject; charsArray = new char[chars.length]; @@ -605,12 +517,41 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver backgroundRect.right = x + backgroundWidth; backgroundRect.bottom = y + sdk7FontObject.descent; - op = new Sdk7DrawString (sdk7FontObject, charsArray, - gc.immutableGC (), drawable, - withBackground, - backgroundRect, x, y); + canvas = drawable.lockCanvas (); + + if (canvas == null) + return 0; + + canvas.save (); + paint = gc.gcPaint; + + if (gc.real_clip_rects != null) + { + for (i = 0; i < gc.real_clip_rects.length; ++i) + canvas.clipRect (gc.real_clip_rects[i]); + } + + paint.setStyle (Paint.Style.FILL); + + if (withBackground) + { + paint.setColor (gc.background | 0xff000000); + canvas.drawRect (backgroundRect, paint); + paint.setColor (gc.foreground | 0xff000000); + } - EmacsService.SERVICE.appendPaintOperation (op); + paint.setTextSize (sdk7FontObject.pixelSize); + paint.setTypeface (sdk7FontObject.typeface.typeface); + paint.setAntiAlias (true); + canvas.drawText (charsArray, 0, chars.length, x, y, paint); + + canvas.restore (); + bounds = new Rect (); + paint.getTextBounds (charsArray, 0, chars.length, bounds); + bounds.offset (x, y); + bounds.union (backgroundRect); + drawable.damageRect (bounds); + paint.setAntiAlias (false); return 1; } }; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 41a45b0bd85..4444b7f1c56 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -19,7 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.lang.Runnable; import java.io.IOException; import java.util.List; import java.util.ArrayList; @@ -59,7 +58,6 @@ public class EmacsService extends Service private EmacsThread thread; private Handler handler; - private EmacsPaintQueue paintQueue; /* Display metrics used by font backends. */ public DisplayMetrics metrics; @@ -191,126 +189,42 @@ public class EmacsService extends Service return view.thing; } - /* X drawing operations. These are quite primitive operations. The - drawing queue is kept on the Emacs thread, but is periodically - flushed to the application thread, upon buffers swaps and once it - gets too big. */ - - - - private void - ensurePaintQueue () - { - if (paintQueue == null) - paintQueue = new EmacsPaintQueue (); - } - - public void - flushPaintQueue () - { - final EmacsPaintQueue queue; - - if (paintQueue == null) - return; - - if (paintQueue.numRequests < 1) - /* No requests to flush. */ - return; - - queue = paintQueue; - - handler.post (new Runnable () { - @Override - public void - run () - { - queue.run (); - } - }); - - /* Clear the paint queue. */ - paintQueue = null; - } - - private void - checkFlush () - { - if (paintQueue != null - && paintQueue.numRequests > MAX_PENDING_REQUESTS) - flushPaintQueue (); - } - public void fillRectangle (EmacsDrawable drawable, EmacsGC gc, int x, int y, int width, int height) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsFillRectangle (drawable, x, y, - width, height, - gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsFillRectangle.perform (drawable, gc, x, y, + width, height); } public void fillPolygon (EmacsDrawable drawable, EmacsGC gc, Point points[]) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsFillPolygon (drawable, points, - gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsFillPolygon.perform (drawable, gc, points); } public void drawRectangle (EmacsDrawable drawable, EmacsGC gc, int x, int y, int width, int height) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsDrawRectangle (drawable, x, y, - width, height, - gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsDrawRectangle.perform (drawable, gc, x, y, + width, height); } public void drawLine (EmacsDrawable drawable, EmacsGC gc, int x, int y, int x2, int y2) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsDrawLine (drawable, x, y, - x2, y2, - gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsDrawLine.perform (drawable, gc, x, y, + x2, y2); } public void drawPoint (EmacsDrawable drawable, EmacsGC gc, int x, int y) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsDrawPoint (drawable, x, y, - gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsDrawPoint.perform (drawable, gc, x, y); } public void @@ -319,15 +233,9 @@ public class EmacsService extends Service int srcX, int srcY, int width, int height, int destX, int destY) { - EmacsPaintReq req; - - ensurePaintQueue (); - - req = new EmacsCopyArea (srcDrawable, dstDrawable, - srcX, srcY, width, height, destX, - destY, gc.immutableGC ()); - paintQueue.appendPaintOperation (req); - checkFlush (); + EmacsCopyArea.perform (srcDrawable, gc, dstDrawable, + srcX, srcY, width, height, destX, + destY); } public void @@ -342,12 +250,4 @@ public class EmacsService extends Service { window.clearArea (x, y, width, height); } - - public void - appendPaintOperation (EmacsPaintReq op) - { - ensurePaintQueue (); - paintQueue.appendPaintOperation (op); - checkFlush (); - } }; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index b8b828e4820..5efb8882263 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -29,6 +29,7 @@ import android.graphics.Rect; public class EmacsSurfaceView extends SurfaceView { + public Object surfaceChangeLock; private boolean created; public @@ -36,33 +37,36 @@ public class EmacsSurfaceView extends SurfaceView { super (view.getContext ()); + surfaceChangeLock = new Object (); + getHolder ().addCallback (new SurfaceHolder.Callback () { @Override public void surfaceChanged (SurfaceHolder holder, int format, int width, int height) { - /* Force a buffer swap now to get the contents of the Emacs - view on screen. */ - view.swapBuffers (true); + view.swapBuffers (); } @Override public void surfaceCreated (SurfaceHolder holder) { - created = true; - - /* Force a buffer swap now to get the contents of the Emacs - view on screen. */ - view.swapBuffers (true); + synchronized (surfaceChangeLock) + { + created = true; + view.swapBuffers (); + } } @Override public void surfaceDestroyed (SurfaceHolder holder) { - created = false; + synchronized (surfaceChangeLock) + { + created = false; + } } }); } diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 7b48eaf0aa6..169a1e42ee3 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -23,6 +23,7 @@ import android.content.res.ColorStateList; import android.view.View; import android.view.KeyEvent; +import android.view.MotionEvent; import android.view.ViewGroup; import android.graphics.Bitmap; @@ -64,6 +65,14 @@ public class EmacsView extends ViewGroup /* The associated surface view. */ private EmacsSurfaceView surfaceView; + /* Whether or not a configure event must be sent for the next layout + event regardless of what changed. */ + public boolean mustReportLayout; + + /* If non-null, whether or not bitmaps must be recreated upon the + next call to getBitmap. */ + private Rect bitmapDirty; + public EmacsView (EmacsWindow window) { @@ -81,6 +90,43 @@ public class EmacsView extends ViewGroup addView (this.surfaceView); } + private void + handleDirtyBitmap () + { + /* Recreate the front and back buffer bitmaps. */ + bitmap + = Bitmap.createBitmap (bitmapDirty.width (), + bitmapDirty.height (), + Bitmap.Config.ARGB_8888); + bitmap.eraseColor (0xffffffff); + + /* And canvases. */ + canvas = new Canvas (bitmap); + + /* If Emacs is drawing to the bitmap right now from the + main thread, the image contents are lost until the next + ConfigureNotify and complete garbage. Sorry! */ + bitmapDirty = null; + } + + public synchronized Bitmap + getBitmap () + { + if (bitmapDirty != null) + handleDirtyBitmap (); + + return bitmap; + } + + public synchronized Canvas + getCanvas () + { + if (bitmapDirty != null) + handleDirtyBitmap (); + + return canvas; + } + @Override protected void onMeasure (int widthMeasureSpec, int heightMeasureSpec) @@ -114,7 +160,7 @@ public class EmacsView extends ViewGroup } @Override - protected void + protected synchronized void onLayout (boolean changed, int left, int top, int right, int bottom) { @@ -122,19 +168,15 @@ public class EmacsView extends ViewGroup View child; Rect windowRect; - if (changed) + if (changed || mustReportLayout) { + mustReportLayout = false; window.viewLayout (left, top, right, bottom); - - /* Recreate the front and back buffer bitmaps. */ - bitmap - = Bitmap.createBitmap (right - left, bottom - top, - Bitmap.Config.ARGB_8888); - - /* And canvases. */ - canvas = new Canvas (bitmap); } + if (changed) + bitmapDirty = new Rect (left, top, right, bottom); + count = getChildCount (); for (i = 0; i < count; ++i) @@ -159,47 +201,60 @@ public class EmacsView extends ViewGroup } } - public void + public synchronized void damageRect (Rect damageRect) { damageRegion.union (damageRect); } - public void + /* This method is called from both the UI thread and the Emacs + thread. */ + + public synchronized void swapBuffers (boolean force) { - Bitmap back; Canvas canvas; Rect damageRect; + Bitmap bitmap; if (damageRegion.isEmpty ()) return; - if (!surfaceView.isCreated ()) - return; + bitmap = getBitmap (); - if (bitmap == null) - return; + /* Emacs must take the following lock to ensure the access to the + canvas occurs with the surface created. Otherwise, Android + will throttle calls to lockCanvas. */ + + synchronized (surfaceView.surfaceChangeLock) + { + damageRect = damageRegion.getBounds (); - /* Lock the canvas with the specified damage. */ - damageRect = damageRegion.getBounds (); - canvas = surfaceView.lockCanvas (damageRect); + if (!surfaceView.isCreated ()) + return; - /* Return if locking the canvas failed. */ - if (canvas == null) - return; + if (bitmap == null) + return; + + /* Lock the canvas with the specified damage. */ + canvas = surfaceView.lockCanvas (damageRect); - /* Copy from the back buffer to the canvas. If damageRect was - made empty, then draw the entire back buffer. */ + /* Return if locking the canvas failed. */ + if (canvas == null) + return; - if (damageRect.isEmpty ()) - canvas.drawBitmap (bitmap, 0f, 0f, paint); - else - canvas.drawBitmap (bitmap, damageRect, damageRect, paint); + /* Copy from the back buffer to the canvas. If damageRect was + made empty, then draw the entire back buffer. */ - /* Unlock the canvas and clear the damage. */ - surfaceView.unlockCanvasAndPost (canvas); - damageRegion.setEmpty (); + if (damageRect.isEmpty ()) + canvas.drawBitmap (bitmap, 0f, 0f, paint); + else + canvas.drawBitmap (bitmap, damageRect, damageRect, paint); + + /* Unlock the canvas and clear the damage. */ + surfaceView.unlockCanvasAndPost (canvas); + damageRegion.setEmpty (); + } } public void @@ -241,4 +296,18 @@ public class EmacsView extends ViewGroup super.onFocusChanged (gainFocus, direction, previouslyFocusedRect); } + + @Override + public boolean + onGenericMotionEvent (MotionEvent motion) + { + return window.onSomeKindOfMotionEvent (motion); + } + + @Override + public boolean + onTouchEvent (MotionEvent motion) + { + return window.onSomeKindOfMotionEvent (motion); + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 26e788a20a8..a9a24b61869 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -31,8 +31,13 @@ import android.graphics.Point; import android.view.View; import android.view.ViewGroup; import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.InputDevice; import android.content.Intent; +import android.util.Log; + +import android.os.Build; /* This defines a window, which is a handle. Windows represent a rectangular subset of the screen with their own contents. @@ -68,6 +73,10 @@ public class EmacsWindow extends EmacsHandleObject window background. */ private EmacsGC scratchGC; + /* The button state and keyboard modifier mask at the time of the + last button press or release event. */ + private int lastButtonState, lastModifiers; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, int width, int height) @@ -212,6 +221,7 @@ public class EmacsWindow extends EmacsHandleObject public void run () { + view.mustReportLayout = true; view.requestLayout (); } }); @@ -224,6 +234,8 @@ public class EmacsWindow extends EmacsHandleObject { rect.right = rect.left + width; rect.bottom = rect.top + height; + + requestViewLayout (); } } @@ -279,17 +291,7 @@ public class EmacsWindow extends EmacsHandleObject public Canvas lockCanvas () { - if (view.canvas != null) - return view.canvas; - - return null; - } - - @Override - public void - unlockCanvas () - { - + return view.getCanvas (); } @Override @@ -302,17 +304,7 @@ public class EmacsWindow extends EmacsHandleObject public void swapBuffers () { - /* Before calling swapBuffers, make sure to flush the paint - queue. */ - EmacsService.SERVICE.flushPaintQueue (); - view.post (new Runnable () { - @Override - public void - run () - { - view.swapBuffers (); - } - }); + view.swapBuffers (); } public void @@ -337,7 +329,7 @@ public class EmacsWindow extends EmacsHandleObject public Bitmap getBitmap () { - return view.bitmap; + return view.getBitmap (); } public void @@ -357,6 +349,7 @@ public class EmacsWindow extends EmacsHandleObject recognized as an ASCII key press event. */ event.getUnicodeChar (state)); + lastModifiers = event.getModifiers (); } public void @@ -372,6 +365,7 @@ public class EmacsWindow extends EmacsHandleObject event.getModifiers (), keyCode, event.getUnicodeChar (state)); + lastModifiers = event.getModifiers (); } public void @@ -386,4 +380,105 @@ public class EmacsWindow extends EmacsHandleObject /* Destroy the associated frame when the activity is detached. */ EmacsNative.sendWindowAction (this.handle, 0); } + + /* Look through the button state to determine what button EVENT was + generated from. DOWN is true if EVENT is a button press event, + false otherwise. Value is the X number of the button. */ + + private int + whatButtonWasIt (MotionEvent event, boolean down) + { + int eventState, notIn; + + if (Build.VERSION.SDK_INT + < Build.VERSION_CODES.ICE_CREAM_SANDWICH) + /* Earlier versions of Android only support one mouse + button. */ + return 1; + + eventState = event.getButtonState (); + notIn = (down ? eventState & ~lastButtonState + : lastButtonState & ~eventState); + + if ((notIn & MotionEvent.BUTTON_PRIMARY) != 0) + return 1; + + if ((notIn & MotionEvent.BUTTON_SECONDARY) != 0) + return 3; + + if ((notIn & MotionEvent.BUTTON_TERTIARY) != 0) + return 2; + + /* Not a real value. */ + return 4; + } + + public boolean + onSomeKindOfMotionEvent (MotionEvent event) + { + if (!event.isFromSource (InputDevice.SOURCE_CLASS_POINTER)) + return false; + + switch (event.getAction ()) + { + case MotionEvent.ACTION_HOVER_ENTER: + EmacsNative.sendEnterNotify (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime ()); + return true; + + case MotionEvent.ACTION_MOVE: + case MotionEvent.ACTION_HOVER_MOVE: + EmacsNative.sendMotionNotify (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime ()); + return true; + + case MotionEvent.ACTION_HOVER_EXIT: + EmacsNative.sendLeaveNotify (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime ()); + return true; + + case MotionEvent.ACTION_BUTTON_PRESS: + /* Find the button which was pressed. */ + EmacsNative.sendButtonPress (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime (), + lastModifiers, + whatButtonWasIt (event, true)); + + if (Build.VERSION.SDK_INT + < Build.VERSION_CODES.ICE_CREAM_SANDWICH) + return true; + + lastButtonState = event.getButtonState (); + return true; + + case MotionEvent.ACTION_BUTTON_RELEASE: + /* Find the button which was released. */ + EmacsNative.sendButtonRelease (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime (), + lastModifiers, + whatButtonWasIt (event, false)); + + if (Build.VERSION.SDK_INT + < Build.VERSION_CODES.ICE_CREAM_SANDWICH) + return true; + + lastButtonState = event.getButtonState (); + return true; + + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_UP: + /* Emacs must return true even though touch events are not yet + handled, because the value of this function is used by the + system to decide whether or not Emacs gets ACTION_MOVE + events. */ + return true; + } + + return false; + } }; -- cgit v1.3 From f9732131cf3c67e24db74a3d49f256d3c189a7e3 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 13 Jan 2023 15:53:08 +0800 Subject: Update Android port * configure.ac (ANDROID_MIN_SDK): New variable. (DX): Remove and replace with D8. (XCONFIGURE): Check for the minimum version of Android the cross compiler compiles for. Generate java/AndroidManifest.xml from java/AndroidManifest.xml.in. Allow using Zlib on Android. * java/AndroidManifest.xml.in: New file. Use the minimum SDK detected by configure. * java/Makefile.in (top_srcdir, version): New variables. (DX, D8): Replace with D8. (ANDROID_MIN_SDK, APK_NAME): New variables. (.PHONY): (.PRECIOUS): (classes.dex): (emacs.apk): Generate $(APK_NAME) instead of `emacs.apk'. * java/debug.sh: New option --attach-existing. Attach to an existing Emacs instance when specified. * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): New field `isPaused'. (invalidateFocus1): Fix infinite recursion. (detachWindow): Deiconify window. (attachWindow): Iconify the window if the activity is paused. (onCreate): Use the ``no title bar'' theme. (onPause, onResume): New functions. * java/org/gnu/emacs/EmacsNative.java (sendTouchUp, sendTouchDown) (sendTouchMove, sendWheel, sendIconified, sendDeiconified): New functions. * java/org/gnu/emacs/EmacsSdk7FontDriver.java (Sdk7Typeface): (list): Remove logging for code that is mostly going to be unused. * java/org/gnu/emacs/EmacsService.java (ringBell, queryTree) (getScreenWidth, getScreenHeight, detectMouse): New functions. * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView) (surfaceChanged, surfaceCreated, surfaceDestroyed): Add extra debug logging. Avoid deadlock in surfaceCreated. * java/org/gnu/emacs/EmacsView.java (EmacsView): Try very hard to make the SurfaceView respect Z order. It didn't work. (handleDirtyBitmap): Copy over the contents from the old bitmap. (explicitlyDirtyBitmap): New function. (onLayout): Don't dirty bitmap if unnecessary. (damageRect, swapBuffers): Don't synchronize so hard. (onTouchEvent): Call window.onTouchEvent instead. (moveChildToBack, raise, lower): New functions. * java/org/gnu/emacs/EmacsWindow.java (Coordinate): New subclass. (pointerMap, isMapped, isIconified, dontFocusOnMap) (dontAcceptFocus): New fields. (EmacsWindow): Don't immediately register unmapped window. (viewLayout): Send configure event outside the lock. (requestViewLayout): Explicitly dirty the bitmap. (mapWindow): Register the window now. Respect dontFocusOnMap. (unmapWindow): Unregister the window now. (figureChange, onTouchEvent): New functions. (onSomeKindOfMotionEvent): Handle scroll wheel events. (reparentTo, makeInputFocus, raise, lower, getWindowGeometry) (noticeIconified, noticeDeiconified, setDontAcceptFocus) (setDontFocusOnMap, getDontFocusOnMap): New functions. * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (registerWindow, detachWindow): Synchronize. (noticeIconified, noticeDeiconified): New functions. (copyWindows): New function. * lisp/frame.el (frame-geometry, frame-edges) (mouse-absolute-pixel-position, set-mouse-absolute-pixel-position) (frame-list-z-order, frame-restack, display-mouse-p) (display-monitor-attributes-list): Implement on Android. * lisp/mwheel.el (mouse-wheel-down-event): (mouse-wheel-up-event): (mouse-wheel-left-event): (mouse-wheel-right-event): Define on Android. * src/android.c (struct android_emacs_service): New methods `ringBell', `queryTree', `getScreenWidth', `getScreenHeight', and `detectMouse'. (struct android_event_queue, android_init_events) (android_next_event, android_write_event): Remove write limit. (android_file_access_p): Handle directories correcty. (android_close): Fix coding style. (android_fclose): New function. (android_init_emacs_service): Initialize new methods. (android_reparent_window): Implement function. (android_bell, android_set_input_focus, android_raise_window) (android_lower_window, android_query_tree, android_get_geometry) (android_get_screen_width, android_get_screen_height) (android_get_mm_width, android_get_mm_height, android_detect_mouse) (android_set_dont_focus_on_map, android_set_dont_accept_focus): New functions. (struct android_dir): New structure. (android_opendir, android_readdir, android_closedir): New functions. (emacs_abort): Implement here on Android and poke debuggerd into generating a tombstone. * src/android.h: Update prototypes. * src/androidfns.c (android_set_parent_frame): New function. (android_default_font_parameter): Use sane font size by default. (Fx_display_pixel_width, Fx_display_pixel_height) (Fx_display_mm_width, Fx_display_mm_height) (Fx_display_monitor_attributes_list): Rename to start with `android-'. Implement. Fiddle with documentation to introduce Android specific nuances. (Fandroid_display_monitor_attributes_list): New function. (Fx_frame_geometry, frame_geometry): New function. (Fandroid_frame_geometry): Implement correctly. (Fx_frame_list_z_order): Rename to start with `android-'. (android_frame_list_z_order, Fandroid_frame_list_z_order): Implement. (Fx_frame_restack): Rename to start with `android-'. (Fandroid_frame_restack): ``Implement''. (Fx_mouse_absolute_pixel_position): Rename to start with `android-'. (Fandroid_mouse_absolute_pixel_position): ``Implement''. (Fx_set_mouse_absolute_pixel_position): Rename to start with `android-'. (Fandroid_set_mouse_absolute_pixel_position): ``Implement''. (Fandroid_detect_mouse): New function. (android_set_menu_bar_lines): Use FRAME_ANDROID_DRAWABLE when clearing area. (android_set_no_focus_on_map, android_set_no_accept_focus): New functions. (android_frame_parm_handlers): Register new frame parameter handlers. (syms_of_androidfns): Update appropriately. * src/androidfont.c (androidfont_draw): Use FRAME_ANDROID_DRAWABLE instead of FRAME_ANDROID_WINDOW. * src/androidgui.h (enum android_event_type): New events. (struct android_touch_event, struct android_wheel_event) (struct android_iconify_event): New structures. (union android_event): Add new events. * src/androidterm.c (android_clear_frame): Use FRAME_ANDROID_DRAWABLE instead of FRAME_ANDROID_WINDOW. (android_flash, android_ring_bell): Implement bell ringing. (android_toggle_invisible_pointer): Don't TODO function that can't be implemented. (show_back_buffer, android_flush_dirty_back_buffer_on): Check if a buffer flip is required before doing the flip. (android_lower_frame, android_raise_frame): Implement functions. (android_update_tools, android_find_tool): New functions. (handle_one_android_event): Handle new iconification, wheel and touch events. (android_read_socket): Implement pending-autoraise-frames. (android_frame_up_to_date): Implement bell ringing. (android_buffer_flipping_unblocked_hook): Check if a buffer flip is required before doing the flip. (android_focus_frame, android_frame_highlight) (android_frame_unhighlight): New function. (android_frame_rehighlight): Implement functions. (android_iconify_frame): Always display error. (android_set_alpha): Update commentary. (android_free_frame_resources): Free frame touch points. (android_scroll_run, android_flip_and_flush) (android_clear_rectangle, android_draw_fringe_bitmap) (android_draw_glyph_string_background, android_fill_triangle) (android_clear_point, android_draw_relief_rect) (android_draw_box_rect, android_draw_glyph_string_bg_rect) (android_draw_image_foreground, android_draw_stretch_glyph_string) (android_draw_underwave, android_draw_glyph_string_foreground) (android_draw_composite_glyph_string_foreground) (android_draw_glyphless_glyph_string_foreground) (android_draw_glyph_string, android_clear_frame_area) (android_clear_under_internal_border, android_draw_hollow_cursor) (android_draw_bar_cursor, android_draw_vertical_window_border) (android_draw_window_divider): Use FRAME_ANDROID_DRAWABLE instead of FRAME_ANDROID_WINDOW for drawing operations. * src/androidterm.h (struct android_touch_point): New structure. (struct android_output): New fields. (FRAME_ANDROID_NEED_BUFFER_FLIP): New macro. * src/dired.c (emacs_readdir, open_directory) (directory_files_internal_unwind, read_dirent) (directory_files_internal, file_name_completion): Add indirection over readdir and opendir. Use android variants on Android. * src/dispnew.c (Fopen_termscript): * src/fileio.c (fclose_unwind): Use emacs_fclose. (Faccess_file): Call android_file_access_p. (file_accessible_directory_p): Append right suffix to Android assets directory. (do_auto_save_unwind): Use emacs_fclose. * src/keyboard.c (lispy_function_keys): Use right function key for page up and page down. (Fopen_dribble_file): Use emacs_fclose. * src/lisp.h: New prototype emacs_fclose. * src/lread.c (close_infile_unwind): Use emacs_fclose. * src/sfnt.c (sfnt_curve_is_flat): Fix area-squared computation. (sfnt_prepare_raster): Compute raster width and height consistently with outline building. (sfnt_build_outline_edges): Use the same offsets used to set offy and offx. (main): Adjust debug code. * src/sfntfont-android.c (sfntfont_android_saturate32): Delete function. (sfntfont_android_blend, sfntfont_android_blendrgb): Remove unnecessary debug code. (sfntfont_android_composite_bitmap): Prevent out of bounds write. (sfntfont_android_put_glyphs): Use FRAME_ANDROID_DRAWABLE. (init_sfntfont_android): Initialize Monospace Serif font to something sensible. * src/sfntfont.c (sfntfont_text_extents): Clear glyph metrics before summing up pcm. (sfntfont_draw): Use s->font instead of s->face->font. * src/sysdep.c (emacs_fclose): Wrap around android_fclose on android. * src/term.c (Fsuspend_tty): (delete_tty): Use emacs_fclose. * src/verbose.mk.in (AM_V_DX): Replace with D8 version. --- configure.ac | 35 +- java/AndroidManifest.xml.in | 84 +++ java/Makefile.in | 30 +- java/debug.sh | 45 +- java/org/gnu/emacs/EmacsActivity.java | 37 +- java/org/gnu/emacs/EmacsNative.java | 25 +- java/org/gnu/emacs/EmacsSdk7FontDriver.java | 7 - java/org/gnu/emacs/EmacsService.java | 116 ++++ java/org/gnu/emacs/EmacsSurfaceView.java | 22 +- java/org/gnu/emacs/EmacsView.java | 189 +++++-- java/org/gnu/emacs/EmacsWindow.java | 498 +++++++++++++++-- .../gnu/emacs/EmacsWindowAttachmentManager.java | 54 +- lisp/frame.el | 32 +- lisp/mwheel.el | 12 +- src/android.c | 551 ++++++++++++++++++- src/android.h | 23 + src/androidfns.c | 471 ++++++++++++++-- src/androidfont.c | 8 +- src/androidgui.h | 83 +++ src/androidterm.c | 589 ++++++++++++++++++--- src/androidterm.h | 28 + src/dired.c | 50 +- src/dispnew.c | 3 +- src/fileio.c | 42 +- src/keyboard.c | 6 +- src/lisp.h | 2 + src/lread.c | 2 +- src/sfnt.c | 35 +- src/sfntfont-android.c | 41 +- src/sfntfont.c | 5 +- src/sysdep.c | 17 +- src/term.c | 10 +- src/verbose.mk.in | 2 +- 33 files changed, 2809 insertions(+), 345 deletions(-) create mode 100644 java/AndroidManifest.xml.in (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/configure.ac b/configure.ac index 68de5f02b97..333a28cc142 100644 --- a/configure.ac +++ b/configure.ac @@ -816,8 +816,8 @@ a valid path to android.jar. See config.log for more details.]) Please verify that the path to the SDK build tools you specified is correct]) fi - AC_PATH_PROGS([DX], [dx d8], [], "${SDK_BUILD_TOOLS}:$PATH") - if test "DX" = ""; then + AC_PATH_PROGS([D8], [d8], [], "${SDK_BUILD_TOOLS}:$PATH") + if test "D8" = ""; then AC_MSG_ERROR([The Android dexer was not found. Please verify that the path to the SDK build tools you specified is correct]) fi @@ -867,6 +867,25 @@ in the ANDROID_CC variable when you ran configure.]) ANDROID_ABI=$android_abi + dnl Obtain the minimum SDK version of the resulting Emacs binary + dnl built with this NDK. + + ANDROID_MIN_SDK=8 + AC_MSG_CHECKING([for the lowest Android version Emacs can run on]) + [android_sdk=`echo "$cc_target" | grep -oE 'android([0-9][0-9]?)'`] + + if test -n "$android_sdk"; then + android_sdk=`echo "$android_sdk" | sed -n 's/android//p'` + AC_MSG_RESULT([$android_sdk]) + ANDROID_MIN_SDK=$android_sdk + else + AC_MSG_RESULT([unknown ($cc_target); assuming 8]) + AC_MSG_WARN([configure could not determine the versions of Android \ +a binary built with this compiler will run on. The generated application \ +package will likely install on older systems but crash on startup.]) + fi + AC_SUBST([ANDROID_MIN_SDK]) + # Save confdefs.h and config.log for now. mv -f confdefs.h _confdefs.h mv -f config.log _config.log @@ -899,7 +918,7 @@ fi AC_SUBST([ANDROID]) AC_SUBST([JAVAC]) AC_SUBST([AAPT]) -AC_SUBST([DX]) +AC_SUBST([D8]) AC_SUBST([ZIPALIGN]) AC_SUBST([ANDROID_JAR]) AC_SUBST([ANDROID_ABI]) @@ -914,7 +933,7 @@ fi AC_SUBST([XCONFIGURE]) if test "$ANDROID" = "yes"; then - # When --with-android is specified, all build options must be + # When --with-android is specified, almost all build options must be # disabled, both within the recursive invocation of configure and # outside. with_xpm=no @@ -938,11 +957,12 @@ if test "$ANDROID" = "yes"; then with_gpm=no with_dbus=no with_gsettings=no - with_selinx=no + with_selinux=no with_gnutls=no - with_zlib=no with_modules=no with_threads=no + + # zlib is available in android. fi dnl This used to use changequote, but, apart from 'changequote is evil' @@ -7211,6 +7231,9 @@ fi ARCH_INDEPENDENT_CONFIG_FILES([java/Makefile]) ARCH_INDEPENDENT_CONFIG_FILES([xcompile/Makefile]) +# Make java/AndroidManifest.xml +ARCH_INDEPENDENT_CONFIG_FILES([java/AndroidManifest.xml]) + AC_OUTPUT if test ! "$with_mailutils"; then diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in new file mode 100644 index 00000000000..b680137a9d0 --- /dev/null +++ b/java/AndroidManifest.xml.in @@ -0,0 +1,84 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/java/Makefile.in b/java/Makefile.in index 31bd22b5a2e..05e61dede89 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -18,13 +18,15 @@ # along with GNU Emacs. If not, see . top_builddir = @top_builddir@ +top_srcdir = @top_srcdir@ +version = @version@ -include ${top_builddir}/src/verbose.mk SHELL = @SHELL@ JAVAC = @JAVAC@ AAPT = @AAPT@ -DX = @DX@ +D8 = @D8@ ZIPALIGN = @ZIPALIGN@ JARSIGNER = @JARSIGNER@ ANDROID_JAR = @ANDROID_JAR@ @@ -39,6 +41,12 @@ SIGN_EMACS = -keystore emacs.keystore -storepass emacs1 JAVA_FILES = $(shell find . -type f -name *.java) CLASS_FILES = $(foreach file,$(JAVA_FILES),$(basename $(file)).class) +# Compute the name for the Emacs application package. This should be: +# emacs---.apk + +ANDROID_MIN_SDK = @ANDROID_MIN_SDK@ +APK_NAME = emacs-$(version)-$(ANDROID_MIN_SDK)-$(ANDROID_ABI).apk + # How this stuff works. # emacs.apk depends on emacs.apk-in, which is simply a ZIP archive @@ -55,7 +63,7 @@ CLASS_FILES = $(foreach file,$(JAVA_FILES),$(basename $(file)).class) # assets/lisp/ .PHONY: emacs.apk-in all -all: emacs.apk +all: $(APK_NAME) # Binaries to cross-compile. CROSS_BINS = ../xcompile/src/android-emacs ../xcompile/lib-src/ctags \ @@ -118,6 +126,18 @@ emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) AndroidManifest.xml pushd install_temp; $(AAPT) add ../$@ `find assets -type f`; popd rm -rf install_temp +# Makefile itself. +.PRECIOUS: ../config.status Makefile +../config.status: $(top_srcdir)/configure.ac $(top_srcdir)/m4/*.m4 + $(MAKE) -C $(dir $@) $(notdir $@) +Makefile: ../config.status $(top_builddir)/java/Makefile.in + $(MAKE) -C .. java/$@ + +# AndroidManifest.xml: +AndroidManifest.xml: $(top_srcdir)/configure.ac $(top_srcdir)/m4/*.m4 \ + AndroidManifest.xml.in + pushd ..; ./config.status java/AndroidManifest.xml; popd + .SUFFIXES: .java .class .java.class &: $(AM_V_JAVAC) $(JAVAC) $(JAVAFLAGS) $< @@ -126,7 +146,7 @@ emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) AndroidManifest.xml # nested classes. classes.dex: $(CLASS_FILES) - $(AM_V_DX) $(DX) --classpath $(ANDROID_JAR) \ + $(AM_V_D8) $(D8) --classpath $(ANDROID_JAR) \ $(subst $$,\$$,$(shell find . -type f -name *.class)) # When emacs.keystore expires, regenerate it with: @@ -136,7 +156,7 @@ classes.dex: $(CLASS_FILES) .PHONY: clean maintainer-clean -emacs.apk: classes.dex emacs.apk-in emacs.keystore +$(APK_NAME): classes.dex emacs.apk-in emacs.keystore cp -f emacs.apk-in $@.unaligned $(AAPT) add $@.unaligned classes.dex $(JARSIGNER) $(SIGN_EMACS) $@.unaligned "Emacs keystore" @@ -144,7 +164,7 @@ emacs.apk: classes.dex emacs.apk-in emacs.keystore rm -f $@.unaligned clean: - rm -f emacs.apk emacs.apk-in *.dex *.unaligned *.class + rm -f *.apk emacs.apk-in *.dex *.unaligned *.class rm -rf install-temp find . -name '*.class' -delete diff --git a/java/debug.sh b/java/debug.sh index dd710dc31af..3e3e3d9c281 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -30,6 +30,7 @@ activity=org.gnu.emacs.EmacsActivity gdb_port=5039 jdb_port=64013 jdb=no +attach_existing=no while [ $# -gt 0 ]; do case "$1" in @@ -48,6 +49,7 @@ while [ $# -gt 0 ]; do echo " --port PORT run the GDB server on a specific port" echo " --jdb-port PORT run the JDB server on a specific port" echo " --jdb run JDB instead of GDB" + echo " --attach-existing attach to an existing process" echo " --help print this message" echo "" echo "Available devices:" @@ -63,6 +65,9 @@ while [ $# -gt 0 ]; do "--port" ) gdb_port=$1 ;; + "--attach-existing" ) + attach_existing=yes + ;; "--" ) shift gdbargs=$@ @@ -120,30 +125,32 @@ package_pids=`awk -- '{ print $1 }' <<< $package_pids` -# Finally, kill each existing process. -for pid in $package_pids; do - echo "Killing existing process $pid..." - adb -s $device shell run-as $package kill -9 $pid &> /dev/null -done - -# Now run the main activity. This must be done as the adb user and -# not as the package user. -echo "Starting activity $activity and attaching debugger" - -# Exit if the activity could not be started. -adb -s $device shell am start -D "$package/$activity" -if [ ! $? ]; then - exit 1; -fi +if [ "$attach_existing" != "yes" ]; then + # Finally, kill each existing process. + for pid in $package_pids; do + echo "Killing existing process $pid..." + adb -s $device shell run-as $package kill -9 $pid &> /dev/null + done + + # Now run the main activity. This must be done as the adb user and + # not as the package user. + echo "Starting activity $activity and attaching debugger" + + # Exit if the activity could not be started. + adb -s $device shell am start -D "$package/$activity" + if [ ! $? ]; then + exit 1; + fi -# Now look for processes matching the package again. -package_pids=`adb -s $device shell run-as $package ps -u $package_uid -o PID,CMD` + # Now look for processes matching the package again. + package_pids=`adb -s $device shell run-as $package ps -u $package_uid -o PID,CMD` -# Next, remove lines matching "ps" itself. -package_pids=`awk -- '{ + # Next, remove lines matching "ps" itself. + package_pids=`awk -- '{ if (!match ($0, /(PID|ps)/)) print $1 }' <<< $package_pids` +fi pid=$package_pids num_pids=`wc -w <<< "$package_pids"` diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 67f411a38c3..2b661024842 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -48,6 +48,9 @@ public class EmacsActivity extends Activity /* The currently focused window. */ public static EmacsWindow focusedWindow; + /* Whether or not this activity is paused. */ + private boolean isPaused; + static { focusedActivities = new ArrayList (); @@ -60,7 +63,7 @@ public class EmacsActivity extends Activity focusedWindow = window; for (EmacsWindow child : window.children) - invalidateFocus1 (window); + invalidateFocus1 (child); } public static void @@ -103,6 +106,9 @@ public class EmacsActivity extends Activity /* Clear the window's pointer to this activity and remove the window's view. */ window.setConsumer (null); + + /* The window can't be iconified any longer. */ + window.noticeDeiconified (); layout.removeView (window.view); window = null; @@ -114,6 +120,8 @@ public class EmacsActivity extends Activity public void attachWindow (EmacsWindow child) { + Log.d (TAG, "attachWindow: " + child); + if (window != null) throw new IllegalStateException ("trying to attach window when one" + " already exists"); @@ -124,6 +132,10 @@ public class EmacsActivity extends Activity layout.addView (window.view); child.setConsumer (this); + /* If the activity is iconified, send that to the window. */ + if (isPaused) + window.noticeIconified (); + /* Invalidate the focus. */ invalidateFocus (); } @@ -148,6 +160,9 @@ public class EmacsActivity extends Activity { FrameLayout.LayoutParams params; + /* Set the theme to one without a title bar. */ + setTheme (android.R.style.Theme_NoTitleBar); + params = new FrameLayout.LayoutParams (LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); @@ -192,4 +207,24 @@ public class EmacsActivity extends Activity invalidateFocus (); } + + @Override + public void + onPause () + { + isPaused = true; + + EmacsWindowAttachmentManager.MANAGER.noticeIconified (this); + super.onResume (); + } + + @Override + public void + onResume () + { + isPaused = false; + + EmacsWindowAttachmentManager.MANAGER.noticeDeiconified (this); + super.onResume (); + } }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index ae48ce30408..a11e509cd7f 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -96,11 +96,34 @@ public class EmacsNative long time, int state, int button); - /* Send an ANDROID_BUTTON_RELEASE event. */ + /* Send an ANDROID_BUTTON_RELEASE event. */ public static native void sendButtonRelease (short window, int x, int y, long time, int state, int button); + /* Send an ANDROID_TOUCH_DOWN event. */ + public static native void sendTouchDown (short window, int x, int y, + long time, int pointerID); + + /* Send an ANDROID_TOUCH_UP event. */ + public static native void sendTouchUp (short window, int x, int y, + long time, int pointerID); + + /* Send an ANDROID_TOUCH_MOVE event. */ + public static native void sendTouchMove (short window, int x, int y, + long time, int pointerID); + + /* Send an ANDROID_WHEEL event. */ + public static native void sendWheel (short window, int x, int y, + long time, int state, + float xDelta, float yDelta); + + /* Send an ANDROID_ICONIFIED event. */ + public static native void sendIconified (short window); + + /* Send an ANDROID_DEICONIFIED event. */ + public static native void sendDeiconified (short window); + static { System.loadLibrary ("emacs"); diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index 8a9426050ae..c0f24c7433a 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -149,8 +149,6 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver } else familyName = fileName; - - Log.d (TAG, "Initialized new typeface " + familyName); } @Override @@ -321,17 +319,12 @@ public class EmacsSdk7FontDriver extends EmacsFontDriver list = new LinkedList (); - Log.d (TAG, ("Looking for fonts matching font spec: " - + fontSpec.toString ())); - for (i = 0; i < typefaceList.length; ++i) { if (checkMatch (typefaceList[i], fontSpec)) list.add (new Sdk7FontEntity (typefaceList[i])); } - Log.d (TAG, "Found font entities: " + list.toString ()); - return (FontEntity[]) list.toArray (new FontEntity[0]); } diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 4444b7f1c56..01a1695f385 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -28,20 +28,27 @@ import android.graphics.Bitmap; import android.graphics.Point; import android.view.View; +import android.view.InputDevice; import android.annotation.TargetApi; import android.app.Service; import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; + import android.os.Build; import android.os.Looper; import android.os.IBinder; import android.os.Handler; +import android.os.Vibrator; +import android.os.VibratorManager; +import android.os.VibrationEffect; import android.util.Log; import android.util.DisplayMetrics; +import android.hardware.input.InputManager; + class Holder { T thing; @@ -250,4 +257,113 @@ public class EmacsService extends Service { window.clearArea (x, y, width, height); } + + @SuppressWarnings ("deprecation") + public void + ringBell () + { + Vibrator vibrator; + VibrationEffect effect; + VibratorManager vibratorManager; + Object tem; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) + { + tem = getSystemService (Context.VIBRATOR_MANAGER_SERVICE); + vibratorManager = (VibratorManager) tem; + vibrator = vibratorManager.getDefaultVibrator (); + } + else + vibrator + = (Vibrator) getSystemService (Context.VIBRATOR_SERVICE); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + effect + = VibrationEffect.createOneShot (50, + VibrationEffect.DEFAULT_AMPLITUDE); + vibrator.vibrate (effect); + } + else + vibrator.vibrate (50); + } + + public short[] + queryTree (EmacsWindow window) + { + short[] array; + List windowList; + int i; + + if (window == null) + /* Just return all the windows without a parent. */ + windowList = EmacsWindowAttachmentManager.MANAGER.copyWindows (); + else + windowList = window.children; + + array = new short[windowList.size () + 1]; + i = 1; + + array[0] = window.parent != null ? 0 : window.parent.handle; + + for (EmacsWindow treeWindow : windowList) + array[i++] = treeWindow.handle; + + return array; + } + + public int + getScreenWidth (boolean mmWise) + { + DisplayMetrics metrics; + + metrics = getResources ().getDisplayMetrics (); + + if (!mmWise) + return metrics.widthPixels; + else + return (int) ((metrics.widthPixels / metrics.xdpi) * 2540.0); + } + + public int + getScreenHeight (boolean mmWise) + { + DisplayMetrics metrics; + + metrics = getResources ().getDisplayMetrics (); + + if (!mmWise) + return metrics.heightPixels; + else + return (int) ((metrics.heightPixels / metrics.ydpi) * 2540.0); + } + + public boolean + detectMouse () + { + InputManager manager; + InputDevice device; + int[] ids; + int i; + + if (Build.VERSION.SDK_INT + < Build.VERSION_CODES.JELLY_BEAN) + return false; + + manager = (InputManager) getSystemService (Context.INPUT_SERVICE); + ids = manager.getInputDeviceIds (); + + for (i = 0; i < ids.length; ++i) + { + device = manager.getInputDevice (ids[i]); + + if (device == null) + continue; + + if (device.supportsSource (InputDevice.SOURCE_MOUSE)) + return true; + } + + return false; + } }; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 5efb8882263..f713818d4bc 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -27,8 +27,12 @@ import android.os.Build; import android.graphics.Canvas; import android.graphics.Rect; +import android.util.Log; + public class EmacsSurfaceView extends SurfaceView { + private static final String TAG = "EmacsSurfaceView"; + public Object surfaceChangeLock; private boolean created; @@ -45,6 +49,7 @@ public class EmacsSurfaceView extends SurfaceView surfaceChanged (SurfaceHolder holder, int format, int width, int height) { + Log.d (TAG, "surfaceChanged: " + view); view.swapBuffers (); } @@ -54,9 +59,13 @@ public class EmacsSurfaceView extends SurfaceView { synchronized (surfaceChangeLock) { + Log.d (TAG, "surfaceCreated: " + view); created = true; - view.swapBuffers (); } + + /* Drop the lock when doing this, or a deadlock can + result. */ + view.swapBuffers (); } @Override @@ -65,6 +74,7 @@ public class EmacsSurfaceView extends SurfaceView { synchronized (surfaceChangeLock) { + Log.d (TAG, "surfaceDestroyed: " + view); created = false; } } @@ -93,6 +103,16 @@ public class EmacsSurfaceView extends SurfaceView return holder.lockCanvas (damage); } + @Override + protected void + onLayout (boolean changed, int left, int top, int right, + int bottom) + { + Log.d (TAG, ("onLayout: " + left + " " + top + " " + right + + " " + bottom + " -- " + changed + " visibility " + + getVisibility ())); + } + /* This method is only used during debugging when it seems damage isn't working correctly. */ diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 169a1e42ee3..41acabab97b 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -87,12 +87,27 @@ public class EmacsView extends ViewGroup /* Create the surface view. */ this.surfaceView = new EmacsSurfaceView (this); + this.surfaceView.setZOrderMediaOverlay (true); addView (this.surfaceView); + + /* Not sure exactly what this does but it makes things magically + work. Why is something as simple as XRaiseWindow so involved + on Android? */ + setChildrenDrawingOrderEnabled (true); + + /* Get rid of the foreground and background tint. */ + setBackgroundTintList (null); + setForegroundTintList (null); } private void handleDirtyBitmap () { + Bitmap oldBitmap; + + /* Save the old bitmap. */ + oldBitmap = bitmap; + /* Recreate the front and back buffer bitmaps. */ bitmap = Bitmap.createBitmap (bitmapDirty.width (), @@ -103,12 +118,23 @@ public class EmacsView extends ViewGroup /* And canvases. */ canvas = new Canvas (bitmap); - /* If Emacs is drawing to the bitmap right now from the - main thread, the image contents are lost until the next - ConfigureNotify and complete garbage. Sorry! */ + /* Copy over the contents of the old bitmap. */ + if (oldBitmap != null) + canvas.drawBitmap (oldBitmap, 0f, 0f, new Paint ()); + bitmapDirty = null; } + public synchronized void + explicitlyDirtyBitmap (Rect rect) + { + if (bitmapDirty == null + && (bitmap == null + || rect.width () != bitmap.getWidth () + || rect.height () != bitmap.getHeight ())) + bitmapDirty = rect; + } + public synchronized Bitmap getBitmap () { @@ -168,25 +194,31 @@ public class EmacsView extends ViewGroup View child; Rect windowRect; + count = getChildCount (); + if (changed || mustReportLayout) { mustReportLayout = false; window.viewLayout (left, top, right, bottom); } - if (changed) + if (changed + /* Check that a change has really happened. */ + && (bitmapDirty == null + || bitmapDirty.width () != right - left + || bitmapDirty.height () != bottom - top)) bitmapDirty = new Rect (left, top, right, bottom); - count = getChildCount (); - for (i = 0; i < count; ++i) { child = getChildAt (i); + Log.d (TAG, "onLayout: " + child); + if (child == surfaceView) /* The child is the surface view, so give it the entire view. */ - child.layout (left, top, right, bottom); + child.layout (0, 0, right - left, bottom - top); else if (child.getVisibility () != GONE) { if (!(child instanceof EmacsView)) @@ -201,59 +233,68 @@ public class EmacsView extends ViewGroup } } - public synchronized void + public void damageRect (Rect damageRect) { - damageRegion.union (damageRect); + synchronized (damageRegion) + { + damageRegion.union (damageRect); + } } /* This method is called from both the UI thread and the Emacs thread. */ - public synchronized void + public void swapBuffers (boolean force) { Canvas canvas; Rect damageRect; Bitmap bitmap; - if (damageRegion.isEmpty ()) - return; + /* Code must always take damageRegion, and then surfaceChangeLock, + never the other way around! */ - bitmap = getBitmap (); + synchronized (damageRegion) + { + if (damageRegion.isEmpty ()) + return; - /* Emacs must take the following lock to ensure the access to the - canvas occurs with the surface created. Otherwise, Android - will throttle calls to lockCanvas. */ + bitmap = getBitmap (); - synchronized (surfaceView.surfaceChangeLock) - { - damageRect = damageRegion.getBounds (); + /* Emacs must take the following lock to ensure the access to the + canvas occurs with the surface created. Otherwise, Android + will throttle calls to lockCanvas. */ - if (!surfaceView.isCreated ()) - return; + synchronized (surfaceView.surfaceChangeLock) + { + damageRect = damageRegion.getBounds (); - if (bitmap == null) - return; + if (!surfaceView.isCreated ()) + return; - /* Lock the canvas with the specified damage. */ - canvas = surfaceView.lockCanvas (damageRect); + if (bitmap == null) + return; - /* Return if locking the canvas failed. */ - if (canvas == null) - return; + /* Lock the canvas with the specified damage. */ + canvas = surfaceView.lockCanvas (damageRect); - /* Copy from the back buffer to the canvas. If damageRect was - made empty, then draw the entire back buffer. */ + /* Return if locking the canvas failed. */ + if (canvas == null) + return; - if (damageRect.isEmpty ()) - canvas.drawBitmap (bitmap, 0f, 0f, paint); - else - canvas.drawBitmap (bitmap, damageRect, damageRect, paint); + /* Copy from the back buffer to the canvas. If damageRect was + made empty, then draw the entire back buffer. */ - /* Unlock the canvas and clear the damage. */ - surfaceView.unlockCanvasAndPost (canvas); - damageRegion.setEmpty (); + if (damageRect.isEmpty ()) + canvas.drawBitmap (bitmap, 0f, 0f, paint); + else + canvas.drawBitmap (bitmap, damageRect, damageRect, paint); + + /* Unlock the canvas and clear the damage. */ + surfaceView.unlockCanvasAndPost (canvas); + damageRegion.setEmpty (); + } } } @@ -308,6 +349,78 @@ public class EmacsView extends ViewGroup public boolean onTouchEvent (MotionEvent motion) { - return window.onSomeKindOfMotionEvent (motion); + return window.onTouchEvent (motion); + } + + private void + moveChildToBack (View child) + { + int index; + + index = indexOfChild (child); + + if (index > 0) + { + detachViewFromParent (index); + + /* The view at 0 is the surface view. */ + attachViewToParent (child, 1, + child.getLayoutParams()); + } + } + + + /* The following two functions must not be called if the view has no + parent, or is parented to an activity. */ + + public void + raise () + { + EmacsView parent; + + parent = (EmacsView) getParent (); + + Log.d (TAG, "raise: parent " + parent); + + if (parent.indexOfChild (this) + == parent.getChildCount () - 1) + return; + + parent.bringChildToFront (this); + + /* Yes, all of this is really necessary! */ + parent.requestLayout (); + parent.invalidate (); + requestLayout (); + invalidate (); + + /* The surface view must be destroyed and recreated. */ + removeView (surfaceView); + addView (surfaceView, 0); + } + + public void + lower () + { + EmacsView parent; + + parent = (EmacsView) getParent (); + + Log.d (TAG, "lower: parent " + parent); + + if (parent.indexOfChild (this) == 1) + return; + + parent.moveChildToBack (this); + + /* Yes, all of this is really necessary! */ + parent.requestLayout (); + parent.invalidate (); + requestLayout (); + invalidate (); + + /* The surface view must be removed and attached again. */ + removeView (surfaceView); + addView (surfaceView, 0); } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index a9a24b61869..1f8596dba50 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -22,6 +22,7 @@ package org.gnu.emacs; import java.lang.IllegalStateException; import java.util.ArrayList; import java.util.List; +import java.util.HashMap; import android.graphics.Rect; import android.graphics.Canvas; @@ -50,9 +51,29 @@ import android.os.Build; Views are also drawables, meaning they can accept drawing requests. */ +/* Help wanted. What does not work includes `EmacsView.raise', + `EmacsView.lower', reparenting a window onto another window. + + All three are likely undocumented restrictions within + EmacsSurface. */ + public class EmacsWindow extends EmacsHandleObject implements EmacsDrawable { + private static final String TAG = "EmacsWindow"; + + private class Coordinate + { + /* Integral coordinate. */ + int x, y; + + Coordinate (int x, int y) + { + this.x = x; + this.y = y; + } + }; + /* The view associated with the window. */ public EmacsView view; @@ -60,12 +81,16 @@ public class EmacsWindow extends EmacsHandleObject private Rect rect; /* The parent window, or null if it is the root window. */ - private EmacsWindow parent; + public EmacsWindow parent; /* List of all children in stacking order. This must be kept consistent! */ public ArrayList children; + /* Map between pointer identifiers and last known position. Used to + compute which pointer changed upon a touch event. */ + private HashMap pointerMap; + /* The window consumer currently attached, if it exists. */ private EmacsWindowAttachmentManager.WindowConsumer attached; @@ -77,6 +102,14 @@ public class EmacsWindow extends EmacsHandleObject last button press or release event. */ private int lastButtonState, lastModifiers; + /* Whether or not the window is mapped, and whether or not it is + deiconified. */ + private boolean isMapped, isIconified; + + /* Whether or not to ask for focus upon being mapped, and whether or + not the window should be focusable. */ + private boolean dontFocusOnMap, dontAcceptFocus; + public EmacsWindow (short handle, final EmacsWindow parent, int x, int y, int width, int height) @@ -84,6 +117,7 @@ public class EmacsWindow extends EmacsHandleObject super (handle); rect = new Rect (x, y, x + width, y + height); + pointerMap = new HashMap (); /* Create the view from the context's UI thread. The window is unmapped, so the view is GONE. */ @@ -97,7 +131,7 @@ public class EmacsWindow extends EmacsHandleObject if (parent != null) { parent.children.add (this); - parent.view.post (new Runnable () { + EmacsService.SERVICE.runOnUiThread (new Runnable () { @Override public void run () @@ -106,23 +140,6 @@ public class EmacsWindow extends EmacsHandleObject } }); } - else - EmacsService.SERVICE.runOnUiThread (new Runnable () { - @Override - public void - run () - { - EmacsWindowAttachmentManager manager; - - manager = EmacsWindowAttachmentManager.MANAGER; - - /* If parent is the root window, notice that there are new - children available for interested activites to pick - up. */ - - manager.registerWindow (EmacsWindow.this); - } - }); scratchGC = new EmacsGC ((short) 0); } @@ -159,7 +176,7 @@ public class EmacsWindow extends EmacsHandleObject + "children!"); /* Remove the view from its parent and make it invisible. */ - view.post (new Runnable () { + EmacsService.SERVICE.runOnUiThread (new Runnable () { public void run () { @@ -174,7 +191,7 @@ public class EmacsWindow extends EmacsHandleObject parent = (View) view.getParent (); - if (parent != null && attached == null) + if (parent != null) ((ViewGroup) parent).removeView (view); manager.detachWindow (EmacsWindow.this); @@ -199,24 +216,33 @@ public class EmacsWindow extends EmacsHandleObject public void viewLayout (int left, int top, int right, int bottom) { + int rectWidth, rectHeight; + synchronized (this) { rect.left = left; rect.top = top; rect.right = right; rect.bottom = bottom; - - EmacsNative.sendConfigureNotify (this.handle, - System.currentTimeMillis (), - left, top, rect.width (), - rect.height ()); } + + rectWidth = right - left; + rectHeight = bottom - top; + + EmacsNative.sendConfigureNotify (this.handle, + System.currentTimeMillis (), + left, top, rectWidth, + rectHeight); } public void requestViewLayout () { - view.post (new Runnable () { + /* This is necessary because otherwise subsequent drawing on the + Emacs thread may be lost. */ + view.explicitlyDirtyBitmap (rect); + + EmacsService.SERVICE.runOnUiThread (new Runnable () { @Override public void run () @@ -261,28 +287,77 @@ public class EmacsWindow extends EmacsHandleObject public void mapWindow () { - view.post (new Runnable () { - @Override - public void - run () - { + if (isMapped) + return; - view.setVisibility (View.VISIBLE); - /* Eventually this should check no-focus-on-map. */ - view.requestFocus (); - } - }); + isMapped = true; + + if (parent == null) + { + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + EmacsWindowAttachmentManager manager; + + /* Make the view visible, first of all. */ + view.setVisibility (View.VISIBLE); + + manager = EmacsWindowAttachmentManager.MANAGER; + + /* If parent is the root window, notice that there are new + children available for interested activites to pick + up. */ + manager.registerWindow (EmacsWindow.this); + + if (!getDontFocusOnMap ()) + /* Eventually this should check no-focus-on-map. */ + view.requestFocus (); + } + }); + } + else + { + /* Do the same thing as above, but don't register this + window. */ + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.setVisibility (View.VISIBLE); + + if (!getDontFocusOnMap ()) + /* Eventually this should check no-focus-on-map. */ + view.requestFocus (); + } + }); + } } public void unmapWindow () { + if (!isMapped) + return; + + isMapped = false; + view.post (new Runnable () { @Override public void run () { + EmacsWindowAttachmentManager manager; + + manager = EmacsWindowAttachmentManager.MANAGER; + view.setVisibility (View.GONE); + + /* Now that the window is unmapped, unregister it as + well. */ + manager.detachWindow (EmacsWindow.this); } }); } @@ -413,6 +488,161 @@ public class EmacsWindow extends EmacsHandleObject return 4; } + /* Return the ID of the pointer which changed in EVENT. Value is -1 + if it could not be determined, else the pointer that changed, or + -2 if -1 would have been returned, but there is also a pointer + that is a mouse. */ + + private int + figureChange (MotionEvent event) + { + int pointerID, i, truncatedX, truncatedY, pointerIndex; + Coordinate coordinate; + boolean mouseFlag; + + /* pointerID is always initialized but the Java compiler is too + dumb to know that. */ + pointerID = -1; + mouseFlag = false; + + switch (event.getActionMasked ()) + { + case MotionEvent.ACTION_DOWN: + /* Primary pointer pressed with index 0. */ + + /* Detect mice. If this is a mouse event, give it to + onSomeKindOfMotionEvent. */ + if ((Build.VERSION.SDK_INT + >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + && event.getToolType (0) == MotionEvent.TOOL_TYPE_MOUSE) + return -2; + + pointerID = event.getPointerId (0); + pointerMap.put (pointerID, + new Coordinate ((int) event.getX (0), + (int) event.getY (0))); + break; + + case MotionEvent.ACTION_UP: + /* Primary pointer released with index 0. */ + pointerID = event.getPointerId (0); + pointerMap.remove (pointerID); + break; + + case MotionEvent.ACTION_POINTER_DOWN: + /* New pointer. Find the pointer ID from the index and place + it in the map. */ + pointerIndex = event.getActionIndex (); + pointerID = event.getPointerId (pointerIndex); + pointerMap.put (pointerID, + new Coordinate ((int) event.getX (pointerID), + (int) event.getY (pointerID))); + break; + + case MotionEvent.ACTION_POINTER_UP: + /* Pointer removed. Remove it from the map. */ + pointerIndex = event.getActionIndex (); + pointerID = event.getPointerId (pointerIndex); + pointerMap.remove (pointerID); + break; + + default: + + /* Loop through each pointer in the event. */ + for (i = 0; i < event.getPointerCount (); ++i) + { + pointerID = event.getPointerId (i); + + /* Look up that pointer in the map. */ + coordinate = pointerMap.get (pointerID); + + if (coordinate != null) + { + /* See if coordinates have changed. */ + truncatedX = (int) event.getX (i); + truncatedY = (int) event.getY (i); + + if (truncatedX != coordinate.x + || truncatedY != coordinate.y) + { + /* The pointer changed. Update the coordinate and + break out of the loop. */ + coordinate.x = truncatedX; + coordinate.y = truncatedY; + + break; + } + } + + /* See if this is a mouse. If so, set the mouseFlag. */ + if ((Build.VERSION.SDK_INT + >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + && event.getToolType (i) == MotionEvent.TOOL_TYPE_MOUSE) + mouseFlag = true; + } + + /* Set the pointer ID to -1 if the loop failed to find any + changed pointer. If a mouse pointer was found, set it to + -2. */ + if (i == event.getPointerCount ()) + pointerID = (mouseFlag ? -2 : -1); + } + + /* Return the pointer ID. */ + return pointerID; + } + + public boolean + onTouchEvent (MotionEvent event) + { + int pointerID, index; + + /* Extract the ``touch ID'' (or in Android, the ``pointer + ID''.) */ + pointerID = figureChange (event); + + if (pointerID < 0) + { + /* If this is a mouse event, give it to + onSomeKindOfMotionEvent. */ + if (pointerID == -2) + return onSomeKindOfMotionEvent (event); + + return false; + } + + /* Find the pointer index corresponding to the event. */ + index = event.findPointerIndex (pointerID); + + switch (event.getActionMasked ()) + { + case MotionEvent.ACTION_DOWN: + case MotionEvent.ACTION_POINTER_DOWN: + /* Touch down event. */ + EmacsNative.sendTouchDown (this.handle, (int) event.getX (index), + (int) event.getY (index), + event.getEventTime (), pointerID); + return true; + + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_POINTER_UP: + /* Touch up event. */ + EmacsNative.sendTouchUp (this.handle, (int) event.getX (index), + (int) event.getY (index), + event.getEventTime (), pointerID); + return true; + + case MotionEvent.ACTION_MOVE: + /* Pointer motion event. */ + EmacsNative.sendTouchMove (this.handle, (int) event.getX (index), + (int) event.getY (index), + event.getEventTime (), pointerID); + return true; + } + + return false; + } + public boolean onSomeKindOfMotionEvent (MotionEvent event) { @@ -472,13 +702,201 @@ public class EmacsWindow extends EmacsHandleObject case MotionEvent.ACTION_DOWN: case MotionEvent.ACTION_UP: - /* Emacs must return true even though touch events are not yet - handled, because the value of this function is used by the - system to decide whether or not Emacs gets ACTION_MOVE + /* Emacs must return true even though touch events are not + handled here, because the value of this function is used by + the system to decide whether or not Emacs gets ACTION_MOVE events. */ return true; + + case MotionEvent.ACTION_SCROLL: + /* Send a scroll event with the specified deltas. */ + EmacsNative.sendWheel (this.handle, (int) event.getX (), + (int) event.getY (), + event.getEventTime (), + lastModifiers, + event.getAxisValue (MotionEvent.AXIS_HSCROLL), + event.getAxisValue (MotionEvent.AXIS_VSCROLL)); + return true; } return false; } + + public void + reparentTo (final EmacsWindow otherWindow, int x, int y) + { + int width, height; + + /* Reparent this window to the other window. */ + + if (parent != null) + parent.children.remove (this); + + if (otherWindow != null) + otherWindow.children.add (this); + + parent = otherWindow; + + /* Move this window to the new location. */ + synchronized (this) + { + width = rect.width (); + height = rect.height (); + rect.left = x; + rect.top = y; + rect.right = x + width; + rect.bottom = y + height; + } + + /* Now do the work necessary on the UI thread to reparent the + window. */ + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + EmacsWindowAttachmentManager manager; + View parent; + + /* First, detach this window if necessary. */ + manager = EmacsWindowAttachmentManager.MANAGER; + manager.detachWindow (EmacsWindow.this); + + /* Also unparent this view. */ + parent = (View) view.getParent (); + + if (parent != null) + ((ViewGroup) parent).removeView (view); + + /* Next, either add this window as a child of the new + parent's view, or make it available again. */ + if (otherWindow != null) + otherWindow.view.addView (view); + else if (EmacsWindow.this.isMapped) + manager.registerWindow (EmacsWindow.this); + + /* Request relayout. */ + view.requestLayout (); + } + }); + } + + public void + makeInputFocus (long time) + { + /* TIME is currently ignored. Request the input focus now. */ + + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.requestFocus (); + } + }); + } + + public void + raise () + { + /* This does nothing here. */ + if (parent == null) + return; + + /* Remove and add this view again. */ + parent.children.remove (this); + parent.children.add (this); + + /* Request a relayout. */ + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.raise (); + } + }); + } + + public void + lower () + { + /* This does nothing here. */ + if (parent == null) + return; + + /* Remove and add this view again. */ + parent.children.remove (this); + parent.children.add (this); + + /* Request a relayout. */ + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.lower (); + } + }); + } + + public int[] + getWindowGeometry () + { + int[] array; + Rect rect; + + array = new int[4]; + rect = getGeometry (); + + array[0] = rect.left; + array[1] = rect.top; + array[2] = rect.width (); + array[3] = rect.height (); + + return array; + } + + public void + noticeIconified () + { + isIconified = true; + EmacsNative.sendIconified (this.handle); + } + + public void + noticeDeiconified () + { + isIconified = false; + EmacsNative.sendDeiconified (this.handle); + } + + public synchronized void + setDontAcceptFocus (final boolean dontAcceptFocus) + { + this.dontAcceptFocus = dontAcceptFocus; + + /* Update the view's focus state. */ + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.setFocusable (!dontAcceptFocus); + view.setFocusableInTouchMode (!dontAcceptFocus); + } + }); + } + + public synchronized void + setDontFocusOnMap (final boolean dontFocusOnMap) + { + this.dontFocusOnMap = dontFocusOnMap; + } + + public synchronized boolean + getDontFocusOnMap () + { + return dontFocusOnMap; + } }; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 34be2ab8789..15eb3bb65c2 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -19,6 +19,7 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import java.util.ArrayList; import java.util.LinkedList; import java.util.List; @@ -68,7 +69,7 @@ public class EmacsWindowAttachmentManager }; private List consumers; - private List windows; + public List windows; public EmacsWindowAttachmentManager () @@ -98,12 +99,19 @@ public class EmacsWindowAttachmentManager EmacsNative.sendWindowAction ((short) 0, 0); } - public void + public synchronized void registerWindow (EmacsWindow window) { Intent intent; - Log.d (TAG, "registerWindow " + window); + Log.d (TAG, "registerWindow (maybe): " + window); + + if (windows.contains (window)) + /* The window is already registered. */ + return; + + Log.d (TAG, "registerWindow: " + window); + windows.add (window); for (WindowConsumer consumer : consumers) @@ -146,7 +154,7 @@ public class EmacsWindowAttachmentManager consumers.remove (consumer); } - public void + public synchronized void detachWindow (EmacsWindow window) { WindowConsumer consumer; @@ -162,5 +170,43 @@ public class EmacsWindowAttachmentManager consumers.remove (consumer); consumer.destroy (); } + + windows.remove (window); + } + + public void + noticeIconified (WindowConsumer consumer) + { + EmacsWindow window; + + Log.d (TAG, "noticeIconified " + consumer); + + /* If a window is attached, send the appropriate iconification + events. */ + window = consumer.getAttachedWindow (); + + if (window != null) + window.noticeIconified (); + } + + public void + noticeDeiconified (WindowConsumer consumer) + { + EmacsWindow window; + + Log.d (TAG, "noticeDeiconified " + consumer); + + /* If a window is attached, send the appropriate iconification + events. */ + window = consumer.getAttachedWindow (); + + if (window != null) + window.noticeDeiconified (); + } + + public synchronized List + copyWindows () + { + return new ArrayList (windows); } }; diff --git a/lisp/frame.el b/lisp/frame.el index 436323f7d58..d35df71a6cc 100644 --- a/lisp/frame.el +++ b/lisp/frame.el @@ -1647,6 +1647,7 @@ live frame and defaults to the selected one." (declare-function ns-frame-geometry "nsfns.m" (&optional frame)) (declare-function pgtk-frame-geometry "pgtkfns.c" (&optional frame)) (declare-function haiku-frame-geometry "haikufns.c" (&optional frame)) +(declare-function android-frame-geometry "androidfns.c" (&optional frame)) (defun frame-geometry (&optional frame) "Return geometric attributes of FRAME. @@ -1700,6 +1701,8 @@ and width values are in pixels. (pgtk-frame-geometry frame)) ((eq frame-type 'haiku) (haiku-frame-geometry frame)) + ((eq frame-type 'android) + (android-frame-geometry frame)) (t (list '(outer-position 0 . 0) @@ -1826,6 +1829,7 @@ of frames like calls to map a frame or change its visibility." (declare-function ns-frame-edges "nsfns.m" (&optional frame type)) (declare-function pgtk-frame-edges "pgtkfns.c" (&optional frame type)) (declare-function haiku-frame-edges "haikufns.c" (&optional frame type)) +(declare-function android-frame-edges "androidfns.c" (&optional frame type)) (defun frame-edges (&optional frame type) "Return coordinates of FRAME's edges. @@ -1853,6 +1857,8 @@ FRAME." (pgtk-frame-edges frame type)) ((eq frame-type 'haiku) (haiku-frame-edges frame type)) + ((eq frame-type 'android) + (android-frame-edges frame type)) (t (list 0 0 (frame-width frame) (frame-height frame)))))) @@ -1861,6 +1867,7 @@ FRAME." (declare-function ns-mouse-absolute-pixel-position "nsfns.m") (declare-function pgtk-mouse-absolute-pixel-position "pgtkfns.c") (declare-function haiku-mouse-absolute-pixel-position "haikufns.c") +(declare-function android-mouse-absolute-pixel-position "androidfns.c") (defun mouse-absolute-pixel-position () "Return absolute position of mouse cursor in pixels. @@ -1879,6 +1886,8 @@ position (0, 0) of the selected frame's terminal." (pgtk-mouse-absolute-pixel-position)) ((eq frame-type 'haiku) (haiku-mouse-absolute-pixel-position)) + ((eq frame-type 'android) + (android-mouse-absolute-pixel-position)) (t (cons 0 0))))) @@ -1887,6 +1896,8 @@ position (0, 0) of the selected frame's terminal." (declare-function w32-set-mouse-absolute-pixel-position "w32fns.c" (x y)) (declare-function x-set-mouse-absolute-pixel-position "xfns.c" (x y)) (declare-function haiku-set-mouse-absolute-pixel-position "haikufns.c" (x y)) +(declare-function android-set-mouse-absolute-pixel-position + "androidfns.c" (x y)) (defun set-mouse-absolute-pixel-position (x y) "Move mouse pointer to absolute pixel position (X, Y). @@ -1903,7 +1914,9 @@ position (0, 0) of the selected frame's terminal." ((eq frame-type 'w32) (w32-set-mouse-absolute-pixel-position x y)) ((eq frame-type 'haiku) - (haiku-set-mouse-absolute-pixel-position x y))))) + (haiku-set-mouse-absolute-pixel-position x y)) + ((eq frame-type 'android) + (android-set-mouse-absolute-pixel-position x y))))) (defun frame-monitor-attributes (&optional frame) "Return the attributes of the physical monitor dominating FRAME. @@ -1999,6 +2012,7 @@ workarea attribute." ;; TODO: implement this on PGTK. ;; (declare-function pgtk-frame-list-z-order "pgtkfns.c" (&optional display)) (declare-function haiku-frame-list-z-order "haikufns.c" (&optional display)) +(declare-function android-frame-list-z-order "androidfns.c" (&optional display)) (defun frame-list-z-order (&optional display) "Return list of Emacs' frames, in Z (stacking) order. @@ -2024,13 +2038,17 @@ Return nil if DISPLAY contains no Emacs frame." ;; (pgtk-frame-list-z-order display) nil) ((eq frame-type 'haiku) - (haiku-frame-list-z-order display))))) + (haiku-frame-list-z-order display)) + ((eq frame-type 'android) + (android-frame-list-z-order display))))) (declare-function x-frame-restack "xfns.c" (frame1 frame2 &optional above)) (declare-function w32-frame-restack "w32fns.c" (frame1 frame2 &optional above)) (declare-function ns-frame-restack "nsfns.m" (frame1 frame2 &optional above)) (declare-function pgtk-frame-restack "pgtkfns.c" (frame1 frame2 &optional above)) (declare-function haiku-frame-restack "haikufns.c" (frame1 frame2 &optional above)) +(declare-function android-frame-restack "androidfns.c" (frame1 frame2 + &optional above)) (defun frame-restack (frame1 frame2 &optional above) "Restack FRAME1 below FRAME2. @@ -2064,7 +2082,9 @@ Some window managers may refuse to restack windows." ((eq frame-type 'haiku) (haiku-frame-restack frame1 frame2 above)) ((eq frame-type 'pgtk) - (pgtk-frame-restack frame1 frame2 above)))) + (pgtk-frame-restack frame1 frame2 above)) + ((eq frame-type 'android) + (android-frame-restack frame1 frame2 above)))) (error "Cannot restack frames"))) (defun frame-size-changed-p (&optional frame) @@ -2113,6 +2133,8 @@ frame's display)." (> w32-num-mouse-buttons 0))) ((memq frame-type '(x ns haiku pgtk)) t) ;; We assume X, NeXTstep, GTK, and Haiku *always* have a pointing device + ((eq frame-type 'android) + (android-detect-mouse)) (t (or (and (featurep 'xt-mouse) xterm-mouse-mode) @@ -2396,6 +2418,8 @@ If DISPLAY is omitted or nil, it defaults to the selected frame's display." (&optional terminal)) (declare-function haiku-display-monitor-attributes-list "haikufns.c" (&optional terminal)) +(declare-function android-display-monitor-attributes-list "androidfns.c" + (&optional terminal)) (defun display-monitor-attributes-list (&optional display) "Return a list of physical monitor attributes on DISPLAY. @@ -2449,6 +2473,8 @@ monitors." (pgtk-display-monitor-attributes-list display)) ((eq frame-type 'haiku) (haiku-display-monitor-attributes-list display)) + ((eq frame-type 'android) + (android-display-monitor-attributes-list display)) (t (let ((geometry (list 0 0 (display-pixel-width display) (display-pixel-height display)))) diff --git a/lisp/mwheel.el b/lisp/mwheel.el index 1be52d24e34..67d93c3558d 100644 --- a/lisp/mwheel.el +++ b/lisp/mwheel.el @@ -58,7 +58,8 @@ (defcustom mouse-wheel-down-event (if (or (featurep 'w32-win) (featurep 'ns-win) - (featurep 'haiku-win) (featurep 'pgtk-win)) + (featurep 'haiku-win) (featurep 'pgtk-win) + (featurep 'android-win)) 'wheel-up 'mouse-4) "Event used for scrolling down." @@ -79,7 +80,8 @@ (defcustom mouse-wheel-up-event (if (or (featurep 'w32-win) (featurep 'ns-win) - (featurep 'haiku-win) (featurep 'pgtk-win)) + (featurep 'haiku-win) (featurep 'pgtk-win) + (featurep 'android-win)) 'wheel-down 'mouse-5) "Event used for scrolling up." @@ -254,7 +256,8 @@ Also see `mouse-wheel-tilt-scroll'." (defvar mouse-wheel-left-event (if (or (featurep 'w32-win) (featurep 'ns-win) - (featurep 'haiku-win) (featurep 'pgtk-win)) + (featurep 'haiku-win) (featurep 'pgtk-win) + (featurep 'android-win)) 'wheel-left 'mouse-6) "Event used for scrolling left.") @@ -268,7 +271,8 @@ Also see `mouse-wheel-tilt-scroll'." (defvar mouse-wheel-right-event (if (or (featurep 'w32-win) (featurep 'ns-win) - (featurep 'haiku-win) (featurep 'pgtk-win)) + (featurep 'haiku-win) (featurep 'pgtk-win) + (featurep 'android-win)) 'wheel-right 'mouse-7) "Event used for scrolling right.") diff --git a/src/android.c b/src/android.c index 2f852662001..eab18dc6152 100644 --- a/src/android.c +++ b/src/android.c @@ -82,6 +82,11 @@ struct android_emacs_service jmethodID copy_area; jmethodID clear_window; jmethodID clear_area; + jmethodID ring_bell; + jmethodID query_tree; + jmethodID get_screen_width; + jmethodID get_screen_height; + jmethodID detect_mouse; }; struct android_emacs_pixmap @@ -196,9 +201,6 @@ struct android_event_queue /* The thread used to run select. */ pthread_t select_thread; - /* Condition variable for the writing side. */ - pthread_cond_t write_var; - /* Condition variables for the reading side. */ pthread_cond_t read_var; @@ -314,11 +316,6 @@ android_init_events (void) "pthread_mutex_init: %s", strerror (errno)); - if (pthread_cond_init (&event_queue.write_var, NULL)) - __android_log_print (ANDROID_LOG_FATAL, __func__, - "pthread_cond_init: %s", - strerror (errno)); - if (pthread_cond_init (&event_queue.read_var, NULL)) __android_log_print (ANDROID_LOG_FATAL, __func__, "pthread_cond_init: %s", @@ -387,8 +384,7 @@ android_next_event (union android_event *event_return) /* Free the container. */ free (container); - /* Signal that events can now be written. */ - pthread_cond_signal (&event_queue.write_var); + /* Unlock the queue. */ pthread_mutex_unlock (&event_queue.mutex); } @@ -407,12 +403,6 @@ android_write_event (union android_event *event) return; pthread_mutex_lock (&event_queue.mutex); - - /* The event queue is full, wait for events to be read. */ - if (event_queue.num_events >= 1024) - pthread_cond_wait (&event_queue.write_var, - &event_queue.mutex); - container->next = event_queue.events.next; container->last = &event_queue.events; container->next->last = container; @@ -421,6 +411,10 @@ android_write_event (union android_event *event) event_queue.num_events++; pthread_cond_signal (&event_queue.read_var); pthread_mutex_unlock (&event_queue.mutex); + + /* Now set pending_signals to true. This allows C-g to be handled + immediately even without SIGIO. */ + pending_signals = true; } int @@ -604,31 +598,60 @@ android_file_access_p (const char *name, int amode) { AAsset *asset; AAssetDir *directory; + int length; if (!asset_manager) return false; if (!(amode & W_OK) && (name = android_get_asset_name (name))) { + if (!strcmp (name, "") || !strcmp (name, "/")) + /* /assets always exists. */ + return true; + /* Check if the asset exists by opening it. Suboptimal! */ asset = AAssetManager_open (asset_manager, name, AASSET_MODE_UNKNOWN); if (!asset) { - /* See if it's a directory also. */ + /* See if it's a directory as well. To open a directory + with the asset manager, the trailing slash (if specified) + must be removed. */ directory = AAssetManager_openDir (asset_manager, name); if (directory) { + /* Make sure the directory actually has files in it. */ + + if (!AAssetDir_getNextFileName (directory)) + { + AAssetDir_close (directory); + errno = ENOENT; + return false; + } + AAssetDir_close (directory); return true; } + errno = ENOENT; return false; } AAsset_close (asset); + + /* If NAME is a directory name, but it was a regular file, set + errno to ENOTDIR and return false. This is to behave like + faccessat. */ + + length = strlen (name); + if (name[length - 1] == '/') + { + errno = ENOTDIR; + return false; + } + return true; } @@ -819,12 +842,31 @@ int android_close (int fd) { if (fd < ANDROID_MAX_ASSET_FD - && (android_table[fd].flags & ANDROID_FD_TABLE_ENTRY_IS_VALID)) + && (android_table[fd].flags + & ANDROID_FD_TABLE_ENTRY_IS_VALID)) android_table[fd].flags = 0; return close (fd); } +/* Like fclose. However, remove any information associated with + FILE's file descriptor from the asset table as well. */ + +int +android_fclose (FILE *stream) +{ + int fd; + + fd = fileno (stream); + + if (fd != -1 && fd < ANDROID_MAX_ASSET_FD + && (android_table[fd].flags + & ANDROID_FD_TABLE_ENTRY_IS_VALID)) + android_table[fd].flags = 0; + + return fclose (stream); +} + /* Return the current user's ``home'' directory, which is actually the app data directory on Android. */ @@ -1010,7 +1052,12 @@ android_init_emacs_service (void) "(Lorg/gnu/emacs/EmacsWindow;)V"); FIND_METHOD (clear_area, "clearArea", "(Lorg/gnu/emacs/EmacsWindow;IIII)V"); - + FIND_METHOD (ring_bell, "ringBell", "()V"); + FIND_METHOD (query_tree, "queryTree", + "(Lorg/gnu/emacs/EmacsWindow;)[S"); + FIND_METHOD (get_screen_width, "getScreenWidth", "(Z)I"); + FIND_METHOD (get_screen_height, "getScreenHeight", "(Z)I"); + FIND_METHOD (detect_mouse, "detectMouse", "()Z"); #undef FIND_METHOD } @@ -1342,6 +1389,97 @@ NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, android_write_event (&event); } +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, + jshort window, jint x, jint y, + jlong time, jint pointer_id) +{ + union android_event event; + + event.touch.type = ANDROID_TOUCH_DOWN; + event.touch.window = window; + event.touch.x = x; + event.touch.y = y; + event.touch.time = time; + event.touch.pointer_id = pointer_id; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, + jshort window, jint x, jint y, + jlong time, jint pointer_id) +{ + union android_event event; + + event.touch.type = ANDROID_TOUCH_UP; + event.touch.window = window; + event.touch.x = x; + event.touch.y = y; + event.touch.time = time; + event.touch.pointer_id = pointer_id; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, + jshort window, jint x, jint y, + jlong time, jint pointer_id) +{ + union android_event event; + + event.touch.type = ANDROID_TOUCH_MOVE; + event.touch.window = window; + event.touch.x = x; + event.touch.y = y; + event.touch.time = time; + event.touch.pointer_id = pointer_id; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, + jshort window, jint x, jint y, + jlong time, jint state, + jfloat x_delta, jfloat y_delta) +{ + union android_event event; + + event.wheel.type = ANDROID_WHEEL; + event.wheel.window = window; + event.wheel.x = x; + event.wheel.y = y; + event.wheel.time = time; + event.wheel.state = state; + event.wheel.x_delta = x_delta; + event.wheel.y_delta = y_delta; + + android_write_event (&event); +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendIconified) (JNIEnv *env, jobject object, + jshort window) +{ + union android_event event; + + event.iconified.type = ANDROID_ICONIFIED; + event.iconified.window = window; +} + +extern JNIEXPORT void JNICALL +NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, + jshort window) +{ + union android_event event; + + event.iconified.type = ANDROID_DEICONIFIED; + event.iconified.window = window; +} + #pragma clang diagnostic pop @@ -1979,10 +2117,22 @@ android_set_clip_rectangles (struct android_gc *gc, int clip_x_origin, } void -android_reparent_window (android_window w, android_window parent, +android_reparent_window (android_window w, android_window parent_handle, int x, int y) { - /* TODO */ + jobject window, parent; + jmethodID method; + + window = android_resolve_handle (w, ANDROID_HANDLE_WINDOW); + parent = android_resolve_handle (parent_handle, + ANDROID_HANDLE_WINDOW); + + method = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "reparentTo", + "(Lorg/gnu/emacs/EmacsWindow;II)V"); + (*android_java_env)->CallVoidMethod (android_java_env, window, + method, + parent, (jint) x, (jint) y); } /* Look up the method with SIGNATURE by NAME in CLASS. Abort if it @@ -2904,6 +3054,163 @@ android_put_image (android_pixmap handle, struct android_image *image) ANDROID_DELETE_LOCAL_REF (bitmap); } +void +android_bell (void) +{ + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.ring_bell); +} + +void +android_set_input_focus (android_window handle, unsigned long time) +{ + jobject window; + jmethodID make_input_focus; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + make_input_focus = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "makeInputFocus", "(J)V"); + + (*android_java_env)->CallVoidMethod (android_java_env, window, + make_input_focus, (jlong) time); +} + +void +android_raise_window (android_window handle) +{ + jobject window; + jmethodID raise; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + raise = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "raise", "()V"); + + (*android_java_env)->CallVoidMethod (android_java_env, window, + raise); +} + +void +android_lower_window (android_window handle) +{ + jobject window; + jmethodID lower; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + lower = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "lower", "()V"); + + (*android_java_env)->CallVoidMethod (android_java_env, window, + lower); +} + +int +android_query_tree (android_window handle, android_window *root_return, + android_window *parent_return, + android_window **children_return, + unsigned int *nchildren_return) +{ + jobject window, array; + jsize nelements, i; + android_window *children; + jshort *shorts; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + + /* window can be NULL, so this is a service method. */ + array + = (*android_java_env)->CallObjectMethod (android_java_env, + emacs_service, + service_class.query_tree, + window); + if (!array) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + /* The first element of the array is the parent window. The rest + are the children. */ + nelements = (*android_java_env)->GetArrayLength (android_java_env, + array); + eassert (nelements); + + /* Now fill in the children. */ + children = xnmalloc (nelements - 1, sizeof *children); + + shorts + = (*android_java_env)->GetShortArrayElements (android_java_env, array, + NULL); + for (i = 1; i < nelements; ++i) + children[i] = shorts[i]; + + /* Finally, return the parent and other values. */ + *root_return = 0; + *parent_return = shorts[0]; + *children_return = children; + + /* Release the array contents. */ + (*android_java_env)->ReleaseShortArrayElements (android_java_env, array, + shorts, JNI_ABORT); + + ANDROID_DELETE_LOCAL_REF (array); + return 1; +} + +void +android_get_geometry (android_window handle, + android_window *root_return, + int *x_return, int *y_return, + unsigned int *width_return, + unsigned int *height_return, + unsigned int *border_width_return) +{ + jobject window; + jarray window_geometry; + jmethodID get_geometry; + jint *ints; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + get_geometry = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "getWindowGeometry", "()[I"); + + window_geometry + = (*android_java_env)->CallObjectMethod (android_java_env, + window, + get_geometry); + if (!window_geometry) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + /* window_geometry is an array containing x, y, width and + height. border_width is always 0 on Android. */ + eassert ((*android_java_env)->GetArrayLength (android_java_env, + window_geometry) + == 4); + + *root_return = 0; + *border_width_return = 0; + + ints + = (*android_java_env)->GetIntArrayElements (android_java_env, + window_geometry, + NULL); + + *x_return = ints[0]; + *y_return = ints[1]; + *width_return = ints[2]; + *height_return = ints[3]; + + (*android_java_env)->ReleaseIntArrayElements (android_java_env, + window_geometry, + ints, JNI_ABORT); + + /* Now free the local reference. */ + ANDROID_DELETE_LOCAL_REF (window_geometry); +} + /* Low level drawing primitives. */ @@ -2998,6 +3305,86 @@ android_damage_window (android_drawable handle, +/* Other misc system routines. */ + +int +android_get_screen_width (void) +{ + return (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + service_class.get_screen_width, + (jboolean) false); +} + +int +android_get_screen_height (void) +{ + return (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + service_class.get_screen_height, + (jboolean) false); +} + +int +android_get_mm_width (void) +{ + return (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + service_class.get_screen_width, + (jboolean) true); +} + +int +android_get_mm_height (void) +{ + return (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + service_class.get_screen_height, + (jboolean) true); +} + +bool +android_detect_mouse (void) +{ + return (*android_java_env)->CallBooleanMethod (android_java_env, + emacs_service, + service_class.detect_mouse); +} + +void +android_set_dont_focus_on_map (android_window handle, + bool no_focus_on_map) +{ + jmethodID method; + jobject window; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + method = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "setDontFocusOnMap", "(Z)V"); + + (*android_java_env)->CallVoidMethod (android_java_env, window, + method, + (jboolean) no_focus_on_map); +} + +void +android_set_dont_accept_focus (android_window handle, + bool no_accept_focus) +{ + jmethodID method; + jobject window; + + window = android_resolve_handle (handle, ANDROID_HANDLE_WINDOW); + method = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "setDontAcceptFocus", "(Z)V"); + + (*android_java_env)->CallVoidMethod (android_java_env, window, + method, + (jboolean) no_accept_focus); +} + + + #undef faccessat /* Replace the system faccessat with one which understands AT_EACCESS. @@ -3015,6 +3402,128 @@ faccessat (int dirfd, const char *pathname, int mode, int flags) return real_faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); } + + +/* Directory listing emulation. */ + +struct android_dir +{ + /* The real DIR *, if it exists. */ + DIR *dir; + + /* Otherwise, the AAssetDir. */ + void *asset_dir; +}; + +/* Like opendir. However, return an asset directory if NAME points to + an asset. */ + +struct android_dir * +android_opendir (const char *name) +{ + struct android_dir *dir; + AAssetDir *asset_dir; + const char *asset_name; + + asset_name = android_get_asset_name (name); + + /* If the asset manager exists and NAME is an asset, return an asset + directory. */ + if (asset_manager && asset_name) + { + asset_dir = AAssetManager_openDir (asset_manager, + asset_name); + + if (!asset_dir) + { + errno = ENOENT; + return NULL; + } + + dir = xmalloc (sizeof *dir); + dir->dir = NULL; + dir->asset_dir = asset_dir; + return dir; + } + + /* Otherwise, open the directory normally. */ + dir = xmalloc (sizeof *dir); + dir->asset_dir = NULL; + dir->dir = opendir (name); + + if (!dir->dir) + { + xfree (dir); + return NULL; + } + + return dir; +} + +/* Like readdir, except it understands asset directories. */ + +struct dirent * +android_readdir (struct android_dir *dir) +{ + static struct dirent dirent; + const char *filename; + + if (dir->asset_dir) + { + filename = AAssetDir_getNextFileName (dir->asset_dir); + errno = 0; + + if (!filename) + return NULL; + + memset (&dirent, 0, sizeof dirent); + dirent.d_ino = 0; + dirent.d_off = 0; + dirent.d_reclen = sizeof dirent; + dirent.d_type = DT_UNKNOWN; + strncpy (dirent.d_name, filename, + sizeof dirent.d_name - 1); + return &dirent; + } + + return readdir (dir->dir); +} + +/* Like closedir, but it also closes asset manager directories. */ + +void +android_closedir (struct android_dir *dir) +{ + if (dir->dir) + closedir (dir->dir); + else + AAssetDir_close (dir->asset_dir); + + xfree (dir); +} + + + +/* emacs_abort implementation for Android. This logs a stack + trace. */ + +void +emacs_abort (void) +{ + volatile char *foo; + + __android_log_print (ANDROID_LOG_FATAL, __func__, + "emacs_abort called, please review the ensuing" + " stack trace"); + + /* Cause a NULL pointer dereference to make debuggerd generate a + tombstone. */ + foo = NULL; + *foo = '\0'; + + abort (); +} + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ diff --git a/src/android.h b/src/android.h index 4cf194f23cf..cddd315b9a3 100644 --- a/src/android.h +++ b/src/android.h @@ -27,7 +27,9 @@ along with GNU Emacs. If not, see . */ #ifndef ANDROID_STUBIFY #include #include + #include +#include #include @@ -53,6 +55,7 @@ extern int android_fstat (int, struct stat *); extern int android_fstatat (int, const char *restrict, struct stat *restrict, int); extern int android_close (int); +extern int android_fclose (FILE *); extern const char *android_get_home_directory (void); extern double android_pixel_density_x, android_pixel_density_y; @@ -71,6 +74,26 @@ extern unsigned char *android_lock_bitmap (android_window, jobject *); extern void android_damage_window (android_window, struct android_rectangle *); +extern int android_get_screen_width (void); +extern int android_get_screen_height (void); +extern int android_get_mm_width (void); +extern int android_get_mm_height (void); +extern bool android_detect_mouse (void); + +extern void android_set_dont_focus_on_map (android_window, bool); +extern void android_set_dont_accept_focus (android_window, bool); + + + +/* Directory listing emulation. */ + +struct android_dir; + +extern struct android_dir *android_opendir (const char *); +extern struct dirent *android_readdir (struct android_dir *); +extern void android_closedir (struct android_dir *); + + #endif diff --git a/src/androidfns.c b/src/androidfns.c index 96c2746a21a..459e407b901 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -21,6 +21,7 @@ along with GNU Emacs. If not, see . */ #include #include "lisp.h" +#include "android.h" #include "androidterm.h" #include "blockinput.h" #include "keyboard.h" @@ -151,6 +152,36 @@ android_decode_color (struct frame *f, Lisp_Object color_name, int mono_color) signal_error ("Undefined color", color_name); } +static void +android_set_parent_frame (struct frame *f, Lisp_Object new_value, + Lisp_Object old_value) +{ + struct frame *p; + + p = NULL; + + if (!NILP (new_value) + && (!FRAMEP (new_value) + || !FRAME_LIVE_P (p = XFRAME (new_value)) + || !FRAME_ANDROID_P (p))) + { + store_frame_param (f, Qparent_frame, old_value); + error ("Invalid specification of `parent-frame'"); + } + + if (p != FRAME_PARENT_FRAME (f)) + { + block_input (); + android_reparent_window (FRAME_ANDROID_WINDOW (f), + (p ? FRAME_ANDROID_WINDOW (p) + : FRAME_DISPLAY_INFO (f)->root_window), + f->left_pos, f->top_pos); + unblock_input (); + + fset_parent_frame (f, new_value); + } +} + void android_implicitly_set_name (struct frame *f, Lisp_Object arg, Lisp_Object oldval) @@ -531,9 +562,9 @@ android_default_font_parameter (struct frame *f, Lisp_Object parms) if (! FONTP (font) && ! STRINGP (font)) { const char *names[] = { - "Droid Sans Mono", - "monospace", - "DroidSansMono", + "Droid Sans Mono-12", + "Monospace-12", + "DroidSansMono-12", NULL }; int i; @@ -1119,8 +1150,7 @@ DEFUN ("x-display-pixel-width", Fx_display_pixel_width, error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + return make_fixnum (android_get_screen_width ()); #endif } @@ -1133,8 +1163,7 @@ DEFUN ("x-display-pixel-height", Fx_display_pixel_height, error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + return make_fixnum (android_get_screen_height ()); #endif } @@ -1185,8 +1214,7 @@ DEFUN ("x-display-mm-width", Fx_display_mm_width, Sx_display_mm_width, error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + return make_fixnum (android_get_mm_width ()); #endif } @@ -1198,8 +1226,7 @@ DEFUN ("x-display-mm-height", Fx_display_mm_height, Sx_display_mm_height, error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + return make_fixnum (android_get_mm_height ()); #endif } @@ -1225,77 +1252,373 @@ DEFUN ("x-display-visual-class", Fx_display_visual_class, return Qtrue_color; } -DEFUN ("x-display-monitor-attributes-list", Fx_display_monitor_attributes_list, - Sx_display_monitor_attributes_list, +DEFUN ("android-display-monitor-attributes-list", + Fandroid_display_monitor_attributes_list, + Sandroid_display_monitor_attributes_list, 0, 1, 0, - doc: /* SKIP: real doc in xfns.c. */) + doc: /* Return a list of physical monitor attributes on the X display TERMINAL. + +The optional argument TERMINAL specifies which display to ask about. +TERMINAL should be a terminal object, a frame or a display name (a string). +If omitted or nil, that stands for the selected frame's display. + +Internal use only, use `display-monitor-attributes-list' instead. */) (Lisp_Object terminal) { #ifdef ANDROID_STUBIFY error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + struct MonitorInfo monitor; + + memset (&monitor, 0, sizeof monitor); + monitor.geom.width = android_get_screen_width (); + monitor.geom.height = android_get_screen_height (); + monitor.mm_width = android_get_mm_width (); + monitor.mm_height = android_get_mm_height (); + monitor.work = monitor.geom; + monitor.name = (char *) "Android device monitor"; + + /* What to do about monitor_frames? */ + return make_monitor_attribute_list (&monitor, 1, + 0, Qnil, NULL); #endif } -DEFUN ("x-frame-geometry", Fx_frame_geometry, Sx_frame_geometry, - 0, 1, 0, doc: /* SKIP: real doc in xfns.c. */) - (Lisp_Object terminal) +#ifndef ANDROID_STUBIFY + +static Lisp_Object +frame_geometry (Lisp_Object frame, Lisp_Object attribute) +{ + struct frame *f = decode_live_frame (frame); + android_window rootw; + unsigned int native_width, native_height, x_border_width = 0; + int x_native = 0, y_native = 0, xptr = 0, yptr = 0; + int left_off = 0, right_off = 0, top_off = 0, bottom_off = 0; + int outer_left, outer_top, outer_right, outer_bottom; + int native_left, native_top, native_right, native_bottom; + int inner_left, inner_top, inner_right, inner_bottom; + int internal_border_width; + bool menu_bar_external = false, tool_bar_external = false; + int menu_bar_height = 0, menu_bar_width = 0; + int tab_bar_height = 0, tab_bar_width = 0; + int tool_bar_height = 0, tool_bar_width = 0; + + if (FRAME_INITIAL_P (f) || !FRAME_ANDROID_P (f) + || !FRAME_ANDROID_WINDOW (f)) + return Qnil; + + block_input (); + android_get_geometry (FRAME_ANDROID_WINDOW (f), + &rootw, &x_native, &y_native, + &native_width, &native_height, &x_border_width); + unblock_input (); + + if (FRAME_PARENT_FRAME (f)) + { + Lisp_Object parent, edges; + + XSETFRAME (parent, FRAME_PARENT_FRAME (f)); + edges = Fandroid_frame_edges (parent, Qnative_edges); + if (!NILP (edges)) + { + x_native += XFIXNUM (Fnth (make_fixnum (0), edges)); + y_native += XFIXNUM (Fnth (make_fixnum (1), edges)); + } + + outer_left = x_native; + outer_top = y_native; + outer_right = outer_left + native_width + 2 * x_border_width; + outer_bottom = outer_top + native_height + 2 * x_border_width; + + native_left = x_native + x_border_width; + native_top = y_native + x_border_width; + native_right = native_left + native_width; + native_bottom = native_top + native_height; + } + else + { + outer_left = xptr; + outer_top = yptr; + outer_right = outer_left + left_off + native_width + right_off; + outer_bottom = outer_top + top_off + native_height + bottom_off; + + native_left = outer_left + left_off; + native_top = outer_top + top_off; + native_right = native_left + native_width; + native_bottom = native_top + native_height; + } + + internal_border_width = FRAME_INTERNAL_BORDER_WIDTH (f); + inner_left = native_left + internal_border_width; + inner_top = native_top + internal_border_width; + inner_right = native_right - internal_border_width; + inner_bottom = native_bottom - internal_border_width; + + menu_bar_height = FRAME_MENU_BAR_HEIGHT (f); + inner_top += menu_bar_height; + menu_bar_width = menu_bar_height ? native_width : 0; + + tab_bar_height = FRAME_TAB_BAR_HEIGHT (f); + tab_bar_width = (tab_bar_height + ? native_width - 2 * internal_border_width + : 0); + inner_top += tab_bar_height; + + tool_bar_height = FRAME_TOOL_BAR_HEIGHT (f); + tool_bar_width = (tool_bar_height + ? native_width - 2 * internal_border_width + : 0); + inner_top += tool_bar_height; + + /* Construct list. */ + if (EQ (attribute, Qouter_edges)) + return list4i (outer_left, outer_top, outer_right, outer_bottom); + else if (EQ (attribute, Qnative_edges)) + return list4i (native_left, native_top, native_right, native_bottom); + else if (EQ (attribute, Qinner_edges)) + return list4i (inner_left, inner_top, inner_right, inner_bottom); + else + return + list (Fcons (Qouter_position, + Fcons (make_fixnum (outer_left), + make_fixnum (outer_top))), + Fcons (Qouter_size, + Fcons (make_fixnum (outer_right - outer_left), + make_fixnum (outer_bottom - outer_top))), + /* Approximate. */ + Fcons (Qexternal_border_size, + Fcons (make_fixnum (right_off), + make_fixnum (bottom_off))), + Fcons (Qouter_border_width, make_fixnum (x_border_width)), + /* Approximate. */ + Fcons (Qtitle_bar_size, + Fcons (make_fixnum (0), + make_fixnum (top_off - bottom_off))), + Fcons (Qmenu_bar_external, menu_bar_external ? Qt : Qnil), + Fcons (Qmenu_bar_size, + Fcons (make_fixnum (menu_bar_width), + make_fixnum (menu_bar_height))), + Fcons (Qtab_bar_size, + Fcons (make_fixnum (tab_bar_width), + make_fixnum (tab_bar_height))), + Fcons (Qtool_bar_external, tool_bar_external ? Qt : Qnil), + Fcons (Qtool_bar_position, FRAME_TOOL_BAR_POSITION (f)), + Fcons (Qtool_bar_size, + Fcons (make_fixnum (tool_bar_width), + make_fixnum (tool_bar_height))), + Fcons (Qinternal_border_width, + make_fixnum (internal_border_width))); +} + +#endif + +DEFUN ("android-frame-geometry", Fandroid_frame_geometry, + Sandroid_frame_geometry, + 0, 1, 0, + doc: /* Return geometric attributes of FRAME. +FRAME must be a live frame and defaults to the selected one. The return +value is an association list of the attributes listed below. All height +and width values are in pixels. + +`outer-position' is a cons of the outer left and top edges of FRAME + relative to the origin - the position (0, 0) - of FRAME's display. + +`outer-size' is a cons of the outer width and height of FRAME. The + outer size includes the title bar and the external borders as well as + any menu and/or tool bar of frame. + +`external-border-size' is a cons of the horizontal and vertical width of + FRAME's external borders as supplied by the window manager. + +`title-bar-size' is a cons of the width and height of the title bar of + FRAME as supplied by the window manager. If both of them are zero, + FRAME has no title bar. If only the width is zero, Emacs was not + able to retrieve the width information. + +`menu-bar-external', if non-nil, means the menu bar is external (never + included in the inner edges of FRAME). + +`menu-bar-size' is a cons of the width and height of the menu bar of + FRAME. + +`tool-bar-external', if non-nil, means the tool bar is external (never + included in the inner edges of FRAME). + +`tool-bar-position' tells on which side the tool bar on FRAME is and can + be one of `left', `top', `right' or `bottom'. If this is nil, FRAME + has no tool bar. + +`tool-bar-size' is a cons of the width and height of the tool bar of + FRAME. + +`internal-border-width' is the width of the internal border of + FRAME. */) + (Lisp_Object frame) { #ifdef ANDROID_STUBIFY error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); + return frame_geometry (frame, Qnil); +#endif +} + +DEFUN ("android-frame-edges", Fandroid_frame_edges, Sandroid_frame_edges, 0, 2, 0, + doc: /* Return edge coordinates of FRAME. +FRAME must be a live frame and defaults to the selected one. The return +value is a list of the form (LEFT, TOP, RIGHT, BOTTOM). All values are +in pixels relative to the origin - the position (0, 0) - of FRAME's +display. + +If optional argument TYPE is the symbol `outer-edges', return the outer +edges of FRAME. The outer edges comprise the decorations of the window +manager (like the title bar or external borders) as well as any external +menu or tool bar of FRAME. If optional argument TYPE is the symbol +`native-edges' or nil, return the native edges of FRAME. The native +edges exclude the decorations of the window manager and any external +menu or tool bar of FRAME. If TYPE is the symbol `inner-edges', return +the inner edges of FRAME. These edges exclude title bar, any borders, +menu bar or tool bar of FRAME. */) + (Lisp_Object frame, Lisp_Object type) +{ +#ifndef ANDROID_STUBIFY + return frame_geometry (frame, ((EQ (type, Qouter_edges) + || EQ (type, Qinner_edges)) + ? type + : Qnative_edges)); +#else return Qnil; #endif } -DEFUN ("x-frame-list-z-order", Fx_frame_list_z_order, - Sx_frame_list_z_order, 0, 1, 0, - doc: /* SKIP: real doc in xfns.c. */) +#ifndef ANDROID_STUBIFY + +static Lisp_Object +android_frame_list_z_order (struct android_display_info *dpyinfo, + android_window window) +{ + android_window root, parent, *children; + unsigned int nchildren; + unsigned long i; + Lisp_Object frames; + + frames = Qnil; + + if (android_query_tree (window, &root, &parent, + &children, &nchildren)) + { + for (i = 0; i < nchildren; i++) + { + Lisp_Object frame, tail; + + FOR_EACH_FRAME (tail, frame) + { + struct frame *cf = XFRAME (frame); + + if (FRAME_ANDROID_P (cf) + && (FRAME_ANDROID_WINDOW (cf) == children[i])) + frames = Fcons (frame, frames); + } + } + + if (children) + xfree (children); + } + + return frames; +} + +#endif + +DEFUN ("android-frame-list-z-order", Fandroid_frame_list_z_order, + Sandroid_frame_list_z_order, 0, 1, 0, + doc: /* Return list of Emacs' frames, in Z (stacking) order. +The optional argument TERMINAL specifies which display to ask about. +TERMINAL should be either a frame or a display name (a string). If +omitted or nil, that stands for the selected frame's display. Return +nil if TERMINAL contains no Emacs frame. + +As a special case, if TERMINAL is non-nil and specifies a live frame, +return the child frames of that frame in Z (stacking) order. + +Frames are listed from topmost (first) to bottommost (last). + +On Android, the order of the frames returned is undefined unless +TERMINAL is a frame. */) (Lisp_Object terminal) { #ifdef ANDROID_STUBIFY error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); - return Qnil; + struct android_display_info *dpyinfo; + android_window window; + + dpyinfo = check_android_display_info (terminal); + + if (FRAMEP (terminal) && FRAME_LIVE_P (XFRAME (terminal))) + window = FRAME_ANDROID_WINDOW (XFRAME (terminal)); + else + window = dpyinfo->root_window; + + return android_frame_list_z_order (dpyinfo, window); #endif } -DEFUN ("x-frame-restack", Fx_frame_restack, Sx_frame_restack, 2, 3, 0, - doc: /* SKIP: real doc in xfns.c. */) +DEFUN ("android-frame-restack", Fandroid_frame_restack, + Sandroid_frame_restack, 2, 3, 0, + doc: /* Restack FRAME1 below FRAME2. +This means that if both frames are visible and the display areas of +these frames overlap, FRAME2 (partially) obscures FRAME1. If optional +third argument ABOVE is non-nil, restack FRAME1 above FRAME2. This +means that if both frames are visible and the display areas of these +frames overlap, FRAME1 (partially) obscures FRAME2. + +This may be thought of as an atomic action performed in two steps: The +first step removes FRAME1's window-step window from the display. The +second step reinserts FRAME1's window below (above if ABOVE is true) +that of FRAME2. Hence the position of FRAME2 in its display's Z +\(stacking) order relative to all other frames excluding FRAME1 remains +unaltered. + +The Android system refuses to restack windows, so this does not +work. */) (Lisp_Object frame1, Lisp_Object frame2, Lisp_Object frame3) { #ifdef ANDROID_STUBIFY error ("Android cross-compilation stub called!"); return Qnil; #else - error ("Not implemented"); + /* This is not supported on Android because of limitations in the + platform that prevent ViewGroups from restacking + SurfaceViews. */ return Qnil; #endif } -DEFUN ("x-mouse-absolute-pixel-position", Fx_mouse_absolute_pixel_position, - Sx_mouse_absolute_pixel_position, 0, 0, 0, - doc: /* SKIP: real doc in xfns.c. */) +DEFUN ("android-mouse-absolute-pixel-position", + Fandroid_mouse_absolute_pixel_position, + Sandroid_mouse_absolute_pixel_position, 0, 0, 0, + doc: /* Return absolute position of mouse cursor in pixels. +The position is returned as a cons cell (X . Y) of the coordinates of +the mouse cursor position in pixels relative to a position (0, 0) of the +selected frame's display. This does not work on Android. */) (void) { - /* TODO: figure out how to implement this. */ + /* This cannot be implemented on Android. */ return Qnil; } -DEFUN ("x-set-mouse-absolute-pixel-position", - Fx_set_mouse_absolute_pixel_position, - Sx_set_mouse_absolute_pixel_position, 2, 2, 0, - doc: /* SKIP: real doc in xfns.c. */) +DEFUN ("android-set-mouse-absolute-pixel-position", + Fandroid_set_mouse_absolute_pixel_position, + Sandroid_set_mouse_absolute_pixel_position, 2, 2, 0, + doc: /* Move mouse pointer to a pixel position at (X, Y). The +coordinates X and Y are interpreted to start from the top-left corner +of the screen. This does not work on Android. */) (Lisp_Object x, Lisp_Object y) { - /* TODO: figure out how to implement this. */ + /* This cannot be implemented on Android. */ return Qnil; } @@ -1364,6 +1687,20 @@ DEFUN ("x-hide-tip", Fx_hide_tip, Sx_hide_tip, 0, 0, 0, #endif } +DEFUN ("android-detect-mouse", Fandroid_detect_mouse, + Sandroid_detect_mouse, 0, 0, 0, + doc: /* Figure out whether or not there is a mouse. +Return non-nil if a mouse is connected to this computer, and nil if +there is no mouse. */) + (void) +{ +#ifndef ANDROID_STUBIFY + return android_detect_mouse () ? Qt : Qnil; +#else + return Qnil; +#endif +} + #ifndef ANDROID_STUBIFY @@ -1579,7 +1916,7 @@ android_set_menu_bar_lines (struct frame *f, Lisp_Object value, y = FRAME_TOP_MARGIN_HEIGHT (f); block_input (); - android_clear_area (FRAME_ANDROID_WINDOW (f), + android_clear_area (FRAME_ANDROID_DRAWABLE (f), 0, y, width, height); unblock_input (); } @@ -1590,7 +1927,7 @@ android_set_menu_bar_lines (struct frame *f, Lisp_Object value, height = nlines * FRAME_LINE_HEIGHT (f) - y; block_input (); - android_clear_area (FRAME_ANDROID_WINDOW (f), 0, y, + android_clear_area (FRAME_ANDROID_DRAWABLE (f), 0, y, width, height); unblock_input (); } @@ -1682,6 +2019,30 @@ android_set_alpha (struct frame *f, Lisp_Object arg, Lisp_Object oldval) } } +static void +android_set_no_focus_on_map (struct frame *f, Lisp_Object new_value, + Lisp_Object old_value) +{ + if (!EQ (new_value, old_value)) + { + android_set_dont_focus_on_map (FRAME_ANDROID_WINDOW (f), + new_value); + FRAME_NO_FOCUS_ON_MAP (f) = !NILP (new_value); + } +} + +static void +android_set_no_accept_focus (struct frame *f, Lisp_Object new_value, + Lisp_Object old_value) +{ + if (!EQ (new_value, old_value)) + { + android_set_dont_accept_focus (FRAME_ANDROID_WINDOW (f), + new_value); + FRAME_NO_ACCEPT_FOCUS (f) = !NILP (new_value); + } +} + frame_parm_handler android_frame_parm_handlers[] = { gui_set_autoraise, @@ -1724,16 +2085,16 @@ frame_parm_handler android_frame_parm_handlers[] = NULL, NULL, NULL, - NULL, /* x_set_undecorated, */ - NULL, /* x_set_parent_frame, */ - NULL, /* x_set_skip_taskbar, */ - NULL, /* x_set_no_focus_on_map, */ - NULL, /* x_set_no_accept_focus, */ - NULL, /* x_set_z_group, */ - NULL, /* x_set_override_redirect, */ + NULL, + android_set_parent_frame, + NULL, + android_set_no_focus_on_map, + android_set_no_accept_focus, + NULL, + NULL, gui_set_no_special_glyphs, - NULL, /* x_set_alpha_background, */ - NULL, /* x_set_use_frame_synchronization, */ + NULL, + NULL, }; #endif @@ -1766,14 +2127,16 @@ syms_of_androidfns (void) defsubr (&Sx_display_mm_height); defsubr (&Sx_display_backing_store); defsubr (&Sx_display_visual_class); - defsubr (&Sx_display_monitor_attributes_list); - defsubr (&Sx_frame_geometry); - defsubr (&Sx_frame_list_z_order); - defsubr (&Sx_frame_restack); - defsubr (&Sx_mouse_absolute_pixel_position); - defsubr (&Sx_set_mouse_absolute_pixel_position); + defsubr (&Sandroid_display_monitor_attributes_list); + defsubr (&Sandroid_frame_geometry); + defsubr (&Sandroid_frame_edges); + defsubr (&Sandroid_frame_list_z_order); + defsubr (&Sandroid_frame_restack); + defsubr (&Sandroid_mouse_absolute_pixel_position); + defsubr (&Sandroid_set_mouse_absolute_pixel_position); defsubr (&Sandroid_get_connection); defsubr (&Sx_display_list); defsubr (&Sx_show_tip); defsubr (&Sx_hide_tip); + defsubr (&Sandroid_detect_mouse); } diff --git a/src/androidfont.c b/src/androidfont.c index b2c83201b06..b0a9f994a85 100644 --- a/src/androidfont.c +++ b/src/androidfont.c @@ -1,4 +1,4 @@ -/* Communication module for Android terminals. +/* Android fallback font driver. Copyright (C) 2023 Free Software Foundation, Inc. @@ -17,6 +17,10 @@ GNU General Public License for more details. You should have received a copy of the GNU General Public License along with GNU Emacs. If not, see . */ +/* Due to the terrible nature of the Android Typeface subsystems, this + font driver is only used as a fallback when sfntfont-android.c + fails to enumerate any fonts at all. */ + #include #include "lisp.h" @@ -636,7 +640,7 @@ androidfont_draw (struct glyph_string *s, int from, int to, gcontext = android_resolve_handle (s->gc->gcontext, ANDROID_HANDLE_GCONTEXT); - drawable = android_resolve_handle (FRAME_ANDROID_WINDOW (s->f), + drawable = android_resolve_handle (FRAME_ANDROID_DRAWABLE (s->f), ANDROID_HANDLE_WINDOW); chars = (*android_java_env)->NewIntArray (android_java_env, to - from); diff --git a/src/androidgui.h b/src/androidgui.h index 7d045f7b450..422e72408c7 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -222,6 +222,12 @@ enum android_event_type ANDROID_MOTION_NOTIFY, ANDROID_BUTTON_PRESS, ANDROID_BUTTON_RELEASE, + ANDROID_TOUCH_DOWN, + ANDROID_TOUCH_UP, + ANDROID_TOUCH_MOVE, + ANDROID_WHEEL, + ANDROID_ICONIFIED, + ANDROID_DEICONIFIED, }; struct android_any_event @@ -312,6 +318,54 @@ struct android_button_event unsigned int button; }; +struct android_touch_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* Window associated with the event. */ + android_window window; + + /* X and Y coordinates of the event. */ + int x, y; + + /* Time of the event, and the pointer identifier. */ + unsigned long time; + + /* Index of the pointer being tracked. */ + unsigned int pointer_id; +}; + +struct android_wheel_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* Window associated with the event. */ + android_window window; + + /* X and Y coordinates of the event. */ + int x, y; + + /* Time of the event, and the pointer identifier. */ + unsigned long time; + + /* Modifier state at the time of the event. */ + int state; + + /* Motion alongside the X and Y axes. */ + double x_delta, y_delta; +}; + +struct android_iconify_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* Window associated with the event. */ + android_window window; +}; + union android_event { enum android_event_type type; @@ -323,8 +377,26 @@ union android_event struct android_crossing_event xcrossing; struct android_motion_event xmotion; struct android_button_event xbutton; + + /* This has no parallel in X, since the X model of having + monotonically increasing touch IDs can't work on Android. */ + struct android_touch_event touch; + + /* This has no parallel in X outside the X Input Extension, and + emulating the input extension interface would be awfully + complicated. */ + struct android_wheel_event wheel; + + /* This has no parallel in X because Android doesn't have window + properties. */ + struct android_iconify_event iconified; }; +enum + { + ANDROID_CURRENT_TIME = 0L, + }; + extern int android_pending (void); extern void android_next_event (union android_event *); @@ -396,6 +468,17 @@ extern void android_clear_area (android_window, int, int, unsigned int, extern android_pixmap android_create_bitmap_from_data (char *, unsigned int, unsigned int); +extern void android_bell (void); +extern void android_set_input_focus (android_window, unsigned long); +extern void android_raise_window (android_window); +extern void android_lower_window (android_window); +extern int android_query_tree (android_window, android_window *, + android_window *, android_window **, + unsigned int *); +extern void android_get_geometry (android_window, android_window *, + int *, int *, unsigned int *, + unsigned int *, unsigned int *); + #endif diff --git a/src/androidterm.c b/src/androidterm.c index 05fe7f01bf9..220858c0fec 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -19,6 +19,7 @@ along with GNU Emacs. If not, see . */ #include #include +#include #include "lisp.h" #include "androidterm.h" @@ -46,6 +47,11 @@ struct android_display_info *x_display_list; static bool any_help_event_p; +/* Counters for tallying up scroll wheel events if + mwheel_coalesce_scroll_events is true. */ + +static double wheel_event_x, wheel_event_y; + enum { ANDROID_EVENT_NORMAL, @@ -83,19 +89,133 @@ android_clear_frame (struct frame *f) /* Clearing the frame will erase any cursor, so mark them all as no longer visible. */ mark_window_cursors_off (XWINDOW (FRAME_ROOT_WINDOW (f))); - android_clear_window (FRAME_ANDROID_WINDOW (f)); + android_clear_window (FRAME_ANDROID_DRAWABLE (f)); +} + +static void +android_flash (struct frame *f) +{ + struct android_gc *gc; + struct android_gc_values values; + int rc; + fd_set fds; + + block_input (); + + values.function = ANDROID_GC_XOR; + values.foreground = (FRAME_FOREGROUND_PIXEL (f) + ^ FRAME_BACKGROUND_PIXEL (f)); + + gc = android_create_gc ((ANDROID_GC_FUNCTION + | ANDROID_GC_FOREGROUND), + &values); + + /* Get the height not including a menu bar widget. */ + int height = FRAME_PIXEL_HEIGHT (f); + /* Height of each line to flash. */ + int flash_height = FRAME_LINE_HEIGHT (f); + /* These will be the left and right margins of the rectangles. */ + int flash_left = FRAME_INTERNAL_BORDER_WIDTH (f); + int flash_right = FRAME_PIXEL_WIDTH (f) - FRAME_INTERNAL_BORDER_WIDTH (f); + int width = flash_right - flash_left; + + /* If window is tall, flash top and bottom line. */ + if (height > 3 * FRAME_LINE_HEIGHT (f)) + { + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, + (FRAME_INTERNAL_BORDER_WIDTH (f) + + FRAME_TOP_MARGIN_HEIGHT (f)), + width, flash_height); + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, + (height - flash_height + - FRAME_INTERNAL_BORDER_WIDTH (f)), + width, flash_height); + + } + else + /* If it is short, flash it all. */ + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, FRAME_INTERNAL_BORDER_WIDTH (f), + width, (height - 2 + * FRAME_INTERNAL_BORDER_WIDTH (f))); + + flush_frame (f); + + struct timespec delay = make_timespec (0, 150 * 1000 * 1000); + struct timespec wakeup = timespec_add (current_timespec (), delay); + + /* Keep waiting until past the time wakeup or any input gets + available. */ + while (! detect_input_pending ()) + { + struct timespec current = current_timespec (); + struct timespec timeout; + + /* Break if result would not be positive. */ + if (timespec_cmp (wakeup, current) <= 0) + break; + + /* How long `select' should wait. */ + timeout = make_timespec (0, 10 * 1000 * 1000); + + /* Wait for some input to become available on the X + connection. */ + FD_ZERO (&fds); + + /* Try to wait that long--but we might wake up sooner. */ + rc = pselect (0, &fds, NULL, NULL, &timeout, NULL); + + /* Some input is available, exit the visible bell. */ + if (rc >= 0) + break; + } + + /* If window is tall, flash top and bottom line. */ + if (height > 3 * FRAME_LINE_HEIGHT (f)) + { + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, + (FRAME_INTERNAL_BORDER_WIDTH (f) + + FRAME_TOP_MARGIN_HEIGHT (f)), + width, flash_height); + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, + (height - flash_height + - FRAME_INTERNAL_BORDER_WIDTH (f)), + width, flash_height); + } + else + /* If it is short, flash it all. */ + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, + flash_left, FRAME_INTERNAL_BORDER_WIDTH (f), + width, (height - 2 + * FRAME_INTERNAL_BORDER_WIDTH (f))); + + android_free_gc (gc); + flush_frame (f); + + unblock_input (); } static void android_ring_bell (struct frame *f) { - /* TODO */ + if (visible_bell) + android_flash (f); + else + { + block_input (); + android_bell (); + unblock_input (); + } } static void android_toggle_invisible_pointer (struct frame *f, bool invisible) { - /* TODO */ + } /* Start an update of frame F. This function is installed as a hook @@ -127,10 +247,17 @@ show_back_buffer (struct frame *f) { struct android_swap_info swap_info; + /* Somehow Android frames can be swapped while garbaged. */ + if (FRAME_GARBAGED_P (f)) + return; + memset (&swap_info, 0, sizeof (swap_info)); swap_info.swap_window = FRAME_ANDROID_WINDOW (f); swap_info.swap_action = ANDROID_COPIED; android_swap_buffers (&swap_info, 1); + + /* Now the back buffer no longer needs to be flipped. */ + FRAME_ANDROID_NEED_BUFFER_FLIP (f) = false; } /* Flip back buffers on F if it has undrawn content. */ @@ -142,7 +269,8 @@ android_flush_dirty_back_buffer_on (struct frame *f) || buffer_flipping_blocked_p () /* If the frame is not already up to date, do not flush buffers on input, as that will result in flicker. */ - || !FRAME_ANDROID_COMPLETE_P (f)) + || !FRAME_ANDROID_COMPLETE_P (f) + && FRAME_ANDROID_NEED_BUFFER_FLIP (f)) return; show_back_buffer (f); @@ -174,13 +302,13 @@ static void android_frame_rehighlight (struct android_display_info *); static void android_lower_frame (struct frame *f) { - /* TODO. */ + android_lower_window (FRAME_ANDROID_WINDOW (f)); } static void android_raise_frame (struct frame *f) { - /* TODO. */ + android_raise_window (FRAME_ANDROID_WINDOW (f)); } static void @@ -354,6 +482,46 @@ android_construct_mouse_click (struct input_event *result, return Qnil; } +/* Generate a TOUCHSCREEN_UPDATE_EVENT for all pressed tools in FRAME. + Return the event in IE. Do not set IE->timestamp, as that is left + to the caller. */ + +static void +android_update_tools (struct frame *f, struct input_event *ie) +{ + struct android_touch_point *touchpoint; + + ie->kind = TOUCHSCREEN_UPDATE_EVENT; + XSETFRAME (ie->frame_or_window, f); + ie->arg = Qnil; + + /* Build the list of active touches. */ + for (touchpoint = FRAME_OUTPUT_DATA (f)->touch_points; + touchpoint; touchpoint = touchpoint->next) + ie->arg = Fcons (list3i (touchpoint->x, + touchpoint->y, + touchpoint->tool_id), + ie->arg); +} + +/* Find and return an existing tool pressed against FRAME, identified + by POINTER_ID. Return NULL if no tool by that ID was found. */ + +static struct android_touch_point * +android_find_tool (struct frame *f, int pointer_id) +{ + struct android_touch_point *touchpoint; + + for (touchpoint = FRAME_OUTPUT_DATA (f)->touch_points; + touchpoint; touchpoint = touchpoint->next) + { + if (touchpoint->tool_id == pointer_id) + return touchpoint; + } + + return NULL; +} + static int handle_one_android_event (struct android_display_info *dpyinfo, union android_event *event, int *finish, @@ -364,6 +532,10 @@ handle_one_android_event (struct android_display_info *dpyinfo, Mouse_HLInfo *hlinfo; union buffered_input_event inev; int modifiers, count, do_help; + struct android_touch_point *touchpoint, **last; + Lisp_Object window; + int scroll_height; + double scroll_unit; /* It is okay for this to not resemble handle_one_xevent so much. Differences in event handling code are much less nasty than @@ -633,6 +805,28 @@ handle_one_android_event (struct android_display_info *dpyinfo, f = mouse_or_wdesc_frame (dpyinfo, event->xbutton.window); + if (f && event->xbutton.type == ANDROID_BUTTON_PRESS + && !popup_activated () + /* && !x_window_to_scroll_bar (event->xbutton.display, */ + /* event->xbutton.window, 2) */ + && !FRAME_NO_ACCEPT_FOCUS (f)) + { + /* When clicking into a child frame or when clicking + into a parent frame with the child frame selected and + `no-accept-focus' is not set, select the clicked + frame. */ + struct frame *hf = dpyinfo->highlight_frame; + + if (FRAME_PARENT_FRAME (f) || (hf && frame_ancestor_p (f, hf))) + { + android_set_input_focus (FRAME_ANDROID_WINDOW (f), + event->xbutton.time); + + if (FRAME_PARENT_FRAME (f)) + android_raise_window (FRAME_ANDROID_WINDOW (f)); + } + } + if (f) { /* Is this in the tab-bar? */ @@ -715,6 +909,223 @@ handle_one_android_event (struct android_display_info *dpyinfo, goto OTHER; + /* Touch events. The events here don't parallel X so much. */ + case ANDROID_TOUCH_DOWN: + + if (!any) + goto OTHER; + + /* This event is sent when a tool is put on the screen. X and Y + are the location of the finger, and pointer_id identifies the + tool for as long as it is still held down. First, see if the + touch point already exists and can be reused (this shouldn't + happen, but be safe.) */ + + touchpoint = android_find_tool (any, event->touch.pointer_id); + + if (touchpoint) + { + /* Simply update the tool position and send an update. */ + touchpoint->x = event->touch.x; + touchpoint->y = event->touch.x; + android_update_tools (any, &inev.ie); + inev.ie.timestamp = event->touch.time; + + goto OTHER; + } + + /* Otherwise, link a new touchpoint onto the output's list of + pressed tools. */ + + touchpoint = xmalloc (sizeof *touchpoint); + touchpoint->tool_id = event->touch.pointer_id; + touchpoint->x = event->touch.x; + touchpoint->y = event->touch.x; + touchpoint->next = FRAME_OUTPUT_DATA (any)->touch_points; + FRAME_OUTPUT_DATA (any)->touch_points = touchpoint; + + /* Now generate the Emacs event. */ + inev.ie.kind = TOUCHSCREEN_BEGIN_EVENT; + inev.ie.timestamp = event->touch.time; + XSETFRAME (inev.ie.frame_or_window, any); + XSETINT (inev.ie.x, event->touch.x); + XSETINT (inev.ie.y, event->touch.y); + XSETINT (inev.ie.arg, event->touch.pointer_id); + + goto OTHER; + + case ANDROID_TOUCH_MOVE: + + if (!any) + goto OTHER; + + /* Look for the tool that moved. */ + + touchpoint = android_find_tool (any, event->touch.pointer_id); + + /* If it doesn't exist, skip processing this event. */ + + if (!touchpoint) + goto OTHER; + + /* Otherwise, update the position and send the update event. */ + + touchpoint->x = event->touch.x; + touchpoint->y = event->touch.y; + android_update_tools (any, &inev.ie); + inev.ie.timestamp = event->touch.time; + + goto OTHER; + + case ANDROID_TOUCH_UP: + + if (!any) + goto OTHER; + + /* Now find and unlink the tool in question. */ + + last = &FRAME_OUTPUT_DATA (any)->touch_points; + while ((touchpoint = *last)) + { + if (touchpoint->tool_id == event->touch.pointer_id) + { + *last = touchpoint->next; + + /* The tool was unlinked. Free it and generate the + appropriate Emacs event. */ + xfree (touchpoint); + inev.ie.kind = TOUCHSCREEN_END_EVENT; + inev.ie.timestamp = event->touch.time; + + XSETFRAME (inev.ie.frame_or_window, any); + XSETINT (inev.ie.x, event->touch.x); + XSETINT (inev.ie.y, event->touch.y); + XSETINT (inev.ie.arg, event->touch.pointer_id); + + /* Break out of the loop. */ + goto OTHER; + } + else + last = &touchpoint->next; + } + + /* No touch point was found. This shouldn't happen. */ + goto OTHER; + + /* Wheel motion. The events here don't parallel X because + Android doesn't have scroll valuators. */ + + case ANDROID_WHEEL: + + if (!any) + goto OTHER; + + if (fabs (event->wheel.x_delta) > 0 + || fabs (event->wheel.y_delta) > 0) + { + if (mwheel_coalesce_scroll_events) + { + if (signbit (event->wheel.x_delta) + != signbit (wheel_event_x)) + wheel_event_x = 0.0; + + if (signbit (event->wheel.y_delta) + != signbit (wheel_event_y)) + wheel_event_y = 0.0; + + /* Tally up deltas until one of them exceeds 1.0. */ + wheel_event_x += event->wheel.x_delta; + wheel_event_y += event->wheel.y_delta; + + if (fabs (wheel_event_x) < 1.0 + && fabs (wheel_event_y) < 1.0) + goto OTHER; + } + else + { + /* Use the deltas in the event. */ + wheel_event_x = event->wheel.x_delta; + wheel_event_y = event->wheel.y_delta; + } + + /* Determine what kind of event to send. */ + inev.ie.kind = ((fabs (wheel_event_y) + >= fabs (wheel_event_x)) + ? WHEEL_EVENT : HORIZ_WHEEL_EVENT); + inev.ie.timestamp = event->wheel.time; + + /* Set the event coordinates. */ + XSETINT (inev.ie.x, event->wheel.x); + XSETINT (inev.ie.y, event->wheel.y); + + /* Set the frame. */ + XSETFRAME (inev.ie.frame_or_window, any); + + /* Figure out the scroll direction. */ + inev.ie.modifiers = (signbit ((fabs (wheel_event_x) + >= fabs (wheel_event_y)) + ? wheel_event_x + : wheel_event_y) + ? down_modifier : up_modifier); + + /* Figure out how much to scale the deltas by. */ + window = window_from_coordinates (any, event->wheel.x, + event->wheel.y, NULL, + false, false); + + if (WINDOWP (window)) + scroll_height = XWINDOW (window)->pixel_height; + else + /* EVENT_X and EVENT_Y can be outside the + frame if F holds the input grab, so fall + back to the height of the frame instead. */ + scroll_height = FRAME_PIXEL_HEIGHT (any); + + scroll_unit = pow (scroll_height, 2.0 / 3.0); + + /* Add the keyboard modifiers. */ + inev.ie.modifiers + |= android_android_to_emacs_modifiers (dpyinfo, + event->wheel.state); + + /* Finally include the scroll deltas. */ + inev.ie.arg = list3 (Qnil, + make_float (wheel_event_x + * scroll_unit), + make_float (wheel_event_y + * scroll_unit)); + + wheel_event_x = 0.0; + wheel_event_y = 0.0; + } + + goto OTHER; + + /* Iconification. This is vastly simpler than on X. */ + case ANDROID_ICONIFIED: + + if (FRAME_ICONIFIED_P (any)) + goto OTHER; + + SET_FRAME_VISIBLE (any, false); + SET_FRAME_ICONIFIED (any, true); + + inev.ie.kind = ICONIFY_EVENT; + XSETFRAME (inev.ie.frame_or_window, any); + goto OTHER; + + case ANDROID_DEICONIFIED: + + if (!FRAME_ICONIFIED_P (any)) + goto OTHER; + + SET_FRAME_VISIBLE (any, true); + SET_FRAME_ICONIFIED (any, false); + + inev.ie.kind = DEICONIFY_EVENT; + XSETFRAME (inev.ie.frame_or_window, any); + goto OTHER; + default: goto OTHER; } @@ -781,8 +1192,7 @@ android_read_socket (struct terminal *terminal, now. */ if (dpyinfo->pending_autoraise_frame) { - /* android_raise_frame (dpyinfo->pending_autoraise_frame); - TODO */ + android_raise_frame (dpyinfo->pending_autoraise_frame); dpyinfo->pending_autoraise_frame = NULL; } @@ -796,7 +1206,8 @@ android_frame_up_to_date (struct frame *f) block_input (); FRAME_MOUSE_UPDATE (f); - if (!buffer_flipping_blocked_p ()) + if (!buffer_flipping_blocked_p () + && FRAME_ANDROID_NEED_BUFFER_FLIP (f)) show_back_buffer (f); /* The frame is now complete, as its contents have been drawn. */ @@ -808,7 +1219,10 @@ static void android_buffer_flipping_unblocked_hook (struct frame *f) { block_input (); - show_back_buffer (f); + + if (FRAME_ANDROID_NEED_BUFFER_FLIP (f)) + show_back_buffer (f); + unblock_input (); } @@ -935,7 +1349,25 @@ android_get_focus_frame (struct frame *f) static void android_focus_frame (struct frame *f, bool noactivate) { - /* TODO */ + /* Set the input focus to the frame's window. The system only lets + this work on child frames. */ + android_set_input_focus (FRAME_ANDROID_WINDOW (f), + ANDROID_CURRENT_TIME); +} + +/* The two procedures below only have to update the cursor on Android, + as there are no window borders there. */ + +static void +android_frame_highlight (struct frame *f) +{ + gui_update_cursor (f, true); +} + +static void +android_frame_unhighlight (struct frame *f) +{ + gui_update_cursor (f, true); } static void @@ -963,12 +1395,10 @@ android_frame_rehighlight (struct android_display_info *dpyinfo) if (dpyinfo->highlight_frame != old_highlight) { /* This is not yet required on Android. */ -#if 0 if (old_highlight) android_frame_unhighlight (old_highlight); if (dpyinfo->highlight_frame) android_frame_highlight (dpyinfo->highlight_frame); -#endif } } @@ -1027,7 +1457,8 @@ android_fullscreen_hook (struct frame *f) void android_iconify_frame (struct frame *f) { - /* TODO */ + /* This really doesn't work on Android. */ + error ("Can't notify window manager of iconification"); } static void @@ -1149,7 +1580,7 @@ android_set_offset (struct frame *f, int xoff, int yoff, static void android_set_alpha (struct frame *f) { - /* TODO */ + /* Not supported on Android. */ } static Lisp_Object @@ -1215,6 +1646,7 @@ android_free_frame_resources (struct frame *f) { struct android_display_info *dpyinfo; Mouse_HLInfo *hlinfo; + struct android_touch_point *last, *next; dpyinfo = FRAME_DISPLAY_INFO (f); hlinfo = &dpyinfo->mouse_highlight; @@ -1256,6 +1688,18 @@ android_free_frame_resources (struct frame *f) if (f == dpyinfo->last_mouse_frame) dpyinfo->last_mouse_frame = NULL; + /* Free all tool presses currently active on this frame. */ + next = FRAME_OUTPUT_DATA (f)->touch_points; + while (next) + { + last = next; + next = next->next; + xfree (last); + } + + /* Clear this in case unblock_input reads events. */ + FRAME_OUTPUT_DATA (f)->touch_points = NULL; + unblock_input (); } @@ -1316,8 +1760,8 @@ android_scroll_run (struct window *w, struct run *run) /* Cursor off. Will be switched on again in gui_update_window_end. */ gui_clear_cursor (w); - android_copy_area (FRAME_ANDROID_WINDOW (f), - FRAME_ANDROID_WINDOW (f), + android_copy_area (FRAME_ANDROID_DRAWABLE (f), + FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x, from_y, width, height, x, to_y); @@ -1337,7 +1781,9 @@ static void android_flip_and_flush (struct frame *f) { block_input (); - show_back_buffer (f); + + if (FRAME_ANDROID_NEED_BUFFER_FLIP (f)) + show_back_buffer (f); /* The frame is complete again as its contents were just flushed. */ @@ -1355,7 +1801,7 @@ android_clear_rectangle (struct frame *f, struct android_gc *gc, int x, | ANDROID_GC_FOREGROUND), &xgcv); android_set_foreground (gc, xgcv.background); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, x, y, width, height); android_set_foreground (gc, xgcv.foreground); } @@ -1405,7 +1851,7 @@ android_draw_fringe_bitmap (struct window *w, struct glyph_row *row, if (face->stipple) { android_set_fill_style (face->gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), face->gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), face->gc, p->bx, p->by, p->nx, p->ny); android_set_fill_style (face->gc, ANDROID_FILL_SOLID); @@ -1428,7 +1874,7 @@ android_draw_fringe_bitmap (struct window *w, struct glyph_row *row, unsigned long background, cursor_pixel; int depth; - drawable = FRAME_ANDROID_WINDOW (f); + drawable = FRAME_ANDROID_DRAWABLE (f); clipmask = ANDROID_NONE; background = face->background; cursor_pixel = f->output_data.android->cursor_pixel; @@ -1697,7 +2143,7 @@ android_draw_glyph_string_background (struct glyph_string *s, bool force_p) { /* Fill background with a stipple pattern. */ android_set_fill_style (s->gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, s->x, s->y + box_line_width, s->background_width, s->height - 2 * box_line_width); @@ -1734,7 +2180,7 @@ android_fill_triangle (struct frame *f, struct android_gc *gc, abc[1] = point2; abc[2] = point3; - android_fill_polygon (FRAME_ANDROID_WINDOW (f), + android_fill_polygon (FRAME_ANDROID_DRAWABLE (f), gc, abc, 3, ANDROID_CONVEX, ANDROID_COORD_MODE_ORIGIN); } @@ -1776,7 +2222,7 @@ android_clear_point (struct frame *f, struct android_gc *gc, android_get_gc_values (gc, ANDROID_GC_BACKGROUND | ANDROID_GC_FOREGROUND, &xgcv); android_set_foreground (gc, xgcv.background); - android_draw_point (FRAME_ANDROID_WINDOW (f), gc, x, y); + android_draw_point (FRAME_ANDROID_DRAWABLE (f), gc, x, y); android_set_foreground (gc, xgcv.foreground); } @@ -1798,7 +2244,7 @@ android_draw_relief_rect (struct frame *f, int left_x, int top_y, int right_x, black_gc = f->output_data.android->black_relief.gc; normal_gc = f->output_data.android->normal_gc; - drawable = FRAME_ANDROID_WINDOW (f); + drawable = FRAME_ANDROID_DRAWABLE (f); android_set_clip_rectangles (white_gc, 0, 0, clip_rect, 1); android_set_clip_rectangles (black_gc, 0, 0, clip_rect, 1); @@ -1811,11 +2257,11 @@ android_draw_relief_rect (struct frame *f, int left_x, int top_y, int right_x, /* Draw lines. */ if (top_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, left_x, top_y, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, left_x, top_y, right_x - left_x + 1, hwidth); if (left_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, left_x, top_y, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, left_x, top_y, vwidth, bottom_y - top_y + 1); if (raised_p) @@ -1824,12 +2270,12 @@ android_draw_relief_rect (struct frame *f, int left_x, int top_y, int right_x, gc = white_gc; if (bot_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, left_x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, left_x, bottom_y - hwidth + 1, right_x - left_x + 1, hwidth); if (right_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, right_x - vwidth + 1, top_y, vwidth, bottom_y - top_y + 1); @@ -1853,7 +2299,7 @@ android_draw_relief_rect (struct frame *f, int left_x, int top_y, int right_x, if (top_p && left_p && bot_p && right_p && hwidth > 1 && vwidth > 1) - android_draw_rectangle (FRAME_ANDROID_WINDOW (f), + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (f), black_gc, left_x, top_y, right_x - left_x, bottom_y - top_y); else @@ -1913,22 +2359,22 @@ android_draw_box_rect (struct glyph_string *s, android_set_clip_rectangles (s->gc, 0, 0, clip_rect, 1); /* Top. */ - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, left_x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, left_x, left_x, right_x - left_x + 1, hwidth); /* Left. */ if (left_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, left_x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, left_x, top_y, vwidth, bottom_y - top_y + 1); /* Bottom. */ - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, left_x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, left_x, bottom_y - hwidth + 1, right_x - left_x + 1, hwidth); /* Right. */ if (right_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, right_x - vwidth + 1, top_y, vwidth, bottom_y - top_y + 1); @@ -2153,7 +2599,7 @@ android_draw_glyph_string_bg_rect (struct glyph_string *s, int x, int y, { /* Fill background with a stipple pattern. */ android_set_fill_style (s->gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x, y, w, h); android_set_fill_style (s->gc, ANDROID_FILL_SOLID); } @@ -2294,7 +2740,7 @@ android_draw_image_foreground (struct glyph_string *s) if (gui_intersect_rectangles (&clip_rect, &image_rect, &r)) android_copy_area (s->img->pixmap, - FRAME_ANDROID_WINDOW (s->f), + FRAME_ANDROID_DRAWABLE (s->f), s->gc, s->slice.x + r.x - x, s->slice.y + r.y - y, r.width, r.height, r.x, r.y); @@ -2307,7 +2753,7 @@ android_draw_image_foreground (struct glyph_string *s) if (s->hl == DRAW_CURSOR && !s->img->mask) { int relief = eabs (s->img->relief); - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x - relief, y - relief, s->slice.width + relief*2 - 1, s->slice.height + relief*2 - 1); @@ -2317,7 +2763,7 @@ android_draw_image_foreground (struct glyph_string *s) } else /* Draw a rectangle if image could not be loaded. */ - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, x, y, + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x, y, s->slice.width - 1, s->slice.height - 1); } @@ -2444,7 +2890,7 @@ android_draw_stretch_glyph_string (struct glyph_string *s) { /* Fill background with a stipple pattern. */ android_set_fill_style (gc, ANDROID_FILL_OPAQUE_STIPPLED); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), gc, x, y, w, h); android_set_fill_style (gc, ANDROID_FILL_SOLID); @@ -2457,7 +2903,7 @@ android_draw_stretch_glyph_string (struct glyph_string *s) | ANDROID_GC_BACKGROUND), &xgcv); android_set_foreground (gc, xgcv.background); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), gc, x, y, w, h); android_set_foreground (gc, xgcv.foreground); } @@ -2536,7 +2982,7 @@ android_draw_underwave (struct glyph_string *s, int decoration_width) while (x1 <= xmax) { - android_draw_line (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_draw_line (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x1, y1, x2, y2); x1 = x2, y1 = y2; x2 += dx, y2 = y0 + odd*dy; @@ -2567,7 +3013,7 @@ android_draw_glyph_string_foreground (struct glyph_string *s) for (i = 0; i < s->nchars; ++i) { struct glyph *g = s->first_glyph + i; - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x, s->y, g->pixel_width - 1, s->height - 1); @@ -2618,7 +3064,7 @@ android_draw_composite_glyph_string_foreground (struct glyph_string *s) if (s->font_not_found_p) { if (s->cmp_from == 0) - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x, s->y, s->width - 1, s->height - 1); } @@ -2754,7 +3200,7 @@ android_draw_glyphless_glyph_string_foreground (struct glyph_string *s) false); } if (glyph->u.glyphless.method != GLYPHLESS_DISPLAY_THIN_SPACE) - android_draw_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, x, s->ybase - glyph->ascent, glyph->pixel_width - 1, glyph->ascent + glyph->descent - 1); @@ -2987,14 +3433,14 @@ android_draw_glyph_string (struct glyph_string *s) s->underline_position = position; y = s->ybase + position; if (s->face->underline_defaulted_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, s->x, y, decoration_width, thickness); else { struct android_gc_values xgcv; android_get_gc_values (s->gc, ANDROID_GC_FOREGROUND, &xgcv); android_set_foreground (s->gc, s->face->underline_color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, s->x, y, decoration_width, thickness); android_set_foreground (s->gc, xgcv.foreground); } @@ -3006,7 +3452,7 @@ android_draw_glyph_string (struct glyph_string *s) unsigned long dy = 0, h = 1; if (s->face->overline_color_defaulted_p) - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, s->x, s->y + dy, decoration_width, h); else @@ -3014,8 +3460,8 @@ android_draw_glyph_string (struct glyph_string *s) struct android_gc_values xgcv; android_get_gc_values (s->gc, ANDROID_GC_FOREGROUND, &xgcv); android_set_foreground (s->gc, s->face->overline_color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, s->x, - s->y + dy, decoration_width, h); + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, + s->x, s->y + dy, decoration_width, h); android_set_foreground (s->gc, xgcv.foreground); } } @@ -3044,8 +3490,9 @@ android_draw_glyph_string (struct glyph_string *s) struct android_gc_values xgcv; android_get_gc_values (s->gc, ANDROID_GC_FOREGROUND, &xgcv); android_set_foreground (s->gc, s->face->strike_through_color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (s->f), s->gc, s->x, - glyph_y + dy, decoration_width, h); + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, + s->x, glyph_y + dy, decoration_width, + h); android_set_foreground (s->gc, xgcv.foreground); } } @@ -3125,7 +3572,7 @@ static void android_clear_frame_area (struct frame *f, int x, int y, int width, int height) { - android_clear_area (FRAME_ANDROID_WINDOW (f), + android_clear_area (FRAME_ANDROID_DRAWABLE (f), x, y, width, height); } @@ -3154,25 +3601,25 @@ android_clear_under_internal_border (struct frame *f) struct android_gc *gc = f->output_data.android->normal_gc; android_set_foreground (gc, color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, 0, margin, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, 0, margin, width, border); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, 0, 0, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, 0, 0, border, height); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, width - border, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, width - border, 0, border, height); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, 0, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, 0, height - border, width, border); android_set_foreground (gc, FRAME_FOREGROUND_PIXEL (f)); } else { - android_clear_area (FRAME_ANDROID_WINDOW (f), 0, 0, + android_clear_area (FRAME_ANDROID_DRAWABLE (f), 0, 0, border, height); - android_clear_area (FRAME_ANDROID_WINDOW (f), 0, + android_clear_area (FRAME_ANDROID_DRAWABLE (f), 0, margin, width, border); - android_clear_area (FRAME_ANDROID_WINDOW (f), width - border, + android_clear_area (FRAME_ANDROID_DRAWABLE (f), width - border, 0, border, height); - android_clear_area (FRAME_ANDROID_WINDOW (f), 0, + android_clear_area (FRAME_ANDROID_DRAWABLE (f), 0, height - border, width, border); } } @@ -3221,7 +3668,7 @@ android_draw_hollow_cursor (struct window *w, struct glyph_row *row) } /* Set clipping, draw the rectangle, and reset clipping again. */ android_clip_to_row (w, row, TEXT_AREA, gc); - android_draw_rectangle (FRAME_ANDROID_WINDOW (f), gc, x, y, wd, h - 1); + android_draw_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, x, y, wd, h - 1); android_reset_clip_rectangles (f, gc); } @@ -3295,7 +3742,7 @@ android_draw_bar_cursor (struct window *w, struct glyph_row *row, int width, if ((cursor_glyph->resolved_level & 1) != 0) x += cursor_glyph->pixel_width - width; - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, x, WINDOW_TO_FRAME_PIXEL_Y (w, w->phys_cursor.y), width, row->height); } @@ -3318,7 +3765,7 @@ android_draw_bar_cursor (struct window *w, struct glyph_row *row, int width, if ((cursor_glyph->resolved_level & 1) != 0 && cursor_glyph->pixel_width > w->phys_cursor_width - 1) x += cursor_glyph->pixel_width - w->phys_cursor_width + 1; - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), gc, x, + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), gc, x, cursor_start_y, w->phys_cursor_width - 1, width); } @@ -3387,7 +3834,7 @@ android_draw_vertical_window_border (struct window *w, int x, int y0, int y1) android_set_foreground (f->output_data.android->normal_gc, face->foreground); - android_draw_line (FRAME_ANDROID_WINDOW (f), + android_draw_line (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x, y0, x, y1); } @@ -3415,17 +3862,17 @@ android_draw_window_divider (struct window *w, int x0, int x1, int y0, int y1) { android_set_foreground (f->output_data.android->normal_gc, color_first); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0, y0, 1, y1 - y0); android_set_foreground (f->output_data.android->normal_gc, color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0 + 1, y0, x1 - x0 - 2, y1 - y0); android_set_foreground (f->output_data.android->normal_gc, color_last); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x1 - 1, y0, 1, y1 - y0); } @@ -3435,16 +3882,16 @@ android_draw_window_divider (struct window *w, int x0, int x1, int y0, int y1) { android_set_foreground (f->output_data.android->normal_gc, color_first); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0, y0, x1 - x0, 1); android_set_foreground (f->output_data.android->normal_gc, color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0, y0 + 1, x1 - x0, y1 - y0 - 2); android_set_foreground (f->output_data.android->normal_gc, color_last); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0, y1 - 1, x1 - x0, 1); } @@ -3453,7 +3900,7 @@ android_draw_window_divider (struct window *w, int x0, int x1, int y0, int y1) /* In any other case do not draw the first and last pixels differently. */ android_set_foreground (f->output_data.android->normal_gc, color); - android_fill_rectangle (FRAME_ANDROID_WINDOW (f), + android_fill_rectangle (FRAME_ANDROID_DRAWABLE (f), f->output_data.android->normal_gc, x0, y0, x1 - x0, y1 - y0); } diff --git a/src/androidterm.h b/src/androidterm.h index 814bcf9f08c..ebde15c40a8 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -134,6 +134,18 @@ struct android_display_info Time last_mouse_movement_time; }; +/* Structure representing a single tool (finger or stylus) pressed + onto a frame. */ + +struct android_touch_point +{ + /* The next tool on this list. */ + struct android_touch_point *next; + + /* The tool ID and the last known X and Y positions. */ + int tool_id, x, y; +}; + struct android_output { /* Graphics contexts for the default font. */ @@ -201,6 +213,10 @@ struct android_output input. */ bool_bf complete : 1; + /* True that indicates whether or not a buffer flip is required + because the frame contents have been dirtied. */ + bool_bf need_buffer_flip : 1; + /* Relief GCs, colors etc. */ struct relief { struct android_gc *gc; @@ -214,6 +230,10 @@ struct android_output /* Focus state. Only present for consistency with X; it is actually a boolean. */ int focus_state; + + /* List of all tools (either styluses or fingers) pressed onto the + frame. */ + struct android_touch_point *touch_points; }; enum @@ -240,6 +260,14 @@ enum #define FRAME_ANDROID_NEED_BUFFER_FLIP(f) \ ((f)->output_data.android->need_buffer_flip) +/* Return the drawable used for rendering to frame F and mark the + frame as needing a buffer flip later. There's no easy way to run + code after any drawing command, but code can be run whenever + someone asks for the handle necessary to draw. */ +#define FRAME_ANDROID_DRAWABLE(f) \ + (((f))->output_data.android->need_buffer_flip = true, \ + FRAME_ANDROID_WINDOW ((f))) + /* Return whether or not the frame F has been completely drawn. Used while handling async input. */ #define FRAME_ANDROID_COMPLETE_P(f) \ diff --git a/src/dired.c b/src/dired.c index 084f3225cb2..57d79b84463 100644 --- a/src/dired.c +++ b/src/dired.c @@ -44,6 +44,21 @@ along with GNU Emacs. If not, see . */ #include "msdos.h" /* for fstatat */ #endif +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) +typedef DIR emacs_dir; +#define emacs_readdir readdir +#define emacs_closedir closedir +#else + +#include "android.h" + +/* The Android emulation of dirent stuff is required to be able to + list the /assets special directory. */ +typedef struct android_dir emacs_dir; +#define emacs_readdir android_readdir +#define emacs_closedir android_closedir +#endif + #ifdef WINDOWSNT extern int is_slow_fs (const char *); #endif @@ -78,19 +93,30 @@ dirent_type (struct dirent *dp) #endif } -static DIR * +static emacs_dir * open_directory (Lisp_Object dirname, Lisp_Object encoded_dirname, int *fdp) { char *name = SSDATA (encoded_dirname); - DIR *d; + emacs_dir *d; int fd, opendir_errno; -#ifdef DOS_NT - /* Directories cannot be opened. The emulation assumes that any - file descriptor other than AT_FDCWD corresponds to the most - recently opened directory. This hack is good enough for Emacs. */ +#if defined DOS_NT || (defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + /* On DOS_NT, directories cannot be opened. The emulation assumes + that any file descriptor other than AT_FDCWD corresponds to the + most recently opened directory. This hack is good enough for + Emacs. + + This code is also used on Android for a different reason: a + special `assets' directory outside the normal file system is used + to open assets inside the Android application package, and must + be listed using the opendir-like interface provided in + android.h. */ fd = 0; +#ifndef HAVE_ANDROID d = opendir (name); +#else + d = android_opendir (name); +#endif opendir_errno = errno; #else fd = emacs_open (name, O_RDONLY | O_DIRECTORY, 0); @@ -125,7 +151,7 @@ directory_files_internal_w32_unwind (Lisp_Object arg) static void directory_files_internal_unwind (void *d) { - closedir (d); + emacs_closedir (d); } /* Return the next directory entry from DIR; DIR's name is DIRNAME. @@ -133,12 +159,12 @@ directory_files_internal_unwind (void *d) Signal any unrecoverable errors. */ static struct dirent * -read_dirent (DIR *dir, Lisp_Object dirname) +read_dirent (emacs_dir *dir, Lisp_Object dirname) { while (true) { errno = 0; - struct dirent *dp = readdir (dir); + struct dirent *dp = emacs_readdir (dir); if (dp || errno == 0) return dp; if (! (errno == EAGAIN || errno == EINTR)) @@ -190,7 +216,7 @@ directory_files_internal (Lisp_Object directory, Lisp_Object full, Lisp_Object encoded_dirfilename = ENCODE_FILE (dirfilename); int fd; - DIR *d = open_directory (dirfilename, encoded_dirfilename, &fd); + emacs_dir *d = open_directory (dirfilename, encoded_dirfilename, &fd); /* Unfortunately, we can now invoke expand-file-name and file-attributes on filenames, both of which can throw, so we must @@ -300,7 +326,7 @@ directory_files_internal (Lisp_Object directory, Lisp_Object full, list = Fcons (attrs ? Fcons (finalname, fileattrs) : finalname, list); } - closedir (d); + emacs_closedir (d); #ifdef WINDOWSNT if (attrs) Vw32_get_true_file_attributes = w32_save; @@ -514,7 +540,7 @@ file_name_completion (Lisp_Object file, Lisp_Object dirname, bool all_flag, } } int fd; - DIR *d = open_directory (dirname, encoded_dir, &fd); + emacs_dir *d = open_directory (dirname, encoded_dir, &fd); record_unwind_protect_ptr (directory_files_internal_unwind, d); /* Loop reading directory entries. */ diff --git a/src/dispnew.c b/src/dispnew.c index 12814bc0b75..f31e185a84b 100644 --- a/src/dispnew.c +++ b/src/dispnew.c @@ -3175,6 +3175,7 @@ redraw_frame (struct frame *f) its redisplay done. */ mark_window_display_accurate (FRAME_ROOT_WINDOW (f), 0); set_window_update_flags (XWINDOW (FRAME_ROOT_WINDOW (f)), true); + f->garbaged = false; } @@ -6053,7 +6054,7 @@ FILE = nil means just close any termscript file currently open. */) if (tty->termscript != 0) { block_input (); - fclose (tty->termscript); + emacs_fclose (tty->termscript); tty->termscript = 0; unblock_input (); } diff --git a/src/fileio.c b/src/fileio.c index 9d81f0ca545..6fa524b3bb4 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -277,7 +277,7 @@ void fclose_unwind (void *arg) { FILE *stream = arg; - fclose (stream); + emacs_fclose (stream); } /* Restore point, having saved it as a marker. */ @@ -2989,6 +2989,12 @@ If there is no error, returns nil. */) encoded_filename = ENCODE_FILE (absname); +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + /* FILE may be some kind of special Android file. */ + if (android_file_access_p (SSDATA (encoded_filename), R_OK)) + return Qnil; +#endif + if (faccessat (AT_FDCWD, SSDATA (encoded_filename), R_OK, AT_EACCESS) != 0) report_file_error (SSDATA (string), filename); @@ -3205,7 +3211,11 @@ file_accessible_directory_p (Lisp_Object file) There are three exceptions: "", "/", and "//". Leave "" alone, as it's invalid. Append only "." to the other two exceptions as "/" and "//" are distinct on some platforms, whereas "/", "///", - "////", etc. are all equivalent. */ + "////", etc. are all equivalent. + + Android has a special directory named "/assets". There is no "." + directory there, but appending a "/" is sufficient to check + whether or not it is a directory. */ if (! len) dir = data; else @@ -3215,11 +3225,27 @@ file_accessible_directory_p (Lisp_Object file) special cases "/" and "//", and it's a safe optimization here. After appending '.', append another '/' to work around a macOS bug (Bug#30350). */ - static char const appended[] = "/./"; - char *buf = SAFE_ALLOCA (len + sizeof appended); - memcpy (buf, data, len); - strcpy (buf + len, &appended[data[len - 1] == '/']); - dir = buf; +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + if (!strncmp ("/assets/", data, + sizeof "/assets" - 1)) + { + static char const appended[] = "/"; + char *buf = SAFE_ALLOCA (len + sizeof appended); + memcpy (buf, data, len); + strcpy (buf + len, &appended[data[len - 1] == '/']); + dir = buf; + } + else + { +#endif + static char const appended[] = "/./"; + char *buf = SAFE_ALLOCA (len + sizeof appended); + memcpy (buf, data, len); + strcpy (buf + len, &appended[data[len - 1] == '/']); + dir = buf; +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + } +#endif } ok = file_access_p (dir, F_OK); @@ -5973,7 +5999,7 @@ do_auto_save_unwind (void *arg) if (stream != NULL) { block_input (); - fclose (stream); + emacs_fclose (stream); unblock_input (); } } diff --git a/src/keyboard.c b/src/keyboard.c index daca964b981..78637ef4f15 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -4956,8 +4956,8 @@ const char *const lispy_function_keys[] = [66] = "return", [67] = "backspace", [82] = "menu", - [92] = "page-up", - [93] = "page-down", + [92] = "prior", + [93] = "next", }; #elif defined HAVE_NTGUI @@ -11219,7 +11219,7 @@ This may include sensitive information such as passwords. */) if (dribble) { block_input (); - fclose (dribble); + emacs_fclose (dribble); unblock_input (); dribble = 0; } diff --git a/src/lisp.h b/src/lisp.h index bf8ff5cdc18..f64a27ce113 100644 --- a/src/lisp.h +++ b/src/lisp.h @@ -28,6 +28,7 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include #ifdef HAVE_SYS_STAT_H #include @@ -5077,6 +5078,7 @@ extern int emacs_open (const char *, int, int); extern int emacs_open_noquit (const char *, int, int); extern int emacs_pipe (int[2]); extern int emacs_close (int); +extern int emacs_fclose (FILE *); extern ptrdiff_t emacs_read (int, void *, ptrdiff_t); extern ptrdiff_t emacs_read_quit (int, void *, ptrdiff_t); extern ptrdiff_t emacs_write (int, void const *, ptrdiff_t); diff --git a/src/lread.c b/src/lread.c index 2ac3c85ea81..d585dece392 100644 --- a/src/lread.c +++ b/src/lread.c @@ -1141,7 +1141,7 @@ close_infile_unwind (void *arg) { struct infile *prev_infile = arg; eassert (infile && infile != prev_infile); - fclose (infile->stream); + emacs_fclose (infile->stream); infile = prev_infile; } diff --git a/src/sfnt.c b/src/sfnt.c index 20cf2376d85..9b6c421212a 100644 --- a/src/sfnt.c +++ b/src/sfnt.c @@ -3072,8 +3072,8 @@ sfnt_curve_is_flat (struct sfnt_point control0, /* 2.0 is a constant describing the area covered at which point the curve is considered "flat". */ - return (abs (sfnt_mul_fixed (g.x, h.x) - - sfnt_mul_fixed (g.y, h.y)) + return (abs (sfnt_mul_fixed (g.x, h.y) + - sfnt_mul_fixed (g.y, h.x)) <= 0400000); } @@ -3261,9 +3261,11 @@ sfnt_prepare_raster (struct sfnt_raster *raster, struct sfnt_glyph_outline *outline) { raster->width - = sfnt_ceil_fixed (outline->xmax - outline->xmin) >> 16; + = (sfnt_ceil_fixed (outline->xmax) + - sfnt_floor_fixed (outline->xmin)) >> 16; raster->height - = sfnt_ceil_fixed (outline->ymax - outline->ymin) >> 16; + = (sfnt_ceil_fixed (outline->ymax) + - sfnt_floor_fixed (outline->ymin)) >> 16; raster->refcount = 0; /* Align the raster to a SFNT_POLY_ALIGNMENT byte boundary. */ @@ -3292,10 +3294,10 @@ sfnt_step_edge (struct sfnt_edge *edge) } /* Build a list of edges for each contour in OUTLINE, applying - OUTLINE->xmin and OUTLINE->ymin as the offset to each edge. Call - EDGE_PROC with DCONTEXT and the resulting edges as arguments. It - is OK to modify the edges given to EDGE_PROC. Align all edges to - the sub-pixel grid. */ + OUTLINE->xmin and floor (OUTLINE->ymin) as the offset to each edge. + Call EDGE_PROC with DCONTEXT and the resulting edges as arguments. + It is OK to modify the edges given to EDGE_PROC. Align all edges + to the sub-pixel grid. */ static void sfnt_build_outline_edges (struct sfnt_glyph_outline *outline, @@ -3303,13 +3305,18 @@ sfnt_build_outline_edges (struct sfnt_glyph_outline *outline, { struct sfnt_edge *edges; size_t i, edge, next_vertex; - sfnt_fixed dx, dy, bot, step_x; + sfnt_fixed dx, dy, bot, step_x, ymin, xmin; int inc_x; size_t top, bottom, y; edges = alloca (outline->outline_used * sizeof *edges); edge = 0; + /* ymin and xmin must be the same as the offset used to set offy and + offx in rasters. */ + ymin = sfnt_floor_fixed (outline->ymin); + xmin = sfnt_floor_fixed (outline->xmin); + for (i = 0; i < outline->outline_used; ++i) { /* Set NEXT_VERTEX to the next point (vertex) in this contour. @@ -3356,12 +3363,12 @@ sfnt_build_outline_edges (struct sfnt_glyph_outline *outline, top = next_vertex; } - bot = (outline->outline[bottom].y - outline->ymin); - edges[edge].top = (outline->outline[top].y - outline->ymin); + bot = (outline->outline[bottom].y - ymin); + edges[edge].top = (outline->outline[top].y - ymin); /* Record the edge. Rasterization happens from bottom to up, so record the X at the bottom. */ - edges[edge].x = (outline->outline[bottom].x - outline->xmin); + edges[edge].x = (outline->outline[bottom].x - xmin); dx = (outline->outline[top].x - outline->outline[bottom].x); dy = abs (outline->outline[top].y - outline->outline[bottom].y); @@ -4585,7 +4592,7 @@ main (int argc, char **argv) /* Time this important bit. */ clock_gettime (CLOCK_THREAD_CPUTIME_ID, &start); outline = sfnt_build_glyph_outline (glyph, head, - 45, + 12, sfnt_test_get_glyph, sfnt_test_free_glyph, &dcontext); @@ -4652,7 +4659,7 @@ main (int argc, char **argv) if (hmtx && head) { - if (!sfnt_lookup_glyph_metrics (code, 36, + if (!sfnt_lookup_glyph_metrics (code, 12, &metrics, hmtx, hhea, head, maxp)) diff --git a/src/sfntfont-android.c b/src/sfntfont-android.c index 47aa27dc113..01bfdbaaf58 100644 --- a/src/sfntfont-android.c +++ b/src/sfntfont-android.c @@ -42,19 +42,6 @@ static Lisp_Object font_cache; -static unsigned int -sfntfont_android_saturate32 (unsigned int a, unsigned int b) -{ - unsigned int c; - - c = a + b; - - if (c < a) - c = -1; - - return c; -} - /* Scale each of the four packed bytes in P in the low 16 bits of P by SCALE. Return the result. @@ -107,8 +94,9 @@ sfntfont_android_blend (unsigned int src, unsigned int dst) src = src & ~0x00ff00ff; src |= (src_rb >> 16 | src_rb << 16); - /* Saturating is unnecessary but helps find bugs. */ - return sfntfont_android_saturate32 (both, src); + /* This addition need not be saturating because both has already + been multiplied by 255 - a. */ + return both + src; } #define U255TO256(x) ((unsigned short) (x) + ((x) >> 7)) @@ -128,8 +116,9 @@ sfntfont_android_blendrgb (unsigned int src, unsigned int dst) both = ag_part | rb_part; - /* Saturating is unnecessary but helps find bugs. */ - return sfntfont_android_saturate32 (both, src); + /* This addition need not be saturating because both has already + been multiplied by 255 - a. */ + return both + src; } /* Composite the bitmap described by BUFFER, STRIDE and TEXT_RECTANGLE @@ -162,6 +151,10 @@ sfntfont_android_composite_bitmap (unsigned char *restrict buffer, src_y = i + (rect->y - text_rectangle->y); + if (src_y > text_rectangle->height) + /* Huh? */ + return; + src_row = (unsigned int *) ((buffer + src_y * stride)); dst_row = (unsigned int *) (dest + ((i + rect->y) * bitmap_info->stride)); @@ -343,7 +336,7 @@ sfntfont_android_put_glyphs (struct glyph_string *s, int from, } /* Lock the bitmap. It must be unlocked later. */ - bitmap_data = android_lock_bitmap (FRAME_ANDROID_WINDOW (s->f), + bitmap_data = android_lock_bitmap (FRAME_ANDROID_DRAWABLE (s->f), &bitmap_info, &bitmap); /* If locking the bitmap fails, just discard the data that was @@ -385,7 +378,7 @@ sfntfont_android_put_glyphs (struct glyph_string *s, int from, ANDROID_DELETE_LOCAL_REF (bitmap); /* Damage the window by the text rectangle. */ - android_damage_window (FRAME_ANDROID_WINDOW (s->f), + android_damage_window (FRAME_ANDROID_DRAWABLE (s->f), &text_rectangle); /* Release the temporary scanline buffer. */ @@ -495,13 +488,19 @@ init_sfntfont_android (void) version of Android the device is running. */ if (android_get_device_api_level () >= 15) Vsfnt_default_family_alist - = list2 (Fcons (build_string ("Monospace"), + = list3 (Fcons (build_string ("Monospace"), + build_string ("Droid Sans Mono")), + /* Android doesn't come with a Monospace Serif font, so + this will have to do. */ + Fcons (build_string ("Monospace Serif"), build_string ("Droid Sans Mono")), Fcons (build_string ("Sans Serif"), build_string ("Roboto"))); else Vsfnt_default_family_alist - = list2 (Fcons (build_string ("Monospace"), + = list3 (Fcons (build_string ("Monospace"), + build_string ("Droid Sans Mono")), + Fcons (build_string ("Monospace Serif"), build_string ("Droid Sans Mono")), Fcons (build_string ("Sans Serif"), build_string ("Droid Sans"))); diff --git a/src/sfntfont.c b/src/sfntfont.c index 9206fbc6629..25cea59f6a7 100644 --- a/src/sfntfont.c +++ b/src/sfntfont.c @@ -1982,6 +1982,9 @@ sfntfont_text_extents (struct font *font, const unsigned int *code, total_width = 0; + /* First clear the metrics array. */ + memset (metrics, 0, sizeof *metrics); + /* Get the metrcs one by one, then sum them up. */ for (i = 0; i < nglyphs; ++i) { @@ -2059,7 +2062,7 @@ sfntfont_draw (struct glyph_string *s, int from, int to, struct sfnt_glyph_metrics metrics; length = to - from; - font = s->face->font; + font = s->font; info = (struct sfnt_font_info *) font; rasters = alloca (length * sizeof *rasters); diff --git a/src/sysdep.c b/src/sysdep.c index dd97ae1cb38..4d89d4f25ae 100644 --- a/src/sysdep.c +++ b/src/sysdep.c @@ -2335,7 +2335,8 @@ emacs_backtrace (int backtrace_limit) } } -#ifndef HAVE_NTGUI +#if !defined HAVE_NTGUI && !(defined HAVE_ANDROID \ + && !defined ANDROID_STUBIFY) void emacs_abort (void) { @@ -2568,6 +2569,20 @@ emacs_close (int fd) } } +/* Wrapper around fclose. On Android, this calls `android_fclose' to + clear information associated with the FILE's file descriptor if + necessary. */ + +int +emacs_fclose (FILE *stream) +{ +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + return fclose (stream); +#else + return android_fclose (stream); +#endif +} + /* Maximum number of bytes to read or write in a single system call. This works around a serious bug in Linux kernels before 2.6.16; see . diff --git a/src/term.c b/src/term.c index 1a21b6f5764..05163cce533 100644 --- a/src/term.c +++ b/src/term.c @@ -2354,8 +2354,8 @@ A suspended tty may be resumed by calling `resume-tty' on it. */) #ifndef MSDOS if (f != t->display_info.tty->output) - fclose (t->display_info.tty->output); - fclose (f); + emacs_fclose (t->display_info.tty->output); + emacs_fclose (f); #endif t->display_info.tty->input = 0; @@ -4632,12 +4632,12 @@ delete_tty (struct terminal *terminal) { delete_keyboard_wait_descriptor (fileno (tty->input)); if (tty->input != stdin) - fclose (tty->input); + emacs_fclose (tty->input); } if (tty->output && tty->output != stdout && tty->output != tty->input) - fclose (tty->output); + emacs_fclose (tty->output); if (tty->termscript) - fclose (tty->termscript); + emacs_fclose (tty->termscript); xfree (tty->old_tty); xfree (tty->Wcm); diff --git a/src/verbose.mk.in b/src/verbose.mk.in index 25dff990881..f826bf18a0a 100644 --- a/src/verbose.mk.in +++ b/src/verbose.mk.in @@ -83,5 +83,5 @@ AM_V_RC = @$(info $ RC $@) # These are used for the Android port. AM_V_JAVAC = @$(info $ JAVAC $@) -AM_V_DX = @$(info $ DX $@) +AM_V_D8 = @$(info $ D8 $@) endif -- cgit v1.3 From 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 +++- src/android.c | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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; diff --git a/src/android.c b/src/android.c index eab18dc6152..fba43129ee3 100644 --- a/src/android.c +++ b/src/android.c @@ -3148,6 +3148,7 @@ android_query_tree (android_window handle, android_window *root_return, *root_return = 0; *parent_return = shorts[0]; *children_return = children; + *nchildren_return = nelements - 1; /* Release the array contents. */ (*android_java_env)->ReleaseShortArrayElements (android_java_env, array, -- cgit v1.3 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/Makefile.in | 2 +- java/debug.sh | 110 +++-- 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 ++++++- src/android.c | 169 ++++++- src/android.h | 6 + src/androidfns.c | 677 ++++++++++++++++++++++++++++- src/androidgui.h | 14 +- src/androidmenu.c | 293 +++++++++++++ src/androidterm.c | 34 +- src/androidterm.h | 6 + src/emacs.c | 4 + xcompile/Makefile.in | 13 +- 18 files changed, 1685 insertions(+), 112 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsContextMenu.java (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/Makefile.in b/java/Makefile.in index 05e61dede89..c539fb0f1fb 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -168,4 +168,4 @@ clean: rm -rf install-temp find . -name '*.class' -delete -maintainer-clean: clean +maintainer-clean distclean bootstrap-clean: clean diff --git a/java/debug.sh b/java/debug.sh index 3e3e3d9c281..aa80aeeebcd 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -31,6 +31,7 @@ gdb_port=5039 jdb_port=64013 jdb=no attach_existing=no +gdbserver= while [ $# -gt 0 ]; do case "$1" in @@ -41,6 +42,7 @@ while [ $# -gt 0 ]; do echo "You must specify an argument to --device" exit 1 fi + shift ;; "--help" ) echo "Usage: $progname [options] -- [gdb options]" @@ -50,6 +52,7 @@ while [ $# -gt 0 ]; do echo " --jdb-port PORT run the JDB server on a specific port" echo " --jdb run JDB instead of GDB" echo " --attach-existing attach to an existing process" + echo " --gdbserver BINARY upload and use the specified gdbserver binary" echo " --help print this message" echo "" echo "Available devices:" @@ -62,9 +65,18 @@ while [ $# -gt 0 ]; do "--jdb" ) jdb=yes ;; + "--gdbserver" ) + shift + gdbserver=$1 + ;; "--port" ) + shift gdb_port=$1 ;; + "--jdb-port" ) + shift + jdb_port=$1 + ;; "--attach-existing" ) attach_existing=yes ;; @@ -170,46 +182,71 @@ elif [ -z $package_pids ]; then exit 1 fi -# Start JDB to make the wait dialog disappear. -echo "Attaching JDB to unblock the application." -adb -s $device forward --remove-all -adb -s $device forward "tcp:$jdb_port" "jdwp:$pid" +# This isn't necessary when attaching gdb to an existing process. +if [ "$jdb" = "yes" ] || [ "$attach_existing" != yes ]; then + # Start JDB to make the wait dialog disappear. + echo "Attaching JDB to unblock the application." + adb -s $device forward --remove-all + adb -s $device forward "tcp:$jdb_port" "jdwp:$pid" -if [ ! $? ]; then - echo "Failed to forward jdwp:$pid to $jdb_port!" - echo "Perhaps you need to specify a different port with --port?" - exit 1; -fi + if [ ! $? ]; then + echo "Failed to forward jdwp:$pid to $jdb_port!" + echo "Perhaps you need to specify a different port with --port?" + exit 1; + fi -jdb_command="jdb -connect \ + jdb_command="jdb -connect \ com.sun.jdi.SocketAttach:hostname=localhost,port=$jdb_port" -if [ $jdb = "yes" ]; then - # Just start JDB and then exit - $jdb_command - exit 1 -fi + if [ $jdb = "yes" ]; then + # Just start JDB and then exit + $jdb_command + exit 1 + fi -exec 4<> /tmp/file-descriptor-stamp + exec 4<> /tmp/file-descriptor-stamp -# Now run JDB with IO redirected to file descriptor 4 in a subprocess. -$jdb_command <&4 >&4 & + # Now run JDB with IO redirected to file descriptor 4 in a subprocess. + $jdb_command <&4 >&4 & -character= -# Next, wait until the prompt is found. -while read -n1 -u 4 character; do - if [ "$character" = ">" ]; then - echo "JDB attached successfully" - break; - fi -done + character= + # Next, wait until the prompt is found. + while read -n1 -u 4 character; do + if [ "$character" = ">" ]; then + echo "JDB attached successfully" + break; + fi + done +fi + +# See if gdbserver has to be uploaded +if [ -z "$gdbserver" ]; then + gdbserver_bin=/system/bin/gdbserver +else + gdbserver_bin=/data/local/tmp/gdbserver + + # Upload the specified gdbserver binary to the device. + adb -s $device push "$gdbserver" "$gdbserver_bin" + adb -s $device shell chmod +x "$gdbserver_bin" +fi # Now start gdbserver on the device asynchronously. echo "Attaching gdbserver to $pid on $device..." exec 5<> /tmp/file-descriptor-stamp -adb -s $device shell run-as $package /system/bin/gdbserver --once \ - "+debug.$package_uid.socket" --attach $pid >&5 & + +if [ -z "$gdbserver" ]; then + adb -s $device shell run-as $package $gdbserver_bin --once \ + "+debug.$package_uid.socket" --attach $pid >&5 & + gdb_socket="localfilesystem:$app_data_dir/debug.$package_uid.socket" +else + # Normally the program cannot access $gdbserver_bin when it is + # placed in /data/local/tmp. + adb -s $device shell $gdbserver_bin --once \ + "+/data/local/tmp/debug.$package_uid.socket" \ + --attach $pid >&5 & + gdb_socket="localfilesystem:/data/local/tmp/debug.$package_uid.socket" +fi # Wait until gdbserver successfully runs. line= @@ -227,16 +264,17 @@ while read -u 5 line; do esac done -# Send EOF to JDB to make it go away. This will also cause Android to -# allow Emacs to continue executing. -echo "Making JDB go away..." -echo "exit" >&4 -read -u 4 line -echo "JDB has gone away with $line" +if [ "$attach_existing" != "yes" ]; then + # Send EOF to JDB to make it go away. This will also cause + # Android to allow Emacs to continue executing. + echo "Making JDB go away..." + echo "exit" >&4 + read -u 4 line + echo "JDB has gone away with $line" +fi # Forward the gdb server port here. -adb -s $device forward "tcp:$gdb_port" \ - "localfilesystem:$app_data_dir/debug.$package_uid.socket" +adb -s $device forward "tcp:$gdb_port" $gdb_socket if [ ! $? ]; then echo "Failed to forward $app_data_dir/debug.$package_uid.socket" echo "to $gdb_port! Perhaps you need to specify a different port" 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; + } }; diff --git a/src/android.c b/src/android.c index fba43129ee3..e4022501f9d 100644 --- a/src/android.c +++ b/src/android.c @@ -87,6 +87,7 @@ struct android_emacs_service jmethodID get_screen_width; jmethodID get_screen_height; jmethodID detect_mouse; + jmethodID name_keysym; }; struct android_emacs_pixmap @@ -229,14 +230,14 @@ static volatile bool android_pselect_completed; /* The global event queue. */ static struct android_event_queue event_queue; -/* Semaphore used to signal select completion. */ -static sem_t android_pselect_sem; +/* Semaphores used to signal select completion and start. */ +static sem_t android_pselect_sem, android_pselect_start_sem; static void * android_run_select_thread (void *data) { sigset_t signals; - int sig, rc; + int rc; sigfillset (&signals); @@ -245,23 +246,10 @@ android_run_select_thread (void *data) "pthread_sigmask: %s", strerror (errno)); - sigemptyset (&signals); - sigaddset (&signals, SIGUSR1); - - if (pthread_sigmask (SIG_UNBLOCK, &signals, NULL)) - __android_log_print (ANDROID_LOG_FATAL, __func__, - "pthread_sigmask: %s", - strerror (errno)); - - sigemptyset (&signals); - sigaddset (&signals, SIGUSR2); - while (true) { - /* Keep waiting for SIGUSR2, ignoring EINTR in the meantime. */ - - while (sigwait (&signals, &sig)) - /* Spin. */; + /* Wait for the thread to be released. */ + sem_wait (&android_pselect_start_sem); /* Get the select lock and call pselect. */ pthread_mutex_lock (&event_queue.select_mutex); @@ -322,6 +310,7 @@ android_init_events (void) strerror (errno)); sem_init (&android_pselect_sem, 0, 0); + sem_init (&android_pselect_start_sem, 0, 0); event_queue.events.next = &event_queue.events; event_queue.events.last = &event_queue.events; @@ -444,7 +433,9 @@ android_select (int nfds, fd_set *readfds, fd_set *writefds, android_pselect_sigset = sigset; pthread_mutex_unlock (&event_queue.select_mutex); - pthread_kill (event_queue.select_thread, SIGUSR2); + /* Release the select thread. */ + sem_post (&android_pselect_start_sem); + pthread_cond_wait (&event_queue.read_var, &event_queue.mutex); /* Interrupt the select thread now, in case it's still in @@ -1058,6 +1049,7 @@ android_init_emacs_service (void) FIND_METHOD (get_screen_width, "getScreenWidth", "(Z)I"); FIND_METHOD (get_screen_height, "getScreenHeight", "(Z)I"); FIND_METHOD (detect_mouse, "detectMouse", "()Z"); + FIND_METHOD (name_keysym, "nameKeysym", "(I)Ljava/lang/String;"); #undef FIND_METHOD } @@ -1678,6 +1670,7 @@ android_create_window (android_window parent, int x, int y, jobject object, parent_object, old; android_window window; android_handle prev_max_handle; + bool override_redirect; parent_object = android_resolve_handle (parent, ANDROID_HANDLE_WINDOW); @@ -1695,7 +1688,8 @@ android_create_window (android_window parent, int x, int y, constructor = (*android_java_env)->GetMethodID (android_java_env, class, "", - "(SLorg/gnu/emacs/EmacsWindow;IIII)V"); + "(SLorg/gnu/emacs/EmacsWindow;" + "IIIIZ)V"); assert (constructor != NULL); old = class; @@ -1707,10 +1701,17 @@ android_create_window (android_window parent, int x, int y, memory_full (0); } + /* N.B. that ANDROID_CW_OVERRIDE_REDIRECT can only be set at window + creation time. */ + override_redirect = ((value_mask + & ANDROID_CW_OVERRIDE_REDIRECT) + && attrs->override_redirect); + object = (*android_java_env)->NewObject (android_java_env, class, constructor, (jshort) window, parent_object, (jint) x, (jint) y, - (jint) width, (jint) height); + (jint) width, (jint) height, + (jboolean) override_redirect); if (!object) { (*android_java_env)->ExceptionClear (android_java_env); @@ -3212,6 +3213,66 @@ android_get_geometry (android_window handle, ANDROID_DELETE_LOCAL_REF (window_geometry); } +void +android_move_resize_window (android_window window, int x, int y, + unsigned int width, unsigned int height) +{ + android_move_window (window, x, y); + android_resize_window (window, width, height); +} + +void +android_map_raised (android_window window) +{ + android_raise_window (window); + android_map_window (window); +} + +void +android_translate_coordinates (android_window src, int x, + int y, int *root_x, int *root_y) +{ + jobject window; + jarray coordinates; + jmethodID method; + jint *ints; + + window = android_resolve_handle (src, ANDROID_HANDLE_WINDOW); + method = android_lookup_method ("org/gnu/emacs/EmacsWindow", + "translateCoordinates", + "(II)[I"); + coordinates + = (*android_java_env)->CallObjectMethod (android_java_env, + window, method, + (jint) x, (jint) y); + + if (!coordinates) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + /* The array must contain two elements: X, Y translated to the root + window. */ + eassert ((*android_java_env)->GetArrayLength (android_java_env, + coordinates) + == 2); + + /* Obtain the coordinates from the array. */ + ints = (*android_java_env)->GetIntArrayElements (android_java_env, + coordinates, NULL); + *root_x = ints[0]; + *root_y = ints[1]; + + /* Release the coordinates. */ + (*android_java_env)->ReleaseIntArrayElements (android_java_env, + coordinates, ints, + JNI_ABORT); + + /* And free the local reference. */ + ANDROID_DELETE_LOCAL_REF (coordinates); +} + /* Low level drawing primitives. */ @@ -3384,6 +3445,30 @@ android_set_dont_accept_focus (android_window handle, (jboolean) no_accept_focus); } +void +android_get_keysym_name (int keysym, char *name_return, size_t size) +{ + jobject string; + const char *buffer; + + string = (*android_java_env)->CallObjectMethod (android_java_env, + emacs_service, + service_class.name_keysym, + (jint) keysym); + android_exception_check (); + + buffer = (*android_java_env)->GetStringUTFChars (android_java_env, + (jstring) string, + NULL); + android_exception_check (); + strncpy (name_return, buffer, size - 1); + + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + (jstring) string, + buffer); + ANDROID_DELETE_LOCAL_REF (string); +} + #undef faccessat @@ -3525,6 +3610,48 @@ emacs_abort (void) abort (); } + + +/* Given a Lisp string TEXT, return a local reference to an equivalent + Java string. */ + +jstring +android_build_string (Lisp_Object text) +{ + Lisp_Object encoded; + jstring string; + + encoded = ENCODE_UTF_8 (text); + + /* Note that Java expects this string to be in ``modified UTF + encoding'', which is actually UTF-8, except with NUL encoded as a + two-byte sequence. The only consequence of passing an actual + UTF-8 string is that NUL bytes cannot be represented, which is + not really of consequence. */ + string = (*android_java_env)->NewStringUTF (android_java_env, + SSDATA (encoded)); + if (!string) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } + + return string; +} + +/* Check for JNI exceptions and call memory_full in that + situation. */ + +void +android_exception_check (void) +{ + if ((*android_java_env)->ExceptionCheck (android_java_env)) + { + (*android_java_env)->ExceptionClear (android_java_env); + memory_full (0); + } +} + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ diff --git a/src/android.h b/src/android.h index af1e42ec370..98f2494e9a3 100644 --- a/src/android.h +++ b/src/android.h @@ -35,6 +35,7 @@ along with GNU Emacs. If not, see . */ #include #include "androidgui.h" +#include "lisp.h" #endif /* This must be used in every symbol declaration to export it to the @@ -84,6 +85,11 @@ extern bool android_detect_mouse (void); extern void android_set_dont_focus_on_map (android_window, bool); extern void android_set_dont_accept_focus (android_window, bool); +extern jstring android_build_string (Lisp_Object); +extern void android_exception_check (void); + +extern void android_get_keysym_name (int, char *, size_t); + /* Directory listing emulation. */ diff --git a/src/androidfns.c b/src/androidfns.c index 459e407b901..ab136bc2722 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -25,12 +25,36 @@ along with GNU Emacs. If not, see . */ #include "androidterm.h" #include "blockinput.h" #include "keyboard.h" +#include "buffer.h" #ifndef ANDROID_STUBIFY /* Some kind of reference count for the image cache. */ static ptrdiff_t image_cache_refcount; +/* The frame of the currently visible tooltip, or nil if none. */ +static Lisp_Object tip_frame; + +/* The window-system window corresponding to the frame of the + currently visible tooltip. */ +static android_window tip_window; + +/* The X and Y deltas of the last call to `x-show-tip'. */ +static Lisp_Object tip_dx, tip_dy; + +/* A timer that hides or deletes the currently visible tooltip when it + fires. */ +static Lisp_Object tip_timer; + +/* STRING argument of last `x-show-tip' call. */ +static Lisp_Object tip_last_string; + +/* Normalized FRAME argument of last `x-show-tip' call. */ +static Lisp_Object tip_last_frame; + +/* PARMS argument of last `x-show-tip' call. */ +static Lisp_Object tip_last_parms; + #endif static struct android_display_info * @@ -180,6 +204,9 @@ android_set_parent_frame (struct frame *f, Lisp_Object new_value, fset_parent_frame (f, new_value); } + + /* Update the fullscreen frame parameter as well. */ + FRAME_TERMINAL (f)->fullscreen_hook (f); } void @@ -858,13 +885,13 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame, gui_default_parameter (f, parms, Qbottom_divider_width, make_fixnum (0), NULL, NULL, RES_TYPE_NUMBER); - /* gui_default_parameter (f, parms, Qvertical_scroll_bars, */ - /* Qleft, */ - /* "verticalScrollBars", "ScrollBars", */ - /* RES_TYPE_SYMBOL); */ - /* gui_default_parameter (f, parms, Qhorizontal_scroll_bars, Qnil, */ - /* "horizontalScrollBars", "ScrollBars", */ - /* RES_TYPE_SYMBOL); TODO */ + gui_default_parameter (f, parms, Qvertical_scroll_bars, + Qleft, + "verticalScrollBars", "ScrollBars", + RES_TYPE_SYMBOL); + gui_default_parameter (f, parms, Qhorizontal_scroll_bars, Qnil, + "horizontalScrollBars", "ScrollBars", + RES_TYPE_SYMBOL); /* Also do the stuff which must be set before the window exists. */ gui_default_parameter (f, parms, Qforeground_color, build_string ("black"), @@ -893,7 +920,7 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame, android_default_scroll_bar_color_parameter (f, parms, Qscroll_bar_background, "scrollBarBackground", "ScrollBarBackground", false); -#endif /* TODO */ +#endif /* Init faces before gui_default_parameter is called for the scroll-bar-width parameter because otherwise we end up in @@ -974,12 +1001,16 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame, "autoLower", "AutoRaiseLower", RES_TYPE_BOOLEAN); gui_default_parameter (f, parms, Qcursor_type, Qbox, "cursorType", "CursorType", RES_TYPE_SYMBOL); + /* Scroll bars are not supported on Android, as they are near + useless. */ +#if 0 gui_default_parameter (f, parms, Qscroll_bar_width, Qnil, "scrollBarWidth", "ScrollBarWidth", RES_TYPE_NUMBER); gui_default_parameter (f, parms, Qscroll_bar_height, Qnil, "scrollBarHeight", "ScrollBarHeight", RES_TYPE_NUMBER); +#endif gui_default_parameter (f, parms, Qalpha, Qnil, "alpha", "Alpha", RES_TYPE_NUMBER); gui_default_parameter (f, parms, Qalpha_background, Qnil, @@ -1009,8 +1040,9 @@ DEFUN ("x-create-frame", Fx_create_frame, Sx_create_frame, /* Process fullscreen parameter here in the hope that normalizing a fullheight/fullwidth frame will produce the size set by the last - adjust_frame_size call. */ - gui_default_parameter (f, parms, Qfullscreen, Qnil, + adjust_frame_size call. Note that Android only supports the + `maximized' state. */ + gui_default_parameter (f, parms, Qfullscreen, Qmaximized, "fullscreen", "Fullscreen", RES_TYPE_SYMBOL); /* When called from `x-create-frame-with-faces' visibility is @@ -1661,6 +1693,391 @@ DEFUN ("x-display-list", Fx_display_list, Sx_display_list, 0, 0, 0, return result; } +#ifndef ANDROID_STUBIFY + +static void +unwind_create_tip_frame (Lisp_Object frame) +{ + Lisp_Object deleted; + + deleted = unwind_create_frame (frame); + if (EQ (deleted, Qt)) + { + tip_window = ANDROID_NONE; + tip_frame = Qnil; + } +} + +static Lisp_Object +android_create_tip_frame (struct android_display_info *dpyinfo, + Lisp_Object parms) +{ + struct frame *f; + Lisp_Object frame; + Lisp_Object name; + specpdl_ref count = SPECPDL_INDEX (); + bool face_change_before = face_change; + + if (!dpyinfo->terminal->name) + error ("Terminal is not live, can't create new frames on it"); + + parms = Fcopy_alist (parms); + + /* Get the name of the frame to use for resource lookup. */ + name = gui_display_get_arg (dpyinfo, parms, Qname, "name", "Name", + RES_TYPE_STRING); + if (!STRINGP (name) + && !BASE_EQ (name, Qunbound) + && !NILP (name)) + error ("Invalid frame name--not a string or nil"); + + frame = Qnil; + f = make_frame (false); + f->wants_modeline = false; + XSETFRAME (frame, f); + record_unwind_protect (unwind_create_tip_frame, frame); + + f->terminal = dpyinfo->terminal; + + /* By setting the output method, we're essentially saying that + the frame is live, as per FRAME_LIVE_P. If we get a signal + from this point on, x_destroy_window might screw up reference + counts etc. */ + f->output_method = output_android; + f->output_data.android = xzalloc (sizeof *f->output_data.android); + FRAME_FONTSET (f) = -1; + f->output_data.android->white_relief.pixel = -1; + f->output_data.android->black_relief.pixel = -1; + + f->tooltip = true; + fset_icon_name (f, Qnil); + FRAME_DISPLAY_INFO (f) = dpyinfo; + f->output_data.android->parent_desc = FRAME_DISPLAY_INFO (f)->root_window; + + /* These colors will be set anyway later, but it's important + to get the color reference counts right, so initialize them! */ + { + Lisp_Object black; + + /* Function android_decode_color can signal an error. Make sure + to initialize color slots so that we won't try to free colors + we haven't allocated. */ + FRAME_FOREGROUND_PIXEL (f) = -1; + FRAME_BACKGROUND_PIXEL (f) = -1; + f->output_data.android->cursor_pixel = -1; + f->output_data.android->cursor_foreground_pixel = -1; + + black = build_string ("black"); + FRAME_FOREGROUND_PIXEL (f) + = android_decode_color (f, black, BLACK_PIX_DEFAULT (f)); + FRAME_BACKGROUND_PIXEL (f) + = android_decode_color (f, black, BLACK_PIX_DEFAULT (f)); + f->output_data.android->cursor_pixel + = android_decode_color (f, black, BLACK_PIX_DEFAULT (f)); + f->output_data.android->cursor_foreground_pixel + = android_decode_color (f, black, BLACK_PIX_DEFAULT (f)); + } + + /* Set the name; the functions to which we pass f expect the name to + be set. */ + if (BASE_EQ (name, Qunbound) || NILP (name)) + f->explicit_name = false; + else + { + fset_name (f, name); + f->explicit_name = true; + /* use the frame's title when getting resources for this frame. */ + specbind (Qx_resource_name, name); + } + + register_font_driver (&androidfont_driver, f); + register_font_driver (&android_sfntfont_driver, f); + + image_cache_refcount + = FRAME_IMAGE_CACHE (f) ? FRAME_IMAGE_CACHE (f)->refcount : 0; +#ifdef GLYPH_DEBUG + dpyinfo_refcount = dpyinfo->reference_count; +#endif /* GLYPH_DEBUG */ + + gui_default_parameter (f, parms, Qfont_backend, Qnil, + "fontBackend", "FontBackend", RES_TYPE_STRING); + + /* Extract the window parameters from the supplied values that are + needed to determine window geometry. */ + android_default_font_parameter (f, parms); + + gui_default_parameter (f, parms, Qborder_width, make_fixnum (0), + "borderWidth", "BorderWidth", RES_TYPE_NUMBER); + + /* This defaults to 1 in order to match xterm. We recognize either + internalBorderWidth or internalBorder (which is what xterm calls + it). */ + if (NILP (Fassq (Qinternal_border_width, parms))) + { + Lisp_Object value; + + value = gui_display_get_arg (dpyinfo, parms, Qinternal_border_width, + "internalBorder", "internalBorder", + RES_TYPE_NUMBER); + if (! BASE_EQ (value, Qunbound)) + parms = Fcons (Fcons (Qinternal_border_width, value), + parms); + } + + gui_default_parameter (f, parms, Qinternal_border_width, make_fixnum (1), + "internalBorderWidth", "internalBorderWidth", + RES_TYPE_NUMBER); + gui_default_parameter (f, parms, Qright_divider_width, make_fixnum (0), + NULL, NULL, RES_TYPE_NUMBER); + gui_default_parameter (f, parms, Qbottom_divider_width, make_fixnum (0), + NULL, NULL, RES_TYPE_NUMBER); + + /* Also do the stuff which must be set before the window exists. */ + gui_default_parameter (f, parms, Qforeground_color, build_string ("black"), + "foreground", "Foreground", RES_TYPE_STRING); + gui_default_parameter (f, parms, Qbackground_color, build_string ("white"), + "background", "Background", RES_TYPE_STRING); + gui_default_parameter (f, parms, Qmouse_color, build_string ("black"), + "pointerColor", "Foreground", RES_TYPE_STRING); + gui_default_parameter (f, parms, Qcursor_color, build_string ("black"), + "cursorColor", "Foreground", RES_TYPE_STRING); + gui_default_parameter (f, parms, Qborder_color, build_string ("black"), + "borderColor", "BorderColor", RES_TYPE_STRING); + gui_default_parameter (f, parms, Qno_special_glyphs, Qnil, + NULL, NULL, RES_TYPE_BOOLEAN); + + { + struct android_set_window_attributes attrs; + unsigned long mask; + + block_input (); + mask = ANDROID_CW_OVERRIDE_REDIRECT; + + attrs.override_redirect = true; + tip_window + = FRAME_ANDROID_WINDOW (f) + = android_create_window (FRAME_DISPLAY_INFO (f)->root_window, + /* x, y, width, height, value-mask, + attrs. */ + 0, 0, 1, 1, mask, &attrs); + unblock_input (); + } + + /* Init faces before gui_default_parameter is called for the + scroll-bar-width parameter because otherwise we end up in + init_iterator with a null face cache, which should not happen. */ + init_frame_faces (f); + + gui_default_parameter (f, parms, Qinhibit_double_buffering, Qnil, + "inhibitDoubleBuffering", "InhibitDoubleBuffering", + RES_TYPE_BOOLEAN); + + gui_figure_window_size (f, parms, false, false); + + f->output_data.android->parent_desc = FRAME_DISPLAY_INFO (f)->root_window; + + android_make_gc (f); + + gui_default_parameter (f, parms, Qauto_raise, Qnil, + "autoRaise", "AutoRaiseLower", RES_TYPE_BOOLEAN); + gui_default_parameter (f, parms, Qauto_lower, Qnil, + "autoLower", "AutoRaiseLower", RES_TYPE_BOOLEAN); + gui_default_parameter (f, parms, Qcursor_type, Qbox, + "cursorType", "CursorType", RES_TYPE_SYMBOL); + gui_default_parameter (f, parms, Qalpha, Qnil, + "alpha", "Alpha", RES_TYPE_NUMBER); + gui_default_parameter (f, parms, Qalpha_background, Qnil, + "alphaBackground", "AlphaBackground", RES_TYPE_NUMBER); + + /* Add `tooltip' frame parameter's default value. */ + if (NILP (Fframe_parameter (frame, Qtooltip))) + { + AUTO_FRAME_ARG (arg, Qtooltip, Qt); + Fmodify_frame_parameters (frame, arg); + } + + /* FIXME - can this be done in a similar way to normal frames? + https://lists.gnu.org/r/emacs-devel/2007-10/msg00641.html */ + + /* Set the `display-type' frame parameter before setting up faces. */ + { + Lisp_Object disptype; + + disptype = Qcolor; + + if (NILP (Fframe_parameter (frame, Qdisplay_type))) + { + AUTO_FRAME_ARG (arg, Qdisplay_type, disptype); + Fmodify_frame_parameters (frame, arg); + } + } + + /* Set up faces after all frame parameters are known. This call + also merges in face attributes specified for new frames. */ + { + Lisp_Object bg = Fframe_parameter (frame, Qbackground_color); + + call2 (Qface_set_after_frame_default, frame, Qnil); + + if (!EQ (bg, Fframe_parameter (frame, Qbackground_color))) + { + AUTO_FRAME_ARG (arg, Qbackground_color, bg); + Fmodify_frame_parameters (frame, arg); + } + } + + f->no_split = true; + + /* Now that the frame will be official, it counts as a reference to + its display and terminal. */ + f->terminal->reference_count++; + + /* It is now ok to make the frame official even if we get an error + below. And the frame needs to be on Vframe_list or making it + visible won't work. */ + Vframe_list = Fcons (frame, Vframe_list); + f->can_set_window_size = true; + adjust_frame_size (f, FRAME_TEXT_WIDTH (f), FRAME_TEXT_HEIGHT (f), + 0, true, Qtip_frame); + + /* Setting attributes of faces of the tooltip frame from resources + and similar will set face_change, which leads to the clearing of + all current matrices. Since this isn't necessary here, avoid it + by resetting face_change to the value it had before we created + the tip frame. */ + face_change = face_change_before; + + /* Discard the unwind_protect. */ + return unbind_to (count, frame); +} + +static Lisp_Object +android_hide_tip (bool delete) +{ + if (!NILP (tip_timer)) + { + call1 (Qcancel_timer, tip_timer); + tip_timer = Qnil; + } + + if (NILP (tip_frame) + || (!delete + && !NILP (tip_frame) + && FRAME_LIVE_P (XFRAME (tip_frame)) + && !FRAME_VISIBLE_P (XFRAME (tip_frame)))) + return Qnil; + else + { + Lisp_Object was_open = Qnil; + + specpdl_ref count = SPECPDL_INDEX (); + specbind (Qinhibit_redisplay, Qt); + specbind (Qinhibit_quit, Qt); + + if (!NILP (tip_frame)) + { + struct frame *f = XFRAME (tip_frame); + + if (FRAME_LIVE_P (f)) + { + if (delete) + { + delete_frame (tip_frame, Qnil); + tip_frame = Qnil; + } + else + android_make_frame_invisible (XFRAME (tip_frame)); + + was_open = Qt; + } + else + tip_frame = Qnil; + } + else + tip_frame = Qnil; + + return unbind_to (count, was_open); + } +} + +static void +compute_tip_xy (struct frame *f, Lisp_Object parms, Lisp_Object dx, + Lisp_Object dy, int width, int height, int *root_x, + int *root_y) +{ + Lisp_Object left, top, right, bottom; + int min_x, min_y, max_x, max_y = -1; + android_window window; + struct frame *mouse_frame; + + /* Initialize these values in case there is no mouse frame. */ + *root_x = 0; + *root_y = 0; + + /* User-specified position? */ + left = CDR (Fassq (Qleft, parms)); + top = CDR (Fassq (Qtop, parms)); + right = CDR (Fassq (Qright, parms)); + bottom = CDR (Fassq (Qbottom, parms)); + + /* Move the tooltip window where the mouse pointer was last seen. + Resize and show it. */ + if ((!FIXNUMP (left) && !FIXNUMP (right)) + || (!FIXNUMP (top) && !FIXNUMP (bottom))) + { + if (x_display_list->last_mouse_motion_frame) + { + *root_x = x_display_list->last_mouse_motion_x; + *root_y = x_display_list->last_mouse_motion_y; + mouse_frame = x_display_list->last_mouse_motion_frame; + window = FRAME_ANDROID_WINDOW (mouse_frame); + + /* Translate the coordinates to the screen. */ + android_translate_coordinates (window, *root_x, *root_y, + root_x, root_y); + } + } + + min_x = 0; + min_y = 0; + max_x = android_get_screen_width (); + max_y = android_get_screen_height (); + + if (FIXNUMP (top)) + *root_y = XFIXNUM (top); + else if (FIXNUMP (bottom)) + *root_y = XFIXNUM (bottom) - height; + else if (*root_y + XFIXNUM (dy) <= min_y) + *root_y = min_y; /* Can happen for negative dy */ + else if (*root_y + XFIXNUM (dy) + height <= max_y) + /* It fits below the pointer */ + *root_y += XFIXNUM (dy); + else if (height + XFIXNUM (dy) + min_y <= *root_y) + /* It fits above the pointer. */ + *root_y -= height + XFIXNUM (dy); + else + /* Put it on the top. */ + *root_y = min_y; + + if (FIXNUMP (left)) + *root_x = XFIXNUM (left); + else if (FIXNUMP (right)) + *root_x = XFIXNUM (right) - width; + else if (*root_x + XFIXNUM (dx) <= min_x) + *root_x = 0; /* Can happen for negative dx */ + else if (*root_x + XFIXNUM (dx) + width <= max_x) + /* It fits to the right of the pointer. */ + *root_x += XFIXNUM (dx); + else if (width + XFIXNUM (dx) + min_x <= *root_x) + /* It fits to the left of the pointer. */ + *root_x -= width + XFIXNUM (dx); + else + /* Put it left justified on the screen -- it ought to fit that way. */ + *root_x = min_x; +} + +#endif + DEFUN ("x-show-tip", Fx_show_tip, Sx_show_tip, 1, 6, 0, doc: /* SKIP: real doc in xfns.c. */) (Lisp_Object string, Lisp_Object frame, Lisp_Object parms, @@ -1670,8 +2087,214 @@ DEFUN ("x-show-tip", Fx_show_tip, Sx_show_tip, 1, 6, 0, error ("Android cross-compilation stub called!"); return Qnil; #else - /* TODO tooltips */ - return Qnil; + struct frame *f, *tip_f; + struct window *w; + int root_x, root_y; + struct buffer *old_buffer; + struct text_pos pos; + int width, height; + int old_windows_or_buffers_changed = windows_or_buffers_changed; + specpdl_ref count = SPECPDL_INDEX (); + Lisp_Object window, size, tip_buf; + bool displayed; +#ifdef ENABLE_CHECKING + struct glyph_row *row, *end; +#endif + AUTO_STRING (tip, " *tip*"); + + specbind (Qinhibit_redisplay, Qt); + + CHECK_STRING (string); + if (SCHARS (string) == 0) + string = make_unibyte_string (" ", 1); + + if (NILP (frame)) + frame = selected_frame; + f = decode_window_system_frame (frame); + + if (NILP (timeout)) + timeout = Vx_show_tooltip_timeout; + CHECK_FIXNAT (timeout); + + if (NILP (dx)) + dx = make_fixnum (5); + else + CHECK_FIXNUM (dx); + + if (NILP (dy)) + dy = make_fixnum (-10); + else + CHECK_FIXNUM (dy); + + tip_dx = dx; + tip_dy = dy; + + if (!NILP (tip_frame) && FRAME_LIVE_P (XFRAME (tip_frame))) + { + if (FRAME_VISIBLE_P (XFRAME (tip_frame)) + && !NILP (Fequal_including_properties (tip_last_string, + string)) + && !NILP (Fequal (tip_last_parms, parms))) + { + /* Only DX and DY have changed. */ + tip_f = XFRAME (tip_frame); + if (!NILP (tip_timer)) + { + call1 (Qcancel_timer, tip_timer); + tip_timer = Qnil; + } + + block_input (); + compute_tip_xy (tip_f, parms, dx, dy, FRAME_PIXEL_WIDTH (tip_f), + FRAME_PIXEL_HEIGHT (tip_f), &root_x, &root_y); + android_move_window (FRAME_ANDROID_WINDOW (tip_f), + root_x, root_y); + unblock_input (); + + goto start_timer; + } + else + android_hide_tip (true); + } + else + android_hide_tip (true); + + tip_last_frame = frame; + tip_last_string = string; + tip_last_parms = parms; + + if (NILP (tip_frame) || !FRAME_LIVE_P (XFRAME (tip_frame))) + { + /* Add default values to frame parameters. */ + if (NILP (Fassq (Qname, parms))) + parms = Fcons (Fcons (Qname, build_string ("tooltip")), parms); + if (NILP (Fassq (Qinternal_border_width, parms))) + parms = Fcons (Fcons (Qinternal_border_width, make_fixnum (3)), + parms); + if (NILP (Fassq (Qborder_width, parms))) + parms = Fcons (Fcons (Qborder_width, make_fixnum (1)), parms); + if (NILP (Fassq (Qborder_color, parms))) + parms = Fcons (Fcons (Qborder_color, build_string ("lightyellow")), + parms); + if (NILP (Fassq (Qbackground_color, parms))) + parms = Fcons (Fcons (Qbackground_color, + build_string ("lightyellow")), + parms); + + /* Create a frame for the tooltip, and record it in the global + variable tip_frame. */ + if (NILP (tip_frame = android_create_tip_frame (FRAME_DISPLAY_INFO (f), + parms))) + /* Creating the tip frame failed. */ + return unbind_to (count, Qnil); + } + + tip_f = XFRAME (tip_frame); + window = FRAME_ROOT_WINDOW (tip_f); + tip_buf = Fget_buffer_create (tip, Qnil); + /* We will mark the tip window a "pseudo-window" below, and such + windows cannot have display margins. */ + bset_left_margin_cols (XBUFFER (tip_buf), make_fixnum (0)); + bset_right_margin_cols (XBUFFER (tip_buf), make_fixnum (0)); + set_window_buffer (window, tip_buf, false, false); + w = XWINDOW (window); + w->pseudo_window_p = true; + /* Try to avoid that `other-window' select us (Bug#47207). */ + Fset_window_parameter (window, Qno_other_window, Qt); + + /* Set up the frame's root window. Note: The following code does not + try to size the window or its frame correctly. Its only purpose is + to make the subsequent text size calculations work. The right + sizes should get installed when the toolkit gets back to us. */ + w->left_col = 0; + w->top_line = 0; + w->pixel_left = 0; + w->pixel_top = 0; + + if (CONSP (Vx_max_tooltip_size) + && RANGED_FIXNUMP (1, XCAR (Vx_max_tooltip_size), INT_MAX) + && RANGED_FIXNUMP (1, XCDR (Vx_max_tooltip_size), INT_MAX)) + { + w->total_cols = XFIXNAT (XCAR (Vx_max_tooltip_size)); + w->total_lines = XFIXNAT (XCDR (Vx_max_tooltip_size)); + } + else + { + w->total_cols = 80; + w->total_lines = 40; + } + + w->pixel_width = w->total_cols * FRAME_COLUMN_WIDTH (tip_f); + w->pixel_height = w->total_lines * FRAME_LINE_HEIGHT (tip_f); + FRAME_TOTAL_COLS (tip_f) = w->total_cols; + adjust_frame_glyphs (tip_f); + + /* Insert STRING into root window's buffer and fit the frame to the + buffer. */ + specpdl_ref count_1 = SPECPDL_INDEX (); + old_buffer = current_buffer; + set_buffer_internal_1 (XBUFFER (w->contents)); + bset_truncate_lines (current_buffer, Qnil); + specbind (Qinhibit_read_only, Qt); + specbind (Qinhibit_modification_hooks, Qt); + specbind (Qinhibit_point_motion_hooks, Qt); + Ferase_buffer (); + Finsert (1, &string); + clear_glyph_matrix (w->desired_matrix); + clear_glyph_matrix (w->current_matrix); + SET_TEXT_POS (pos, BEGV, BEGV_BYTE); + displayed = try_window (window, pos, TRY_WINDOW_IGNORE_FONTS_CHANGE); + + if (!displayed && NILP (Vx_max_tooltip_size)) + { +#ifdef ENABLE_CHECKING + row = w->desired_matrix->rows; + end = w->desired_matrix->rows + w->desired_matrix->nrows; + + while (row < end) + { + if (!row->displays_text_p + || row->ends_at_zv_p) + break; + ++row; + } + + eassert (row < end && row->ends_at_zv_p); +#endif + } + + /* Calculate size of tooltip window. */ + size = Fwindow_text_pixel_size (window, Qnil, Qnil, Qnil, + make_fixnum (w->pixel_height), Qnil, + Qnil); + /* Add the frame's internal border to calculated size. */ + width = XFIXNUM (CAR (size)) + 2 * FRAME_INTERNAL_BORDER_WIDTH (tip_f); + height = XFIXNUM (CDR (size)) + 2 * FRAME_INTERNAL_BORDER_WIDTH (tip_f); + + /* Calculate position of tooltip frame. */ + compute_tip_xy (tip_f, parms, dx, dy, width, height, &root_x, &root_y); + + /* Show tooltip frame. */ + block_input (); + android_move_resize_window (FRAME_ANDROID_WINDOW (tip_f), + root_x, root_y, width, + height); + android_map_raised (FRAME_ANDROID_WINDOW (tip_f)); + unblock_input (); + + w->must_be_updated_p = true; + update_single_window (w); + flush_frame (tip_f); + set_buffer_internal_1 (old_buffer); + unbind_to (count_1, Qnil); + windows_or_buffers_changed = old_windows_or_buffers_changed; + + start_timer: + /* Let the tip disappear after timeout seconds. */ + tip_timer = call3 (Qrun_at_time, timeout, Qnil, + Qx_hide_tip); + + return unbind_to (count, Qnil); #endif } @@ -1683,7 +2306,7 @@ DEFUN ("x-hide-tip", Fx_hide_tip, Sx_hide_tip, 0, 0, 0, error ("Android cross-compilation stub called!"); return Qnil; #else - return Qnil; + return android_hide_tip (true); #endif } @@ -2112,6 +2735,17 @@ syms_of_androidfns (void) doc: /* SKIP: real doc in xfns.c. */); Vx_cursor_fore_pixel = Qnil; + /* Used by Fx_show_tip. */ + DEFSYM (Qrun_at_time, "run-at-time"); + DEFSYM (Qx_hide_tip, "x-hide-tip"); + DEFSYM (Qcancel_timer, "cancel-timer"); + DEFSYM (Qassq_delete_all, "assq-delete-all"); + DEFSYM (Qcolor, "color"); + + DEFVAR_LISP ("x-max-tooltip-size", Vx_max_tooltip_size, + doc: /* SKIP: real doc in xfns.c. */); + Vx_max_tooltip_size = Qnil; + /* Functions defined. */ defsubr (&Sx_create_frame); defsubr (&Sxw_color_defined_p); @@ -2139,4 +2773,21 @@ syms_of_androidfns (void) defsubr (&Sx_show_tip); defsubr (&Sx_hide_tip); defsubr (&Sandroid_detect_mouse); + +#ifndef ANDROID_STUBIFY + tip_timer = Qnil; + staticpro (&tip_timer); + tip_frame = Qnil; + staticpro (&tip_frame); + tip_last_frame = Qnil; + staticpro (&tip_last_frame); + tip_last_string = Qnil; + staticpro (&tip_last_string); + tip_last_parms = Qnil; + staticpro (&tip_last_parms); + tip_dx = Qnil; + staticpro (&tip_dx); + tip_dy = Qnil; + staticpro (&tip_dy); +#endif } diff --git a/src/androidgui.h b/src/androidgui.h index 422e72408c7..8450a1f637b 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -80,13 +80,18 @@ enum android_fill_style enum android_window_value_mask { - ANDROID_CW_BACK_PIXEL = (1 << 1), + ANDROID_CW_BACK_PIXEL = (1 << 1), + ANDROID_CW_OVERRIDE_REDIRECT = (1 << 2), }; struct android_set_window_attributes { /* The background pixel. */ unsigned long background_pixel; + + /* Whether or not the window is override redirect. This cannot be + set after creation on Android. */ + bool override_redirect; }; struct android_gc_values @@ -260,7 +265,7 @@ struct android_key_event ((key) == 57 || (key) == 58 || (key) == 113 || (key) == 114 \ || (key) == 119 || (key) == 117 || (key) == 118 || (key) == 78 \ || (key) == 94 || (key) == 59 || (key) == 60 || (key) == 95 \ - || (key) == 63) + || (key) == 63 || (key) == 115) struct android_configure_event { @@ -478,6 +483,11 @@ extern int android_query_tree (android_window, android_window *, extern void android_get_geometry (android_window, android_window *, int *, int *, unsigned int *, unsigned int *, unsigned int *); +extern void android_move_resize_window (android_window, int, int, + unsigned int, unsigned int); +extern void android_map_raised (android_window); +extern void android_translate_coordinates (android_window, int, + int, int *, int *); #endif diff --git a/src/androidmenu.c b/src/androidmenu.c index 6f6e4ca8de9..0f0c6f4ef1f 100644 --- a/src/androidmenu.c +++ b/src/androidmenu.c @@ -21,6 +21,10 @@ along with GNU Emacs. If not, see . */ #include "lisp.h" #include "androidterm.h" +#include "android.h" +#include "blockinput.h" +#include "keyboard.h" +#include "menu.h" #ifndef ANDROID_STUBIFY @@ -35,4 +39,293 @@ popup_activated (void) return popup_activated_flag; } + + +/* Toolkit menu implementation. */ + +/* Structure describing the EmacsContextMenu class. */ + +struct android_emacs_context_menu +{ + jclass class; + jmethodID create_context_menu; + jmethodID add_item; + jmethodID add_submenu; + jmethodID add_pane; + jmethodID parent; + jmethodID display; +}; + +/* Identifiers associated with the EmacsContextMenu class. */ +static struct android_emacs_context_menu menu_class; + +static void +android_init_emacs_context_menu (void) +{ + jclass old; + + menu_class.class + = (*android_java_env)->FindClass (android_java_env, + "org/gnu/emacs/EmacsContextMenu"); + eassert (menu_class.class); + + old = menu_class.class; + menu_class.class + = (jclass) (*android_java_env)->NewGlobalRef (android_java_env, + (jobject) old); + ANDROID_DELETE_LOCAL_REF (old); + + if (!menu_class.class) + emacs_abort (); + +#define FIND_METHOD(c_name, name, signature) \ + menu_class.c_name \ + = (*android_java_env)->GetMethodID (android_java_env, \ + menu_class.class, \ + name, signature); \ + eassert (menu_class.c_name); + +#define FIND_METHOD_STATIC(c_name, name, signature) \ + menu_class.c_name \ + = (*android_java_env)->GetStaticMethodID (android_java_env, \ + menu_class.class, \ + name, signature); \ + eassert (menu_class.c_name); + + FIND_METHOD_STATIC (create_context_menu, "createContextMenu", + "(Ljava/lang/String;)Lorg/gnu/emacs/EmacsContextMenu;"); + + FIND_METHOD (add_item, "addItem", "(ILjava/lang/String;Z)V"); + FIND_METHOD (add_submenu, "addSubmenu", "(Ljava/lang/String;" + "Ljava/lang/String;)Lorg/gnu/emacs/EmacsContextMenu;"); + FIND_METHOD (add_pane, "addPane", "(Ljava/lang/String;)V"); + FIND_METHOD (parent, "parent", "()Lorg/gnu/emacs/EmacsContextMenu;"); + FIND_METHOD (display, "display", "(Lorg/gnu/emacs/EmacsWindow;II)Z"); + +#undef FIND_METHOD +#undef FIND_METHOD_STATIC +} + +static void +android_unwind_local_frame (void) +{ + (*android_java_env)->PopLocalFrame (android_java_env, NULL); +} + +/* Push a local reference frame to the JVM stack and record it on the + specpdl. Release local references created within that frame when + the specpdl is unwound past where it is after returning. */ + +static void +android_push_local_frame (void) +{ + int rc; + + rc = (*android_java_env)->PushLocalFrame (android_java_env, 30); + + /* This means the JVM ran out of memory. */ + if (rc < 1) + android_exception_check (); + + record_unwind_protect_void (android_unwind_local_frame); +} + +Lisp_Object +android_menu_show (struct frame *f, int x, int y, int menuflags, + Lisp_Object title, const char **error_name) +{ + jobject context_menu, current_context_menu; + jobject title_string, temp; + size_t i; + Lisp_Object pane_name, prefix; + const char *pane_string; + specpdl_ref count, count1; + Lisp_Object item_name, enable, def; + jmethodID method; + jobject store; + bool rc; + jobject window; + + count = SPECPDL_INDEX (); + + block_input (); + + /* Push the first local frame. */ + android_push_local_frame (); + + /* Push the first local frame for the context menu. */ + title_string = (!NILP (title) + ? (jobject) android_build_string (title) + : NULL); + method = menu_class.create_context_menu; + current_context_menu = context_menu + = (*android_java_env)->CallStaticObjectMethod (android_java_env, + menu_class.class, + method, + title_string); + + if (title_string) + ANDROID_DELETE_LOCAL_REF (title_string); + + /* Push the second local frame for temporaries. */ + count1 = SPECPDL_INDEX (); + android_push_local_frame (); + + /* Iterate over the menu. */ + i = 0; + + while (i < menu_items_used) + { + if (NILP (AREF (menu_items, i))) + { + /* This is the start of a new submenu. However, it can be + ignored here. */ + i += 1; + } + else if (EQ (AREF (menu_items, i), Qlambda)) + { + /* This is the end of a submenu. Go back to the previous + context menu. */ + store = current_context_menu; + current_context_menu + = (*android_java_env)->CallObjectMethod (android_java_env, + current_context_menu, + menu_class.parent); + android_exception_check (); + + if (store != context_menu) + ANDROID_DELETE_LOCAL_REF (store); + i += 1; + + eassert (current_context_menu); + } + else if (EQ (AREF (menu_items, i), Qquote)) + i += 1; + else if (EQ (AREF (menu_items, i), Qt)) + { + /* This is a new pane. Switch back to the topmost context + menu. */ + if (current_context_menu != context_menu) + ANDROID_DELETE_LOCAL_REF (current_context_menu); + current_context_menu = context_menu; + + /* Now figure out the title of this pane. */ + pane_name = AREF (menu_items, i + MENU_ITEMS_PANE_NAME); + prefix = AREF (menu_items, i + MENU_ITEMS_PANE_PREFIX); + pane_string = (NILP (pane_name) + ? "" : SSDATA (pane_name)); + if ((menuflags & MENU_KEYMAPS) && !NILP (prefix)) + pane_string++; + + /* Add the pane. */ + temp = (*android_java_env)->NewStringUTF (android_java_env, + pane_string); + android_exception_check (); + + (*android_java_env)->CallVoidMethod (android_java_env, + current_context_menu, + menu_class.add_pane, + temp); + android_exception_check (); + ANDROID_DELETE_LOCAL_REF (temp); + + i += MENU_ITEMS_PANE_LENGTH; + } + else + { + item_name = AREF (menu_items, i + MENU_ITEMS_ITEM_NAME); + enable = AREF (menu_items, i + MENU_ITEMS_ITEM_ENABLE); + def = AREF (menu_items, i + MENU_ITEMS_ITEM_DEFINITION); + + /* This is an actual menu item (or submenu). Add it to the + menu. */ + + if (i + MENU_ITEMS_ITEM_LENGTH < menu_items_used && + NILP (AREF (menu_items, i + MENU_ITEMS_ITEM_LENGTH))) + { + /* This is a submenu. Add it. */ + title_string = (!NILP (item_name) + ? android_build_string (item_name) + : NULL); + store = current_context_menu; + current_context_menu + = (*android_java_env)->CallObjectMethod (android_java_env, + current_context_menu, + menu_class.add_submenu, + title_string); + android_exception_check (); + + if (store != context_menu) + ANDROID_DELETE_LOCAL_REF (store); + + if (title_string) + ANDROID_DELETE_LOCAL_REF (title_string); + } + else if (NILP (def) && menu_separator_name_p (SSDATA (item_name))) + /* Ignore this separator item. */ + ; + else + { + /* Add this menu item with the appropriate state. */ + + title_string = (!NILP (item_name) + ? android_build_string (item_name) + : NULL); + (*android_java_env)->CallVoidMethod (android_java_env, + current_context_menu, + menu_class.add_item, + (jint) 1, + title_string, + (jboolean) !NILP (enable)); + android_exception_check (); + + if (title_string) + ANDROID_DELETE_LOCAL_REF (title_string); + } + + i += MENU_ITEMS_ITEM_LENGTH; + } + } + + /* The menu has now been built. Pop the second local frame. */ + unbind_to (count1, Qnil); + + /* Now, display the context menu. */ + window = android_resolve_handle (FRAME_ANDROID_WINDOW (f), + ANDROID_HANDLE_WINDOW); + rc = (*android_java_env)->CallBooleanMethod (android_java_env, + context_menu, + window, (jint) x, + (jint) y); + android_exception_check (); + + if (!rc) + /* This means displaying the menu failed. */ + goto finish; + +#if 0 + record_unwind_protect_ptr (android_dismiss_menu, &context_menu); + + /* Otherwise, loop waiting for the menu event to arrive. */ + android_process_events_for_menu (&id); + + if (!id) + /* This means no menu item was selected. */ + goto finish; + +#endif + + finish: + unblock_input (); + return unbind_to (count, Qnil); +} + +#endif + +void +init_androidmenu (void) +{ +#ifndef ANDROID_STUBIFY + android_init_emacs_context_menu (); #endif +} diff --git a/src/androidterm.c b/src/androidterm.c index a8325312498..002d39af707 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -559,6 +559,9 @@ handle_one_android_event (struct android_display_info *dpyinfo, f = android_window_to_frame (dpyinfo, configureEvent.xconfigure.window); + if (!f) + goto OTHER; + int width = configureEvent.xconfigure.width; int height = configureEvent.xconfigure.height; @@ -884,10 +887,6 @@ handle_one_android_event (struct android_display_info *dpyinfo, inev.ie.arg = tab_bar_arg; } } - else - { - /* TODO: scroll bars */ - } if (event->type == ANDROID_BUTTON_PRESS) { @@ -1451,7 +1450,12 @@ android_make_frame_visible_invisible (struct frame *f, bool visible) static void android_fullscreen_hook (struct frame *f) { - /* TODO */ + /* Explicitly setting fullscreen is not supported on Android. */ + + if (!FRAME_PARENT_FRAME (f)) + store_frame_param (f, Qfullscreen, Qmaximized); + else + store_frame_param (f, Qfullscreen, Qnil); } void @@ -2360,7 +2364,7 @@ android_draw_box_rect (struct glyph_string *s, /* Top. */ android_fill_rectangle (FRAME_ANDROID_DRAWABLE (s->f), s->gc, left_x, - left_x, right_x - left_x + 1, hwidth); + top_y, right_x - left_x + 1, hwidth); /* Left. */ if (left_p) @@ -3958,7 +3962,14 @@ frame_set_mouse_pixel_position (struct frame *f, int pix_x, int pix_y) char * get_keysym_name (int keysym) { - return (char *) "UNKNOWN KEYSYM"; + static char buffer[64]; + +#ifndef ANDROID_STUBIFY + android_get_keysym_name (keysym, buffer, 64); +#else + emacs_abort (); +#endif + return buffer; } @@ -4009,20 +4020,13 @@ android_create_terminal (struct android_display_info *dpyinfo) terminal->set_new_font_hook = android_new_font; terminal->set_bitmap_icon_hook = android_bitmap_icon; terminal->implicit_set_name_hook = android_implicitly_set_name; - /* terminal->menu_show_hook = android_menu_show; XXX */ + terminal->menu_show_hook = android_menu_show; terminal->change_tab_bar_height_hook = android_change_tab_bar_height; terminal->change_tool_bar_height_hook = android_change_tool_bar_height; - /* terminal->set_vertical_scroll_bar_hook */ - /* = android_set_vertical_scroll_bar; */ - /* terminal->set_horizontal_scroll_bar_hook */ - /* = android_set_horizontal_scroll_bar; */ terminal->set_scroll_bar_default_width_hook = android_set_scroll_bar_default_width; terminal->set_scroll_bar_default_height_hook = android_set_scroll_bar_default_height; - /* terminal->condemn_scroll_bars_hook = android_condemn_scroll_bars; */ - /* terminal->redeem_scroll_bars_hook = android_redeem_scroll_bars; */ - /* terminal->judge_scroll_bars_hook = android_judge_scroll_bars; */ terminal->free_pixmap = android_free_pixmap_hook; terminal->delete_frame_hook = android_delete_frame; terminal->delete_terminal_hook = android_delete_terminal; diff --git a/src/androidterm.h b/src/androidterm.h index ebde15c40a8..e83e32a5854 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -402,6 +402,12 @@ extern void syms_of_androidfont (void); extern void android_finalize_font_entity (struct font_entity *); +/* Defined in androidmenu.c. */ + +extern Lisp_Object android_menu_show (struct frame *, int, int, int, + Lisp_Object, const char **); +extern void init_androidmenu (void); + /* Defined in sfntfont-android.c. */ extern const struct font_driver android_sfntfont_driver; diff --git a/src/emacs.c b/src/emacs.c index e3f6c7d66f7..8f5be53aad9 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -2499,6 +2499,10 @@ Using an Emacs configured with --with-x-toolkit=lucid does not have this problem init_window (); init_font (); +#ifdef HAVE_ANDROID + init_androidmenu (); +#endif + #if defined HAVE_ANDROID && !defined ANDROID_STUBIFY init_androidfont (); init_sfntfont (); diff --git a/xcompile/Makefile.in b/xcompile/Makefile.in index ed2b10b91a7..9f817bb4c53 100644 --- a/xcompile/Makefile.in +++ b/xcompile/Makefile.in @@ -110,7 +110,7 @@ lib/libgnu.a: src/verbose.mk config.status $(LIB_DEPS) $(PRE_BUILD_DEPS) +make -C lib libgnu.a src/Makefile src/config.h &: $(top_builddir)/src/config.h.android \ - $(top_builddir)/src/Makefile.android $(PRE_BUILD_DEPS) + $(top_builddir)/src/Makefile.android mkdir -p src src/deps # Copy config.h to src/ cp -f -p $(top_builddir)/src/config.h.android src/config.h @@ -155,13 +155,20 @@ $(LIBSRC_BINARIES) &: src/verbose.mk $(top_builddir)/$@ lib/libgnu.a \ # Finally, go into lib-src and make everything being built +make -C lib-src $(foreach bin,$(LIBSRC_BINARIES),$(notdir $(bin))) -.PHONY: clean maintainer-clean +.PHONY: clean maintainer-clean distclean clean: rm -rf $(CLEAN_SUBDIRS) *.bak sys if [ -e lib/Makefile ]; then \ make -C lib clean; \ fi - rm -rf lib/gnulib.mk lib/Makefile lib/config.h + rm -rf lib/config.h + +distclean bootstrap-clean: clean + if [ -e lib/Makefile ]; then \ + make -C lib distclean; \ + fi +# Just in case. + rm -rf lib/Makefile lib/gnulib.mk maintainer-clean: clean if [ -e lib/Makefile ]; then \ -- cgit v1.3 From da9ae10636b84b88e9eb9c827b03cdaabd1611d1 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 15 Jan 2023 15:45:29 +0800 Subject: Implement submenus on Android * java/org/gnu/emacs/EmacsActivity.java (onCreate): Set the default theme to Theme.DeviceDefault.NoActionBar if possible. (onContextMenuClosed): Add hack for Android bug. * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu) (onMenuItemClick): Set flag upon submenu selection. (inflateMenuItems): Set onClickListener for submenus as well. (display1): Clear new flag. * java/org/gnu/emacs/EmacsDrawRectangle.java (perform): Fix rectangle bounds. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): * java/org/gnu/emacs/EmacsService.java (onCreate): Pass cache directory. (sync): New function. * src/android.c (struct android_emacs_service): New method `sync'. (setEmacsParams, initEmacs): Handle cache directory. (android_init_emacs_service): Initialize new method `sync'. (android_sync): New function. * src/androidfns.c (Fx_show_tip): Call both functions. * src/androidgui.h: Update prototypes. * src/androidmenu.c (struct android_menu_subprefix) (android_free_subprefixes, android_menu_show): Handle submenu prefixes correctly. * src/androidterm.c (handle_one_android_event): Clear help echo on MotionNotify like on X. * src/menu.c (single_menu_item): Enable submenus on Android. --- java/org/gnu/emacs/EmacsActivity.java | 12 ++- java/org/gnu/emacs/EmacsContextMenu.java | 31 ++++++- java/org/gnu/emacs/EmacsDrawRectangle.java | 2 +- java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsService.java | 36 +++++++- src/android.c | 38 ++++++++- src/androidfns.c | 8 ++ src/androidgui.h | 1 + src/androidmenu.c | 128 ++++++++++++++++++++++++++--- src/androidterm.c | 10 +++ src/menu.c | 9 +- 11 files changed, 255 insertions(+), 24 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 4b96a376987..79c0991a5d3 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -27,6 +27,7 @@ import android.app.Activity; import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.os.Build; import android.util.Log; import android.widget.FrameLayout; import android.widget.FrameLayout.LayoutParams; @@ -162,7 +163,11 @@ public class EmacsActivity extends Activity FrameLayout.LayoutParams params; /* Set the theme to one without a title bar. */ - setTheme (android.R.style.Theme_NoTitleBar); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + setTheme (android.R.style.Theme_DeviceDefault_NoActionBar); + else + setTheme (android.R.style.Theme_NoTitleBar); params = new FrameLayout.LayoutParams (LayoutParams.MATCH_PARENT, LayoutParams.MATCH_PARENT); @@ -235,6 +240,11 @@ public class EmacsActivity extends Activity { Log.d (TAG, "onContextMenuClosed: " + menu); + /* See the comment inside onMenuItemClick. */ + if (EmacsContextMenu.wasSubmenuSelected + && menu.toString ().contains ("ContextMenuBuilder")) + return; + /* Send a context menu event given that no menu item has already been selected. */ if (!EmacsContextMenu.itemAlreadySelected) diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 02dd1c7efa9..00e204c9949 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -30,6 +30,7 @@ import android.os.Bundle; import android.view.Menu; import android.view.MenuItem; import android.view.View; +import android.view.SubMenu; import android.util.Log; @@ -47,6 +48,9 @@ public class EmacsContextMenu /* Whether or not an item was selected. */ public static boolean itemAlreadySelected; + /* Whether or not a submenu was selected. */ + public static boolean wasSubmenuSelected; + private class Item implements MenuItem.OnMenuItemClickListener { public int itemID; @@ -60,6 +64,20 @@ public class EmacsContextMenu { Log.d (TAG, "onMenuItemClick: " + itemName + " (" + itemID + ")"); + if (subMenu != null) + { + /* After opening a submenu within a submenu, Android will + send onContextMenuClosed for a ContextMenuBuilder. This + will normally confuse Emacs into thinking that the + context menu has been dismissed. Wrong! + + Setting this flag makes EmacsActivity to only handle + SubMenuBuilder being closed, which always means the menu + has actually been dismissed. */ + wasSubmenuSelected = true; + return false; + } + /* Send a context menu event. */ EmacsNative.sendContextMenu ((short) 0, itemID); @@ -144,7 +162,7 @@ public class EmacsContextMenu { Intent intent; MenuItem menuItem; - Menu submenu; + SubMenu submenu; for (Item item : menuItems) { @@ -153,7 +171,11 @@ public class EmacsContextMenu /* This is a submenu. Create the submenu and add the contents of the menu to it. */ submenu = menu.addSubMenu (item.itemName); - inflateMenuItems (submenu); + item.subMenu.inflateMenuItems (submenu); + + /* This is still needed to set wasSubmenuSelected. */ + menuItem = submenu.getItem (); + menuItem.setOnMenuItemClickListener (item); } else { @@ -184,7 +206,7 @@ public class EmacsContextMenu public EmacsContextMenu parent () { - return parent; + return this.parent; } /* Like display, but does the actual work and runs in the main @@ -197,6 +219,9 @@ public class EmacsContextMenu send 0 in response to the context menu being closed. */ itemAlreadySelected = false; + /* No submenu has been selected yet. */ + wasSubmenuSelected = false; + return window.view.popupMenu (this, xPosition, yPosition); } diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index 84ff498847b..b42e9556e8c 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -59,7 +59,7 @@ public class EmacsDrawRectangle } paint = gc.gcPaint; - rect = new Rect (x + 1, y + 1, x + width, y + height); + rect = new Rect (x, y, x + width, y + height); paint.setStyle (Paint.Style.STROKE); diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 4a80f88edcf..2f3a732ea7c 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -38,6 +38,9 @@ public class EmacsNative libDir must be the package's data storage location for native libraries. It is used as PATH. + cacheDir must be the package's cache directory. It is used as + the `temporary-file-directory'. + pixelDensityX and pixelDensityY are the DPI values that will be used by Emacs. @@ -45,6 +48,7 @@ public class EmacsNative public static native void setEmacsParams (AssetManager assetManager, String filesDir, String libDir, + String cacheDir, float pixelDensityX, float pixelDensityY, EmacsService emacsService); diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index f935b63fa0d..ca38f93dc98 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -108,7 +108,7 @@ public class EmacsService extends Service { AssetManager manager; Context app_context; - String filesDir, libDir; + String filesDir, libDir, cacheDir; double pixelDensityX; double pixelDensityY; @@ -126,12 +126,13 @@ public class EmacsService extends Service parameters. */ filesDir = app_context.getFilesDir ().getCanonicalPath (); libDir = getLibraryDirectory (); + cacheDir = app_context.getCacheDir ().getCanonicalPath (); Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir + " and libDir = " + libDir); EmacsNative.setEmacsParams (manager, filesDir, libDir, - (float) pixelDensityX, + cacheDir, (float) pixelDensityX, (float) pixelDensityY, this); @@ -407,4 +408,35 @@ public class EmacsService extends Service { return KeyEvent.keyCodeToString (keysym); } + + public void + sync () + { + Runnable runnable; + + runnable = new Runnable () { + public void + run () + { + synchronized (this) + { + notify (); + } + } + }; + + synchronized (runnable) + { + runOnUiThread (runnable); + + try + { + runnable.wait (); + } + catch (InterruptedException e) + { + EmacsNative.emacsAbort (); + } + } + } }; diff --git a/src/android.c b/src/android.c index ed162a903ba..3a965286460 100644 --- a/src/android.c +++ b/src/android.c @@ -88,6 +88,7 @@ struct android_emacs_service jmethodID get_screen_height; jmethodID detect_mouse; jmethodID name_keysym; + jmethodID sync; }; struct android_emacs_pixmap @@ -116,15 +117,18 @@ static AAssetManager *asset_manager; /* Whether or not Emacs has been initialized. */ static int emacs_initialized; -/* The path used to store site-lisp. */ +/* The directory used to store site-lisp. */ char *android_site_load_path; -/* The path used to store native libraries. */ +/* The directory used to store native libraries. */ char *android_lib_dir; -/* The path used to store game files. */ +/* The directory used to store game files. */ char *android_game_path; +/* The directory used to store temporary files. */ +char *android_cache_dir; + /* The display's pixel densities. */ double android_pixel_density_x, android_pixel_density_y; @@ -911,6 +915,7 @@ JNIEXPORT void JNICALL NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jobject local_asset_manager, jobject files_dir, jobject libs_dir, + jobject cache_dir, jfloat pixel_density_x, jfloat pixel_density_y, jobject emacs_service_object) @@ -986,6 +991,20 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, (*env)->ReleaseStringUTFChars (env, (jstring) libs_dir, java_string); + java_string = (*env)->GetStringUTFChars (env, (jstring) cache_dir, + NULL); + + if (!java_string) + emacs_abort (); + + android_cache_dir = strdup ((const char *) java_string); + + if (!android_files_dir) + emacs_abort (); + + (*env)->ReleaseStringUTFChars (env, (jstring) cache_dir, + java_string); + /* Calculate the site-lisp path. */ android_site_load_path = malloc (PATH_MAX + 1); @@ -1083,6 +1102,7 @@ android_init_emacs_service (void) FIND_METHOD (get_screen_height, "getScreenHeight", "(Z)I"); FIND_METHOD (detect_mouse, "detectMouse", "()Z"); FIND_METHOD (name_keysym, "nameKeysym", "(I)Ljava/lang/String;"); + FIND_METHOD (sync, "sync", "()V"); #undef FIND_METHOD } @@ -1216,6 +1236,9 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) /* Set HOME to the app data directory. */ setenv ("HOME", android_files_dir, 1); + /* Set TMPDIR to the temporary files directory. */ + setenv ("TMPDIR", android_cache_dir, 1); + /* Set the cwd to that directory as well. */ if (chdir (android_files_dir)) __android_log_print (ANDROID_LOG_WARN, __func__, @@ -3519,6 +3542,15 @@ android_get_keysym_name (int keysym, char *name_return, size_t size) ANDROID_DELETE_LOCAL_REF (string); } +void +android_sync (void) +{ + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.sync); + android_exception_check (); +} + #undef faccessat diff --git a/src/androidfns.c b/src/androidfns.c index ab136bc2722..bb37c415069 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -26,6 +26,7 @@ along with GNU Emacs. If not, see . */ #include "blockinput.h" #include "keyboard.h" #include "buffer.h" +#include "androidgui.h" #ifndef ANDROID_STUBIFY @@ -2282,6 +2283,13 @@ DEFUN ("x-show-tip", Fx_show_tip, Sx_show_tip, 1, 6, 0, android_map_raised (FRAME_ANDROID_WINDOW (tip_f)); unblock_input (); + /* Synchronize with the UI thread. This is required to prevent ugly + black splotches. */ + android_sync (); + + /* Garbage the tip frame too. */ + SET_FRAME_GARBAGED (tip_f); + w->must_be_updated_p = true; update_single_window (w); flush_frame (tip_f); diff --git a/src/androidgui.h b/src/androidgui.h index 0e075fda95e..9df5b073a7c 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -504,6 +504,7 @@ extern void android_move_resize_window (android_window, int, int, extern void android_map_raised (android_window); extern void android_translate_coordinates (android_window, int, int, int *, int *); +extern void android_sync (void); #endif diff --git a/src/androidmenu.c b/src/androidmenu.c index 7522f9c5a52..6fb4963174b 100644 --- a/src/androidmenu.c +++ b/src/androidmenu.c @@ -28,6 +28,8 @@ along with GNU Emacs. If not, see . */ #ifndef ANDROID_STUBIFY +#include + /* Flag indicating whether or not a popup menu has been posted and not yet popped down. */ @@ -188,6 +190,35 @@ android_process_events_for_menu (int *id) *id = x_display_list->menu_event_id; } +/* Structure describing a ``subprefix'' in the menu. */ + +struct android_menu_subprefix +{ + /* The subprefix above. */ + struct android_menu_subprefix *last; + + /* The subprefix itself. */ + Lisp_Object subprefix; +}; + +/* Free the subprefixes starting from *DATA. */ + +static void +android_free_subprefixes (void *data) +{ + struct android_menu_subprefix **head, *subprefix; + + head = data; + + while (*head) + { + subprefix = *head; + *head = subprefix->last; + + xfree (subprefix); + } +} + Lisp_Object android_menu_show (struct frame *f, int x, int y, int menuflags, Lisp_Object title, const char **error_name) @@ -198,13 +229,15 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, Lisp_Object pane_name, prefix; const char *pane_string; specpdl_ref count, count1; - Lisp_Object item_name, enable, def, tem; + Lisp_Object item_name, enable, def, tem, entry; jmethodID method; jobject store; bool rc; jobject window; - int id, item_id; + int id, item_id, submenu_depth; struct android_dismiss_menu_data data; + struct android_menu_subprefix *subprefix, *temp_subprefix; + struct android_menu_subprefix *subprefix_1; count = SPECPDL_INDEX (); @@ -232,7 +265,7 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, android_push_local_frame (); /* Iterate over the menu. */ - i = 0; + i = 0, submenu_depth = 0; while (i < menu_items_used) { @@ -241,6 +274,7 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, /* This is the start of a new submenu. However, it can be ignored here. */ i += 1; + submenu_depth += 1; } else if (EQ (AREF (menu_items, i), Qlambda)) { @@ -256,9 +290,18 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, if (store != context_menu) ANDROID_DELETE_LOCAL_REF (store); i += 1; + submenu_depth -= 1; - eassert (current_context_menu); + if (!current_context_menu || submenu_depth < 0) + { + __android_log_print (ANDROID_LOG_FATAL, __func__, + "unbalanced submenu pop in menu_items"); + emacs_abort (); + } } + else if (EQ (AREF (menu_items, i), Qt) + && submenu_depth != 0) + i += MENU_ITEMS_PANE_LENGTH; else if (EQ (AREF (menu_items, i), Qquote)) i += 1; else if (EQ (AREF (menu_items, i), Qt)) @@ -300,8 +343,8 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, /* This is an actual menu item (or submenu). Add it to the menu. */ - if (i + MENU_ITEMS_ITEM_LENGTH < menu_items_used && - NILP (AREF (menu_items, i + MENU_ITEMS_ITEM_LENGTH))) + if (i + MENU_ITEMS_ITEM_LENGTH < menu_items_used + && NILP (AREF (menu_items, i + MENU_ITEMS_ITEM_LENGTH))) { /* This is a submenu. Add it. */ title_string = (!NILP (item_name) @@ -312,7 +355,7 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, = (*android_java_env)->CallObjectMethod (android_java_env, current_context_menu, menu_class.add_submenu, - title_string); + title_string, NULL); android_exception_check (); if (store != context_menu) @@ -385,13 +428,78 @@ android_menu_show (struct frame *f, int x, int y, int menuflags, /* This means no menu item was selected. */ goto finish; - /* id is an index into menu_items. Check that it remains - valid. */ + /* This means the id is invalid. */ if (id >= ASIZE (menu_items)) goto finish; /* Now return the menu item at that location. */ - tem = AREF (menu_items, id); + tem = Qnil; + subprefix = NULL; + record_unwind_protect_ptr (android_free_subprefixes, &subprefix); + + /* Find the selected item, and its pane, to return + the proper value. */ + + prefix = entry = Qnil; + i = 0; + while (i < menu_items_used) + { + if (NILP (AREF (menu_items, i))) + { + temp_subprefix = xmalloc (sizeof *temp_subprefix); + temp_subprefix->last = subprefix; + subprefix = temp_subprefix; + subprefix->subprefix = prefix; + + prefix = entry; + i++; + } + else if (EQ (AREF (menu_items, i), Qlambda)) + { + prefix = subprefix->subprefix; + temp_subprefix = subprefix->last; + xfree (subprefix); + subprefix = temp_subprefix; + + i++; + } + else if (EQ (AREF (menu_items, i), Qt)) + { + prefix + = AREF (menu_items, i + MENU_ITEMS_PANE_PREFIX); + i += MENU_ITEMS_PANE_LENGTH; + } + /* Ignore a nil in the item list. + It's meaningful only for dialog boxes. */ + else if (EQ (AREF (menu_items, i), Qquote)) + i += 1; + else + { + entry = AREF (menu_items, i + MENU_ITEMS_ITEM_VALUE); + + if (i + MENU_ITEMS_ITEM_VALUE == id) + { + if (menuflags & MENU_KEYMAPS) + { + entry = list1 (entry); + + if (!NILP (prefix)) + entry = Fcons (prefix, entry); + + for (subprefix_1 = subprefix; subprefix_1; + subprefix_1 = subprefix_1->last) + if (!NILP (subprefix_1->subprefix)) + entry = Fcons (subprefix_1->subprefix, entry); + } + + tem = entry; + } + i += MENU_ITEMS_ITEM_LENGTH; + } + } + + Fprint (tem, Qexternal_debugging_output); + unblock_input (); return unbind_to (count, tem); diff --git a/src/androidterm.c b/src/androidterm.c index 4017fec60a5..6f452a52d85 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -689,6 +689,16 @@ handle_one_android_event (struct android_display_info *dpyinfo, goto OTHER; case ANDROID_MOTION_NOTIFY: + + previous_help_echo_string = help_echo_string; + help_echo_string = Qnil; + + if (hlinfo->mouse_face_hidden) + { + hlinfo->mouse_face_hidden = false; + clear_mouse_face (hlinfo); + } + f = any; if (f) diff --git a/src/menu.c b/src/menu.c index 73d4215b94b..e1f899858d3 100644 --- a/src/menu.c +++ b/src/menu.c @@ -167,7 +167,7 @@ ensure_menu_items (int items) } } -#ifdef HAVE_EXT_MENU_BAR +#if defined HAVE_EXT_MENU_BAR || defined HAVE_ANDROID /* Begin a submenu. */ @@ -191,7 +191,7 @@ push_submenu_end (void) menu_items_submenu_depth--; } -#endif /* HAVE_EXT_MENU_BAR */ +#endif /* HAVE_EXT_MENU_BAR || HAVE_ANDROID */ /* Indicate boundary between left and right. */ @@ -420,8 +420,9 @@ single_menu_item (Lisp_Object key, Lisp_Object item, Lisp_Object dummy, void *sk AREF (item_properties, ITEM_PROPERTY_SELECTED), AREF (item_properties, ITEM_PROPERTY_HELP)); -#if defined (USE_X_TOOLKIT) || defined (USE_GTK) || defined (HAVE_NS) \ - || defined (HAVE_NTGUI) || defined (HAVE_HAIKU) || defined (HAVE_PGTK) +#if defined (USE_X_TOOLKIT) || defined (USE_GTK) || defined (HAVE_NS) \ + || defined (HAVE_NTGUI) || defined (HAVE_HAIKU) || defined (HAVE_PGTK) \ + || defined (HAVE_ANDROID) /* Display a submenu using the toolkit. */ if (FRAME_WINDOW_P (XFRAME (Vmenu_updating_frame)) && ! (NILP (map) || NILP (enabled))) -- cgit v1.3 From 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. --- .gitignore | 2 + INSTALL.android | 35 ++ admin/merge-gnulib | 4 +- build-aux/config.guess | 6 +- build-aux/config.sub | 6 +- configure.ac | 160 ++++++- doc/emacs/android.texi | 11 +- doc/emacs/emacs.texi | 6 + doc/emacs/input.texi | 60 +++ doc/lispref/commands.texi | 46 +- doc/lispref/frames.texi | 6 +- etc/NEWS | 20 + java/Makefile.in | 37 +- java/debug.sh | 155 ++++--- 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 +++- lib-src/Makefile.in | 11 + lib-src/asset-directory-tool.c | 280 ++++++++++++ lib/faccessat.c | 4 - lib/fpending.c | 2 +- lib/getdelim.c | 147 ++++++ lib/getline.c | 27 ++ lib/gnulib.mk.in | 40 ++ lib/stdalign.in.h | 109 +---- lib/stdio-impl.h | 8 +- lisp/button.el | 30 +- lisp/frame.el | 8 +- lisp/subr.el | 14 +- lisp/touch-screen.el | 189 +++++++- lisp/wid-edit.el | 122 +++-- m4/getdelim.m4 | 111 +++++ m4/getline.m4 | 109 +++++ m4/gnulib-comp.m4 | 32 ++ m4/stdalign.m4 | 127 ++++-- m4/unistd_h.m4 | 1 + m4/utimens.m4 | 3 +- m4/utimensat.m4 | 4 +- m4/xattr.m4 | 42 +- src/alloc.c | 34 +- src/android.c | 569 +++++++++++++++++++----- src/android.h | 8 + src/androidterm.c | 5 +- src/emacs.c | 8 + src/fileio.c | 15 + src/filelock.c | 12 +- src/frame.c | 2 + src/keyboard.c | 6 +- src/menu.c | 9 +- src/sfnt.c | 10 +- src/sfntfont-android.c | 4 +- src/sfntfont.c | 111 ++++- xcompile/lib/faccessat.c | 4 - xcompile/lib/getdelim.c | 147 ++++++ xcompile/lib/getline.c | 27 ++ xcompile/lib/gnulib.mk.in | 42 ++ xcompile/lib/qcopy-acl.c | 36 +- xcompile/lib/verify.h | 8 +- xcompile/malloc/dynarray-skeleton.c | 528 ++++++++++++++++++++++ xcompile/malloc/dynarray.h | 177 ++++++++ xcompile/malloc/dynarray_at_failure.c | 40 ++ xcompile/malloc/dynarray_emplace_enlarge.c | 77 ++++ xcompile/malloc/dynarray_finalize.c | 66 +++ xcompile/malloc/dynarray_resize.c | 68 +++ xcompile/malloc/dynarray_resize_clear.c | 39 ++ xcompile/malloc/scratch_buffer.h | 135 ++++++ xcompile/malloc/scratch_buffer_dupfree.c | 41 ++ xcompile/malloc/scratch_buffer_grow.c | 56 +++ xcompile/malloc/scratch_buffer_grow_preserve.c | 67 +++ xcompile/malloc/scratch_buffer_set_array_size.c | 64 +++ 81 files changed, 4118 insertions(+), 628 deletions(-) create mode 100644 doc/emacs/input.texi create mode 100644 lib-src/asset-directory-tool.c create mode 100644 lib/getdelim.c create mode 100644 lib/getline.c create mode 100644 m4/getdelim.m4 create mode 100644 m4/getline.m4 create mode 100644 xcompile/lib/getdelim.c create mode 100644 xcompile/lib/getline.c create mode 100644 xcompile/malloc/dynarray-skeleton.c create mode 100644 xcompile/malloc/dynarray.h create mode 100644 xcompile/malloc/dynarray_at_failure.c create mode 100644 xcompile/malloc/dynarray_emplace_enlarge.c create mode 100644 xcompile/malloc/dynarray_finalize.c create mode 100644 xcompile/malloc/dynarray_resize.c create mode 100644 xcompile/malloc/dynarray_resize_clear.c create mode 100644 xcompile/malloc/scratch_buffer.h create mode 100644 xcompile/malloc/scratch_buffer_dupfree.c create mode 100644 xcompile/malloc/scratch_buffer_grow.c create mode 100644 xcompile/malloc/scratch_buffer_grow_preserve.c create mode 100644 xcompile/malloc/scratch_buffer_set_array_size.c (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/.gitignore b/.gitignore index cf739958403..3bc40c67489 100644 --- a/.gitignore +++ b/.gitignore @@ -231,6 +231,7 @@ ID # Executables. *.exe a.out +lib-src/asset-directory-tool lib-src/be-resources lib-src/blessmail lib-src/ctags @@ -253,6 +254,7 @@ nextstep/GNUstep/Emacs.base/Resources/Info-gnustep.plist src/bootstrap-emacs src/emacs src/emacs-[0-9]* +src/sfnt src/Emacs src/temacs src/dmpstruct.h diff --git a/INSTALL.android b/INSTALL.android index ee54b053a9b..63c856fe56c 100644 --- a/INSTALL.android +++ b/INSTALL.android @@ -59,6 +59,41 @@ built for. The generated package can be uploaded onto an SD card (or similar medium) and installed on-device. +BUILDING WITH OLD NDK VERSIONS + +Building Emacs with an old version of the Android NDK requires special +setup. This is because there is no separate C compiler binary for +each version of Android in those versions of the NDK. + +Before running `configure', you must identify three variables: + + - What kind of Android system you are building Emacs for. + + - The minimum API version of Android you want to build Emacs for. + + - The locations of the system root and include files for that + version of Android in the NDK. + +That information must then be specified as arguments to the NDK C +compiler. For example: + + ./configure [...] \ + ANDROID_CC="i686-linux-android-gcc \ + --sysroot=/path/to/ndk/platforms/android-14/arch-x86/" + ANDROID_CFLAGS="-isystem /path/to/ndk/sysroot/usr/include \ + -isystem /path/to/ndk/sysroot/usr/include/i686-linux-android \ + -D__ANDROID_API__=14" + +Where __ANDROID_API__ and the version identifier in +"platforms/android-14" defines the version of Android you are building +for, and the include directories specify the paths to the relevant +Android headers. In addition, it may be necessary to specify +"-gdwarf-2", due to a bug in the Android NDK. + +Emacs is known to build for Android 2.2 (API version 8) or later, and +run on Android 2.3 or later. It is supposed to run on Android 2.2 as +well. + This file is part of GNU Emacs. diff --git a/admin/merge-gnulib b/admin/merge-gnulib index b99e38672a4..38f1418c759 100755 --- a/admin/merge-gnulib +++ b/admin/merge-gnulib @@ -37,7 +37,7 @@ GNULIB_MODULES=' fchmodat fcntl fcntl-h fdopendir file-has-acl filemode filename filevercmp flexmember fpieee free-posix fstatat fsusage fsync futimens - getloadavg getopt-gnu getrandom gettime gettimeofday gitlog-to-changelog + getline getloadavg getopt-gnu getrandom gettime gettimeofday gitlog-to-changelog ieee754-h ignore-value intprops largefile libgmp lstat manywarnings memmem-simple mempcpy memrchr memset_explicit minmax mkostemp mktime @@ -141,7 +141,7 @@ cp -- "$gnulib_srcdir"/lib/af_alg.h \ ./autogen.sh # Finally, update the files in lib/ to xcompile/lib. -rsync "$src"/lib "$src"/xcompile +rsync -r "$src"/lib "$src"/xcompile # Remove unnecessary files. rm -f "$src"/xcompile/lib/*.mk.in "$src"/xcompile/lib/Makefile.in diff --git a/build-aux/config.guess b/build-aux/config.guess index 980b0208381..69188da73d7 100755 --- a/build-aux/config.guess +++ b/build-aux/config.guess @@ -1,10 +1,10 @@ #! /bin/sh # Attempt to guess a canonical system name. -# Copyright 1992-2022 Free Software Foundation, Inc. +# Copyright 1992-2023 Free Software Foundation, Inc. # shellcheck disable=SC2006,SC2268 # see below for rationale -timestamp='2022-09-17' +timestamp='2023-01-01' # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by @@ -60,7 +60,7 @@ version="\ GNU config.guess ($timestamp) Originally written by Per Bothner. -Copyright 1992-2022 Free Software Foundation, Inc. +Copyright 1992-2023 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." diff --git a/build-aux/config.sub b/build-aux/config.sub index baf1512b3c0..d6731d655c0 100755 --- a/build-aux/config.sub +++ b/build-aux/config.sub @@ -1,10 +1,10 @@ #! /bin/sh # Configuration validation subroutine script. -# Copyright 1992-2022 Free Software Foundation, Inc. +# Copyright 1992-2023 Free Software Foundation, Inc. # shellcheck disable=SC2006,SC2268 # see below for rationale -timestamp='2022-09-17' +timestamp='2023-01-01' # This file is free software; you can redistribute it and/or modify it # under the terms of the GNU General Public License as published by @@ -76,7 +76,7 @@ Report bugs and patches to ." version="\ GNU config.sub ($timestamp) -Copyright 1992-2022 Free Software Foundation, Inc. +Copyright 1992-2023 Free Software Foundation, Inc. This is free software; see the source for copying conditions. There is NO warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE." diff --git a/configure.ac b/configure.ac index 92cf58fdf28..1f5618a172f 100644 --- a/configure.ac +++ b/configure.ac @@ -31,8 +31,19 @@ if test "$XCONFIGURE" = "android"; then # Android! AC_MSG_NOTICE([called to recursively configure Emacs \ for Android.]) - # Set CC to ANDROID_CC. + # Set CC to ANDROID_CC and CFLAGS to ANDROID_CFLAGS. CC=$ANDROID_CC + # Set -Wno-implicit-function-declaration. Building Emacs for older + # versions of Android requires configure tests to fail if the + # functions are not defined, as the Android library in the NDK + # defines subroutines that are not available in the headers being + # used. + CFLAGS="$ANDROID_CFLAGS -Werror=implicit-function-declaration" + # Don't explicitly enable support for large files unless Emacs is + # being built for API 21 or later. Otherwise, mmap does not work. + if test "$ANDROID_SDK" -lt "21"; then + enable_largefile=no + fi fi dnl Set emacs_config_options to the options of 'configure', quoted for the shell, @@ -758,7 +769,10 @@ tools such as aapt, dx, and aidl): The cross-compiler should then be specified: - ANDROID_CC=/path/to/armv7a-linux-androideabi19-clang]) + ANDROID_CC=/path/to/armv7a-linux-androideabi19-clang + +In addition, you may pass any special arguments to the cross-compiler +via the ANDROID_CFLAGS environment variable.]) elif test "$with_android" = "no" || test "$with_android" = ""; then ANDROID=no else @@ -808,6 +822,29 @@ EOF a valid path to android.jar. See config.log for more details.]) fi + AC_CACHE_CHECK([whether or not android.jar is new enough], + [emacs_cv_android_s_or_later], + AS_IF([rm -f conftest.class +cat << EOF > conftest.java + +import android.os.Build; + +class conftest +{ + private static int test = Build.VERSION_CODES.S; +} + +EOF +("$JAVAC" -classpath "$with_android" -target 1.7 -source 1.7 conftest.java \ + -d . >&AS_MESSAGE_LOG_FD 2>&1) && test -s conftest.class && rm -f conftest.class], + [emacs_cv_android_s_or_later=yes], + [emacs_cv_android_s_or_later=no])) + + if test "$emacs_cv_android_s_or_later" = "no"; then + AC_MSG_ERROR([Emacs must be built with an android.jar file produced for \ +Android 13 (S) or later.]) + fi + ANDROID_JAR="$with_android" AC_PATH_PROGS([AAPT], [aapt], [], "${SDK_BUILD_TOOLS}:$PATH") @@ -845,6 +882,7 @@ for your machine. For example: AC_MSG_CHECKING([for the kind of Android system Emacs is being built for]) cc_target=`${ANDROID_CC} -v 2>&1 | sed -n 's/Target: //p'` case "$cc_target" in +[ *i[3-6]86*) android_abi=x86 ;; *x86_64*) android_abi=x86_64 @@ -857,10 +895,13 @@ for your machine. For example: ;; *arm*) android_abi=armeabi ;; +] *) AC_MSG_ERROR([configure could not determine the type of Android \ binary Emacs is being configured for. Please port this configure script \ to your Android system, or verify that you specified the correct compiler \ -in the ANDROID_CC variable when you ran configure.]) +in the ANDROID_CC variable when you ran configure. + +The compiler target is: $cc_target]) ;; esac AC_MSG_RESULT([$android_abi]) @@ -879,18 +920,121 @@ in the ANDROID_CC variable when you ran configure.]) AC_MSG_RESULT([$android_sdk]) ANDROID_MIN_SDK=$android_sdk else - AC_MSG_RESULT([unknown ($cc_target); assuming 8]) - AC_MSG_WARN([configure could not determine the versions of Android \ + # This is probably GCC. + [ cat << EOF > conftest.c +#include +extern const char *foo; + +int +main (void) +{ +#if __ANDROID_API__ < 7 + foo = "emacs_api_6"; +#elif __ANDROID_API__ < 8 + foo = "emacs_api_7"; +#elif __ANDROID_API__ < 9 + foo = "emacs_api_8"; +#elif __ANDROID_API__ < 10 + foo = "emacs_api_9"; +#elif __ANDROID_API__ < 11 + foo = "emacs_api_10"; +#elif __ANDROID_API__ < 12 + foo = "emacs_api_11"; +#elif __ANDROID_API__ < 13 + foo = "emacs_api_12"; +#elif __ANDROID_API__ < 14 + foo = "emacs_api_13"; +#elif __ANDROID_API__ < 15 + foo = "emacs_api_14"; +#elif __ANDROID_API__ < 16 + foo = "emacs_api_15"; +#elif __ANDROID_API__ < 17 + foo = "emacs_api_16"; +#elif __ANDROID_API__ < 18 + foo = "emacs_api_17"; +#elif __ANDROID_API__ < 19 + foo = "emacs_api_18"; +#elif __ANDROID_API__ < 20 + foo = "emacs_api_19"; +#elif __ANDROID_API__ < 21 + foo = "emacs_api_20"; +#elif __ANDROID_API__ < 22 + foo = "emacs_api_21"; +#elif __ANDROID_API__ < 23 + foo = "emacs_api_22"; +#elif __ANDROID_API__ < 24 + foo = "emacs_api_23"; +#elif __ANDROID_API__ < 25 + foo = "emacs_api_24"; +#elif __ANDROID_API__ < 26 + foo = "emacs_api_25"; +#elif __ANDROID_API__ < 27 + foo = "emacs_api_26"; +#elif __ANDROID_API__ < 28 + foo = "emacs_api_27"; +#elif __ANDROID_API__ < 29 + foo = "emacs_api_28"; +#elif __ANDROID_API__ < 30 + foo = "emacs_api_29"; +#elif __ANDROID_API__ < 31 + foo = "emacs_api_30"; +#elif __ANDROID_API__ < 32 + foo = "emacs_api_31"; +#elif __ANDROID_API__ < 33 + foo = "emacs_api_32"; +#elif __ANDROID_API__ < 34 + foo = "emacs_api_33"; +#else + foo = "emacs_api_future"; +#endif +} +EOF] + + AC_CACHE_VAL([emacs_cv_android_api], + [$ANDROID_CC $ANDROID_CFLAGS -c conftest.c -o conftest.o \ + && emacs_cv_android_api=`grep -ao -E \ + "emacs_api_([[0-9][0-9]]?|future)" conftest.o`]) + android_sdk="$emacs_cv_android_api" + rm -rf conftest.c conftest.o + + # If this version of the NDK requires __ANDROID_API__ to be + # specified, then complain to the user. + if test "$android_sdk" = "emacs_api_future"; then + AC_MSG_ERROR([The version of Android to build for was not specified. +You must tell the Android compiler what version of Android to build for, +by defining the __ANDROID_API__ preprocessor macro in ANDROID_CC, like so: + + ANDROID_CC="/path/to/ndk/arm-linux-android-gcc -D__ANDROID_API__=8"]) + fi + + if test -n "$android_sdk"; then + android_sdk=`echo $android_sdk | sed -n 's/emacs_api_//p'` + AC_MSG_RESULT([$android_sdk]) + ANDROID_MIN_SDK=$android_sdk + else + AC_MSG_RESULT([unknown ($cc_target); assuming 8]) + AC_MSG_ERROR([configure could not determine the versions of Android \ a binary built with this compiler will run on. The generated application \ package will likely install on older systems but crash on startup.]) + android_sdk=8 + fi fi AC_SUBST([ANDROID_MIN_SDK]) + # Now tell java/Makefile if Emacs is being built for Android 4.3 or + # earlier. + ANDROID_SDK_18_OR_EARLIER= + if test "$android_sdk" -lt "18"; then + ANDROID_SDK_18_OR_EARLIER=yes + fi + AC_SUBST([ANDROID_SDK_18_OR_EARLIER]) + # Save confdefs.h and config.log for now. mv -f confdefs.h _confdefs.h mv -f config.log _config.log - AS_IF([XCONFIGURE=android ANDROID_CC="$ANDROID_CC" $0], [], + AS_IF([XCONFIGURE=android ANDROID_CC="$ANDROID_CC" \ + ANDROID_SDK="$android_sdk" $0], [], [AC_MSG_ERROR([Failed to cross-configure Emacs for android.])]) # Now set ANDROID to yes. @@ -1989,7 +2133,6 @@ AC_CACHE_CHECK([for math library], d = frexp (d, &i); d = ldexp (d, i); d = log (d); - d = log2 (d); d = log10 (d); d = pow (d, d); d = rint (d); @@ -2283,6 +2426,9 @@ for Android, but all API calls need to be stubbed out]) # Link with the sfnt font library and sfntfont.o, along with # sfntfont-android.o. ANDROID_OBJ="$ANDROID_OBJ sfnt.o sfntfont.o sfntfont-android.o" + + # Check for some functions not always present in the NDK. + AC_CHECK_DECLS([android_get_device_api_level]) fi fi diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 8806a2a2bf6..01a2e68a97e 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -1,5 +1,5 @@ @c This is part of the Emacs manual. -@c Copyright (C) 2021--2023 Free Software Foundation, Inc. +@c Copyright (C) 2023 Free Software Foundation, Inc. @c See file emacs.texi for copying conditions. @node Android @appendix Emacs and Android @@ -9,6 +9,10 @@ Alliance. This section describes the peculiarities of using Emacs on an Android device running Android 2.2 or later. + Android devices commonly rely on user input through a touch screen +or digitizer device. For more information about using them with +Emacs, @pxref{other Input Devices}. + @menu * What is Android?:: Preamble. * Android Startup:: Starting up Emacs on Android. @@ -108,11 +112,6 @@ There are no @file{.} and @file{..} directories inside the @item Files in the @file{/assets} directory are always read only, and have to be completely read in to memory each time they are opened. - -@item -@code{directory-files} does not return a useful value on the -@file{/assets} directory itself, and does not return subdirectories -inside subdirectories of the @file{/assets} directory. @end itemize Aside from the @file{/assets} directory, Android programs normally diff --git a/doc/emacs/emacs.texi b/doc/emacs/emacs.texi index d21d09bf920..9fd169cd5cc 100644 --- a/doc/emacs/emacs.texi +++ b/doc/emacs/emacs.texi @@ -225,6 +225,7 @@ Appendices * Haiku:: Using Emacs on Haiku. * Android:: Using Emacs on Android. * Microsoft Windows:: Using Emacs on Microsoft Windows and MS-DOS. +* Other Input Devices:: Using Emacs with other input devices. * Manifesto:: What's GNU? Gnu's Not Unix! * Glossary:: Terms used in this manual. @@ -1265,6 +1266,10 @@ Emacs and Android * Android Environment:: Running Emacs under Android. * Android Fonts:: Font selection under Android. +Emacs and unconventional input devices + +* Touchscreens:: Using Emacs on touchscreens. + Emacs and Microsoft Windows/MS-DOS * Windows Startup:: How to start Emacs on Windows. @@ -1636,6 +1641,7 @@ Lisp programming. @include macos.texi @include haiku.texi @include android.texi +@include input.texi @c Includes msdos-xtra. @include msdos.texi @include gnu.texi diff --git a/doc/emacs/input.texi b/doc/emacs/input.texi new file mode 100644 index 00000000000..0e029c401e3 --- /dev/null +++ b/doc/emacs/input.texi @@ -0,0 +1,60 @@ +@c This is part of the Emacs manual. +@c Copyright (C) 2023 Free Software Foundation, Inc. +@c See file emacs.texi for copying conditions. +@node Other Input Devices +@appendix Emacs and unconventional input devices +@cindex other input devices + + Emacs was originally developed with the assumption that users will +be sitting in front of a desktop computer, with a keyboard and perhaps +a suitable pointing device such as a mouse. + + However, recent developments in the X Window System, and in other +operating systems such as Android, mean that this assumption no longer +holds true. As a result, Emacs now has support for other kinds of +input devices, which is detailed here. + +@menu +* Touchscreens:: Using Emacs on touchscreens. +@end menu + +@node Touchscreens +@section Using Emacs on touchscreens +@cindex touchscreens + + Touchscreen input works by having the user press tools onto the +screen, which can be his own fingers, or a pointing device such as a +stylus, in order to manipulate the contents there in. + + When running under the X Window System or Android, Emacs +automatically detects and maps the following touchscreen gestures to +common actions: + +@itemize @bullet +@item +@cindex tapping, touchscreens + ``Tapping'', meaning to briefly place and lift a tool from the +display, will result in Emacs selecting the window that was tapped, +and executing any command bound to @code{mouse-1} at that location in +the window. If the tap happened on top of a link (@pxref{Mouse +References}), then Emacs will follow the link instead. + +@item +@cindex scrolling, touchscreens + ``Scrolling'', meaning to place a tool on the display and move it up +or down, will result in Emacs scrolling the window contents in the +direction where the tool moves. + +@item +@cindex dragging, touchscreens + ``Dragging'', meaning to place a tool on the display and leave it +there for a while before moving the tool around, will make Emacs set +the point to where the tool was and begin selecting text under the +tool as it moves around, much like what would happen if @code{mouse-1} +were to be held down. @xref{Mouse Commands}. +@end itemize + +@vindex touch-screen-delay + By default, Emacs considers a tool as having been left on the +display for a while after 0.7 seconds, but this can be changed by +customizing the variable @code{touch-screen-delay}. diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index 6d1ce145fbf..59367a2cecc 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -1999,24 +1999,24 @@ each point is represented by a cons of an arbitrary number identifying the point and a mouse position list (@pxref{Click Events}) specifying the position of the finger when the event occurred. -In addition, @code{touchscreen-begin} events also have imaginary -prefixes keys added by @code{read-key-sequence} when they originate on -top of a special part of a frame or window. @xref{Key Sequence -Input}. The reason the other touch screen events do not undergo this -treatment is that they are rarely useful without being used in tandem -from their corresponding @code{touchscreen-begin} events. - @table @code @cindex @code{touchscreen-begin} event @item (touchscreen-begin @var{point}) This event is sent when @var{point} is created by the user pressing a finger against the touchscreen. +These events also have imaginary prefixes keys added by +@code{read-key-sequence} when they originate on top of a special part +of a frame or window. @xref{Key Sequence Input}. The reason the +other touch screen events do not undergo this treatment is that they +are rarely useful without being used in tandem from their +corresponding @code{touchscreen-begin} events. + @cindex @code{touchscreen-update} event @item (touchscreen-update @var{points}) This event is sent when a point on the touchscreen has changed position. @var{points} is a list of touch points containing the -up-to-date positions of each touch point currently on the touchscreen. +up-to-date positions of each touch point currently on the touchscxcompile/reen. @cindex @code{touchscreen-end} event @item (touchscreen-end @var{point}) @@ -2030,6 +2030,36 @@ generate any corresponding @code{touchscreen-begin} or @code{touchscreen-end} events; instead, the menu bar may be displayed when @code{touchscreen-end} should have been delivered. +@cindex handling touch screen events +@cindex tap and drag, touch screen gestures +Emacs provides two functions to handle touch screen events. They are +intended to be used by a command bound to @code{touchscreen-begin} to +handle common gestures. + +@defun touch-screen-track-tap event &optional update data +This function is used to track a single ``tap'' gesture originating +from the @code{touchscreen-begin} event @var{event}, often used to +set the point or to activate a button. It waits for a +@code{touchscreen-end} event with the same touch identifier to arrive, +at which point it returns @code{t}, signifying the end of the gesture. + +If a @code{touchscreen-update} event arrives in the mean time and +contains at least one touchpoint with the same identifier as in +@var{event}, the function @var{update} is called with two arguments, +the list of touchpoints in that @code{touchscreen-update} event, and +@var{data}. + +If any other event arrives in the mean time, @code{nil} is returned. +The caller should not perform any action in that case. +@end defun + +@defun touch-screen-track-drag event update &optional data +This function is used to track a single ``drag'' gesture originating +from the @code{touchscreen-begin} event @code{event}. Currently, it +behaves identically to @code{touch-screen-track-tap}, but differences +are anticipated in the future. +@end defun + @node Focus Events @subsection Focus Events @cindex focus event diff --git a/doc/lispref/frames.texi b/doc/lispref/frames.texi index fb96b96ec8c..2ceafab7a6b 100644 --- a/doc/lispref/frames.texi +++ b/doc/lispref/frames.texi @@ -3725,9 +3725,9 @@ This function displays a pop-up menu and returns an indication of what selection the user makes. The argument @var{position} specifies where on the screen to put the -top left corner of the menu. It can be either a mouse button event -(which says to put the menu where the user actuated the button) or a -list of this form: +top left corner of the menu. It can be either a mouse button or +@code{touchscreen-begin} event (which says to put the menu where the +user actuated the button) or a list of this form: @example ((@var{xoffset} @var{yoffset}) @var{window}) diff --git a/etc/NEWS b/etc/NEWS index cde6783349f..a309cd17a93 100644 --- a/etc/NEWS +++ b/etc/NEWS @@ -24,6 +24,12 @@ applies, and please also update docstrings as needed. * Installation Changes in Emacs 30.1 +** Emacs has been ported to the Android operating system. +This requires Emacs to be compiled on another computer. The Android +NDK, SDK, and a suitable Java compiler must also be installed. + +See the file 'INSTALL.android' for more details. + * Startup Changes in Emacs 30.1 @@ -53,6 +59,10 @@ trash when deleting. Default is nil. * Editing Changes in Emacs 30.1 +** Emacs now has better support for touchscreen events. +Many touch screen gestures are now implemented, as is support for +tapping buttons and opening menus. + ** New helper variable 'transpose-sexps-function'. Emacs now can set this variable to customize the behavior of the 'transpose-sexps' function. @@ -189,6 +199,16 @@ This user option has been obsoleted in Emacs 27, use * Lisp Changes in Emacs 30.1 ++++ +** 'x-popup-menu' now understands touch screen events. +When a 'touchscreen-begin' or 'touchscreen-end' event is passed as the +POSITION argument, it will behave as if that event was a mouse event. + +** New functions for handling touch screen events. +The new functions 'touch-screen-track-tap' and +'touch-screen-track-drag' handle tracking common touch screen gestures +from within a command. + ** New or changed byte-compilation warnings --- diff --git a/java/Makefile.in b/java/Makefile.in index c539fb0f1fb..22c912fdce5 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -21,6 +21,10 @@ top_builddir = @top_builddir@ top_srcdir = @top_srcdir@ version = @version@ +# This is the host lib-src and lib, not the cross compiler's lib-src. +libsrc = ../lib-src +EXEEXT = @EXEEXT@ + -include ${top_builddir}/src/verbose.mk SHELL = @SHELL@ @@ -29,14 +33,25 @@ AAPT = @AAPT@ D8 = @D8@ ZIPALIGN = @ZIPALIGN@ JARSIGNER = @JARSIGNER@ +JARSIGNER_FLAGS = ANDROID_JAR = @ANDROID_JAR@ ANDROID_ABI = @ANDROID_ABI@ +ANDROID_SDK_18_OR_EARLIER = @ANDROID_SDK_18_OR_EARLIER@ WARN_JAVAFLAGS = -Xlint:deprecation JAVAFLAGS = -classpath "$(ANDROID_JAR):." -target 1.7 -source 1.7 \ $(WARN_JAVAFLAGS) -SIGN_EMACS = -keystore emacs.keystore -storepass emacs1 +# Android 4.3 and earlier require Emacs to be signed with a different +# digital signature algorithm. + +ifneq (,$(ANDROID_SDK_18_OR_EARLIER)) +JARSIGNER_FLAGS = -sigalg MD5withRSA -digestalg SHA1 +else +JARSIGNER_FLAGS = +endif + +SIGN_EMACS = -keystore emacs.keystore -storepass emacs1 $(JARSIGNER_FLAGS) JAVA_FILES = $(shell find . -type f -name *.java) CLASS_FILES = $(foreach file,$(JAVA_FILES),$(basename $(file)).class) @@ -82,7 +97,14 @@ CROSS_LIBS = ../xcompile/src/libemacs.so ../xcompile/lib-src/ctags ../xcompile/lib-src/ebrowse &: make -C ../xcompile lib-src/$(notdir $@) -emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) AndroidManifest.xml +# This is needed to generate the ``.directory-tree'' file used by the +# Android emulations of readdir and faccessat. + +$(libsrc)/asset-directory-tool: + $(MAKE) -C $(libsrc) $(notdir $@) + +emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) $(libsrc)/asset-directory-tool \ + AndroidManifest.xml # Make the working directory for this stuff rm -rf install_temp mkdir -p install_temp/lib/$(ANDROID_ABI) @@ -106,6 +128,9 @@ emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) AndroidManifest.xml rm -rf $${subdir}/[mM]akefile*[.-]in ; \ rm -rf $${subdir}/Makefile; \ done +# Generate the directory tree for those directories. + $(libsrc)/asset-directory-tool install_temp/assets \ + install_temp/assets/directory-tree # Install architecture dependents to lib/$(ANDROID_ABI). This # perculiar naming scheme is required to make Android preserve these # binaries upon installation. @@ -120,10 +145,12 @@ emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) AndroidManifest.xml cp -f $$file install_temp/lib/$(ANDROID_ABI); \ fi \ done -# Package everything. - $(AAPT) package -I "$(ANDROID_JAR)" -F $@ -f -M AndroidManifest.xml +# Package everything. Specifying the assets on this command line is +# necessary for AAssetManager_getNextFileName to work on old versions +# of Android. + $(AAPT) package -I "$(ANDROID_JAR)" -F $@ -f -M AndroidManifest.xml \ + -A install_temp/assets pushd install_temp; $(AAPT) add ../$@ `find lib -type f`; popd - pushd install_temp; $(AAPT) add ../$@ `find assets -type f`; popd rm -rf install_temp # Makefile itself. diff --git a/java/debug.sh b/java/debug.sh index aa80aeeebcd..7008664c049 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -93,7 +93,7 @@ while [ $# -gt 0 ]; do shift done -if [ -z $devices ]; then +if [ -z "$devices" ]; then echo "No devices are available." exit 1 fi @@ -117,25 +117,43 @@ if [ -z $app_data_dir ]; then echo "Is it installed?" fi -echo "Found application data directory at $app_data_dir..." - -# Find which PIDs are associated with org.gnu.emacs -package_uid=`adb -s $device shell run-as $package id -u` - -if [ -z $package_uid ]; then - echo "Failed to obtain UID of packages named $package" - exit 1 -fi - -# First, run ps -u $package_uid -o PID,CMD to fetch the list of -# process IDs. -package_pids=`adb -s $device shell run-as $package ps -u $package_uid -o PID,CMD` - -# Next, remove lines matching "ps" itself. -package_pids=`awk -- '{ - if (!match ($0, /(PID|ps)/)) - print $1 -}' <<< $package_pids` +echo "Found application data directory at" "$app_data_dir" + +# Generate an awk script to extract PIDs from Android ps output. It +# is enough to run `ps' as the package user on newer versions of +# Android, but that doesn't work on Android 2.3. +cat << EOF > tmp.awk +BEGIN { + pid = 0; + pid_column = 2; +} + +{ + # Remove any trailing carriage return from the input line. + gsub ("\r", "", \$NF) + + # If this is line 1, figure out which column contains the PID. + if (NR == 1) + { + for (n = 1; n <= NF; ++n) + { + if (\$n == "PID") + pid_column=n; + } + } + else if (\$NF == "$package") + print \$pid_column +} +EOF + +# Make sure that file disappears once this script exits. +trap "rm -f $(pwd)/tmp.awk" 0 + +# First, run ps to fetch the list of process IDs. +package_pids=`adb -s $device shell ps` + +# Next, extract the list of PIDs currently running. +package_pids=`awk -f tmp.awk <<< $package_pids` if [ "$attach_existing" != "yes" ]; then # Finally, kill each existing process. @@ -149,19 +167,20 @@ if [ "$attach_existing" != "yes" ]; then echo "Starting activity $activity and attaching debugger" # Exit if the activity could not be started. - adb -s $device shell am start -D "$package/$activity" + adb -s $device shell am start -D -n "$package/$activity" if [ ! $? ]; then exit 1; fi + # Sleep for a bit. Otherwise, the process may not have started + # yet. + sleep 1 + # Now look for processes matching the package again. - package_pids=`adb -s $device shell run-as $package ps -u $package_uid -o PID,CMD` + package_pids=`adb -s $device shell ps` # Next, remove lines matching "ps" itself. - package_pids=`awk -- '{ - if (!match ($0, /(PID|ps)/)) - print $1 -}' <<< $package_pids` + package_pids=`awk -f tmp.awk <<< $package_pids` fi pid=$package_pids @@ -170,10 +189,10 @@ num_pids=`wc -w <<< "$package_pids"` if [ $num_pids -gt 1 ]; then echo "More than one process was started:" echo "" - adb -s $device shell run-as $package ps -u $package_uid | awk -- '{ - if (!match ($0, /ps/)) - print $0 - }' + adb -s $device shell run-as $package ps | awk -- "{ + if (!match (\$0, /ps/) && match (\$0, /$package/)) + print \$0 + }" echo "" printf "Which one do you want to attach to? " read pid @@ -182,10 +201,12 @@ elif [ -z $package_pids ]; then exit 1 fi -# This isn't necessary when attaching gdb to an existing process. +# If either --jdb was specified or debug.sh is not connecting to an +# existing process, then store a suitable JDB invocation in +# jdb_command. GDB will then run JDB to unblock the application from +# the wait dialog after startup. + if [ "$jdb" = "yes" ] || [ "$attach_existing" != yes ]; then - # Start JDB to make the wait dialog disappear. - echo "Attaching JDB to unblock the application." adb -s $device forward --remove-all adb -s $device forward "tcp:$jdb_port" "jdwp:$pid" @@ -203,20 +224,42 @@ if [ "$jdb" = "yes" ] || [ "$attach_existing" != yes ]; then $jdb_command exit 1 fi +fi - exec 4<> /tmp/file-descriptor-stamp +if [ -n "$jdb_command" ]; then + echo "Starting JDB to unblock application." - # Now run JDB with IO redirected to file descriptor 4 in a subprocess. - $jdb_command <&4 >&4 & + # Start JDB to unblock the application. + coproc JDB { $jdb_command; } - character= - # Next, wait until the prompt is found. - while read -n1 -u 4 character; do - if [ "$character" = ">" ]; then - echo "JDB attached successfully" - break; + # Tell JDB to first suspend all threads. + echo "suspend" >&${JDB[1]} + + # Tell JDB to print a magic string once the program is + # initialized. + echo "print \"__verify_jdb_has_started__\"" >&${JDB[1]} + + # Now wait for JDB to give the string back. + line= + while :; do + read -u ${JDB[0]} line + if [ ! $? ]; then + echo "Failed to read JDB output" + exit 1 fi + + case "$line" in + *__verify_jdb_has_started__*) + # Android only polls for a Java debugger every 200ms, so + # the debugger must be connected for at least that long. + echo "Pausing 1 second for the program to continue." + sleep 1 + break + ;; + esac done + + # Note that JDB does not exit until GDB is fully attached! fi # See if gdbserver has to be uploaded @@ -234,18 +277,19 @@ fi echo "Attaching gdbserver to $pid on $device..." exec 5<> /tmp/file-descriptor-stamp +rm -f /tmp/file-descriptor-stamp if [ -z "$gdbserver" ]; then adb -s $device shell run-as $package $gdbserver_bin --once \ - "+debug.$package_uid.socket" --attach $pid >&5 & - gdb_socket="localfilesystem:$app_data_dir/debug.$package_uid.socket" + "+debug.$package.socket" --attach $pid >&5 & + gdb_socket="localfilesystem:$app_data_dir/debug.$package.socket" else # Normally the program cannot access $gdbserver_bin when it is # placed in /data/local/tmp. adb -s $device shell $gdbserver_bin --once \ - "+/data/local/tmp/debug.$package_uid.socket" \ + "+/data/local/tmp/debug.$package.socket" \ --attach $pid >&5 & - gdb_socket="localfilesystem:/data/local/tmp/debug.$package_uid.socket" + gdb_socket="localfilesystem:/data/local/tmp/debug.$package.socket" fi # Wait until gdbserver successfully runs. @@ -256,7 +300,7 @@ while read -u 5 line; do break; ;; *error* | *Error* | failed ) - echo $line + echo "GDB error:" $line exit 1 ;; * ) @@ -264,19 +308,18 @@ while read -u 5 line; do esac done -if [ "$attach_existing" != "yes" ]; then - # Send EOF to JDB to make it go away. This will also cause - # Android to allow Emacs to continue executing. - echo "Making JDB go away..." - echo "exit" >&4 - read -u 4 line - echo "JDB has gone away with $line" +# Now that GDB is attached, tell the Java debugger to resume execution +# and then exit. + +if [ -n "$jdb_command" ]; then + echo "resume" >&${JDB[1]} + echo "exit" >&${JDB[1]} fi # Forward the gdb server port here. adb -s $device forward "tcp:$gdb_port" $gdb_socket if [ ! $? ]; then - echo "Failed to forward $app_data_dir/debug.$package_uid.socket" + echo "Failed to forward $app_data_dir/debug.$package.socket" echo "to $gdb_port! Perhaps you need to specify a different port" echo "with --port?" exit 1; @@ -284,4 +327,4 @@ fi # Finally, start gdb with any extra arguments needed. cd "$oldpwd" -gdb --eval-command "" --eval-command "target remote localhost:$gdb_port" $gdbargs +gdb --eval-command "target remote localhost:$gdb_port" $gdbargs 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 diff --git a/lib-src/Makefile.in b/lib-src/Makefile.in index 7d491557529..ddf141e2770 100644 --- a/lib-src/Makefile.in +++ b/lib-src/Makefile.in @@ -148,6 +148,9 @@ HAVE_BE_APP=@HAVE_BE_APP@ HAIKU_LIBS=@HAIKU_LIBS@ HAIKU_CFLAGS=@HAIKU_CFLAGS@ +## Android build-time support +ANDROID=@ANDROID@ + # emacsclientw.exe for MinGW, empty otherwise CLIENTW = @CLIENTW@ @@ -164,8 +167,13 @@ UTILITIES = hexl${EXEEXT} \ ifeq ($(HAVE_BE_APP),yes) DONT_INSTALL= make-docfile${EXEEXT} make-fingerprint${EXEEXT} be-resources else +ifeq ($(HAVE_ANDROID),yes) +DONT_INSTALL = make-docfile${EXEEXT} make-fingerprint${EXEEXT} \ + asset-directory-tool +else DONT_INSTALL= make-docfile${EXEEXT} make-fingerprint${EXEEXT} endif +endif # Like UTILITIES, but they're not system-dependent, and should not be # deleted by the distclean target. @@ -414,6 +422,9 @@ etags${EXEEXT}: ${etags_deps} ctags${EXEEXT}: ${srcdir}/ctags.c ${etags_deps} $(AM_V_CCLD)$(CC) ${ALL_CFLAGS} -o $@ $< $(etags_libs) +asset-directory-tool${EXEEXT}: ${srcdir}/asset-directory-tool.c $(config_h) + $(AM_V_CCLD)$(CC) ${ALL_CFLAGS} $< $(LOADLIBES) -o $@ + ebrowse${EXEEXT}: ${srcdir}/ebrowse.c ${srcdir}/../lib/min-max.h $(NTLIB) \ $(config_h) $(AM_V_CCLD)$(CC) ${ALL_CFLAGS} -o $@ $< $(NTLIB) $(LOADLIBES) diff --git a/lib-src/asset-directory-tool.c b/lib-src/asset-directory-tool.c new file mode 100644 index 00000000000..e53398eceb0 --- /dev/null +++ b/lib-src/asset-directory-tool.c @@ -0,0 +1,280 @@ +/* Android asset directory tool. + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +/* This program takes a directory as input, and generates a + ``directory-tree'' file suitable for inclusion in an Android + application package. + + Such a file records the layout of the `assets' directory in the + package. Emacs records this information itself and uses it in the + Android emulation of readdir, because the system asset manager APIs + are routinely buggy, and are often unable to locate directories or + files. + + The file is packed, with no data alignment guarantees made. The + file starts with the bytes "EMACS", following which is the name of + the first file or directory, a NULL byte and an unsigned int + indicating the offset from the start of the file to the start of + the next sibling. Following that is a list of subdirectories or + files in the same format. The long is stored LSB first. */ + + + +struct directory_tree +{ + /* The offset to the next sibling. */ + size_t offset; + + /* The name of this directory or file. */ + char *name; + + /* Subdirectories and files inside this directory. */ + struct directory_tree *children, *next; +}; + +/* Exit with EXIT_FAILURE, after printing a description of a failing + function WHAT along with the details of the error. */ + +static _Noreturn void +croak (const char *what) +{ + perror (what); + exit (EXIT_FAILURE); +} + +/* Like malloc, but aborts on failure. */ + +static void * +xmalloc (size_t size) +{ + void *ptr; + + ptr = malloc (size); + + if (!ptr) + croak ("malloc"); + + return ptr; +} + +/* Recursively build a struct directory_tree structure for each + subdirectory or file in DIR, in preparation for writing it out to + disk. PARENT should be the directory tree associated with the + parent directory, or else PARENT->offset must be initialized to + 5. */ + +static void +main_1 (DIR *dir, struct directory_tree *parent) +{ + struct dirent *dirent; + int dir_fd, fd; + struct stat statb; + struct directory_tree *this, **last; + size_t length; + DIR *otherdir; + + dir_fd = dirfd (dir); + last = &parent->children; + + while ((dirent = readdir (dir))) + { + /* Determine what kind of file DIRENT is. */ + + if (fstatat (dir_fd, dirent->d_name, &statb, + AT_SYMLINK_NOFOLLOW) == -1) + croak ("fstatat"); + + /* Ignore . and ... */ + + if (!strcmp (dirent->d_name, ".") + || !strcmp (dirent->d_name, "..")) + continue; + + length = strlen (dirent->d_name); + + if (statb.st_mode & S_IFDIR) + { + /* This is a directory. Write its name followed by a + trailing slash, then a NULL byte, and the offset to the + next sibling. */ + this = xmalloc (sizeof *this); + this->children = NULL; + this->next = NULL; + *last = this; + last = &this->next; + this->name = xmalloc (length + 2); + strcpy (this->name, dirent->d_name); + + /* Now record the offset to the end of this directory. This + is length + 1, for the file name, and 5 more bytes for + the trailing NULL and long. */ + this->offset = parent->offset + length + 6; + + /* Terminate that with a slash and trailing NULL byte. */ + this->name[length] = '/'; + this->name[length + 1] = '\0'; + + /* Open and build that directory recursively. */ + + fd = openat (dir_fd, dirent->d_name, O_DIRECTORY, + O_RDONLY); + if (fd < 0) + croak ("openat"); + otherdir = fdopendir (fd); + if (!otherdir) + croak ("fdopendir"); + + main_1 (otherdir, this); + + /* Close this directory. */ + closedir (otherdir); + + /* Finally, set parent->offset to this->offset as well. */ + parent->offset = this->offset; + } + else if (statb.st_mode & S_IFREG) + { + /* This is a regular file. */ + this = xmalloc (sizeof *this); + this->children = NULL; + this->next = NULL; + *last = this; + last = &this->next; + this->name = xmalloc (length + 1); + strcpy (this->name, dirent->d_name); + + /* This is one byte shorter because there is no trailing + slash. */ + this->offset = parent->offset + length + 5; + parent->offset = this->offset; + } + } +} + +/* Write the struct directory_tree TREE and all of is children to the + file descriptor FD. OFFSET is the offset of TREE and may be + modified; it is only used for checking purposes. */ + +static void +main_2 (int fd, struct directory_tree *tree, size_t *offset) +{ + ssize_t size; + struct directory_tree *child; + unsigned int output; + + /* Write tree->name with the trailing NULL byte. */ + size = strlen (tree->name) + 1; + if (write (fd, tree->name, size) < size) + croak ("write"); + + /* Write the offset. */ + output = htole32 (tree->offset); + if (write (fd, &output, 4) < 1) + croak ("write"); + size += 4; + + /* Now update offset. */ + *offset += size; + + /* Write out each child. */ + for (child = tree->children; child; child = child->next) + main_2 (fd, child, offset); + + /* Verify the offset is correct. */ + if (tree->offset != *offset) + { + fprintf (stderr, + "asset-directory-tool: invalid offset: expected %tu, " + "got %tu.\n" + "Please report this bug to bug-gnu-emacs@gnu.org, along\n" + "with an archive containing the contents of the java/inst" + "all_temp directory.\n", + tree->offset, *offset); + abort (); + } +} + +int +main (int argc, char **argv) +{ + int fd; + DIR *indir; + struct directory_tree tree; + size_t offset; + + if (argc != 3) + { + fprintf (stderr, "usage: %s directory output-file\n", + argv[0]); + return EXIT_FAILURE; + } + + fd = open (argv[2], O_CREAT | O_TRUNC | O_RDWR, + S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP); + + if (fd < 0) + { + perror ("open"); + return EXIT_FAILURE; + } + + indir = opendir (argv[1]); + + if (!indir) + { + perror ("opendir"); + return EXIT_FAILURE; + } + + /* Write the first 5 byte header to FD. */ + + if (write (fd, "EMACS", 5) < 5) + { + perror ("write"); + return EXIT_FAILURE; + } + + /* Now iterate through children of INDIR, building the directory + tree. */ + tree.offset = 5; + tree.children = NULL; + + main_1 (indir, &tree); + closedir (indir); + + /* Finally, write the directory tree to the output file. */ + offset = 5; + for (; tree.children; tree.children = tree.children->next) + main_2 (fd, tree.children, &offset); + + return 0; +} diff --git a/lib/faccessat.c b/lib/faccessat.c index 807e2669683..ac8977cfd65 100644 --- a/lib/faccessat.c +++ b/lib/faccessat.c @@ -43,11 +43,7 @@ orig_faccessat (int fd, char const *name, int mode, int flag) /* Write "unistd.h" here, not , otherwise OSF/1 5.1 DTK cc eliminates this include because of the preliminary #include above. */ -#ifdef __ANROID__ -#include -#else #include "unistd.h" -#endif #ifndef HAVE_ACCESS /* Mingw lacks access, but it also lacks real vs. effective ids, so diff --git a/lib/fpending.c b/lib/fpending.c index afa840b8512..e57155e586e 100644 --- a/lib/fpending.c +++ b/lib/fpending.c @@ -41,7 +41,7 @@ __fpending (FILE *fp) return fp->_IO_write_ptr - fp->_IO_write_base; #elif defined __sferror || defined __DragonFly__ || defined __ANDROID__ /* FreeBSD, NetBSD, OpenBSD, DragonFly, Mac OS X, Cygwin < 1.7.34, Minix 3, Android */ - return fp->_p - fp->_bf._base; + return fp_->_p - fp_->_bf._base; #elif defined __EMX__ /* emx+gcc */ return fp->_ptr - fp->_buffer; #elif defined __minix /* Minix */ diff --git a/lib/getdelim.c b/lib/getdelim.c new file mode 100644 index 00000000000..79ec3dd12a3 --- /dev/null +++ b/lib/getdelim.c @@ -0,0 +1,147 @@ +/* getdelim.c --- Implementation of replacement getdelim function. + Copyright (C) 1994, 1996-1998, 2001, 2003, 2005-2023 Free Software + Foundation, Inc. + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + This file 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . */ + +/* Ported from glibc by Simon Josefsson. */ + +/* Don't use __attribute__ __nonnull__ in this compilation unit. Otherwise gcc + optimizes away the lineptr == NULL || n == NULL || fp == NULL tests below. */ +#define _GL_ARG_NONNULL(params) + +#include + +#include + +#include +#include +#include +#include + +#ifndef SSIZE_MAX +# define SSIZE_MAX ((ssize_t) (SIZE_MAX / 2)) +#endif + +#if USE_UNLOCKED_IO +# include "unlocked-io.h" +# define getc_maybe_unlocked(fp) getc(fp) +#elif !HAVE_FLOCKFILE || !HAVE_FUNLOCKFILE || !HAVE_DECL_GETC_UNLOCKED +# undef flockfile +# undef funlockfile +# define flockfile(x) ((void) 0) +# define funlockfile(x) ((void) 0) +# define getc_maybe_unlocked(fp) getc(fp) +#else +# define getc_maybe_unlocked(fp) getc_unlocked(fp) +#endif + +static void +alloc_failed (void) +{ +#if defined _WIN32 && ! defined __CYGWIN__ + /* Avoid errno problem without using the realloc module; see: + https://lists.gnu.org/r/bug-gnulib/2016-08/msg00025.html */ + errno = ENOMEM; +#endif +} + +/* Read up to (and including) a DELIMITER from FP into *LINEPTR (and + NUL-terminate it). *LINEPTR is a pointer returned from malloc (or + NULL), pointing to *N characters of space. It is realloc'ed as + necessary. Returns the number of characters read (not including + the null terminator), or -1 on error or EOF. */ + +ssize_t +getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp) +{ + ssize_t result; + size_t cur_len = 0; + + if (lineptr == NULL || n == NULL || fp == NULL) + { + errno = EINVAL; + return -1; + } + + flockfile (fp); + + if (*lineptr == NULL || *n == 0) + { + char *new_lineptr; + *n = 120; + new_lineptr = (char *) realloc (*lineptr, *n); + if (new_lineptr == NULL) + { + alloc_failed (); + result = -1; + goto unlock_return; + } + *lineptr = new_lineptr; + } + + for (;;) + { + int i; + + i = getc_maybe_unlocked (fp); + if (i == EOF) + { + result = -1; + break; + } + + /* Make enough space for len+1 (for final NUL) bytes. */ + if (cur_len + 1 >= *n) + { + size_t needed_max = + SSIZE_MAX < SIZE_MAX ? (size_t) SSIZE_MAX + 1 : SIZE_MAX; + size_t needed = 2 * *n + 1; /* Be generous. */ + char *new_lineptr; + + if (needed_max < needed) + needed = needed_max; + if (cur_len + 1 >= needed) + { + result = -1; + errno = EOVERFLOW; + goto unlock_return; + } + + new_lineptr = (char *) realloc (*lineptr, needed); + if (new_lineptr == NULL) + { + alloc_failed (); + result = -1; + goto unlock_return; + } + + *lineptr = new_lineptr; + *n = needed; + } + + (*lineptr)[cur_len] = i; + cur_len++; + + if (i == delimiter) + break; + } + (*lineptr)[cur_len] = '\0'; + result = cur_len ? cur_len : result; + + unlock_return: + funlockfile (fp); /* doesn't set errno */ + + return result; +} diff --git a/lib/getline.c b/lib/getline.c new file mode 100644 index 00000000000..85f16ab8bac --- /dev/null +++ b/lib/getline.c @@ -0,0 +1,27 @@ +/* getline.c --- Implementation of replacement getline function. + Copyright (C) 2005-2007, 2009-2023 Free Software Foundation, Inc. + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + This file 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . */ + +/* Written by Simon Josefsson. */ + +#include + +#include + +ssize_t +getline (char **lineptr, size_t *n, FILE *stream) +{ + return getdelim (lineptr, n, '\n', stream); +} diff --git a/lib/gnulib.mk.in b/lib/gnulib.mk.in index 2097850c812..66ba4a4adf9 100644 --- a/lib/gnulib.mk.in +++ b/lib/gnulib.mk.in @@ -109,6 +109,7 @@ # fsusage \ # fsync \ # futimens \ +# getline \ # getloadavg \ # getopt-gnu \ # getrandom \ @@ -172,11 +173,19 @@ MOSTLYCLEANFILES += core *.stackdump # Start of GNU Make output. +AAPT = @AAPT@ ALLOCA = @ALLOCA@ ALLOCA_H = @ALLOCA_H@ ALSA_CFLAGS = @ALSA_CFLAGS@ ALSA_LIBS = @ALSA_LIBS@ AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +ANDROID = @ANDROID@ +ANDROID_ABI = @ANDROID_ABI@ +ANDROID_CFLAGS = @ANDROID_CFLAGS@ +ANDROID_JAR = @ANDROID_JAR@ +ANDROID_LIBS = @ANDROID_LIBS@ +ANDROID_MIN_SDK = @ANDROID_MIN_SDK@ +ANDROID_OBJ = @ANDROID_OBJ@ APPLE_UNIVERSAL_BUILD = @APPLE_UNIVERSAL_BUILD@ AR = @AR@ ARFLAGS = @ARFLAGS@ @@ -216,6 +225,7 @@ CYGWIN_OBJ = @CYGWIN_OBJ@ C_SWITCH_MACHINE = @C_SWITCH_MACHINE@ C_SWITCH_SYSTEM = @C_SWITCH_SYSTEM@ C_SWITCH_X_SITE = @C_SWITCH_X_SITE@ +D8 = @D8@ DBUS_CFLAGS = @DBUS_CFLAGS@ DBUS_LIBS = @DBUS_LIBS@ DBUS_OBJ = @DBUS_OBJ@ @@ -278,8 +288,10 @@ GL_COND_OBJ_FSTATAT_CONDITION = @GL_COND_OBJ_FSTATAT_CONDITION@ GL_COND_OBJ_FSUSAGE_CONDITION = @GL_COND_OBJ_FSUSAGE_CONDITION@ GL_COND_OBJ_FSYNC_CONDITION = @GL_COND_OBJ_FSYNC_CONDITION@ GL_COND_OBJ_FUTIMENS_CONDITION = @GL_COND_OBJ_FUTIMENS_CONDITION@ +GL_COND_OBJ_GETDELIM_CONDITION = @GL_COND_OBJ_GETDELIM_CONDITION@ GL_COND_OBJ_GETDTABLESIZE_CONDITION = @GL_COND_OBJ_GETDTABLESIZE_CONDITION@ GL_COND_OBJ_GETGROUPS_CONDITION = @GL_COND_OBJ_GETGROUPS_CONDITION@ +GL_COND_OBJ_GETLINE_CONDITION = @GL_COND_OBJ_GETLINE_CONDITION@ GL_COND_OBJ_GETLOADAVG_CONDITION = @GL_COND_OBJ_GETLOADAVG_CONDITION@ GL_COND_OBJ_GETOPT_CONDITION = @GL_COND_OBJ_GETOPT_CONDITION@ GL_COND_OBJ_GETRANDOM_CONDITION = @GL_COND_OBJ_GETRANDOM_CONDITION@ @@ -883,6 +895,8 @@ INSTALL_PROGRAM = @INSTALL_PROGRAM@ INSTALL_SCRIPT = @INSTALL_SCRIPT@ INT32_MAX_LT_INTMAX_MAX = @INT32_MAX_LT_INTMAX_MAX@ INT64_MAX_EQ_LONG_MAX = @INT64_MAX_EQ_LONG_MAX@ +JARSIGNER = @JARSIGNER@ +JAVAC = @JAVAC@ JSON_CFLAGS = @JSON_CFLAGS@ JSON_LIBS = @JSON_LIBS@ JSON_OBJ = @JSON_OBJ@ @@ -1211,6 +1225,7 @@ REPLACE_WCTOMB = @REPLACE_WCTOMB@ REPLACE_WRITE = @REPLACE_WRITE@ RSVG_CFLAGS = @RSVG_CFLAGS@ RSVG_LIBS = @RSVG_LIBS@ +SDK_BULD_TOOLS = @SDK_BULD_TOOLS@ SEPCHAR = @SEPCHAR@ SETFATTR = @SETFATTR@ SETTINGS_CFLAGS = @SETTINGS_CFLAGS@ @@ -1268,6 +1283,7 @@ XARGS_LIMIT = @XARGS_LIMIT@ XCB_LIBS = @XCB_LIBS@ XCOMPOSITE_CFLAGS = @XCOMPOSITE_CFLAGS@ XCOMPOSITE_LIBS = @XCOMPOSITE_LIBS@ +XCONFIGURE = @XCONFIGURE@ XCRUN = @XCRUN@ XDBE_CFLAGS = @XDBE_CFLAGS@ XDBE_LIBS = @XDBE_LIBS@ @@ -1292,6 +1308,7 @@ XSYNC_CFLAGS = @XSYNC_CFLAGS@ XSYNC_LIBS = @XSYNC_LIBS@ XWIDGETS_OBJ = @XWIDGETS_OBJ@ X_TOOLKIT_TYPE = @X_TOOLKIT_TYPE@ +ZIPALIGN = @ZIPALIGN@ ac_ct_CC = @ac_ct_CC@ ac_ct_CXX = @ac_ct_CXX@ ac_ct_OBJC = @ac_ct_OBJC@ @@ -1337,6 +1354,7 @@ gl_GNULIB_ENABLED_e80bf6f757095d2e5fc94dafb8f8fc8b_CONDITION = @gl_GNULIB_ENABLE gl_GNULIB_ENABLED_ef455225c00f5049c808c2eda3e76866_CONDITION = @gl_GNULIB_ENABLED_ef455225c00f5049c808c2eda3e76866_CONDITION@ gl_GNULIB_ENABLED_euidaccess_CONDITION = @gl_GNULIB_ENABLED_euidaccess_CONDITION@ gl_GNULIB_ENABLED_fd38c7e463b54744b77b98aeafb4fa7c_CONDITION = @gl_GNULIB_ENABLED_fd38c7e463b54744b77b98aeafb4fa7c_CONDITION@ +gl_GNULIB_ENABLED_getdelim_CONDITION = @gl_GNULIB_ENABLED_getdelim_CONDITION@ gl_GNULIB_ENABLED_getdtablesize_CONDITION = @gl_GNULIB_ENABLED_getdtablesize_CONDITION@ gl_GNULIB_ENABLED_getgroups_CONDITION = @gl_GNULIB_ENABLED_getgroups_CONDITION@ gl_GNULIB_ENABLED_lchmod_CONDITION = @gl_GNULIB_ENABLED_lchmod_CONDITION@ @@ -2093,6 +2111,18 @@ gl_V_at = $(AM_V_GEN) endif ## end gnulib module gen-header +## begin gnulib module getdelim +ifeq (,$(OMIT_GNULIB_MODULE_getdelim)) + +ifneq (,$(gl_GNULIB_ENABLED_getdelim_CONDITION)) +ifneq (,$(GL_COND_OBJ_GETDELIM_CONDITION)) +libgnu_a_SOURCES += getdelim.c +endif + +endif +endif +## end gnulib module getdelim + ## begin gnulib module getdtablesize ifeq (,$(OMIT_GNULIB_MODULE_getdtablesize)) @@ -2117,6 +2147,16 @@ endif endif ## end gnulib module getgroups +## begin gnulib module getline +ifeq (,$(OMIT_GNULIB_MODULE_getline)) + +ifneq (,$(GL_COND_OBJ_GETLINE_CONDITION)) +libgnu_a_SOURCES += getline.c +endif + +endif +## end gnulib module getline + ## begin gnulib module getloadavg ifeq (,$(OMIT_GNULIB_MODULE_getloadavg)) diff --git a/lib/stdalign.in.h b/lib/stdalign.in.h index 17357810c7c..6523546f16d 100644 --- a/lib/stdalign.in.h +++ b/lib/stdalign.in.h @@ -17,117 +17,18 @@ /* Written by Paul Eggert and Bruno Haible. */ +/* Define two obsolescent C11 macros, assuming alignas and alignof are + either keywords or alignasof-defined macros. */ + #ifndef _GL_STDALIGN_H #define _GL_STDALIGN_H -/* ISO C11 for platforms that lack it. - - References: - ISO C11 (latest free draft - ) - sections 6.5.3.4, 6.7.5, 7.15. - C++11 (latest free draft - ) - section 18.10. */ - -/* alignof (TYPE), also known as _Alignof (TYPE), yields the alignment - requirement of a structure member (i.e., slot or field) that is of - type TYPE, as an integer constant expression. - - This differs from GCC's and clang's __alignof__ operator, which can - yield a better-performing alignment for an object of that type. For - example, on x86 with GCC and on Linux/x86 with clang, - __alignof__ (double) and __alignof__ (long long) are 8, whereas - alignof (double) and alignof (long long) are 4 unless the option - '-malign-double' is used. - - The result cannot be used as a value for an 'enum' constant, if you - want to be portable to HP-UX 10.20 cc and AIX 3.2.5 xlc. */ - -/* FreeBSD 9.1 , included by and lots of other - standard headers, defines conflicting implementations of _Alignas - and _Alignof that are no better than ours; override them. */ -#undef _Alignas -#undef _Alignof - -/* GCC releases before GCC 4.9 had a bug in _Alignof. See GCC bug 52023 - . - clang versions < 8.0.0 have the same bug. */ -#if (!defined __STDC_VERSION__ || __STDC_VERSION__ < 201112 \ - || (defined __GNUC__ && __GNUC__ < 4 + (__GNUC_MINOR__ < 9) \ - && !defined __clang__) \ - || (defined __clang__ && __clang_major__ < 8)) -# ifdef __cplusplus -# if (201103 <= __cplusplus || defined _MSC_VER) -# define _Alignof(type) alignof (type) -# else - template struct __alignof_helper { char __a; __t __b; }; -# define _Alignof(type) offsetof (__alignof_helper, __b) -# define _GL_STDALIGN_NEEDS_STDDEF 1 -# endif -# else -# define _Alignof(type) offsetof (struct { char __a; type __b; }, __b) -# define _GL_STDALIGN_NEEDS_STDDEF 1 -# endif -#endif -#if ! (defined __cplusplus && (201103 <= __cplusplus || defined _MSC_VER)) -# define alignof _Alignof -#endif -#define __alignof_is_defined 1 - -/* alignas (A), also known as _Alignas (A), aligns a variable or type - to the alignment A, where A is an integer constant expression. For - example: - - int alignas (8) foo; - struct s { int a; int alignas (8) bar; }; - - aligns the address of FOO and the offset of BAR to be multiples of 8. - - A should be a power of two that is at least the type's alignment - and at most the implementation's alignment limit. This limit is - 2**28 on typical GNUish hosts, and 2**13 on MSVC. To be portable - to MSVC through at least version 10.0, A should be an integer - constant, as MSVC does not support expressions such as 1 << 3. - To be portable to Sun C 5.11, do not align auto variables to - anything stricter than their default alignment. - - The following C11 requirements are not supported here: - - - If A is zero, alignas has no effect. - - alignas can be used multiple times; the strictest one wins. - - alignas (TYPE) is equivalent to alignas (alignof (TYPE)). - - */ - -#if !defined __STDC_VERSION__ || __STDC_VERSION__ < 201112 -# if defined __cplusplus && (201103 <= __cplusplus || defined _MSC_VER) -# define _Alignas(a) alignas (a) -# elif (!defined __attribute__ \ - && ((defined __APPLE__ && defined __MACH__ \ - ? 4 < __GNUC__ + (1 <= __GNUC_MINOR__) \ - : __GNUC__ && !defined __ibmxl__) \ - || (4 <= __clang_major__) \ - || (__ia64 && (61200 <= __HP_cc || 61200 <= __HP_aCC)) \ - || __ICC || 0x590 <= __SUNPRO_C || 0x0600 <= __xlC__)) -# define _Alignas(a) __attribute__ ((__aligned__ (a))) -# elif 1300 <= _MSC_VER -# define _Alignas(a) __declspec (align (a)) -# endif -#endif -#if ((defined _Alignas \ - && !(defined __cplusplus && (201103 <= __cplusplus || defined _MSC_VER))) \ - || (defined __STDC_VERSION__ && 201112 <= __STDC_VERSION__)) -# define alignas _Alignas -#endif #if (defined alignas \ + || (defined __STDC_VERSION__ && 202311 <= __STDC_VERSION__) \ || (defined __cplusplus && (201103 <= __cplusplus || defined _MSC_VER))) # define __alignas_is_defined 1 #endif -/* Include if needed for offsetof. */ -#if _GL_STDALIGN_NEEDS_STDDEF -# include -#endif +#define __alignof_is_defined 1 #endif /* _GL_STDALIGN_H */ diff --git a/lib/stdio-impl.h b/lib/stdio-impl.h index 81e7f838372..46608bed198 100644 --- a/lib/stdio-impl.h +++ b/lib/stdio-impl.h @@ -70,6 +70,12 @@ # define _gl_flags_file_t int # else # define _gl_flags_file_t short +# endif +# ifdef __LP64__ +# define _gl_file_offset_t int64_t +# else + /* see https://android.googlesource.com/platform/bionic/+/master/docs/32-bit-abi.md */ +# define _gl_file_offset_t __kernel_off_t # endif /* Up to this commit from 2015-10-12 @@ -96,7 +102,7 @@ unsigned char _nbuf[1]; \ struct { unsigned char *_base; size_t _size; } _lb; \ int _blksize; \ - fpos_t _offset; \ + _gl_file_offset_t _offset; \ /* More fields, not relevant here. */ \ } *) fp) # else diff --git a/lisp/button.el b/lisp/button.el index f043073ea86..65abb81ec46 100644 --- a/lisp/button.el +++ b/lisp/button.el @@ -72,7 +72,12 @@ Mode-specific keymaps may want to use this as their parent keymap." ;; mode-line or header-line, the `mode-line' or `header-line' prefix ;; shouldn't be necessary! " " #'push-button - " " #'push-button) + " " #'push-button + ;; `push-button' will automatically dispatch to + ;; `touch-screen-track-tap'. + " " #'push-button + " " #'push-button + "" #'push-button) (define-minor-mode button-mode "A minor mode for navigating to buttons with the TAB key." @@ -454,18 +459,22 @@ instead of starting at the next button." (defun push-button (&optional pos use-mouse-action) "Perform the action specified by a button at location POS. -POS may be either a buffer position or a mouse-event. If -USE-MOUSE-ACTION is non-nil, invoke the button's `mouse-action' -property instead of its `action' property; if the button has no -`mouse-action', the value of `action' is used instead. +POS may be either a buffer position, a mouse-event, or a +`touchscreen-down' event. If USE-MOUSE-ACTION is non-nil, invoke +the button's `mouse-action' property instead of its `action' +property; if the button has no `mouse-action', the value of +`action' is used instead. + +If POS is a `touchscreen-down' event, wait for the corresponding +`touchscreen-up' event before calling `push-button'. The action in both cases may be either a function to call or a marker to display and is invoked using `button-activate' (which see). POS defaults to point, except when `push-button' is invoked -interactively as the result of a mouse-event, in which case, the -mouse event is used. +interactively as the result of a mouse-event or touchscreen +event, in which case, the position in the event event is used. If there's no button at POS, do nothing and return nil, otherwise return t. @@ -483,7 +492,12 @@ pushing a button, use the `button-describe' command." (if str-button ;; mode-line, header-line, or display string event. (button-activate str t) - (push-button (posn-point posn) t))))) + (if (eq (car pos) 'touchscreen-down) + ;; If touch-screen-track tap returns nil, then the + ;; tap was cancelled. + (when (touch-screen-track-tap pos) + (push-button (posn-point posn) t)) + (push-button (posn-point posn) t)))))) ;; POS is just normal position (let ((button (button-at (or pos (point))))) (when button diff --git a/lisp/frame.el b/lisp/frame.el index d35df71a6cc..f21c0c369bd 100644 --- a/lisp/frame.el +++ b/lisp/frame.el @@ -2149,8 +2149,12 @@ frame's display)." "Return non-nil if popup menus are supported on DISPLAY. DISPLAY can be a display name, a frame, or nil (meaning the selected frame's display). -Support for popup menus requires that the mouse be available." - (display-mouse-p display)) +Support for popup menus requires that a suitable pointing device +be available." + ;; Android menus work fine with touch screens as well, and one must + ;; be present. + (or (eq (framep-on-display display) 'android) + (display-mouse-p display))) (defun display-graphic-p (&optional display) "Return non-nil if DISPLAY is a graphic display. diff --git a/lisp/subr.el b/lisp/subr.el index f909b63aabe..345816dbd23 100644 --- a/lisp/subr.el +++ b/lisp/subr.el @@ -1636,7 +1636,13 @@ nil or (STRING . POSITION)'. `posn-timestamp': The time the event occurred, in milliseconds. For more information, see Info node `(elisp)Click Events'." - (or (and (consp event) (nth 1 event)) + (or (and (consp event) + ;; Ignore touchscreen events. They store the posn in a + ;; different format, and can have multiple posns. + (not (memq (car event) '(touchscreen-begin + touchscreen-update + touchscreen-end))) + (nth 1 event)) (event--posn-at-point))) (defun event-end (event) @@ -1644,7 +1650,11 @@ For more information, see Info node `(elisp)Click Events'." EVENT should be a click, drag, or key press event. See `event-start' for a description of the value returned." - (or (and (consp event) (nth (if (consp (nth 2 event)) 2 1) event)) + (or (and (consp event) + (not (memq (car event) '(touchscreen-begin + touchscreen-update + touchscreen-end))) + (nth (if (consp (nth 2 event)) 2 1) event)) (event--posn-at-point))) (defsubst event-click-count (event) diff --git a/lisp/touch-screen.el b/lisp/touch-screen.el index 192a09b3a29..bcc4f5e9be3 100644 --- a/lisp/touch-screen.el +++ b/lisp/touch-screen.el @@ -105,10 +105,10 @@ known position of the tool." (setcar (nthcdr 3 touch-screen-current-tool) 'held) ;; Go to the initial position of the touchpoint and activate the ;; mark. - (with-selected-window (cadr touch-screen-current-tool) - (set-mark (posn-point (nth 4 touch-screen-current-tool))) - (goto-char (mark)) - (activate-mark))))) + (select-window (cadr touch-screen-current-tool)) + (set-mark (posn-point (nth 4 touch-screen-current-tool))) + (goto-char (mark)) + (activate-mark)))) (defun touch-screen-handle-point-update (point) "Notice that the touch point POINT has changed position. @@ -132,9 +132,7 @@ happens, cancel `touch-screen-current-timer', and set the field to `drag'. Then, activate the mark and start dragging. If the fourth element of `touch-screen-current-tool' is `drag', -then move point to the position of POINT. - -Set `touch-screen-current-tool' to nil should any error occur." +then move point to the position of POINT." (let ((window (nth 1 touch-screen-current-tool)) (what (nth 3 touch-screen-current-tool))) (cond ((null what) @@ -252,19 +250,42 @@ POINT should be the point currently tracked as If the fourth argument of `touch-screen-current-tool' is nil, move point to the position of POINT, selecting the window under -POINT as well; if there is a button at POINT, then activate the -button there. Otherwise, deactivate the mark. Then, display the -on-screen keyboard." +POINT as well, and deactivate the mark; if there is a button or +link at POINT, call the command bound to `mouse-2' there. +Otherwise, call the command bound to `mouse-1'." (let ((what (nth 3 touch-screen-current-tool))) (cond ((null what) (when (windowp (posn-window (cdr point))) ;; Select the window that was tapped. (select-window (posn-window (cdr point))) - (let ((button (button-at (posn-point (cdr point))))) - (when button - (button-activate button t)) + ;; Now simulate a mouse click there. If there is a link + ;; or a button, use mouse-2 to push it. + (let ((event (list (if (or (mouse-on-link-p (cdr point)) + (button-at (posn-point (cdr point)))) + 'mouse-2 + 'mouse-1) + (cdr point))) + ;; Look for an extra keymap to look in. + (keymap (and (posn-object (cdr point)) + (stringp + (posn-object (cdr point))) + (get-text-property + 0 'keymap + (posn-object (cdr point))))) + command) + (save-excursion + (when (posn-point (cdr point)) + (goto-char (posn-point (cdr point)))) + (if keymap + (setq keymap (cons keymap (current-active-maps t))) + (setq keymap (current-active-maps t))) + (setq command (lookup-key keymap (vector (car event))))) + (deactivate-mark) + ;; This is necessary for following links. (goto-char (posn-point (cdr point))) - (deactivate-mark))))))) + (when command + (call-interactively command nil + (vector event))))))))) (defun touch-screen-handle-touch (event) "Handle a single touch EVENT, and perform associated actions. @@ -317,6 +338,146 @@ touchscreen-end event." (define-key global-map [touchscreen-update] #'touch-screen-handle-touch) (define-key global-map [touchscreen-end] #'touch-screen-handle-touch) + +;; Exports. These functions are intended for use externally. + +(defun touch-screen-track-tap (event &optional update data) + "Track a single tap starting from EVENT. +EVENT should be a `touchscreen-begin' event. + +Read touch screen events until a `touchscreen-end' event is +received with the same ID as in EVENT. If UPDATE is non-nil and +a `touchscreen-update' event is received in the mean time and +contains a touch point with the same ID as in EVENT, call UPDATE +with that event and DATA. + +Return nil immediately if any other kind of event is received; +otherwise, return t once the `touchscreen-end' event arrives." + (catch 'finish + (while t + (let ((new-event (read-event))) + (cond + ((eq (car-safe new-event) 'touchscreen-update) + (when (and update (assq (caadr event) (cadr new-event))) + (funcall update new-event data))) + ((eq (car-safe new-event) 'touchscreen-end) + (throw 'finish + ;; Now determine whether or not the `touchscreen-end' + ;; event has the same ID as EVENT. If it doesn't, + ;; then this is another touch, so return nil. + (eq (caadr event) (caadr new-event)))) + (t (throw 'finish nil))))))) + +(defun touch-screen-track-drag (event update &optional data) + "Track a single drag starting from EVENT. +EVENT should be a `touchscreen-end' event. + +Read touch screen events until a `touchscreen-end' event is +received with the same ID as in EVENT. For each +`touchscreen-update' event received in the mean time containing a +touch point with the same ID as in EVENT, call UPDATE with the +touch point in event and DATA. + +Return nil immediately if any other kind of event is received; +otherwise, return t once the `touchscreen-end' event arrives." + (catch 'finish + (while t + (let ((new-event (read-event))) + (cond + ((eq (car-safe new-event) 'touchscreen-update) + (let ((tool (assq (caadr event) (nth 1 new-event)))) + (when (and update tool) + (funcall update new-event data)))) + ((eq (car-safe new-event) 'touchscreen-end) + (throw 'finish + ;; Now determine whether or not the `touchscreen-end' + ;; event has the same ID as EVENT. If it doesn't, + ;; then this is another touch, so return nil. + (eq (caadr event) (caadr new-event)))) + (t (throw 'finish nil))))))) + + + +;; Modeline dragging. + +(defun touch-screen-drag-mode-line-1 (event) + "Internal helper for `touch-screen-drag-mode-line'. +This is called when that function determines it need not execute +any keymaps on the mode line at that particular spot." + ;; Find the window that should be dragged and the starting position. + (let* ((window (posn-window (cdadr event))) + (relative-xy (touch-screen-relative-xy + (cdadr event) window)) + (last-position (cdr relative-xy))) + (when (window-resizable window 0) + (touch-screen-track-drag + event (lambda (new-event &optional _data) + ;; Find the position of the touchpoint in NEW-EVENT. + (let* ((touchpoint (assq (caadr event) (cadr new-event))) + (new-relative-xy + (touch-screen-relative-xy (cdr touchpoint) + window)) + (position (cdr new-relative-xy)) + growth) + ;; Now set the new height of the window. + ;; If new-relative-y is above relative-xy, then + ;; make the window that much shorter. Otherwise, + ;; make it bigger. + (unless (or (zerop (setq growth (- position last-position))) + (and (> growth 0) + (< position (+ (window-pixel-top window) + (window-pixel-height window)))) + (and (< growth 0) + (> position (+ (window-pixel-top window) + (window-pixel-height window))))) + (adjust-window-trailing-edge window growth nil t)) + (setq last-position position))))))) + +(defun touch-screen-drag-mode-line (event) + "Begin dragging the mode line in response to a touch EVENT. +If EVENT lies on top of text with a mouse command bound, run +that command instead. + +Change the height of the window based on where the touch point +in EVENT moves." + (interactive "e") + ;; If there is an object at EVENT, then look either a keymap bound + ;; to [down-mouse-1] or a command bound to [mouse-1]. Then, if a + ;; keymap was found, pop it up as a menu. Otherwise, wait for a tap + ;; to complete and run the command found. + (let* ((object (posn-object (cdadr event))) + (object-keymap (and (consp object) + (stringp (car object)) + (or (get-text-property (cdr object) + 'keymap + (car object)) + (get-text-property (cdr object) + 'local-map + (car object))))) + (keymap (lookup-key object-keymap [mode-line down-mouse-1])) + (command (or (lookup-key object-keymap [mode-line mouse-1]) + keymap))) + (if (or (keymapp keymap) command) + (if (keymapp keymap) + (when (touch-screen-track-tap event) + (when-let* ((command (x-popup-menu event keymap)) + (tem (lookup-key keymap + (if (consp command) + (apply #'vector command) + (vector command)) + t))) + (call-interactively tem))) + (when (and (commandp command) + (touch-screen-track-tap event)) + (call-interactively command nil + (vector (list 'mouse-1 (cdadr event)))))) + (touch-screen-drag-mode-line-1 event)))) + +(global-set-key [mode-line touchscreen-begin] + #'touch-screen-drag-mode-line) +(global-set-key [bottom-divider touchscreen-begin] + #'touch-screen-drag-mode-line) + (provide 'touch-screen) ;;; touch-screen ends here diff --git a/lisp/wid-edit.el b/lisp/wid-edit.el index 60bd2baa6fb..4c52d827980 100644 --- a/lisp/wid-edit.el +++ b/lisp/wid-edit.el @@ -65,8 +65,11 @@ ;;; Compatibility. (defun widget-event-point (event) - "Character position of the end of event if that exists, or nil." - (posn-point (event-end event))) + "Character position of the end of event if that exists, or nil. +EVENT can either be a mouse event or a touch screen event." + (if (eq (car-safe event) 'touchscreen-begin) + (posn-point (cdadr event)) + (posn-point (event-end event)))) (defun widget-button-release-event-p (event) "Non-nil if EVENT is a mouse-button-release event object." @@ -1017,6 +1020,7 @@ button end points." (define-key map [backtab] 'widget-backward) (define-key map [down-mouse-2] 'widget-button-click) (define-key map [down-mouse-1] 'widget-button-click) + (define-key map [touchscreen-begin] 'widget-button-click) ;; The following definition needs to avoid using escape sequences that ;; might get converted to ^M when building loaddefs.el (define-key map [(control ?m)] 'widget-button-press) @@ -1072,8 +1076,18 @@ Note that such modes will need to require wid-edit.") "If non-nil, `widget-button-click' moves point to a button after invoking it. If nil, point returns to its original position after invoking a button.") +(defun widget-event-start (event) + "Return the start of EVENT. +If EVENT is not a touchscreen event, simply return its +`event-start'. Otherwise, it is a touchscreen event, so return +the posn of its touchpoint." + (if (eq (car event) 'touchscreen-begin) + (cdadr event) + (event-start event))) + (defun widget-button--check-and-call-button (event button) "Call BUTTON if BUTTON is a widget and EVENT is correct for it. +EVENT can either be a mouse event or a touchscreen-begin event. If nothing was called, return non-nil." (let* ((oevent event) (mouse-1 (memq (event-basic-type event) '(mouse-1 down-mouse-1))) @@ -1084,49 +1098,58 @@ If nothing was called, return non-nil." ;; in a save-excursion so that the click on the button ;; doesn't change point. (save-selected-window - (select-window (posn-window (event-start event))) + (select-window (posn-window (widget-event-start event))) (save-excursion - (goto-char (posn-point (event-start event))) + (goto-char (posn-point (widget-event-start event))) (let* ((overlay (widget-get button :button-overlay)) (pressed-face (or (widget-get button :pressed-face) widget-button-pressed-face)) (face (overlay-get overlay 'face)) (mouse-face (overlay-get overlay 'mouse-face))) (unwind-protect - ;; Read events, including mouse-movement - ;; events, waiting for a release event. If we - ;; began with a mouse-1 event and receive a - ;; movement event, that means the user wants - ;; to perform drag-selection, so cancel the - ;; button press and do the default mouse-1 - ;; action. For mouse-2, just highlight/ - ;; unhighlight the button the mouse was - ;; initially on when we move over it. + ;; Read events, including mouse-movement events, + ;; waiting for a release event. If we began with a + ;; mouse-1 event and receive a movement event, that + ;; means the user wants to perform drag-selection, so + ;; cancel the button press and do the default mouse-1 + ;; action. For mouse-2, just highlight/ unhighlight + ;; the button the mouse was initially on when we move + ;; over it. + ;; + ;; If this function was called in response to a + ;; touchscreen event, then wait for a corresponding + ;; touchscreen-end event instead. (save-excursion (when face ; avoid changing around image (overlay-put overlay 'face pressed-face) (overlay-put overlay 'mouse-face pressed-face)) - (unless (widget-apply button :mouse-down-action event) - (let ((track-mouse t)) - (while (not (widget-button-release-event-p event)) - (setq event (read--potential-mouse-event)) - (when (and mouse-1 (mouse-movement-p event)) - (push event unread-command-events) - (setq event oevent) - (throw 'button-press-cancelled t)) - (unless (or (integerp event) - (memq (car event) - '(switch-frame select-window)) - (eq (car event) 'scroll-bar-movement)) - (setq pos (widget-event-point event)) - (if (and pos - (eq (get-char-property pos 'button) - button)) - (when face - (overlay-put overlay 'face pressed-face) - (overlay-put overlay 'mouse-face pressed-face)) - (overlay-put overlay 'face face) - (overlay-put overlay 'mouse-face mouse-face)))))) + (if (eq (car event) 'touchscreen-begin) + ;; This a touchscreen event and must be handled + ;; specially through `touch-screen-track-tap'. + (progn + (unless (touch-screen-track-tap event) + (throw 'button-press-cancelled t))) + (unless (widget-apply button :mouse-down-action event) + (let ((track-mouse t)) + (while (not (widget-button-release-event-p event)) + (setq event (read--potential-mouse-event)) + (when (and mouse-1 (mouse-movement-p event)) + (push event unread-command-events) + (setq event oevent) + (throw 'button-press-cancelled t)) + (unless (or (integerp event) + (memq (car event) + '(switch-frame select-window)) + (eq (car event) 'scroll-bar-movement)) + (setq pos (widget-event-point event)) + (if (and pos + (eq (get-char-property pos 'button) + button)) + (when face + (overlay-put overlay 'face pressed-face) + (overlay-put overlay 'mouse-face pressed-face)) + (overlay-put overlay 'face face) + (overlay-put overlay 'mouse-face mouse-face))))))) ;; When mouse is released over the button, run ;; its action function. @@ -1148,32 +1171,35 @@ If nothing was called, return non-nil." (if (widget-event-point event) (let* ((mouse-1 (memq (event-basic-type event) '(mouse-1 down-mouse-1))) (pos (widget-event-point event)) - (start (event-start event)) + (start (widget-event-start event)) (button (get-char-property pos 'button (and (windowp (posn-window start)) (window-buffer (posn-window start)))))) (when (or (null button) (widget-button--check-and-call-button event button)) - (let ((up t) + (let ((up (not (eq (car event) 'touchscreen-begin))) command) ;; Mouse click not on a widget button. Find the global ;; command to run, and check whether it is bound to an ;; up event. - (if mouse-1 - (cond ((setq command ;down event - (lookup-key widget-global-map [down-mouse-1])) - (setq up nil)) - ((setq command ;up event - (lookup-key widget-global-map [mouse-1])))) - (cond ((setq command ;down event - (lookup-key widget-global-map [down-mouse-2])) - (setq up nil)) - ((setq command ;up event - (lookup-key widget-global-map [mouse-2]))))) + (cond + ((eq (car event) 'touchscreen-begin) + (setq command (lookup-key widget-global-map + [touchscreen-begin]))) + (mouse-1 (cond ((setq command ;down event + (lookup-key widget-global-map [down-mouse-1])) + (setq up nil)) + ((setq command ;up event + (lookup-key widget-global-map [mouse-1]))))) + (t (cond ((setq command ;down event + (lookup-key widget-global-map [down-mouse-2])) + (setq up nil)) + ((setq command ;up event + (lookup-key widget-global-map [mouse-2])))))) (when up ;; Don't execute up events twice. - (while (not (widget-button-release-event-p event)) + (while (not (and (widget-button-release-event-p event))) (setq event (read--potential-mouse-event)))) (when command (call-interactively command))))) diff --git a/m4/getdelim.m4 b/m4/getdelim.m4 new file mode 100644 index 00000000000..9aaed202abe --- /dev/null +++ b/m4/getdelim.m4 @@ -0,0 +1,111 @@ +# getdelim.m4 serial 16 + +dnl Copyright (C) 2005-2007, 2009-2023 Free Software Foundation, Inc. +dnl +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +AC_PREREQ([2.59]) + +AC_DEFUN([gl_FUNC_GETDELIM], +[ + AC_REQUIRE([gl_STDIO_H_DEFAULTS]) + AC_REQUIRE([AC_CANONICAL_HOST]) + + dnl Persuade glibc to declare getdelim(). + AC_REQUIRE([AC_USE_SYSTEM_EXTENSIONS]) + + AC_CHECK_DECLS_ONCE([getdelim]) + + AC_CHECK_FUNCS_ONCE([getdelim]) + if test $ac_cv_func_getdelim = yes; then + HAVE_GETDELIM=1 + dnl Found it in some library. Verify that it works. + AC_CACHE_CHECK([for working getdelim function], + [gl_cv_func_working_getdelim], + [case "$host_os" in + darwin*) + dnl On macOS 10.13, valgrind detected an out-of-bounds read during + dnl the GNU sed test suite: + dnl Invalid read of size 16 + dnl at 0x100EE6A05: _platform_memchr$VARIANT$Base (in /usr/lib/system/libsystem_platform.dylib) + dnl by 0x100B7B0BD: getdelim (in /usr/lib/system/libsystem_c.dylib) + dnl by 0x10000B0BE: ck_getdelim (utils.c:254) + gl_cv_func_working_getdelim=no ;; + *) + echo fooNbarN | tr -d '\012' | tr N '\012' > conftest.data + AC_RUN_IFELSE([AC_LANG_SOURCE([[ +# include +# include +# include + int main () + { + FILE *in = fopen ("./conftest.data", "r"); + if (!in) + return 1; + { + /* Test result for a NULL buffer and a zero size. + Based on a test program from Karl Heuer. */ + char *line = NULL; + size_t siz = 0; + int len = getdelim (&line, &siz, '\n', in); + if (!(len == 4 && line && strcmp (line, "foo\n") == 0)) + { free (line); fclose (in); return 2; } + free (line); + } + { + /* Test result for a NULL buffer and a non-zero size. + This crashes on FreeBSD 8.0. */ + char *line = NULL; + size_t siz = (size_t)(~0) / 4; + if (getdelim (&line, &siz, '\n', in) == -1) + { fclose (in); return 3; } + free (line); + } + fclose (in); + return 0; + } + ]])], + [gl_cv_func_working_getdelim=yes], + [gl_cv_func_working_getdelim=no], + [dnl We're cross compiling. + dnl Guess it works on glibc2 systems and musl systems. + AC_EGREP_CPP([Lucky GNU user], + [ +#include +#ifdef __GNU_LIBRARY__ + #if (__GLIBC__ >= 2) && !defined __UCLIBC__ + Lucky GNU user + #endif +#endif + ], + [gl_cv_func_working_getdelim="guessing yes"], + [case "$host_os" in + *-musl*) gl_cv_func_working_getdelim="guessing yes" ;; + *) gl_cv_func_working_getdelim="$gl_cross_guess_normal" ;; + esac + ]) + ]) + ;; + esac + ]) + case "$gl_cv_func_working_getdelim" in + *yes) ;; + *) REPLACE_GETDELIM=1 ;; + esac + else + HAVE_GETDELIM=0 + fi + + if test $ac_cv_have_decl_getdelim = no; then + HAVE_DECL_GETDELIM=0 + fi +]) + +# Prerequisites of lib/getdelim.c. +AC_DEFUN([gl_PREREQ_GETDELIM], +[ + AC_CHECK_FUNCS([flockfile funlockfile]) + AC_CHECK_DECLS([getc_unlocked]) +]) diff --git a/m4/getline.m4 b/m4/getline.m4 new file mode 100644 index 00000000000..03569f06b2c --- /dev/null +++ b/m4/getline.m4 @@ -0,0 +1,109 @@ +# getline.m4 serial 30 + +dnl Copyright (C) 1998-2003, 2005-2007, 2009-2023 Free Software Foundation, +dnl Inc. +dnl +dnl This file is free software; the Free Software Foundation +dnl gives unlimited permission to copy and/or distribute it, +dnl with or without modifications, as long as this notice is preserved. + +AC_PREREQ([2.59]) + +dnl See if there's a working, system-supplied version of the getline function. +dnl We can't just do AC_REPLACE_FUNCS([getline]) because some systems +dnl have a function by that name in -linet that doesn't have anything +dnl to do with the function we need. +AC_DEFUN([gl_FUNC_GETLINE], +[ + AC_REQUIRE([gl_STDIO_H_DEFAULTS]) + AC_REQUIRE([AC_CANONICAL_HOST]) dnl for cross-compiles + + dnl Persuade glibc to declare getline(). + AC_REQUIRE([AC_USE_SYSTEM_EXTENSIONS]) + + AC_CHECK_DECLS_ONCE([getline]) + + gl_getline_needs_run_time_check=no + AC_CHECK_FUNC([getline], + [dnl Found it in some library. Verify that it works. + gl_getline_needs_run_time_check=yes], + [am_cv_func_working_getline=no]) + if test $gl_getline_needs_run_time_check = yes; then + AC_CACHE_CHECK([for working getline function], + [am_cv_func_working_getline], + [echo fooNbarN | tr -d '\012' | tr N '\012' > conftest.data + AC_RUN_IFELSE([AC_LANG_SOURCE([[ +# include +# include +# include + int main () + { + FILE *in = fopen ("./conftest.data", "r"); + if (!in) + return 1; + { + /* Test result for a NULL buffer and a zero size. + Based on a test program from Karl Heuer. */ + char *line = NULL; + size_t siz = 0; + int len = getline (&line, &siz, in); + if (!(len == 4 && line && strcmp (line, "foo\n") == 0)) + { free (line); fclose (in); return 2; } + free (line); + } + { + /* Test result for a NULL buffer and a non-zero size. + This crashes on FreeBSD 8.0. */ + char *line = NULL; + size_t siz = (size_t)(~0) / 4; + if (getline (&line, &siz, in) == -1) + { fclose (in); return 3; } + free (line); + } + fclose (in); + return 0; + } + ]])], + [am_cv_func_working_getline=yes], + [am_cv_func_working_getline=no], + [dnl We're cross compiling. + dnl Guess it works on glibc2 systems and musl systems. + AC_EGREP_CPP([Lucky GNU user], + [ +#include +#ifdef __GNU_LIBRARY__ + #if (__GLIBC__ >= 2) && !defined __UCLIBC__ + Lucky GNU user + #endif +#endif + ], + [am_cv_func_working_getline="guessing yes"], + [case "$host_os" in + *-musl*) am_cv_func_working_getline="guessing yes" ;; + *) am_cv_func_working_getline="$gl_cross_guess_normal" ;; + esac + ]) + ]) + ]) + fi + + if test $ac_cv_have_decl_getline = no; then + HAVE_DECL_GETLINE=0 + fi + + case "$am_cv_func_working_getline" in + *yes) ;; + *) + dnl Set REPLACE_GETLINE always: Even if we have not found the broken + dnl getline function among $LIBS, it may exist in libinet and the + dnl executable may be linked with -linet. + REPLACE_GETLINE=1 + ;; + esac +]) + +# Prerequisites of lib/getline.c. +AC_DEFUN([gl_PREREQ_GETLINE], +[ + : +]) diff --git a/m4/gnulib-comp.m4 b/m4/gnulib-comp.m4 index 10c74fa2392..501fe20f4e5 100644 --- a/m4/gnulib-comp.m4 +++ b/m4/gnulib-comp.m4 @@ -44,6 +44,7 @@ AC_DEFUN([gl_EARLY], # Code from module absolute-header: # Code from module acl-permissions: + # Code from module alignasof: # Code from module alloca-opt: # Code from module allocator: # Code from module assert-h: @@ -103,8 +104,10 @@ AC_DEFUN([gl_EARLY], # Code from module fsync: # Code from module futimens: # Code from module gen-header: + # Code from module getdelim: # Code from module getdtablesize: # Code from module getgroups: + # Code from module getline: # Code from module getloadavg: # Code from module getopt-gnu: # Code from module getopt-posix: @@ -231,6 +234,7 @@ AC_DEFUN([gl_INIT], gl_source_base='lib' gl_source_base_prefix= gl_FUNC_ACL + gl_ALIGNASOF gl_FUNC_ALLOCA gl_CONDITIONAL_HEADER([alloca.h]) AC_PROG_MKDIR_P @@ -342,6 +346,12 @@ AC_DEFUN([gl_INIT], gl_CONDITIONAL([GL_COND_OBJ_FUTIMENS], [test $HAVE_FUTIMENS = 0 || test $REPLACE_FUTIMENS = 1]) gl_SYS_STAT_MODULE_INDICATOR([futimens]) + gl_FUNC_GETLINE + gl_CONDITIONAL([GL_COND_OBJ_GETLINE], [test $REPLACE_GETLINE = 1]) + AM_COND_IF([GL_COND_OBJ_GETLINE], [ + gl_PREREQ_GETLINE + ]) + gl_STDIO_MODULE_INDICATOR([getline]) AC_REQUIRE([AC_CANONICAL_HOST]) gl_GETLOADAVG gl_CONDITIONAL([GL_COND_OBJ_GETLOADAVG], [test $HAVE_GETLOADAVG = 0]) @@ -637,6 +647,7 @@ AC_DEFUN([gl_INIT], gl_gnulib_enabled_dirfd=false gl_gnulib_enabled_925677f0343de64b89a9f0c790b4104c=false gl_gnulib_enabled_euidaccess=false + gl_gnulib_enabled_getdelim=false gl_gnulib_enabled_getdtablesize=false gl_gnulib_enabled_getgroups=false gl_gnulib_enabled_be453cec5eecf5731a274f2de7f2db36=false @@ -708,6 +719,19 @@ AC_DEFUN([gl_INIT], func_gl_gnulib_m4code_6099e9737f757db36c47fa9d9f02e88c fi } + func_gl_gnulib_m4code_getdelim () + { + if ! $gl_gnulib_enabled_getdelim; then + gl_FUNC_GETDELIM + gl_CONDITIONAL([GL_COND_OBJ_GETDELIM], + [test $HAVE_GETDELIM = 0 || test $REPLACE_GETDELIM = 1]) + AM_COND_IF([GL_COND_OBJ_GETDELIM], [ + gl_PREREQ_GETDELIM + ]) + gl_STDIO_MODULE_INDICATOR([getdelim]) + gl_gnulib_enabled_getdelim=true + fi + } func_gl_gnulib_m4code_getdtablesize () { if ! $gl_gnulib_enabled_getdtablesize; then @@ -973,6 +997,9 @@ AC_DEFUN([gl_INIT], if test $HAVE_FUTIMENS = 0 || test $REPLACE_FUTIMENS = 1; then func_gl_gnulib_m4code_utimens fi + if test $REPLACE_GETLINE = 1; then + func_gl_gnulib_m4code_getdelim + fi if case $host_os in mingw*) false;; *) test $HAVE_GETLOADAVG = 0;; esac; then func_gl_gnulib_m4code_open fi @@ -1012,6 +1039,7 @@ AC_DEFUN([gl_INIT], AM_CONDITIONAL([gl_GNULIB_ENABLED_dirfd], [$gl_gnulib_enabled_dirfd]) AM_CONDITIONAL([gl_GNULIB_ENABLED_925677f0343de64b89a9f0c790b4104c], [$gl_gnulib_enabled_925677f0343de64b89a9f0c790b4104c]) AM_CONDITIONAL([gl_GNULIB_ENABLED_euidaccess], [$gl_gnulib_enabled_euidaccess]) + AM_CONDITIONAL([gl_GNULIB_ENABLED_getdelim], [$gl_gnulib_enabled_getdelim]) AM_CONDITIONAL([gl_GNULIB_ENABLED_getdtablesize], [$gl_gnulib_enabled_getdtablesize]) AM_CONDITIONAL([gl_GNULIB_ENABLED_getgroups], [$gl_gnulib_enabled_getgroups]) AM_CONDITIONAL([gl_GNULIB_ENABLED_be453cec5eecf5731a274f2de7f2db36], [$gl_gnulib_enabled_be453cec5eecf5731a274f2de7f2db36]) @@ -1278,8 +1306,10 @@ AC_DEFUN([gl_FILE_LIST], [ lib/ftoastr.h lib/futimens.c lib/get-permissions.c + lib/getdelim.c lib/getdtablesize.c lib/getgroups.c + lib/getline.c lib/getloadavg.c lib/getopt-cdefs.in.h lib/getopt-core.h @@ -1456,8 +1486,10 @@ AC_DEFUN([gl_FILE_LIST], [ m4/fsusage.m4 m4/fsync.m4 m4/futimens.m4 + m4/getdelim.m4 m4/getdtablesize.m4 m4/getgroups.m4 + m4/getline.m4 m4/getloadavg.m4 m4/getopt.m4 m4/getrandom.m4 diff --git a/m4/stdalign.m4 b/m4/stdalign.m4 index b1438eeaced..0bb9281f5ee 100644 --- a/m4/stdalign.m4 +++ b/m4/stdalign.m4 @@ -5,9 +5,11 @@ dnl This file is free software; the Free Software Foundation dnl gives unlimited permission to copy and/or distribute it, dnl with or without modifications, as long as this notice is preserved. +dnl Written by Paul Eggert and Bruno Haible. + # Prepare for substituting if it is not supported. -AC_DEFUN([gl_STDALIGN_H], +AC_DEFUN([gl_ALIGNASOF], [ AC_CACHE_CHECK([for alignas and alignof], [gl_cv_header_working_stdalign_h], @@ -58,16 +60,11 @@ AC_DEFUN([gl_STDALIGN_H], test "$gl_cv_header_working_stdalign_h" != no && break done]) - GL_GENERATE_STDALIGN_H=false AS_CASE([$gl_cv_header_working_stdalign_h], - [no], - [GL_GENERATE_STDALIGN_H=true], [yes*keyword*], [AC_DEFINE([HAVE_C_ALIGNASOF], [1], [Define to 1 if the alignas and alignof keywords work.])]) - AC_CHECK_HEADERS_ONCE([stdalign.h]) - dnl The "zz" puts this toward config.h's end, to avoid potential dnl collisions with other definitions. AH_VERBATIM([zzalignas], @@ -75,11 +72,33 @@ AC_DEFUN([gl_STDALIGN_H], # if HAVE_STDALIGN_H # include # else - /* Substitute. Keep consistent with gnulib/lib/stdalign.in.h. */ -# ifndef _GL_STDALIGN_H -# define _GL_STDALIGN_H -# undef _Alignas -# undef _Alignof +/* ISO C23 alignas and alignof for platforms that lack it. + + References: + ISO C23 (latest free draft + ) + sections 6.5.3.4, 6.7.5, 7.15. + C++11 (latest free draft + ) + section 18.10. */ + +/* alignof (TYPE), also known as _Alignof (TYPE), yields the alignment + requirement of a structure member (i.e., slot or field) that is of + type TYPE, as an integer constant expression. + + This differs from GCC's and clang's __alignof__ operator, which can + yield a better-performing alignment for an object of that type. For + example, on x86 with GCC and on Linux/x86 with clang, + __alignof__ (double) and __alignof__ (long long) are 8, whereas + alignof (double) and alignof (long long) are 4 unless the option + '-malign-double' is used. + + The result cannot be used as a value for an 'enum' constant, if you + want to be portable to HP-UX 10.20 cc and AIX 3.2.5 xlc. */ + +/* GCC releases before GCC 4.9 had a bug in _Alignof. See GCC bug 52023 + . + clang versions < 8.0.0 have the same bug. */ # if (!defined __STDC_VERSION__ || __STDC_VERSION__ < 201112 \ || (defined __GNUC__ && __GNUC__ < 4 + (__GNUC_MINOR__ < 9) \ && !defined __clang__) \ @@ -100,35 +119,65 @@ AC_DEFUN([gl_STDALIGN_H], # if ! (defined __cplusplus && (201103 <= __cplusplus || defined _MSC_VER)) # define alignof _Alignof # endif -# define __alignof_is_defined 1 -# if !defined __STDC_VERSION__ || __STDC_VERSION__ < 201112 -# if defined __cplusplus && (201103 <= __cplusplus || defined _MSC_VER) -# define _Alignas(a) alignas (a) -# elif (!defined __attribute__ \ - && ((defined __APPLE__ && defined __MACH__ \ - ? 4 < __GNUC__ + (1 <= __GNUC_MINOR__) \ - : __GNUC__ && !defined __ibmxl__) \ - || (4 <= __clang_major__) \ - || (__ia64 && (61200 <= __HP_cc || 61200 <= __HP_aCC)) \ - || __ICC || 0x590 <= __SUNPRO_C || 0x0600 <= __xlC__)) -# define _Alignas(a) __attribute__ ((__aligned__ (a))) -# elif 1300 <= _MSC_VER -# define _Alignas(a) __declspec (align (a)) -# endif -# endif -# if ((defined _Alignas \ - && !(defined __cplusplus && (201103 <= __cplusplus || defined _MSC_VER))) \ - || (defined __STDC_VERSION__ && 201112 <= __STDC_VERSION__)) -# define alignas _Alignas -# endif -# if (defined alignas \ - || (defined __cplusplus && (201103 <= __cplusplus || defined _MSC_VER))) -# define __alignas_is_defined 1 -# endif -# if _GL_STDALIGN_NEEDS_STDDEF -# include + +/* alignas (A), also known as _Alignas (A), aligns a variable or type + to the alignment A, where A is an integer constant expression. For + example: + + int alignas (8) foo; + struct s { int a; int alignas (8) bar; }; + + aligns the address of FOO and the offset of BAR to be multiples of 8. + + A should be a power of two that is at least the type's alignment + and at most the implementation's alignment limit. This limit is + 2**28 on typical GNUish hosts, and 2**13 on MSVC. To be portable + to MSVC through at least version 10.0, A should be an integer + constant, as MSVC does not support expressions such as 1 << 3. + To be portable to Sun C 5.11, do not align auto variables to + anything stricter than their default alignment. + + The following C23 requirements are not supported here: + + - If A is zero, alignas has no effect. + - alignas can be used multiple times; the strictest one wins. + - alignas (TYPE) is equivalent to alignas (alignof (TYPE)). + + */ +# if !defined __STDC_VERSION__ || __STDC_VERSION__ < 201112 +# if defined __cplusplus && (201103 <= __cplusplus || defined _MSC_VER) +# define _Alignas(a) alignas (a) +# elif (!defined __attribute__ \ + && ((defined __APPLE__ && defined __MACH__ \ + ? 4 < __GNUC__ + (1 <= __GNUC_MINOR__) \ + : __GNUC__ && !defined __ibmxl__) \ + || (4 <= __clang_major__) \ + || (__ia64 && (61200 <= __HP_cc || 61200 <= __HP_aCC)) \ + || __ICC || 0x590 <= __SUNPRO_C || 0x0600 <= __xlC__)) +# define _Alignas(a) __attribute__ ((__aligned__ (a))) +# elif 1300 <= _MSC_VER +# define _Alignas(a) __declspec (align (a)) # endif -# endif /* _GL_STDALIGN_H */ +# endif +# if ((defined _Alignas \ + && !(defined __cplusplus \ + && (201103 <= __cplusplus || defined _MSC_VER))) \ + || (defined __STDC_VERSION__ && 201112 <= __STDC_VERSION__)) +# define alignas _Alignas +# endif +# if _GL_STDALIGN_NEEDS_STDDEF +# include +# endif # endif #endif]) ]) + +AC_DEFUN([gl_STDALIGN_H], +[ + AC_REQUIRE([gl_ALIGNASOF]) + GL_GENERATE_STDALIGN_H=false + AS_IF([test "$gl_cv_header_working_stdalign_h" = no], + [GL_GENERATE_STDALIGN_H=true]) + + AC_CHECK_HEADERS_ONCE([stdalign.h]) +]) diff --git a/m4/unistd_h.m4 b/m4/unistd_h.m4 index f4384027e37..dd799ae27db 100644 --- a/m4/unistd_h.m4 +++ b/m4/unistd_h.m4 @@ -50,6 +50,7 @@ AC_DEFUN_ONCE([gl_UNISTD_H], group_member isatty lchown link linkat lseek pipe pipe2 pread pwrite readlink readlinkat rmdir sethostname sleep symlink symlinkat truncate ttyname_r unlink unlinkat usleep]) + gl_CHECK_FUNCS_ANDROID([ftruncate], [[#include ]]) AC_REQUIRE([AC_C_RESTRICT]) diff --git a/m4/utimens.m4 b/m4/utimens.m4 index c5d9b69e6f5..2b87f0149b5 100644 --- a/m4/utimens.m4 +++ b/m4/utimens.m4 @@ -11,7 +11,8 @@ AC_DEFUN([gl_UTIMENS], AC_REQUIRE([gl_FUNC_UTIMES]) AC_REQUIRE([gl_CHECK_TYPE_STRUCT_TIMESPEC]) AC_REQUIRE([AC_CANONICAL_HOST]) dnl for cross-compiles - AC_CHECK_FUNCS_ONCE([futimens utimensat lutimes]) + AC_CHECK_FUNCS_ONCE([futimens lutimes]) + gl_CHECK_FUNCS_ANDROID([utimensat], [[#include ]]) gl_CHECK_FUNCS_ANDROID([futimes], [[#include ]]) gl_CHECK_FUNCS_ANDROID([futimesat], [[#include ]]) diff --git a/m4/utimensat.m4 b/m4/utimensat.m4 index dd210fc989a..f92fc3af7d1 100644 --- a/m4/utimensat.m4 +++ b/m4/utimensat.m4 @@ -13,7 +13,9 @@ AC_DEFUN([gl_FUNC_UTIMENSAT], AC_REQUIRE([gl_SYS_STAT_H_DEFAULTS]) AC_REQUIRE([gl_USE_SYSTEM_EXTENSIONS]) AC_REQUIRE([AC_CANONICAL_HOST]) dnl for cross-compiles - AC_CHECK_FUNCS_ONCE([utimensat]) + # This is necessary for cross-compiles, because otherwise utimensat + # will appear to work. + gl_CHECK_FUNCS_ANDROID([utimensat], [[#include ]]) if test $ac_cv_func_utimensat = no; then HAVE_UTIMENSAT=0 else diff --git a/m4/xattr.m4 b/m4/xattr.m4 index 6141515652a..0e179cc0d1d 100644 --- a/m4/xattr.m4 +++ b/m4/xattr.m4 @@ -1,5 +1,5 @@ # xattr.m4 - check for Extended Attributes (Linux) -# serial 5 +# serial 6 # Copyright (C) 2003-2023 Free Software Foundation, Inc. # This file is free software; the Free Software Foundation @@ -17,23 +17,33 @@ AC_DEFUN([gl_FUNC_XATTR], AC_SUBST([LIB_XATTR]) if test "$use_xattr" = yes; then - AC_CHECK_HEADERS([attr/error_context.h attr/libattr.h]) - use_xattr=no - if test "$ac_cv_header_attr_libattr_h" = yes \ - && test "$ac_cv_header_attr_error_context_h" = yes; then - xattr_saved_LIBS=$LIBS - AC_SEARCH_LIBS([attr_copy_file], [attr], - [test "$ac_cv_search_attr_copy_file" = "none required" || - LIB_XATTR="$ac_cv_search_attr_copy_file"]) - AC_CHECK_FUNCS([attr_copy_file]) - LIBS=$xattr_saved_LIBS - if test "$ac_cv_func_attr_copy_file" = yes; then - use_xattr=yes - fi - fi - if test $use_xattr = no; then + AC_CACHE_CHECK([for xattr library with ATTR_ACTION_PERMISSIONS], + [gl_cv_xattr_lib], + [gl_cv_xattr_lib=no + AC_LANG_CONFTEST( + [AC_LANG_PROGRAM( + [[#include + #include + static int + is_attr_permissions (const char *name, struct error_context *ctx) + { + return attr_copy_action (name, ctx) == ATTR_ACTION_PERMISSIONS; + } + ]], + [[return attr_copy_fd ("/", 0, "/", 0, is_attr_permissions, 0); + ]])]) + AC_LINK_IFELSE([], + [gl_cv_xattr_lib='none required'], + [xattr_saved_LIBS=$LIBS + LIBS="-lattr $LIBS" + AC_LINK_IFELSE([], [gl_cv_xattr_lib=-lattr]) + LIBS=$xattr_saved_LIBS])]) + if test "$gl_cv_xattr_lib" = no; then AC_MSG_WARN([libattr development library was not found or not usable.]) AC_MSG_WARN([AC_PACKAGE_NAME will be built without xattr support.]) + use_xattr=no + elif test "$gl_cv_xattr_lib" != 'none required'; then + LIB_XATTR=$gl_cv_xattr_lib fi fi if test "$use_xattr" = yes; then diff --git a/src/alloc.c b/src/alloc.c index 86e019b931b..ed55ae32710 100644 --- a/src/alloc.c +++ b/src/alloc.c @@ -6172,16 +6172,44 @@ mark_pinned_objects (void) mark_object (pobj->object); } +#if defined HAVE_ANDROID && !defined (__clang__) + +/* The Android gcc is broken and needs the following version of + make_lisp_symbol. Otherwise a mysterious ICE pops up. */ + +#define make_lisp_symbol android_make_lisp_symbol + +static Lisp_Object +android_make_lisp_symbol (struct Lisp_Symbol *sym) +{ + intptr_t symoffset; + Lisp_Object a; + + symoffset = (intptr_t) sym; + INT_SUBTRACT_WRAPV (symoffset, (intptr_t) &lispsym, + &symoffset); + + a = TAG_PTR (Lisp_Symbol, symoffset); + return a; +} + +#endif + static void mark_pinned_symbols (void) { struct symbol_block *sblk; - int lim = (symbol_block_pinned == symbol_block - ? symbol_block_index : SYMBOL_BLOCK_SIZE); + int lim; + struct Lisp_Symbol *sym, *end; + + if (symbol_block_pinned == symbol_block) + lim = symbol_block_index; + else + lim = SYMBOL_BLOCK_SIZE; for (sblk = symbol_block_pinned; sblk; sblk = sblk->next) { - struct Lisp_Symbol *sym = sblk->symbols, *end = sym + lim; + sym = sblk->symbols, end = sym + lim; for (; sym < end; ++sym) if (sym->u.s.pinned) mark_object (make_lisp_symbol (sym)); diff --git a/src/android.c b/src/android.c index cfb79045c0b..eb9c404f1a3 100644 --- a/src/android.c +++ b/src/android.c @@ -54,6 +54,9 @@ bool android_init_gui; #include #include +#include + +#include #define ANDROID_THROW(env, class, msg) \ ((*(env))->ThrowNew ((env), (*(env))->FindClass ((env), class), msg)) @@ -114,6 +117,12 @@ struct android_emacs_drawable jmethodID damage_rect; }; +struct android_emacs_window +{ + jclass class; + jmethodID swap_buffers; +}; + /* The asset manager being used. */ static AAssetManager *asset_manager; @@ -181,6 +190,9 @@ static struct android_graphics_point point_class; /* Various methods associated with the EmacsDrawable class. */ static struct android_emacs_drawable drawable_class; +/* Various methods associated with the EmacsWindow class. */ +static struct android_emacs_window window_class; + /* Event handling functions. Events are stored on a (circular) queue @@ -247,10 +259,21 @@ android_run_select_thread (void *data) sigfillset (&signals); +#if __ANDROID_API__ < 16 + /* sigprocmask must be used instead of pthread_sigmask due to a bug + in Android versions earlier than 16. It only affects the calling + thread on Android anyhow. */ + + if (sigprocmask (SIG_BLOCK, &signals, NULL)) + __android_log_print (ANDROID_LOG_FATAL, __func__, + "sigprocmask: %s", + strerror (errno)); +#else if (pthread_sigmask (SIG_BLOCK, &signals, NULL)) __android_log_print (ANDROID_LOG_FATAL, __func__, "pthread_sigmask: %s", strerror (errno)); +#endif sigdelset (&signals, SIGUSR1); sigemptyset (&waitset); @@ -292,6 +315,8 @@ android_run_select_thread (void *data) still be locked, so this must come before. */ sem_post (&android_pselect_sem); } + + return NULL; } static void @@ -538,6 +563,200 @@ android_run_debug_thread (void *data) +/* Asset directory handling functions. ``directory-tree'' is a file in + the root of the assets directory describing its contents. + + See lib-src/asset-directory-tool for more details. */ + +/* The Android directory tree. */ +static const char *directory_tree; + +/* The size of the directory tree. */ +static size_t directory_tree_size; + +/* Read an unaligned (32-bit) long from the address POINTER. */ + +static unsigned int +android_extract_long (char *pointer) +{ + unsigned int number; + + memcpy (&number, pointer, sizeof number); + return number; +} + +/* Scan to the file FILE in the asset directory tree. Return a + pointer to the end of that file (immediately before any children) + in the directory tree, or NULL if that file does not exist. + + If returning non-NULL, also return the offset to the end of the + last subdirectory or file in *LIMIT_RETURN. LIMIT_RETURN may be + NULL. + + FILE must have less than 11 levels of nesting. If it ends with a + trailing slash, then NULL will be returned if it is not actually a + directory. */ + +static const char * +android_scan_directory_tree (char *file, size_t *limit_return) +{ + char *token, *saveptr, *copy, *copy1, *start, *max, *limit; + size_t token_length, ntokens, i; + char *tokens[10]; + + USE_SAFE_ALLOCA; + + /* Skip past the 5 byte header. */ + start = (char *) directory_tree + 5; + + /* Figure out the current limit. */ + limit = (char *) directory_tree + directory_tree_size; + + /* Now, split `file' into tokens, with the delimiter being the file + name separator. Look for the file and seek past it. */ + + ntokens = 0; + saveptr = NULL; + copy = copy1 = xstrdup (file); + memset (tokens, 0, sizeof tokens); + + while ((token = strtok_r (copy, "/", &saveptr))) + { + copy = NULL; + + /* Make sure ntokens is within bounds. */ + if (ntokens == ARRAYELTS (tokens)) + { + xfree (copy1); + goto fail; + } + + tokens[ntokens] = SAFE_ALLOCA (strlen (token) + 1); + memcpy (tokens[ntokens], token, strlen (token) + 1); + ntokens++; + } + + /* Free the copy created for strtok_r. */ + xfree (copy1); + + /* If there are no tokens, just return the start of the directory + tree. */ + if (!ntokens) + { + SAFE_FREE (); + + /* Subtract the initial header bytes. */ + if (limit_return) + *limit_return = directory_tree_size - 5; + + return start; + } + + /* Loop through tokens, indexing the directory tree each time. */ + + for (i = 0; i < ntokens; ++i) + { + token = tokens[i]; + + /* Figure out how many bytes to compare. */ + token_length = strlen (token); + + again: + + /* If this would be past the directory, return NULL. */ + if (start + token_length > limit) + goto fail; + + /* Now compare the file name. */ + if (!memcmp (start, token, token_length)) + { + /* They probably match. Find the NULL byte. It must be + either one byte past start + token_length, with the last + byte a trailing slash (indicating that it is a + directory), or just start + token_length. Return 4 bytes + past the next NULL byte. */ + + max = memchr (start, 0, limit - start); + + if (max != start + token_length + && !(max == start + token_length + 1 + && *(max - 1) == '/')) + goto false_positive; + + /* Return it if it exists and is in range, and this is the + last token. Otherwise, set it as start and the limit as + start + the offset and continue the loop. */ + + if (max && max + 5 <= limit) + { + if (i < ntokens - 1) + { + start = max + 5; + limit = ((char *) directory_tree + + android_extract_long (max + 1)); + + /* Make sure limit is still in range. */ + if (limit > directory_tree + directory_tree_size + || start > directory_tree + directory_tree_size) + goto fail; + + continue; + } + + /* Now see if max is not a directory and file is. If + file is a directory, then return NULL. */ + if (*(max - 1) != '/' && file[strlen (file) - 1] == '/') + max = NULL; + else + { + /* Figure out the limit. */ + if (limit_return) + *limit_return = android_extract_long (max + 1); + + /* Go to the end of this file. */ + max += 5; + } + + SAFE_FREE (); + return max; + } + + /* Return NULL otherwise. */ + __android_log_print (ANDROID_LOG_WARN, __func__, + "could not scan to end of directory tree" + ": %s", file); + goto fail; + } + + false_positive: + + /* No match was found. Set start to the next sibling and try + again. */ + + start = memchr (start, 0, limit - start); + + if (!start || start + 5 > limit) + goto fail; + + start = ((char *) directory_tree + + android_extract_long (start + 1)); + + /* Make sure start is still in bounds. */ + + if (start > limit) + goto fail; + + /* Continue the loop. */ + goto again; + } + + fail: + SAFE_FREE (); + return NULL; +} + + + /* Intercept USER_FULL_NAME and return something that makes sense if pw->pw_gecos is NULL. */ @@ -624,10 +843,6 @@ android_fstatat (int dirfd, const char *restrict pathname, bool android_file_access_p (const char *name, int amode) { - AAsset *asset; - AAssetDir *directory; - int length; - if (!asset_manager) return false; @@ -637,50 +852,11 @@ android_file_access_p (const char *name, int amode) /* /assets always exists. */ return true; - /* Check if the asset exists by opening it. Suboptimal! */ - asset = AAssetManager_open (asset_manager, name, - AASSET_MODE_UNKNOWN); - - if (!asset) - { - /* See if it's a directory as well. To open a directory - with the asset manager, the trailing slash (if specified) - must be removed. */ - directory = AAssetManager_openDir (asset_manager, name); - - if (directory) - { - /* Make sure the directory actually has files in it. */ - - if (!AAssetDir_getNextFileName (directory)) - { - AAssetDir_close (directory); - errno = ENOENT; - return false; - } - - AAssetDir_close (directory); - return true; - } - - errno = ENOENT; - return false; - } - - AAsset_close (asset); - - /* If NAME is a directory name, but it was a regular file, set - errno to ENOTDIR and return false. This is to behave like - faccessat. */ - - length = strlen (name); - if (name[length - 1] == '/') - { - errno = ENOTDIR; - return false; - } - - return true; + /* Check if the file exists by looking in the ``directory tree'' + asset generated during the build process. This is used + instead of the AAsset functions, because the latter are + buggy and treat directories inconsistently. */ + return android_scan_directory_tree ((char *) name, NULL) != NULL; } return false; @@ -908,8 +1084,13 @@ android_get_home_directory (void) /* JNI functions called by Java. */ +#ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wmissing-prototypes" +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-prototypes" +#endif JNIEXPORT void JNICALL NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, @@ -923,6 +1104,7 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, int pipefd[2]; pthread_t thread; const char *java_string; + AAsset *asset; /* This may be called from multiple threads. setEmacsParams should only ever be called once. */ @@ -943,6 +1125,33 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, /* Set the asset manager. */ asset_manager = AAssetManager_fromJava (env, local_asset_manager); + /* Initialize the directory tree. */ + asset = AAssetManager_open (asset_manager, "directory-tree", + AASSET_MODE_BUFFER); + + if (!asset) + { + __android_log_print (ANDROID_LOG_FATAL, __func__, + "Failed to open directory tree"); + emacs_abort (); + } + + directory_tree = AAsset_getBuffer (asset); + + if (!directory_tree) + emacs_abort (); + + /* Now figure out how big the directory tree is, and compare the + first few bytes. */ + directory_tree_size = AAsset_getLength (asset); + if (directory_tree_size < 5 + || memcmp (directory_tree, "EMACS", 5)) + { + __android_log_print (ANDROID_LOG_FATAL, __func__, + "Directory tree has bad magic"); + emacs_abort (); + } + /* Hold a VM reference to the asset manager to prevent the native object from being deleted. */ (*env)->NewGlobalRef (env, local_asset_manager); @@ -1199,6 +1408,36 @@ android_init_emacs_drawable (void) #undef FIND_METHOD } +static void +android_init_emacs_window (void) +{ + jclass old; + + window_class.class + = (*android_java_env)->FindClass (android_java_env, + "org/gnu/emacs/EmacsWindow"); + eassert (window_class.class); + + old = window_class.class; + window_class.class + = (jclass) (*android_java_env)->NewGlobalRef (android_java_env, + (jobject) old); + ANDROID_DELETE_LOCAL_REF (old); + + if (!window_class.class) + emacs_abort (); + +#define FIND_METHOD(c_name, name, signature) \ + window_class.c_name \ + = (*android_java_env)->GetMethodID (android_java_env, \ + window_class.class, \ + name, signature); \ + assert (window_class.c_name); + + FIND_METHOD (swap_buffers, "swapBuffers", "()V"); +#undef FIND_METHOD +} + extern JNIEXPORT void JNICALL NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) { @@ -1232,6 +1471,7 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) android_init_emacs_pixmap (); android_init_graphics_point (); android_init_emacs_drawable (); + android_init_emacs_window (); /* Set HOME to the app data directory. */ setenv ("HOME", android_files_dir, 1); @@ -1545,7 +1785,11 @@ NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, android_write_event (&event); } +#ifdef __clang__ #pragma clang diagnostic pop +#else +#pragma GCC diagnostic pop +#endif @@ -1557,8 +1801,6 @@ NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, This means that every local reference must be explicitly destroyed with DeleteLocalRef. A helper macro is provided to do this. */ -#define MAX_HANDLE 65535 - struct android_handle_entry { /* The type. */ @@ -1883,7 +2125,7 @@ android_init_emacs_gc_class (void) emacs_gc_mark_dirty = (*android_java_env)->GetMethodID (android_java_env, emacs_gc_class, - "markDirty", "()V"); + "markDirty", "(Z)V"); assert (emacs_gc_mark_dirty); old = emacs_gc_class; @@ -2011,6 +2253,9 @@ android_change_gc (struct android_gc *gc, struct android_gc_values *values) { jobject what, gcontext; + jboolean clip_changed; + + clip_changed = false; android_init_emacs_gc_class (); gcontext = android_resolve_handle (gc->gcontext, @@ -2041,16 +2286,22 @@ android_change_gc (struct android_gc *gc, values->function); if (mask & ANDROID_GC_CLIP_X_ORIGIN) - (*android_java_env)->SetIntField (android_java_env, - gcontext, - emacs_gc_clip_x_origin, - values->clip_x_origin); + { + (*android_java_env)->SetIntField (android_java_env, + gcontext, + emacs_gc_clip_x_origin, + values->clip_x_origin); + clip_changed = true; + } if (mask & ANDROID_GC_CLIP_Y_ORIGIN) - (*android_java_env)->SetIntField (android_java_env, - gcontext, - emacs_gc_clip_y_origin, - values->clip_y_origin); + { + (*android_java_env)->SetIntField (android_java_env, + gcontext, + emacs_gc_clip_y_origin, + values->clip_y_origin); + clip_changed = true; + } if (mask & ANDROID_GC_CLIP_MASK) { @@ -2070,6 +2321,7 @@ android_change_gc (struct android_gc *gc, xfree (gc->clip_rects); gc->clip_rects = NULL; gc->num_clip_rects = -1; + clip_changed = true; } if (mask & ANDROID_GC_STIPPLE) @@ -2103,7 +2355,8 @@ android_change_gc (struct android_gc *gc, if (mask) (*android_java_env)->CallVoidMethod (android_java_env, gcontext, - emacs_gc_mark_dirty); + emacs_gc_mark_dirty, + (jboolean) clip_changed); } void @@ -2174,7 +2427,8 @@ android_set_clip_rectangles (struct android_gc *gc, int clip_x_origin, (*android_java_env)->CallVoidMethod (android_java_env, gcontext, - emacs_gc_mark_dirty); + emacs_gc_mark_dirty, + (jboolean) true); /* Cache the clip rectangles on the C side for sfntfont-android.c. */ @@ -2327,18 +2581,15 @@ android_swap_buffers (struct android_swap_info *swap_info, int num_windows) { jobject window; - jmethodID swap_buffers; int i; - swap_buffers = android_lookup_method ("org/gnu/emacs/EmacsWindow", - "swapBuffers", "()V"); - for (i = 0; i < num_windows; ++i) { window = android_resolve_handle (swap_info[i].swap_window, ANDROID_HANDLE_WINDOW); (*android_java_env)->CallVoidMethod (android_java_env, - window, swap_buffers); + window, + window_class.swap_buffers); } } @@ -3555,11 +3806,17 @@ android_sync (void) +#if __ANDROID_API__ >= 17 + #undef faccessat /* Replace the system faccessat with one which understands AT_EACCESS. Android's faccessat simply fails upon using AT_EACCESS, so repalce - it with zero here. This isn't caught during configuration. */ + it with zero here. This isn't caught during configuration. + + This replacement is only done when building for Android 17 or + later, because earlier versions use the gnulib replacement that + lacks these issues. */ int faccessat (int dirfd, const char *pathname, int mode, int flags) @@ -3572,6 +3829,8 @@ faccessat (int dirfd, const char *pathname, int mode, int flags) return real_faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); } +#endif /* __ANDROID_API__ < 16 */ + /* Directory listing emulation. */ @@ -3581,8 +3840,11 @@ struct android_dir /* The real DIR *, if it exists. */ DIR *dir; - /* Otherwise, the AAssetDir. */ - void *asset_dir; + /* Otherwise, the pointer to the directory in directory_tree. */ + char *asset_dir; + + /* And the end of the files in asset_dir. */ + char *asset_limit; }; /* Like opendir. However, return an asset directory if NAME points to @@ -3592,8 +3854,9 @@ struct android_dir * android_opendir (const char *name) { struct android_dir *dir; - AAssetDir *asset_dir; + char *asset_dir; const char *asset_name; + size_t limit; asset_name = android_get_asset_name (name); @@ -3601,8 +3864,9 @@ android_opendir (const char *name) directory. */ if (asset_manager && asset_name) { - asset_dir = AAssetManager_openDir (asset_manager, - asset_name); + asset_dir + = (char *) android_scan_directory_tree ((char *) asset_name, + &limit); if (!asset_dir) { @@ -3613,6 +3877,20 @@ android_opendir (const char *name) dir = xmalloc (sizeof *dir); dir->dir = NULL; dir->asset_dir = asset_dir; + dir->asset_limit = (char *) directory_tree + limit; + + /* Make sure dir->asset_limit is within bounds. It is a limit, + and as such can be exactly one byte past directory_tree. */ + if (dir->asset_limit > directory_tree + directory_tree_size) + { + xfree (dir); + __android_log_print (ANDROID_LOG_VERBOSE, __func__, + "Invalid dir tree, limit %zu, size %zu\n", + limit, directory_tree_size); + dir = NULL; + errno = EACCES; + } + return dir; } @@ -3636,23 +3914,55 @@ struct dirent * android_readdir (struct android_dir *dir) { static struct dirent dirent; - const char *filename; + const char *last; if (dir->asset_dir) { - filename = AAssetDir_getNextFileName (dir->asset_dir); - errno = 0; + /* There are no more files to read. */ + if (dir->asset_dir >= dir->asset_limit) + return NULL; + + /* Otherwise, scan forward looking for the next NULL byte. */ + last = memchr (dir->asset_dir, 0, + dir->asset_limit - dir->asset_dir); + + /* No more NULL bytes remain. */ + if (!last) + return NULL; + + /* Forward last past the NULL byte. */ + last++; - if (!filename) + /* Make sure it is still within the directory tree. */ + if (last >= directory_tree + directory_tree_size) return NULL; + /* Now, fill in the dirent with the name. */ memset (&dirent, 0, sizeof dirent); dirent.d_ino = 0; dirent.d_off = 0; dirent.d_reclen = sizeof dirent; dirent.d_type = DT_UNKNOWN; - strncpy (dirent.d_name, filename, - sizeof dirent.d_name - 1); + + /* Note that dir->asset_dir is actually a NULL terminated + string. */ + memcpy (dirent.d_name, dir->asset_dir, + MIN (sizeof dirent.d_name, + last - dir->asset_dir)); + dirent.d_name[sizeof dirent.d_name - 1] = '\0'; + + /* Strip off the trailing slash, if any. */ + if (dirent.d_name[MIN (sizeof dirent.d_name, + last - dir->asset_dir) + - 2] == '/') + dirent.d_name[MIN (sizeof dirent.d_name, + last - dir->asset_dir) + - 2] = '\0'; + + /* Finally, forward dir->asset_dir to the file past last. */ + dir->asset_dir = ((char *) directory_tree + + android_extract_long ((char *) last)); + return &dirent; } @@ -3666,8 +3976,9 @@ android_closedir (struct android_dir *dir) { if (dir->dir) closedir (dir->dir); - else - AAssetDir_close (dir->asset_dir); + + /* There is no need to close anything else, as the directory tree + lies in statically allocated memory. */ xfree (dir); } @@ -3795,40 +4106,40 @@ android_four_corners_bilinear (unsigned int tl, unsigned int tr, unsigned int bl, unsigned int br, int distx, int disty) { - int distxy, distxiy, distixy, distixiy; - uint32_t f, r; - - distxy = distx * disty; - distxiy = (distx << 8) - distxy; - distixy = (disty << 8) - distxy; - distixiy = (256 * 256 - (disty << 8) - - (distx << 8) + distxy); - - /* Red */ - r = ((tl & 0x000000ff) * distixiy + (tr & 0x000000ff) * distxiy - + (bl & 0x000000ff) * distixy + (br & 0x000000ff) * distxy); - - /* Green */ - f = ((tl & 0x0000ff00) * distixiy + (tr & 0x0000ff00) * distxiy - + (bl & 0x0000ff00) * distixy + (br & 0x0000ff00) * distxy); - r |= f & 0xff000000; - - /* Now do the upper two components. */ - tl >>= 16; - tr >>= 16; - bl >>= 16; - br >>= 16; - r >>= 16; - - /* Blue */ - f = ((tl & 0x000000ff) * distixiy + (tr & 0x000000ff) * distxiy - + (bl & 0x000000ff) * distixy + (br & 0x000000ff) * distxy); - r |= f & 0x00ff0000; - - /* Alpha */ - f = ((tl & 0x0000ff00) * distixiy + (tr & 0x0000ff00) * distxiy - + (bl & 0x0000ff00) * distixy + (br & 0x0000ff00) * distxy); - r |= f & 0xff000000; + int distxy, distxiy, distixy, distixiy; + uint32_t f, r; + + distxy = distx * disty; + distxiy = (distx << 8) - distxy; + distixy = (disty << 8) - distxy; + distixiy = (256 * 256 - (disty << 8) + - (distx << 8) + distxy); + + /* Red */ + r = ((tl & 0x000000ff) * distixiy + (tr & 0x000000ff) * distxiy + + (bl & 0x000000ff) * distixy + (br & 0x000000ff) * distxy); + + /* Green */ + f = ((tl & 0x0000ff00) * distixiy + (tr & 0x0000ff00) * distxiy + + (bl & 0x0000ff00) * distixy + (br & 0x0000ff00) * distxy); + r |= f & 0xff000000; + + /* Now do the upper two components. */ + tl >>= 16; + tr >>= 16; + bl >>= 16; + br >>= 16; + r >>= 16; + + /* Blue */ + f = ((tl & 0x000000ff) * distixiy + (tr & 0x000000ff) * distxiy + + (bl & 0x000000ff) * distixy + (br & 0x000000ff) * distxy); + r |= f & 0x00ff0000; + + /* Alpha */ + f = ((tl & 0x0000ff00) * distixiy + (tr & 0x0000ff00) * distxiy + + (bl & 0x0000ff00) * distixy + (br & 0x0000ff00) * distxy); + r |= f & 0xff000000; return r; } @@ -4010,6 +4321,36 @@ android_project_image_nearest (struct android_image *image, } } + + +/* System call wrappers for stuff missing in bionic. */ + +#ifndef HAVE_FTRUNCATE + +/* ftruncate wrapper for Android, for systems without ftruncate in the + C library. + + Such systems are always 32 bit systems, since Android 21 and later + all support ftruncate. In addition, ARM and MIPS require registers + used to store long long parameters to be aligned to an even + register pair. */ + +int +android_ftruncate (int fd, off_t length) +{ + int rc; + +#if defined __arm__ || defined __mips__ + return syscall (SYS_ftruncate64, fd, 0, + (unsigned int) (length & 0xffffffff), + (unsigned int) (length >> 32)); +#else + return syscall (SYS_ftruncate64, fd, length); +#endif +} + +#endif + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ diff --git a/src/android.h b/src/android.h index 036e6d266fd..240bc90d831 100644 --- a/src/android.h +++ b/src/android.h @@ -102,6 +102,14 @@ extern struct android_dir *android_opendir (const char *); extern struct dirent *android_readdir (struct android_dir *); extern void android_closedir (struct android_dir *); +#ifndef HAVE_FTRUNCATE +extern int android_ftruncate (int, off_t); + +/* Replace calls to ftruncate with android_ftruncate when ftruncate is + not defined. */ +#define ftruncate android_ftruncate +#endif + #endif diff --git a/src/androidterm.c b/src/androidterm.c index f19cee5b11b..3c16b542d91 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -683,7 +683,7 @@ handle_one_android_event (struct android_display_info *dpyinfo, XSETFRAME (inev.ie.frame_or_window, f); } else - /* A new frame must be created. */; + ((void) 0) /* A new frame must be created. */; } case ANDROID_ENTER_NOTIFY: @@ -988,6 +988,9 @@ handle_one_android_event (struct android_display_info *dpyinfo, if (!NILP (Vmouse_highlight)) { + /* Clear the pointer invisible flag to always make + note_mouse_highlight do its thing. */ + any->pointer_invisible = false; note_mouse_highlight (any, x, y); /* Always allow future mouse motion to diff --git a/src/emacs.c b/src/emacs.c index f4973c70610..994a4d1db93 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -423,7 +423,15 @@ using_utf8 (void) the result is known in advance anyway... */ #if defined HAVE_WCHAR_H && !defined WINDOWSNT wchar_t wc; +#ifndef HAVE_ANDROID mbstate_t mbs = { 0 }; +#else + mbstate_t mbs; + + /* Not sure how mbstate works on Android, but this seems to be + required. */ + memset (&mbs, 0, sizeof mbs); +#endif return mbrtowc (&wc, "\xc4\x80", 2, &mbs) == 2 && wc == 0x100; #else return false; diff --git a/src/fileio.c b/src/fileio.c index 6fa524b3bb4..b5a79312d88 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -6325,6 +6325,11 @@ effect except for flushing STREAM's data. */) #ifndef DOS_NT +#if defined STAT_STATFS2_BSIZE || defined STAT_STATFS2_FRSIZE \ + || defined STAT_STATFS2_FSIZE || defined STAT_STATFS3_OSF1 \ + || defined STAT_STATFS4 || defined STAT_STATVFS \ + || defined STAT_STATVFS64 + /* Yield a Lisp number equal to BLOCKSIZE * BLOCKS, with the result negated if NEGATE. */ static Lisp_Object @@ -6339,6 +6344,8 @@ blocks_to_bytes (uintmax_t blocksize, uintmax_t blocks, bool negate) return CALLN (Ftimes, bs, make_uint (blocks)); } +#endif + DEFUN ("file-system-info", Ffile_system_info, Sfile_system_info, 1, 1, 0, doc: /* Return storage information about the file system FILENAME is on. Value is a list of numbers (TOTAL FREE AVAIL), where TOTAL is the total @@ -6360,6 +6367,11 @@ If the underlying system call fails, value is nil. */) error ("Invalid handler in `file-name-handler-alist'"); } + /* Try to detect whether or not fsusage.o is actually built. */ +#if defined STAT_STATFS2_BSIZE || defined STAT_STATFS2_FRSIZE \ + || defined STAT_STATFS2_FSIZE || defined STAT_STATFS3_OSF1 \ + || defined STAT_STATFS4 || defined STAT_STATVFS \ + || defined STAT_STATVFS64 struct fs_usage u; if (get_fs_usage (SSDATA (ENCODE_FILE (filename)), NULL, &u) != 0) return errno == ENOSYS ? Qnil : file_attribute_errno (filename, errno); @@ -6367,6 +6379,9 @@ If the underlying system call fails, value is nil. */) blocks_to_bytes (u.fsu_blocksize, u.fsu_bfree, false), blocks_to_bytes (u.fsu_blocksize, u.fsu_bavail, u.fsu_bavail_top_bit_set)); +#else + return Qnil; +#endif } #endif /* !DOS_NT */ diff --git a/src/filelock.c b/src/filelock.c index 51e1ffca9db..45eac5a19a1 100644 --- a/src/filelock.c +++ b/src/filelock.c @@ -65,6 +65,12 @@ along with GNU Emacs. If not, see . */ #define BOOT_TIME_FILE "/var/run/random-seed" #endif +/* Boot time is not available on Android. */ + +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY +#undef BOOT_TIME +#endif + #if !defined WTMP_FILE && !defined WINDOWSNT && defined BOOT_TIME #define WTMP_FILE "/var/log/wtmp" #endif @@ -120,12 +126,6 @@ along with GNU Emacs. If not, see . */ * Non-forced locks on non-MS-Windows systems that support neither hard nor symbolic links. */ -/* Boot time is not available on Android. */ - -#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY -#undef BOOT_TIME -#endif - /* Return the time of the last system boot. */ diff --git a/src/frame.c b/src/frame.c index 286c9a2cb71..e98256fe9ed 100644 --- a/src/frame.c +++ b/src/frame.c @@ -5666,6 +5666,8 @@ On Nextstep, this just calls `ns-parse-geometry'. */) int x UNINIT, y UNINIT; unsigned int width, height; + width = height = 0; + CHECK_STRING (string); #ifdef HAVE_NS diff --git a/src/keyboard.c b/src/keyboard.c index 834049b496a..11fa1e62c89 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -62,6 +62,10 @@ along with GNU Emacs. If not, see . */ #include "syssignal.h" +#if defined HAVE_STACK_OVERFLOW_HANDLING && !defined WINDOWSNT +#include +#endif + #include #include #include @@ -4955,7 +4959,7 @@ const char *const lispy_function_keys[] = [278] = "copy", [279] = "paste", [28] = "clear", - [4] = "back", + [4] = "XF86Back", [61] = "tab", [66] = "return", [67] = "backspace", diff --git a/src/menu.c b/src/menu.c index e1f899858d3..e02ee880119 100644 --- a/src/menu.c +++ b/src/menu.c @@ -1152,7 +1152,7 @@ x_popup_menu_1 (Lisp_Object position, Lisp_Object menu) else { menuflags |= MENU_FOR_CLICK; - tem = Fcar (XCDR (position)); /* EVENT_START (position) */ + tem = EVENT_START (position); /* EVENT_START (position) */ window = Fcar (tem); /* POSN_WINDOW (tem) */ tem2 = Fcar (Fcdr (tem)); /* POSN_POSN (tem) */ /* The MENU_KBD_NAVIGATION field is set when the menu @@ -1466,9 +1466,10 @@ cached information about equivalent key sequences. If the user gets rid of the menu without making a valid choice, for instance by clicking the mouse away from a valid choice or by typing keyboard input, then this normally results in a quit and -`x-popup-menu' does not return. But if POSITION is a mouse button -event (indicating that the user invoked the menu with the mouse) then -no quit occurs and `x-popup-menu' returns nil. */) +`x-popup-menu' does not return. But if POSITION is a mouse button or +touch screen event (indicating that the user invoked the menu with the +a pointing device) then no quit occurs and `x-popup-menu' returns +nil. */) (Lisp_Object position, Lisp_Object menu) { init_raw_keybuf_count (); diff --git a/src/sfnt.c b/src/sfnt.c index 6d58798c599..7300915a504 100644 --- a/src/sfnt.c +++ b/src/sfnt.c @@ -4624,8 +4624,14 @@ main (int argc, char **argv) } if (meta) - fprintf (stderr, "meta table with count: %"PRIu32"\n", - meta->num_data_maps); + { + fprintf (stderr, "meta table with count: %"PRIu32"\n", + meta->num_data_maps); + + for (i = 0; i < meta->num_data_maps; ++i) + fprintf (stderr, " meta tag: %"PRIx32"\n", + meta->data_maps[i].tag); + } loca_long = NULL; loca_short = NULL; diff --git a/src/sfntfont-android.c b/src/sfntfont-android.c index 1b01a4d9be4..37fd81953f6 100644 --- a/src/sfntfont-android.c +++ b/src/sfntfont-android.c @@ -310,7 +310,7 @@ sfntfont_android_put_glyphs (struct glyph_string *s, int from, /* Allocate enough to hold text_rectangle.height, aligned to 8 bytes. Then fill it with the background. */ - stride = (text_rectangle.width * sizeof *buffer) + 7 & ~7; + stride = ((text_rectangle.width * sizeof *buffer) + 7) & ~7; GET_SCANLINE_BUFFER (buffer, text_rectangle.height, stride); memset (buffer, 0, text_rectangle.height * stride); @@ -546,6 +546,7 @@ init_sfntfont_android (void) { /* Make sure to pick the right Sans Serif font depending on what version of Android the device is running. */ +#if HAVE_DECL_ANDROID_GET_DEVICE_API_LEVEL if (android_get_device_api_level () >= 15) Vsfnt_default_family_alist = list3 (Fcons (build_string ("Monospace"), @@ -557,6 +558,7 @@ init_sfntfont_android (void) Fcons (build_string ("Sans Serif"), build_string ("Roboto"))); else +#endif Vsfnt_default_family_alist = list3 (Fcons (build_string ("Monospace"), build_string ("Droid Sans Mono")), diff --git a/src/sfntfont.c b/src/sfntfont.c index e2d18517fcb..87f93473ff3 100644 --- a/src/sfntfont.c +++ b/src/sfntfont.c @@ -21,6 +21,7 @@ Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. */ #include #include +#include #include "lisp.h" @@ -61,6 +62,12 @@ struct sfnt_font_desc /* Designer (foundry) of the font. */ Lisp_Object designer; + /* Style tokens that could not be parsed. */ + Lisp_Object adstyle; + + /* List of design languages. */ + Lisp_Object languages; + /* Numeric width, weight, slant and spacing. */ int width, weight, slant, spacing; @@ -344,7 +351,9 @@ static struct sfnt_style_desc sfnt_width_descriptions[] = }; /* Figure out DESC->width, DESC->weight, DESC->slant and DESC->spacing - based on the style name passed as STYLE_NAME. */ + based on the style name passed as STYLE_NAME. + + Also append any unknown tokens to DESC->adstyle. */ static void sfnt_parse_style (Lisp_Object style_name, struct sfnt_font_desc *desc) @@ -419,16 +428,85 @@ sfnt_parse_style (Lisp_Object style_name, struct sfnt_font_desc *desc) } } - next: + /* This token is extraneous or was not recognized. Capitalize + the first letter and set it as the adstyle. */ - /* Break early if everything has been found. */ - if (desc->slant != 100 && desc->width != 100 && desc->weight != 80) - break; + if (strlen (single)) + { + if (islower (single[0])) + single[0] = toupper (single[0]); + + if (NILP (desc->adstyle)) + desc->adstyle = build_string (single); + else + desc->adstyle = CALLN (Fconcat, desc->adstyle, + build_string (" "), + build_string (single)); + } + next: continue; } } +/* Parse the list of design languages in META, a font metadata table, + and place the results in DESC->languages. Do nothing if there is + no such metadata. */ + +static void +sfnt_parse_languages (struct sfnt_meta_table *meta, + struct sfnt_font_desc *desc) +{ + char *data, *metadata, *tag; + struct sfnt_meta_data_map map; + char *saveptr; + + /* Look up the ``design languages'' metadata. This is a comma (and + possibly space) separated list of scripts that the font was + designed for. Here is an example of one such tag: + + zh-Hans,Jpan,Kore + + for a font that covers Simplified Chinese, along with Japanese + and Korean text. */ + + saveptr = NULL; + data = sfnt_find_metadata (meta, SFNT_META_DATA_TAG_DLNG, + &map); + + if (!data) + return; + + USE_SAFE_ALLOCA; + + /* Now copy metadata and add a trailing NULL byte. */ + + if (map.data_length >= SIZE_MAX) + memory_full (SIZE_MAX); + + metadata = SAFE_ALLOCA ((size_t) map.data_length + 1); + memcpy (metadata, data, map.data_length); + metadata[map.data_length] = '\0'; + + /* Loop through each script-language tag. Note that there may be + extra leading spaces. */ + while ((tag = strtok_r (metadata, ",", &saveptr))) + { + metadata = NULL; + + if (strstr (tag, "Hans") || strstr (tag, "Hant")) + desc->languages = Fcons (Qzh, desc->languages); + + if (strstr (tag, "Japn")) + desc->languages = Fcons (Qja, desc->languages); + + if (strstr (tag, "Kore")) + desc->languages = Fcons (Qko, desc->languages); + } + + SAFE_FREE (); +} + /* Enumerate the offset subtable SUBTABLES in the file FD, whose file name is FILE. OFFSET should be the offset of the subtable within the font file, and is recorded for future use. Value is 1 upon @@ -481,6 +559,10 @@ sfnt_enum_font_1 (int fd, const char *file, /* Parse the style. */ sfnt_parse_style (style, desc); + /* If the meta table exists, parse the list of design languages. */ + if (meta) + sfnt_parse_languages (meta, desc); + /* Figure out the spacing. Some fancy test like what Fontconfig does is probably in order but not really necessary. */ if (!NILP (Fstring_search (Fdowncase (family), @@ -990,11 +1072,10 @@ sfntfont_list_1 (struct sfnt_font_desc *desc, Lisp_Object spec) desc->family))) return false; - /* Check that no adstyle has been specified. That's a relic from - the Postscript era. */ + /* Check that the adstyle specified matches. */ tem = AREF (spec, FONT_ADSTYLE_INDEX); - if (!NILP (tem)) + if (!NILP (tem) && NILP (Fequal (tem, desc->adstyle))) return false; /* Check the style. */ @@ -1069,6 +1150,11 @@ sfntfont_list_1 (struct sfnt_font_desc *desc, Lisp_Object spec) } } + /* Now check that the language is supported. */ + tem = assq_no_quit (QClang, extra); + if (!NILP (tem) && NILP (Fmemq (tem, desc->languages))) + goto fail; + /* Set desc->subtable if cmap was specified. */ if (cmap) desc->subtable = subtable; @@ -2076,9 +2162,9 @@ sfntfont_text_extents (struct font *font, const unsigned int *code, if (pcm.descent > metrics->descent) metrics->descent = pcm.descent; - } - total_width += pcm.width; + total_width += pcm.width; + } } metrics->width = total_width; @@ -2239,6 +2325,9 @@ syms_of_sfntfont (void) DEFSYM (Qapple_roman, "apple-roman"); DEFSYM (Qjisx0208_1983_0, "jisx0208.1983-0"); DEFSYM (Qksc5601_1987_0, "ksc5601.1987-0"); + DEFSYM (Qzh, "zh"); + DEFSYM (Qja, "ja"); + DEFSYM (Qko, "ko"); /* Char-table purpose. */ DEFSYM (Qfont_lookup_cache, "font-lookup-cache"); @@ -2269,6 +2358,8 @@ mark_sfntfont (void) { mark_object (desc->family); mark_object (desc->style); + mark_object (desc->adstyle); + mark_object (desc->languages); mark_object (desc->char_cache); mark_object (desc->designer); } diff --git a/xcompile/lib/faccessat.c b/xcompile/lib/faccessat.c index 807e2669683..ac8977cfd65 100644 --- a/xcompile/lib/faccessat.c +++ b/xcompile/lib/faccessat.c @@ -43,11 +43,7 @@ orig_faccessat (int fd, char const *name, int mode, int flag) /* Write "unistd.h" here, not , otherwise OSF/1 5.1 DTK cc eliminates this include because of the preliminary #include above. */ -#ifdef __ANROID__ -#include -#else #include "unistd.h" -#endif #ifndef HAVE_ACCESS /* Mingw lacks access, but it also lacks real vs. effective ids, so diff --git a/xcompile/lib/getdelim.c b/xcompile/lib/getdelim.c new file mode 100644 index 00000000000..79ec3dd12a3 --- /dev/null +++ b/xcompile/lib/getdelim.c @@ -0,0 +1,147 @@ +/* getdelim.c --- Implementation of replacement getdelim function. + Copyright (C) 1994, 1996-1998, 2001, 2003, 2005-2023 Free Software + Foundation, Inc. + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + This file 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . */ + +/* Ported from glibc by Simon Josefsson. */ + +/* Don't use __attribute__ __nonnull__ in this compilation unit. Otherwise gcc + optimizes away the lineptr == NULL || n == NULL || fp == NULL tests below. */ +#define _GL_ARG_NONNULL(params) + +#include + +#include + +#include +#include +#include +#include + +#ifndef SSIZE_MAX +# define SSIZE_MAX ((ssize_t) (SIZE_MAX / 2)) +#endif + +#if USE_UNLOCKED_IO +# include "unlocked-io.h" +# define getc_maybe_unlocked(fp) getc(fp) +#elif !HAVE_FLOCKFILE || !HAVE_FUNLOCKFILE || !HAVE_DECL_GETC_UNLOCKED +# undef flockfile +# undef funlockfile +# define flockfile(x) ((void) 0) +# define funlockfile(x) ((void) 0) +# define getc_maybe_unlocked(fp) getc(fp) +#else +# define getc_maybe_unlocked(fp) getc_unlocked(fp) +#endif + +static void +alloc_failed (void) +{ +#if defined _WIN32 && ! defined __CYGWIN__ + /* Avoid errno problem without using the realloc module; see: + https://lists.gnu.org/r/bug-gnulib/2016-08/msg00025.html */ + errno = ENOMEM; +#endif +} + +/* Read up to (and including) a DELIMITER from FP into *LINEPTR (and + NUL-terminate it). *LINEPTR is a pointer returned from malloc (or + NULL), pointing to *N characters of space. It is realloc'ed as + necessary. Returns the number of characters read (not including + the null terminator), or -1 on error or EOF. */ + +ssize_t +getdelim (char **lineptr, size_t *n, int delimiter, FILE *fp) +{ + ssize_t result; + size_t cur_len = 0; + + if (lineptr == NULL || n == NULL || fp == NULL) + { + errno = EINVAL; + return -1; + } + + flockfile (fp); + + if (*lineptr == NULL || *n == 0) + { + char *new_lineptr; + *n = 120; + new_lineptr = (char *) realloc (*lineptr, *n); + if (new_lineptr == NULL) + { + alloc_failed (); + result = -1; + goto unlock_return; + } + *lineptr = new_lineptr; + } + + for (;;) + { + int i; + + i = getc_maybe_unlocked (fp); + if (i == EOF) + { + result = -1; + break; + } + + /* Make enough space for len+1 (for final NUL) bytes. */ + if (cur_len + 1 >= *n) + { + size_t needed_max = + SSIZE_MAX < SIZE_MAX ? (size_t) SSIZE_MAX + 1 : SIZE_MAX; + size_t needed = 2 * *n + 1; /* Be generous. */ + char *new_lineptr; + + if (needed_max < needed) + needed = needed_max; + if (cur_len + 1 >= needed) + { + result = -1; + errno = EOVERFLOW; + goto unlock_return; + } + + new_lineptr = (char *) realloc (*lineptr, needed); + if (new_lineptr == NULL) + { + alloc_failed (); + result = -1; + goto unlock_return; + } + + *lineptr = new_lineptr; + *n = needed; + } + + (*lineptr)[cur_len] = i; + cur_len++; + + if (i == delimiter) + break; + } + (*lineptr)[cur_len] = '\0'; + result = cur_len ? cur_len : result; + + unlock_return: + funlockfile (fp); /* doesn't set errno */ + + return result; +} diff --git a/xcompile/lib/getline.c b/xcompile/lib/getline.c new file mode 100644 index 00000000000..85f16ab8bac --- /dev/null +++ b/xcompile/lib/getline.c @@ -0,0 +1,27 @@ +/* getline.c --- Implementation of replacement getline function. + Copyright (C) 2005-2007, 2009-2023 Free Software Foundation, Inc. + + This file is free software: you can redistribute it and/or modify + it under the terms of the GNU Lesser General Public License as + published by the Free Software Foundation; either version 2.1 of the + License, or (at your option) any later version. + + This file 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 Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public License + along with this program. If not, see . */ + +/* Written by Simon Josefsson. */ + +#include + +#include + +ssize_t +getline (char **lineptr, size_t *n, FILE *stream) +{ + return getdelim (lineptr, n, '\n', stream); +} diff --git a/xcompile/lib/gnulib.mk.in b/xcompile/lib/gnulib.mk.in index 2ebf187e867..66ba4a4adf9 100644 --- a/xcompile/lib/gnulib.mk.in +++ b/xcompile/lib/gnulib.mk.in @@ -109,6 +109,7 @@ # fsusage \ # fsync \ # futimens \ +# getline \ # getloadavg \ # getopt-gnu \ # getrandom \ @@ -172,11 +173,19 @@ MOSTLYCLEANFILES += core *.stackdump # Start of GNU Make output. +AAPT = @AAPT@ ALLOCA = @ALLOCA@ ALLOCA_H = @ALLOCA_H@ ALSA_CFLAGS = @ALSA_CFLAGS@ ALSA_LIBS = @ALSA_LIBS@ AM_DEFAULT_VERBOSITY = @AM_DEFAULT_VERBOSITY@ +ANDROID = @ANDROID@ +ANDROID_ABI = @ANDROID_ABI@ +ANDROID_CFLAGS = @ANDROID_CFLAGS@ +ANDROID_JAR = @ANDROID_JAR@ +ANDROID_LIBS = @ANDROID_LIBS@ +ANDROID_MIN_SDK = @ANDROID_MIN_SDK@ +ANDROID_OBJ = @ANDROID_OBJ@ APPLE_UNIVERSAL_BUILD = @APPLE_UNIVERSAL_BUILD@ AR = @AR@ ARFLAGS = @ARFLAGS@ @@ -216,6 +225,7 @@ CYGWIN_OBJ = @CYGWIN_OBJ@ C_SWITCH_MACHINE = @C_SWITCH_MACHINE@ C_SWITCH_SYSTEM = @C_SWITCH_SYSTEM@ C_SWITCH_X_SITE = @C_SWITCH_X_SITE@ +D8 = @D8@ DBUS_CFLAGS = @DBUS_CFLAGS@ DBUS_LIBS = @DBUS_LIBS@ DBUS_OBJ = @DBUS_OBJ@ @@ -278,8 +288,10 @@ GL_COND_OBJ_FSTATAT_CONDITION = @GL_COND_OBJ_FSTATAT_CONDITION@ GL_COND_OBJ_FSUSAGE_CONDITION = @GL_COND_OBJ_FSUSAGE_CONDITION@ GL_COND_OBJ_FSYNC_CONDITION = @GL_COND_OBJ_FSYNC_CONDITION@ GL_COND_OBJ_FUTIMENS_CONDITION = @GL_COND_OBJ_FUTIMENS_CONDITION@ +GL_COND_OBJ_GETDELIM_CONDITION = @GL_COND_OBJ_GETDELIM_CONDITION@ GL_COND_OBJ_GETDTABLESIZE_CONDITION = @GL_COND_OBJ_GETDTABLESIZE_CONDITION@ GL_COND_OBJ_GETGROUPS_CONDITION = @GL_COND_OBJ_GETGROUPS_CONDITION@ +GL_COND_OBJ_GETLINE_CONDITION = @GL_COND_OBJ_GETLINE_CONDITION@ GL_COND_OBJ_GETLOADAVG_CONDITION = @GL_COND_OBJ_GETLOADAVG_CONDITION@ GL_COND_OBJ_GETOPT_CONDITION = @GL_COND_OBJ_GETOPT_CONDITION@ GL_COND_OBJ_GETRANDOM_CONDITION = @GL_COND_OBJ_GETRANDOM_CONDITION@ @@ -883,6 +895,8 @@ INSTALL_PROGRAM = @INSTALL_PROGRAM@ INSTALL_SCRIPT = @INSTALL_SCRIPT@ INT32_MAX_LT_INTMAX_MAX = @INT32_MAX_LT_INTMAX_MAX@ INT64_MAX_EQ_LONG_MAX = @INT64_MAX_EQ_LONG_MAX@ +JARSIGNER = @JARSIGNER@ +JAVAC = @JAVAC@ JSON_CFLAGS = @JSON_CFLAGS@ JSON_LIBS = @JSON_LIBS@ JSON_OBJ = @JSON_OBJ@ @@ -949,6 +963,7 @@ LIB_PTHREAD = @LIB_PTHREAD@ LIB_PTHREAD_SIGMASK = @LIB_PTHREAD_SIGMASK@ LIB_TIMER_TIME = @LIB_TIMER_TIME@ LIB_WSOCK32 = @LIB_WSOCK32@ +LIB_XATTR = @LIB_XATTR@ LIMITS_H = @LIMITS_H@ LN_S_FILEONLY = @LN_S_FILEONLY@ LTLIBGMP = @LTLIBGMP@ @@ -1041,6 +1056,7 @@ PROFILING_CFLAGS = @PROFILING_CFLAGS@ PTHREAD_H_DEFINES_STRUCT_TIMESPEC = @PTHREAD_H_DEFINES_STRUCT_TIMESPEC@ PTHREAD_SIGMASK_LIB = @PTHREAD_SIGMASK_LIB@ PTRDIFF_T_SUFFIX = @PTRDIFF_T_SUFFIX@ +QCOPY_ACL_LIB = @QCOPY_ACL_LIB@ RALLOC_OBJ = @RALLOC_OBJ@ RANLIB = @RANLIB@ REPLACE_ACCESS = @REPLACE_ACCESS@ @@ -1209,6 +1225,7 @@ REPLACE_WCTOMB = @REPLACE_WCTOMB@ REPLACE_WRITE = @REPLACE_WRITE@ RSVG_CFLAGS = @RSVG_CFLAGS@ RSVG_LIBS = @RSVG_LIBS@ +SDK_BULD_TOOLS = @SDK_BULD_TOOLS@ SEPCHAR = @SEPCHAR@ SETFATTR = @SETFATTR@ SETTINGS_CFLAGS = @SETTINGS_CFLAGS@ @@ -1266,6 +1283,7 @@ XARGS_LIMIT = @XARGS_LIMIT@ XCB_LIBS = @XCB_LIBS@ XCOMPOSITE_CFLAGS = @XCOMPOSITE_CFLAGS@ XCOMPOSITE_LIBS = @XCOMPOSITE_LIBS@ +XCONFIGURE = @XCONFIGURE@ XCRUN = @XCRUN@ XDBE_CFLAGS = @XDBE_CFLAGS@ XDBE_LIBS = @XDBE_LIBS@ @@ -1290,6 +1308,7 @@ XSYNC_CFLAGS = @XSYNC_CFLAGS@ XSYNC_LIBS = @XSYNC_LIBS@ XWIDGETS_OBJ = @XWIDGETS_OBJ@ X_TOOLKIT_TYPE = @X_TOOLKIT_TYPE@ +ZIPALIGN = @ZIPALIGN@ ac_ct_CC = @ac_ct_CC@ ac_ct_CXX = @ac_ct_CXX@ ac_ct_OBJC = @ac_ct_OBJC@ @@ -1335,6 +1354,7 @@ gl_GNULIB_ENABLED_e80bf6f757095d2e5fc94dafb8f8fc8b_CONDITION = @gl_GNULIB_ENABLE gl_GNULIB_ENABLED_ef455225c00f5049c808c2eda3e76866_CONDITION = @gl_GNULIB_ENABLED_ef455225c00f5049c808c2eda3e76866_CONDITION@ gl_GNULIB_ENABLED_euidaccess_CONDITION = @gl_GNULIB_ENABLED_euidaccess_CONDITION@ gl_GNULIB_ENABLED_fd38c7e463b54744b77b98aeafb4fa7c_CONDITION = @gl_GNULIB_ENABLED_fd38c7e463b54744b77b98aeafb4fa7c_CONDITION@ +gl_GNULIB_ENABLED_getdelim_CONDITION = @gl_GNULIB_ENABLED_getdelim_CONDITION@ gl_GNULIB_ENABLED_getdtablesize_CONDITION = @gl_GNULIB_ENABLED_getdtablesize_CONDITION@ gl_GNULIB_ENABLED_getgroups_CONDITION = @gl_GNULIB_ENABLED_getgroups_CONDITION@ gl_GNULIB_ENABLED_lchmod_CONDITION = @gl_GNULIB_ENABLED_lchmod_CONDITION@ @@ -2091,6 +2111,18 @@ gl_V_at = $(AM_V_GEN) endif ## end gnulib module gen-header +## begin gnulib module getdelim +ifeq (,$(OMIT_GNULIB_MODULE_getdelim)) + +ifneq (,$(gl_GNULIB_ENABLED_getdelim_CONDITION)) +ifneq (,$(GL_COND_OBJ_GETDELIM_CONDITION)) +libgnu_a_SOURCES += getdelim.c +endif + +endif +endif +## end gnulib module getdelim + ## begin gnulib module getdtablesize ifeq (,$(OMIT_GNULIB_MODULE_getdtablesize)) @@ -2115,6 +2147,16 @@ endif endif ## end gnulib module getgroups +## begin gnulib module getline +ifeq (,$(OMIT_GNULIB_MODULE_getline)) + +ifneq (,$(GL_COND_OBJ_GETLINE_CONDITION)) +libgnu_a_SOURCES += getline.c +endif + +endif +## end gnulib module getline + ## begin gnulib module getloadavg ifeq (,$(OMIT_GNULIB_MODULE_getloadavg)) diff --git a/xcompile/lib/qcopy-acl.c b/xcompile/lib/qcopy-acl.c index 883bcf7d588..0f4159b7fd9 100644 --- a/xcompile/lib/qcopy-acl.c +++ b/xcompile/lib/qcopy-acl.c @@ -23,6 +23,20 @@ #include "acl-internal.h" +#if USE_XATTR + +# include + +/* Returns 1 if NAME is the name of an extended attribute that is related + to permissions, i.e. ACLs. Returns 0 otherwise. */ + +static int +is_attr_permissions (const char *name, struct error_context *ctx) +{ + return attr_copy_action (name, ctx) == ATTR_ACTION_PERMISSIONS; +} + +#endif /* USE_XATTR */ /* Copy access control lists from one file to another. If SOURCE_DESC is a valid file descriptor, use file descriptor operations, else use @@ -39,13 +53,33 @@ int qcopy_acl (const char *src_name, int source_desc, const char *dst_name, int dest_desc, mode_t mode) { - struct permission_context ctx; int ret; +#ifdef USE_XATTR + /* in case no ACLs present and also to set higher mode bits + we chmod before setting ACLs as doing it after could overwrite them + (especially true for NFSv4, posix ACL has that ugly "mask" hack that + nobody understands) */ + ret = chmod_or_fchmod (dst_name, dest_desc, mode); + /* Rather than fiddling with acls one by one, we just copy the whole ACL xattrs + (Posix or NFSv4). Of course, that won't address ACLs conversion + (i.e. posix <-> nfs4) but we can't do it anyway, so for now, we don't care + Functions attr_copy_* return 0 in case we copied something OR nothing + to copy */ + if (ret == 0) + ret = source_desc <= 0 || dest_desc <= 0 + ? attr_copy_file (src_name, dst_name, is_attr_permissions, NULL) + : attr_copy_fd (src_name, source_desc, dst_name, dest_desc, + is_attr_permissions, NULL); +#else + /* no XATTR, so we proceed the old dusty way */ + struct permission_context ctx; + ret = get_permissions (src_name, source_desc, mode, &ctx); if (ret != 0) return -2; ret = set_permissions (&ctx, dst_name, dest_desc); free_permission_context (&ctx); +#endif return ret; } diff --git a/xcompile/lib/verify.h b/xcompile/lib/verify.h index 17d6e78c816..b63cb264321 100644 --- a/xcompile/lib/verify.h +++ b/xcompile/lib/verify.h @@ -258,7 +258,9 @@ template /* @assert.h omit start@ */ -#if 3 < __GNUC__ + (3 < __GNUC_MINOR__ + (4 <= __GNUC_PATCHLEVEL__)) +#if defined __clang_major__ && __clang_major__ < 5 +# define _GL_HAS_BUILTIN_TRAP 0 +#elif 3 < __GNUC__ + (3 < __GNUC_MINOR__ + (4 <= __GNUC_PATCHLEVEL__)) # define _GL_HAS_BUILTIN_TRAP 1 #elif defined __has_builtin # define _GL_HAS_BUILTIN_TRAP __has_builtin (__builtin_trap) @@ -266,7 +268,9 @@ template # define _GL_HAS_BUILTIN_TRAP 0 #endif -#if 4 < __GNUC__ + (5 <= __GNUC_MINOR__) +#if defined __clang_major__ && __clang_major__ < 5 +# define _GL_HAS_BUILTIN_UNREACHABLE 0 +#elif 4 < __GNUC__ + (5 <= __GNUC_MINOR__) # define _GL_HAS_BUILTIN_UNREACHABLE 1 #elif defined __has_builtin # define _GL_HAS_BUILTIN_UNREACHABLE __has_builtin (__builtin_unreachable) diff --git a/xcompile/malloc/dynarray-skeleton.c b/xcompile/malloc/dynarray-skeleton.c new file mode 100644 index 00000000000..580c278b7c5 --- /dev/null +++ b/xcompile/malloc/dynarray-skeleton.c @@ -0,0 +1,528 @@ +/* Type-safe arrays which grow dynamically. + Copyright (C) 2017-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +/* Pre-processor macros which act as parameters: + + DYNARRAY_STRUCT + The struct tag of dynamic array to be defined. + DYNARRAY_ELEMENT + The type name of the element type. Elements are copied + as if by memcpy, and can change address as the dynamic + array grows. + DYNARRAY_PREFIX + The prefix of the functions which are defined. + + The following parameters are optional: + + DYNARRAY_ELEMENT_FREE + DYNARRAY_ELEMENT_FREE (E) is evaluated to deallocate the + contents of elements. E is of type DYNARRAY_ELEMENT *. + DYNARRAY_ELEMENT_INIT + DYNARRAY_ELEMENT_INIT (E) is evaluated to initialize a new + element. E is of type DYNARRAY_ELEMENT *. + If DYNARRAY_ELEMENT_FREE but not DYNARRAY_ELEMENT_INIT is + defined, new elements are automatically zero-initialized. + Otherwise, new elements have undefined contents. + DYNARRAY_INITIAL_SIZE + The size of the statically allocated array (default: + at least 2, more elements if they fit into 128 bytes). + Must be a preprocessor constant. If DYNARRAY_INITIAL_SIZE is 0, + there is no statically allocated array at, and all non-empty + arrays are heap-allocated. + DYNARRAY_FINAL_TYPE + The name of the type which holds the final array. If not + defined, is PREFIX##finalize not provided. DYNARRAY_FINAL_TYPE + must be a struct type, with members of type DYNARRAY_ELEMENT and + size_t at the start (in this order). + + These macros are undefined after this header file has been + included. + + The following types are provided (their members are private to the + dynarray implementation): + + struct DYNARRAY_STRUCT + + The following functions are provided: + + void DYNARRAY_PREFIX##init (struct DYNARRAY_STRUCT *); + void DYNARRAY_PREFIX##free (struct DYNARRAY_STRUCT *); + bool DYNARRAY_PREFIX##has_failed (const struct DYNARRAY_STRUCT *); + void DYNARRAY_PREFIX##mark_failed (struct DYNARRAY_STRUCT *); + size_t DYNARRAY_PREFIX##size (const struct DYNARRAY_STRUCT *); + DYNARRAY_ELEMENT *DYNARRAY_PREFIX##begin (const struct DYNARRAY_STRUCT *); + DYNARRAY_ELEMENT *DYNARRAY_PREFIX##end (const struct DYNARRAY_STRUCT *); + DYNARRAY_ELEMENT *DYNARRAY_PREFIX##at (struct DYNARRAY_STRUCT *, size_t); + void DYNARRAY_PREFIX##add (struct DYNARRAY_STRUCT *, DYNARRAY_ELEMENT); + DYNARRAY_ELEMENT *DYNARRAY_PREFIX##emplace (struct DYNARRAY_STRUCT *); + bool DYNARRAY_PREFIX##resize (struct DYNARRAY_STRUCT *, size_t); + void DYNARRAY_PREFIX##remove_last (struct DYNARRAY_STRUCT *); + void DYNARRAY_PREFIX##clear (struct DYNARRAY_STRUCT *); + + The following functions are provided are provided if the + prerequisites are met: + + bool DYNARRAY_PREFIX##finalize (struct DYNARRAY_STRUCT *, + DYNARRAY_FINAL_TYPE *); + (if DYNARRAY_FINAL_TYPE is defined) + DYNARRAY_ELEMENT *DYNARRAY_PREFIX##finalize (struct DYNARRAY_STRUCT *, + size_t *); + (if DYNARRAY_FINAL_TYPE is not defined) +*/ + +#include + +#include +#include +#include + +#ifndef DYNARRAY_STRUCT +# error "DYNARRAY_STRUCT must be defined" +#endif + +#ifndef DYNARRAY_ELEMENT +# error "DYNARRAY_ELEMENT must be defined" +#endif + +#ifndef DYNARRAY_PREFIX +# error "DYNARRAY_PREFIX must be defined" +#endif + +#ifdef DYNARRAY_INITIAL_SIZE +# if DYNARRAY_INITIAL_SIZE < 0 +# error "DYNARRAY_INITIAL_SIZE must be non-negative" +# endif +# if DYNARRAY_INITIAL_SIZE > 0 +# define DYNARRAY_HAVE_SCRATCH 1 +# else +# define DYNARRAY_HAVE_SCRATCH 0 +# endif +#else +/* Provide a reasonable default which limits the size of + DYNARRAY_STRUCT. */ +# define DYNARRAY_INITIAL_SIZE \ + (sizeof (DYNARRAY_ELEMENT) > 64 ? 2 : 128 / sizeof (DYNARRAY_ELEMENT)) +# define DYNARRAY_HAVE_SCRATCH 1 +#endif + +/* Public type definitions. */ + +/* All fields of this struct are private to the implementation. */ +struct DYNARRAY_STRUCT +{ + union + { + struct dynarray_header dynarray_abstract; + struct + { + /* These fields must match struct dynarray_header. */ + size_t used; + size_t allocated; + DYNARRAY_ELEMENT *array; + } dynarray_header; + } u; + +#if DYNARRAY_HAVE_SCRATCH + /* Initial inline allocation. */ + DYNARRAY_ELEMENT scratch[DYNARRAY_INITIAL_SIZE]; +#endif +}; + +/* Internal use only: Helper macros. */ + +/* Ensure macro-expansion of DYNARRAY_PREFIX. */ +#define DYNARRAY_CONCAT0(prefix, name) prefix##name +#define DYNARRAY_CONCAT1(prefix, name) DYNARRAY_CONCAT0(prefix, name) +#define DYNARRAY_NAME(name) DYNARRAY_CONCAT1(DYNARRAY_PREFIX, name) + +/* Use DYNARRAY_FREE instead of DYNARRAY_NAME (free), + so that Gnulib does not change 'free' to 'rpl_free'. */ +#define DYNARRAY_FREE DYNARRAY_CONCAT1 (DYNARRAY_NAME (f), ree) + +/* Address of the scratch buffer if any. */ +#if DYNARRAY_HAVE_SCRATCH +# define DYNARRAY_SCRATCH(list) (list)->scratch +#else +# define DYNARRAY_SCRATCH(list) NULL +#endif + +/* Internal use only: Helper functions. */ + +/* Internal function. Call DYNARRAY_ELEMENT_FREE with the array + elements. Name mangling needed due to the DYNARRAY_ELEMENT_FREE + macro expansion. */ +static inline void +DYNARRAY_NAME (free__elements__) (DYNARRAY_ELEMENT *__dynarray_array, + size_t __dynarray_used) +{ +#ifdef DYNARRAY_ELEMENT_FREE + for (size_t __dynarray_i = 0; __dynarray_i < __dynarray_used; ++__dynarray_i) + DYNARRAY_ELEMENT_FREE (&__dynarray_array[__dynarray_i]); +#endif /* DYNARRAY_ELEMENT_FREE */ +} + +/* Internal function. Free the non-scratch array allocation. */ +static inline void +DYNARRAY_NAME (free__array__) (struct DYNARRAY_STRUCT *list) +{ +#if DYNARRAY_HAVE_SCRATCH + if (list->u.dynarray_header.array != list->scratch) + free (list->u.dynarray_header.array); +#else + free (list->u.dynarray_header.array); +#endif +} + +/* Public functions. */ + +/* Initialize a dynamic array object. This must be called before any + use of the object. */ +__attribute_nonnull__ ((1)) +static void +DYNARRAY_NAME (init) (struct DYNARRAY_STRUCT *list) +{ + list->u.dynarray_header.used = 0; + list->u.dynarray_header.allocated = DYNARRAY_INITIAL_SIZE; + list->u.dynarray_header.array = DYNARRAY_SCRATCH (list); +} + +/* Deallocate the dynamic array and its elements. */ +__attribute_maybe_unused__ __attribute_nonnull__ ((1)) +static void +DYNARRAY_FREE (struct DYNARRAY_STRUCT *list) +{ + DYNARRAY_NAME (free__elements__) + (list->u.dynarray_header.array, list->u.dynarray_header.used); + DYNARRAY_NAME (free__array__) (list); + DYNARRAY_NAME (init) (list); +} + +/* Return true if the dynamic array is in an error state. */ +__attribute_nonnull__ ((1)) +static inline bool +DYNARRAY_NAME (has_failed) (const struct DYNARRAY_STRUCT *list) +{ + return list->u.dynarray_header.allocated == __dynarray_error_marker (); +} + +/* Mark the dynamic array as failed. All elements are deallocated as + a side effect. */ +__attribute_nonnull__ ((1)) +static void +DYNARRAY_NAME (mark_failed) (struct DYNARRAY_STRUCT *list) +{ + DYNARRAY_NAME (free__elements__) + (list->u.dynarray_header.array, list->u.dynarray_header.used); + DYNARRAY_NAME (free__array__) (list); + list->u.dynarray_header.array = DYNARRAY_SCRATCH (list); + list->u.dynarray_header.used = 0; + list->u.dynarray_header.allocated = __dynarray_error_marker (); +} + +/* Return the number of elements which have been added to the dynamic + array. */ +__attribute_nonnull__ ((1)) +static inline size_t +DYNARRAY_NAME (size) (const struct DYNARRAY_STRUCT *list) +{ + return list->u.dynarray_header.used; +} + +/* Return a pointer to the array element at INDEX. Terminate the + process if INDEX is out of bounds. */ +__attribute_nonnull__ ((1)) +static inline DYNARRAY_ELEMENT * +DYNARRAY_NAME (at) (struct DYNARRAY_STRUCT *list, size_t index) +{ + if (__glibc_unlikely (index >= DYNARRAY_NAME (size) (list))) + __libc_dynarray_at_failure (DYNARRAY_NAME (size) (list), index); + return list->u.dynarray_header.array + index; +} + +/* Return a pointer to the first array element, if any. For a + zero-length array, the pointer can be NULL even though the dynamic + array has not entered the failure state. */ +__attribute_nonnull__ ((1)) +static inline DYNARRAY_ELEMENT * +DYNARRAY_NAME (begin) (struct DYNARRAY_STRUCT *list) +{ + return list->u.dynarray_header.array; +} + +/* Return a pointer one element past the last array element. For a + zero-length array, the pointer can be NULL even though the dynamic + array has not entered the failure state. */ +__attribute_nonnull__ ((1)) +static inline DYNARRAY_ELEMENT * +DYNARRAY_NAME (end) (struct DYNARRAY_STRUCT *list) +{ + return list->u.dynarray_header.array + list->u.dynarray_header.used; +} + +/* Internal function. Slow path for the add function below. */ +static void +DYNARRAY_NAME (add__) (struct DYNARRAY_STRUCT *list, DYNARRAY_ELEMENT item) +{ + if (__glibc_unlikely + (!__libc_dynarray_emplace_enlarge (&list->u.dynarray_abstract, + DYNARRAY_SCRATCH (list), + sizeof (DYNARRAY_ELEMENT)))) + { + DYNARRAY_NAME (mark_failed) (list); + return; + } + + /* Copy the new element and increase the array length. */ + list->u.dynarray_header.array[list->u.dynarray_header.used++] = item; +} + +/* Add ITEM at the end of the array, enlarging it by one element. + Mark *LIST as failed if the dynamic array allocation size cannot be + increased. */ +__attribute_nonnull__ ((1)) +static inline void +DYNARRAY_NAME (add) (struct DYNARRAY_STRUCT *list, DYNARRAY_ELEMENT item) +{ + /* Do nothing in case of previous error. */ + if (DYNARRAY_NAME (has_failed) (list)) + return; + + /* Enlarge the array if necessary. */ + if (__glibc_unlikely (list->u.dynarray_header.used + == list->u.dynarray_header.allocated)) + { + DYNARRAY_NAME (add__) (list, item); + return; + } + + /* Copy the new element and increase the array length. */ + list->u.dynarray_header.array[list->u.dynarray_header.used++] = item; +} + +/* Internal function. Building block for the emplace functions below. + Assumes space for one more element in *LIST. */ +static inline DYNARRAY_ELEMENT * +DYNARRAY_NAME (emplace__tail__) (struct DYNARRAY_STRUCT *list) +{ + DYNARRAY_ELEMENT *result + = &list->u.dynarray_header.array[list->u.dynarray_header.used]; + ++list->u.dynarray_header.used; +#if defined (DYNARRAY_ELEMENT_INIT) + DYNARRAY_ELEMENT_INIT (result); +#elif defined (DYNARRAY_ELEMENT_FREE) + memset (result, 0, sizeof (*result)); +#endif + return result; +} + +/* Internal function. Slow path for the emplace function below. */ +static DYNARRAY_ELEMENT * +DYNARRAY_NAME (emplace__) (struct DYNARRAY_STRUCT *list) +{ + if (__glibc_unlikely + (!__libc_dynarray_emplace_enlarge (&list->u.dynarray_abstract, + DYNARRAY_SCRATCH (list), + sizeof (DYNARRAY_ELEMENT)))) + { + DYNARRAY_NAME (mark_failed) (list); + return NULL; + } + return DYNARRAY_NAME (emplace__tail__) (list); +} + +/* Allocate a place for a new element in *LIST and return a pointer to + it. The pointer can be NULL if the dynamic array cannot be + enlarged due to a memory allocation failure. */ +__attribute_maybe_unused__ __attribute_warn_unused_result__ +__attribute_nonnull__ ((1)) +static +/* Avoid inlining with the larger initialization code. */ +#if !(defined (DYNARRAY_ELEMENT_INIT) || defined (DYNARRAY_ELEMENT_FREE)) +inline +#endif +DYNARRAY_ELEMENT * +DYNARRAY_NAME (emplace) (struct DYNARRAY_STRUCT *list) +{ + /* Do nothing in case of previous error. */ + if (DYNARRAY_NAME (has_failed) (list)) + return NULL; + + /* Enlarge the array if necessary. */ + if (__glibc_unlikely (list->u.dynarray_header.used + == list->u.dynarray_header.allocated)) + return (DYNARRAY_NAME (emplace__) (list)); + return DYNARRAY_NAME (emplace__tail__) (list); +} + +/* Change the size of *LIST to SIZE. If SIZE is larger than the + existing size, new elements are added (which can be initialized). + Otherwise, the list is truncated, and elements are freed. Return + false on memory allocation failure (and mark *LIST as failed). */ +__attribute_maybe_unused__ __attribute_nonnull__ ((1)) +static bool +DYNARRAY_NAME (resize) (struct DYNARRAY_STRUCT *list, size_t size) +{ + if (size > list->u.dynarray_header.used) + { + bool ok; +#if defined (DYNARRAY_ELEMENT_INIT) + /* The new elements have to be initialized. */ + size_t old_size = list->u.dynarray_header.used; + ok = __libc_dynarray_resize (&list->u.dynarray_abstract, + size, DYNARRAY_SCRATCH (list), + sizeof (DYNARRAY_ELEMENT)); + if (ok) + for (size_t i = old_size; i < size; ++i) + { + DYNARRAY_ELEMENT_INIT (&list->u.dynarray_header.array[i]); + } +#elif defined (DYNARRAY_ELEMENT_FREE) + /* Zero initialization is needed so that the elements can be + safely freed. */ + ok = __libc_dynarray_resize_clear + (&list->u.dynarray_abstract, size, + DYNARRAY_SCRATCH (list), sizeof (DYNARRAY_ELEMENT)); +#else + ok = __libc_dynarray_resize (&list->u.dynarray_abstract, + size, DYNARRAY_SCRATCH (list), + sizeof (DYNARRAY_ELEMENT)); +#endif + if (__glibc_unlikely (!ok)) + DYNARRAY_NAME (mark_failed) (list); + return ok; + } + else + { + /* The list has shrunk in size. Free the removed elements. */ + DYNARRAY_NAME (free__elements__) + (list->u.dynarray_header.array + size, + list->u.dynarray_header.used - size); + list->u.dynarray_header.used = size; + return true; + } +} + +/* Remove the last element of LIST if it is present. */ +__attribute_maybe_unused__ __attribute_nonnull__ ((1)) +static void +DYNARRAY_NAME (remove_last) (struct DYNARRAY_STRUCT *list) +{ + /* used > 0 implies that the array is the non-failed state. */ + if (list->u.dynarray_header.used > 0) + { + size_t new_length = list->u.dynarray_header.used - 1; +#ifdef DYNARRAY_ELEMENT_FREE + DYNARRAY_ELEMENT_FREE (&list->u.dynarray_header.array[new_length]); +#endif + list->u.dynarray_header.used = new_length; + } +} + +/* Remove all elements from the list. The elements are freed, but the + list itself is not. */ +__attribute_maybe_unused__ __attribute_nonnull__ ((1)) +static void +DYNARRAY_NAME (clear) (struct DYNARRAY_STRUCT *list) +{ + /* free__elements__ does nothing if the list is in the failed + state. */ + DYNARRAY_NAME (free__elements__) + (list->u.dynarray_header.array, list->u.dynarray_header.used); + list->u.dynarray_header.used = 0; +} + +#ifdef DYNARRAY_FINAL_TYPE +/* Transfer the dynamic array to a permanent location at *RESULT. + Returns true on success on false on allocation failure. In either + case, *LIST is re-initialized and can be reused. A NULL pointer is + stored in *RESULT if LIST refers to an empty list. On success, the + pointer in *RESULT is heap-allocated and must be deallocated using + free. */ +__attribute_maybe_unused__ __attribute_warn_unused_result__ +__attribute_nonnull__ ((1, 2)) +static bool +DYNARRAY_NAME (finalize) (struct DYNARRAY_STRUCT *list, + DYNARRAY_FINAL_TYPE *result) +{ + struct dynarray_finalize_result res; + if (__libc_dynarray_finalize (&list->u.dynarray_abstract, + DYNARRAY_SCRATCH (list), + sizeof (DYNARRAY_ELEMENT), &res)) + { + /* On success, the result owns all the data. */ + DYNARRAY_NAME (init) (list); + *result = (DYNARRAY_FINAL_TYPE) { res.array, res.length }; + return true; + } + else + { + /* On error, we need to free all data. */ + DYNARRAY_FREE (list); + errno = ENOMEM; + return false; + } +} +#else /* !DYNARRAY_FINAL_TYPE */ +/* Transfer the dynamic array to a heap-allocated array and return a + pointer to it. The pointer is NULL if memory allocation fails, or + if the array is empty, so this function should be used only for + arrays which are known not be empty (usually because they always + have a sentinel at the end). If LENGTHP is not NULL, the array + length is written to *LENGTHP. *LIST is re-initialized and can be + reused. */ +__attribute_maybe_unused__ __attribute_warn_unused_result__ +__attribute_nonnull__ ((1)) +static DYNARRAY_ELEMENT * +DYNARRAY_NAME (finalize) (struct DYNARRAY_STRUCT *list, size_t *lengthp) +{ + struct dynarray_finalize_result res; + if (__libc_dynarray_finalize (&list->u.dynarray_abstract, + DYNARRAY_SCRATCH (list), + sizeof (DYNARRAY_ELEMENT), &res)) + { + /* On success, the result owns all the data. */ + DYNARRAY_NAME (init) (list); + if (lengthp != NULL) + *lengthp = res.length; + return res.array; + } + else + { + /* On error, we need to free all data. */ + DYNARRAY_FREE (list); + errno = ENOMEM; + return NULL; + } +} +#endif /* !DYNARRAY_FINAL_TYPE */ + +/* Undo macro definitions. */ + +#undef DYNARRAY_CONCAT0 +#undef DYNARRAY_CONCAT1 +#undef DYNARRAY_NAME +#undef DYNARRAY_SCRATCH +#undef DYNARRAY_HAVE_SCRATCH + +#undef DYNARRAY_STRUCT +#undef DYNARRAY_ELEMENT +#undef DYNARRAY_PREFIX +#undef DYNARRAY_ELEMENT_FREE +#undef DYNARRAY_ELEMENT_INIT +#undef DYNARRAY_INITIAL_SIZE +#undef DYNARRAY_FINAL_TYPE diff --git a/xcompile/malloc/dynarray.h b/xcompile/malloc/dynarray.h new file mode 100644 index 00000000000..a9a3b0859c1 --- /dev/null +++ b/xcompile/malloc/dynarray.h @@ -0,0 +1,177 @@ +/* Type-safe arrays which grow dynamically. Shared definitions. + Copyright (C) 2017-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +/* To use the dynarray facility, you need to include + and define the parameter macros + documented in that file. + + A minimal example which provides a growing list of integers can be + defined like this: + + struct int_array + { + // Pointer to result array followed by its length, + // as required by DYNARRAY_FINAL_TYPE. + int *array; + size_t length; + }; + + #define DYNARRAY_STRUCT dynarray_int + #define DYNARRAY_ELEMENT int + #define DYNARRAY_PREFIX dynarray_int_ + #define DYNARRAY_FINAL_TYPE struct int_array + #include + + To create a three-element array with elements 1, 2, 3, use this + code: + + struct dynarray_int dyn; + dynarray_int_init (&dyn); + for (int i = 1; i <= 3; ++i) + { + int *place = dynarray_int_emplace (&dyn); + assert (place != NULL); + *place = i; + } + struct int_array result; + bool ok = dynarray_int_finalize (&dyn, &result); + assert (ok); + assert (result.length == 3); + assert (result.array[0] == 1); + assert (result.array[1] == 2); + assert (result.array[2] == 3); + free (result.array); + + If the elements contain resources which must be freed, define + DYNARRAY_ELEMENT_FREE appropriately, like this: + + struct str_array + { + char **array; + size_t length; + }; + + #define DYNARRAY_STRUCT dynarray_str + #define DYNARRAY_ELEMENT char * + #define DYNARRAY_ELEMENT_FREE(ptr) free (*ptr) + #define DYNARRAY_PREFIX dynarray_str_ + #define DYNARRAY_FINAL_TYPE struct str_array + #include + + Compared to scratch buffers, dynamic arrays have the following + features: + + - They have an element type, and are not just an untyped buffer of + bytes. + + - When growing, previously stored elements are preserved. (It is + expected that scratch_buffer_grow_preserve and + scratch_buffer_set_array_size eventually go away because all + current users are moved to dynamic arrays.) + + - Scratch buffers have a more aggressive growth policy because + growing them typically means a retry of an operation (across an + NSS service module boundary), which is expensive. + + - For the same reason, scratch buffers have a much larger initial + stack allocation. */ + +#ifndef _DYNARRAY_H +#define _DYNARRAY_H + +#include +#include + +struct dynarray_header +{ + size_t used; + size_t allocated; + void *array; +}; + +/* Marker used in the allocated member to indicate that an error was + encountered. */ +static inline size_t +__dynarray_error_marker (void) +{ + return -1; +} + +/* Internal function. See the has_failed function in + dynarray-skeleton.c. */ +static inline bool +__dynarray_error (struct dynarray_header *list) +{ + return list->allocated == __dynarray_error_marker (); +} + +/* Internal function. Enlarge the dynamically allocated area of the + array to make room for one more element. SCRATCH is a pointer to + the scratch area (which is not heap-allocated and must not be + freed). ELEMENT_SIZE is the size, in bytes, of one element. + Return false on failure, true on success. */ +bool __libc_dynarray_emplace_enlarge (struct dynarray_header *, + void *scratch, size_t element_size); + +/* Internal function. Enlarge the dynamically allocated area of the + array to make room for at least SIZE elements (which must be larger + than the existing used part of the dynamic array). SCRATCH is a + pointer to the scratch area (which is not heap-allocated and must + not be freed). ELEMENT_SIZE is the size, in bytes, of one element. + Return false on failure, true on success. */ +bool __libc_dynarray_resize (struct dynarray_header *, size_t size, + void *scratch, size_t element_size); + +/* Internal function. Like __libc_dynarray_resize, but clear the new + part of the dynamic array. */ +bool __libc_dynarray_resize_clear (struct dynarray_header *, size_t size, + void *scratch, size_t element_size); + +/* Internal type. */ +struct dynarray_finalize_result +{ + void *array; + size_t length; +}; + +/* Internal function. Copy the dynamically-allocated area to an + explicitly-sized heap allocation. SCRATCH is a pointer to the + embedded scratch space. ELEMENT_SIZE is the size, in bytes, of the + element type. On success, true is returned, and pointer and length + are written to *RESULT. On failure, false is returned. The caller + has to take care of some of the memory management; this function is + expected to be called from dynarray-skeleton.c. */ +bool __libc_dynarray_finalize (struct dynarray_header *list, void *scratch, + size_t element_size, + struct dynarray_finalize_result *result); + + +/* Internal function. Terminate the process after an index error. + SIZE is the number of elements of the dynamic array. INDEX is the + lookup index which triggered the failure. */ +_Noreturn void __libc_dynarray_at_failure (size_t size, size_t index); + +#ifndef _ISOMAC +libc_hidden_proto (__libc_dynarray_emplace_enlarge) +libc_hidden_proto (__libc_dynarray_resize) +libc_hidden_proto (__libc_dynarray_resize_clear) +libc_hidden_proto (__libc_dynarray_finalize) +libc_hidden_proto (__libc_dynarray_at_failure) +#endif + +#endif /* _DYNARRAY_H */ diff --git a/xcompile/malloc/dynarray_at_failure.c b/xcompile/malloc/dynarray_at_failure.c new file mode 100644 index 00000000000..ebc9310982c --- /dev/null +++ b/xcompile/malloc/dynarray_at_failure.c @@ -0,0 +1,40 @@ +/* Report an dynamic array index out of bounds condition. + Copyright (C) 2017-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#ifndef _LIBC +# include +# include +#endif + +#include +#include + +void +__libc_dynarray_at_failure (size_t size, size_t index) +{ +#ifdef _LIBC + char buf[200]; + __snprintf (buf, sizeof (buf), "Fatal glibc error: " + "array index %zu not less than array length %zu\n", + index, size); + __libc_fatal (buf); +#else + abort (); +#endif +} +libc_hidden_def (__libc_dynarray_at_failure) diff --git a/xcompile/malloc/dynarray_emplace_enlarge.c b/xcompile/malloc/dynarray_emplace_enlarge.c new file mode 100644 index 00000000000..7da539316c1 --- /dev/null +++ b/xcompile/malloc/dynarray_emplace_enlarge.c @@ -0,0 +1,77 @@ +/* Increase the size of a dynamic array in preparation of an emplace operation. + Copyright (C) 2017-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#ifndef _LIBC +# include +#endif + +#include +#include +#include +#include +#include + +bool +__libc_dynarray_emplace_enlarge (struct dynarray_header *list, + void *scratch, size_t element_size) +{ + size_t new_allocated; + if (list->allocated == 0) + { + /* No scratch buffer provided. Choose a reasonable default + size. */ + if (element_size < 4) + new_allocated = 16; + else if (element_size < 8) + new_allocated = 8; + else + new_allocated = 4; + } + else + /* Increase the allocated size, using an exponential growth + policy. */ + { + new_allocated = list->allocated + list->allocated / 2 + 1; + if (new_allocated <= list->allocated) + { + /* Overflow. */ + __set_errno (ENOMEM); + return false; + } + } + + size_t new_size; + if (INT_MULTIPLY_WRAPV (new_allocated, element_size, &new_size)) + return false; + void *new_array; + if (list->array == scratch) + { + /* The previous array was not heap-allocated. */ + new_array = malloc (new_size); + if (new_array != NULL && list->array != NULL) + memcpy (new_array, list->array, list->used * element_size); + } + else + new_array = realloc (list->array, new_size); + if (new_array == NULL) + return false; + list->array = new_array; + list->allocated = new_allocated; + return true; +} +libc_hidden_def (__libc_dynarray_emplace_enlarge) diff --git a/xcompile/malloc/dynarray_finalize.c b/xcompile/malloc/dynarray_finalize.c new file mode 100644 index 00000000000..673595a5fad --- /dev/null +++ b/xcompile/malloc/dynarray_finalize.c @@ -0,0 +1,66 @@ +/* Copy the dynamically-allocated area to an explicitly-sized heap allocation. + Copyright (C) 2017-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#ifndef _LIBC +# include +#endif + +#include +#include +#include + +bool +__libc_dynarray_finalize (struct dynarray_header *list, + void *scratch, size_t element_size, + struct dynarray_finalize_result *result) +{ + if (__dynarray_error (list)) + /* The caller will reported the deferred error. */ + return false; + + size_t used = list->used; + + /* Empty list. */ + if (used == 0) + { + /* An empty list could still be backed by a heap-allocated + array. Free it if necessary. */ + if (list->array != scratch) + free (list->array); + *result = (struct dynarray_finalize_result) { NULL, 0 }; + return true; + } + + size_t allocation_size = used * element_size; + void *heap_array = malloc (allocation_size); + if (heap_array != NULL) + { + /* The new array takes ownership of the strings. */ + if (list->array != NULL) + memcpy (heap_array, list->array, allocation_size); + if (list->array != scratch) + free (list->array); + *result = (struct dynarray_finalize_result) + { .array = heap_array, .length = used }; + return true; + } + else + /* The caller will perform the freeing operation. */ + return false; +} +libc_hidden_def (__libc_dynarray_finalize) diff --git a/xcompile/malloc/dynarray_resize.c b/xcompile/malloc/dynarray_resize.c new file mode 100644 index 00000000000..7ecd4de63b9 --- /dev/null +++ b/xcompile/malloc/dynarray_resize.c @@ -0,0 +1,68 @@ +/* Increase the size of a dynamic array. + Copyright (C) 2017-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#ifndef _LIBC +# include +#endif + +#include +#include +#include +#include +#include + +bool +__libc_dynarray_resize (struct dynarray_header *list, size_t size, + void *scratch, size_t element_size) +{ + /* The existing allocation provides sufficient room. */ + if (size <= list->allocated) + { + list->used = size; + return true; + } + + /* Otherwise, use size as the new allocation size. The caller is + expected to provide the final size of the array, so there is no + over-allocation here. */ + + size_t new_size_bytes; + if (INT_MULTIPLY_WRAPV (size, element_size, &new_size_bytes)) + { + /* Overflow. */ + __set_errno (ENOMEM); + return false; + } + void *new_array; + if (list->array == scratch) + { + /* The previous array was not heap-allocated. */ + new_array = malloc (new_size_bytes); + if (new_array != NULL && list->array != NULL) + memcpy (new_array, list->array, list->used * element_size); + } + else + new_array = realloc (list->array, new_size_bytes); + if (new_array == NULL) + return false; + list->array = new_array; + list->allocated = size; + list->used = size; + return true; +} +libc_hidden_def (__libc_dynarray_resize) diff --git a/xcompile/malloc/dynarray_resize_clear.c b/xcompile/malloc/dynarray_resize_clear.c new file mode 100644 index 00000000000..bb23c522a14 --- /dev/null +++ b/xcompile/malloc/dynarray_resize_clear.c @@ -0,0 +1,39 @@ +/* Increase the size of a dynamic array and clear the new part. + Copyright (C) 2017-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#ifndef _LIBC +# include +#endif + +#include +#include + +bool +__libc_dynarray_resize_clear (struct dynarray_header *list, size_t size, + void *scratch, size_t element_size) +{ + size_t old_size = list->used; + if (!__libc_dynarray_resize (list, size, scratch, element_size)) + return false; + /* __libc_dynarray_resize already checked for overflow. */ + char *array = list->array; + memset (array + (old_size * element_size), 0, + (size - old_size) * element_size); + return true; +} +libc_hidden_def (__libc_dynarray_resize_clear) diff --git a/xcompile/malloc/scratch_buffer.h b/xcompile/malloc/scratch_buffer.h new file mode 100644 index 00000000000..33fd2b29cd5 --- /dev/null +++ b/xcompile/malloc/scratch_buffer.h @@ -0,0 +1,135 @@ +/* Variable-sized buffer with on-stack default allocation. + Copyright (C) 2015-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#ifndef _SCRATCH_BUFFER_H +#define _SCRATCH_BUFFER_H + +/* Scratch buffers with a default stack allocation and fallback to + heap allocation. It is expected that this function is used in this + way: + + struct scratch_buffer tmpbuf; + scratch_buffer_init (&tmpbuf); + + while (!function_that_uses_buffer (tmpbuf.data, tmpbuf.length)) + if (!scratch_buffer_grow (&tmpbuf)) + return -1; + + scratch_buffer_free (&tmpbuf); + return 0; + + The allocation functions (scratch_buffer_grow, + scratch_buffer_grow_preserve, scratch_buffer_set_array_size) make + sure that the heap allocation, if any, is freed, so that the code + above does not have a memory leak. The buffer still remains in a + state that can be deallocated using scratch_buffer_free, so a loop + like this is valid as well: + + struct scratch_buffer tmpbuf; + scratch_buffer_init (&tmpbuf); + + while (!function_that_uses_buffer (tmpbuf.data, tmpbuf.length)) + if (!scratch_buffer_grow (&tmpbuf)) + break; + + scratch_buffer_free (&tmpbuf); + + scratch_buffer_grow and scratch_buffer_grow_preserve are guaranteed + to grow the buffer by at least 512 bytes. This means that when + using the scratch buffer as a backing store for a non-character + array whose element size, in bytes, is 512 or smaller, the scratch + buffer only has to grow once to make room for at least one more + element. +*/ + +#include +#include +#include + +/* Scratch buffer. Must be initialized with scratch_buffer_init + before its use. */ +struct scratch_buffer { + void *data; /* Pointer to the beginning of the scratch area. */ + size_t length; /* Allocated space at the data pointer, in bytes. */ + union { max_align_t __align; char __c[1024]; } __space; +}; + +/* Initializes *BUFFER so that BUFFER->data points to BUFFER->__space + and BUFFER->length reflects the available space. */ +static inline void +scratch_buffer_init (struct scratch_buffer *buffer) +{ + buffer->data = buffer->__space.__c; + buffer->length = sizeof (buffer->__space); +} + +/* Deallocates *BUFFER (if it was heap-allocated). */ +static inline void +scratch_buffer_free (struct scratch_buffer *buffer) +{ + if (buffer->data != buffer->__space.__c) + free (buffer->data); +} + +/* Grow *BUFFER by some arbitrary amount. The buffer contents is NOT + preserved. Return true on success, false on allocation failure (in + which case the old buffer is freed). On success, the new buffer is + larger than the previous size. On failure, *BUFFER is deallocated, + but remains in a free-able state, and errno is set. */ +bool __libc_scratch_buffer_grow (struct scratch_buffer *buffer); +libc_hidden_proto (__libc_scratch_buffer_grow) + +/* Alias for __libc_scratch_buffer_grow. */ +static __always_inline bool +scratch_buffer_grow (struct scratch_buffer *buffer) +{ + return __glibc_likely (__libc_scratch_buffer_grow (buffer)); +} + +/* Like __libc_scratch_buffer_grow, but preserve the old buffer + contents on success, as a prefix of the new buffer. */ +bool __libc_scratch_buffer_grow_preserve (struct scratch_buffer *buffer); +libc_hidden_proto (__libc_scratch_buffer_grow_preserve) + +/* Alias for __libc_scratch_buffer_grow_preserve. */ +static __always_inline bool +scratch_buffer_grow_preserve (struct scratch_buffer *buffer) +{ + return __glibc_likely (__libc_scratch_buffer_grow_preserve (buffer)); +} + +/* Grow *BUFFER so that it can store at least NELEM elements of SIZE + bytes. The buffer contents are NOT preserved. Both NELEM and SIZE + can be zero. Return true on success, false on allocation failure + (in which case the old buffer is freed, but *BUFFER remains in a + free-able state, and errno is set). It is unspecified whether this + function can reduce the array size. */ +bool __libc_scratch_buffer_set_array_size (struct scratch_buffer *buffer, + size_t nelem, size_t size); +libc_hidden_proto (__libc_scratch_buffer_set_array_size) + +/* Alias for __libc_scratch_set_array_size. */ +static __always_inline bool +scratch_buffer_set_array_size (struct scratch_buffer *buffer, + size_t nelem, size_t size) +{ + return __glibc_likely (__libc_scratch_buffer_set_array_size + (buffer, nelem, size)); +} + +#endif /* _SCRATCH_BUFFER_H */ diff --git a/xcompile/malloc/scratch_buffer_dupfree.c b/xcompile/malloc/scratch_buffer_dupfree.c new file mode 100644 index 00000000000..2f60fbb54e8 --- /dev/null +++ b/xcompile/malloc/scratch_buffer_dupfree.c @@ -0,0 +1,41 @@ +/* Variable-sized buffer with on-stack default allocation. + Copyright (C) 2020-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#ifndef _LIBC +# include +#endif + +#include +#include + +void * +__libc_scratch_buffer_dupfree (struct scratch_buffer *buffer, size_t size) +{ + void *data = buffer->data; + if (data == buffer->__space.__c) + { + void *copy = malloc (size); + return copy != NULL ? memcpy (copy, data, size) : NULL; + } + else + { + void *copy = realloc (data, size); + return copy != NULL ? copy : data; + } +} +libc_hidden_def (__libc_scratch_buffer_dupfree) diff --git a/xcompile/malloc/scratch_buffer_grow.c b/xcompile/malloc/scratch_buffer_grow.c new file mode 100644 index 00000000000..a5e8f2f7230 --- /dev/null +++ b/xcompile/malloc/scratch_buffer_grow.c @@ -0,0 +1,56 @@ +/* Variable-sized buffer with on-stack default allocation. + Copyright (C) 2015-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#ifndef _LIBC +# include +#endif + +#include +#include + +bool +__libc_scratch_buffer_grow (struct scratch_buffer *buffer) +{ + void *new_ptr; + size_t new_length = buffer->length * 2; + + /* Discard old buffer. */ + scratch_buffer_free (buffer); + + /* Check for overflow. */ + if (__glibc_likely (new_length >= buffer->length)) + new_ptr = malloc (new_length); + else + { + __set_errno (ENOMEM); + new_ptr = NULL; + } + + if (__glibc_unlikely (new_ptr == NULL)) + { + /* Buffer must remain valid to free. */ + scratch_buffer_init (buffer); + return false; + } + + /* Install new heap-based buffer. */ + buffer->data = new_ptr; + buffer->length = new_length; + return true; +} +libc_hidden_def (__libc_scratch_buffer_grow) diff --git a/xcompile/malloc/scratch_buffer_grow_preserve.c b/xcompile/malloc/scratch_buffer_grow_preserve.c new file mode 100644 index 00000000000..c0b5d87b7e4 --- /dev/null +++ b/xcompile/malloc/scratch_buffer_grow_preserve.c @@ -0,0 +1,67 @@ +/* Variable-sized buffer with on-stack default allocation. + Copyright (C) 2015-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#ifndef _LIBC +# include +#endif + +#include +#include +#include + +bool +__libc_scratch_buffer_grow_preserve (struct scratch_buffer *buffer) +{ + size_t new_length = 2 * buffer->length; + void *new_ptr; + + if (buffer->data == buffer->__space.__c) + { + /* Move buffer to the heap. No overflow is possible because + buffer->length describes a small buffer on the stack. */ + new_ptr = malloc (new_length); + if (new_ptr == NULL) + return false; + memcpy (new_ptr, buffer->__space.__c, buffer->length); + } + else + { + /* Buffer was already on the heap. Check for overflow. */ + if (__glibc_likely (new_length >= buffer->length)) + new_ptr = realloc (buffer->data, new_length); + else + { + __set_errno (ENOMEM); + new_ptr = NULL; + } + + if (__glibc_unlikely (new_ptr == NULL)) + { + /* Deallocate, but buffer must remain valid to free. */ + free (buffer->data); + scratch_buffer_init (buffer); + return false; + } + } + + /* Install new heap-based buffer. */ + buffer->data = new_ptr; + buffer->length = new_length; + return true; +} +libc_hidden_def (__libc_scratch_buffer_grow_preserve) diff --git a/xcompile/malloc/scratch_buffer_set_array_size.c b/xcompile/malloc/scratch_buffer_set_array_size.c new file mode 100644 index 00000000000..24c39350ade --- /dev/null +++ b/xcompile/malloc/scratch_buffer_set_array_size.c @@ -0,0 +1,64 @@ +/* Variable-sized buffer with on-stack default allocation. + Copyright (C) 2015-2023 Free Software Foundation, Inc. + This file is part of the GNU C Library. + + The GNU C Library is free software; you can redistribute it and/or + modify it under the terms of the GNU Lesser General Public + License as published by the Free Software Foundation; either + version 2.1 of the License, or (at your option) any later version. + + The GNU C Library 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 + Lesser General Public License for more details. + + You should have received a copy of the GNU Lesser General Public + License along with the GNU C Library; if not, see + . */ + +#ifndef _LIBC +# include +#endif + +#include +#include +#include + +bool +__libc_scratch_buffer_set_array_size (struct scratch_buffer *buffer, + size_t nelem, size_t size) +{ + size_t new_length = nelem * size; + + /* Avoid overflow check if both values are small. */ + if ((nelem | size) >> (sizeof (size_t) * CHAR_BIT / 2) != 0 + && nelem != 0 && size != new_length / nelem) + { + /* Overflow. Discard the old buffer, but it must remain valid + to free. */ + scratch_buffer_free (buffer); + scratch_buffer_init (buffer); + __set_errno (ENOMEM); + return false; + } + + if (new_length <= buffer->length) + return true; + + /* Discard old buffer. */ + scratch_buffer_free (buffer); + + char *new_ptr = malloc (new_length); + if (new_ptr == NULL) + { + /* Buffer must remain valid to free. */ + scratch_buffer_init (buffer); + return false; + } + + /* Install new heap-based buffer. */ + buffer->data = new_ptr; + buffer->length = new_length; + return true; +} +libc_hidden_def (__libc_scratch_buffer_set_array_size) -- cgit v1.3 From d44b60c2f001d57b010f0e9b82f798fbad9a23d6 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 20 Jan 2023 19:06:32 +0800 Subject: Update Android port * .gitignore: Don't ignore verbose.mk.android. * doc/emacs/Makefile.in (EMACSSOURCES): Add android.texi and input.texi. * doc/emacs/android.texi (Android): Document support for the on-screen keyboard. (Android Startup): Document how to start Emacs with -Q on Android. (Android Environment): Document how Emacs works around the system ``task killer''. Document changes to frame deletion behavior. * doc/emacs/emacs.texi (Top): * doc/emacs/input.texi (Other Input Devices, On-Screen Keyboards): Document how to use Emacs with virtual keyboards. * doc/lispref/commands.texi (Touchscreen Events): Document changes to `touch-screen-track-drag'. * doc/lispref/frames.texi (Frames, On-Screen Keyboards): New node. * java/AndroidManifest.xml.in: Add settings activity and appropriate OSK adjustment mode. * java/org/gnu/emacs/EmacsActivity.java (onCreate): Allow creating Emacs with -Q. (onDestroy): Don't remove if killed by the system. * java/org/gnu/emacs/EmacsContextMenu.java (inflateMenuItems): Fix context menus again. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Make all event sending functions return long. * java/org/gnu/emacs/EmacsPreferencesActivity.java (EmacsPreferencesActivity): New class. * java/org/gnu/emacs/EmacsService.java (EmacsService) (onStartCommand, onCreate, startEmacsService): Start as a foreground service if necessary to bypass system restrictions. * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView): * java/org/gnu/emacs/EmacsThread.java (EmacsThread, run): * java/org/gnu/emacs/EmacsView.java (EmacsView, onLayout) (onDetachedFromWindow): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, viewLayout): Implement frame resize synchronization.. * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager, removeWindowConsumer): Adjust accordingly for changes to frame deletion behavior. * lisp/frame.el (android-toggle-on-screen-keyboard) (frame-toggle-on-screen-keyboard): New function. * lisp/minibuffer.el (minibuffer-setup-on-screen-keyboard) (minibuffer-exit-on-screen-keyboard): New functions. (minibuffer-setup-hook, minibuffer-exit-hook): Add new functions to hooks. * lisp/touch-screen.el (touch-screen-relative-xy): Accept new value of window `frame'. Return frame coordinates in that case. (touch-screen-set-point-commands): New variable. (touch-screen-handle-point-up): Respect that variable. (touch-screen-track-drag): Return `no-drag' where appropriate. (touch-screen-drag-mode-line-1, touch-screen-drag-mode-line): Refactor to use `no-drag'. * src/android.c (struct android_emacs_window): New methods. Make all event sending functions return the event serial. (android_toggle_on_screen_keyboard, android_window_updated): New functions. * src/android.h: Update prototypes. * src/androidfns.c (Fandroid_toggle_on_screen_keyboard) (syms_of_androidfns): New function. * src/androidgui.h (struct android_any_event) (struct android_key_event, struct android_configure_event) (struct android_focus_event, struct android_window_action_event) (struct android_crossing_event, struct android_motion_event) (struct android_button_event, struct android_touch_event) (struct android_wheel_event, struct android_iconify_event) (struct android_menu_event): Add `serial' fields. * src/androidterm.c (handle_one_android_event) (android_frame_up_to_date): * src/androidterm.h (struct android_output): Implement frame resize synchronization. --- .gitignore | 1 + doc/emacs/Makefile.in | 2 + doc/emacs/android.texi | 70 +++++-- doc/emacs/emacs.texi | 1 + doc/emacs/input.texi | 36 ++++ doc/lispref/commands.texi | 8 +- doc/lispref/frames.texi | 21 +++ java/AndroidManifest.xml.in | 12 ++ java/org/gnu/emacs/EmacsActivity.java | 20 +- java/org/gnu/emacs/EmacsContextMenu.java | 1 + java/org/gnu/emacs/EmacsNative.java | 39 ++-- java/org/gnu/emacs/EmacsPreferencesActivity.java | 98 ++++++++++ java/org/gnu/emacs/EmacsService.java | 62 ++++++- java/org/gnu/emacs/EmacsSurfaceView.java | 127 +++++++++---- java/org/gnu/emacs/EmacsThread.java | 12 +- java/org/gnu/emacs/EmacsView.java | 55 +++++- java/org/gnu/emacs/EmacsWindow.java | 58 +++++- .../gnu/emacs/EmacsWindowAttachmentManager.java | 4 +- lisp/frame.el | 17 ++ lisp/minibuffer.el | 23 +++ lisp/touch-screen.el | 203 +++++++++++++-------- src/android.c | 147 ++++++++++++--- src/android.h | 2 + src/androidfns.c | 25 +++ src/androidgui.h | 20 ++ src/androidterm.c | 24 +++ src/androidterm.h | 5 + 27 files changed, 894 insertions(+), 199 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsPreferencesActivity.java (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/.gitignore b/.gitignore index 3bc40c67489..6494e4e8f39 100644 --- a/.gitignore +++ b/.gitignore @@ -55,6 +55,7 @@ src/emacs-module.h # Built by recursive call to `configure'. *.android !INSTALL.android +!verbose.mk.android # Built by `java'. java/install_temp/* diff --git a/doc/emacs/Makefile.in b/doc/emacs/Makefile.in index 161bdcb1c59..c7415312753 100644 --- a/doc/emacs/Makefile.in +++ b/doc/emacs/Makefile.in @@ -146,6 +146,8 @@ EMACSSOURCES= \ ${srcdir}/glossary.texi \ ${srcdir}/ack.texi \ ${srcdir}/kmacro.texi \ + ${srcdir}/android.texi \ + ${srcdir}/input.texi \ $(EMACS_XTRA) ## Disable implicit rules. diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 01a2e68a97e..ba5d2ca3e09 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -10,8 +10,8 @@ Alliance. This section describes the peculiarities of using Emacs on an Android device running Android 2.2 or later. Android devices commonly rely on user input through a touch screen -or digitizer device. For more information about using them with -Emacs, @pxref{other Input Devices}. +or digitizer device and on-screen keyboard. For more information +about using such devices with Emacs, @pxref{Other Input Devices}. @menu * What is Android?:: Preamble. @@ -82,6 +82,16 @@ command on that other system: $ adb logcat | grep -E "(android_run_debug_thread|[Ee]macs)" @end example +@cindex emacs -Q, android +Since Android has no command line, there is normally no way to specify +command-line arguments. However, Emacs can be started with the +equivalent of the @code{--quick} option (@pxref{Initial Options}) +through a special preferences screen, which can be accessed through +the Emacs ``app info'' page in the system settings application. + +Consult the manufacturer of your device for more details, as how to do +this varies by device. + @node Android File System @section What files Emacs can access under Android @cindex /assets directory, android @@ -149,7 +159,7 @@ which is the app data directory (@pxref{Android File System}.) directories, and the app data directories of other applications. In recent versions of Android, the system also prohibits, for security reasons, even Emacs itself from running executables inside the app -data directory! +data directory. Emacs comes with several binaries. While being executable files, they are packaged as libraries in the library directory, because @@ -173,9 +183,28 @@ within itself. Application processes are treated as disposable entities by the system. When all Emacs frames move to the background, Emacs is liable to be killed by the system at any time, for the purpose of saving -resources. There is currently no easy way to bypass these -restrictions, aside from keeping Emacs constantly running in the -foreground. +system resources. + + On Android 7.1 and earlier, Emacs tells the system to treat it as a +``background service''. The system will try to avoid killing Emacs +unless the device is under memory stress. + + Android 8.0 removed the ability for background services to receive +such special treatment. However, Emacs applies a workaround: the +system considers applications that create a permanent notification to +be performing active work, and will avoid killing such applications. +Thus, on those systems, Emacs displays a permanant notification for as +long as it is running. Once the notification is displayed, it can be +safely hidden through the system settings without resulting in Emacs +being killed. + + However, it is not guaranteed that the system will not kill Emacs, +even if the notification is being displayed. While the Open Handset +Alliance's sample implementation of Android behaves correctly, many +manufacturers place additional restrictions on program execution in +the background in their proprietary versions of Android. There is a +list of such troublesome manufacturers and sometimes workarounds, at +@url{https://dontkillmyapp.com/}. @section Android permissions @cindex external storage, android @@ -272,14 +301,14 @@ maximized or full-screen, and only one window can be displayed at a time. On larger devices, the system allows up to four windows to be tiled on the screen at any time. -Windows on Android do not continue to exist indefinitely after they + Windows on Android do not continue to exist indefinitely after they are created. Instead, the system may choose to terminate windows that are not on screen in order to save memory, with the assumption that the program will save its contents to disk and restore them later, when the user asks to open it again. As this is obvious not possible with Emacs, Emacs separates a frame from a system window. -Each system window created (including the initial window created + Each system window created (including the initial window created during Emacs startup) is appended to a list of windows that do not have associated frames. When a frame is created, Emacs looks up any window within that list, and displays the contents of the frame @@ -287,11 +316,28 @@ within; if there is no window at all, then one is created. Likewise, when a new window is created by the system, Emacs places the contents of any frame that is not already displayed within a window inside. When a frame is closed, the corresponding system window is also -closed. +closed. Upon startup, the system creates a window itself (within +which Emacs displays the first window system frame shortly +thereafter.) Emacs differentiates between that window and windows +created on behalf of other frames to determine what to do when the +system window associated with a frame is closed: + +@itemize @bullet +@item +When the system closes the window created during application startup +in order to save memory, Emacs retains the frame for when that window +is created later. -This strategy works as long as one window is in the foreground. -Otherwise, Emacs can only run in the background for a limited amount -of time before the process is killed completely. +@item +When the user closes the window created during application startup, +and the window was not previously closed by the system in order to +save resources, Emacs deletes any frame displayed within that window. + +@item +When the user or the system closes any window created by Emacs on +behalf of a specific frame, Emacs deletes the frame displayed within +that window. +@end itemize @cindex windowing limitations, android @cindex frame parameters, android diff --git a/doc/emacs/emacs.texi b/doc/emacs/emacs.texi index 9fd169cd5cc..0cb454e5294 100644 --- a/doc/emacs/emacs.texi +++ b/doc/emacs/emacs.texi @@ -1269,6 +1269,7 @@ Emacs and Android Emacs and unconventional input devices * Touchscreens:: Using Emacs on touchscreens. +* On-Screen Keyboards:: Using Emacs with virtual keyboards. Emacs and Microsoft Windows/MS-DOS diff --git a/doc/emacs/input.texi b/doc/emacs/input.texi index 0e029c401e3..1a58d1ca0ac 100644 --- a/doc/emacs/input.texi +++ b/doc/emacs/input.texi @@ -16,6 +16,7 @@ input devices, which is detailed here. @menu * Touchscreens:: Using Emacs on touchscreens. +* On-Screen Keyboards:: Using Emacs with virtual keyboards. @end menu @node Touchscreens @@ -58,3 +59,38 @@ were to be held down. @xref{Mouse Commands}. By default, Emacs considers a tool as having been left on the display for a while after 0.7 seconds, but this can be changed by customizing the variable @code{touch-screen-delay}. + +@node On-Screen Keyboards +@section Using Emacs with virtual keyboards +@cindex virtual keyboards +@cindex on-screen keyboards + + When there is no physical keyboard attached to a system, the +windowing system typically provides an on-screen keyboard, more often +known as a ``virtual keyboard'', containing rows of clickable buttons +that send keyboard input to the application, much like a real keyboard +would. This virtual keyboard is hidden by default, as it uses up +valuable on-screen real estate, and must be opened once the program +being used is ready to accept keyboard input. + + Under the X Window System, the client that provides the on-screen +keyboard typically detects when the application is ready to accept +keyboard input through a set of complex heuristics, and automatically +displays the keyboard when necessary. + + On other systems such as Android, Emacs must tell the system when it +is ready to accept keyboard input. Typically, this is done in +response to a touchscreen ``tap'' gesture (@pxref{Touchscreens}), or +once to the minibuffer becomes in use (@pxref{Minibuffer}.) + +@vindex touch-screen-set-point-commands + When a ``tap'' gesture results in a command being executed, Emacs +checks to see whether or not the command is supposed to set the point +by looking for it in the list @code{touch-screen-set-point-commands}. +If it is, then Emacs looks up whether or not the text under the point +is read-only; if not, it activates the on-screen keyboard, assuming +that the user is about to enter text in to the current buffer. + + Emacs also provides a set of functions to show or hide the on-screen +keyboard. For more details, @pxref{On-Screen Keyboards,,, elisp, The +Emacs Lisp Reference Manual}. diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index 59367a2cecc..484c7dc2a06 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -2055,9 +2055,11 @@ The caller should not perform any action in that case. @defun touch-screen-track-drag event update &optional data This function is used to track a single ``drag'' gesture originating -from the @code{touchscreen-begin} event @code{event}. Currently, it -behaves identically to @code{touch-screen-track-tap}, but differences -are anticipated in the future. +from the @code{touchscreen-begin} event @code{event}. + +It behaves like @code{touch-screen-track-tap}, except that it returns +@code{no-drag} if the touchpoint in @code{event} did not move far +enough to qualify as an actual drag. @end defun @node Focus Events diff --git a/doc/lispref/frames.texi b/doc/lispref/frames.texi index 2ceafab7a6b..497715bdb19 100644 --- a/doc/lispref/frames.texi +++ b/doc/lispref/frames.texi @@ -104,6 +104,7 @@ window of another Emacs frame. @xref{Child Frames}. * Mouse Tracking:: Getting events that say when the mouse moves. * Mouse Position:: Asking where the mouse is, or moving it. * Pop-Up Menus:: Displaying a menu for the user to select from. +* On-Screen Keyboards:: Displaying the virtual keyboard. * Dialog Boxes:: Displaying a box to ask yes or no. * Pointer Shape:: Specifying the shape of the mouse pointer. * Window System Selections:: Transferring text to and from other X clients. @@ -3812,6 +3813,26 @@ keymap. It won't be called if @code{x-popup-menu} returns for some other reason without displaying a pop-up menu. @end defvar +@node On-Screen Keyboards +@section On-Screen Keyboards + + An on-screen keyboard is a special kind of pop up provided by the +system, with rows of clickable buttons that act as a real keyboard. + + On certain systems (@pxref{On-Screen Keyboards,,,emacs, The Emacs +Manual}), Emacs is supposed to display and hide the on screen keyboard +depending on whether or not the user is about to type something. + +@defun frame-toggle-on-screen-keyboard frame hide +This function displays or hides the on-screen keyboard on behalf of +the frame @var{frame}. If @var{hide} is non-@code{nil}, then the +on-screen keyboard is hidden; otherwise, it is displayed. + +This has no effect if the system automatically detects when to display +the on-screen keyboard, or when it does not provide any on-screen +keyboard. +@end defun + @node Dialog Boxes @section Dialog Boxes @cindex dialog boxes diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in index b680137a9d0..74f69d2a8e5 100644 --- a/java/AndroidManifest.xml.in +++ b/java/AndroidManifest.xml.in @@ -62,8 +62,10 @@ along with GNU Emacs. If not, see . --> android:theme="@android:style/Theme" android:debuggable="true" android:extractNativeLibs="true"> + @@ -73,8 +75,18 @@ along with GNU Emacs. If not, see . --> + + + + + + + . */ + +package org.gnu.emacs; + +import android.app.Activity; +import android.content.Intent; +import android.os.Bundle; +import android.os.Build; +import android.view.View; +import android.view.ViewGroup.LayoutParams; +import android.widget.LinearLayout; +import android.widget.TextView; + +import android.R; + +/* This module provides a ``preferences'' display for Emacs. It is + supposed to be launched from inside the Settings application to + perform various actions, such as starting Emacs with the ``-Q'' + option, which would not be possible otherwise, as there is no + command line on Android. */ + +public class EmacsPreferencesActivity extends Activity +{ + /* The linear layout associated with the activity. */ + private LinearLayout layout; + + /* Restart Emacs with -Q. Call EmacsThread.exit to kill Emacs now, and + tell the system to EmacsActivity with some parameters later. */ + + private void + startEmacsQ () + { + Intent intent; + + intent = new Intent (this, EmacsActivity.class); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK + | Intent.FLAG_ACTIVITY_CLEAR_TASK); + intent.putExtra ("org.gnu.emacs.START_DASH_Q", true); + startActivity (intent); + System.exit (0); + } + + @Override + public void + onCreate (Bundle savedInstanceState) + { + LinearLayout layout; + TextView textView; + LinearLayout.LayoutParams params; + int resid; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) + setTheme (R.style.Theme_DeviceDefault_Settings); + else if (Build.VERSION.SDK_INT + >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + setTheme (R.style.Theme_DeviceDefault); + + layout = new LinearLayout (this); + layout.setOrientation (LinearLayout.VERTICAL); + setContentView (layout); + + textView = new TextView (this); + textView.setPadding (8, 20, 20, 8); + + params = new LinearLayout.LayoutParams (LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT); + textView.setLayoutParams (params); + textView.setText ("(Re)start Emacs with -Q"); + textView.setOnClickListener (new View.OnClickListener () { + @Override + public void + onClick (View view) + { + startEmacsQ (); + } + }); + layout.addView (textView); + + super.onCreate (savedInstanceState); + } +}; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index bcf8d9ff6e8..95f21b211a3 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -32,7 +32,13 @@ import android.view.InputDevice; import android.view.KeyEvent; import android.annotation.TargetApi; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.NotificationChannel; +import android.app.PendingIntent; import android.app.Service; + import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; @@ -63,6 +69,7 @@ public class EmacsService extends Service public static final String TAG = "EmacsService"; public static final int MAX_PENDING_REQUESTS = 256; public static volatile EmacsService SERVICE; + public static boolean needDashQ; private EmacsThread thread; private Handler handler; @@ -74,6 +81,31 @@ public class EmacsService extends Service public int onStartCommand (Intent intent, int flags, int startId) { + Notification notification; + NotificationManager manager; + NotificationChannel channel; + String infoBlurb; + Object tem; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) + { + tem = getSystemService (Context.NOTIFICATION_SERVICE); + manager = (NotificationManager) tem; + infoBlurb = ("See (emacs)Android Environment for more" + + " details about this notification."); + channel + = new NotificationChannel ("emacs", "Emacs persistent notification", + NotificationManager.IMPORTANCE_DEFAULT); + manager.createNotificationChannel (channel); + notification = (new Notification.Builder (this, "emacs") + .setContentTitle ("Emacs") + .setContentText (infoBlurb) + .setSmallIcon (android.R.drawable.sym_def_app_icon) + .build ()); + manager.notify (1, notification); + startForeground (1, notification); + } + return START_NOT_STICKY; } @@ -137,7 +169,7 @@ public class EmacsService extends Service this); /* Start the thread that runs Emacs. */ - thread = new EmacsThread (this); + thread = new EmacsThread (this, needDashQ); thread.start (); } catch (IOException exception) @@ -444,4 +476,32 @@ public class EmacsService extends Service } } } + + + + /* Start the Emacs service if necessary. On Android 26 and up, + start Emacs as a foreground service with a notification, to avoid + it being killed by the system. + + On older systems, simply start it as a normal background + service. */ + + public static void + startEmacsService (Context context) + { + PendingIntent intent; + + if (EmacsService.SERVICE == null) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + /* Start the Emacs service now. */ + context.startService (new Intent (context, + EmacsService.class)); + else + /* Display the permanant notification and start Emacs as a + foreground service. */ + context.startForegroundService (new Intent (context, + EmacsService.class)); + } + } }; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index f713818d4bc..2fe9e103b2b 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -32,53 +32,106 @@ import android.util.Log; public class EmacsSurfaceView extends SurfaceView { private static final String TAG = "EmacsSurfaceView"; - public Object surfaceChangeLock; private boolean created; + private EmacsView view; - public - EmacsSurfaceView (final EmacsView view) - { - super (view.getContext ()); - - surfaceChangeLock = new Object (); + /* This is the callback used on Android 8 to 25. */ - getHolder ().addCallback (new SurfaceHolder.Callback () { - @Override - public void - surfaceChanged (SurfaceHolder holder, int format, - int width, int height) + private class Callback implements SurfaceHolder.Callback + { + @Override + public void + surfaceChanged (SurfaceHolder holder, int format, + int width, int height) + { + Log.d (TAG, "surfaceChanged: " + view + ", " + view.pendingConfigure); + + /* Make sure not to swap buffers if there is pending + configuration, because otherwise the redraw callback will not + run correctly. */ + + if (view.pendingConfigure == 0) + view.swapBuffers (); + } + + @Override + public void + surfaceCreated (SurfaceHolder holder) + { + synchronized (surfaceChangeLock) { - Log.d (TAG, "surfaceChanged: " + view); - view.swapBuffers (); + Log.d (TAG, "surfaceCreated: " + view); + created = true; } - @Override - public void - surfaceCreated (SurfaceHolder holder) - { - synchronized (surfaceChangeLock) - { - Log.d (TAG, "surfaceCreated: " + view); - created = true; - } - - /* Drop the lock when doing this, or a deadlock can - result. */ - view.swapBuffers (); - } + /* Drop the lock when doing this, or a deadlock can + result. */ + view.swapBuffers (); + } - @Override - public void - surfaceDestroyed (SurfaceHolder holder) + @Override + public void + surfaceDestroyed (SurfaceHolder holder) + { + synchronized (surfaceChangeLock) { - synchronized (surfaceChangeLock) - { - Log.d (TAG, "surfaceDestroyed: " + view); - created = false; - } + Log.d (TAG, "surfaceDestroyed: " + view); + created = false; } - }); + } + } + + /* And this is the callback used on Android 26 and later. It is + used because it can tell the system when drawing completes. */ + + private class Callback2 extends Callback implements SurfaceHolder.Callback2 + { + @Override + public void + surfaceRedrawNeeded (SurfaceHolder holder) + { + /* This version is not supported. */ + return; + } + + @Override + public void + surfaceRedrawNeededAsync (SurfaceHolder holder, + Runnable drawingFinished) + { + Runnable old; + + Log.d (TAG, "surfaceRedrawNeededAsync: " + view.pendingConfigure); + + /* The system calls this function when it wants to know whether + or not Emacs is still configuring itself in response to a + resize. + + If the view did not send an outstanding ConfigureNotify + event, then call drawingFinish immediately. Else, give it to + the view to execute after drawing completes. */ + + if (view.pendingConfigure == 0) + drawingFinished.run (); + else + /* And set this runnable to run once drawing completes. */ + view.drawingFinished = drawingFinished; + } + } + + public + EmacsSurfaceView (final EmacsView view) + { + super (view.getContext ()); + + this.surfaceChangeLock = new Object (); + this.view = view; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + getHolder ().addCallback (new Callback ()); + else + getHolder ().addCallback (new Callback2 ()); } public boolean diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 0882753747f..f9bc132f354 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -23,12 +23,13 @@ import java.lang.Thread; public class EmacsThread extends Thread { - EmacsService context; + /* Whether or not Emacs should be started -Q. */ + private boolean startDashQ; public - EmacsThread (EmacsService service) + EmacsThread (EmacsService service, boolean startDashQ) { - context = service; + this.startDashQ = startDashQ; } public void @@ -36,7 +37,10 @@ public class EmacsThread extends Thread { String args[]; - args = new String[] { "libandroid-emacs.so", }; + if (!startDashQ) + args = new String[] { "libandroid-emacs.so", }; + else + args = new String[] { "libandroid-emacs.so", "-Q", }; /* Run the native code now. */ EmacsNative.initEmacs (args); diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 82f44acaebe..74bbb7b3ecc 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -19,6 +19,7 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import android.content.Context; import android.content.res.ColorStateList; import android.view.ContextMenu; @@ -27,6 +28,8 @@ import android.view.KeyEvent; import android.view.MotionEvent; import android.view.ViewGroup; +import android.view.inputmethod.InputMethodManager; + import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; @@ -86,11 +89,23 @@ public class EmacsView extends ViewGroup /* The serial of the last clip rectangle change. */ private long lastClipSerial; + /* The InputMethodManager for this view's context. */ + private InputMethodManager imManager; + + /* Runnable that will run once drawing completes. */ + public Runnable drawingFinished; + + /* Serial of the last ConfigureNotify event sent that Emacs has not + yet responded to. 0 if there is no such outstanding event. */ + public long pendingConfigure; + public EmacsView (EmacsWindow window) { super (EmacsService.SERVICE); + Object tem; + this.window = window; this.damageRegion = new Region (); this.paint = new Paint (); @@ -111,6 +126,10 @@ public class EmacsView extends ViewGroup /* Get rid of the default focus highlight. */ if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) setDefaultFocusHighlightEnabled (false); + + /* Obtain the input method manager. */ + tem = getContext ().getSystemService (Context.INPUT_METHOD_SERVICE); + imManager = (InputMethodManager) tem; } private void @@ -259,7 +278,8 @@ public class EmacsView extends ViewGroup if (changed || mustReportLayout) { mustReportLayout = false; - window.viewLayout (left, top, right, bottom); + pendingConfigure + = window.viewLayout (left, top, right, bottom); } measuredWidth = right - left; @@ -538,4 +558,37 @@ public class EmacsView extends ViewGroup Runtime.getRuntime ().gc (); } } + + public void + showOnScreenKeyboard () + { + /* Specifying no flags at all tells the system the user asked for + the input method to be displayed. */ + imManager.showSoftInput (this, 0); + } + + public void + hideOnScreenKeyboard () + { + imManager.hideSoftInputFromWindow (this.getWindowToken (), + 0); + } + + public void + windowUpdated (long serial) + { + Log.d (TAG, "windowUpdated: serial is " + serial); + + if (pendingConfigure <= serial + /* Detect wraparound. */ + || pendingConfigure - serial >= 0x7fffffff) + { + pendingConfigure = 0; + + if (drawingFinished != null) + drawingFinished.run (); + + drawingFinished = null; + } + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index c5b1522086c..8511af9193e 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -233,7 +233,7 @@ public class EmacsWindow extends EmacsHandleObject return attached; } - public void + public long viewLayout (int left, int top, int right, int bottom) { int rectWidth, rectHeight; @@ -249,10 +249,10 @@ public class EmacsWindow extends EmacsHandleObject rectWidth = right - left; rectHeight = bottom - top; - EmacsNative.sendConfigureNotify (this.handle, - System.currentTimeMillis (), - left, top, rectWidth, - rectHeight); + return EmacsNative.sendConfigureNotify (this.handle, + System.currentTimeMillis (), + left, top, rectWidth, + rectHeight); } public void @@ -589,11 +589,20 @@ public class EmacsWindow extends EmacsHandleObject EmacsActivity.invalidateFocus (); } + /* Notice that the activity has been detached or destroyed. + + ISFINISHING is set if the activity is not the main activity, or + if the activity was not destroyed in response to explicit user + action. */ + public void - onActivityDetached () + onActivityDetached (boolean isFinishing) { - /* Destroy the associated frame when the activity is detached. */ - EmacsNative.sendWindowAction (this.handle, 0); + /* Destroy the associated frame when the activity is detached in + response to explicit user action. */ + + if (isFinishing) + EmacsNative.sendWindowAction (this.handle, 0); } /* Look through the button state to determine what button EVENT was @@ -1064,4 +1073,37 @@ public class EmacsWindow extends EmacsHandleObject /* Return the resulting coordinates. */ return array; } + + public void + toggleOnScreenKeyboard (final boolean on) + { + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + if (on) + view.showOnScreenKeyboard (); + else + view.hideOnScreenKeyboard (); + } + }); + } + + /* Notice that outstanding configure events have been processed. + SERIAL is checked in the UI thread to verify that no new + configure events have been generated in the mean time. */ + + public void + windowUpdated (final long serial) + { + EmacsService.SERVICE.runOnUiThread (new Runnable () { + @Override + public void + run () + { + view.windowUpdated (serial); + } + }); + } }; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 15eb3bb65c2..510300571b8 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -134,7 +134,7 @@ public class EmacsWindowAttachmentManager } public void - removeWindowConsumer (WindowConsumer consumer) + removeWindowConsumer (WindowConsumer consumer, boolean isFinishing) { EmacsWindow window; @@ -147,7 +147,7 @@ public class EmacsWindowAttachmentManager Log.d (TAG, "removeWindowConsumer: detaching " + window); consumer.detachWindow (); - window.onActivityDetached (); + window.onActivityDetached (isFinishing); } Log.d (TAG, "removeWindowConsumer: removing " + consumer); diff --git a/lisp/frame.el b/lisp/frame.el index f21c0c369bd..f8ef17325ec 100644 --- a/lisp/frame.el +++ b/lisp/frame.el @@ -2553,6 +2553,23 @@ symbols." ((string= name "Virtual core keyboard") 'core-keyboard)))))) + +;;;; On-screen keyboard management. + +(declare-function android-toggle-on-screen-keyboard "androidfns.c") + +(defun frame-toggle-on-screen-keyboard (frame hide) + "Display or hide the on-screen keyboard. +On systems with an on-screen keyboard, display the on screen +keyboard on behalf of the frame FRAME if HIDE is nil. Else, hide +the on screen keyboard. + +FRAME must already have the input focus for this to work + reliably." + (let ((frame-type (framep-on-display frame))) + (cond ((eq frame-type 'android) + (android-toggle-on-screen-keyboard frame hide))))) + ;;;; Frame geometry values diff --git a/lisp/minibuffer.el b/lisp/minibuffer.el index 21d4607e7cf..3c42f29cd1f 100644 --- a/lisp/minibuffer.el +++ b/lisp/minibuffer.el @@ -4576,6 +4576,29 @@ is included in the return value." default))) ": ")) + +;;; On screen keyboard support. +;; Try to display the on screen keyboard whenever entering the +;; mini-buffer, and hide it whenever leaving. + +(defun minibuffer-setup-on-screen-keyboard () + "Maybe display the on-screen keyboard in the current frame. +Display the on-screen keyboard in the current frame if the +last device to have sent an input event is not a keyboard. +This is run upon minibuffer setup." + (when (not (memq (device-class last-event-frame + last-event-device) + '(keyboard core-keyboard))) + (frame-toggle-on-screen-keyboard (selected-frame) nil))) + +(defun minibuffer-exit-on-screen-keyboard () + "Hide the on-screen keyboard if it was displayed. +This is run upon minibuffer exit." + (frame-toggle-on-screen-keyboard (selected-frame) t)) + +(add-hook 'minibuffer-setup-hook #'minibuffer-setup-on-screen-keyboard) +(add-hook 'minibuffer-exit-hook #'minibuffer-exit-on-screen-keyboard) + (provide 'minibuffer) ;;; minibuffer.el ends here diff --git a/lisp/touch-screen.el b/lisp/touch-screen.el index bcc4f5e9be3..a1c9e0b4afd 100644 --- a/lisp/touch-screen.el +++ b/lisp/touch-screen.el @@ -38,6 +38,12 @@ touch point, and the initial position of the touchpoint. See `touch-screen-handle-point-update' for the meanings of the fourth element.") +(defvar touch-screen-set-point-commands '(mouse-set-point) + "List of commands known to set the point. +This is used to determine whether or not to display the on-screen +keyboard after a mouse command is executed in response to a +`touchscreen-end' event.") + (defvar touch-screen-current-timer nil "Timer used to track long-presses. This is always cleared upon any significant state change.") @@ -54,20 +60,28 @@ However, return the coordinates relative to WINDOW. If (posn-window posn) is the same as window, simply return the coordinates in POSN. Otherwise, convert them to the frame, and -then back again." - (if (eq (posn-window posn) window) +then back again. + +If WINDOW is the symbol `frame', simply convert the coordinates +to the frame that they belong in." + (if (or (eq (posn-window posn) window) + (and (eq window 'frame) + (framep (posn-window posn)))) (posn-x-y posn) (let ((xy (posn-x-y posn)) - (edges (window-inside-pixel-edges window))) + (edges (and (windowp window) + (window-inside-pixel-edges window)))) ;; Make the X and Y positions frame relative. (when (windowp (posn-window posn)) (let ((edges (window-inside-pixel-edges (posn-window posn)))) (setq xy (cons (+ (car xy) (car edges)) (+ (cdr xy) (cadr edges)))))) - ;; Make the X and Y positions window relative again. - (cons (- (car xy) (car edges)) - (- (cdr xy) (cadr edges)))))) + (if (eq window 'frame) + xy + ;; Make the X and Y positions window relative again. + (cons (- (car xy) (car edges)) + (- (cdr xy) (cadr edges))))))) (defun touch-screen-handle-scroll (dx dy) "Scroll the display assuming that a touch point has moved by DX and DY." @@ -252,7 +266,12 @@ If the fourth argument of `touch-screen-current-tool' is nil, move point to the position of POINT, selecting the window under POINT as well, and deactivate the mark; if there is a button or link at POINT, call the command bound to `mouse-2' there. -Otherwise, call the command bound to `mouse-1'." +Otherwise, call the command bound to `mouse-1'. + +If the command being executed is listed in +`touch-screen-set-point-commands' also display the on-screen +keyboard if the current buffer and the character at the new point +is not read-only." (let ((what (nth 3 touch-screen-current-tool))) (cond ((null what) (when (windowp (posn-window (cdr point))) @@ -283,9 +302,17 @@ Otherwise, call the command bound to `mouse-1'." (deactivate-mark) ;; This is necessary for following links. (goto-char (posn-point (cdr point))) + ;; Figure out if the on screen keyboard needs to be + ;; displayed. (when command (call-interactively command nil - (vector event))))))))) + (vector event)) + (when (memq command touch-screen-set-point-commands) + (if (not (or buffer-read-only + (get-text-property (point) 'read-only))) + (frame-toggle-on-screen-keyboard (selected-frame) nil) + ;; Otherwise, hide the on screen keyboard now. + (frame-toggle-on-screen-keyboard (selected-frame) t)))))))))) (defun touch-screen-handle-touch (event) "Handle a single touch EVENT, and perform associated actions. @@ -379,22 +406,34 @@ touch point with the same ID as in EVENT, call UPDATE with the touch point in event and DATA. Return nil immediately if any other kind of event is received; -otherwise, return t once the `touchscreen-end' event arrives." - (catch 'finish - (while t - (let ((new-event (read-event))) - (cond - ((eq (car-safe new-event) 'touchscreen-update) - (let ((tool (assq (caadr event) (nth 1 new-event)))) - (when (and update tool) - (funcall update new-event data)))) - ((eq (car-safe new-event) 'touchscreen-end) - (throw 'finish - ;; Now determine whether or not the `touchscreen-end' - ;; event has the same ID as EVENT. If it doesn't, - ;; then this is another touch, so return nil. - (eq (caadr event) (caadr new-event)))) - (t (throw 'finish nil))))))) +otherwise, return either t or `no-drag' once the +`touchscreen-end' event arrives; return `no-drag' returned if the +touch point in EVENT did not move significantly, and t otherwise." + (let ((return-value 'no-drag) + (start-xy (touch-screen-relative-xy (cdadr event) + 'frame))) + (catch 'finish + (while t + (let ((new-event (read-event))) + (cond + ((eq (car-safe new-event) 'touchscreen-update) + (when-let* ((tool (assq (caadr event) (nth 1 new-event))) + (xy (touch-screen-relative-xy (cdr tool) 'frame))) + (when (or (> (- (car xy) (car start-xy)) 5) + (< (- (car xy) (car start-xy)) -5) + (> (- (cdr xy) (cdr start-xy)) 5) + (< (- (cdr xy) (cdr start-xy)) -5)) + (setq return-value t)) + (when (and update tool) + (funcall update new-event data)))) + ((eq (car-safe new-event) 'touchscreen-end) + (throw 'finish + ;; Now determine whether or not the `touchscreen-end' + ;; event has the same ID as EVENT. If it doesn't, + ;; then this is another touch, so return nil. + (and (eq (caadr event) (caadr new-event)) + return-value))) + (t (throw 'finish nil)))))))) @@ -402,45 +441,8 @@ otherwise, return t once the `touchscreen-end' event arrives." (defun touch-screen-drag-mode-line-1 (event) "Internal helper for `touch-screen-drag-mode-line'. -This is called when that function determines it need not execute -any keymaps on the mode line at that particular spot." - ;; Find the window that should be dragged and the starting position. - (let* ((window (posn-window (cdadr event))) - (relative-xy (touch-screen-relative-xy - (cdadr event) window)) - (last-position (cdr relative-xy))) - (when (window-resizable window 0) - (touch-screen-track-drag - event (lambda (new-event &optional _data) - ;; Find the position of the touchpoint in NEW-EVENT. - (let* ((touchpoint (assq (caadr event) (cadr new-event))) - (new-relative-xy - (touch-screen-relative-xy (cdr touchpoint) - window)) - (position (cdr new-relative-xy)) - growth) - ;; Now set the new height of the window. - ;; If new-relative-y is above relative-xy, then - ;; make the window that much shorter. Otherwise, - ;; make it bigger. - (unless (or (zerop (setq growth (- position last-position))) - (and (> growth 0) - (< position (+ (window-pixel-top window) - (window-pixel-height window)))) - (and (< growth 0) - (> position (+ (window-pixel-top window) - (window-pixel-height window))))) - (adjust-window-trailing-edge window growth nil t)) - (setq last-position position))))))) - -(defun touch-screen-drag-mode-line (event) - "Begin dragging the mode line in response to a touch EVENT. -If EVENT lies on top of text with a mouse command bound, run -that command instead. - -Change the height of the window based on where the touch point -in EVENT moves." - (interactive "e") +This is called when that function determines that no drag really +happened. EVENT is the same as in `touch-screen-drag-mode-line'." ;; If there is an object at EVENT, then look either a keymap bound ;; to [down-mouse-1] or a command bound to [mouse-1]. Then, if a ;; keymap was found, pop it up as a menu. Otherwise, wait for a tap @@ -457,21 +459,66 @@ in EVENT moves." (keymap (lookup-key object-keymap [mode-line down-mouse-1])) (command (or (lookup-key object-keymap [mode-line mouse-1]) keymap))) - (if (or (keymapp keymap) command) - (if (keymapp keymap) - (when (touch-screen-track-tap event) - (when-let* ((command (x-popup-menu event keymap)) - (tem (lookup-key keymap - (if (consp command) - (apply #'vector command) - (vector command)) - t))) - (call-interactively tem))) - (when (and (commandp command) - (touch-screen-track-tap event)) - (call-interactively command nil - (vector (list 'mouse-1 (cdadr event)))))) - (touch-screen-drag-mode-line-1 event)))) + (when (or (keymapp keymap) command) + (if (keymapp keymap) + (when-let* ((command (x-popup-menu event keymap)) + (tem (lookup-key keymap + (if (consp command) + (apply #'vector command) + (vector command)) + t))) + (call-interactively tem)) + (when (commandp command) + (call-interactively command nil + (vector (list 'mouse-1 (cdadr event))))))))) + +(defun touch-screen-drag-mode-line (event) + "Begin dragging the mode line in response to a touch EVENT. +Change the height of the window based on where the touch point in +EVENT moves. + +If it does not actually move anywhere and the touch point is +removed, and EVENT lies on top of text with a mouse command +bound, run that command instead." + (interactive "e") + ;; Find the window that should be dragged and the starting position. + (let* ((window (posn-window (cdadr event))) + (relative-xy (touch-screen-relative-xy + (cdadr event) window)) + (last-position (cdr relative-xy))) + (when (window-resizable window 0) + (when (eq + (touch-screen-track-drag + event (lambda (new-event &optional _data) + ;; Find the position of the touchpoint in + ;; NEW-EVENT. + (let* ((touchpoint (assq (caadr event) + (cadr new-event))) + (new-relative-xy + (touch-screen-relative-xy (cdr touchpoint) + window)) + (position (cdr new-relative-xy)) + growth) + ;; Now set the new height of the window. If + ;; new-relative-y is above relative-xy, then + ;; make the window that much shorter. + ;; Otherwise, make it bigger. + (unless (or (zerop (setq growth + (- position last-position))) + (and (> growth 0) + (< position + (+ (window-pixel-top window) + (window-pixel-height window)))) + (and (< growth 0) + (> position + (+ (window-pixel-top window) + (window-pixel-height window))))) + (adjust-window-trailing-edge window growth nil t)) + (setq last-position position)))) + 'no-drag) + ;; Dragging did not actually happen, so try to run any command + ;; necessary. + (touch-screen-drag-mode-line-1 event))))) (global-set-key [mode-line touchscreen-begin] #'touch-screen-drag-mode-line) diff --git a/src/android.c b/src/android.c index eb9c404f1a3..43ac3b3f754 100644 --- a/src/android.c +++ b/src/android.c @@ -121,6 +121,8 @@ struct android_emacs_window { jclass class; jmethodID swap_buffers; + jmethodID toggle_on_screen_keyboard; + jmethodID window_updated; }; /* The asset manager being used. */ @@ -193,6 +195,10 @@ static struct android_emacs_drawable drawable_class; /* Various methods associated with the EmacsWindow class. */ static struct android_emacs_window window_class; +/* The last event serial used. This is a 32 bit value, but it is + stored in unsigned long to be consistent with X. */ +static unsigned int event_serial; + /* Event handling functions. Events are stored on a (circular) queue @@ -1435,6 +1441,9 @@ android_init_emacs_window (void) assert (window_class.c_name); FIND_METHOD (swap_buffers, "swapBuffers", "()V"); + FIND_METHOD (toggle_on_screen_keyboard, + "toggleOnScreenKeyboard", "(Z)V"); + FIND_METHOD (window_updated, "windowUpdated", "(J)V"); #undef FIND_METHOD } @@ -1497,7 +1506,7 @@ NATIVE_NAME (emacsAbort) (JNIEnv *env, jobject object) emacs_abort (); } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, jshort window, jlong time, jint x, jint y, jint width, @@ -1506,6 +1515,7 @@ NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, union android_event event; event.xconfigure.type = ANDROID_CONFIGURE_NOTIFY; + event.xconfigure.serial = ++event_serial; event.xconfigure.window = window; event.xconfigure.time = time; event.xconfigure.x = x; @@ -1514,9 +1524,10 @@ NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, event.xconfigure.height = height; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, jshort window, jlong time, jint state, jint keycode, @@ -1525,6 +1536,7 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, union android_event event; event.xkey.type = ANDROID_KEY_PRESS; + event.xkey.serial = ++event_serial; event.xkey.window = window; event.xkey.time = time; event.xkey.state = state; @@ -1532,9 +1544,10 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, event.xkey.unicode_char = unicode_char; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, jshort window, jlong time, jint state, jint keycode, @@ -1543,6 +1556,7 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, union android_event event; event.xkey.type = ANDROID_KEY_RELEASE; + event.xkey.serial = ++event_serial; event.xkey.window = window; event.xkey.time = time; event.xkey.state = state; @@ -1550,48 +1564,55 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, event.xkey.unicode_char = unicode_char; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendFocusIn) (JNIEnv *env, jobject object, jshort window, jlong time) { union android_event event; - event.xkey.type = ANDROID_FOCUS_IN; - event.xkey.window = window; - event.xkey.time = time; + event.xfocus.type = ANDROID_FOCUS_IN; + event.xfocus.serial = ++event_serial; + event.xfocus.window = window; + event.xfocus.time = time; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendFocusOut) (JNIEnv *env, jobject object, jshort window, jlong time) { union android_event event; - event.xkey.type = ANDROID_FOCUS_OUT; - event.xkey.window = window; - event.xkey.time = time; + event.xfocus.type = ANDROID_FOCUS_OUT; + event.xfocus.serial = ++event_serial; + event.xfocus.window = window; + event.xfocus.time = time; android_write_event (&event); + return ++event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendWindowAction) (JNIEnv *env, jobject object, jshort window, jint action) { union android_event event; event.xaction.type = ANDROID_WINDOW_ACTION; + event.xaction.serial = ++event_serial; event.xaction.window = window; event.xaction.action = action; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendEnterNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -1599,15 +1620,17 @@ NATIVE_NAME (sendEnterNotify) (JNIEnv *env, jobject object, union android_event event; event.xcrossing.type = ANDROID_ENTER_NOTIFY; + event.xcrossing.serial = ++event_serial; event.xcrossing.window = window; event.xcrossing.x = x; event.xcrossing.y = y; event.xcrossing.time = time; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendLeaveNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -1615,15 +1638,17 @@ NATIVE_NAME (sendLeaveNotify) (JNIEnv *env, jobject object, union android_event event; event.xcrossing.type = ANDROID_LEAVE_NOTIFY; + event.xcrossing.serial = ++event_serial; event.xcrossing.window = window; event.xcrossing.x = x; event.xcrossing.y = y; event.xcrossing.time = time; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendMotionNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -1631,15 +1656,17 @@ NATIVE_NAME (sendMotionNotify) (JNIEnv *env, jobject object, union android_event event; event.xmotion.type = ANDROID_MOTION_NOTIFY; + event.xmotion.serial = ++event_serial; event.xmotion.window = window; event.xmotion.x = x; event.xmotion.y = y; event.xmotion.time = time; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -1648,6 +1675,7 @@ NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, union android_event event; event.xbutton.type = ANDROID_BUTTON_PRESS; + event.xbutton.serial = ++event_serial; event.xbutton.window = window; event.xbutton.x = x; event.xbutton.y = y; @@ -1656,9 +1684,10 @@ NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, event.xbutton.button = button; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -1667,6 +1696,7 @@ NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, union android_event event; event.xbutton.type = ANDROID_BUTTON_RELEASE; + event.xbutton.serial = ++event_serial; event.xbutton.window = window; event.xbutton.x = x; event.xbutton.y = y; @@ -1675,9 +1705,10 @@ NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, event.xbutton.button = button; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -1685,6 +1716,7 @@ NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, union android_event event; event.touch.type = ANDROID_TOUCH_DOWN; + event.touch.serial = ++event_serial; event.touch.window = window; event.touch.x = x; event.touch.y = y; @@ -1692,9 +1724,10 @@ NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, event.touch.pointer_id = pointer_id; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -1702,6 +1735,7 @@ NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, union android_event event; event.touch.type = ANDROID_TOUCH_UP; + event.touch.serial = ++event_serial; event.touch.window = window; event.touch.x = x; event.touch.y = y; @@ -1709,9 +1743,10 @@ NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, event.touch.pointer_id = pointer_id; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -1719,6 +1754,7 @@ NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, union android_event event; event.touch.type = ANDROID_TOUCH_MOVE; + event.touch.serial = ++event_serial; event.touch.window = window; event.touch.x = x; event.touch.y = y; @@ -1726,9 +1762,10 @@ NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, event.touch.pointer_id = pointer_id; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -1737,6 +1774,7 @@ NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, union android_event event; event.wheel.type = ANDROID_WHEEL; + event.wheel.serial = ++event_serial; event.wheel.window = window; event.wheel.x = x; event.wheel.y = y; @@ -1746,43 +1784,50 @@ NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, event.wheel.y_delta = y_delta; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendIconified) (JNIEnv *env, jobject object, jshort window) { union android_event event; event.iconified.type = ANDROID_ICONIFIED; + event.iconified.serial = ++event_serial; event.iconified.window = window; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, jshort window) { union android_event event; event.iconified.type = ANDROID_DEICONIFIED; + event.iconified.serial = ++event_serial; event.iconified.window = window; android_write_event (&event); + return event_serial; } -extern JNIEXPORT void JNICALL +extern JNIEXPORT jlong JNICALL NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, jshort window, jint menu_event_id) { union android_event event; event.menu.type = ANDROID_CONTEXT_MENU; + event.menu.serial = ++event_serial; event.menu.window = window; event.menu.menu_event_id = menu_event_id; android_write_event (&event); + return event_serial; } #ifdef __clang__ @@ -3599,6 +3644,15 @@ android_translate_coordinates (android_window src, int x, ANDROID_DELETE_LOCAL_REF (coordinates); } +void +android_sync (void) +{ + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.sync); + android_exception_check (); +} + /* Low level drawing primitives. */ @@ -3795,12 +3849,45 @@ android_get_keysym_name (int keysym, char *name_return, size_t size) ANDROID_DELETE_LOCAL_REF (string); } +/* Display the on screen keyboard on window WINDOW, or hide it if SHOW + is false. Ask the system to bring up or hide the on-screen + keyboard on behalf of WINDOW. The request may be rejected by the + system, especially when the window does not have the input + focus. */ + void -android_sync (void) +android_toggle_on_screen_keyboard (android_window window, bool show) { - (*android_java_env)->CallVoidMethod (android_java_env, - emacs_service, - service_class.sync); + jobject object; + jmethodID method; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + method = window_class.toggle_on_screen_keyboard; + + /* Now display the on screen keyboard. */ + (*android_java_env)->CallVoidMethod (android_java_env, object, + method, (jboolean) show); + + /* Check for out of memory errors. */ + android_exception_check (); +} + +/* Tell the window system that all configure events sent to WINDOW + have been fully processed, and that it is now okay to display its + new contents. SERIAL is the serial of the last configure event + processed. */ + +void +android_window_updated (android_window window, unsigned long serial) +{ + jobject object; + jmethodID method; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + method = window_class.window_updated; + + (*android_java_env)->CallVoidMethod (android_java_env, object, + method, (jlong) serial); android_exception_check (); } @@ -3811,7 +3898,7 @@ android_sync (void) #undef faccessat /* Replace the system faccessat with one which understands AT_EACCESS. - Android's faccessat simply fails upon using AT_EACCESS, so repalce + Android's faccessat simply fails upon using AT_EACCESS, so replace it with zero here. This isn't caught during configuration. This replacement is only done when building for Android 17 or @@ -3829,7 +3916,7 @@ faccessat (int dirfd, const char *pathname, int mode, int flags) return real_faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); } -#endif /* __ANDROID_API__ < 16 */ +#endif /* __ANDROID_API__ >= 17 */ diff --git a/src/android.h b/src/android.h index 240bc90d831..97818ab4911 100644 --- a/src/android.h +++ b/src/android.h @@ -91,6 +91,8 @@ extern void android_exception_check (void); extern void android_get_keysym_name (int, char *, size_t); extern void android_wait_event (void); +extern void android_toggle_on_screen_keyboard (android_window, bool); +extern void android_window_updated (android_window, unsigned long); diff --git a/src/androidfns.c b/src/androidfns.c index bb37c415069..77ee2e8de44 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -2332,6 +2332,30 @@ there is no mouse. */) #endif } +DEFUN ("android-toggle-on-screen-keyboard", + Fandroid_toggle_on_screen_keyboard, + Sandroid_toggle_on_screen_keyboard, 2, 2, 0, + doc: /* Display or hide the on-screen keyboard. +If HIDE is non-nil, hide the on screen keyboard if it is currently +being displayed. Else, request that the system display it on behalf +of FRAME. This request may be rejected if FRAME does not have the +input focus. */) + (Lisp_Object frame, Lisp_Object hide) +{ +#ifndef ANDROID_STUBIFY + struct frame *f; + + f = decode_window_system_frame (frame); + + block_input (); + android_toggle_on_screen_keyboard (FRAME_ANDROID_WINDOW (f), + NILP (hide)); + unblock_input (); +#endif + + return Qnil; +} + #ifndef ANDROID_STUBIFY @@ -2781,6 +2805,7 @@ syms_of_androidfns (void) defsubr (&Sx_show_tip); defsubr (&Sx_hide_tip); defsubr (&Sandroid_detect_mouse); + defsubr (&Sandroid_toggle_on_screen_keyboard); #ifndef ANDROID_STUBIFY tip_timer = Qnil; diff --git a/src/androidgui.h b/src/androidgui.h index 1f28c18ff34..3b9a74dc0b0 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -239,6 +239,7 @@ enum android_event_type struct android_any_event { enum android_event_type type; + unsigned long serial; android_window window; }; @@ -252,6 +253,7 @@ enum android_modifier_mask struct android_key_event { enum android_event_type type; + unsigned long serial; android_window window; android_time time; unsigned int state; @@ -271,6 +273,7 @@ struct android_key_event struct android_configure_event { enum android_event_type type; + unsigned long serial; android_window window; android_time time; int x, y; @@ -280,6 +283,7 @@ struct android_configure_event struct android_focus_event { enum android_event_type type; + unsigned long serial; android_window window; android_time time; }; @@ -287,6 +291,7 @@ struct android_focus_event struct android_window_action_event { enum android_event_type type; + unsigned long serial; /* The window handle. This can be ANDROID_NONE. */ android_window window; @@ -301,6 +306,7 @@ struct android_window_action_event struct android_crossing_event { enum android_event_type type; + unsigned long serial; android_window window; int x, y; unsigned long time; @@ -309,6 +315,7 @@ struct android_crossing_event struct android_motion_event { enum android_event_type type; + unsigned long serial; android_window window; int x, y; unsigned long time; @@ -317,6 +324,7 @@ struct android_motion_event struct android_button_event { enum android_event_type type; + unsigned long serial; android_window window; int x, y; unsigned long time; @@ -329,6 +337,9 @@ struct android_touch_event /* Type of the event. */ enum android_event_type type; + /* Serial identifying the event. */ + unsigned long serial; + /* Window associated with the event. */ android_window window; @@ -347,6 +358,9 @@ struct android_wheel_event /* Type of the event. */ enum android_event_type type; + /* Serial identifying the event. */ + unsigned long serial; + /* Window associated with the event. */ android_window window; @@ -368,6 +382,9 @@ struct android_iconify_event /* Type of the event. */ enum android_event_type type; + /* Serial identifying the event. */ + unsigned long serial; + /* Window associated with the event. */ android_window window; }; @@ -377,6 +394,9 @@ struct android_menu_event /* Type of the event. */ enum android_event_type type; + /* Serial identifying the event. */ + unsigned long serial; + /* Window associated with the event. Always None. */ android_window window; diff --git a/src/androidterm.c b/src/androidterm.c index 3c16b542d91..3024890d789 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -591,7 +591,17 @@ handle_one_android_event (struct android_display_info *dpyinfo, android_clear_under_internal_border (f); SET_FRAME_GARBAGED (f); cancel_mouse_face (f); + + /* Now stash the serial of this configure event somewhere, + and call android_window_updated with it once the redraw + completes. */ + FRAME_OUTPUT_DATA (f)->last_configure_serial + = configureEvent.xconfigure.serial; } + else + /* Reply to this ConfigureNotify event immediately. */ + android_window_updated (FRAME_ANDROID_WINDOW (f), + configureEvent.xconfigure.serial); goto OTHER; @@ -1194,6 +1204,9 @@ handle_one_android_event (struct android_display_info *dpyinfo, /* Iconification. This is vastly simpler than on X. */ case ANDROID_ICONIFIED: + if (!any) + goto OTHER; + if (FRAME_ICONIFIED_P (any)) goto OTHER; @@ -1206,6 +1219,9 @@ handle_one_android_event (struct android_display_info *dpyinfo, case ANDROID_DEICONIFIED: + if (!any) + goto OTHER; + if (!FRAME_ICONIFIED_P (any)) goto OTHER; @@ -1311,6 +1327,14 @@ android_frame_up_to_date (struct frame *f) /* The frame is now complete, as its contents have been drawn. */ FRAME_ANDROID_COMPLETE_P (f) = true; + /* If there was an outstanding configure event, then tell system + that the update has finished and the new contents can now be + displayed. */ + if (FRAME_OUTPUT_DATA (f)->last_configure_serial) + android_window_updated (FRAME_ANDROID_WINDOW (f), + FRAME_OUTPUT_DATA (f)->last_configure_serial); + FRAME_OUTPUT_DATA (f)->last_configure_serial = 0; + /* Shrink the scanline buffer used by the font backend. */ sfntfont_android_shrink_scanline_buffer (); unblock_input (); diff --git a/src/androidterm.h b/src/androidterm.h index c0f862e35fb..11b24d40b73 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -241,6 +241,11 @@ struct android_output /* List of all tools (either styluses or fingers) pressed onto the frame. */ struct android_touch_point *touch_points; + + /* Event serial of the last ConfigureNotify event received that has + not yet been drawn. This is used to synchronize resize with the + window system. 0 if no such outstanding event exists. */ + unsigned long last_configure_serial; }; enum -- cgit v1.3 From 56e55a80080f78754db6f385d574d17c3631ee30 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 24 Jan 2023 17:31:16 +0800 Subject: Update Android port * INSTALL.android: Update. * build-aux/ndk-build-helper-1.mk: Fix typo. * configure.ac: Enable --with-json on Android. * cross/ndk-build/ndk-build-shared-library.mk: (NDK_CFLAGS_$(LOCAL_MODULE)): (LOCAL_MODULE_FILENAME): * cross/ndk-build/ndk-build-static-library.mk: (ALL_OBJECT_FILES$(LOCAL_MODULE)): (LOCAL_MODULE_FILENAME): Recursively resolve dependencies. * cross/ndk-build/ndk-resolve.mk: New function. * doc/emacs/android.texi (Android Startup): Document how Emacs is dumped during initial startup. * java/Makefile.in (filename): Fix build with multiple shared libraries. * java/README: Improve commentary. * java/org/gnu/emacs/EmacsApplication.java (onCreate): Look and set dump file. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function getFingerprint. * java/org/gnu/emacs/EmacsPreferencesActivity.java (onCreate): Add option to erase the dump file. * java/org/gnu/emacs/EmacsService.java (browseUrl): New function. * java/org/gnu/emacs/EmacsThread.java (run): Specify dump file if found. * lisp/loadup.el: Always dump during loadup on Android. * lisp/net/browse-url.el (browse-url--browser-defcustom-type): (browse-url-default-browser): (browse-url-default-android-browser): New browse url type. * m4/ndk-build.m4 (ndk_package_map): Map jansson to libjansson. * src/android.c (struct android_emacs_service): New method `browse_url'. (getFingerprint): New function. (android_init_emacs_service): Initialize new method. (android_browse_url): New function. * src/android.h: Update prototypes. * src/androidselect.c (Fandroid_browse_url): New function. (syms_of_androidselect): Define it. * src/emacs.c (load_pdump): Don't look in fancy places on Android. * src/pdumper.c (Fdump_emacs_portable): Allow dumping while interactive on Android. (syms_of_pdumper): New variable `pdumper-fingerprint'. * src/sfntfont-android.c (sfntfont_android_composite_bitmap): Fix unused variables. --- INSTALL.android | 37 +++++- build-aux/ndk-build-helper-1.mk | 2 +- configure.ac | 3 +- cross/ndk-build/ndk-build-shared-library.mk | 23 +++- cross/ndk-build/ndk-build-static-library.mk | 14 ++- cross/ndk-build/ndk-resolve.mk | 47 ++++++++ doc/emacs/android.texi | 45 ++++--- java/Makefile.in | 3 +- java/README | 8 +- java/org/gnu/emacs/EmacsApplication.java | 54 ++++++++- java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsPreferencesActivity.java | 27 +++++ java/org/gnu/emacs/EmacsService.java | 25 ++++ java/org/gnu/emacs/EmacsThread.java | 19 ++- lisp/loadup.el | 146 ++++++++++++++--------- lisp/net/browse-url.el | 22 ++++ m4/ndk-build.m4 | 2 +- src/android.c | 61 ++++++++++ src/android.h | 4 + src/androidselect.c | 20 ++++ src/emacs.c | 29 +++++ src/pdumper.c | 18 +++ src/sfntfont-android.c | 7 +- 23 files changed, 518 insertions(+), 102 deletions(-) create mode 100644 cross/ndk-build/ndk-resolve.mk (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/INSTALL.android b/INSTALL.android index 9abea74a55d..79366425125 100644 --- a/INSTALL.android +++ b/INSTALL.android @@ -148,6 +148,14 @@ work: (You must add LOCAL_EXPORT_CFLAGS := -I$(LOCAL_PATH) before its Android.mk includes $(BUILD_STATIC_LIBRARY)) +In addition, some Emacs dependencies provide `ndk-build' support +themselves: + + libjansson - https://github.com/akheron/jansson + (You must add LOCAL_EXPORT_INCLUDES := $(LOCAL_C_INCLUDES) before + its Android.mk includes $(BUILD_SHARED_LIBRARY), then copy + android/jansson_config.h to android/jansson_private_config.h.) + We anticipate that most untested non-trivial ndk-build dependencies will need adjustments in Emacs to work, as the Emacs build system which emulates ndk-build is in an extremely early state. @@ -173,7 +181,7 @@ one awk script in build-awx, run during configure: build-aux/ndk-module-extract.awk -six Makefiles in cross/ndk-build, +seven Makefiles in cross/ndk-build, cross/ndk-build/ndk-build-shared-library.mk cross/ndk-build/ndk-build-static-library.mk @@ -181,6 +189,7 @@ six Makefiles in cross/ndk-build, cross/ndk-build/ndk-clear-vars.mk cross/ndk-build/ndk-prebuilt-shared-library.mk cross/ndk-build/ndk-prebuilt-static-library.mk + cross/ndk-build/ndk-resolve.mk and finally, two more Makefiles in cross/ndk-build, generated by configure: @@ -373,11 +382,27 @@ them. The name under which the module is linked is the same as the Make target found on the sixth line of output from build-aux/ndk-build-helper.mk. -However, none of the Makefiles in -cross/ndk-build/ndk-build-shared-library.mk perform any kind of -dependency resolution! Instead, they only define rules to build -individual modules, leaving dependency resolution up to the Makefiles -in the `build-aux' directory. +While the rules defined by the Makefiles in cross/ndk-build do not +have their dependencies as prerequisites (with the assumption that the +ndk-build.m4 and the Makefiles in build-aux have already added all of +the necessary targets to ndk-build.mk), dependency resolution is still +performed, as the CFLAGS and includes from dependencies must be +appended to the module's CFLAGS. + +This is done by including cross/ndk-build/ndk-resolve.mk each time a +shared or static library module is going to be built. How is this +done? + +First, ndk-resolve.mk saves the LOCAL_PATH, LOCAL_STATIC_LIBRARIES, +LOCAL_SHARED_LIBRARIES, LOCAL_EXPORT_CFLAGS and +LOCAL_EXPORT_C_INCLUDES from the module. + +Next, ndk-resolve loops through the dependencies the module has +specified, appending its CFLAGS and includes to the command line for +the current module. + +Then, that process is repeated for each such dependency which has not +already been resolved, until all dependencies have been resolved. libpng is a very simple module, providing only a single shared object module. This module is named libpng_emacs.so and is eventually built diff --git a/build-aux/ndk-build-helper-1.mk b/build-aux/ndk-build-helper-1.mk index fb8e4da2f0a..d20f7d4472f 100644 --- a/build-aux/ndk-build-helper-1.mk +++ b/build-aux/ndk-build-helper-1.mk @@ -33,7 +33,7 @@ NDK_$(LOCAL_MODULE)_SHARED_LIBRARIES := $(LOCAL_SHARED_LIBRARIES) $(info Building $(build_kind)) $(info $(LOCAL_MODULE)) -$(info $(addprefix $(ANDROID_MODULE_DIRECTORY)?,$(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES$(EMACS_ABI)))) +$(info $(addprefix $(ANDROID_MODULE_DIRECTORY)/,$(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES$(EMACS_ABI)))) $(info $(foreach dir,$(LOCAL_EXPORT_C_INCLUDE_DIRS) $(LOCAL_EXPORT_C_INCLUDES),-I$(dir))) $(info $(LOCAL_EXPORT_CFLAGS)) diff --git a/configure.ac b/configure.ac index 9df4a6a073f..f3a4b4301a4 100644 --- a/configure.ac +++ b/configure.ac @@ -1045,6 +1045,7 @@ package will likely install on older systems but crash on startup.]) passthrough="$passthrough --with-png=$with_png" passthrough="$passthrough --with-webp=$with_webp" passthrough="$passthrough --with-gif=$with_gif" + passthrough="$passthrough --with-json=$with_json" AS_IF([XCONFIGURE=android ANDROID_CC="$ANDROID_CC" \ ANDROID_SDK="$android_sdk" android_abi=$android_abi \ @@ -1112,6 +1113,7 @@ if test "$ANDROID" = "yes"; then with_png=no with_webp=no with_gif=no + with_json=no fi with_xml2=no @@ -1121,7 +1123,6 @@ if test "$ANDROID" = "yes"; then with_libsystemd=no with_cairo=no with_imagemagick=no - with_json=no with_tree_sitter=no with_xft=no with_harfbuzz=no diff --git a/cross/ndk-build/ndk-build-shared-library.mk b/cross/ndk-build/ndk-build-shared-library.mk index 0d90d89e55d..c4fb523a0c1 100644 --- a/cross/ndk-build/ndk-build-shared-library.mk +++ b/cross/ndk-build/ndk-build-shared-library.mk @@ -44,11 +44,13 @@ ALL_OBJECT_FILES$(LOCAL_MODULE) += $(call objname,$(LOCAL_MODULE),$(basename $(1 endef -NDK_CFLAGS_$(LOCAL_MODULE) := $(addprefix -I,$(addprefix $(LOCAL_PATH),$(LOCAL_C_INCLUDES))) -NDK_CFLAGS_$(LOCAL_MODULE) ::= -fPIC -iquote $(LOCAL_EXPORT_CFLAGS) $(LOCAL_PATH) $(LOCAL_CFLAGS) -NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDLIBS) -NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDFLAGS) -ALL_OBJECT_FILES_$(LOCAL_MODULE) := +# Make sure to not add a prefix to local includes that already specify +# $(LOCAL_PATH). +NDK_CFLAGS_$(LOCAL_MODULE) := $(addprefix -I,$(LOCAL_C_INCLUDES)) +NDK_CFLAGS_$(LOCAL_MODULE) += -fPIC -iquote $(LOCAL_PATH) $(LOCAL_EXPORT_CFLAGS) $(LOCAL_CFLAGS) $(LOCAL_CFLAGS$(NDK_BUILD_ARCH)) +NDK_ASFLAGS_$(LOCAL_MODULE) := $(LOCAL_ASFLAGS) $(LOCAL_ASFLAGS_$(NDK_BUILD_ARCH)) +NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDLIBS) $(LOCAL_LDFLAGS) +ALL_OBJECT_FILES$(LOCAL_MODULE) := ifeq ($(NDK_BUILD_ARCH)$(NDK_ARM_MODE),armarm) NDK_CFLAGS ::= -marm @@ -74,8 +76,17 @@ endif LOCAL_MODULE_FILENAME := $(LOCAL_MODULE_FILENAME).so +# Record this module's dependencies and exported includes and CFLAGS, +# and then add that of its dependencies. + +include ndk-resolve.mk + # Then define rules to build all objects. -ALL_SOURCE_FILES = $(LOCAL_SRC_FILES) +ALL_SOURCE_FILES = $(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES_$(NDK_BUILD_ARCH)) + +# This defines all dependencies. +ALL_OBJECT_FILES$(LOCAL_MODULE) = + $(foreach source,$(ALL_SOURCE_FILES),$(eval $(call single-object-target,$(source)))) # Now define the rule to build the shared library. diff --git a/cross/ndk-build/ndk-build-static-library.mk b/cross/ndk-build/ndk-build-static-library.mk index 1423cf243cf..2eddb215429 100644 --- a/cross/ndk-build/ndk-build-static-library.mk +++ b/cross/ndk-build/ndk-build-static-library.mk @@ -43,11 +43,10 @@ endif ALL_OBJECT_FILES$(LOCAL_MODULE) += $(call objname,$(LOCAL_MODULE),$(basename $(1))) endef -NDK_CFLAGS_$(LOCAL_MODULE) := $(addprefix -I,$(addprefix $(LOCAL_PATH),$(LOCAL_C_INCLUDES))) -NDK_CFLAGS_$(LOCAL_MODULE) ::= -iquote $(LOCAL_PATH) $(LOCAL_EXPORT_CFLAGS) $(LOCAL_CFLAGS) $(LOCAL_CFLAGS_$(NDK_BUILD_ARCH)) -NDK_ASFLAGS_$(LOCAL_MODULE) ::= $(LOCAL_ASFLAGS) $(LOCAL_ASFLAGS_$(NDK_BUILD_ARCH)) -NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDLIBS) -NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDFLAGS) +NDK_CFLAGS_$(LOCAL_MODULE) := $(addprefix -I,$(LOCAL_C_INCLUDES)) +NDK_CFLAGS_$(LOCAL_MODULE) += -iquote $(LOCAL_PATH) $(LOCAL_EXPORT_CFLAGS) $(LOCAL_CFLAGS) $(LOCAL_CFLAGS_$(NDK_BUILD_ARCH)) +NDK_ASFLAGS_$(LOCAL_MODULE) := $(LOCAL_ASFLAGS) $(LOCAL_ASFLAGS_$(NDK_BUILD_ARCH)) +NDK_LDFLAGS_$(LOCAL_MODULE) := $(LOCAL_LDLIBS) $(LOCAL_LDFLAGS) ALL_OBJECT_FILES$(LOCAL_MODULE) := ifeq ($(NDK_BUILD_ARCH)$(NDK_ARM_MODE),armarm) @@ -70,6 +69,11 @@ endif LOCAL_MODULE_FILENAME := $(LOCAL_MODULE_FILENAME).a +# Record this module's dependencies and exported includes and CFLAGS, +# and then add that of its dependencies. + +include ndk-resolve.mk + # Then define rules to build all objects. ALL_SOURCE_FILES = $(LOCAL_SRC_FILES) $(LOCAL_SRC_FILES_$(NDK_BUILD_ARCH)) diff --git a/cross/ndk-build/ndk-resolve.mk b/cross/ndk-build/ndk-resolve.mk new file mode 100644 index 00000000000..29dd5c9908d --- /dev/null +++ b/cross/ndk-build/ndk-resolve.mk @@ -0,0 +1,47 @@ +# Copyright 2023 Free Software Foundation, Inc. + +# This file is part of GNU Emacs. + +# GNU Emacs is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. + +# GNU Emacs is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. + +# You should have received a copy of the GNU General Public License +# along with GNU Emacs. If not, see . + +# ndk-build works by including a bunch of Makefiles which set +# variables, and then having those Makefiles include another makefile +# which actually builds targets. + +# Save information. +NDK_LOCAL_PATH_$(LOCAL_MODULE) := $(LOCAL_PATH) +NDK_LOCAL_STATIC_LIBRARIES_$(LOCAL_MODULE) := $(LOCAL_STATIC_LIBRARIES) $(LOCAL_WHOLE_STATIC_LIBRARIES) +NDK_LOCAL_SHARED_LIBRARIES_$(LOCAL_MODULE) := $(LOCAL_SHARED_LIBRARIES) +NDK_LOCAL_EXPORT_CFLAGS_$(LOCAL_MODULE) := $(LOCAL_EXPORT_CFLAGS) +NDK_LOCAL_EXPORT_C_INCLUDES_$(LOCAL_MODULE) := $(LOCAL_EXPORT_C_INCLUDES) + +# List of all dependencies resolved for this module thus far. +# Used to avoid infinite recursion. +NDK_RESOLVED_$(LOCAL_MODULE) := + +define ndk-resolve + +ifeq ($(patsubst $(1),,$(NDK_RESOLVED$(LOCAL_MODULE))),$(NDK_RESOLVED$(LOCAL_MODULE))) +NDK_RESOLVED$(LOCAL_MODULE) += $(1) +NDK_CFLAGS_$(LOCAL_MODULE) += $(NDK_LOCAL_EXPORT_CFLAGS_$(1)) +NDK_CFLAGS_$(LOCAL_MODULE) += $(addprefix -I,$(NDK_LOCAL_EXPORT_C_INCLUDES_$(1))) + +$$(foreach module,$$(NDK_LOCAL_STATIC_LIBRARIES_$(1)),$$(eval $$(call ndk-resolve,$$(module)))) +$$(foreach module,$$(NDK_LOCAL_SHARED_LIBRARIES_$(1)),$$(eval $$(call ndk-resolve,$$(module)))) +endif + +endef + +$(foreach module,$(LOCAL_SHARED_LIBRARIES),$(eval $(call ndk-resolve,$(module)))) +$(foreach module,$(LOCAL_STATIC_LIBRARIES) $(LOCAL_WHOLE_STATIC_LIBRARIES),$(eval $(call ndk-resolve,$(module)))) diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index e4498da401d..cda0eadf226 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -63,18 +63,18 @@ package manager. Instead, Emacs is compiled for Android on a different operating system, with the resulting binaries packaged into an archive, that is then transferred to the device and unpacked. -After being unpacked, Emacs instructs the system to display an + After being unpacked, Emacs instructs the system to display an application icon on the desktop. Emacs then starts up once the application icon is clicked. @cindex ``adb logcat'' -During startup, Emacs will display messages in the system log buffer; -reading that buffer requires the Android Debug Bridge (@code{adb}) -utility to be installed on another computer; it cannot be read on the -computer running Android itself. + During startup, Emacs will display messages in the system log +buffer; reading that buffer requires the Android Debug Bridge +(@code{adb}) utility to be installed on another computer; it cannot be +read on the computer running Android itself. -After enabling the ``USB Debugging'' feature on the Android system, + After enabling the ``USB Debugging'' feature on the Android system, and connecting it via USB to another system with the @code{adb} utility installed, the log can be viewed by running the following command on that other system: @@ -84,14 +84,31 @@ $ adb logcat | grep -E "(android_run_debug_thread|[Ee]macs)" @end example @cindex emacs -Q, android -Since Android has no command line, there is normally no way to specify -command-line arguments. However, Emacs can be started with the -equivalent of the @code{--quick} option (@pxref{Initial Options}) -through a special preferences screen, which can be accessed through -the Emacs ``app info'' page in the system settings application. - -Consult the manufacturer of your device for more details, as how to do -this varies by device. + Since Android has no command line, there is normally no way to +specify command-line arguments when starting Emacs. However, Emacs +can be started with the equivalent of the @code{--quick} option +(@pxref{Initial Options}) through a special preferences screen, which +can be accessed through the Emacs ``app info'' page in the system +settings application. + + Consult the manufacturer of your device for more details, as how to +do this varies by device. + +@cindex dumping, android + The first time any given copy of Emacs starts on a device, it spends +a while loading the preloaded Lisp files which normally come with +Emacs. This produces a ``dump file'' (@pxref{Initial Options}) in the +files directory, containing an identifier unique to this copy of +Emacs. + + The next time that same copy of Emacs starts up, it simply loads the +preloaded Lisp files contained in that dump file, greatly improving +start up time. + + However, if by some unforseen circumstance the dump file is +corrupted, Emacs can crash. If that happens, the dump file stored in +the Emacs files directory can be erased through the same preferences +screen. @node Android File System @section What files Emacs can access under Android diff --git a/java/Makefile.in b/java/Makefile.in index d27775ea3db..b5e0cd7bb56 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -148,8 +148,7 @@ emacs.apk-in: $(CROSS_BINS) $(CROSS_LIBS) $(libsrc)/asset-directory-tool \ cp -f $$file install_temp/lib/$(ANDROID_ABI); \ fi \ done - $(foreach module,$(NDK_BUILD_SHARED), \ - cp -f $(module) install_temp/lib/$(ANDROID_ABI)) + cp -f $(NDK_BUILD_SHARED) install_temp/lib/$(ANDROID_ABI) # Package everything. Specifying the assets on this command line is # necessary for AAssetManager_getNextFileName to work on old versions # of Android. diff --git a/java/README b/java/README index 50c2332ce95..05edf7744de 100644 --- a/java/README +++ b/java/README @@ -1,5 +1,11 @@ This directory holds the Java sources of the port of GNU Emacs to -Android-like systems. +Android-like systems, along with files needed to create an application +package out of them. + +`emacs.keystore' is the signing key used to build Emacs. It is kept +here, and we encourage all people redistributing Emacs to use this +key. It holds no security value, and otherwise it will be impossible +to install different builds of Emacs on top of each other. Please keep the Java code indented with tabs and formatted according to the rules for C code in the GNU coding standards. Always use diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java index 125da05cfd4..87085c32d62 100644 --- a/java/org/gnu/emacs/EmacsApplication.java +++ b/java/org/gnu/emacs/EmacsApplication.java @@ -19,9 +19,59 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import java.io.File; +import java.io.FileFilter; + import android.app.Application; +import android.util.Log; -public class EmacsApplication extends Application +public class EmacsApplication extends Application implements FileFilter { - /* This class currently does nothing. */ + private static final String TAG = "EmacsApplication"; + + /* The name of the dump file to use. */ + public static String dumpFileName; + + @Override + public boolean + accept (File file) + { + return (!file.isDirectory () + && file.getName ().endsWith (".pdmp")); + } + + @Override + public void + onCreate () + { + File filesDirectory; + File[] allFiles; + String wantedDumpFile; + int i; + + wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint () + + ".pdmp"); + + Log.d (TAG, "onCreate: looking for " + wantedDumpFile); + + /* Obtain a list of all files ending with ``.pdmp''. Then, look + for a file named ``emacs-.pdmp'' and delete the + rest. */ + filesDirectory = getFilesDir (); + allFiles = filesDirectory.listFiles (this); + + /* Now try to find the right dump file. */ + for (i = 0; i < allFiles.length; ++i) + { + if (allFiles[i].getName ().equals (wantedDumpFile)) + dumpFileName = allFiles[i].getAbsolutePath (); + else + /* Delete this outdated dump file. */ + allFiles[i].delete (); + } + + Log.d (TAG, "onCreate: found " + dumpFileName); + + super.onCreate (); + } }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 962538bef7b..9636561a524 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -25,6 +25,10 @@ import android.content.res.AssetManager; public class EmacsNative { + /* Obtain the fingerprint of this build of Emacs. The fingerprint + can be used to determine the dump file name. */ + public static native String getFingerprint (); + /* Set certain parameters before initializing Emacs. This proves that libemacs.so is being loaded from Java code. diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java index 0db983984fd..ed1db68f732 100644 --- a/java/org/gnu/emacs/EmacsPreferencesActivity.java +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java @@ -19,6 +19,8 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import java.io.File; + import android.app.Activity; import android.content.Intent; import android.os.Bundle; @@ -93,6 +95,31 @@ public class EmacsPreferencesActivity extends Activity }); layout.addView (textView); + textView = new TextView (this); + textView.setPadding (8, 20, 20, 8); + + params = new LinearLayout.LayoutParams (LayoutParams.MATCH_PARENT, + LayoutParams.WRAP_CONTENT); + textView.setLayoutParams (params); + textView.setText ("Erase dump file"); + textView.setOnClickListener (new View.OnClickListener () { + @Override + public void + onClick (View view) + { + String wantedDumpFile; + File file; + + wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint () + + ".pdmp"); + file = new File (getFilesDir (), wantedDumpFile); + + if (file.exists ()) + file.delete (); + } + }); + layout.addView (textView); + super.onCreate (savedInstanceState); } }; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 95f21b211a3..4db1ea5359f 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -43,6 +43,8 @@ import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; +import android.net.Uri; + import android.os.Build; import android.os.Looper; import android.os.IBinder; @@ -504,4 +506,27 @@ public class EmacsService extends Service EmacsService.class)); } } + + /* Ask the system to open the specified URL. + Value is NULL upon success, or a string describing the error + upon failure. */ + + public String + browseUrl (String url) + { + Intent intent; + + try + { + intent = new Intent (Intent.ACTION_VIEW, Uri.parse (url)); + intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity (intent); + } + catch (Exception e) + { + return e.toString (); + } + + return null; + } }; diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 21d8612703a..5b76d11db4b 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -38,10 +38,23 @@ public class EmacsThread extends Thread { String args[]; - if (!startDashQ) - args = new String[] { "libandroid-emacs.so", }; + if (EmacsApplication.dumpFileName == null) + { + if (!startDashQ) + args = new String[] { "libandroid-emacs.so", }; + else + args = new String[] { "libandroid-emacs.so", "-Q", }; + } else - args = new String[] { "libandroid-emacs.so", "-Q", }; + { + if (!startDashQ) + args = new String[] { "libandroid-emacs.so", "--dump-file", + EmacsApplication.dumpFileName, }; + else + args = new String[] { "libandroid-emacs.so", "-Q", + "--dump-file", + EmacsApplication.dumpFileName, }; + } /* Run the native code now. */ EmacsNative.initEmacs (args); diff --git a/lisp/loadup.el b/lisp/loadup.el index b9ae8212d2d..8d2e4bbb7c2 100644 --- a/lisp/loadup.el +++ b/lisp/loadup.el @@ -550,66 +550,96 @@ lost after dumping"))) -(if dump-mode - (let ((output (cond ((equal dump-mode "pdump") "emacs.pdmp") - ((equal dump-mode "dump") "emacs") - ((equal dump-mode "bootstrap") "emacs") - ((equal dump-mode "pbootstrap") "bootstrap-emacs.pdmp") - (t (error "Unrecognized dump mode %s" dump-mode))))) - (when (and (featurep 'native-compile) - (equal dump-mode "pdump")) - ;; Don't enable this before bootstrap is completed, as the - ;; compiler infrastructure may not be usable yet. - (setq comp-enable-subr-trampolines t)) - (message "Dumping under the name %s" output) - (condition-case () - (delete-file output) - (file-error nil)) - ;; On MS-Windows, the current directory is not necessarily the - ;; same as invocation-directory. - (let (success) - (unwind-protect - (let ((tmp-dump-mode dump-mode) - (dump-mode nil) - (lexical-binding nil)) - (if (member tmp-dump-mode '("pdump" "pbootstrap")) - (dump-emacs-portable (expand-file-name output invocation-directory)) - (dump-emacs output (if (eq system-type 'ms-dos) - "temacs.exe" - "temacs")) - (message "%d pure bytes used" pure-bytes-used)) - (setq success t)) - (unless success - (ignore-errors - (delete-file output))))) - ;; Recompute NAME now, so that it isn't set when we dump. - (if (not (or (eq system-type 'ms-dos) - (eq system-type 'haiku) ;; BFS doesn't support hard links - ;; Don't bother adding another name if we're just - ;; building bootstrap-emacs. - (member dump-mode '("pbootstrap" "bootstrap")))) - (let ((name (format "emacs-%s.%d" emacs-version emacs-build-number)) - (exe (if (eq system-type 'windows-nt) ".exe" ""))) - (while (string-match "[^-+_.a-zA-Z0-9]+" name) - (setq name (concat (downcase (substring name 0 (match-beginning 0))) - "-" - (substring name (match-end 0))))) - (message "Adding name %s" (concat name exe)) - ;; When this runs on Windows, invocation-directory is not - ;; necessarily the current directory. - (add-name-to-file (expand-file-name (concat "emacs" exe) - invocation-directory) - (expand-file-name (concat name exe) - invocation-directory) - t) - (when (equal dump-mode "pdump") - (message "Adding name %s" (concat name ".pdmp")) - (add-name-to-file (expand-file-name "emacs.pdmp" +(if (eq system-type 'android) + (progn + ;; Dumping Emacs on Android works slightly differently from + ;; everywhere else. The first time Emacs starts, Emacs dumps + ;; itself to "emacs-%s.pdump", and then proceeds with loadup, + ;; where %s is replaced by the dump fingerprint. + ;; EmacsApplication.java removes any pdump files with a + ;; different build fingerprint upon being created, which happens + ;; the moment the Android system starts Emacs. Then, it passes + ;; the appropriate "--dump-file" to libemacs.so as it starts. + (let ((temp-dir (getenv "TEMP")) + (dump-file-name (format "%semacs-%s.pdmp" + (file-name-as-directory "~") + pdumper-fingerprint)) + (dump-temp-file-name (format "%s~emacs-%s.pdmp" + (file-name-as-directory "~") + pdumper-fingerprint))) + (unless (pdumper-stats) + (condition-case () + (progn + (dump-emacs-portable dump-temp-file-name) + ;; Move the dumped file to the actual dump file name. + (rename-file dump-temp-file-name dump-file-name) + ;; Continue with loadup. + nil) + (error nil))))) + (if dump-mode + (let ((output (cond ((equal dump-mode "pdump") "emacs.pdmp") + ((equal dump-mode "dump") "emacs") + ((equal dump-mode "bootstrap") "emacs") + ((equal dump-mode "pbootstrap") "bootstrap-emacs.pdmp") + (t (error "Unrecognized dump mode %s" dump-mode))))) + (when (and (featurep 'native-compile) + (equal dump-mode "pdump")) + ;; Don't enable this before bootstrap is completed, as the + ;; compiler infrastructure may not be usable yet. + (setq comp-enable-subr-trampolines t)) + (message "Dumping under the name %s" output) + (condition-case () + (delete-file output) + (file-error nil)) + ;; On MS-Windows, the current directory is not necessarily the + ;; same as invocation-directory. + (let (success) + (unwind-protect + (let ((tmp-dump-mode dump-mode) + (dump-mode nil) + (lexical-binding nil)) + (if (member tmp-dump-mode '("pdump" "pbootstrap")) + (dump-emacs-portable (expand-file-name output invocation-directory)) + (dump-emacs output (if (eq system-type 'ms-dos) + "temacs.exe" + "temacs")) + (message "%d pure bytes used" pure-bytes-used)) + (setq success t)) + (unless success + (ignore-errors + (delete-file output))))) + ;; Recompute NAME now, so that it isn't set when we dump. + (if (not (or (eq system-type 'ms-dos) + (eq system-type 'haiku) ;; BFS doesn't support hard links + ;; There's no point keeping old dumps around for + ;; the binary used to build Lisp on the build + ;; machine. + (featurep 'android) + ;; Don't bother adding another name if we're just + ;; building bootstrap-emacs. + (member dump-mode '("pbootstrap" "bootstrap")))) + (let ((name (format "emacs-%s.%d" emacs-version emacs-build-number)) + (exe (if (eq system-type 'windows-nt) ".exe" ""))) + (while (string-match "[^-+_.a-zA-Z0-9]+" name) + (setq name (concat (downcase (substring name 0 (match-beginning 0))) + "-" + (substring name (match-end 0))))) + (message "Adding name %s" (concat name exe)) + ;; When this runs on Windows, invocation-directory is not + ;; necessarily the current directory. + (add-name-to-file (expand-file-name (concat "emacs" exe) invocation-directory) - (expand-file-name (concat name ".pdmp") + (expand-file-name (concat name exe) invocation-directory) - t)))) - (kill-emacs))) + t) + (when (equal dump-mode "pdump") + (message "Adding name %s" (concat name ".pdmp")) + (add-name-to-file (expand-file-name "emacs.pdmp" + invocation-directory) + (expand-file-name (concat name ".pdmp") + invocation-directory) + t)))) + (kill-emacs)))) ;; This file must be loaded each time Emacs is run from scratch, e.g., temacs. ;; So run the startup code now. First, remove `-l loadup' from args. diff --git a/lisp/net/browse-url.el b/lisp/net/browse-url.el index 0177d12f236..c2629b69d59 100644 --- a/lisp/net/browse-url.el +++ b/lisp/net/browse-url.el @@ -52,6 +52,7 @@ ;; browse-url-xdg-open freedesktop.org xdg-open ;; browse-url-kde KDE konqueror (kfm) ;; browse-url-elinks Elinks Don't know (tried with 0.12.GIT) +;; browse-url-default-android-browser Android 2.3.3 (should work on 2.2 too) ;; eww-browse-url Emacs Web Wowser ;; Browsers can cache web pages so it may be necessary to tell them to @@ -173,6 +174,9 @@ ,@(when (eq system-type 'darwin) (list '(function-item :tag "Default macOS browser" :value browse-url-default-macosx-browser))) + ,@(when (eq system-type 'android) + (list '(function-item :tag "Default Android browser" + :value browse-url-default-android-browser))) (function-item :tag "Default browser" :value browse-url-default-browser) (function :tag "Your own function") @@ -1057,6 +1061,8 @@ instead of `browse-url-new-window-flag'." 'browse-url-default-macosx-browser) ((featurep 'haiku) 'browse-url-default-haiku-browser) + ((eq system-type 'android) + 'browse-url-default-android-browser) ((browse-url-can-use-xdg-open) 'browse-url-xdg-open) ;;; ((executable-find browse-url-gnome-moz-program) 'browse-url-gnome-moz) ((executable-find browse-url-firefox-program) 'browse-url-firefox) @@ -1294,6 +1300,22 @@ Default to the URL around or before point." (function-put 'browse-url-default-haiku-browser 'browse-url-browser-kind 'external) +(declare-function android-browse-url "androidselect.c") + +;;;###autoload +(defun browse-url-default-android-browser (url &optional _new-window) + "Browse URL with the system default browser. +Default to the URL around or before point." + (interactive (browse-url-interactive-arg "URL: ")) + (setq url (browse-url-encode-url url)) + ;; Make sure the URL starts with an appropriate scheme. + (unless (string-match "\\(.+\\):/" url) + (setq url (concat "http://" url))) + (android-browse-url url)) + +(function-put 'browse-url-default-android-browser + 'browse-url-browser-kind 'external) + ;;;###autoload (defun browse-url-emacs (url &optional same-window) "Ask Emacs to load URL into a buffer and show it in another window. diff --git a/m4/ndk-build.m4 b/m4/ndk-build.m4 index a35900884d9..d3614a9f490 100644 --- a/m4/ndk-build.m4 +++ b/m4/ndk-build.m4 @@ -67,7 +67,7 @@ esac # This is a map between pkg-config style package names and Android # ones. -ndk_package_map="libwebpdemux:webpdemux libxml-2.0:libxml2" +ndk_package_map="libwebpdemux:webpdemux libxml-2.0:libxml2 jansson:libjansson" # Replace ndk_module with the appropriate Android module name if it is # found in ndk_package_map. diff --git a/src/android.c b/src/android.c index d788c6f19b4..c186b462360 100644 --- a/src/android.c +++ b/src/android.c @@ -33,6 +33,7 @@ along with GNU Emacs. If not, see . */ #include #include +#include #include "android.h" #include "androidgui.h" @@ -95,6 +96,7 @@ struct android_emacs_service jmethodID detect_mouse; jmethodID name_keysym; jmethodID sync; + jmethodID browse_url; }; struct android_emacs_pixmap @@ -1294,6 +1296,18 @@ android_get_home_directory (void) #pragma GCC diagnostic ignored "-Wmissing-prototypes" #endif +JNIEXPORT jstring JNICALL +NATIVE_NAME (getFingerprint) (JNIEnv *env, jobject object) +{ + char buffer[sizeof fingerprint * 2 + 1]; + + memset (buffer, 0, sizeof buffer); + hexbuf_digest (buffer, (char *) fingerprint, + sizeof fingerprint); + + return (*env)->NewStringUTF (env, buffer); +} + JNIEXPORT void JNICALL NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jobject local_asset_manager, @@ -1514,6 +1528,8 @@ android_init_emacs_service (void) FIND_METHOD (detect_mouse, "detectMouse", "()Z"); FIND_METHOD (name_keysym, "nameKeysym", "(I)Ljava/lang/String;"); FIND_METHOD (sync, "sync", "()V"); + FIND_METHOD (browse_url, "browseUrl", "(Ljava/lang/String;)" + "Ljava/lang/String;"); #undef FIND_METHOD } @@ -4735,6 +4751,51 @@ android_project_image_nearest (struct android_image *image, +/* Other miscellaneous functions. */ + +/* Ask the system to start browsing the specified encoded URL. Upon + failure, return a string describing the error. Else, value is + nil. */ + +Lisp_Object +android_browse_url (Lisp_Object url) +{ + jobject value, string; + Lisp_Object tem; + const char *buffer; + + string = android_build_string (url); + value = (*android_java_env)->CallObjectMethod (android_java_env, + emacs_service, + service_class.browse_url, + string); + android_exception_check (); + + ANDROID_DELETE_LOCAL_REF (string); + + /* If no string was returned, return Qnil. */ + if (!value) + return Qnil; + + buffer = (*android_java_env)->GetStringUTFChars (android_java_env, + (jstring) value, + NULL); + android_exception_check (); + + /* Otherwise, build the string describing the error. */ + tem = build_string_from_utf8 (buffer); + + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + (jstring) value, + buffer); + + /* And return it. */ + ANDROID_DELETE_LOCAL_REF (value); + return tem; +} + + + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ diff --git a/src/android.h b/src/android.h index eee3a7a6498..ce0ccfc4338 100644 --- a/src/android.h +++ b/src/android.h @@ -107,6 +107,10 @@ extern void android_closedir (struct android_dir *); +/* Very miscellaneous functions. */ + +extern Lisp_Object android_browse_url (Lisp_Object); + #endif /* JNI functions should not be built when Emacs is stubbed out for the diff --git a/src/androidselect.c b/src/androidselect.c index 4a71b376e28..2e2f2d7e483 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -213,6 +213,25 @@ DEFUN ("android-clipboard-exists-p", Fandroid_clipboard_exists_p, return rc ? Qt : Qnil; } +DEFUN ("android-browse-url", Fandroid_browse_url, + Sandroid_browse_url, 1, 1, 0, + doc: /* Start the system web browser. +Then, point the web browser to URL, which should be a URL-encoded +URL with a scheme specified. Signal an error upon failure. */) + (Lisp_Object url) +{ + Lisp_Object value; + + CHECK_STRING (url); + value = android_browse_url (url); + + /* Signal an error upon failure. */ + if (!NILP (value)) + signal_error ("Error browsing URL", value); + + return Qnil; +} + void @@ -246,4 +265,5 @@ syms_of_androidselect (void) defsubr (&Sandroid_set_clipboard); defsubr (&Sandroid_get_clipboard); defsubr (&Sandroid_clipboard_exists_p); + defsubr (&Sandroid_browse_url); } diff --git a/src/emacs.c b/src/emacs.c index 02016e53c45..b0c19fe0070 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -744,6 +744,8 @@ argmatch (char **argv, int argc, const char *sstr, const char *lstr, } } +#if !defined HAVE_ANDROID || defined ANDROID_STUBIFY + /* Find a name (absolute or relative) of the Emacs executable whose name (as passed into this program) is ARGV0. Called early in initialization by portable dumper loading code, so avoid Lisp and @@ -843,6 +845,8 @@ find_emacs_executable (char const *argv0, ptrdiff_t *candidate_size) #endif /* !WINDOWSNT */ } +#endif + #ifdef HAVE_PDUMPER static const char * @@ -875,6 +879,30 @@ dump_error_to_string (int result) static char * load_pdump (int argc, char **argv) { +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + char *dump_file = NULL; + int skip_args = 0, result; + + while (skip_args < argc - 1) + { + if (argmatch (argv, argc, "-dump-file", "--dump-file", 6, + &dump_file, &skip_args) + || argmatch (argv, argc, "--", NULL, 2, NULL, &skip_args)) + break; + skip_args++; + } + + if (!dump_file) + return argv[0]; + + result = pdumper_load (dump_file, argv[0]); + + if (result != PDUMPER_LOAD_SUCCESS) + fatal ("could not load dump file \"%s\": %s", + dump_file, dump_error_to_string (result)); + return argv[0]; +#else + const char *const suffix = ".pdmp"; int result; char *emacs_executable = argv[0]; @@ -1067,6 +1095,7 @@ load_pdump (int argc, char **argv) xfree (dump_file); return emacs_executable; +#endif } #endif /* HAVE_PDUMPER */ diff --git a/src/pdumper.c b/src/pdumper.c index eca64725211..dbdeae9a799 100644 --- a/src/pdumper.c +++ b/src/pdumper.c @@ -4072,10 +4072,12 @@ types. */) { eassert (initialized); +#ifndef HAVE_ANDROID if (! noninteractive) error ("Dumping Emacs currently works only in batch mode. " "If you'd like it to work interactively, please consider " "contributing a patch to Emacs."); +#endif if (will_dump_with_unexec_p ()) error ("This Emacs instance was started under the assumption " @@ -5842,6 +5844,10 @@ void syms_of_pdumper (void) { #ifdef HAVE_PDUMPER + unsigned char desired[sizeof fingerprint]; + int i; + char hexbuf[2 * sizeof fingerprint]; + defsubr (&Sdump_emacs_portable); defsubr (&Sdump_emacs_portable__sort_predicate); defsubr (&Sdump_emacs_portable__sort_predicate_copied); @@ -5854,5 +5860,17 @@ syms_of_pdumper (void) DEFSYM (Qdump_file_name, "dump-file-name"); DEFSYM (Qafter_pdump_load_hook, "after-pdump-load-hook"); defsubr (&Spdumper_stats); + + for (i = 0; i < sizeof fingerprint; i++) + desired[i] = fingerprint[i]; + + hexbuf_digest (hexbuf, desired, sizeof desired); + + DEFVAR_LISP ("pdumper-fingerprint", Vpdumper_fingerprint, + doc: /* The fingerprint of this Emacs binary. +It is a string that is supposed to be unique to each build of +Emacs. */); + Vpdumper_fingerprint = make_unibyte_string ((char *) hexbuf, + sizeof hexbuf); #endif /* HAVE_PDUMPER */ } diff --git a/src/sfntfont-android.c b/src/sfntfont-android.c index 5c9835fb13e..bc8f1b35b84 100644 --- a/src/sfntfont-android.c +++ b/src/sfntfont-android.c @@ -320,7 +320,10 @@ sfntfont_android_composite_bitmap (unsigned char *restrict buffer, { unsigned int *src_row; unsigned int *dst_row; - unsigned int i, src_y, x, src_x, max_x, dst_x, lim_x; + unsigned int i, src_y, x, src_x, max_x, dst_x; +#ifdef __aarch64__ + unsigned int lim_x; +#endif if ((intptr_t) dest & 3 || bitmap_info->stride & 3) /* This shouldn't be possible as Android is supposed to align the @@ -353,10 +356,10 @@ sfntfont_android_composite_bitmap (unsigned char *restrict buffer, src_x = x + (rect->x - text_rectangle->x); dst_x = x + rect->x; +#ifdef __aarch64__ /* This is the largest value of src_x. */ lim_x = max_x + (rect->x - text_rectangle->x); -#ifdef __aarch64__ if (!sfntfont_android_over_8888 (src_row + src_x, dst_row + dst_x, src_row + lim_x, -- cgit v1.3 From 0900bfbcc57c555909cb75c38eb0ed26fb6964ef Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 25 Jan 2023 18:44:47 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Startup, Android Environment): Document that restrictions on starting Emacs have been lifted. * java/README: Document Java for Emacs developers and how the Android port works. * java/org/gnu/emacs/EmacsApplication.java (EmacsApplication) (findDumpFile): New function. (onCreate): Factor out dump file finding functions to there. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Update function declarations. * java/org/gnu/emacs/EmacsNoninteractive.java (EmacsNoninteractive): New class. * java/org/gnu/emacs/EmacsService.java (EmacsService, getApkFile) (onCreate): Pass classpath to setEmacsParams. * java/org/gnu/emacs/EmacsThread.java (EmacsThread): Make run an override. * lisp/loadup.el: Don't dump on Android when noninteractive. * lisp/shell.el (shell--command-completion-data): Handle inaccessible directories. * src/Makefile.in (android-emacs): Link with gnulib. * src/android-emacs.c (main): Implement to launch app-process and then EmacsNoninteractive. * src/android.c (setEmacsParams): New argument `class_path'. Don't set stuff up when running noninteractive. * src/android.h (initEmacs): Likewise. * src/androidfont.c (init_androidfont): * src/androidselect.c (init_androidselect): Don't initialize when running noninteractive. * src/emacs.c (load_pdump): New argument `dump_file'. (android_emacs_init): Give new argument `dump_file' to `load_pdump'. * src/sfntfont-android.c (init_sfntfont_android): Don't initialize when running noninteractive. --- doc/emacs/android.texi | 18 +- java/README | 824 ++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsApplication.java | 39 +- java/org/gnu/emacs/EmacsNative.java | 16 +- java/org/gnu/emacs/EmacsNoninteractive.java | 164 ++++++ java/org/gnu/emacs/EmacsService.java | 48 +- java/org/gnu/emacs/EmacsThread.java | 22 +- lisp/loadup.el | 33 +- lisp/shell.el | 7 +- src/Makefile.in | 2 +- src/android-emacs.c | 83 ++- src/android.c | 114 +++- src/android.h | 8 +- src/androidfont.c | 3 + src/androidselect.c | 3 + src/emacs.c | 28 +- src/sfntfont-android.c | 3 + 17 files changed, 1306 insertions(+), 109 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsNoninteractive.java (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index cda0eadf226..e910e482ad8 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -102,8 +102,7 @@ files directory, containing an identifier unique to this copy of Emacs. The next time that same copy of Emacs starts up, it simply loads the -preloaded Lisp files contained in that dump file, greatly improving -start up time. +data contained in that dump file, greatly improving start up time. However, if by some unforseen circumstance the dump file is corrupted, Emacs can crash. If that happens, the dump file stored in @@ -192,12 +191,17 @@ installed. This means, instead of specifying @code{ctags} or @code{libctags.so} or @code{libemacsclient.so} on the commnd line instead when starting either of those programs in a subprocess. -@c TODO: remove this limitation. - In addition, the @file{/assets} directory containing Emacs start-up -files is inaccessible to processes not directly created by + The @file{/assets} directory containing Emacs start-up files is +supposed to be inaccessible to processes not directly created by @code{zygote}, the system service responsible for starting -applications. This makes it impossible to run Emacs in a subprocess -within itself. +applications. Since required Lisp is found in the @file{/assets} +directory, it would thus follow that it is not possible for Emacs to +start itself as a subprocess. A special binary named +@command{libandroid-emacs.so} is provided with Emacs, and does its +best to start Emacs, for the purpose of running Lisp in batch mode. +However, the approach it takes was devised by reading Android source +code, and is not sanctioned by the Android compatibility definition +documents, so your mileage may vary. @section Running Emacs in the background @cindex emacs killed, android diff --git a/java/README b/java/README index 05edf7744de..44f5a415162 100644 --- a/java/README +++ b/java/README @@ -10,3 +10,827 @@ to install different builds of Emacs on top of each other. Please keep the Java code indented with tabs and formatted according to the rules for C code in the GNU coding standards. Always use C-style comments. + +====================================================================== + +OVERVIEW OF JAVA + +Emacs developers do not know Java, and there is no reason they should +have to. Thus, the code in this directory is confined to what is +strictly necessary to support Emacs, and only uses a subset of Java +written in a way that is easily understandable to C programmers. + +Java is required because the entire Android runtime is based around +Java, and there is no way to write an Android program which runs +without Java. + +This text exists to prime other Emacs developers, already familar with +C, on the basic architecture of the Android port, and to teach them +how to read and write the Java code found in this directory. + +Java is an object oriented language with automatic memory management +compiled down to bytecode, which is then subject to interpretation by +a Java virtual machine. + +What that means, is that: + +struct emacs_window +{ + int some_fields; + int of_emacs_window; +}; + +static void +do_something_with_emacs_window (struct emacs_window *a, int n) +{ + a->some_fields = a->of_emacs_window + n; +} + +would be written: + +public class EmacsWindow +{ + public int someFields; + public int ofEmacsWindow; + + public void + doSomething (int n) + { + someFields = ofEmacsWindow + n; + } +} + +and instead of doing: + +do_something_with_emacs_window (my_window, 1); + +you say: + +myWindow.doSomething (1); + +In addition to functions associated with an object of a given class +(such as EmacsWindow), Java also has two other kinds of functions. + +The first are so-called ``static'' functions (the static means +something entirely different from what it does in C.) + +A static function, while still having to be defined within a class, +can be called without any object. Instead of the object, you write +the name of the Java class within which it is defined. For example, +the following C code: + +int +multiply_a_with_b_and_then_add_c (int a, int b, int c) +{ + return a * b + c; +} + +would be: + +public class EmacsSomething +{ + public static int + multiplyAWithBAndThenAddC (int a, int b, int c) + { + return a * b + c; + } +}; + +Then, instead of calling: + +int foo; + +foo = multiply_a_with_b_then_add_c (1, 2, 3); + +you say: + +int foo; + +foo = EmacsSomething.multiplyAWithBAndThenAddC (1, 2, 3); + +In Java, ``static'' does not mean that the function is only used +within its compilation unit! Instead, the ``private'' qualifier is +used to mean more or less the same thing: + +static void +this_procedure_is_only_used_within_this_file (void) +{ + do_something (); +} + +becomes + +public class EmacsSomething +{ + private static void + thisProcedureIsOnlyUsedWithinThisClass () + { + + } +} + +the other kind are called ``constructors''. They are functions that +must be called to allocate memory to hold a class: + +public class EmacsFoo +{ + int bar; + + public + EmacsFoo (int tokenA, int tokenB) + { + bar = tokenA + tokenB; + } +} + +now, the following statement: + +EmacsFoo foo; + +foo = new EmacsFoo (1, 2); + +becomes more or less equivalent to the following C code: + +struct emacs_foo +{ + int bar; +}; + +struct emacs_foo * +make_emacs_foo (int token_a, int token_b) +{ + struct emacs_foo *foo; + + foo = xmalloc (sizeof *foo); + foo->bar = token_a + token_b; + + return foo; +} + +/* ... */ + +struct emacs_foo *foo; + +foo = make_emacs_foo (1, 2); + +A class may have any number of constructors, or no constructors at +all, in which case the compiler inserts an empty constructor. + + + +Sometimes, you will see Java code that looks like this: + + allFiles = filesDirectory.listFiles (new FileFilter () { + @Override + public boolean + accept (File file) + { + return (!file.isDirectory () + && file.getName ().endsWith (".pdmp")); + } + }); + +This is Java's version of GCC's nested function extension. The major +difference is that the nested function may still be called even after +it goes out of scope, and always retains a reference to the class and +local variables around where it was called. + +Being an object-oriented language, Java also allows defining that a +class ``extends'' another class. The following C code: + +struct a +{ + long thirty_two; +}; + +struct b +{ + struct a a; + long long sixty_four; +}; + +extern void do_something (struct a *); + +void +my_function (struct b *b) +{ + do_something (&b->a); +} + +is roughly equivalent to the following Java code, split into two +files: + + A.java + +public class A +{ + int thirtyTwo; + + public void + doSomething () + { + etcEtcEtc (); + } +}; + + B.java + +public class B extends A +{ + long sixty_four; + + public static void + myFunction (B b) + { + b.doSomething (); + } +} + +the Java runtime has transformed the call to ``b.doSomething'' to +``((A) b).doSomething''. + +However, Java also allows overriding this behavior, by specifying the +@Override keyword: + +public class B extends A +{ + long sixty_four; + + @Override + public void + doSomething () + { + Something.doSomethingTwo (); + super.doSomething (); + } +} + +now, any call to ``doSomething'' on a ``B'' created using ``new B ()'' +will end up calling ``Something.doSomethingTwo'', before calling back +to ``A.doSomething''. This override also applies in reverse; that is +to say, even if you write: + + ((A) b).doSomething (); + +B's version of doSomething will still be called, if ``b'' was created +using ``new B ()''. + +This mechanism is used extensively throughout the Java language and +Android windowing APIs. + +Elsewhere, you will encounter Java code that defines arrays: + +public class EmacsFrobinicator +{ + public static void + emacsFrobinicate (int something) + { + int[] primesFromSomething; + + primesFromSomething = new int[numberOfPrimes]; + /* ... */ + } +} + +Java arrays are similar to C arrays in that they can not grow. But +they are very much unlike C arrays in that they are always references +(as opposed to decaying into pointers in various situations), and +contain information about their length. + +If another function named ``frobinicate1'' takes an array as an +argument, then it need not take the length of the array. + +Instead, it simply iterates over the array like so: + +int i, k; + +for (i = 0; i < array.length; ++i) + { + k = array[i]; + + Whatever.doSomethingWithK (k); + } + +The syntax used to define arrays is also slightly different. As +arrays are always references, there is no way for you to tell the +runtime to allocate an array of size N in a structure (class.) + +Instead, if you need an array of that size, you must declare a field +with the type of the array, and allocate the array inside the class's +constructor, like so: + +public class EmacsArrayContainer +{ + public int[] myArray; + + public + EmacsArrayContainer () + { + myArray = new array[10]; + } +} + +while in C, you could just have written: + +struct emacs_array_container +{ + int my_array[10]; +}; + +or, possibly even better, + +typedef int my_array[10]; + +Alas, Java has no equivalent of `typedef'. + +JAVA NATIVE INTERFACE + +Java also provides an interface for C code to interface with Java. + +C functions exported from a shared library become static Java +functions within a class, like so: + +public class EmacsNative +{ + /* Obtain the fingerprint of this build of Emacs. The fingerprint + can be used to determine the dump file name. */ + public static native String getFingerprint (); + + /* Set certain parameters before initializing Emacs. + + assetManager must be the asset manager associated with the + context that is loading Emacs. It is saved and remains for the + remainder the lifetime of the Emacs process. + + filesDir must be the package's data storage location for the + current Android user. + + libDir must be the package's data storage location for native + libraries. It is used as PATH. + + cacheDir must be the package's cache directory. It is used as + the `temporary-file-directory'. + + pixelDensityX and pixelDensityY are the DPI values that will be + used by Emacs. + + classPath must be the classpath of this app_process process, or + NULL. + + emacsService must be the EmacsService singleton, or NULL. */ + public static native void setEmacsParams (AssetManager assetManager, + String filesDir, + String libDir, + String cacheDir, + float pixelDensityX, + float pixelDensityY, + String classPath, + EmacsService emacsService); +} + +Where the corresponding C functions are located in android.c, and +loaded by the special invocation: + + static + { + System.loadLibrary ("emacs"); + }; + + +See http://docs.oracle.com/en/java/javase/19/docs/specs/jni/intro.html +for more details. + + + +OVERVIEW OF ANDROID + +When the Android system starts an application, it does not actually +call the application's ``main'' function. It may not even start the +application's process if one is already running. + +Instead, Android is organized around components. When the user opens +the ``Emacs'' icon, the Android system looks up and starts the +component associated with the ``Emacs'' icon. In this case, the +component is called an activity, and is declared in +the AndroidManifest.xml in this directory: + + + + + + + + + +This tells Android to start the activity defined in ``EmacsActivity'' +(defined in org/gnu/emacs/EmacsActivity.java), a class extending the +Android class ``Activity''. + +To do so, the Android system creates an instance of ``EmacsActivity'' +and the window system window associated with it, and eventually calls: + + Activity activity; + + activity.onCreate (...); + +But which ``onCreate'' is really called? +It is actually the ``onCreate'' defined in EmacsActivity.java, as +it overrides the ``onCreate'' defined in Android's own Activity class: + + @Override + public void + onCreate (Bundle savedInstanceState) + { + FrameLayout.LayoutParams params; + Intent intent; + +Then, this is what happens step-by-step within the ``onCreate'' +function: + + /* See if Emacs should be started with -Q. */ + intent = getIntent (); + EmacsService.needDashQ + = intent.getBooleanExtra ("org.gnu.emacs.START_DASH_Q", + false); + +Here, Emacs obtains the intent (a request to start a component) which +was used to start Emacs, and sets a special flag if it contains a +request for Emacs to start with the ``-Q'' command-line argument. + + /* Set the theme to one without a title bar. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) + setTheme (android.R.style.Theme_DeviceDefault_NoActionBar); + else + setTheme (android.R.style.Theme_NoTitleBar); + +Next, Emacs sets an appropriate theme for the activity's associated +window decorations. + + params = new FrameLayout.LayoutParams (LayoutParams.MATCH_PARENT, + LayoutParams.MATCH_PARENT); + + /* Make the frame layout. */ + layout = new FrameLayout (this); + layout.setLayoutParams (params); + + /* Set it as the content view. */ + setContentView (layout); + +Then, Emacs creates a ``FrameLayout'', a widget that holds a single +other widget, and makes it the activity's ``content view''. + +The activity itself is a ``FrameLayout'', so the ``layout parameters'' +here apply to the FrameLayout itself, and not its children. + + /* Maybe start the Emacs service if necessary. */ + EmacsService.startEmacsService (this); + +And after that, Emacs calls the static function ``startEmacsService'', +defined in the class ``EmacsService''. This starts the Emacs service +component if necessary. + + /* Add this activity to the list of available activities. */ + EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this); + + super.onCreate (savedInstanceState); + +Finally, Emacs registers that this activity is now ready to receive +top-level frames (windows) created from Lisp. + +Activities come and go, but Emacs has to stay running in the mean +time. Thus, Emacs also defines a ``service'', which is a long-running +component that the Android system allows to run in the background. + +Let us go back and review the definition of ``startEmacsService'': + + public static void + startEmacsService (Context context) + { + if (EmacsService.SERVICE == null) + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) + /* Start the Emacs service now. */ + context.startService (new Intent (context, + EmacsService.class)); + else + /* Display the permanant notification and start Emacs as a + foreground service. */ + context.startForegroundService (new Intent (context, + EmacsService.class)); + } + } + +If ``EmacsService.SERVICE'' does not yet exist, what this does is to +tell the ``context'' (the equivalent of an Xlib Display *) to start a +service defined by the class ``EmacsService''. Eventually, this +results in ``EmacsService.onCreate'' being called: + + @Override + public void + onCreate () + { + AssetManager manager; + Context app_context; + String filesDir, libDir, cacheDir, classPath; + double pixelDensityX; + double pixelDensityY; + +Here is what this function does, step-by-step: + + SERVICE = this; + +First, it sets the special static variable ``SERVICE'' to ``this'', +which is a pointer to the ``EmacsService' object that was created. + + handler = new Handler (Looper.getMainLooper ()); + +Next, it creates a ``Handler'' object for the ``main looper''. +This is a helper structure which allows executing code on the Android +user interface thread. + + manager = getAssets (); + app_context = getApplicationContext (); + metrics = getResources ().getDisplayMetrics (); + pixelDensityX = metrics.xdpi; + pixelDensityY = metrics.ydpi; + +Finally, it obtains: + + - the asset manager, which is used to retrieve assets packaged + into the Emacs application package. + + - the application context, used to obtain application specific + information. + + - the display metrics, and from them, the X and Y densities in dots + per inch. + +Then, inside a ``try'' block: + + try + { + /* Configure Emacs with the asset manager and other necessary + parameters. */ + filesDir = app_context.getFilesDir ().getCanonicalPath (); + libDir = getLibraryDirectory (); + cacheDir = app_context.getCacheDir ().getCanonicalPath (); + +It obtains the names of the Emacs home, shared library, and temporary +file directories. + + /* Now provide this application's apk file, so a recursive + invocation of app_process (through android-emacs) can + find EmacsNoninteractive. */ + classPath = getApkFile (); + +The name of the Emacs application package. + + Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir + + ", libDir = " + libDir + ", and classPath = " + classPath); + +Prints a debug message to the Android system log with this +information. + + EmacsNative.setEmacsParams (manager, filesDir, libDir, + cacheDir, (float) pixelDensityX, + (float) pixelDensityY, + classPath, this); + +And calls the native function ``setEmacsParams'' (defined in +android.c) to configure Emacs with this information. + + /* Start the thread that runs Emacs. */ + thread = new EmacsThread (this, needDashQ); + thread.start (); + +Then, it allocates an ``EmacsThread'' object, and starts that thread. +Inside that thread is where Emacs's C code runs. + + } + catch (IOException exception) + { + EmacsNative.emacsAbort (); + return; + +And here is the purpose of the ``try'' block. Functions related to +file names in Java will signal errors of various types upon failure. + +This ``catch'' block means that the Java virtual machine will abort +execution of the contents of the ``try'' block as soon as an error of +type ``IOException'' is encountered, and begin executing the contents +of the ``catch'' block. + +Any failure of that type here is a crash, and +``EmacsNative.emacsAbort'' is called to quickly abort the process to +get a useful backtrace. + } + } + +Now, let us look at the definition of the class ``EmacsThread'', found +in org/gnu/emacs/EmacsThread.java: + +public class EmacsThread extends Thread +{ + /* Whether or not Emacs should be started -Q. */ + private boolean startDashQ; + + public + EmacsThread (EmacsService service, boolean startDashQ) + { + super ("Emacs main thread"); + this.startDashQ = startDashQ; + } + + @Override + public void + run () + { + String args[]; + + if (!startDashQ) + args = new String[] { "libandroid-emacs.so", }; + else + args = new String[] { "libandroid-emacs.so", "-Q", }; + + /* Run the native code now. */ + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); + } +}; + +The class itself defines a single field, ``startDashQ'', a constructor +with an unused argument of the type ``EmacsService'' (which is useful +while debugging) and a flag ``startDashQ'', and a single function +``run'', overriding the same function in the class ``Thread''. + +When ``thread.start'' is called, the Java virtual machine creates a +new thread, and then calls the function ``run'' within that thread. + +This function then computes a suitable argument vector, and calls +``EmacsNative.initEmacs'' (defined in android.c), which then calls a +modified version of the regular Emacs ``main'' function. + +At that point, Emacs initialization proceeds as usual: +Vinitial_window_system is set, loadup.el calls `normal-top-level', +which calls `command-line', and finally +`window-system-initialization', which initializes the `android' +terminal interface as usual. + +What happens here is the same as on other platforms. Now, here is +what happens when the initial frame is created: Fx_create_frame calls +`android_create_frame_window' to create a top level window: + +static void +android_create_frame_window (struct frame *f) +{ + struct android_set_window_attributes attributes; + enum android_window_value_mask attribute_mask; + + attributes.background_pixel = FRAME_BACKGROUND_PIXEL (f); + attribute_mask = ANDROID_CW_BACK_PIXEL; + + block_input (); + FRAME_ANDROID_WINDOW (f) + = android_create_window (FRAME_DISPLAY_INFO (f)->root_window, + f->left_pos, + f->top_pos, + FRAME_PIXEL_WIDTH (f), + FRAME_PIXEL_HEIGHT (f), + attribute_mask, &attributes); + unblock_input (); +} + +This calls the function `android_create_window' with some arguments +whose meanings are identical to the arguments to `XCreateWindow'. + +Here is the definition of `android_create_window', in android.c: + +android_window +android_create_window (android_window parent, int x, int y, + int width, int height, + enum android_window_value_mask value_mask, + struct android_set_window_attributes *attrs) +{ + static jclass class; + static jmethodID constructor; + jobject object, parent_object, old; + android_window window; + android_handle prev_max_handle; + bool override_redirect; + +What does it do? First, some context: + +At any time, there can be at most 65535 Java objects referred to by +the rest of Emacs through the Java native interface. Each such object +is assigned a ``handle'' (similar to an XID on X) and given a unique +type. The function `android_resolve_handle' returns the JNI `jobject' +associated with a given handle. + + parent_object = android_resolve_handle (parent, ANDROID_HANDLE_WINDOW); + +Here, it is being used to look up the `jobject' associated with the +`parent' handle. + + prev_max_handle = max_handle; + window = android_alloc_id (); + +Next, `max_handle' is saved, and a new handle is allocated for +`window'. + + if (!window) + error ("Out of window handles!"); + +An error is signalled if Emacs runs out of available handles. + + if (!class) + { + class = (*android_java_env)->FindClass (android_java_env, + "org/gnu/emacs/EmacsWindow"); + assert (class != NULL); + +Then, if this initialization has not yet been completed, Emacs +proceeds to find the Java class named ``EmacsWindow''. + + constructor + = (*android_java_env)->GetMethodID (android_java_env, class, "", + "(SLorg/gnu/emacs/EmacsWindow;" + "IIIIZ)V"); + assert (constructor != NULL); + +And it tries to look up the constructor, which should take seven +arguments: + + S - a short. (the handle ID) + Lorg/gnu/Emacs/EmacsWindow; - an instance of the EmacsWindow + class. (the parent) + IIII - four ints. (the window geometry.) + Z - a boolean. (whether or not the + window is override-redirect; see + XChangeWindowAttributes.) + + old = class; + class = (*android_java_env)->NewGlobalRef (android_java_env, class); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (old); + +Next, it saves a global reference to the class and deletes the local +reference. Global references will never be deallocated by the Java +virtual machine as long as they still exist. + + if (!class) + memory_full (0); + } + + /* N.B. that ANDROID_CW_OVERRIDE_REDIRECT can only be set at window + creation time. */ + override_redirect = ((value_mask + & ANDROID_CW_OVERRIDE_REDIRECT) + && attrs->override_redirect); + + object = (*android_java_env)->NewObject (android_java_env, class, + constructor, (jshort) window, + parent_object, (jint) x, (jint) y, + (jint) width, (jint) height, + (jboolean) override_redirect); + +Then, it creates an instance of the ``EmacsWindow'' class with the +appropriate arguments and previously determined constructor. + + if (!object) + { + (*android_java_env)->ExceptionClear (android_java_env); + + max_handle = prev_max_handle; + memory_full (0); + +If creating the object fails, Emacs clears the ``pending exception'' +and signals that it is out of memory. + } + + android_handles[window].type = ANDROID_HANDLE_WINDOW; + android_handles[window].handle + = (*android_java_env)->NewGlobalRef (android_java_env, + object); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (object); + +Otherwise, it associates a new global reference to the object with the +handle, and deletes the local reference returned from the JNI +NewObject function. + + if (!android_handles[window].handle) + memory_full (0); + +If allocating the global reference fails, Emacs signals that it is out +of memory. + + android_change_window_attributes (window, value_mask, attrs); + return window; + +Otherwise, it applies the specified window attributes and returns the +handle of the new window. +} diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java index 87085c32d62..96328b99d1c 100644 --- a/java/org/gnu/emacs/EmacsApplication.java +++ b/java/org/gnu/emacs/EmacsApplication.java @@ -22,27 +22,20 @@ package org.gnu.emacs; import java.io.File; import java.io.FileFilter; +import android.content.Context; + import android.app.Application; import android.util.Log; -public class EmacsApplication extends Application implements FileFilter +public class EmacsApplication extends Application { private static final String TAG = "EmacsApplication"; /* The name of the dump file to use. */ public static String dumpFileName; - @Override - public boolean - accept (File file) - { - return (!file.isDirectory () - && file.getName ().endsWith (".pdmp")); - } - - @Override - public void - onCreate () + public static void + findDumpFile (Context context) { File filesDirectory; File[] allFiles; @@ -52,13 +45,19 @@ public class EmacsApplication extends Application implements FileFilter wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint () + ".pdmp"); - Log.d (TAG, "onCreate: looking for " + wantedDumpFile); - /* Obtain a list of all files ending with ``.pdmp''. Then, look for a file named ``emacs-.pdmp'' and delete the rest. */ - filesDirectory = getFilesDir (); - allFiles = filesDirectory.listFiles (this); + filesDirectory = context.getFilesDir (); + allFiles = filesDirectory.listFiles (new FileFilter () { + @Override + public boolean + accept (File file) + { + return (!file.isDirectory () + && file.getName ().endsWith (".pdmp")); + } + }); /* Now try to find the right dump file. */ for (i = 0; i < allFiles.length; ++i) @@ -69,9 +68,13 @@ public class EmacsApplication extends Application implements FileFilter /* Delete this outdated dump file. */ allFiles[i].delete (); } + } - Log.d (TAG, "onCreate: found " + dumpFileName); - + @Override + public void + onCreate () + { + findDumpFile (this); super.onCreate (); } }; diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 9636561a524..a772b965301 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -29,8 +29,7 @@ public class EmacsNative can be used to determine the dump file name. */ public static native String getFingerprint (); - /* Set certain parameters before initializing Emacs. This proves - that libemacs.so is being loaded from Java code. + /* Set certain parameters before initializing Emacs. assetManager must be the asset manager associated with the context that is loading Emacs. It is saved and remains for the @@ -48,19 +47,26 @@ public class EmacsNative pixelDensityX and pixelDensityY are the DPI values that will be used by Emacs. - emacsService must be the emacsService singleton. */ + classPath must be the classpath of this app_process process, or + NULL. + + emacsService must be the EmacsService singleton, or NULL. */ public static native void setEmacsParams (AssetManager assetManager, String filesDir, String libDir, String cacheDir, float pixelDensityX, float pixelDensityY, + String classPath, EmacsService emacsService); /* Initialize Emacs with the argument array ARGV. Each argument must contain a NULL terminated string, or else the behavior is - undefined. */ - public static native void initEmacs (String argv[]); + undefined. + + DUMPFILE is the dump file to use, or NULL if Emacs is to load + loadup.el itself. */ + public static native void initEmacs (String argv[], String dumpFile); /* Abort and generate a native core dump. */ public static native void emacsAbort (); diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java new file mode 100644 index 00000000000..a3aefee5e0b --- /dev/null +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -0,0 +1,164 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.os.Looper; +import android.os.Build; + +import android.content.Context; +import android.content.res.AssetManager; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Method; + +/* Noninteractive Emacs. + + This is the class that libandroid-emacs.so starts. + libandroid-emacs.so figures out the system classpath, then starts + dalvikvm with the framework jars. + + At that point, dalvikvm calls main, which sets up the main looper, + creates an ActivityThread and attaches it to the main thread. + + Then, it obtains an application context for the LoadedApk in the + application thread. + + Finally, it obtains the necessary context specific objects and + initializes Emacs. */ + +@SuppressWarnings ("unchecked") +public class EmacsNoninteractive +{ + private static String + getLibraryDirectory (Context context) + { + int apiLevel; + + apiLevel = Build.VERSION.SDK_INT; + + if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) + return context.getApplicationInfo().nativeLibraryDir; + else if (apiLevel >= Build.VERSION_CODES.DONUT) + return context.getApplicationInfo().dataDir + "/lib"; + + return "/data/data/" + context.getPackageName() + "/lib"; + } + + public static void + main (String[] args) + { + Object activityThread, loadedApk; + Class activityThreadClass, loadedApkClass, contextImplClass; + Class compatibilityInfoClass; + Method method; + Context context; + AssetManager assets; + String filesDir, libDir, cacheDir; + + Looper.prepare (); + context = null; + assets = null; + filesDir = libDir = cacheDir = null; + + try + { + /* Get the activity thread. */ + activityThreadClass = Class.forName ("android.app.ActivityThread"); + + /* Get the systemMain method. */ + method = activityThreadClass.getMethod ("systemMain"); + + /* Create and attach the activity thread. */ + activityThread = method.invoke (null); + + /* Now get an LoadedApk. */ + loadedApkClass = Class.forName ("android.app.LoadedApk"); + + /* Get a LoadedApk. How to do this varies by Android version. + On Android 2.3.3 and earlier, there is no + ``compatibilityInfo'' argument to getPackageInfo. */ + + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.GINGERBREAD) + { + method + = activityThreadClass.getMethod ("getPackageInfo", + String.class, + int.class); + loadedApk = method.invoke (activityThread, "org.gnu.emacs", + 0); + } + else + { + compatibilityInfoClass + = Class.forName ("android.content.res.CompatibilityInfo"); + + method + = activityThreadClass.getMethod ("getPackageInfo", + String.class, + compatibilityInfoClass, + int.class); + loadedApk = method.invoke (activityThread, "org.gnu.emacs", null, + 0); + } + + if (loadedApk == null) + throw new RuntimeException ("getPackageInfo returned NULL"); + + /* Now, get a context. */ + contextImplClass = Class.forName ("android.app.ContextImpl"); + method = contextImplClass.getDeclaredMethod ("createAppContext", + activityThreadClass, + loadedApkClass); + method.setAccessible (true); + context = (Context) method.invoke (null, activityThread, loadedApk); + + /* Don't actually start the looper or anything. Instead, obtain + an AssetManager. */ + assets = context.getAssets (); + + /* Now configure Emacs. The class path should already be set. */ + + filesDir = context.getFilesDir ().getCanonicalPath (); + libDir = getLibraryDirectory (context); + cacheDir = context.getCacheDir ().getCanonicalPath (); + } + catch (Exception e) + { + System.err.println ("Internal error: " + e); + System.err.println ("This means that the Android platform changed,"); + System.err.println ("and that Emacs needs adjustments in order to"); + System.err.println ("obtain required system internal resources."); + System.err.println ("Please report this bug to bug-gnu-emacs@gnu.org."); + + System.exit (1); + } + + EmacsNative.setEmacsParams (assets, filesDir, + libDir, cacheDir, 0.0f, + 0.0f, null, null); + + /* Now find the dump file that Emacs should use, if it has already + been dumped. */ + EmacsApplication.findDumpFile (context); + + /* Start Emacs. */ + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); + } +}; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 4db1ea5359f..91db76b08e3 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -41,6 +41,9 @@ import android.app.Service; import android.content.Context; import android.content.Intent; +import android.content.pm.ApplicationInfo; +import android.content.pm.PackageManager.ApplicationInfoFlags; +import android.content.pm.PackageManager; import android.content.res.AssetManager; import android.net.Uri; @@ -118,8 +121,38 @@ public class EmacsService extends Service return null; } + @SuppressWarnings ("deprecation") + private String + getApkFile () + { + PackageManager manager; + ApplicationInfo info; + + manager = getPackageManager (); + + try + { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) + info = manager.getApplicationInfo ("org.gnu.emacs", 0); + else + info = manager.getApplicationInfo ("org.gnu.emacs", + ApplicationInfoFlags.of (0)); + + /* Return an empty string upon failure. */ + + if (info.sourceDir != null) + return info.sourceDir; + + return ""; + } + catch (Exception e) + { + return ""; + } + } + @TargetApi (Build.VERSION_CODES.GINGERBREAD) - String + private String getLibraryDirectory () { int apiLevel; @@ -142,7 +175,7 @@ public class EmacsService extends Service { AssetManager manager; Context app_context; - String filesDir, libDir, cacheDir; + String filesDir, libDir, cacheDir, classPath; double pixelDensityX; double pixelDensityY; @@ -162,13 +195,18 @@ public class EmacsService extends Service libDir = getLibraryDirectory (); cacheDir = app_context.getCacheDir ().getCanonicalPath (); + /* Now provide this application's apk file, so a recursive + invocation of app_process (through android-emacs) can + find EmacsNoninteractive. */ + classPath = getApkFile (); + Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir - + " and libDir = " + libDir); + + ", libDir = " + libDir + ", and classPath = " + classPath); EmacsNative.setEmacsParams (manager, filesDir, libDir, cacheDir, (float) pixelDensityX, (float) pixelDensityY, - this); + classPath, this); /* Start the thread that runs Emacs. */ thread = new EmacsThread (this, needDashQ); @@ -491,8 +529,6 @@ public class EmacsService extends Service public static void startEmacsService (Context context) { - PendingIntent intent; - if (EmacsService.SERVICE == null) { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index 5b76d11db4b..f5e9d54044a 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -33,30 +33,18 @@ public class EmacsThread extends Thread this.startDashQ = startDashQ; } + @Override public void run () { String args[]; - if (EmacsApplication.dumpFileName == null) - { - if (!startDashQ) - args = new String[] { "libandroid-emacs.so", }; - else - args = new String[] { "libandroid-emacs.so", "-Q", }; - } + if (!startDashQ) + args = new String[] { "libandroid-emacs.so", }; else - { - if (!startDashQ) - args = new String[] { "libandroid-emacs.so", "--dump-file", - EmacsApplication.dumpFileName, }; - else - args = new String[] { "libandroid-emacs.so", "-Q", - "--dump-file", - EmacsApplication.dumpFileName, }; - } + args = new String[] { "libandroid-emacs.so", "-Q", }; /* Run the native code now. */ - EmacsNative.initEmacs (args); + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); } }; diff --git a/lisp/loadup.el b/lisp/loadup.el index 8d2e4bbb7c2..1747d1d960a 100644 --- a/lisp/loadup.el +++ b/lisp/loadup.el @@ -560,22 +560,23 @@ lost after dumping"))) ;; different build fingerprint upon being created, which happens ;; the moment the Android system starts Emacs. Then, it passes ;; the appropriate "--dump-file" to libemacs.so as it starts. - (let ((temp-dir (getenv "TEMP")) - (dump-file-name (format "%semacs-%s.pdmp" - (file-name-as-directory "~") - pdumper-fingerprint)) - (dump-temp-file-name (format "%s~emacs-%s.pdmp" - (file-name-as-directory "~") - pdumper-fingerprint))) - (unless (pdumper-stats) - (condition-case () - (progn - (dump-emacs-portable dump-temp-file-name) - ;; Move the dumped file to the actual dump file name. - (rename-file dump-temp-file-name dump-file-name) - ;; Continue with loadup. - nil) - (error nil))))) + (when (not noninteractive) + (let ((temp-dir (getenv "TEMP")) + (dump-file-name (format "%semacs-%s.pdmp" + (file-name-as-directory "~") + pdumper-fingerprint)) + (dump-temp-file-name (format "%s~emacs-%s.pdmp" + (file-name-as-directory "~") + pdumper-fingerprint))) + (unless (pdumper-stats) + (condition-case () + (progn + (dump-emacs-portable dump-temp-file-name) + ;; Move the dumped file to the actual dump file name. + (rename-file dump-temp-file-name dump-file-name) + ;; Continue with loadup. + nil) + (error nil)))))) (if dump-mode (let ((output (cond ((equal dump-mode "pdump") "emacs.pdmp") ((equal dump-mode "dump") "emacs") diff --git a/lisp/shell.el b/lisp/shell.el index 5cf108bfa3b..29f7d5c02d4 100644 --- a/lisp/shell.el +++ b/lisp/shell.el @@ -1332,7 +1332,12 @@ Returns t if successful." (while path-dirs (setq dir (file-name-as-directory (comint-directory (or (car path-dirs) "."))) comps-in-dir (and (file-accessible-directory-p dir) - (file-name-all-completions filenondir dir))) + (condition-case nil + (file-name-all-completions filenondir dir) + ;; Systems such as Android sometimes + ;; put inaccessible directories in + ;; PATH. + (permission-denied nil)))) ;; Go thru each completion found, to see whether it should be used. (while comps-in-dir (setq file (car comps-in-dir) diff --git a/src/Makefile.in b/src/Makefile.in index 29beb519abb..fe745770b9f 100644 --- a/src/Makefile.in +++ b/src/Makefile.in @@ -768,7 +768,7 @@ libemacs.so: $(ALLOBJS) $(LIBEGNU_ARCHIVE) $(EMACSRES) \ android-emacs: libemacs.so android-emacs.o $(AM_V_CCLD)$(CC) -o $@ $(ALL_CFLAGS) $(LDFLAGS) \ - -L. "-l:libemacs.so" android-emacs.o + $(LIBEGNU_ARCHIVE) -L. "-l:libemacs.so" android-emacs.o endif ## The following oldxmenu-related rules are only (possibly) used if diff --git a/src/android-emacs.c b/src/android-emacs.c index d4fa14e39fb..c1f2a6f43bb 100644 --- a/src/android-emacs.c +++ b/src/android-emacs.c @@ -18,13 +18,88 @@ You should have received a copy of the GNU General Public License along with GNU Emacs. If not, see . */ #include -#include "android.h" +#include +#include +#include +#include -/* android-emacs is a wrapper around libemacs. It simply calls - android_emacs_init with the argv and argc given to it. */ +/* android-emacs is a wrapper around /system/bin/app_process(64). + It invokes app_process(64) with the right class path and then + starts org.gnu.emacs.EmacsNoninteractive. + + The main function in that class tries to load an activity thread + and obtain a context and asset manager before calling + android_emacs_init, which is required for Emacs to find required + preloaded Lisp. */ int main (int argc, char **argv) { - return android_emacs_init (argc, argv); + char **args; + int i; + char *bootclasspath, *emacs_class_path; + + /* Allocate enough to hold the arguments to app_process. */ + args = alloca ((10 + argc) * sizeof *args); + + /* Clear args. */ + memset (args, 0, (10 + argc) * sizeof *args); + + /* First, figure out what program to start. */ +#if defined __x86_64__ || defined __aarch64__ + args[0] = (char *) "/system/bin/app_process64"; +#else + args[0] = (char *) "/system/bin/app_process"; +#endif + + /* Next, obtain the boot class path. */ + bootclasspath = getenv ("BOOTCLASSPATH"); + + /* And the Emacs class path. */ + emacs_class_path = getenv ("EMACS_CLASS_PATH"); + + if (!bootclasspath) + { + fprintf (stderr, "The BOOTCLASSPATH environment variable" + " is not set. As a result, Emacs does not know" + " how to start app_process.\n" + "This is likely a change in the Android platform." + " Please report this to bug-gnu-emacs@gnu.org.\n"); + return 1; + } + + if (!emacs_class_path) + { + fprintf (stderr, "EMACS_CLASS_PATH not set." + " Please make sure Emacs is being started" + " from within a running copy of Emacs.\n"); + return 1; + } + + if (asprintf (&bootclasspath, "-Djava.class.path=%s:%s", + bootclasspath, emacs_class_path) < 0) + { + perror ("asprintf"); + return 1; + } + + args[1] = bootclasspath; + args[2] = (char *) "/system/bin"; + args[3] = (char *) "--nice-name=emacs"; + args[4] = (char *) "org.gnu.emacs.EmacsNoninteractive"; + + /* Arguments from here on are passed to main in + EmacsNoninteractive.java. */ + args[5] = argv[0]; + + /* Now copy the rest of the arguments over. */ + for (i = 1; i < argc; ++i) + args[5 + i] = argv[i]; + + /* Finally, try to start the app_process. */ + execvp (args[0], args); + + /* If exit fails, return an error indication. */ + perror ("exec"); + return 1; } diff --git a/src/android.c b/src/android.c index c186b462360..8c4442e3397 100644 --- a/src/android.c +++ b/src/android.c @@ -145,6 +145,10 @@ char *android_game_path; /* The directory used to store temporary files. */ char *android_cache_dir; +/* The list of archive files within which the Java virtual macine + looks for class files. */ +char *android_class_path; + /* The display's pixel densities. */ double android_pixel_density_x, android_pixel_density_y; @@ -1315,6 +1319,7 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jobject cache_dir, jfloat pixel_density_x, jfloat pixel_density_y, + jobject class_path, jobject emacs_service_object) { int pipefd[2]; @@ -1372,19 +1377,30 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, object from being deleted. */ (*env)->NewGlobalRef (env, local_asset_manager); - /* Create a pipe and duplicate it to stdout and stderr. Next, make - a thread that prints stderr to the system log. */ + if (emacs_service_object) + { + /* Create a pipe and duplicate it to stdout and stderr. Next, + make a thread that prints stderr to the system log. - if (pipe2 (pipefd, O_CLOEXEC) < 0) - emacs_abort (); + Notice that this function is called in one of two ways. The + first is when Emacs is being started as a GUI application by + the system, and the second is when Emacs is being started by + libandroid-emacs.so as an ordinary noninteractive Emacs. - if (dup2 (pipefd[1], 2) < 0) - emacs_abort (); - close (pipefd[1]); + In the second case, stderr is usually connected to a PTY, so + this is unnecessary. */ - if (pthread_create (&thread, NULL, android_run_debug_thread, - (void *) (intptr_t) pipefd[0])) - emacs_abort (); + if (pipe2 (pipefd, O_CLOEXEC) < 0) + emacs_abort (); + + if (dup2 (pipefd[1], 2) < 0) + emacs_abort (); + close (pipefd[1]); + + if (pthread_create (&thread, NULL, android_run_debug_thread, + (void *) (intptr_t) pipefd[0])) + emacs_abort (); + } /* Now set the path to the site load directory. */ @@ -1430,6 +1446,23 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, (*env)->ReleaseStringUTFChars (env, (jstring) cache_dir, java_string); + if (class_path) + { + java_string = (*env)->GetStringUTFChars (env, (jstring) class_path, + NULL); + + if (!java_string) + emacs_abort (); + + android_class_path = strdup ((const char *) java_string); + + if (!android_files_dir) + emacs_abort (); + + (*env)->ReleaseStringUTFChars (env, (jstring) class_path, + java_string); + } + /* Calculate the site-lisp path. */ android_site_load_path = malloc (PATH_MAX + 1); @@ -1450,16 +1483,32 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, "Site-lisp directory: %s\n" "Files directory: %s\n" "Native code directory: %s\n" - "Game score path: %s\n", + "Game score path: %s\n" + "Class path: %s\n", android_site_load_path, android_files_dir, - android_lib_dir, android_game_path); + android_lib_dir, android_game_path, + (android_class_path + ? android_class_path + : "None")); + + if (android_class_path) + /* Set EMACS_CLASS_PATH to the class path where + EmacsNoninteractive can be found. */ + setenv ("EMACS_CLASS_PATH", android_class_path, 1); + + /* Set LD_LIBRARY_PATH to an appropriate value. */ + setenv ("LD_LIBRARY_PATH", android_lib_dir, 1); /* Make a reference to the Emacs service. */ - emacs_service = (*env)->NewGlobalRef (env, emacs_service_object); - if (!emacs_service) - emacs_abort (); + if (emacs_service_object) + { + emacs_service = (*env)->NewGlobalRef (env, emacs_service_object); + + if (!emacs_service) + emacs_abort (); + } /* Set up events. */ android_init_events (); @@ -1660,12 +1709,14 @@ android_init_emacs_window (void) } extern JNIEXPORT void JNICALL -NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) +NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, + jobject dump_file_object) { char **c_argv; jsize nelements, i; jobject argument; const char *c_argument; + char *dump_file; android_java_env = env; @@ -1705,9 +1756,34 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv) __android_log_print (ANDROID_LOG_WARN, __func__, "chdir: %s", strerror (errno)); - /* Initialize the Android GUI. */ - android_init_gui = true; - android_emacs_init (nelements, c_argv); + /* Initialize the Android GUI as long as the service object was + set. */ + + if (emacs_service) + android_init_gui = true; + + /* Now see if a dump file has been specified and should be used. */ + dump_file = NULL; + + if (dump_file_object) + { + c_argument + = (*env)->GetStringUTFChars (env, (jstring) dump_file_object, + NULL); + + /* Copy the Java string data once. */ + dump_file = strdup (c_argument); + + /* Release the Java string data. */ + (*env)->ReleaseStringUTFChars (env, (jstring) dump_file_object, + c_argument); + } + + /* Delete local references to objects that are no longer needed. */ + ANDROID_DELETE_LOCAL_REF (argv); + ANDROID_DELETE_LOCAL_REF (dump_file_object); + + android_emacs_init (nelements, c_argv, dump_file); /* android_emacs_init should never return. */ emacs_abort (); } diff --git a/src/android.h b/src/android.h index ce0ccfc4338..6c20995e4a1 100644 --- a/src/android.h +++ b/src/android.h @@ -38,15 +38,11 @@ along with GNU Emacs. If not, see . */ #include "lisp.h" #endif -/* This must be used in every symbol declaration to export it to the - JNI Emacs wrapper. */ -#define ANDROID_EXPORT __attribute__ ((visibility ("default"))) - -extern bool ANDROID_EXPORT android_init_gui; -extern int ANDROID_EXPORT android_emacs_init (int, char **); +extern bool android_init_gui; #ifndef ANDROID_STUBIFY +extern int android_emacs_init (int, char **, char *); extern int android_select (int, fd_set *, fd_set *, fd_set *, struct timespec *); diff --git a/src/androidfont.c b/src/androidfont.c index bec5a59ca77..55f45758352 100644 --- a/src/androidfont.c +++ b/src/androidfont.c @@ -1104,6 +1104,9 @@ syms_of_androidfont (void) void init_androidfont (void) { + if (!android_init_gui) + return; + android_init_font_driver (); android_init_font_spec (); android_init_font_metrics (); diff --git a/src/androidselect.c b/src/androidselect.c index 2e2f2d7e483..4585d64b7e8 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -240,6 +240,9 @@ init_androidselect (void) jobject tem; jmethodID make_clipboard; + if (!android_init_gui) + return; + android_init_emacs_clipboard (); make_clipboard = clipboard_class.make_clipboard; diff --git a/src/emacs.c b/src/emacs.c index b0c19fe0070..a24f9960494 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -875,19 +875,23 @@ dump_error_to_string (int result) } } -/* This function returns the Emacs executable. */ +/* This function returns the Emacs executable. DUMP_FILE is ignored + outside of Android. Otherwise, it is the name of the dump file to + use, or NULL if Emacs should look for a ``--dump-file'' argument + instead. */ + static char * -load_pdump (int argc, char **argv) +load_pdump (int argc, char **argv, char *dump_file) { #if defined HAVE_ANDROID && !defined ANDROID_STUBIFY - char *dump_file = NULL; int skip_args = 0, result; while (skip_args < argc - 1) { - if (argmatch (argv, argc, "-dump-file", "--dump-file", 6, - &dump_file, &skip_args) - || argmatch (argv, argc, "--", NULL, 2, NULL, &skip_args)) + if (argmatch (argv, argc, "-dump-file", "--dump-file", + 6, &dump_file, &skip_args) + || argmatch (argv, argc, "--", NULL, 2, NULL, + &skip_args)) break; skip_args++; } @@ -933,7 +937,7 @@ load_pdump (int argc, char **argv) /* Look for an explicitly-specified dump file. */ const char *path_exec = PATH_EXEC; - char *dump_file = NULL; + dump_file = NULL; int skip_args = 0; while (skip_args < argc - 1) { @@ -1276,7 +1280,7 @@ maybe_load_seccomp (int argc, char **argv) #if defined HAVE_ANDROID && !defined ANDROID_STUBIFY int -android_emacs_init (int argc, char **argv) +android_emacs_init (int argc, char **argv, char *dump_file) #else int main (int argc, char **argv) @@ -1286,6 +1290,12 @@ main (int argc, char **argv) for pointers. */ void *stack_bottom_variable; int old_argc; +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + char *dump_file; + + /* This is just a dummy argument used to avoid extra defines. */ + dump_file = NULL; +#endif /* First, check whether we should apply a seccomp filter. This should come at the very beginning to allow the filter to protect @@ -1415,7 +1425,7 @@ main (int argc, char **argv) #ifdef HAVE_PDUMPER if (attempt_load_pdump) - initial_emacs_executable = load_pdump (argc, argv); + initial_emacs_executable = load_pdump (argc, argv, dump_file); #else ptrdiff_t bufsize; initial_emacs_executable = find_emacs_executable (argv[0], &bufsize); diff --git a/src/sfntfont-android.c b/src/sfntfont-android.c index bc8f1b35b84..ab90a2e4ecd 100644 --- a/src/sfntfont-android.c +++ b/src/sfntfont-android.c @@ -729,6 +729,9 @@ syms_of_sfntfont_android_for_pdumper (void) void init_sfntfont_android (void) { + if (!android_init_gui) + return; + /* Make sure to pick the right Sans Serif font depending on what version of Android the device is running. */ #if HAVE_DECL_ANDROID_GET_DEVICE_API_LEVEL -- cgit v1.3 From 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 ++++++++++++++ src/alloc.c | 16 +++++ src/android-emacs.c | 94 ++++++++++++++++++++++++----- src/android.c | 14 +++++ src/callproc.c | 5 ++ 8 files changed, 186 insertions(+), 29 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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; + } }; diff --git a/src/alloc.c b/src/alloc.c index ed55ae32710..bc43f22005d 100644 --- a/src/alloc.c +++ b/src/alloc.c @@ -5662,6 +5662,22 @@ find_string_data_in_pure (const char *data, ptrdiff_t nbytes) if (pure_bytes_used_non_lisp <= nbytes) return NULL; + /* The Android GCC generates code like: + + 0xa539e755 <+52>: lea 0x430(%esp),%esi +=> 0xa539e75c <+59>: movdqa %xmm0,0x0(%ebp) + 0xa539e761 <+64>: add $0x10,%ebp + + but data is not aligned appropriately, so a GP fault results. */ + +#if defined __i386__ \ + && defined HAVE_ANDROID \ + && !defined ANDROID_STUBIFY \ + && !defined (__clang__) + if ((intptr_t) data & 15) + return NULL; +#endif + /* Set up the Boyer-Moore table. */ skip = nbytes + 1; for (i = 0; i < 256; i++) diff --git a/src/android-emacs.c b/src/android-emacs.c index c1f2a6f43bb..e64caf9a9d4 100644 --- a/src/android-emacs.c +++ b/src/android-emacs.c @@ -52,12 +52,37 @@ main (int argc, char **argv) args[0] = (char *) "/system/bin/app_process"; #endif + /* Machines with ART require the boot classpath to be manually + specified. Machines with Dalvik however refuse to do so, as they + open the jars inside the BOOTCLASSPATH environment variable at + startup, resulting in the following crash: + + W/dalvikvm( 1608): Refusing to reopen boot DEX + '/system/framework/core.jar' + W/dalvikvm( 1608): Refusing to reopen boot DEX + '/system/framework/bouncycastle.jar' + E/dalvikvm( 1608): Too many exceptions during init (failed on + 'Ljava/io/IOException;' 'Re-opening BOOTCLASSPATH DEX files is + not allowed') + E/dalvikvm( 1608): VM aborting */ + +#if HAVE_DECL_ANDROID_GET_DEVICE_API_LEVEL + if (android_get_device_api_level () < 21) + { + bootclasspath = NULL; + goto skip_setup; + } +#else + if (__ANDROID_API__ < 21) + { + bootclasspath = NULL; + goto skip_setup; + } +#endif + /* Next, obtain the boot class path. */ bootclasspath = getenv ("BOOTCLASSPATH"); - /* And the Emacs class path. */ - emacs_class_path = getenv ("EMACS_CLASS_PATH"); - if (!bootclasspath) { fprintf (stderr, "The BOOTCLASSPATH environment variable" @@ -68,6 +93,11 @@ main (int argc, char **argv) return 1; } + skip_setup: + + /* And the Emacs class path. */ + emacs_class_path = getenv ("EMACS_CLASS_PATH"); + if (!emacs_class_path) { fprintf (stderr, "EMACS_CLASS_PATH not set." @@ -76,25 +106,59 @@ main (int argc, char **argv) return 1; } - if (asprintf (&bootclasspath, "-Djava.class.path=%s:%s", - bootclasspath, emacs_class_path) < 0) + if (bootclasspath) { - perror ("asprintf"); - return 1; + if (asprintf (&bootclasspath, "-Djava.class.path=%s:%s", + bootclasspath, emacs_class_path) < 0) + { + perror ("asprintf"); + return 1; + } + } + else + { + if (asprintf (&bootclasspath, "-Djava.class.path=%s", + emacs_class_path) < 0) + { + perror ("asprintf"); + return 1; + } } args[1] = bootclasspath; args[2] = (char *) "/system/bin"; - args[3] = (char *) "--nice-name=emacs"; - args[4] = (char *) "org.gnu.emacs.EmacsNoninteractive"; - /* Arguments from here on are passed to main in - EmacsNoninteractive.java. */ - args[5] = argv[0]; +#if HAVE_DECL_ANDROID_GET_DEVICE_API_LEVEL + /* I don't know exactly when --nice-name was introduced; this is + just a guess. */ + if (android_get_device_api_level () >= 26) + { + args[3] = (char *) "--nice-name=emacs"; + args[4] = (char *) "org.gnu.emacs.EmacsNoninteractive"; + + /* Arguments from here on are passed to main in + EmacsNoninteractive.java. */ + args[5] = argv[0]; - /* Now copy the rest of the arguments over. */ - for (i = 1; i < argc; ++i) - args[5 + i] = argv[i]; + /* Now copy the rest of the arguments over. */ + for (i = 1; i < argc; ++i) + args[5 + i] = argv[i]; + } + else + { +#endif + args[3] = (char *) "org.gnu.emacs.EmacsNoninteractive"; + + /* Arguments from here on are passed to main in + EmacsNoninteractive.java. */ + args[4] = argv[0]; + + /* Now copy the rest of the arguments over. */ + for (i = 1; i < argc; ++i) + args[4 + i] = argv[i]; +#if HAVE_DECL_ANDROID_GET_DEVICE_API_LEVEL + } +#endif /* Finally, try to start the app_process. */ execvp (args[0], args); diff --git a/src/android.c b/src/android.c index 8c4442e3397..2f21a03b53f 100644 --- a/src/android.c +++ b/src/android.c @@ -205,6 +205,9 @@ static struct android_emacs_window window_class; stored in unsigned long to be consistent with X. */ static unsigned int event_serial; +/* Unused pointer used to control compiler optimizations. */ +void *unused_pointer; + /* Event handling functions. Events are stored on a (circular) queue @@ -1718,6 +1721,17 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, const char *c_argument; char *dump_file; + /* android_emacs_init is not main, so GCC is not nice enough to add + the stack alignment prologue. + + Unfortunately for us, dalvik on Android 4.0.x calls native code + with a 4 byte aligned stack. */ + + __attribute__ ((aligned (32))) char buffer[32]; + + /* Trick GCC into not optimizing this variable away. */ + unused_pointer = buffer; + android_java_env = env; nelements = (*env)->GetArrayLength (env, argv); diff --git a/src/callproc.c b/src/callproc.c index 85895a7d9f2..e15eebe23dd 100644 --- a/src/callproc.c +++ b/src/callproc.c @@ -1987,7 +1987,12 @@ init_callproc (void) dir_warning ("arch-independent data dir", Vdata_directory); sh = getenv ("SHELL"); +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + /* The Android shell is found under /system/bin, not /bin. */ + Vshell_file_name = build_string (sh ? sh : "/system/bin/sh"); +#else Vshell_file_name = build_string (sh ? sh : "/bin/sh"); +#endif Lisp_Object gamedir = Qnil; if (PATH_GAME) -- cgit v1.3 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 ++++++++++++ src/android.c | 19 +++++++++++++++++++ src/android.h | 1 + src/emacs.c | 16 +++++++++++++++- 4 files changed, 47 insertions(+), 1 deletion(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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); + } }; diff --git a/src/android.c b/src/android.c index 021bea1fc2f..598d002fb1c 100644 --- a/src/android.c +++ b/src/android.c @@ -97,6 +97,7 @@ struct android_emacs_service jmethodID name_keysym; jmethodID sync; jmethodID browse_url; + jmethodID restart_emacs; }; struct android_emacs_pixmap @@ -1659,6 +1660,7 @@ android_init_emacs_service (void) FIND_METHOD (sync, "sync", "()V"); FIND_METHOD (browse_url, "browseUrl", "(Ljava/lang/String;)" "Ljava/lang/String;"); + FIND_METHOD (restart_emacs, "restartEmacs", "()V"); #undef FIND_METHOD } @@ -4987,6 +4989,23 @@ android_browse_url (Lisp_Object url) return tem; } +/* Tell the system to restart Emacs in a short amount of time, and + then kill Emacs. Never return. This is used to implement + `restart-emacs'. */ + +_Noreturn void +android_restart_emacs (void) +{ + /* Try to call the Java side function. Normally, this should call + System.exit to terminate this process. */ + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.restart_emacs); + + /* Exit anyway, in case EmacsService did not do so. */ + exit (0); +} + #else /* ANDROID_STUBIFY */ diff --git a/src/android.h b/src/android.h index 8234dbb07c0..9b2eca807cb 100644 --- a/src/android.h +++ b/src/android.h @@ -89,6 +89,7 @@ extern void android_get_keysym_name (int, char *, size_t); extern void android_wait_event (void); extern void android_toggle_on_screen_keyboard (android_window, bool); extern void android_window_updated (android_window, unsigned long); +extern _Noreturn void android_restart_emacs (void); diff --git a/src/emacs.c b/src/emacs.c index a24f9960494..79156c63017 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -2948,7 +2948,14 @@ killed. */ #ifndef WINDOWSNT /* Do some checking before shutting down Emacs, because errors can't be meaningfully reported afterwards. */ - if (!NILP (restart)) + if (!NILP (restart) + /* Don't perform the following checks when Emacs is running as + an Android GUI application, because there the system is + relied on to restart Emacs. */ +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + && !android_init_gui +#endif + ) { /* This is very unlikely, but it's possible to execute a binary (on some systems) with no argv. */ @@ -3010,6 +3017,13 @@ killed. */ if (!NILP (restart)) { turn_on_atimers (false); +#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY + /* Re-executing the Emacs process created by the system doesn't + work. Instead, schedule a restart for a few hundered + milliseconds and exit Emacs. */ + if (android_init_gui) + android_restart_emacs (); +#endif #ifdef WINDOWSNT if (w32_reexec_emacs (initial_cmdline, initial_wd) < 0) #else -- cgit v1.3 From 420533a8f9b345699dad9eeafeb3ccecfed516b2 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sat, 4 Feb 2023 23:32:07 +0800 Subject: Add emacsclient desktop file equivalent on Android * doc/emacs/android.texi (Android File System): * java/AndroidManifest.xml.in: Update with new activity. Remove Android 10 restrictions through a special flag. * java/org/gnu/emacs/EmacsNative.java (getProcName): New function. * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity): New file. * java/org/gnu/emacs/EmacsService.java (getLibraryDirection): Remove unused annotation. * lib-src/emacsclient.c (decode_options): Set alt_display on Android. * src/android.c (android_proc_name): New function. (NATIVE_NAME): Export via JNI. --- doc/emacs/android.texi | 16 +- java/AndroidManifest.xml.in | 79 +++++++ java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsOpenActivity.java | 357 ++++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsService.java | 1 - lib-src/emacsclient.c | 2 + src/android.c | 44 ++++ 7 files changed, 490 insertions(+), 13 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsOpenActivity.java (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index cfdf77454eb..9dc6fddbb72 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -167,8 +167,8 @@ system settings. The external storage directory is found at @file{/sdcard}; the other directories are not found at any fixed location. -@cindex file system limitations, Android 10 - On Android 10 and later, the Android system restricts applications +@cindex file system limitations, Android 11 + On Android 11 and later, the Android system restricts applications from accessing files in the @file{/sdcard} directory using file-related system calls such as @code{open} and @code{readdir}. @@ -177,16 +177,8 @@ makes the system more secure. Unfortunately, it also means that Emacs cannot access files in those directories, despite holding the necessary permissions. Thankfully, the Open Handset Alliance's version of Android allows this restriction to be disabled on a -per-program basis; on Android 10, the corresponding option in the -system settings panel is: - -@indentedblock -System -> Developer Options -> App Compatibility Changes -> Emacs -> -DEFAULT_SCOPED_STORAGE -@end indentedblock - - And on Android 11 and later, the corresponding option in the systems -settings panel is: +per-program basis; the corresponding option in the system settings +panel is: @indentedblock System -> Apps -> Special App Access -> All files access -> Emacs diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in index 544c87e1f1e..923c5a005d5 100644 --- a/java/AndroidManifest.xml.in +++ b/java/AndroidManifest.xml.in @@ -24,6 +24,7 @@ along with GNU Emacs. If not, see . --> package="org.gnu.emacs" android:targetSandboxVersion="1" android:installLocation="auto" + android:requestLegacyExternalStorage="true" android:versionCode="@emacs_major_version@" android:versionName="@version@"> @@ -82,6 +83,84 @@ along with GNU Emacs. If not, see . --> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + . */ + +package org.gnu.emacs; + +/* This class makes the Emacs server work reasonably on Android. + + There is no way to make the Unix socket publicly available on + Android. + + Instead, this activity tries to connect to the Emacs server, to + make it open files the system asks Emacs to open, and to emulate + some reasonable behavior when Emacs has not yet started. + + First, Emacs registers itself as an application that can open text + and image files. + + Then, when the user is asked to open a file and selects ``Emacs'' + as the application that will open the file, the system pops up a + window, this activity, and calls the `onCreate' function. + + `onCreate' then tries very to find the file name of the file that + was selected, and give it to emacsclient. + + If emacsclient successfully opens the file, then this activity + starts EmacsActivity (to bring it on to the screen); otherwise, it + displays the output of emacsclient or any error message that occurs + and exits. */ + +import android.app.AlertDialog; +import android.app.Activity; + +import android.content.Context; +import android.content.ContentResolver; +import android.content.DialogInterface; +import android.content.Intent; + +import android.net.Uri; + +import android.os.Build; +import android.os.Bundle; +import android.os.ParcelFileDescriptor; + +import java.io.File; +import java.io.FileReader; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.UnsupportedEncodingException; + +public class EmacsOpenActivity extends Activity + implements DialogInterface.OnClickListener +{ + private class EmacsClientThread extends Thread + { + private ProcessBuilder builder; + + public + EmacsClientThread (ProcessBuilder processBuilder) + { + builder = processBuilder; + } + + @Override + public void + run () + { + Process process; + InputStream error; + String errorText; + + try + { + /* Start emacsclient. */ + process = builder.start (); + process.waitFor (); + + /* Now figure out whether or not starting the process was + successful. */ + if (process.exitValue () == 0) + finishSuccess (); + else + finishFailure ("Error opening file", null); + } + catch (IOException exception) + { + finishFailure ("Internal error", exception.toString ()); + } + catch (InterruptedException exception) + { + finishFailure ("Internal error", exception.toString ()); + } + } + } + + @Override + public void + onClick (DialogInterface dialog, int which) + { + finish (); + } + + public String + readEmacsClientLog () + { + File file, cache; + FileReader reader; + char[] buffer; + int rc; + String what; + + cache = getCacheDir (); + file = new File (cache, "emacsclient.log"); + what = ""; + + try + { + reader = new FileReader (file); + buffer = new char[2048]; + + while ((rc = reader.read (buffer, 0, 2048)) != -1) + what += String.valueOf (buffer, 0, 2048); + + reader.close (); + return what; + } + catch (IOException exception) + { + return ("Couldn't read emacsclient.log: " + + exception.toString ()); + } + } + + private void + displayFailureDialog (String title, String text) + { + AlertDialog.Builder builder; + AlertDialog dialog; + + builder = new AlertDialog.Builder (this); + dialog = builder.create (); + dialog.setTitle (title); + + if (text == null) + /* Read in emacsclient.log instead. */ + text = readEmacsClientLog (); + + dialog.setMessage (text); + dialog.setButton (DialogInterface.BUTTON_POSITIVE, "OK", this); + dialog.show (); + } + + /* Finish this activity in response to emacsclient having + successfully opened a file. + + In the main thread, close this window, and open a window + belonging to an Emacs frame. */ + + public void + finishSuccess () + { + runOnUiThread (new Runnable () { + @Override + public void + run () + { + Intent intent; + + intent = new Intent (EmacsOpenActivity.this, + EmacsActivity.class); + intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity (intent); + + EmacsOpenActivity.this.finish (); + } + }); + } + + /* Finish this activity after displaying a dialog associated with + failure to open a file. + + Use TITLE as the title of the dialog. If TEXT is non-NULL, + display that text in the dialog. Otherwise, use the contents of + emacsclient.log in the cache directory instead. */ + + public void + finishFailure (final String title, final String text) + { + runOnUiThread (new Runnable () { + @Override + public void + run () + { + displayFailureDialog (title, text); + } + }); + } + + public String + getLibraryDirectory () + { + int apiLevel; + Context context; + + context = getApplicationContext (); + apiLevel = Build.VERSION.SDK_INT; + + if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) + return context.getApplicationInfo().nativeLibraryDir; + else if (apiLevel >= Build.VERSION_CODES.DONUT) + return context.getApplicationInfo().dataDir + "/lib"; + + return "/data/data/" + context.getPackageName() + "/lib"; + } + + public void + startEmacsClient (String fileName) + { + String libDir; + ProcessBuilder builder; + Process process; + EmacsClientThread thread; + File file; + + file = new File (getCacheDir (), "emacsclient.log"); + + libDir = getLibraryDirectory (); + builder = new ProcessBuilder (libDir + "/libemacsclient.so", + fileName, "--reuse-frame", + "--timeout=10", "--no-wait"); + + /* Redirect standard error to a file so that errors can be + meaningfully reported. */ + + if (file.exists ()) + file.delete (); + + builder.redirectError (file); + + /* Track process output in a new thread, since this is the UI + thread and doing so here can cause deadlocks when EmacsService + decides to wait for something. */ + + thread = new EmacsClientThread (builder); + thread.start (); + } + + @Override + public void + onCreate (Bundle savedInstanceState) + { + String action, fileName; + Intent intent; + Uri uri; + ContentResolver resolver; + ParcelFileDescriptor fd; + byte[] names; + String errorBlurb; + + super.onCreate (savedInstanceState); + + /* Obtain the intent that started Emacs. */ + intent = getIntent (); + action = intent.getAction (); + + if (action == null) + { + finish (); + return; + } + + /* Now see if the action specified is supported by Emacs. */ + + if (action.equals ("android.intent.action.VIEW") + || action.equals ("android.intent.action.EDIT") + || action.equals ("android.intent.action.PICK")) + { + /* Obtain the URI of the action. */ + uri = intent.getData (); + + if (uri == null) + { + finish (); + return; + } + + /* Now, try to get the file name. */ + + if (uri.getScheme ().equals ("file")) + fileName = uri.getPath (); + else + { + fileName = null; + + if (uri.getScheme ().equals ("content")) + { + /* This is one of the annoying Android ``content'' + URIs. Most of the time, there is actually an + underlying file, but it cannot be found without + opening the file and doing readlink on its file + descriptor in /proc/self/fd. */ + resolver = getContentResolver (); + + try + { + fd = resolver.openFileDescriptor (uri, "r"); + names = EmacsNative.getProcName (fd.getFd ()); + fd.close (); + + /* What is the right encoding here? */ + + if (names != null) + fileName = new String (names, "UTF-8"); + } + catch (FileNotFoundException exception) + { + /* Do nothing. */ + } + catch (IOException exception) + { + /* Do nothing. */ + } + } + + if (fileName == null) + { + errorBlurb = ("The URI: " + uri + " could not be opened" + + ", as it does not encode file name inform" + + "ation."); + displayFailureDialog ("Error opening file", errorBlurb); + return; + } + } + + /* And start emacsclient. */ + startEmacsClient (fileName); + } + else + finish (); + } +} diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index d17f6d1286c..2ec2ddf9bda 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -152,7 +152,6 @@ public class EmacsService extends Service } } - @TargetApi (Build.VERSION_CODES.GINGERBREAD) private String getLibraryDirectory () { diff --git a/lib-src/emacsclient.c b/lib-src/emacsclient.c index 698bf9b50ae..a72fced1bf2 100644 --- a/lib-src/emacsclient.c +++ b/lib-src/emacsclient.c @@ -626,6 +626,8 @@ decode_options (int argc, char **argv) alt_display = "w32"; #elif defined (HAVE_HAIKU) alt_display = "be"; +#elif defined (HAVE_ANDROID) + alt_display = "android"; #endif #ifdef HAVE_PGTK diff --git a/src/android.c b/src/android.c index 57a95bcd4f9..a0e64471a05 100644 --- a/src/android.c +++ b/src/android.c @@ -1369,6 +1369,27 @@ android_get_home_directory (void) return android_files_dir; } +/* Return the name of the file behind a file descriptor FD by reading + /proc/self/fd/. Place the name in BUFFER, which should be able to + hold size bytes. Value is 0 upon success, and 1 upon failure. */ + +static int +android_proc_name (int fd, char *buffer, size_t size) +{ + char format[sizeof "/proc/self/fd/" + + INT_STRLEN_BOUND (int)]; + ssize_t read; + + sprintf (format, "/proc/self/fd/%d", fd); + read = readlink (format, buffer, size - 1); + + if (read == -1) + return 1; + + buffer[read] = '\0'; + return 0; +} + /* JNI functions called by Java. */ @@ -1598,6 +1619,29 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, now. */ } +JNIEXPORT jobject JNICALL +NATIVE_NAME (getProcName) (JNIEnv *env, jobject object, jint fd) +{ + char buffer[PATH_MAX + 1]; + size_t length; + jbyteArray array; + + if (android_proc_name (fd, buffer, PATH_MAX + 1)) + return NULL; + + /* Return a byte array, as Java strings cannot always encode file + names. */ + length = strlen (buffer); + array = (*env)->NewByteArray (env, length); + if (!array) + return NULL; + + (*env)->SetByteArrayRegion (env, array, 0, length, + (jbyte *) buffer); + + return array; +} + /* Initialize service_class, aborting if something goes wrong. */ static void -- cgit v1.3 From a158c1d5b964fda36f752998cef076d581dc4df4 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 15 Feb 2023 12:23:03 +0800 Subject: Update Android port * configure.ac (HAVE_TEXT_CONVERSION): Define on Android. * doc/emacs/input.texi (On-Screen Keyboards): Document ``text conversion'' slightly. * doc/lispref/commands.texi (Misc Events): Document new `text-conversion' event. * java/org/gnu/emacs/EmacsContextMenu.java (display): Use `syncRunnable'. * java/org/gnu/emacs/EmacsDialog.java (display): Likewise. * java/org/gnu/emacs/EmacsEditable.java: Delete file. * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): Reimplement from scratch. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): Add new functions. * java/org/gnu/emacs/EmacsService.java (EmacsService, getEmacsView) (getLocationOnScreen, sync, getClipboardManager, restartEmacs): Use syncRunnable. (syncRunnable): New function. (updateIC, resetIC): New functions. * java/org/gnu/emacs/EmacsView.java (EmacsView): New field `inputConnection' and `icMode'. (onCreateInputConnection): Update accordingly. (setICMode, getICMode): New functions. * lisp/bindings.el (global-map): Ignore text conversion events. * src/alloc.c (mark_frame): Mark text conversion data. * src/android.c (struct android_emacs_service): New fields `update_ic' and `reset_ic'. (event_serial): Export. (android_query_sem): New function. (android_init_events): Initialize new semaphore. (android_write_event): Export. (android_select): Check for UI thread code. (setEmacsParams, android_init_emacs_service): Initialize new methods. (android_check_query, android_begin_query, android_end_query) (android_run_in_emacs_thread): (android_update_ic, android_reset_ic): New functions for managing synchronous queries from one thread to another. * src/android.h: Export new functions. * src/androidgui.h (enum android_event_type): Add input method events. (enum android_ime_operation, struct android_ime_event) (union android_event, enum android_ic_mode): New structs and enums. * src/androidterm.c (android_window_to_frame): Allow DPYINFO to be NULL. (android_decode_utf16, android_handle_ime_event) (handle_one_android_event, android_sync_edit) (android_copy_java_string, beginBatchEdit, endBatchEdit) (commitCompletion, deleteSurroundingText, finishComposingText) (getSelectedtext, getTextAfterCursor, getTextBeforeCursor) (setComposingText, setComposingRegion, setSelection, getSelection) (performEditorAction, getExtractedText): New functions. (struct android_conversion_query_context): (android_perform_conversion_query): (android_text_to_string): (struct android_get_selection_context): (android_get_selection): (struct android_get_extracted_text_context): (android_get_extracted_text): (struct android_extracted_text_request_class): (struct android_extracted_text_class): (android_update_selection): (android_reset_conversion): (android_set_point): (android_compose_region_changed): (android_notify_conversion): (text_conversion_interface): New functions and structures. (android_term_init): Initialize text conversion. * src/coding.c (syms_of_coding): Define Qutf_16le on Android. * src/frame.c (make_frame): Clear conversion data. (delete_frame): Reset conversion state. * src/frame.h (enum text_conversion_operation) (struct text_conversion_action, struct text_conversion_state) (GCALIGNED_STRUCT): Update structures. * src/keyboard.c (read_char, readable_events, kbd_buffer_get_event) (syms_of_keyboard): Handle text conversion events. * src/lisp.h: * src/process.c: Fix includes. * src/textconv.c (enum textconv_batch_edit_flags, textconv_query) (reset_frame_state, detect_conversion_events) (restore_selected_window, really_commit_text) (really_finish_composing_text, really_set_composing_text) (really_set_composing_region, really_delete_surrounding_text) (really_set_point, complete_edit) (handle_pending_conversion_events_1) (handle_pending_conversion_events, start_batch_edit) (end_batch_edit, commit_text, finish_composing_text) (set_composing_text, set_composing_region, textconv_set_point) (delete_surrounding_text, get_extracted_text) (report_selected_window_change, report_point_change) (register_texconv_interface): New functions. * src/textconv.h (struct textconv_interface) (TEXTCONV_SKIP_CONVERSION_REGION): Update prototype. * src/xdisp.c (mark_window_display_accurate_1): * src/xfns.c (xic_string_conversion_callback): * src/xterm.c (init_xterm): Adjust accordingly. --- configure.ac | 2 +- doc/emacs/input.texi | 17 + doc/lispref/commands.texi | 9 + java/org/gnu/emacs/EmacsContextMenu.java | 15 +- java/org/gnu/emacs/EmacsDialog.java | 15 +- java/org/gnu/emacs/EmacsEditable.java | 300 -------- java/org/gnu/emacs/EmacsInputConnection.java | 187 ++--- java/org/gnu/emacs/EmacsNative.java | 46 ++ java/org/gnu/emacs/EmacsService.java | 117 +-- java/org/gnu/emacs/EmacsView.java | 55 +- lisp/bindings.el | 3 + src/alloc.c | 14 + src/android.c | 295 +++++++- src/android.h | 10 + src/androidgui.h | 60 ++ src/androidterm.c | 1037 +++++++++++++++++++++++++- src/coding.c | 2 +- src/frame.c | 17 + src/frame.h | 62 ++ src/keyboard.c | 50 ++ src/lisp.h | 3 + src/process.c | 1 + src/textconv.c | 906 +++++++++++++++++++++- src/textconv.h | 38 +- src/xdisp.c | 28 + src/xfns.c | 2 +- src/xterm.c | 2 +- 27 files changed, 2787 insertions(+), 506 deletions(-) delete mode 100644 java/org/gnu/emacs/EmacsEditable.java (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/configure.ac b/configure.ac index 44529260892..fc95bcd09b6 100644 --- a/configure.ac +++ b/configure.ac @@ -7217,7 +7217,7 @@ if test "$window_system" != "none"; then [Define if you poll periodically to detect C-g.]) WINDOW_SYSTEM_OBJ="fontset.o fringe.o image.o" - if test "$window_system" = "x11"; then + if test "$window_system" = "x11" || test "$REALLY_ANDROID" = "yes"; then AC_DEFINE([HAVE_TEXT_CONVERSION], [1], [Define if the window system has text conversion support.]) WINDOW_SYSTEM_OBJ="$WINDOW_SYSTEM_OBJ textconv.o" diff --git a/doc/emacs/input.texi b/doc/emacs/input.texi index b306a63b6cb..2463a75edcd 100644 --- a/doc/emacs/input.texi +++ b/doc/emacs/input.texi @@ -109,3 +109,20 @@ Emacs quitting. @xref{Quitting}. The exact button is used to do this varies by system: on X, it is defined in the variable @code{x-quit-keysym}, and on Android, it is always the volume down button. + +@cindex text conversion, keyboards + Most input methods designed to work with on-screen keyboards perform +buffer edits differently from desktop input methods. + + On a conventional desktop windowing system, an input method will +simply display the contents of any on going character compositions on +screen, and send the appropriate key events to Emacs after completion. + + However, on screen keyboard input methods directly perform edits to +the selected window of each frame; this is known as ``text +conversion'', or ``string conversion'' under the X Window System. + + Text conversion is performed asynchronously whenever Emacs receives +a request to perform the conversion from the input method. After the +conversion completes, a @code{text-conversion} event is sent. +@xref{Misc Events,,, elisp, the Emacs Reference Manual}. diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index 2c0787521a5..2807d3d61b2 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -2200,6 +2200,15 @@ the buffer in which the xwidget will be displayed, using A few other event types represent occurrences within the system. @table @code +@cindex @code{text-conversion} event +@item text-conversion +This kind of event is sent @strong{after} a system-wide input method +performs an edit to one or more buffers. + +Once the event is sent, the input method may already have made changes +to multiple frames. @c TODO: allow querying which frames to which +@c changes have been made. + @cindex @code{delete-frame} event @item (delete-frame (@var{frame})) This kind of event indicates that the user gave the window manager diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 92429410d03..184c611bf9d 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -279,20 +279,7 @@ public class EmacsContextMenu } }; - synchronized (runnable) - { - EmacsService.SERVICE.runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + EmacsService.syncRunnable (runnable); return rc.thing; } diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index bd5e9ba8ee7..bc0333e99b9 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -317,20 +317,7 @@ public class EmacsDialog implements DialogInterface.OnDismissListener } }; - synchronized (runnable) - { - EmacsService.SERVICE.runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + EmacsService.syncRunnable (runnable); return rc.thing; } diff --git a/java/org/gnu/emacs/EmacsEditable.java b/java/org/gnu/emacs/EmacsEditable.java deleted file mode 100644 index 79af65a6ccd..00000000000 --- a/java/org/gnu/emacs/EmacsEditable.java +++ /dev/null @@ -1,300 +0,0 @@ -/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- - -Copyright (C) 2023 Free Software Foundation, Inc. - -This file is part of GNU Emacs. - -GNU Emacs is free software: you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation, either version 3 of the License, or (at -your option) any later version. - -GNU Emacs is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with GNU Emacs. If not, see . */ - -package org.gnu.emacs; - -import android.text.InputFilter; -import android.text.SpannableStringBuilder; -import android.text.Spanned; -import android.text.SpanWatcher; -import android.text.Selection; - -import android.content.Context; - -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.ExtractedText; -import android.view.inputmethod.ExtractedTextRequest; - -import android.text.Spannable; - -import android.util.Log; - -import android.os.Build; - -/* Android input methods insist on having access to buffer contents. - Since Emacs is not designed like ``any other Android text editor'', - that is not possible. - - This file provides a fake editing buffer that is designed to weasel - as much information as possible out of an input method, without - actually providing buffer contents to Emacs. - - The basic idea is to have the fake editing buffer be initially - empty. - - When the input method inserts composed text, it sets a flag. - Updates to the buffer while the flag is set are sent to Emacs to be - displayed as ``preedit text''. - - Once some heuristics decide that composition has been completed, - the composed text is sent to Emacs, and the text that was inserted - in this editing buffer is erased. */ - -public class EmacsEditable extends SpannableStringBuilder - implements SpanWatcher -{ - private static final String TAG = "EmacsEditable"; - - /* Whether or not composition is currently in progress. */ - private boolean isComposing; - - /* The associated input connection. */ - private EmacsInputConnection connection; - - /* The associated IM manager. */ - private InputMethodManager imManager; - - /* Any extracted text an input method may be monitoring. */ - private ExtractedText extractedText; - - /* The corresponding text request. */ - private ExtractedTextRequest extractRequest; - - /* The number of nested batch edits. */ - private int batchEditCount; - - /* Whether or not invalidateInput should be called upon batch edits - ending. */ - private boolean pendingInvalidate; - - /* The ``composing span'' indicating the bounds of an ongoing - character composition. */ - private Object composingSpan; - - public - EmacsEditable (EmacsInputConnection connection) - { - /* Initialize the editable with one initial space, so backspace - always works. */ - super (); - - Object tem; - Context context; - - this.connection = connection; - - context = connection.view.getContext (); - tem = context.getSystemService (Context.INPUT_METHOD_SERVICE); - imManager = (InputMethodManager) tem; - - /* To watch for changes to text properties on Android, you - add... a text property. */ - setSpan (this, 0, 0, Spanned.SPAN_INCLUSIVE_INCLUSIVE); - } - - public void - endBatchEdit () - { - if (batchEditCount < 1) - return; - - if (--batchEditCount == 0 && pendingInvalidate) - invalidateInput (); - } - - public void - beginBatchEdit () - { - ++batchEditCount; - } - - public void - setExtractedTextAndRequest (ExtractedText text, - ExtractedTextRequest request, - boolean monitor) - { - /* Extract the text. If monitor is set, also record it as the - text that is currently being extracted. */ - - text.startOffset = 0; - text.selectionStart = Selection.getSelectionStart (this); - text.selectionEnd = Selection.getSelectionStart (this); - text.text = this; - - if (monitor) - { - extractedText = text; - extractRequest = request; - } - } - - public void - compositionStart () - { - isComposing = true; - } - - public void - compositionEnd () - { - isComposing = false; - sendComposingText (null); - } - - private void - sendComposingText (String string) - { - EmacsWindow window; - long time, serial; - - window = connection.view.window; - - if (window.isDestroyed ()) - return; - - time = System.currentTimeMillis (); - - /* A composition event is simply a special key event with a - keycode of -1. */ - - synchronized (window.eventStrings) - { - serial - = EmacsNative.sendKeyPress (window.handle, time, 0, -1, -1); - - /* Save the string so that android_lookup_string can find - it. */ - if (string != null) - window.saveUnicodeString ((int) serial, string); - } - } - - private void - invalidateInput () - { - int start, end, composingSpanStart, composingSpanEnd; - - if (batchEditCount > 0) - { - Log.d (TAG, "invalidateInput: deferring for batch edit"); - pendingInvalidate = true; - return; - } - - pendingInvalidate = false; - - start = Selection.getSelectionStart (this); - end = Selection.getSelectionEnd (this); - - if (composingSpan != null) - { - composingSpanStart = getSpanStart (composingSpan); - composingSpanEnd = getSpanEnd (composingSpan); - } - else - { - composingSpanStart = -1; - composingSpanEnd = -1; - } - - Log.d (TAG, "invalidateInput: now " + start + ", " + end); - - /* Tell the input method that the cursor changed. */ - imManager.updateSelection (connection.view, start, end, - composingSpanStart, - composingSpanEnd); - - /* If there is any extracted text, tell the IME that it has - changed. */ - if (extractedText != null) - imManager.updateExtractedText (connection.view, - extractRequest.token, - extractedText); - } - - public SpannableStringBuilder - replace (int start, int end, CharSequence tb, int tbstart, - int tbend) - { - super.replace (start, end, tb, tbstart, tbend); - - /* If a change happens during composition, perform the change and - then send the text being composed. */ - - if (isComposing) - sendComposingText (toString ()); - - return this; - } - - private boolean - isSelectionSpan (Object span) - { - return ((Selection.SELECTION_START == span - || Selection.SELECTION_END == span) - && (getSpanFlags (span) - & Spanned.SPAN_INTERMEDIATE) == 0); - } - - @Override - public void - onSpanAdded (Spannable text, Object what, int start, int end) - { - Log.d (TAG, "onSpanAdded: " + text + " " + what + " " - + start + " " + end); - - /* Try to find the composing span. This isn't a public API. */ - - if (what.getClass ().getName ().contains ("ComposingText")) - composingSpan = what; - - if (isSelectionSpan (what)) - invalidateInput (); - } - - @Override - public void - onSpanChanged (Spannable text, Object what, int ostart, - int oend, int nstart, int nend) - { - Log.d (TAG, "onSpanChanged: " + text + " " + what + " " - + nstart + " " + nend); - - if (isSelectionSpan (what)) - invalidateInput (); - } - - @Override - public void - onSpanRemoved (Spannable text, Object what, - int start, int end) - { - Log.d (TAG, "onSpanRemoved: " + text + " " + what + " " - + start + " " + end); - - if (isSelectionSpan (what)) - invalidateInput (); - } - - public boolean - isInBatchEdit () - { - return batchEditCount > 0; - } -} diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 897a393b984..3cf4419838b 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -25,6 +25,7 @@ import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.InputMethodManager; import android.view.inputmethod.SurroundingText; +import android.view.inputmethod.TextSnapshot; import android.view.KeyEvent; import android.text.Editable; @@ -38,138 +39,156 @@ import android.util.Log; public class EmacsInputConnection extends BaseInputConnection { private static final String TAG = "EmacsInputConnection"; - public EmacsView view; - private EmacsEditable editable; - - /* The length of the last string to be committed. */ - private int lastCommitLength; - - int currentLargeOffset; + private EmacsView view; + private short windowHandle; public EmacsInputConnection (EmacsView view) { - super (view, false); + super (view, true); + this.view = view; - this.editable = new EmacsEditable (this); + this.windowHandle = view.window.handle; } @Override - public Editable - getEditable () + public boolean + beginBatchEdit () { - return editable; + Log.d (TAG, "beginBatchEdit"); + EmacsNative.beginBatchEdit (windowHandle); + return true; } @Override public boolean - setComposingText (CharSequence text, int newCursorPosition) + endBatchEdit () { - editable.compositionStart (); - super.setComposingText (text, newCursorPosition); + Log.d (TAG, "endBatchEdit"); + EmacsNative.endBatchEdit (windowHandle); return true; } @Override public boolean - setComposingRegion (int start, int end) + commitCompletion (CompletionInfo info) { - int i; - - if (lastCommitLength != 0) - { - Log.d (TAG, "Restarting composition for: " + lastCommitLength); - - for (i = 0; i < lastCommitLength; ++i) - sendKeyEvent (new KeyEvent (KeyEvent.ACTION_DOWN, - KeyEvent.KEYCODE_DEL)); + Log.d (TAG, "commitCompletion: " + info); + EmacsNative.commitCompletion (windowHandle, + info.getText ().toString (), + info.getPosition ()); + return true; + } - lastCommitLength = 0; - } + @Override + public boolean + commitText (CharSequence text, int newCursorPosition) + { + Log.d (TAG, "commitText: " + text + " " + newCursorPosition); + EmacsNative.commitText (windowHandle, text.toString (), + newCursorPosition); + return true; + } - editable.compositionStart (); - super.setComposingRegion (start, end); + @Override + public boolean + deleteSurroundingText (int leftLength, int rightLength) + { + Log.d (TAG, ("deleteSurroundingText: " + + leftLength + " " + rightLength)); + EmacsNative.deleteSurroundingText (windowHandle, leftLength, + rightLength); return true; } - + @Override public boolean finishComposingText () { - editable.compositionEnd (); - return super.finishComposingText (); + Log.d (TAG, "finishComposingText"); + + EmacsNative.finishComposingText (windowHandle); + return true; + } + + @Override + public String + getSelectedText (int flags) + { + Log.d (TAG, "getSelectedText: " + flags); + + return EmacsNative.getSelectedText (windowHandle, flags); + } + + @Override + public String + getTextAfterCursor (int length, int flags) + { + Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); + + return EmacsNative.getTextAfterCursor (windowHandle, length, + flags); + } + + @Override + public String + getTextBeforeCursor (int length, int flags) + { + Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); + + return EmacsNative.getTextBeforeCursor (windowHandle, length, + flags); } @Override public boolean - beginBatchEdit () + setComposingText (CharSequence text, int newCursorPosition) { - editable.beginBatchEdit (); - return super.beginBatchEdit (); + Log.d (TAG, "setComposingText: " + newCursorPosition); + + EmacsNative.setComposingText (windowHandle, text.toString (), + newCursorPosition); + return true; } @Override public boolean - endBatchEdit () + setComposingRegion (int start, int end) { - editable.endBatchEdit (); - return super.endBatchEdit (); + Log.d (TAG, "setComposingRegion: " + start + " " + end); + + EmacsNative.setComposingRegion (windowHandle, start, end); + return true; } - + @Override public boolean - commitText (CharSequence text, int newCursorPosition) + performEditorAction (int editorAction) { - editable.compositionEnd (); - super.commitText (text, newCursorPosition); - - /* An observation is that input methods rarely recompose trailing - spaces. Avoid re-setting the commit length in that case. */ - - if (text.toString ().equals (" ")) - lastCommitLength += 1; - else - /* At this point, the editable is now empty. - - The input method may try to cancel the edit upon a subsequent - backspace by calling setComposingRegion with a region that is - the length of TEXT. - - Record this length in order to be able to send backspace - events to ``delete'' the text in that case. */ - lastCommitLength = text.length (); - - Log.d (TAG, "commitText: \"" + text + "\""); + Log.d (TAG, "performEditorAction: " + editorAction); + EmacsNative.performEditorAction (windowHandle, editorAction); return true; } - /* Return a large offset, cycling through 100000, 30000, 0. - The offset is typically used to force the input method to update - its notion of ``surrounding text'', bypassing any caching that - it might have in progress. + @Override + public ExtractedText + getExtractedText (ExtractedTextRequest request, int flags) + { + Log.d (TAG, "getExtractedText: " + request + " " + flags); + + return EmacsNative.getExtractedText (windowHandle, request, + flags); + } - There must be another way to do this, but I can't find it. */ + + /* Override functions which are not implemented. */ - public int - largeSelectionOffset () + @Override + public TextSnapshot + takeSnapshot () { - switch (currentLargeOffset) - { - case 0: - currentLargeOffset = 100000; - return 100000; - - case 100000: - currentLargeOffset = 30000; - return 30000; - - case 30000: - currentLargeOffset = 0; - return 0; - } - - currentLargeOffset = 0; - return -1; + Log.d (TAG, "takeSnapshot"); + return null; } } diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index f0219843d35..aaac9446510 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -22,6 +22,8 @@ package org.gnu.emacs; import java.lang.System; import android.content.res.AssetManager; +import android.view.inputmethod.ExtractedText; +import android.view.inputmethod.ExtractedTextRequest; public class EmacsNative { @@ -161,6 +163,50 @@ public class EmacsNative descriptor, or NULL if there is none. */ public static native byte[] getProcName (int fd); + /* Notice that the Emacs thread will now start waiting for the main + thread's looper to respond. */ + public static native void beginSynchronous (); + + /* Notice that the Emacs thread will has finished waiting for the + main thread's looper to respond. */ + public static native void endSynchronous (); + + + + /* Input connection functions. These mostly correspond to their + counterparts in Android's InputConnection. */ + + public static native void beginBatchEdit (short window); + public static native void endBatchEdit (short window); + public static native void commitCompletion (short window, String text, + int position); + public static native void commitText (short window, String text, + int position); + public static native void deleteSurroundingText (short window, + int leftLength, + int rightLength); + public static native void finishComposingText (short window); + public static native String getSelectedText (short window, int flags); + public static native String getTextAfterCursor (short window, int length, + int flags); + public static native String getTextBeforeCursor (short window, int length, + int flags); + public static native void setComposingText (short window, String text, + int newCursorPosition); + public static native void setComposingRegion (short window, int start, + int end); + public static native void setSelection (short window, int start, int end); + public static native void performEditorAction (short window, + int editorAction); + public static native ExtractedText getExtractedText (short window, + ExtractedTextRequest req, + int flags); + + + /* Return the current value of the selection, or -1 upon + failure. */ + public static native int getSelection (short window); + static { /* Older versions of Android cannot link correctly with shared diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 2ec2ddf9bda..855a738a30f 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -80,6 +80,11 @@ public class EmacsService extends Service private EmacsThread thread; private Handler handler; + /* Keep this in synch with androidgui.h. */ + public static final int IC_MODE_NULL = 0; + public static final int IC_MODE_ACTION = 1; + public static final int IC_MODE_TEXT = 2; + /* Display metrics used by font backends. */ public DisplayMetrics metrics; @@ -258,20 +263,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + syncRunnable (runnable); return view.thing; } @@ -292,19 +284,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } + syncRunnable (runnable); } public void @@ -502,19 +482,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } + syncRunnable (runnable); } @@ -594,20 +562,7 @@ public class EmacsService extends Service } }; - synchronized (runnable) - { - runOnUiThread (runnable); - - try - { - runnable.wait (); - } - catch (InterruptedException e) - { - EmacsNative.emacsAbort (); - } - } - + syncRunnable (runnable); return manager.thing; } @@ -622,4 +577,58 @@ public class EmacsService extends Service startActivity (intent); System.exit (0); } + + /* Wait synchronously for the specified RUNNABLE to complete in the + UI thread. Must be called from the Emacs thread. */ + + public static void + syncRunnable (Runnable runnable) + { + EmacsNative.beginSynchronous (); + + synchronized (runnable) + { + SERVICE.runOnUiThread (runnable); + + while (true) + { + try + { + runnable.wait (); + break; + } + catch (InterruptedException e) + { + continue; + } + } + } + + EmacsNative.endSynchronous (); + } + + public void + updateIC (EmacsWindow window, int newSelectionStart, + int newSelectionEnd, int composingRegionStart, + int composingRegionEnd) + { + Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart + + " " + newSelectionEnd + " " + + composingRegionStart + " " + + composingRegionEnd)); + window.view.imManager.updateSelection (window.view, + newSelectionStart, + newSelectionEnd, + composingRegionStart, + composingRegionEnd); + } + + public void + resetIC (EmacsWindow window, int icMode) + { + Log.d (TAG, "resetIC: " + window); + + window.view.setICMode (icMode); + window.view.imManager.restartInput (window.view); + } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index bc3716f6da8..52da6d41f7d 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -103,6 +103,13 @@ public class EmacsView extends ViewGroup displayed whenever possible. */ public boolean isCurrentlyTextEditor; + /* The associated input connection. */ + private EmacsInputConnection inputConnection; + + /* The current IC mode. See `android_reset_ic' for more + details. */ + private int icMode; + public EmacsView (EmacsWindow window) { @@ -554,14 +561,46 @@ public class EmacsView extends ViewGroup public InputConnection onCreateInputConnection (EditorInfo info) { + int selection, mode; + + /* Figure out what kind of IME behavior Emacs wants. */ + mode = getICMode (); + /* Make sure the input method never displays a full screen input box that obscures Emacs. */ info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; /* Set a reasonable inputType. */ - info.inputType = InputType.TYPE_NULL; + info.inputType = InputType.TYPE_CLASS_TEXT; + + /* Obtain the current position of point and set it as the + selection. */ + selection = EmacsNative.getSelection (window.handle); + + Log.d (TAG, "onCreateInputConnection: current selection is: " + selection); + + /* If this fails or ANDROID_IC_MODE_NULL was requested, then don't + initialize the input connection. */ + if (selection == -1 || mode == EmacsService.IC_MODE_NULL) + { + info.inputType = InputType.TYPE_NULL; + return null; + } + + if (mode == EmacsService.IC_MODE_ACTION) + info.imeOptions |= EditorInfo.IME_ACTION_DONE; - return null; + /* Set the initial selection fields. */ + info.initialSelStart = selection; + info.initialSelEnd = selection; + + /* Create the input connection if necessary. */ + + if (inputConnection == null) + inputConnection = new EmacsInputConnection (this); + + /* Return the input connection. */ + return inputConnection; } @Override @@ -572,4 +611,16 @@ public class EmacsView extends ViewGroup keyboard. */ return isCurrentlyTextEditor; } + + public synchronized void + setICMode (int icMode) + { + this.icMode = icMode; + } + + public synchronized int + getICMode () + { + return icMode; + } }; diff --git a/lisp/bindings.el b/lisp/bindings.el index c77b64c05da..057c870958d 100644 --- a/lisp/bindings.el +++ b/lisp/bindings.el @@ -1521,6 +1521,9 @@ if `inhibit-field-text-motion' is non-nil." (define-key special-event-map [sigusr1] 'ignore) (define-key special-event-map [sigusr2] 'ignore) +;; Text conversion +(define-key global-map [text-conversion] 'ignore) + ;; Don't look for autoload cookies in this file. ;; Local Variables: ;; no-update-autoloads: t diff --git a/src/alloc.c b/src/alloc.c index bc43f22005d..6d8658e7bb0 100644 --- a/src/alloc.c +++ b/src/alloc.c @@ -6939,6 +6939,11 @@ static void mark_frame (struct Lisp_Vector *ptr) { struct frame *f = (struct frame *) ptr; +#ifdef HAVE_TEXT_CONVERSION + struct text_conversion_action *tem; +#endif + + mark_vectorlike (&ptr->header); mark_face_cache (f->face_cache); #ifdef HAVE_WINDOW_SYSTEM @@ -6950,6 +6955,15 @@ mark_frame (struct Lisp_Vector *ptr) mark_vectorlike (&font->header); } #endif + +#ifdef HAVE_TEXT_CONVERSION + mark_object (f->conversion.compose_region_start); + mark_object (f->conversion.compose_region_end); + mark_object (f->conversion.compose_region_overlay); + + for (tem = f->conversion.actions; tem; tem = tem->next) + mark_object (tem->data); +#endif } static void diff --git a/src/android.c b/src/android.c index 479eb9b10d4..8f446224dab 100644 --- a/src/android.c +++ b/src/android.c @@ -98,6 +98,8 @@ struct android_emacs_service jmethodID sync; jmethodID browse_url; jmethodID restart_emacs; + jmethodID update_ic; + jmethodID reset_ic; }; struct android_emacs_pixmap @@ -207,7 +209,7 @@ static struct android_emacs_window window_class; /* The last event serial used. This is a 32 bit value, but it is stored in unsigned long to be consistent with X. */ -static unsigned int event_serial; +unsigned int event_serial; /* Unused pointer used to control compiler optimizations. */ void *unused_pointer; @@ -408,6 +410,10 @@ android_handle_sigusr1 (int sig, siginfo_t *siginfo, void *arg) #endif +/* Semaphore used to indicate completion of a query. + This should ideally be defined further down. */ +static sem_t android_query_sem; + /* Set up the global event queue by initializing the mutex and two condition variables, and the linked list of events. This must be called before starting the Emacs thread. Also, initialize the @@ -438,6 +444,7 @@ android_init_events (void) sem_init (&android_pselect_sem, 0, 0); sem_init (&android_pselect_start_sem, 0, 0); + sem_init (&android_query_sem, 0, 0); event_queue.events.next = &event_queue.events; event_queue.events.last = &event_queue.events; @@ -538,7 +545,7 @@ android_next_event (union android_event *event_return) pthread_mutex_unlock (&event_queue.mutex); } -static void +void android_write_event (union android_event *event) { struct android_event_container *container; @@ -576,6 +583,10 @@ android_select (int nfds, fd_set *readfds, fd_set *writefds, static char byte; #endif + /* Check for and run anything the UI thread wants to run on the main + thread. */ + android_check_query (); + pthread_mutex_lock (&event_queue.mutex); if (event_queue.num_events) @@ -634,6 +645,10 @@ android_select (int nfds, fd_set *readfds, fd_set *writefds, if (nfds_return < 0) errno = EINTR; + /* Now check for and run anything the UI thread wants to run in the + main thread. */ + android_check_query (); + return nfds_return; } @@ -1431,7 +1446,7 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, /* This may be called from multiple threads. setEmacsParams should only ever be called once. */ - if (__atomic_fetch_add (&emacs_initialized, -1, __ATOMIC_RELAXED)) + if (__atomic_fetch_add (&emacs_initialized, -1, __ATOMIC_SEQ_CST)) { ANDROID_THROW (env, "java/lang/IllegalArgumentException", "Emacs was already initialized!"); @@ -1705,6 +1720,10 @@ android_init_emacs_service (void) FIND_METHOD (browse_url, "browseUrl", "(Ljava/lang/String;)" "Ljava/lang/String;"); FIND_METHOD (restart_emacs, "restartEmacs", "()V"); + FIND_METHOD (update_ic, "updateIC", + "(Lorg/gnu/emacs/EmacsWindow;IIII)V"); + FIND_METHOD (reset_ic, "resetIC", + "(Lorg/gnu/emacs/EmacsWindow;I)V"); #undef FIND_METHOD } @@ -1834,7 +1853,7 @@ android_init_emacs_window (void) #undef FIND_METHOD } -extern JNIEXPORT void JNICALL +JNIEXPORT void JNICALL NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, jobject dump_file_object, jint api_level) { @@ -1928,19 +1947,19 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, emacs_abort (); } -extern JNIEXPORT void JNICALL +JNIEXPORT void JNICALL NATIVE_NAME (emacsAbort) (JNIEnv *env, jobject object) { emacs_abort (); } -extern JNIEXPORT void JNICALL +JNIEXPORT void JNICALL NATIVE_NAME (quit) (JNIEnv *env, jobject object) { Vquit_flag = Qt; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, jshort window, jlong time, jint x, jint y, jint width, @@ -1961,7 +1980,7 @@ NATIVE_NAME (sendConfigureNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, jshort window, jlong time, jint state, jint keycode, @@ -1981,7 +2000,7 @@ NATIVE_NAME (sendKeyPress) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, jshort window, jlong time, jint state, jint keycode, @@ -2001,7 +2020,7 @@ NATIVE_NAME (sendKeyRelease) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendFocusIn) (JNIEnv *env, jobject object, jshort window, jlong time) { @@ -2016,7 +2035,7 @@ NATIVE_NAME (sendFocusIn) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendFocusOut) (JNIEnv *env, jobject object, jshort window, jlong time) { @@ -2031,7 +2050,7 @@ NATIVE_NAME (sendFocusOut) (JNIEnv *env, jobject object, return ++event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendWindowAction) (JNIEnv *env, jobject object, jshort window, jint action) { @@ -2046,7 +2065,7 @@ NATIVE_NAME (sendWindowAction) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendEnterNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -2064,7 +2083,7 @@ NATIVE_NAME (sendEnterNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendLeaveNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -2082,7 +2101,7 @@ NATIVE_NAME (sendLeaveNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendMotionNotify) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time) @@ -2100,7 +2119,7 @@ NATIVE_NAME (sendMotionNotify) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -2121,7 +2140,7 @@ NATIVE_NAME (sendButtonPress) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -2142,7 +2161,7 @@ NATIVE_NAME (sendButtonRelease) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -2161,7 +2180,7 @@ NATIVE_NAME (sendTouchDown) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -2180,7 +2199,7 @@ NATIVE_NAME (sendTouchUp) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint pointer_id) @@ -2199,7 +2218,7 @@ NATIVE_NAME (sendTouchMove) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jlong time, jint state, @@ -2221,7 +2240,7 @@ NATIVE_NAME (sendWheel) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendIconified) (JNIEnv *env, jobject object, jshort window) { @@ -2235,7 +2254,7 @@ NATIVE_NAME (sendIconified) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, jshort window) { @@ -2249,7 +2268,7 @@ NATIVE_NAME (sendDeiconified) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, jshort window, jint menu_event_id) { @@ -2264,7 +2283,7 @@ NATIVE_NAME (sendContextMenu) (JNIEnv *env, jobject object, return event_serial; } -extern JNIEXPORT jlong JNICALL +JNIEXPORT jlong JNICALL NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object, jshort window, jint x, jint y, jint width, jint height) @@ -2283,6 +2302,23 @@ NATIVE_NAME (sendExpose) (JNIEnv *env, jobject object, return event_serial; } +/* Forward declarations of deadlock prevention functions. */ + +static void android_begin_query (void); +static void android_end_query (void); + +JNIEXPORT void JNICALL +NATIVE_NAME (beginSynchronous) (JNIEnv *env, jobject object) +{ + android_begin_query (); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (endSynchronous) (JNIEnv *env, jobject object) +{ + android_end_query (); +} + #ifdef __clang__ #pragma clang diagnostic pop #else @@ -5155,6 +5191,215 @@ android_get_current_api_level (void) +/* Whether or not a query is currently being made. */ +static bool android_servicing_query; + +/* Function that is waiting to be run in the Emacs thread. */ +static void (*android_query_function) (void *); + +/* Context for that function. */ +static void *android_query_context; + +/* Deadlock protection. The UI thread and the Emacs thread must + sometimes make synchronous queries to each other, which are + normally answered inside each thread's respective event loop. + Deadlocks can happen when both threads simultaneously make such + synchronous queries and block waiting for each others responses. + + The Emacs thread can be interrupted to service any queries made by + the UI thread, but is not possible the other way around. + + To avoid such deadlocks, an atomic counter is provided. This + counter is incremented every time a query starts, and is set to + zerp every time one ends. If the UI thread tries to make a query + and sees that the counter is non-zero, it simply returns so that + its event loop can proceed to perform and respond to the query. If + the Emacs thread sees the same thing, then it stops to service all + queries being made by the input method, then proceeds to make its + query. */ + +/* Run any function that the UI thread has asked to run, and then + signal its completion. */ + +void +android_check_query (void) +{ + void (*proc) (void *); + void *closure; + + if (!__atomic_load_n (&android_servicing_query, __ATOMIC_SEQ_CST)) + return; + + /* First, load the procedure and closure. */ + __atomic_load (&android_query_context, &closure, __ATOMIC_SEQ_CST); + __atomic_load (&android_query_function, &proc, __ATOMIC_SEQ_CST); + + if (!proc) + return; + + proc (closure); + + /* Finish the query. */ + __atomic_store_n (&android_query_context, NULL, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_query_function, NULL, __ATOMIC_SEQ_CST); + __atomic_clear (&android_servicing_query, __ATOMIC_SEQ_CST); + + /* Signal completion. */ + sem_post (&android_query_sem); +} + +/* Notice that the Emacs thread will start blocking waiting for a + response from the UI thread. Process any pending queries from the + UI thread. + + This function may be called from Java. */ + +static void +android_begin_query (void) +{ + if (__atomic_test_and_set (&android_servicing_query, + __ATOMIC_SEQ_CST)) + { + /* Answer the query that is currently being made. */ + assert (android_query_function != NULL); + android_check_query (); + + /* Wait for that query to complete. */ + while (__atomic_load_n (&android_servicing_query, + __ATOMIC_SEQ_CST)) + ;; + } +} + +/* Notice that a query has stopped. This function may be called from + Java. */ + +static void +android_end_query (void) +{ + __atomic_clear (&android_servicing_query, __ATOMIC_SEQ_CST); +} + +/* Synchronously ask the Emacs thread to run the specified PROC with + the given CLOSURE. Return if this fails, or once PROC is run. + + PROC may be run from inside maybe_quit. + + It is not okay to run Lisp code which signals or performs non + trivial tasks inside PROC. + + Return 1 if the Emacs thread is currently waiting for the UI thread + to respond and PROC could not be run, or 0 otherwise. */ + +int +android_run_in_emacs_thread (void (*proc) (void *), void *closure) +{ + union android_event event; + + event.xaction.type = ANDROID_WINDOW_ACTION; + event.xaction.serial = ++event_serial; + event.xaction.window = 0; + event.xaction.action = 0; + + /* Set android_query_function and android_query_context. */ + __atomic_store_n (&android_query_context, closure, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_query_function, proc, __ATOMIC_SEQ_CST); + + /* Don't allow deadlocks to happen; make sure the Emacs thread is + not waiting for something to be done. */ + + if (__atomic_test_and_set (&android_servicing_query, + __ATOMIC_SEQ_CST)) + { + __atomic_store_n (&android_query_context, NULL, + __ATOMIC_SEQ_CST); + __atomic_store_n (&android_query_function, NULL, + __ATOMIC_SEQ_CST); + + return 1; + } + + /* Send a dummy event. `android_check_query' will be called inside + wait_reading_process_output after the event arrives. + + Otherwise, android_select will call android_check_thread the next + time it is entered. */ + android_write_event (&event); + + /* Start waiting for the function to be executed. */ + while (sem_wait (&android_query_sem) < 0) + ;; + + return 0; +} + + + +/* Input method related functions. */ + +/* Change WINDOW's active selection to the characters between + SELECTION_START and SELECTION_END. + + Also, update the composing region to COMPOSING_REGION_START and + COMPOSING_REGION_END. + + If any value cannot fit in jint, then the behavior of the input + method is undefined. */ + +void +android_update_ic (android_window window, ptrdiff_t selection_start, + ptrdiff_t selection_end, ptrdiff_t composing_region_start, + ptrdiff_t composing_region_end) +{ + jobject object; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.update_ic, + object, + (jint) selection_start, + (jint) selection_end, + (jint) composing_region_start, + (jint) composing_region_end); + android_exception_check (); +} + +/* Reinitialize any ongoing input method connection on WINDOW. + + Any input method that is connected to WINDOW will invalidate its + cache of the buffer contents. + + MODE controls certain aspects of the input method's behavior: + + - If MODE is ANDROID_IC_MODE_NULL, the input method will be + deactivated, and an ASCII only keyboard will be displayed + instead. + + - If MODE is ANDROID_IC_MODE_ACTION, the input method will + edit text normally, but send ``return'' as a key event. + This is useful inside the mini buffer. + + - If MODE is ANDROID_IC_MODE_TEXT, the input method is free + to behave however it wants. */ + +void +android_reset_ic (android_window window, enum android_ic_mode mode) +{ + jobject object; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.reset_ic, + object, (jint) mode); + android_exception_check (); +} + + + #else /* ANDROID_STUBIFY */ /* X emulation functions for Android. */ diff --git a/src/android.h b/src/android.h index 9006f5f34c5..ec4fa33dfc3 100644 --- a/src/android.h +++ b/src/android.h @@ -108,6 +108,16 @@ extern void android_closedir (struct android_dir *); extern Lisp_Object android_browse_url (Lisp_Object); + + +/* Event loop functions. */ + +extern void android_check_query (void); +extern int android_run_in_emacs_thread (void (*) (void *), void *); +extern void android_write_event (union android_event *); + +extern unsigned int event_serial; + #endif /* JNI functions should not be built when Emacs is stubbed out for the diff --git a/src/androidgui.h b/src/androidgui.h index 1f0d34e67aa..25dc6754fff 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -235,6 +235,7 @@ enum android_event_type ANDROID_DEICONIFIED, ANDROID_CONTEXT_MENU, ANDROID_EXPOSE, + ANDROID_INPUT_METHOD, }; struct android_any_event @@ -419,6 +420,52 @@ struct android_menu_event int menu_event_id; }; +enum android_ime_operation + { + ANDROID_IME_COMMIT_TEXT, + ANDROID_IME_DELETE_SURROUNDING_TEXT, + ANDROID_IME_FINISH_COMPOSING_TEXT, + ANDROID_IME_SET_COMPOSING_TEXT, + ANDROID_IME_SET_COMPOSING_REGION, + ANDROID_IME_SET_POINT, + ANDROID_IME_START_BATCH_EDIT, + ANDROID_IME_END_BATCH_EDIT, + }; + +struct android_ime_event +{ + /* Type of the event. */ + enum android_event_type type; + + /* The event serial. */ + unsigned long serial; + + /* The associated window. */ + android_window window; + + /* What operation is being performed. */ + enum android_ime_operation operation; + + /* The details of the operation. START and END provide buffer + indices, and may actually mean ``left'' and ``right''. */ + ptrdiff_t start, end, position; + + /* The number of characters in TEXT. */ + size_t length; + + /* TEXT is either NULL, or a pointer to LENGTH bytes of malloced + UTF-16 encoded text that must be decoded by Emacs. + + POSITION is where point should end up after the text is + committed, relative to TEXT. If POSITION is less than 0, it is + relative to TEXT's start; otherwise, it is relative to its + end. */ + unsigned short *text; + + /* Value to set the counter to after the operation completes. */ + unsigned long counter; +}; + union android_event { enum android_event_type type; @@ -447,6 +494,9 @@ union android_event /* This is only used to transmit selected menu items. */ struct android_menu_event menu; + + /* This is used to dispatch input method editing requests. */ + struct android_ime_event ime; }; enum @@ -463,6 +513,13 @@ enum android_lookup_status ANDROID_LOOKUP_BOTH, }; +enum android_ic_mode + { + ANDROID_IC_MODE_NULL = 0, + ANDROID_IC_MODE_ACTION = 1, + ANDROID_IC_MODE_TEXT = 2, + }; + extern int android_pending (void); extern void android_next_event (union android_event *); @@ -554,6 +611,9 @@ extern void android_sync (void); extern int android_wc_lookup_string (android_key_pressed_event *, wchar_t *, int, int *, enum android_lookup_status *); +extern void android_update_ic (android_window, ptrdiff_t, ptrdiff_t, + ptrdiff_t, ptrdiff_t); +extern void android_reset_ic (android_window, enum android_ic_mode); #endif diff --git a/src/androidterm.c b/src/androidterm.c index a57d5623c66..a44bae954da 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -20,6 +20,9 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include +#include +#include #include "lisp.h" #include "androidterm.h" @@ -28,6 +31,8 @@ along with GNU Emacs. If not, see . */ #include "android.h" #include "buffer.h" #include "window.h" +#include "textconv.h" +#include "coding.h" /* This is a chain of structures for all the X displays currently in use. */ @@ -59,6 +64,12 @@ enum ANDROID_EVENT_DROP, }; +/* Find the frame whose window has the identifier WDESC. + + This is like x_window_to_frame in xterm.c, except that DPYINFO may + be NULL, as there is only at most one Android display, and is only + specified in order to stay consistent with X. */ + static struct frame * android_window_to_frame (struct android_display_info *dpyinfo, android_window wdesc) @@ -73,7 +84,7 @@ android_window_to_frame (struct android_display_info *dpyinfo, { f = XFRAME (frame); - if (!FRAME_ANDROID_P (f) || FRAME_DISPLAY_INFO (f) != dpyinfo) + if (!FRAME_ANDROID_P (f)) continue; if (FRAME_ANDROID_WINDOW (f) == wdesc) @@ -527,6 +538,103 @@ android_find_tool (struct frame *f, int pointer_id) return NULL; } +/* Decode STRING, an array of N little endian UTF-16 characters, into + a Lisp string. Return Qnil if the string is too large, and the + encoded string otherwise. */ + +static Lisp_Object +android_decode_utf16 (unsigned short *utf16, size_t n) +{ + struct coding_system coding; + ptrdiff_t size; + + if (INT_MULTIPLY_WRAPV (n, sizeof *utf16, &size)) + return Qnil; + + /* Set up the coding system. Decoding a UTF-16 string (with no BOM) + should not signal. */ + + memset (&coding, 0, sizeof coding); + + setup_coding_system (Qutf_16le, &coding); + coding.source = (const unsigned char *) utf16; + decode_coding_object (&coding, Qnil, 0, 0, size, + size, Qt); + + return coding.dst_object; +} + +/* Handle a single input method event EVENT, delivered to the frame + F. + + Perform the text conversion action specified inside. */ + +static void +android_handle_ime_event (union android_event *event, struct frame *f) +{ + Lisp_Object text; + + /* First, decode the text if necessary. */ + + switch (event->ime.operation) + { + case ANDROID_IME_COMMIT_TEXT: + case ANDROID_IME_FINISH_COMPOSING_TEXT: + case ANDROID_IME_SET_COMPOSING_TEXT: + text = android_decode_utf16 (event->ime.text, + event->ime.length); + xfree (event->ime.text); + break; + + default: + break; + } + + /* Finally, perform the appropriate conversion action. */ + + switch (event->ime.operation) + { + case ANDROID_IME_COMMIT_TEXT: + commit_text (f, text, event->ime.position, + event->ime.counter); + break; + + case ANDROID_IME_DELETE_SURROUNDING_TEXT: + delete_surrounding_text (f, event->ime.start, + event->ime.end, + event->ime.counter); + break; + + case ANDROID_IME_FINISH_COMPOSING_TEXT: + finish_composing_text (f, event->ime.counter); + break; + + case ANDROID_IME_SET_COMPOSING_TEXT: + set_composing_text (f, text, event->ime.position, + event->ime.counter); + break; + + case ANDROID_IME_SET_COMPOSING_REGION: + set_composing_region (f, event->ime.start, + event->ime.end, + event->ime.counter); + break; + + case ANDROID_IME_SET_POINT: + textconv_set_point (f, event->ime.position, + event->ime.counter); + break; + + case ANDROID_IME_START_BATCH_EDIT: + start_batch_edit (f, event->ime.counter); + break; + + case ANDROID_IME_END_BATCH_EDIT: + end_batch_edit (f, event->ime.counter); + break; + } +} + static int handle_one_android_event (struct android_display_info *dpyinfo, union android_event *event, int *finish, @@ -745,6 +853,17 @@ handle_one_android_event (struct android_display_info *dpyinfo, case ANDROID_WINDOW_ACTION: + /* This is a special event sent by android_run_in_emacs_thread + used to make Android run stuff. */ + + if (!event->xaction.window && !event->xaction.action) + { + /* Check for and run anything the UI thread wants to run on the main + thread. */ + android_check_query (); + goto OTHER; + } + f = any; if (event->xaction.action == 0) @@ -1334,6 +1453,19 @@ handle_one_android_event (struct android_display_info *dpyinfo, goto OTHER; + /* Input method events. textconv.c functions are called here to + queue events, which are then executed in a safe context + inside keyboard.c. */ + case ANDROID_INPUT_METHOD: + + if (!any) + /* Free any text allocated for this event. */ + xfree (event->ime.text); + else + android_handle_ime_event (event, any); + + goto OTHER; + default: goto OTHER; } @@ -4148,6 +4280,904 @@ android_draw_window_divider (struct window *w, int x0, int x1, int y0, int y1) +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#else +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-prototypes" +#endif + +/* Input method related functions. Some of these are called from Java + within the UI thread. */ + +/* A counter used to decide when an editing request completes. */ +static unsigned long edit_counter; + +/* The last counter known to have completed. */ +static unsigned long last_edit_counter; + +/* Semaphore posted every time the counter increases. */ +static sem_t edit_sem; + +/* Try to synchronize with the UI thread, waiting a certain amount of + time for outstanding editing requests to complete. + + Every time one of the text retrieval functions is called and an + editing request is made, Emacs gives the main thread approximately + 50 ms to process it, in order to mostly keep the input method in + sync with the buffer contents. */ + +static void +android_sync_edit (void) +{ + struct timespec start, end, rem; + + if (__atomic_load_n (&last_edit_counter, + __ATOMIC_SEQ_CST) + == edit_counter) + return; + + start = current_timespec (); + end = timespec_add (start, make_timespec (0, 50000000)); + + while (true) + { + rem = timespec_sub (end, current_timespec ()); + + /* Timeout. */ + if (timespec_sign (rem) < 0) + break; + + if (__atomic_load_n (&last_edit_counter, + __ATOMIC_SEQ_CST) + == edit_counter) + break; + + sem_timedwait (&edit_sem, &end); + } +} + +/* Return a copy of the specified Java string and its length in + *LENGTH. Use the JNI environment ENV. Value is NULL if copying + *the string fails. */ + +static unsigned short * +android_copy_java_string (JNIEnv *env, jstring string, size_t *length) +{ + jsize size, i; + const jchar *java; + unsigned short *buffer; + + size = (*env)->GetStringLength (env, string); + buffer = malloc (size * sizeof *buffer); + + if (!buffer) + return NULL; + + java = (*env)->GetStringChars (env, string, NULL); + + if (!java) + { + free (buffer); + return NULL; + } + + for (i = 0; i < size; ++i) + buffer[i] = java[i]; + + *length = size; + (*env)->ReleaseStringChars (env, string, java); + return buffer; +} + +JNIEXPORT void JNICALL +NATIVE_NAME (beginBatchEdit) (JNIEnv *env, jobject object, jshort window) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_START_BATCH_EDIT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (endBatchEdit) (JNIEnv *env, jobject object, jshort window) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_END_BATCH_EDIT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (commitCompletion) (JNIEnv *env, jobject object, jshort window, + jstring completion_text, jint position) +{ + union android_event event; + unsigned short *text; + size_t length; + + /* First, obtain a copy of the Java string. */ + text = android_copy_java_string (env, completion_text, &length); + + if (!text) + return; + + /* Next, populate the event. Events will always eventually be + delivered on Android, so handle_one_android_event can be relied + on to free text. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_COMMIT_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = min (length, PTRDIFF_MAX); + event.ime.position = position; + event.ime.text = text; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (commitText) (JNIEnv *env, jobject object, jshort window, + jstring commit_text, jint position) +{ + union android_event event; + unsigned short *text; + size_t length; + + /* First, obtain a copy of the Java string. */ + text = android_copy_java_string (env, commit_text, &length); + + if (!text) + return; + + /* Next, populate the event. Events will always eventually be + delivered on Android, so handle_one_android_event can be relied + on to free text. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_COMMIT_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = min (length, PTRDIFF_MAX); + event.ime.position = position; + event.ime.text = text; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (deleteSurroundingText) (JNIEnv *env, jobject object, + jshort window, jint left_length, + jint right_length) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_DELETE_SURROUNDING_TEXT; + event.ime.start = left_length; + event.ime.end = right_length; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (finishComposingText) (JNIEnv *env, jobject object, + jshort window) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_FINISH_COMPOSING_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +/* Structure describing the context used for a text query. */ + +struct android_conversion_query_context +{ + /* The conversion request. */ + struct textconv_callback_struct query; + + /* The window the request is being made on. */ + android_window window; + + /* Whether or not the request was successful. */ + bool success; +}; + +/* Obtain the text from the frame whose window is that specified in + DATA using the text conversion query specified there. + + Adjust the query position to skip over any active composing region. + + Set ((struct android_conversion_query_context *) DATA)->success on + success. */ + +static void +android_perform_conversion_query (void *data) +{ + struct android_conversion_query_context *context; + struct frame *f; + + context = data; + + /* Find the frame associated with the window. */ + f = android_window_to_frame (NULL, context->window); + + if (!f) + return; + + textconv_query (f, &context->query, + TEXTCONV_SKIP_CONVERSION_REGION); + + /* context->query.text will have been set even if textconv_query + returns 1. */ + + context->success = true; +} + +/* Convert a string BUFFERS containing N characters in Emacs's + internal multibyte encoding to a Java string utilizing the + specified JNI environment. + + If N is equal to BYTES, then BUFFER is a single byte buffer. + Otherwise, BUFFER is a multibyte buffer. + + Make sure N and BYTES are absolutely correct, or you are asking for + trouble. + + Value is the string upon success, NULL otherwise. Any exceptions + generated are not cleared. */ + +static jstring +android_text_to_string (JNIEnv *env, char *buffer, ptrdiff_t n, + ptrdiff_t bytes) +{ + jchar *utf16; + size_t size, index; + jstring string; + int encoded; + + if (n == bytes) + { + /* This buffer holds no multibyte characters. */ + + if (INT_MULTIPLY_WRAPV (n, sizeof *utf16, &size)) + return NULL; + + utf16 = malloc (size); + index = 0; + + if (!utf16) + return NULL; + + while (n--) + { + utf16[index] = buffer[index]; + index++; + } + + string = (*env)->NewString (env, utf16, bytes); + free (utf16); + + return string; + } + + /* Allocate enough to hold N characters. */ + + if (INT_MULTIPLY_WRAPV (n, sizeof *utf16, &size)) + return NULL; + + utf16 = malloc (size); + index = 0; + + if (!utf16) + return NULL; + + while (n--) + { + eassert (CHAR_HEAD_P (*buffer)); + encoded = STRING_CHAR ((unsigned char *) buffer); + + /* Now figure out how to save ENCODED into the string. + Emacs operates on multibyte characters, not UTF-16 + characters with surrogate pairs as Android does. + + However, character positions in Java are represented in 2 + byte units, meaning that the text position reported to + Android can become out of sync if characters are found in a + buffer that require surrogate pairs. + + The hack used by Emacs is to simply replace each multibyte + character that doesn't fit in a jchar with the NULL + character. */ + + if (encoded >= 65536) + encoded = 0; + + utf16[index++] = encoded; + buffer += BYTES_BY_CHAR_HEAD (*buffer); + } + + /* Create the string. */ + string = (*env)->NewString (env, utf16, index); + free (utf16); + return string; +} + +JNIEXPORT jstring JNICALL +NATIVE_NAME (getSelectedText) (JNIEnv *env, jobject object, + jshort window) +{ + return NULL; +} + +JNIEXPORT jstring JNICALL +NATIVE_NAME (getTextAfterCursor) (JNIEnv *env, jobject object, jshort window, + jint length, jint flags) +{ + struct android_conversion_query_context context; + jstring string; + + /* First, set up the conversion query. */ + context.query.position = 0; + context.query.direction = TEXTCONV_FORWARD_CHAR; + context.query.factor = min (length, 65535); + context.query.operation = TEXTCONV_RETRIEVAL; + + /* Next, set the rest of the context. */ + context.window = window; + context.success = false; + + /* Now try to perform the query. */ + android_sync_edit (); + if (android_run_in_emacs_thread (android_perform_conversion_query, + &context)) + return NULL; + + if (!context.success) + return NULL; + + /* context->query.text now contains the text in Emacs's internal + UTF-8 based encoding. + + Convert it to Java's UTF-16 encoding, which is the same as + UTF-16, except that NULL bytes are encoded as surrogate pairs. + + This assumes that `free' can free data allocated with xmalloc. */ + + string = android_text_to_string (env, context.query.text.text, + context.query.text.length, + context.query.text.bytes); + free (context.query.text.text); + + return string; +} + +JNIEXPORT jstring JNICALL +NATIVE_NAME (getTextBeforeCursor) (JNIEnv *env, jobject object, jshort window, + jint length, jint flags) +{ + struct android_conversion_query_context context; + jstring string; + + /* First, set up the conversion query. */ + context.query.position = 0; + context.query.direction = TEXTCONV_BACKWARD_CHAR; + context.query.factor = min (length, 65535); + context.query.operation = TEXTCONV_RETRIEVAL; + + /* Next, set the rest of the context. */ + context.window = window; + context.success = false; + + /* Now try to perform the query. */ + android_sync_edit (); + if (android_run_in_emacs_thread (android_perform_conversion_query, + &context)) + return NULL; + + if (!context.success) + return NULL; + + /* context->query.text now contains the text in Emacs's internal + UTF-8 based encoding. + + Convert it to Java's UTF-16 encoding, which is the same as + UTF-16, except that NULL bytes are encoded as surrogate pairs. + + This assumes that `free' can free data allocated with xmalloc. */ + + string = android_text_to_string (env, context.query.text.text, + context.query.text.length, + context.query.text.bytes); + free (context.query.text.text); + + return string; +} + +JNIEXPORT void JNICALL +NATIVE_NAME (setComposingText) (JNIEnv *env, jobject object, jshort window, + jstring composing_text, + jint new_cursor_position) +{ + union android_event event; + unsigned short *text; + size_t length; + + /* First, obtain a copy of the Java string. */ + text = android_copy_java_string (env, composing_text, &length); + + if (!text) + return; + + /* Next, populate the event. Events will always eventually be + delivered on Android, so handle_one_android_event can be relied + on to free text. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_SET_COMPOSING_TEXT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = min (length, PTRDIFF_MAX); + event.ime.position = new_cursor_position; + event.ime.text = text; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (setComposingRegion) (JNIEnv *env, jobject object, jshort window, + jint start, jint end) +{ + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_SET_COMPOSING_REGION; + event.ime.start = start; + event.ime.end = end; + event.ime.length = 0; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (setSelection) (JNIEnv *env, jobject object, jshort window, + jint start, jint end) +{ + union android_event event; + + /* While IMEs want access to the entire selection, Emacs only + supports setting the point. */ + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_SET_POINT; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = 0; + event.ime.position = start; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; +} + +/* Structure describing the context for `getSelection'. */ + +struct android_get_selection_context +{ + /* The window in question. */ + android_window window; + + /* The position of the window's point when it was last + redisplayed. */ + ptrdiff_t point; +}; + +/* Function run on the main thread by `getSelection'. + Place the character position of point in PT. */ + +static void +android_get_selection (void *data) +{ + struct android_get_selection_context *context; + struct frame *f; + struct window *w; + + context = data; + + /* Look up the associated frame and its selected window. */ + f = android_window_to_frame (NULL, context->window); + + if (!f) + context->point = -1; + else + { + w = XWINDOW (f->selected_window); + + /* Return W's point at the time of the last redisplay. This is + rather important to keep the input method consistent with the + contents of the display. */ + context->point = w->last_point; + } +} + +JNIEXPORT jint JNICALL +NATIVE_NAME (getSelection) (JNIEnv *env, jobject object, jshort window) +{ + struct android_get_selection_context context; + + context.window = window; + + android_sync_edit (); + if (android_run_in_emacs_thread (android_get_selection, + &context)) + return -1; + + if (context.point == -1) + return -1; + + return min (context.point, TYPE_MAXIMUM (jint)); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (performEditorAction) (JNIEnv *env, jobject object, + jshort window, int action) +{ + union android_event event; + + /* Undocumented behavior: performEditorAction is apparently expected + to finish composing any text. */ + + NATIVE_NAME (finishComposingText) (env, object, window); + + event.xkey.type = ANDROID_KEY_PRESS; + event.xkey.serial = ++event_serial; + event.xkey.window = window; + event.xkey.time = 0; + event.xkey.state = 0; + event.xkey.keycode = 66; + event.xkey.unicode_char = 0; + + android_write_event (&event); +} + +struct android_get_extracted_text_context +{ + /* The parameters of the request. */ + int hint_max_chars; + + /* Token for the request. */ + int token; + + /* The returned text, or NULL. */ + char *text; + + /* The size of that text in characters and bytes. */ + ptrdiff_t length, bytes; + + /* Offsets into that text. */ + ptrdiff_t start, offset; + + /* The window. */ + android_window window; +}; + +/* Return the extracted text in the extracted text context specified + by DATA. */ + +static void +android_get_extracted_text (void *data) +{ + struct android_get_extracted_text_context *request; + struct frame *f; + + request = data; + + /* Find the frame associated with the window. */ + f = android_window_to_frame (NULL, request->window); + + if (!f) + return; + + /* Now get the extracted text. */ + request->text + = get_extracted_text (f, min (request->hint_max_chars, 600), + &request->start, &request->offset, + &request->length, &request->bytes); +} + +/* Structure describing the `ExtractedTextRequest' class. + Valid only on the UI thread. */ + +struct android_extracted_text_request_class +{ + bool initialized; + jfieldID hint_max_chars; + jfieldID token; +}; + +/* Structure describing the `ExtractedText' class. + Valid only on the UI thread. */ + +struct android_extracted_text_class +{ + jclass class; + jmethodID constructor; + jfieldID partial_start_offset; + jfieldID partial_end_offset; + jfieldID selection_start; + jfieldID selection_end; + jfieldID start_offset; + jfieldID text; +}; + +JNIEXPORT jobject JNICALL +NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, + jshort window, jobject request, + jint flags) +{ + struct android_get_extracted_text_context context; + static struct android_extracted_text_request_class request_class; + static struct android_extracted_text_class text_class; + jstring string; + jclass class; + jobject object; + + /* TODO: report changes to extracted text. */ + + /* Initialize both classes if necessary. */ + + if (!request_class.initialized) + { + class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/ExtractedTextRequest")); + assert (class); + + request_class.hint_max_chars + = (*env)->GetFieldID (env, class, "hintMaxChars", "I"); + assert (request_class.hint_max_chars); + + request_class.token + = (*env)->GetFieldID (env, class, "token", "I"); + assert (request_class.token); + + request_class.initialized = true; + } + + if (!text_class.class) + { + text_class.class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/ExtractedText")); + assert (text_class.class); + + class + = text_class.class + = (*env)->NewGlobalRef (env, text_class.class); + assert (text_class.class); + + text_class.partial_start_offset + = (*env)->GetFieldID (env, class, "partialStartOffset", "I"); + text_class.partial_end_offset + = (*env)->GetFieldID (env, class, "partialEndOffset", "I"); + text_class.selection_start + = (*env)->GetFieldID (env, class, "selectionStart", "I"); + text_class.selection_end + = (*env)->GetFieldID (env, class, "selectionEnd", "I"); + text_class.start_offset + = (*env)->GetFieldID (env, class, "startOffset", "I"); + text_class.text + = (*env)->GetFieldID (env, class, "text", "Ljava/lang/CharSequence;"); + text_class.constructor + = (*env)->GetMethodID (env, class, "", "()V"); + } + + context.hint_max_chars + = (*env)->GetIntField (env, request, request_class.hint_max_chars); + context.token + = (*env)->GetIntField (env, request, request_class.token); + context.text = NULL; + context.window = window; + + android_sync_edit (); + if (android_run_in_emacs_thread (android_get_extracted_text, + &context)) + return NULL; + + if (!context.text) + return NULL; + + /* Encode the returned text. */ + string = android_text_to_string (env, context.text, context.length, + context.bytes); + free (context.text); + + if (!string) + return NULL; + + /* Create an ExtractedText object containing this information. */ + object = (*android_java_env)->NewObject (env, text_class.class, + text_class.constructor); + if (!object) + return NULL; + + (*env)->SetIntField (env, object, text_class.partial_start_offset, -1); + (*env)->SetIntField (env, object, text_class.partial_end_offset, -1); + (*env)->SetIntField (env, object, text_class.selection_start, + min (context.offset, TYPE_MAXIMUM (jint))); + (*env)->SetIntField (env, object, text_class.selection_end, + min (context.offset, TYPE_MAXIMUM (jint))); + (*env)->SetIntField (env, object, text_class.start_offset, + min (context.start, TYPE_MAXIMUM (jint))); + (*env)->SetObjectField (env, object, text_class.text, string); + return object; +} + +#ifdef __clang__ +#pragma clang diagnostic pop +#else +#pragma GCC diagnostic pop +#endif + + + +/* Tell the input method where the composing region and selection of + F's selected window is located. W should be F's selected window; + if it is NULL, then F->selected_window is used in its place. */ + +static void +android_update_selection (struct frame *f, struct window *w) +{ + ptrdiff_t start, end, point; + + if (MARKERP (f->conversion.compose_region_start)) + { + eassert (MARKERP (f->conversion.compose_region_end)); + + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + } + else + start = -1, end = -1; + + /* Now constrain START and END to the maximium size of a Java + integer. */ + start = min (start, TYPE_MAXIMUM (jint)); + end = min (end, TYPE_MAXIMUM (jint)); + + if (!w) + w = XWINDOW (f->selected_window); + + /* Figure out where the point is. */ + point = min (w->last_point, TYPE_MAXIMUM (jint)); + + /* Send the update. */ + android_update_ic (FRAME_ANDROID_WINDOW (f), point, point, + start, end); +} + +/* Notice that the input method connection to F should be reset as a + result of a change to its contents. */ + +static void +android_reset_conversion (struct frame *f) +{ + /* Reset the input method. + + Pick an appropriate ``input mode'' based on whether or not the + minibuffer window is selected; this controls whether or not + ``RET'' inserts a newline or sends an actual key event. */ + android_reset_ic (FRAME_ANDROID_WINDOW (f), + (EQ (f->selected_window, + f->minibuffer_window) + ? ANDROID_IC_MODE_ACTION + : ANDROID_IC_MODE_TEXT)); + + /* Move its selection to the specified position. */ + android_update_selection (f, NULL); +} + +/* Notice that point has moved in the F's selected window's selected + buffer. W is the window, and BUFFER is that buffer. */ + +static void +android_set_point (struct frame *f, struct window *w, + struct buffer *buffer) +{ + android_update_selection (f, w); +} + +/* Notice that the composition region on F's old selected window has + changed. */ + +static void +android_compose_region_changed (struct frame *f) +{ + android_update_selection (f, XWINDOW (f->old_selected_window)); +} + +/* Notice that the text conversion has completed. */ + +static void +android_notify_conversion (unsigned long counter) +{ + int sval; + + if (last_edit_counter < counter) + __atomic_store_n (&last_edit_counter, counter, + __ATOMIC_SEQ_CST); + + sem_getvalue (&edit_sem, &sval); + + if (sval <= 0) + sem_post (&edit_sem); +} + +/* Android text conversion interface. */ + +static struct textconv_interface text_conversion_interface = + { + android_reset_conversion, + android_set_point, + android_compose_region_changed, + android_notify_conversion, + }; + + + extern frame_parm_handler android_frame_parm_handlers[]; #endif /* !ANDROID_STUBIFY */ @@ -4327,6 +5357,11 @@ android_term_init (void) /* Set the baud rate to the same value it gets set to on X. */ baud_rate = 19200; + +#ifndef ANDROID_STUBIFY + sem_init (&edit_sem, false, 0); + register_textconv_interface (&text_conversion_interface); +#endif } diff --git a/src/coding.c b/src/coding.c index be5a0252ede..3ac4ada2939 100644 --- a/src/coding.c +++ b/src/coding.c @@ -11759,7 +11759,7 @@ syms_of_coding (void) DEFSYM (Qutf_8_unix, "utf-8-unix"); DEFSYM (Qutf_8_emacs, "utf-8-emacs"); -#if defined (WINDOWSNT) || defined (CYGWIN) +#if defined (WINDOWSNT) || defined (CYGWIN) || defined HAVE_ANDROID /* No, not utf-16-le: that one has a BOM. */ DEFSYM (Qutf_16le, "utf-16le"); #endif diff --git a/src/frame.c b/src/frame.c index a1d73d0799d..874e8c4cac1 100644 --- a/src/frame.c +++ b/src/frame.c @@ -997,6 +997,16 @@ make_frame (bool mini_p) f->select_mini_window_flag = false; /* This one should never be zero. */ f->change_stamp = 1; + +#ifdef HAVE_TEXT_CONVERSION + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + f->conversion.compose_region_overlay = Qnil; + f->conversion.batch_edit_count = 0; + f->conversion.batch_edit_flags = 0; + f->conversion.actions = NULL; +#endif + root_window = make_window (); rw = XWINDOW (root_window); if (mini_p) @@ -2264,6 +2274,13 @@ delete_frame (Lisp_Object frame, Lisp_Object force) f->terminal = 0; /* Now the frame is dead. */ unblock_input (); + /* Clear markers and overlays set by F on behalf of an input + method. */ +#ifdef HAVE_TEXT_CONVERSION + if (FRAME_WINDOW_P (f)) + reset_frame_state (f); +#endif + /* If needed, delete the terminal that this frame was on. (This must be done after the frame is killed.) */ terminal->reference_count--; diff --git a/src/frame.h b/src/frame.h index 4405c2df860..27484936ff1 100644 --- a/src/frame.h +++ b/src/frame.h @@ -76,6 +76,63 @@ enum ns_appearance_type #endif #endif /* HAVE_WINDOW_SYSTEM */ +#ifdef HAVE_TEXT_CONVERSION + +enum text_conversion_operation + { + TEXTCONV_START_BATCH_EDIT, + TEXTCONV_END_BATCH_EDIT, + TEXTCONV_COMMIT_TEXT, + TEXTCONV_FINISH_COMPOSING_TEXT, + TEXTCONV_SET_COMPOSING_TEXT, + TEXTCONV_SET_COMPOSING_REGION, + TEXTCONV_SET_POINT, + TEXTCONV_DELETE_SURROUNDING_TEXT, + }; + +/* Structure describing a single edit being performed by the input + method that should be executed in the context of + kbd_buffer_get_event. */ + +struct text_conversion_action +{ + /* The next text conversion action. */ + struct text_conversion_action *next; + + /* Any associated data. */ + Lisp_Object data; + + /* The operation being performed. */ + enum text_conversion_operation operation; + + /* Counter value. */ + unsigned long counter; +}; + +/* Structure describing the text conversion state associated with a + frame. */ + +struct text_conversion_state +{ + /* List of text conversion actions associated with this frame. */ + struct text_conversion_action *actions; + + /* Markers representing the composing region. */ + Lisp_Object compose_region_start, compose_region_end; + + /* Overlay representing the composing region. */ + Lisp_Object compose_region_overlay; + + /* The number of ongoing ``batch edits'' that are causing point + reporting to be delayed. */ + int batch_edit_count; + + /* Mask containing what must be updated after batch edits end. */ + int batch_edit_flags; +}; + +#endif + /* The structure representing a frame. */ struct frame @@ -664,6 +721,11 @@ struct frame enum ns_appearance_type ns_appearance; bool_bf ns_transparent_titlebar; #endif + +#ifdef HAVE_TEXT_CONVERSION + /* Text conversion state used by certain input methods. */ + struct text_conversion_state conversion; +#endif } GCALIGNED_STRUCT; /* Most code should use these functions to set Lisp fields in struct frame. */ diff --git a/src/keyboard.c b/src/keyboard.c index 11fa1e62c89..538fdffc199 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -44,6 +44,11 @@ along with GNU Emacs. If not, see . */ #include "atimer.h" #include "process.h" #include "menu.h" + +#ifdef HAVE_TEXT_CONVERSION +#include "textconv.h" +#endif + #include #ifdef HAVE_PTHREAD @@ -3020,6 +3025,10 @@ read_char (int commandflag, Lisp_Object map, { struct buffer *prev_buffer = current_buffer; last_input_event = c; + + /* All a `text-conversion' event does is prevent Emacs from + staying idle. It is not useful. */ + call4 (Qcommand_execute, tem, Qnil, Fvector (1, &last_input_event), Qt); if (CONSP (c) && !NILP (Fmemq (XCAR (c), Vwhile_no_input_ignore_events)) @@ -3582,6 +3591,11 @@ readable_events (int flags) return 1; #endif +#ifdef HAVE_TEXT_CONVERSION + if (detect_conversion_events ()) + return 1; +#endif + if (!(flags & READABLE_EVENTS_IGNORE_SQUEEZABLES) && some_mouse_moved ()) return 1; if (single_kboard) @@ -3914,6 +3928,11 @@ kbd_buffer_get_event (KBOARD **kbp, had_pending_selection_requests = false; #endif +#ifdef HAVE_TEXT_CONVERSION + bool had_pending_conversion_events; + + had_pending_conversion_events = false; +#endif #ifdef subprocesses if (kbd_on_hold_p () && kbd_buffer_nr_stored () < KBD_BUFFER_SIZE / 4) @@ -3977,6 +3996,13 @@ kbd_buffer_get_event (KBOARD **kbp, had_pending_selection_requests = true; break; } +#endif +#ifdef HAVE_TEXT_CONVERSION + if (detect_conversion_events ()) + { + had_pending_conversion_events = true; + break; + } #endif if (end_time) { @@ -4024,6 +4050,14 @@ kbd_buffer_get_event (KBOARD **kbp, x_handle_pending_selection_requests (); #endif +#ifdef HAVE_TEXT_CONVERSION + /* Handle pending ``text conversion'' requests from an input + method. */ + + if (had_pending_conversion_events) + handle_pending_conversion_events (); +#endif + if (CONSP (Vunread_command_events)) { Lisp_Object first; @@ -4380,12 +4414,25 @@ kbd_buffer_get_event (KBOARD **kbp, #ifdef HAVE_X_WINDOWS else if (had_pending_selection_requests) obj = Qnil; +#endif +#ifdef HAVE_TEXT_CONVERSION + /* This is an internal event used to prevent Emacs from becoming + idle immediately after a text conversion operation. */ + else if (had_pending_conversion_events) + obj = Qtext_conversion; #endif else /* We were promised by the above while loop that there was something for us to read! */ emacs_abort (); +#ifdef HAVE_TEXT_CONVERSION + /* While not implemented as keyboard commands, changes made by the + input method still mean that Emacs is no longer idle. */ + if (had_pending_conversion_events) + timer_stop_idle (); +#endif + input_pending = readable_events (0); Vlast_event_frame = internal_last_event_frame; @@ -12902,6 +12949,9 @@ See also `pre-command-hook'. */); DEFSYM (Qcoding, "coding"); DEFSYM (Qtouchscreen, "touchscreen"); +#ifdef HAVE_TEXT_CONVERSION + DEFSYM (Qtext_conversion, "text-conversion"); +#endif Fset (Qecho_area_clear_hook, Qnil); diff --git a/src/lisp.h b/src/lisp.h index 894b939b7b9..2dc51d32481 100644 --- a/src/lisp.h +++ b/src/lisp.h @@ -5230,7 +5230,10 @@ extern char *emacs_root_dir (void); #ifdef HAVE_TEXT_CONVERSION /* Defined in textconv.c. */ +extern void reset_frame_state (struct frame *); extern void report_selected_window_change (struct frame *); +extern void report_point_change (struct frame *, struct window *, + struct buffer *); #endif #ifdef HAVE_NATIVE_COMP diff --git a/src/process.c b/src/process.c index 9a8b0d7fd85..e7ccb2c604e 100644 --- a/src/process.c +++ b/src/process.c @@ -121,6 +121,7 @@ static struct rlimit nofile_limit; #ifdef HAVE_ANDROID #include "android.h" +#include "androidterm.h" #endif #ifdef HAVE_WINDOW_SYSTEM diff --git a/src/textconv.c b/src/textconv.c index d5db6d11717..b62ed7d1365 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -25,7 +25,10 @@ along with GNU Emacs. If not, see . */ ability to ``undo'' or ``edit'' previously composed text. This is most commonly seen in input methods for CJK laguages for X Windows, and is extensively used throughout Android by input methods for all - kinds of scripts. */ + kinds of scripts. + + In addition, these input methods may also need to make detailed + edits to the content of a buffer. That is also handled here. */ #include @@ -44,6 +47,15 @@ along with GNU Emacs. If not, see . */ static struct textconv_interface *text_interface; +/* Flags used to determine what must be sent after a batch edit + ends. */ + +enum textconv_batch_edit_flags + { + PENDING_POINT_CHANGE = 1, + PENDING_COMPOSE_CHANGE = 2, + }; + /* Copy the portion of the current buffer described by BEG, BEG_BYTE, @@ -94,14 +106,18 @@ copy_buffer (ptrdiff_t beg, ptrdiff_t beg_byte, Then, either delete that text from the buffer if QUERY->operation is TEXTCONV_SUBSTITUTION, or return 0. + If FLAGS & TEXTCONV_SKIP_CONVERSION_REGION, then first move PT past + the conversion region in the specified direction if it is inside. + Value is 0 if QUERY->operation was not TEXTCONV_SUBSTITUTION or if deleting the text was successful, and 1 otherwise. */ int -textconv_query (struct frame *f, struct textconv_callback_struct *query) +textconv_query (struct frame *f, struct textconv_callback_struct *query, + int flags) { specpdl_ref count; - ptrdiff_t pos, pos_byte, end, end_byte; + ptrdiff_t pos, pos_byte, end, end_byte, start; ptrdiff_t temp, temp1; char *buffer; @@ -113,14 +129,46 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query) /* Inhibit quitting. */ specbind (Qinhibit_quit, Qt); - /* Temporarily switch to F's selected window. */ - Fselect_window (f->selected_window, Qt); + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window ((WINDOW_LIVE_P (f->old_selected_window) + ? f->old_selected_window + : f->selected_window), Qt); /* Now find the appropriate text bounds for QUERY. First, move point QUERY->position steps forward or backwards. */ pos = PT; + /* Next, if POS lies within the conversion region and the caller + asked for it to be moved away, move it away from the conversion + region. */ + + if (flags & TEXTCONV_SKIP_CONVERSION_REGION + && MARKERP (f->conversion.compose_region_start)) + { + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + + if (pos >= start && pos < end) + { + switch (query->direction) + { + case TEXTCONV_FORWARD_CHAR: + case TEXTCONV_FORWARD_WORD: + case TEXTCONV_CARET_DOWN: + case TEXTCONV_NEXT_LINE: + case TEXTCONV_LINE_START: + pos = end; + break; + + default: + pos = max (BEGV, start - 1); + break; + } + } + } + /* If pos is outside the accessible part of the buffer or if it overflows, move back to point or to the extremes of the accessible region. */ @@ -287,6 +335,828 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query) return 0; } +/* Reset F's text conversion state. Delete any overlays or + markers inside. */ + +void +reset_frame_state (struct frame *f) +{ + struct text_conversion_action *last, *next; + + /* Make the composition region markers point elsewhere. */ + + if (!NILP (f->conversion.compose_region_start)) + { + Fset_marker (f->conversion.compose_region_start, Qnil, Qnil); + Fset_marker (f->conversion.compose_region_end, Qnil, Qnil); + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + } + + /* Delete the composition region overlay. */ + + if (!NILP (f->conversion.compose_region_overlay)) + Fdelete_overlay (f->conversion.compose_region_overlay); + + /* Delete each text conversion action queued up. */ + + next = f->conversion.actions; + while (next) + { + last = next; + next = next->next; + + /* Say that the conversion is finished. */ + if (text_interface && text_interface->notify_conversion) + text_interface->notify_conversion (last->counter); + + xfree (last); + } + f->conversion.actions = NULL; + + /* Clear batch edit state. */ + f->conversion.batch_edit_count = 0; + f->conversion.batch_edit_flags = 0; +} + +/* Return whether or not there are pending edits from an input method + on any frame. */ + +bool +detect_conversion_events (void) +{ + Lisp_Object tail, frame; + + FOR_EACH_FRAME (tail, frame) + { + if (XFRAME (frame)->conversion.actions) + return true; + } + + return false; +} + +/* Restore the selected window WINDOW. */ + +static void +restore_selected_window (Lisp_Object window) +{ + /* FIXME: not sure what to do if WINDOW has been deleted. */ + Fselect_window (window, Qt); +} + +/* Commit the given text in the composing region. If there is no + composing region, then insert the text after F's selected window's + last point instead. Finally, remove the composing region. */ + +static void +really_commit_text (struct frame *f, EMACS_INT position, + Lisp_Object text) +{ + specpdl_ref count; + ptrdiff_t wanted, start, end; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + /* Now detect whether or not there is a composing region. + If there is, then replace it with TEXT. Don't do that + otherwise. */ + + if (MARKERP (f->conversion.compose_region_start)) + { + /* Replace its contents. */ + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + safe_del_range (start, end); + Finsert (1, &text); + + /* Move to a the position specified in POSITION. */ + + if (position < 0) + { + wanted + = marker_position (f->conversion.compose_region_start); + + if (INT_SUBTRACT_WRAPV (wanted, position, &wanted) + || wanted < BEGV) + wanted = BEGV; + + if (wanted > ZV) + wanted = ZV; + + set_point (wanted); + } + else + { + wanted + = marker_position (f->conversion.compose_region_end); + + if (INT_ADD_WRAPV (wanted, position - 1, &wanted) + || wanted > ZV) + wanted = ZV; + + if (wanted < BEGV) + wanted = BEGV; + + set_point (wanted); + } + + /* Make the composition region markers point elsewhere. */ + + if (!NILP (f->conversion.compose_region_start)) + { + Fset_marker (f->conversion.compose_region_start, Qnil, Qnil); + Fset_marker (f->conversion.compose_region_end, Qnil, Qnil); + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + } + + /* Delete the composition region overlay. */ + + if (!NILP (f->conversion.compose_region_overlay)) + Fdelete_overlay (f->conversion.compose_region_overlay); + } + else + { + /* Otherwise, move the text and point to an appropriate + location. */ + wanted = PT; + Finsert (1, &text); + + if (position < 0) + { + if (INT_SUBTRACT_WRAPV (wanted, position, &wanted) + || wanted < BEGV) + wanted = BEGV; + + if (wanted > ZV) + wanted = ZV; + + set_point (wanted); + } + else + { + wanted = PT; + + if (INT_ADD_WRAPV (wanted, position - 1, &wanted) + || wanted > ZV) + wanted = ZV; + + if (wanted < BEGV) + wanted = BEGV; + + set_point (wanted); + } + } + + unbind_to (count, Qnil); +} + +/* Remove the composition region on the frame F, while leaving its + contents intact. */ + +static void +really_finish_composing_text (struct frame *f) +{ + if (!NILP (f->conversion.compose_region_start)) + { + Fset_marker (f->conversion.compose_region_start, Qnil, Qnil); + Fset_marker (f->conversion.compose_region_end, Qnil, Qnil); + f->conversion.compose_region_start = Qnil; + f->conversion.compose_region_end = Qnil; + } + + /* Delete the composition region overlay. */ + + if (!NILP (f->conversion.compose_region_overlay)) + Fdelete_overlay (f->conversion.compose_region_overlay); +} + +/* Set the composing text on F to TEXT. Then, move point to an + appropriate position relative to POSITION, and call + `compose_region_changed' in the text conversion interface should + point not have been changed relative to F's old selected window's + last point. */ + +static void +really_set_composing_text (struct frame *f, ptrdiff_t position, + Lisp_Object text) +{ + specpdl_ref count; + ptrdiff_t start, wanted, end; + struct window *w; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + w = XWINDOW (f->old_selected_window); + Fselect_window (f->old_selected_window, Qt); + + /* Now set up the composition region if necessary. */ + + if (!MARKERP (f->conversion.compose_region_start)) + { + f->conversion.compose_region_start = Fmake_marker (); + f->conversion.compose_region_end = Fmake_marker (); + Fset_marker (f->conversion.compose_region_start, + Fpoint (), Qnil); + Fset_marker (f->conversion.compose_region_end, + Fpoint (), Qnil); + Fset_marker_insertion_type (f->conversion.compose_region_end, + Qt); + } + else + { + /* Delete the text between the start of the composition region + and its end. TODO: avoid this widening. */ + record_unwind_protect (save_restriction_restore, + save_restriction_save ()); + Fwiden (); + start = marker_position (f->conversion.compose_region_start); + end = marker_position (f->conversion.compose_region_end); + safe_del_range (start, end); + set_point (start); + } + + /* Insert the new text. */ + Finsert (1, &text); + + /* Now move point to an appropriate location. */ + if (position < 0) + { + wanted = start; + + if (INT_SUBTRACT_WRAPV (wanted, position, &wanted) + || wanted < BEGV) + wanted = BEGV; + + if (wanted > ZV) + wanted = ZV; + } + else + { + end = marker_position (f->conversion.compose_region_end); + wanted = end; + + /* end should be PT after the edit. */ + eassert (end == PT); + + if (INT_ADD_WRAPV (wanted, position - 1, &wanted) + || wanted > ZV) + wanted = ZV; + + if (wanted < BEGV) + wanted = BEGV; + } + + set_point (wanted); + + /* If PT hasn't changed, the conversion region definitely has. + Otherwise, redisplay will update the input method instead. */ + + if (PT == w->last_point + && text_interface + && text_interface->compose_region_changed) + { + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_flags |= PENDING_COMPOSE_CHANGE; + else + text_interface->compose_region_changed (f); + } + + unbind_to (count, Qnil); +} + +/* Set the composing region to START by END. Make it that it is not + already set. */ + +static void +really_set_composing_region (struct frame *f, ptrdiff_t start, + ptrdiff_t end) +{ + specpdl_ref count; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + /* If MAX (0, start) == end, then this should behave the same as + really_finish_composing_text. */ + + if (max (0, start) == max (0, end)) + { + really_finish_composing_text (f); + return; + } + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + /* Now set up the composition region if necessary. */ + + if (!MARKERP (f->conversion.compose_region_start)) + { + f->conversion.compose_region_start = Fmake_marker (); + f->conversion.compose_region_end = Fmake_marker (); + Fset_marker_insertion_type (f->conversion.compose_region_end, + Qt); + } + + Fset_marker (f->conversion.compose_region_start, + make_fixnum (start), Qnil); + Fset_marker (f->conversion.compose_region_end, + make_fixnum (end), Qnil); + + unbind_to (count, Qnil); +} + +/* Delete LEFT and RIGHT chars around point. */ + +static void +really_delete_surrounding_text (struct frame *f, ptrdiff_t left, + ptrdiff_t right) +{ + specpdl_ref count; + ptrdiff_t start, end; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + start = max (BEGV, PT - left); + end = min (ZV, PT + right); + + safe_del_range (start, end); + unbind_to (count, Qnil); +} + +/* Set point in F to POSITION. + + If it has not changed, signal an update through the text input + interface, which is necessary for the IME to acknowledge that the + change has completed. */ + +static void +really_set_point (struct frame *f, ptrdiff_t point) +{ + specpdl_ref count; + + /* If F's old selected window is no longer live, fail. */ + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return; + + count = SPECPDL_INDEX (); + record_unwind_protect (restore_selected_window, + selected_window); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + if (point == PT) + { + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_flags |= PENDING_POINT_CHANGE; + else + text_interface->point_changed (f, + XWINDOW (f->old_selected_window), + current_buffer); + } + else + /* Set the point. */ + Fgoto_char (make_fixnum (point)); + + unbind_to (count, Qnil); +} + +/* Complete the edit specified by the counter value inside *TOKEN. */ + +static void +complete_edit (void *token) +{ + if (text_interface && text_interface->notify_conversion) + text_interface->notify_conversion (*(unsigned long *) token); +} + +/* Process and free the text conversion ACTION. F must be the frame + on which ACTION will be performed. */ + +static void +handle_pending_conversion_events_1 (struct frame *f, + struct text_conversion_action *action) +{ + Lisp_Object data; + enum text_conversion_operation operation; + struct buffer *buffer; + struct window *w; + specpdl_ref count; + unsigned long token; + + /* Next, process this action and free it. */ + + data = action->data; + operation = action->operation; + token = action->counter; + xfree (action); + + /* Make sure completion is signalled. */ + count = SPECPDL_INDEX (); + record_unwind_protect_ptr (complete_edit, &token); + + switch (operation) + { + case TEXTCONV_START_BATCH_EDIT: + f->conversion.batch_edit_count++; + break; + + case TEXTCONV_END_BATCH_EDIT: + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_count--; + + if (!WINDOW_LIVE_P (f->old_selected_window)) + break; + + if (f->conversion.batch_edit_flags & PENDING_POINT_CHANGE) + { + w = XWINDOW (f->old_selected_window); + buffer = XBUFFER (WINDOW_BUFFER (w)); + + text_interface->point_changed (f, w, buffer); + } + + if (f->conversion.batch_edit_flags & PENDING_COMPOSE_CHANGE) + text_interface->compose_region_changed (f); + + f->conversion.batch_edit_flags = 0; + break; + + case TEXTCONV_COMMIT_TEXT: + really_commit_text (f, XFIXNUM (XCAR (data)), XCDR (data)); + break; + + case TEXTCONV_FINISH_COMPOSING_TEXT: + really_finish_composing_text (f); + break; + + case TEXTCONV_SET_COMPOSING_TEXT: + really_set_composing_text (f, XFIXNUM (XCAR (data)), + XCDR (data)); + break; + + case TEXTCONV_SET_COMPOSING_REGION: + really_set_composing_region (f, XFIXNUM (XCAR (data)), + XFIXNUM (XCDR (data))); + break; + + case TEXTCONV_SET_POINT: + really_set_point (f, XFIXNUM (data)); + break; + + case TEXTCONV_DELETE_SURROUNDING_TEXT: + really_delete_surrounding_text (f, XFIXNUM (XCAR (data)), + XFIXNUM (XCDR (data))); + break; + } + + unbind_to (count, Qnil); +} + +/* Process any outstanding text conversion events. + This may run Lisp or signal. */ + +void +handle_pending_conversion_events (void) +{ + struct frame *f; + Lisp_Object tail, frame; + struct text_conversion_action *action, *next; + bool handled; + + handled = false; + + FOR_EACH_FRAME (tail, frame) + { + f = XFRAME (frame); + + /* Test if F has any outstanding conversion events. Then + process them in bottom to up order. */ + for (action = f->conversion.actions; action; action = next) + { + /* Redisplay in between if there is more than one + action. */ + + if (handled) + redisplay (); + + /* Unlink this action. */ + next = action->next; + f->conversion.actions = next; + + /* Handle and free the action. */ + handle_pending_conversion_events_1 (f, action); + handled = true; + } + } +} + +/* Start a ``batch edit'' in F. During a batch edit, point_changed + will not be called until the batch edit ends. + + Process the actual operation in the event loop in keyboard.c; then, + call `notify_conversion' in the text conversion interface with + COUNTER. */ + +void +start_batch_edit (struct frame *f, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_START_BATCH_EDIT; + action->data = Qnil; + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* End a ``batch edit''. It is ok to call this function even if a + batch edit has not yet started, in which case it does nothing. + + COUNTER means the same as in `start_batch_edit'. */ + +void +end_batch_edit (struct frame *f, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_END_BATCH_EDIT; + action->data = Qnil; + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Insert the specified STRING into F's current buffer's composition + region, and set point to POSITION relative to STRING. + + COUNTER means the same as in `start_batch_edit'. */ + +void +commit_text (struct frame *f, Lisp_Object string, + ptrdiff_t position, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_COMMIT_TEXT; + action->data = Fcons (make_fixnum (position), string); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Remove the composition region and its overlay from F's current + buffer. Leave the text being composed intact. + + COUNTER means the same as in `start_batch_edit'. */ + +void +finish_composing_text (struct frame *f, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_FINISH_COMPOSING_TEXT; + action->data = Qnil; + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Insert the given STRING and make it the currently active + composition. + + If there is currently no composing region, then the new value of + point is used as the composing region. + + Then, the composing region is replaced with the text in the + specified string. + + Finally, move point to new_point, which is relative to either the + start or the end of OBJECT depending on whether or not it is less + than zero. + + COUNTER means the same as in `start_batch_edit'. */ + +void +set_composing_text (struct frame *f, Lisp_Object object, + ptrdiff_t new_point, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_SET_COMPOSING_TEXT; + action->data = Fcons (make_fixnum (new_point), + object); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Make the region between START and END the currently active + ``composing region''. + + The ``composing region'' is a region of text in the buffer that is + about to undergo editing by the input method. */ + +void +set_composing_region (struct frame *f, ptrdiff_t start, + ptrdiff_t end, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + if (start > MOST_POSITIVE_FIXNUM) + start = MOST_POSITIVE_FIXNUM; + + if (end > MOST_POSITIVE_FIXNUM) + end = MOST_POSITIVE_FIXNUM; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_SET_COMPOSING_REGION; + action->data = Fcons (make_fixnum (start), + make_fixnum (end)); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Move point in F's selected buffer to POINT. + + COUNTER means the same as in `start_batch_edit'. */ + +void +textconv_set_point (struct frame *f, ptrdiff_t point, + unsigned long counter) +{ + struct text_conversion_action *action, **last; + + if (point > MOST_POSITIVE_FIXNUM) + point = MOST_POSITIVE_FIXNUM; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_SET_POINT; + action->data = make_fixnum (point); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Delete LEFT and RIGHT characters around point in F's old selected + window. */ + +void +delete_surrounding_text (struct frame *f, ptrdiff_t left, + ptrdiff_t right, unsigned long counter) +{ + struct text_conversion_action *action, **last; + + action = xmalloc (sizeof *action); + action->operation = TEXTCONV_DELETE_SURROUNDING_TEXT; + action->data = Fcons (make_fixnum (left), + make_fixnum (right)); + action->next = NULL; + action->counter = counter; + for (last = &f->conversion.actions; *last; last = &(*last)->next) + ;; + *last = action; + input_pending = true; +} + +/* Return N characters of text around point in F's old selected + window. + + Set *N to the actual number of characters returned, *START_RETURN + to the position of the first character returned, *OFFSET to the + offset of point within that text, *LENGTH to the actual number of + characters returned, and *BYTES to the actual number of bytes + returned. + + Value is NULL upon failure, and a malloced string upon success. */ + +char * +get_extracted_text (struct frame *f, ptrdiff_t n, + ptrdiff_t *start_return, + ptrdiff_t *offset, ptrdiff_t *length, + ptrdiff_t *bytes) +{ + specpdl_ref count; + ptrdiff_t start, end, start_byte, end_byte; + char *buffer; + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return NULL; + + /* Save the excursion, as there will be extensive changes to the + selected window. */ + count = SPECPDL_INDEX (); + record_unwind_protect_excursion (); + + /* Inhibit quitting. */ + specbind (Qinhibit_quit, Qt); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + Fselect_window (f->old_selected_window, Qt); + + /* Figure out the bounds of the text to return. */ + start = PT - n / 2; + end = PT + n - n / 2; + start = max (start, BEGV); + end = min (end, ZV); + buffer = NULL; + + /* Detect overflow. */ + + if (!(start <= PT <= end)) + goto finish; + + /* Convert the character positions to byte positions. */ + start_byte = CHAR_TO_BYTE (start); + end_byte = CHAR_TO_BYTE (end); + + /* Extract the text from the buffer. */ + buffer = xmalloc (end_byte - start_byte); + copy_buffer (start, start_byte, end, end_byte, + buffer); + + /* Return the offsets. */ + *start_return = start; + *offset = PT - start; + *length = end - start; + *bytes = end_byte - start_byte; + + finish: + unbind_to (count, Qnil); + return buffer; +} + /* Window system interface. These are called from the rest of @@ -298,16 +1168,40 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query) void report_selected_window_change (struct frame *f) { + reset_frame_state (f); + if (!text_interface) return; text_interface->reset (f); } +/* Notice that the point in F's selected window's current buffer has + changed. + + F is the frame whose selected window was changed, W is the window + in question, and BUFFER is that window's current buffer. + + Tell the text conversion interface about the change; it will likely + pass the information on to the system input method. */ + +void +report_point_change (struct frame *f, struct window *window, + struct buffer *buffer) +{ + if (!text_interface || !text_interface->point_changed) + return; + + if (f->conversion.batch_edit_count > 0) + f->conversion.batch_edit_flags |= PENDING_POINT_CHANGE; + else + text_interface->point_changed (f, window, buffer); +} + /* Register INTERFACE as the text conversion interface. */ void -register_texconv_interface (struct textconv_interface *interface) +register_textconv_interface (struct textconv_interface *interface) { text_interface = interface; } diff --git a/src/textconv.h b/src/textconv.h index f6e7eb7925f..ce003847637 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -34,6 +34,20 @@ struct textconv_interface happen if the window is deleted or switches buffers, or an unexpected buffer change occurs.) */ void (*reset) (struct frame *); + + /* Notice that point has moved in the specified frame's selected + window's selected buffer. The second argument is the window + whose point changed, and the third argument is the buffer. */ + void (*point_changed) (struct frame *, struct window *, + struct buffer *); + + /* Notice that the preconversion region has changed without point + being moved. */ + void (*compose_region_changed) (struct frame *); + + /* Notice that an asynch conversion identified by COUNTER has + completed. */ + void (*notify_conversion) (unsigned long); }; @@ -103,7 +117,27 @@ struct textconv_callback_struct struct textconv_conversion_text text; }; -extern int textconv_query (struct frame *, struct textconv_callback_struct *); -extern void register_texconv_interface (struct textconv_interface *); +#define TEXTCONV_SKIP_CONVERSION_REGION (1 << 0) + +extern int textconv_query (struct frame *, struct textconv_callback_struct *, + int); +extern bool detect_conversion_events (void); +extern void handle_pending_conversion_events (void); +extern void start_batch_edit (struct frame *, unsigned long); +extern void end_batch_edit (struct frame *, unsigned long); +extern void commit_text (struct frame *, Lisp_Object, ptrdiff_t, + unsigned long); +extern void finish_composing_text (struct frame *, unsigned long); +extern void set_composing_text (struct frame *, Lisp_Object, + ptrdiff_t, unsigned long); +extern void set_composing_region (struct frame *, ptrdiff_t, ptrdiff_t, + unsigned long); +extern void textconv_set_point (struct frame *, ptrdiff_t, unsigned long); +extern void delete_surrounding_text (struct frame *, ptrdiff_t, + ptrdiff_t, unsigned long); +extern char *get_extracted_text (struct frame *, ptrdiff_t, ptrdiff_t *, + ptrdiff_t *, ptrdiff_t *, ptrdiff_t *); + +extern void register_textconv_interface (struct textconv_interface *); #endif /* _TEXTCONV_H_ */ diff --git a/src/xdisp.c b/src/xdisp.c index 8bbb80b1c8c..4b91fcf31ca 100644 --- a/src/xdisp.c +++ b/src/xdisp.c @@ -17267,6 +17267,9 @@ static void mark_window_display_accurate_1 (struct window *w, bool accurate_p) { struct buffer *b = XBUFFER (w->contents); +#ifdef HAVE_TEXT_CONVERSION + ptrdiff_t prev_point; +#endif w->last_modified = accurate_p ? BUF_MODIFF (b) : 0; w->last_overlay_modified = accurate_p ? BUF_OVERLAY_MODIFF (b) : 0; @@ -17296,11 +17299,36 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) w->last_cursor_vpos = w->cursor.vpos; w->last_cursor_off_p = w->cursor_off_p; +#ifdef HAVE_TEXT_CONVERSION + prev_point = w->last_point; +#endif + if (w == XWINDOW (selected_window)) w->last_point = BUF_PT (b); else w->last_point = marker_position (w->pointm); +#ifdef HAVE_TEXT_CONVERSION + /* Point motion is only propagated to the input method for use + in text conversion during a redisplay. While this can lead + to inconsistencies when point has moved but the change has + not yet been displayed, it leads to better results most of + the time, as point often changes within calls to + `save-excursion', and the only way to detect such calls is to + observe that the next redisplay never ends with those changes + applied. + + Changes to buffer text are immediately propagated to the + input method, and the position of point is also updated + during such a change, so the consequences are not that + severe. */ + + if (prev_point != w->last_point + && FRAME_WINDOW_P (WINDOW_XFRAME (w)) + && w == XWINDOW (WINDOW_XFRAME (w)->selected_window)) + report_point_change (WINDOW_XFRAME (w), w, b); +#endif + w->window_end_valid = true; w->update_mode_line = false; w->preserve_vscroll_p = false; diff --git a/src/xfns.c b/src/xfns.c index 9e004f6a678..0e4a25de04a 100644 --- a/src/xfns.c +++ b/src/xfns.c @@ -3861,7 +3861,7 @@ xic_string_conversion_callback (XIC ic, XPointer client_data, request.operation = TEXTCONV_RETRIEVAL; /* Now perform the string conversion. */ - rc = textconv_query (f, &request); + rc = textconv_query (f, &request, 0); if (rc) { diff --git a/src/xterm.c b/src/xterm.c index ccaacec2c57..49b88de2759 100644 --- a/src/xterm.c +++ b/src/xterm.c @@ -31615,7 +31615,7 @@ init_xterm (void) #endif #ifdef HAVE_X_I18N - register_texconv_interface (&text_conversion_interface); + register_textconv_interface (&text_conversion_interface); #endif } -- cgit v1.3 From 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. --- doc/emacs/input.texi | 3 + doc/lispref/commands.texi | 48 +++- java/org/gnu/emacs/EmacsInputConnection.java | 68 ++++-- java/org/gnu/emacs/EmacsService.java | 14 +- lisp/bindings.el | 2 +- lisp/electric.el | 1 + lisp/progmodes/prog-mode.el | 2 + lisp/simple.el | 52 +++++ lisp/textmodes/text-mode.el | 6 + src/androidterm.c | 88 ++++++-- src/buffer.c | 20 ++ src/buffer.h | 11 + src/emacs.c | 3 + src/frame.h | 2 +- src/lisp.h | 1 + src/marker.c | 26 +++ src/textconv.c | 315 ++++++++++++++++++++++++--- src/textconv.h | 11 +- src/window.h | 4 + src/xdisp.c | 11 +- 20 files changed, 607 insertions(+), 81 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/input.texi b/doc/emacs/input.texi index 2463a75edcd..154b7025ff4 100644 --- a/doc/emacs/input.texi +++ b/doc/emacs/input.texi @@ -121,6 +121,9 @@ screen, and send the appropriate key events to Emacs after completion. However, on screen keyboard input methods directly perform edits to the selected window of each frame; this is known as ``text conversion'', or ``string conversion'' under the X Window System. +Emacs enables these input methods whenever the buffer local value of +@code{text-conversion-style} is non-@code{nil}, normally inside +derivatives of @code{text-mode} and @code{prog-mode}. Text conversion is performed asynchronously whenever Emacs receives a request to perform the conversion from the input method. After the diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index 2807d3d61b2..5fb2217a03b 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -2205,9 +2205,51 @@ A few other event types represent occurrences within the system. This kind of event is sent @strong{after} a system-wide input method performs an edit to one or more buffers. -Once the event is sent, the input method may already have made changes -to multiple frames. @c TODO: allow querying which frames to which -@c changes have been made. +@vindex text-conversion-edits +Once the event is sent, the input method may already have made +changes to multiple buffers inside many different frames. To +determine which buffers have been changed, and what edits have +been made to them, use the variable +@code{text-conversion-edits}, which is set prior to each +@code{text-conversion} event being sent; it is a list of the +form: + +@indentedblock +@w{@code{(@var{buffer} @var{beg} @var{end} @var{ephemeral})}} +@end indentedblock + +Where @var{ephemeral} is the buffer which was modified, +@var{beg} and @var{end} are the positions of the edit at the +time it was completed, and @var{ephemeral} is either a string, +containing any text which was inserted, @code{t}, meaning that +the edit is a temporary edit made by the input method, and +@code{nil}, meaning that some text was deleted. + +@vindex text-conversion-style +Whether or not this event is sent depends on the value of the +buffer-local variable @code{text-conversion-style}, which determines +how an input method that wishes to make edits to buffer contents will +behave. + +This variable can have one three values: + +@table @code +@item nil +This means that the input method will be disabled entirely, and key +events will be sent instead of text conversion events. + +@item action +This means that the input method will be enabled, but @key{RET} will +be sent wherever the input method wanted to insert a new line. + +@item t +This, or any other value, means that the input method will be enabled +and make edits terminated by @code{text-conversion} events. +@end itemize + +Changes to the value of this variable will only take effect upon +the next redisplay after the buffer becomes the selected buffer +of a frame. @cindex @code{delete-frame} event @item (delete-frame (@var{frame})) 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); diff --git a/lisp/bindings.el b/lisp/bindings.el index 057c870958d..eec51a4e413 100644 --- a/lisp/bindings.el +++ b/lisp/bindings.el @@ -1522,7 +1522,7 @@ if `inhibit-field-text-motion' is non-nil." (define-key special-event-map [sigusr2] 'ignore) ;; Text conversion -(define-key global-map [text-conversion] 'ignore) +(define-key global-map [text-conversion] 'analyze-text-conversion) ;; Don't look for autoload cookies in this file. ;; Local Variables: diff --git a/lisp/electric.el b/lisp/electric.el index bac3f5a2b3c..3865d1d5234 100644 --- a/lisp/electric.el +++ b/lisp/electric.el @@ -294,6 +294,7 @@ or comment." ;;;###autoload (define-key global-map "\C-j" 'electric-newline-and-maybe-indent) + ;;;###autoload (defun electric-newline-and-maybe-indent () "Insert a newline. diff --git a/lisp/progmodes/prog-mode.el b/lisp/progmodes/prog-mode.el index 04071703184..7a53399ad14 100644 --- a/lisp/progmodes/prog-mode.el +++ b/lisp/progmodes/prog-mode.el @@ -336,6 +336,8 @@ support it." (setq-local require-final-newline mode-require-final-newline) (setq-local parse-sexp-ignore-comments t) (add-hook 'context-menu-functions 'prog-context-menu 10 t) + ;; Enable text conversion in this buffer. + (setq-local text-conversion-style t) ;; Any programming language is always written left to right. (setq bidi-paragraph-direction 'left-to-right)) diff --git a/lisp/simple.el b/lisp/simple.el index bed6dfb8292..6a12585a55d 100644 --- a/lisp/simple.el +++ b/lisp/simple.el @@ -10864,6 +10864,58 @@ If the buffer doesn't exist, create it first." "Change value in PLIST of PROP to VAL, comparing with `equal'." (declare (obsolete plist-put "29.1")) (plist-put plist prop val #'equal)) + + + +;; Text conversion support. See textconv.c for more details about +;; what this is. + + +;; Actually in textconv.c. +(defvar text-conversion-edits) + +(defun analyze-text-conversion () + "Analyze the results of the previous text conversion event. + +For each insertion: + + - Look for the insertion of a string starting or ending with a + character inside `auto-fill-chars', and fill the text around + it if `auto-fill-mode' is enabled. + + - Look for the insertion of a new line, and cause automatic + line breaking of the previous line when `auto-fill-mode' is + enabled. + + - Look for the insertion of a new line, and indent this new + line if `electric-indent-mode' is enabled." + (interactive) + (dolist (edit text-conversion-edits) + ;; Filter out ephemeral edits and deletions. + (when (and (not (eq (nth 1 edit) (nth 2 edit))) + (stringp (nth 3 edit))) + (with-current-buffer (car edit) + (let* ((inserted (nth 3 edit)) + ;; Get the first and last characters. + (start (aref inserted 0)) + (end (aref inserted (1- (length inserted)))) + ;; Figure out whether or not to auto-fill. + (auto-fill-p (or (aref auto-fill-chars start) + (aref auto-fill-chars end))) + ;; Figure out whether or not a newline was inserted. + (newline-p (string-search "\n" inserted))) + (save-excursion + (if (and auto-fill-function newline-p) + (progn (goto-char (nth 2 edit)) + (previous-logical-line) + (funcall auto-fill-function)) + (when (and auto-fill-function auto-fill-p) + (progn (goto-char (nth 2 edit)) + (funcall auto-fill-function))))) + (when (and electric-indent-mode newline-p) + (goto-char (nth 2 edit)) + (indent-according-to-mode))))))) + (provide 'simple) diff --git a/lisp/textmodes/text-mode.el b/lisp/textmodes/text-mode.el index 48cefc74d06..ccba1b063ab 100644 --- a/lisp/textmodes/text-mode.el +++ b/lisp/textmodes/text-mode.el @@ -41,6 +41,9 @@ "Non-nil if this buffer's major mode is a variant of Text mode.") (make-obsolete-variable 'text-mode-variant 'derived-mode-p "27.1") +;; Actually defined in textconv.c. +(defvar text-conversion-style) + (defvar text-mode-syntax-table (let ((st (make-syntax-table))) (modify-syntax-entry ?\" ". " st) @@ -125,6 +128,9 @@ You can thus get the full benefit of adaptive filling Turning on Text mode runs the normal hook `text-mode-hook'." (setq-local text-mode-variant t) (setq-local require-final-newline mode-require-final-newline) + + ;; Enable text conversion in this buffer. + (setq-local text-conversion-style t) (add-hook 'context-menu-functions 'text-mode-context-menu 10 t)) (define-derived-mode paragraph-indent-text-mode text-mode "Parindent" diff --git a/src/androidterm.c b/src/androidterm.c index 767b7d8240c..0c990d3d2d2 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -621,8 +621,9 @@ android_handle_ime_event (union android_event *event, struct frame *f) break; case ANDROID_IME_SET_POINT: - textconv_set_point (f, event->ime.position, - event->ime.counter); + textconv_set_point_and_mark (f, event->ime.start, + event->ime.end, + event->ime.counter); break; case ANDROID_IME_START_BATCH_EDIT: @@ -4305,7 +4306,7 @@ static sem_t edit_sem; Every time one of the text retrieval functions is called and an editing request is made, Emacs gives the main thread approximately - 50 ms to process it, in order to mostly keep the input method in + 100 ms to process it, in order to mostly keep the input method in sync with the buffer contents. */ static void @@ -4319,7 +4320,7 @@ android_sync_edit (void) return; start = current_timespec (); - end = timespec_add (start, make_timespec (0, 50000000)); + end = timespec_add (start, make_timespec (0, 100000000)); while (true) { @@ -4550,8 +4551,7 @@ android_perform_conversion_query (void *data) if (!f) return; - textconv_query (f, &context->query, - TEXTCONV_SKIP_CONVERSION_REGION); + textconv_query (f, &context->query, TEXTCONV_SKIP_ACTIVE_REGION); /* context->query.text will have been set even if textconv_query returns 1. */ @@ -4648,13 +4648,6 @@ android_text_to_string (JNIEnv *env, char *buffer, ptrdiff_t n, return string; } -JNIEXPORT jstring JNICALL -NATIVE_NAME (getSelectedText) (JNIEnv *env, jobject object, - jshort window) -{ - return NULL; -} - JNIEXPORT jstring JNICALL NATIVE_NAME (getTextAfterCursor) (JNIEnv *env, jobject object, jshort window, jint length, jint flags) @@ -4805,8 +4798,8 @@ NATIVE_NAME (setSelection) (JNIEnv *env, jobject object, jshort window, event.ime.serial = ++event_serial; event.ime.window = window; event.ime.operation = ANDROID_IME_SET_POINT; - event.ime.start = 0; - event.ime.end = 0; + event.ime.start = start; + event.ime.end = end; event.ime.length = 0; event.ime.position = start; event.ime.text = NULL; @@ -5068,6 +5061,34 @@ NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, return object; } +JNIEXPORT jstring JNICALL +NATIVE_NAME (getSelectedText) (JNIEnv *env, jobject object, + jshort window) +{ + struct android_get_extracted_text_context context; + jstring string; + + context.hint_max_chars = -1; + context.token = 0; + context.text = NULL; + context.window = window; + + android_sync_edit (); + if (android_run_in_emacs_thread (android_get_extracted_text, + &context)) + return NULL; + + if (!context.text) + return NULL; + + /* Encode the returned text. */ + string = android_text_to_string (env, context.text, context.length, + context.bytes); + free (context.text); + + return string; +} + #ifdef __clang__ #pragma clang diagnostic pop #else @@ -5083,7 +5104,8 @@ NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, static void android_update_selection (struct frame *f, struct window *w) { - ptrdiff_t start, end, point; + ptrdiff_t start, end, point, mark; + struct buffer *b; if (MARKERP (f->conversion.compose_region_start)) { @@ -5103,12 +5125,20 @@ android_update_selection (struct frame *f, struct window *w) if (!w) w = XWINDOW (f->selected_window); - /* Figure out where the point is. */ + /* Figure out where the point and mark are. If the mark is not + active, then point is set to equal mark. */ + b = XBUFFER (w->contents); point = min (w->last_point, TYPE_MAXIMUM (jint)); + mark = ((!NILP (BVAR (b, mark_active)) + && w->last_mark != -1) + ? min (w->last_mark, TYPE_MAXIMUM (jint)) + : point); - /* Send the update. */ - android_update_ic (FRAME_ANDROID_WINDOW (f), point, point, - start, end); + /* Send the update. Android doesn't have a concept of ``point'' and + ``mark''; instead, it only has a selection, where the start of + the selection is less than or equal to the end. */ + android_update_ic (FRAME_ANDROID_WINDOW (f), min (point, mark), + max (point, mark), start, end); } /* Notice that the input method connection to F should be reset as a @@ -5117,16 +5147,32 @@ android_update_selection (struct frame *f, struct window *w) static void android_reset_conversion (struct frame *f) { + enum android_ic_mode mode; + struct window *w; + struct buffer *buffer; + /* Reset the input method. Pick an appropriate ``input mode'' based on whether or not the minibuffer window is selected; this controls whether or not ``RET'' inserts a newline or sends an actual key event. */ + + w = XWINDOW (f->selected_window); + buffer = XBUFFER (WINDOW_BUFFER (w)); + + if (NILP (BVAR (buffer, text_conversion_style))) + mode = ANDROID_IC_MODE_NULL; + else if (EQ (BVAR (buffer, text_conversion_style), + Qaction)) + mode = ANDROID_IC_MODE_ACTION; + else + mode = ANDROID_IC_MODE_TEXT; + android_reset_ic (FRAME_ANDROID_WINDOW (f), (EQ (f->selected_window, f->minibuffer_window) ? ANDROID_IC_MODE_ACTION - : ANDROID_IC_MODE_TEXT)); + : mode)); /* Move its selection to the specified position. */ android_update_selection (f, NULL); diff --git a/src/buffer.c b/src/buffer.c index 38648519ba0..af4aa583c96 100644 --- a/src/buffer.c +++ b/src/buffer.c @@ -4710,6 +4710,7 @@ init_buffer_once (void) #ifdef HAVE_TREE_SITTER XSETFASTINT (BVAR (&buffer_local_flags, ts_parser_list), idx); ++idx; #endif + XSETFASTINT (BVAR (&buffer_local_flags, text_conversion_style), idx); ++idx; XSETFASTINT (BVAR (&buffer_local_flags, cursor_in_non_selected_windows), idx); ++idx; /* buffer_local_flags contains no pointers, so it's safe to treat it @@ -4780,6 +4781,9 @@ init_buffer_once (void) bset_extra_line_spacing (&buffer_defaults, Qnil); #ifdef HAVE_TREE_SITTER bset_ts_parser_list (&buffer_defaults, Qnil); +#endif +#ifdef HAVE_TEXT_CONVERSION + bset_text_conversion_style (&buffer_defaults, Qnil); #endif bset_cursor_in_non_selected_windows (&buffer_defaults, Qt); @@ -5852,6 +5856,22 @@ If t, displays a cursor related to the usual cursor type You can also specify the cursor type as in the `cursor-type' variable. Use Custom to set this variable and update the display. */); + /* While this is defined here, each *term.c module must implement + the logic itself. */ + + DEFVAR_PER_BUFFER ("text-conversion-style", &BVAR (current_buffer, + text_conversion_style), + Qnil, + "How the on screen keyboard's input method should insert in this buffer.\n\ +When nil, the input method will be disabled and an ordinary keyboard\n\ +will be displayed in its place.\n\ +When the symbol `action', the input method will insert text directly, but\n\ +will send `return' key events instead of inserting new line characters.\n\ +Any other value means that the input method will insert text directly.\n\ +\n\ +This variable does not take immediate effect when set; rather, it takes\n\ +effect upon the next redisplay after the selected window or buffer changes."); + DEFVAR_LISP ("kill-buffer-query-functions", Vkill_buffer_query_functions, doc: /* List of functions called with no args to query before killing a buffer. The buffer being killed will be current while the functions are running. diff --git a/src/buffer.h b/src/buffer.h index e700297a264..e71ffe28045 100644 --- a/src/buffer.h +++ b/src/buffer.h @@ -566,6 +566,11 @@ struct buffer /* A list of tree-sitter parsers for this buffer. */ Lisp_Object ts_parser_list_; #endif + + /* What type of text conversion the input method should apply to + this buffer. */ + Lisp_Object text_conversion_style_; + /* Cursor type to display in non-selected windows. t means to use hollow box cursor. See `cursor-type' for other values. */ @@ -842,6 +847,12 @@ bset_width_table (struct buffer *b, Lisp_Object val) b->width_table_ = val; } +INLINE void +bset_text_conversion_style (struct buffer *b, Lisp_Object val) +{ + b->text_conversion_style_ = val; +} + /* BUFFER_CEILING_OF (resp. BUFFER_FLOOR_OF), when applied to n, return the max (resp. min) p such that diff --git a/src/emacs.c b/src/emacs.c index d7de3c85bbe..2f953510a3d 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -2000,6 +2000,9 @@ Using an Emacs configured with --with-x-toolkit=lucid does not have this problem #ifdef HAVE_WINDOW_SYSTEM init_fringe_once (); /* Swap bitmaps if necessary. */ #endif /* HAVE_WINDOW_SYSTEM */ +#ifdef HAVE_TEXT_CONVERSION + syms_of_textconv (); +#endif } init_alloc (); diff --git a/src/frame.h b/src/frame.h index 27484936ff1..ca4cca17d74 100644 --- a/src/frame.h +++ b/src/frame.h @@ -86,7 +86,7 @@ enum text_conversion_operation TEXTCONV_FINISH_COMPOSING_TEXT, TEXTCONV_SET_COMPOSING_TEXT, TEXTCONV_SET_COMPOSING_REGION, - TEXTCONV_SET_POINT, + TEXTCONV_SET_POINT_AND_MARK, TEXTCONV_DELETE_SURROUNDING_TEXT, }; diff --git a/src/lisp.h b/src/lisp.h index 2dc51d32481..a39ca8cc541 100644 --- a/src/lisp.h +++ b/src/lisp.h @@ -5234,6 +5234,7 @@ extern void reset_frame_state (struct frame *); extern void report_selected_window_change (struct frame *); extern void report_point_change (struct frame *, struct window *, struct buffer *); +extern void syms_of_textconv (void); #endif #ifdef HAVE_NATIVE_COMP diff --git a/src/marker.c b/src/marker.c index e42c49a5434..7b15cd62f1e 100644 --- a/src/marker.c +++ b/src/marker.c @@ -23,6 +23,7 @@ along with GNU Emacs. If not, see . */ #include "lisp.h" #include "character.h" #include "buffer.h" +#include "window.h" /* Record one cached position found recently by buf_charpos_to_bytepos or buf_bytepos_to_charpos. */ @@ -566,6 +567,31 @@ set_marker_internal (Lisp_Object marker, Lisp_Object position, attach_marker (m, b, charpos, bytepos); } + +#ifdef HAVE_TEXT_CONVERSION + + /* If B is the buffer's mark and there is a window displaying B, and + text conversion is enabled while the mark is active, redisplay + the buffer. + + propagate_window_redisplay will propagate this redisplay to the + window, which will eventually reach + mark_window_display_accurate_1. At that point, + report_point_change will be told to update the mark as seen by + the input method. + + This is done all the way in (the seemingly irrelevant) redisplay + because the selection reported to the input method is actually what + is visible on screen, namely w->last_point. */ + + if (m->buffer + && EQ (marker, BVAR (m->buffer, mark)) + && !NILP (BVAR (m->buffer, mark_active)) + && buffer_window_count (m->buffer)) + bset_redisplay (m->buffer); + +#endif + return marker; } diff --git a/src/textconv.c b/src/textconv.c index a39748457d3..835d03f3037 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -89,13 +89,27 @@ copy_buffer (ptrdiff_t beg, ptrdiff_t beg_byte, size = end0 - beg0; memcpy (buffer, BYTE_POS_ADDR (beg0), size); if (beg1 != -1) - memcpy (buffer, BEG_ADDR + beg1, end1 - beg1); + memcpy (buffer + size, BEG_ADDR + beg1, end1 - beg1); } /* Conversion query. */ +/* Return the position of the active mark, or -1 if there is no mark + or it is not active. */ + +static ptrdiff_t +get_mark (void) +{ + if (!NILP (BVAR (current_buffer, mark_active)) + && XMARKER (BVAR (current_buffer, mark))->buffer) + return marker_position (BVAR (current_buffer, + mark)); + + return -1; +} + /* Perform the text conversion operation specified in QUERY and return the results. @@ -109,6 +123,9 @@ copy_buffer (ptrdiff_t beg, ptrdiff_t beg_byte, If FLAGS & TEXTCONV_SKIP_CONVERSION_REGION, then first move PT past the conversion region in the specified direction if it is inside. + If FLAGS & TEXTCONV_SKIP_ACTIVE_REGION, then also move PT past the + region if the mark is active. + Value is 0 if QUERY->operation was not TEXTCONV_SUBSTITUTION or if deleting the text was successful, and 1 otherwise. */ @@ -169,6 +186,39 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query, } } + /* Likewise for the region if the mark is active. */ + + if (flags & TEXTCONV_SKIP_ACTIVE_REGION) + { + temp = get_mark (); + + if (temp == -1) + goto escape; + + start = min (temp, PT); + end = max (temp, PT); + + if (pos >= start && pos < end) + { + switch (query->direction) + { + case TEXTCONV_FORWARD_CHAR: + case TEXTCONV_FORWARD_WORD: + case TEXTCONV_CARET_DOWN: + case TEXTCONV_NEXT_LINE: + case TEXTCONV_LINE_START: + pos = end; + break; + + default: + pos = max (BEGV, start); + break; + } + } + } + + escape: + /* If pos is outside the accessible part of the buffer or if it overflows, move back to point or to the extremes of the accessible region. */ @@ -335,6 +385,55 @@ textconv_query (struct frame *f, struct textconv_callback_struct *query, return 0; } +/* Update the overlay displaying the conversion area on F after a + change to the conversion region. */ + +static void +sync_overlay (struct frame *f) +{ + if (MARKERP (f->conversion.compose_region_start)) + { + if (NILP (f->conversion.compose_region_overlay)) + { + f->conversion.compose_region_overlay + = Fmake_overlay (f->conversion.compose_region_start, + f->conversion.compose_region_end, Qnil, + Qt, Qnil); + Foverlay_put (f->conversion.compose_region_overlay, + Qface, Qunderline); + } + + Fmove_overlay (f->conversion.compose_region_overlay, + f->conversion.compose_region_start, + f->conversion.compose_region_end, Qnil); + } + else if (!NILP (f->conversion.compose_region_overlay)) + { + Fdelete_overlay (f->conversion.compose_region_overlay); + f->conversion.compose_region_overlay = Qnil; + } +} + +/* Record a change to the current buffer as a result of an + asynchronous text conversion operation on F. + + Consult the doc string of `text-conversion-edits' for the meaning + of BEG, END, and EPHEMERAL. */ + +static void +record_buffer_change (ptrdiff_t beg, ptrdiff_t end, + Lisp_Object ephemeral) +{ + Lisp_Object buffer; + + XSETBUFFER (buffer, current_buffer); + + Vtext_conversion_edits + = Fcons (list4 (buffer, make_fixnum (beg), + make_fixnum (end), ephemeral), + Vtext_conversion_edits); +} + /* Reset F's text conversion state. Delete any overlays or markers inside. */ @@ -438,8 +537,10 @@ really_commit_text (struct frame *f, EMACS_INT position, /* Replace its contents. */ start = marker_position (f->conversion.compose_region_start); end = marker_position (f->conversion.compose_region_end); - safe_del_range (start, end); + del_range (start, end); + record_buffer_change (start, start, Qnil); Finsert (1, &text); + record_buffer_change (start, PT, text); /* Move to a the position specified in POSITION. */ @@ -493,6 +594,7 @@ really_commit_text (struct frame *f, EMACS_INT position, location. */ wanted = PT; Finsert (1, &text); + record_buffer_change (wanted, PT, text); if (position < 0) { @@ -520,6 +622,8 @@ really_commit_text (struct frame *f, EMACS_INT position, } } + /* This should deactivate the mark. */ + call0 (Qdeactivate_mark); unbind_to (count, Qnil); } @@ -575,12 +679,11 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, if (!MARKERP (f->conversion.compose_region_start)) { - f->conversion.compose_region_start = Fmake_marker (); - f->conversion.compose_region_end = Fmake_marker (); - Fset_marker (f->conversion.compose_region_start, - Fpoint (), Qnil); - Fset_marker (f->conversion.compose_region_end, - Fpoint (), Qnil); + f->conversion.compose_region_start + = build_marker (current_buffer, PT, PT_BYTE); + f->conversion.compose_region_end + = build_marker (current_buffer, PT, PT_BYTE); + Fset_marker_insertion_type (f->conversion.compose_region_end, Qt); @@ -595,13 +698,19 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, Fwiden (); start = marker_position (f->conversion.compose_region_start); end = marker_position (f->conversion.compose_region_end); - safe_del_range (start, end); + del_range (start, end); set_point (start); + + if (start != end) + record_buffer_change (start, start, Qnil); } /* Insert the new text. */ Finsert (1, &text); + if (start != PT) + record_buffer_change (start, PT, Qnil); + /* Now move point to an appropriate location. */ if (position < 0) { @@ -632,6 +741,12 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, set_point (wanted); + /* This should deactivate the mark. */ + call0 (Qdeactivate_mark); + + /* Move the composition overlay. */ + sync_overlay (f); + /* If PT hasn't changed, the conversion region definitely has. Otherwise, redisplay will update the input method instead. */ @@ -693,18 +808,21 @@ really_set_composing_region (struct frame *f, ptrdiff_t start, make_fixnum (start), Qnil); Fset_marker (f->conversion.compose_region_end, make_fixnum (end), Qnil); + sync_overlay (f); unbind_to (count, Qnil); } -/* Delete LEFT and RIGHT chars around point. */ +/* Delete LEFT and RIGHT chars around point or the active mark, + whichever is larger, avoiding the composing region if + necessary. */ static void really_delete_surrounding_text (struct frame *f, ptrdiff_t left, ptrdiff_t right) { specpdl_ref count; - ptrdiff_t start, end; + ptrdiff_t start, end, a, b, a1, b1, lstart, rstart; /* If F's old selected window is no longer live, fail. */ @@ -719,21 +837,71 @@ really_delete_surrounding_text (struct frame *f, ptrdiff_t left, redisplay. */ Fselect_window (f->old_selected_window, Qt); - start = max (BEGV, PT - left); - end = min (ZV, PT + right); + /* Figure out where to start deleting from. */ + + a = get_mark (); + + if (a != -1 && a != PT) + lstart = rstart = max (a, PT); + else + lstart = rstart = PT; + + /* Avoid the composing text. This behavior is identical to how + Android's BaseInputConnection actually implements avoiding the + composing span. */ + + if (MARKERP (f->conversion.compose_region_start)) + { + a = marker_position (f->conversion.compose_region_start); + b = marker_position (f->conversion.compose_region_end); + + a1 = min (a, b); + b1 = max (a, b); + + lstart = min (lstart, min (PT, a1)); + rstart = max (rstart, max (PT, b1)); + } + + if (lstart == rstart) + { + start = max (BEGV, lstart - left); + end = min (ZV, rstart + right); + + del_range (start, end); + record_buffer_change (start, start, Qnil); + } + else + { + start = max (BEGV, lstart - left); + end = lstart; + + del_range (start, end); + record_buffer_change (start, start, Qnil); + + start = rstart; + end = min (ZV, rstart + right); + del_range (start, end); + record_buffer_change (start, start, Qnil); + } + + /* if the mark is now equal to start, deactivate it. */ + + if (get_mark () == PT) + call0 (Qdeactivate_mark); - safe_del_range (start, end); unbind_to (count, Qnil); } -/* Set point in F to POSITION. +/* Set point in F to POSITION. If MARK is not POSITION, activate the + mark and set MARK to that as well. If it has not changed, signal an update through the text input interface, which is necessary for the IME to acknowledge that the change has completed. */ static void -really_set_point (struct frame *f, ptrdiff_t point) +really_set_point_and_mark (struct frame *f, ptrdiff_t point, + ptrdiff_t mark) { specpdl_ref count; @@ -763,6 +931,11 @@ really_set_point (struct frame *f, ptrdiff_t point) /* Set the point. */ Fgoto_char (make_fixnum (point)); + if (mark == point && BVAR (current_buffer, mark_active)) + call0 (Qdeactivate_mark); + else + call1 (Qpush_mark, make_fixnum (mark)); + unbind_to (count, Qnil); } @@ -845,8 +1018,9 @@ handle_pending_conversion_events_1 (struct frame *f, XFIXNUM (XCDR (data))); break; - case TEXTCONV_SET_POINT: - really_set_point (f, XFIXNUM (data)); + case TEXTCONV_SET_POINT_AND_MARK: + really_set_point_and_mark (f, XFIXNUM (XCAR (data)), + XFIXNUM (XCDR (data))); break; case TEXTCONV_DELETE_SURROUNDING_TEXT: @@ -858,6 +1032,17 @@ handle_pending_conversion_events_1 (struct frame *f, unbind_to (count, Qnil); } +/* Decrement the variable pointed to by *PTR. */ + +static void +decrement_inside (void *ptr) +{ + int *i; + + i = ptr; + (*i)--; +} + /* Process any outstanding text conversion events. This may run Lisp or signal. */ @@ -868,9 +1053,22 @@ handle_pending_conversion_events (void) Lisp_Object tail, frame; struct text_conversion_action *action, *next; bool handled; + static int inside; + specpdl_ref count; handled = false; + /* Reset Vtext_conversion_edits. Do not do this if called + reentrantly. */ + + if (!inside) + Vtext_conversion_edits = Qnil; + + inside++; + + count = SPECPDL_INDEX (); + record_unwind_protect_ptr (decrement_inside, &inside); + FOR_EACH_FRAME (tail, frame) { f = XFRAME (frame); @@ -905,6 +1103,8 @@ handle_pending_conversion_events (void) handled = true; } } + + unbind_to (count, Qnil); } /* Start a ``batch edit'' in F. During a batch edit, point_changed @@ -1057,13 +1257,13 @@ set_composing_region (struct frame *f, ptrdiff_t start, input_pending = true; } -/* Move point in F's selected buffer to POINT. +/* Move point in F's selected buffer to POINT and maybe push MARK. COUNTER means the same as in `start_batch_edit'. */ void -textconv_set_point (struct frame *f, ptrdiff_t point, - unsigned long counter) +textconv_set_point_and_mark (struct frame *f, ptrdiff_t point, + ptrdiff_t mark, unsigned long counter) { struct text_conversion_action *action, **last; @@ -1071,8 +1271,9 @@ textconv_set_point (struct frame *f, ptrdiff_t point, point = MOST_POSITIVE_FIXNUM; action = xmalloc (sizeof *action); - action->operation = TEXTCONV_SET_POINT; - action->data = make_fixnum (point); + action->operation = TEXTCONV_SET_POINT_AND_MARK; + action->data = Fcons (make_fixnum (point), + make_fixnum (mark)); action->next = NULL; action->counter = counter; for (last = &f->conversion.actions; *last; last = &(*last)->next) @@ -1105,6 +1306,9 @@ delete_surrounding_text (struct frame *f, ptrdiff_t left, /* Return N characters of text around point in F's old selected window. + If N is -1, return the text between point and mark instead, given + that the mark is active. + Set *N to the actual number of characters returned, *START_RETURN to the position of the first character returned, *OFFSET to the offset of point within that text, *LENGTH to the actual number of @@ -1137,13 +1341,38 @@ get_extracted_text (struct frame *f, ptrdiff_t n, /* Temporarily switch to F's selected window at the time of the last redisplay. */ Fselect_window (f->old_selected_window, Qt); + buffer = NULL; /* Figure out the bounds of the text to return. */ - start = PT - n / 2; - end = PT + n - n / 2; + if (n != -1) + { + start = PT - n / 2; + end = PT + n - n / 2; + } + else + { + if (!NILP (BVAR (current_buffer, mark_active)) + && XMARKER (BVAR (current_buffer, mark))->buffer) + { + start = marker_position (BVAR (current_buffer, mark)); + end = PT; + + /* Sort start and end. start_byte is used to hold a + temporary value. */ + + if (start > end) + { + start_byte = end; + end = start; + start = start_byte; + } + } + else + goto finish; + } + start = max (start, BEGV); end = min (end, ZV); - buffer = NULL; /* Detect overflow. */ @@ -1218,3 +1447,37 @@ register_textconv_interface (struct textconv_interface *interface) { text_interface = interface; } + + + +void +syms_of_textconv (void) +{ + DEFSYM (Qaction, "action"); + DEFSYM (Qtext_conversion, "text-conversion"); + DEFSYM (Qpush_mark, "push-mark"); + DEFSYM (Qunderline, "underline"); + + DEFVAR_LISP ("text-conversion-edits", Vtext_conversion_edits, + doc: /* List of buffers that were last edited as a result of text conversion. + +This list can be used while handling a `text-conversion' event to +determine the changes which have taken place. + +Each element of the list describes a single edit in a buffer, of the +form: + + (BUFFER BEG END EPHEMERAL) + +If an insertion or a change occured, then BEG and END are buffer +positions denote the bounds of the text that was changed or inserted. +If EPHEMERAL is t, then the input method will shortly make more +changes to the text, so any actions that would otherwise be taken +(such as indenting or automatically filling text) should not take +place; otherwise, it is a string describing the text which was +inserted. + +If a deletion occured, then BEG and END are the same, and EPHEMERAL is +nil. */); + Vtext_conversion_edits = Qnil; +} diff --git a/src/textconv.h b/src/textconv.h index ce003847637..034c663521a 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -35,9 +35,10 @@ struct textconv_interface unexpected buffer change occurs.) */ void (*reset) (struct frame *); - /* Notice that point has moved in the specified frame's selected - window's selected buffer. The second argument is the window - whose point changed, and the third argument is the buffer. */ + /* Notice that point or mark has moved in the specified frame's + selected window's selected buffer. The second argument is the + window whose point changed, and the third argument is the + buffer. */ void (*point_changed) (struct frame *, struct window *, struct buffer *); @@ -118,6 +119,7 @@ struct textconv_callback_struct }; #define TEXTCONV_SKIP_CONVERSION_REGION (1 << 0) +#define TEXTCONV_SKIP_ACTIVE_REGION (1 << 1) extern int textconv_query (struct frame *, struct textconv_callback_struct *, int); @@ -132,7 +134,8 @@ extern void set_composing_text (struct frame *, Lisp_Object, ptrdiff_t, unsigned long); extern void set_composing_region (struct frame *, ptrdiff_t, ptrdiff_t, unsigned long); -extern void textconv_set_point (struct frame *, ptrdiff_t, unsigned long); +extern void textconv_set_point_and_mark (struct frame *, ptrdiff_t, + ptrdiff_t, unsigned long); extern void delete_surrounding_text (struct frame *, ptrdiff_t, ptrdiff_t, unsigned long); extern char *get_extracted_text (struct frame *, ptrdiff_t, ptrdiff_t *, diff --git a/src/window.h b/src/window.h index 32b5fe14f4f..463c7f89b9b 100644 --- a/src/window.h +++ b/src/window.h @@ -286,6 +286,10 @@ struct window it should be positive. */ ptrdiff_t last_point; + /* Value of mark in the selected window at the time of the last + redisplay. */ + ptrdiff_t last_mark; + /* Line number and position of a line somewhere above the top of the screen. If this field is zero, it means we don't have a base line. */ ptrdiff_t base_line_number; diff --git a/src/xdisp.c b/src/xdisp.c index 4b91fcf31ca..525eaa64b3a 100644 --- a/src/xdisp.c +++ b/src/xdisp.c @@ -17268,7 +17268,7 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) { struct buffer *b = XBUFFER (w->contents); #ifdef HAVE_TEXT_CONVERSION - ptrdiff_t prev_point; + ptrdiff_t prev_point, prev_mark; #endif w->last_modified = accurate_p ? BUF_MODIFF (b) : 0; @@ -17301,6 +17301,7 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) #ifdef HAVE_TEXT_CONVERSION prev_point = w->last_point; + prev_mark = w->last_mark; #endif if (w == XWINDOW (selected_window)) @@ -17308,6 +17309,11 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) else w->last_point = marker_position (w->pointm); + if (XMARKER (BVAR (b, mark))->buffer == b) + w->last_mark = marker_position (BVAR (b, mark)); + else + w->last_mark = -1; + #ifdef HAVE_TEXT_CONVERSION /* Point motion is only propagated to the input method for use in text conversion during a redisplay. While this can lead @@ -17323,7 +17329,8 @@ mark_window_display_accurate_1 (struct window *w, bool accurate_p) during such a change, so the consequences are not that severe. */ - if (prev_point != w->last_point + if ((prev_point != w->last_point + || prev_mark != w->last_mark) && FRAME_WINDOW_P (WINDOW_XFRAME (w)) && w == XWINDOW (WINDOW_XFRAME (w)->selected_window)) report_point_change (WINDOW_XFRAME (w), w, b); -- cgit v1.3 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. --- INSTALL.android | 15 +- configure.ac | 23 +- java/Makefile.in | 14 +- java/debug.sh | 8 +- java/org/gnu/emacs/EmacsApplication.java | 1 + java/org/gnu/emacs/EmacsService.java | 25 +- java/org/gnu/emacs/EmacsThread.java | 9 +- src/android-asset.h | 423 +++++++++++++++++++++++++++++++ src/android.c | 72 +++++- src/process.c | 2 +- src/sfntfont-android.c | 2 +- src/sfntfont.c | 2 +- src/textconv.c | 2 +- 13 files changed, 574 insertions(+), 24 deletions(-) create mode 100644 src/android-asset.h (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/INSTALL.android b/INSTALL.android index a8d73331493..ab39a55eaf3 100644 --- a/INSTALL.android +++ b/INSTALL.android @@ -91,9 +91,18 @@ for, and the include directories specify the paths to the relevant Android headers. In addition, it may be necessary to specify "-gdwarf-2", due to a bug in the Android NDK. -Emacs is known to build for Android 2.2 (API version 8) or later, and -run on Android 2.3 or later. It is supposed to run on Android 2.2 as -well. +Even older versions of the Android SDK do not require the extra +`-isystem' directives. + +Emacs is known to run on Android 2.2 (API version 8) or later, with +the NDK r10b or later. We wanted to make Emacs work on even older +versions of Android, but they are missing the required JNI graphics +library that allows Emacs to display text from C code. + +Due to an extremely nasty bug in the Android 2.2 system, the generated +Emacs package cannot be compressed in builds for Android 2.2. As a +result, the Emacs package will be approximately 100 megabytes larger +than a compressed package for a newer version of Android. DEBUG AND RELEASE BUILDS diff --git a/configure.ac b/configure.ac index b4fed263cf5..a4a16ec8062 100644 --- a/configure.ac +++ b/configure.ac @@ -1044,11 +1044,18 @@ package will likely install on older systems but crash on startup.]) # Now tell java/Makefile if Emacs is being built for Android 4.3 or # earlier. ANDROID_SDK_18_OR_EARLIER= - if test "$android_sdk" -lt "18"; then + if test "$android_sdk" -le "18"; then ANDROID_SDK_18_OR_EARLIER=yes fi AC_SUBST([ANDROID_SDK_18_OR_EARLIER]) + # Likewise for Android 2.2. + ANDROID_SDK_8_OR_EARLIER= + if test "$android_sdk" -le "8"; then + ANDROID_SDK_8_OR_EARLIER=yes + fi + AC_SUBST([ANDROID_SDK_8_OR_EARLIER]) + # Save confdefs.h and config.log for now. mv -f confdefs.h _confdefs.h mv -f config.log _config.log @@ -2251,6 +2258,10 @@ AC_DEFINE_UNQUOTED([SYSTEM_TYPE], ["$SYSTEM_TYPE"], [The type of system you are compiling for; sets 'system-type'.]) AC_SUBST([SYSTEM_TYPE]) +# Check for pw_gecos in struct passwd; this is known to be missing on +# Android. + +AC_CHECK_MEMBERS([struct passwd.pw_gecos], [], [], [#include ]) pre_PKG_CONFIG_CFLAGS=$CFLAGS pre_PKG_CONFIG_LIBS=$LIBS @@ -2487,7 +2498,13 @@ for Android, but all API calls need to be stubbed out]) ANDROID_CFLAGS="$ANDROID_CFLAGS -ftree-vectorize" # Link with libraries required for Android support. - ANDROID_LIBS="-landroid -llog -ljnigraphics" + # API 9 and later require `-landroid' for the asset manager. + # API 8 uses an emulation via the JNI. + if test "$ANDROID_SDK" -lt "9"; then + ANDROID_LIBS="-llog -ljnigraphics" + else + ANDROID_LIBS="-landroid -llog -ljnigraphics" + fi # This is required to make the system load emacs.apk's libpng # (among others) instead of the system's own. But it doesn't work @@ -5610,7 +5627,7 @@ OLD_LIBS=$LIBS LIBS="$LIB_PTHREAD $LIB_MATH $LIBS" AC_CHECK_FUNCS([accept4 fchdir gethostname \ getrusage get_current_dir_name \ -lrand48 random rint trunc \ +lrand48 random rint tcdrain trunc \ select getpagesize setlocale newlocale \ getrlimit setrlimit shutdown \ pthread_sigmask strsignal setitimer \ diff --git a/java/Makefile.in b/java/Makefile.in index fc0d23980e4..92c03469c69 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -39,6 +39,7 @@ JARSIGNER_FLAGS = ANDROID_JAR = @ANDROID_JAR@ ANDROID_ABI = @ANDROID_ABI@ ANDROID_SDK_18_OR_EARLIER = @ANDROID_SDK_18_OR_EARLIER@ +ANDROID_SDK_8_OR_EARLIER = @ANDROID_SDK_8_OR_EARLIER@ WARN_JAVAFLAGS = -Xlint:deprecation JAVAFLAGS = -classpath "$(ANDROID_JAR):." -target 1.7 -source 1.7 \ @@ -53,6 +54,16 @@ else JARSIGNER_FLAGS = endif +# When building Emacs for Android 2.2, assets must not be compressed. +# Otherwise, the asset manager fails to extract files larger than 1 +# MB. + +ifneq (,$(ANDROID_SDK_8_OR_EARLIER)) +AAPT_ASSET_ARGS = -0 "" +else +AAPT_ASSET_ARGS = +endif + SIGN_EMACS = -keystore emacs.keystore -storepass emacs1 $(JARSIGNER_FLAGS) SIGN_EMACS_V2 = sign --v2-signing-enabled --ks emacs.keystore \ --debuggable-apk-permitted --ks-pass pass:emacs1 @@ -192,7 +203,8 @@ emacs.apk-in: install_temp install_temp/assets/directory-tree \ # of Android. Make sure not to generate R.java, as it's already been # generated. $(AM_V_AAPT) $(AAPT) p -I "$(ANDROID_JAR)" -F $@ \ - -f -M AndroidManifest.xml -A install_temp/assets \ + -f -M AndroidManifest.xml $(AAPT_ASSET_ARGS) \ + -A install_temp/assets \ -S res -J install_temp $(AM_V_SILENT) pushd install_temp &> /dev/null; \ $(AAPT) add ../$@ `find lib -type f`; \ diff --git a/java/debug.sh b/java/debug.sh index cbef7518984..30e5a94eee5 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -32,6 +32,7 @@ jdb_port=64013 jdb=no attach_existing=no gdbserver= +gdb=gdb while [ $# -gt 0 ]; do case "$1" in @@ -51,6 +52,7 @@ while [ $# -gt 0 ]; do echo " --port PORT run the GDB server on a specific port" echo " --jdb-port PORT run the JDB server on a specific port" echo " --jdb run JDB instead of GDB" + echo " --gdb use specified GDB binary" echo " --attach-existing attach to an existing process" echo " --gdbserver BINARY upload and use the specified gdbserver binary" echo " --help print this message" @@ -65,6 +67,10 @@ while [ $# -gt 0 ]; do "--jdb" ) jdb=yes ;; + "--gdb" ) + shift + gdb=$1 + ;; "--gdbserver" ) shift gdbserver=$1 @@ -355,4 +361,4 @@ fi # Finally, start gdb with any extra arguments needed. cd "$oldpwd" -gdb --eval-command "target remote localhost:$gdb_port" $gdbargs +$gdb --eval-command "target remote localhost:$gdb_port" $gdbargs 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); diff --git a/src/android-asset.h b/src/android-asset.h new file mode 100644 index 00000000000..b0e83bbf424 --- /dev/null +++ b/src/android-asset.h @@ -0,0 +1,423 @@ +/* Android initialization for GNU Emacs. + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +#include + +/* This file contains an emulation of the Android asset manager API + used on builds for Android 2.2. It is included by android.c + whenever appropriate. + + The replacements in this file are not thread safe and must only be + called from the creating thread. */ + +struct android_asset_manager +{ + /* JNI environment. */ + JNIEnv *env; + + /* Asset manager class and functions. */ + jclass class; + jmethodID open_fd; + + /* Asset file descriptor class and functions. */ + jclass fd_class; + jmethodID get_length; + jmethodID create_input_stream; + jmethodID close; + + /* Input stream class and functions. */ + jclass input_stream_class; + jmethodID read; + jmethodID stream_close; + + /* Associated asset manager object. */ + jobject asset_manager; +}; + +typedef struct android_asset_manager AAssetManager; + +struct android_asset +{ + /* The asset manager. */ + AAssetManager *manager; + + /* The length of the asset, or -1. */ + jlong length; + + /* The asset file descriptor and input stream. */ + jobject fd, stream; + + /* The mode. */ + int mode; +}; + +typedef struct android_asset AAsset; + +static AAssetManager * +AAssetManager_fromJava (JNIEnv *env, jobject java_manager) +{ + AAssetManager *manager; + jclass temp; + + manager = malloc (sizeof *manager); + + if (!manager) + return NULL; + + manager->env = env; + manager->asset_manager + = (*env)->NewGlobalRef (env, java_manager); + + if (!manager->asset_manager) + { + free (manager); + return NULL; + } + + manager->class + = (*env)->FindClass (env, "android/content/res/AssetManager"); + assert (manager->class); + + manager->open_fd + = (*env)->GetMethodID (env, manager->class, "openFd", + "(Ljava/lang/String;)" + "Landroid/content/res/AssetFileDescriptor;"); + assert (manager->open); + + manager->fd_class + = (*env)->FindClass (env, "android/content/res/AssetFileDescriptor"); + assert (manager->fd_class); + + manager->get_length + = (*env)->GetMethodID (env, manager->fd_class, "getLength", + "()J"); + assert (manager->get_length); + + manager->create_input_stream + = (*env)->GetMethodID (env, manager->fd_class, + "createInputStream", + "()Ljava/io/FileInputStream;"); + assert (manager->create_input_stream); + + manager->close + = (*env)->GetMethodID (env, manager->fd_class, + "close", "()V"); + assert (manager->close); + + manager->input_stream_class + = (*env)->FindClass (env, "java/io/InputStream"); + assert (manager->input_stream_class); + + manager->read + = (*env)->GetMethodID (env, manager->input_stream_class, + "read", "([B)I"); + assert (manager->read); + + manager->stream_close + = (*env)->GetMethodID (env, manager->input_stream_class, + "close", "()V"); + assert (manager->stream_close); + + /* Now convert all the class references to global ones. */ + temp = manager->class; + manager->class + = (*env)->NewGlobalRef (env, temp); + assert (manager->class); + (*env)->DeleteLocalRef (env, temp); + temp = manager->fd_class; + manager->fd_class + = (*env)->NewGlobalRef (env, temp); + assert (manager->fd_class); + (*env)->DeleteLocalRef (env, temp); + temp = manager->input_stream_class; + manager->input_stream_class + = (*env)->NewGlobalRef (env, temp); + assert (manager->input_stream_class); + (*env)->DeleteLocalRef (env, temp); + + /* Return the asset manager. */ + return manager; +} + +enum + { + AASSET_MODE_STREAMING = 0, + AASSET_MODE_BUFFER = 1, + }; + +static AAsset * +AAssetManager_open (AAssetManager *manager, const char *c_name, + int mode) +{ + jobject desc; + jstring name; + AAsset *asset; + + /* Push a local frame. */ + asset = NULL; + + (*(manager->env))->PushLocalFrame (manager->env, 3); + + if ((*(manager->env))->ExceptionCheck (manager->env)) + goto fail; + + /* Encoding issues can be ignored for now as there are only ASCII + file names in Emacs. */ + name = (*(manager->env))->NewStringUTF (manager->env, c_name); + + if (!name) + goto fail; + + /* Now try to open an ``AssetFileDescriptor''. */ + desc = (*(manager->env))->CallObjectMethod (manager->env, + manager->asset_manager, + manager->open_fd, + name); + + if (!desc) + goto fail; + + /* Allocate the asset. */ + asset = calloc (1, sizeof *asset); + + if (!asset) + { + (*(manager->env))->CallVoidMethod (manager->env, + desc, + manager->close); + goto fail; + } + + /* Pop the local frame and return desc. */ + desc = (*(manager->env))->NewGlobalRef (manager->env, desc); + + if (!desc) + goto fail; + + (*(manager->env))->PopLocalFrame (manager->env, NULL); + + asset->manager = manager; + asset->length = -1; + asset->fd = desc; + asset->mode = mode; + + return asset; + + fail: + (*(manager->env))->ExceptionClear (manager->env); + (*(manager->env))->PopLocalFrame (manager->env, NULL); + free (asset); + + return NULL; +} + +static AAsset * +AAsset_close (AAsset *asset) +{ + JNIEnv *env; + + env = asset->manager->env; + + (*env)->CallVoidMethod (asset->manager->env, + asset->fd, + asset->manager->close); + (*env)->DeleteGlobalRef (asset->manager->env, + asset->fd); + + if (asset->stream) + { + (*env)->CallVoidMethod (asset->manager->env, + asset->stream, + asset->manager->stream_close); + (*env)->DeleteGlobalRef (asset->manager->env, + asset->stream); + } + + free (asset); +} + +/* Create an input stream associated with the given ASSET. Set + ASSET->stream to its global reference. + + Value is 1 upon failure, else 0. ASSET must not already have an + input stream. */ + +static int +android_asset_create_stream (AAsset *asset) +{ + jobject stream; + JNIEnv *env; + + env = asset->manager->env; + stream + = (*env)->CallObjectMethod (env, asset->fd, + asset->manager->create_input_stream); + + if (!stream) + { + (*env)->ExceptionClear (env); + return 1; + } + + asset->stream + = (*env)->NewGlobalRef (env, stream); + + if (!asset->stream) + { + (*env)->ExceptionClear (env); + (*env)->DeleteLocalRef (env, stream); + return 1; + } + + (*env)->DeleteLocalRef (env, stream); + return 0; +} + +/* Read NBYTES from the specified asset into the given BUFFER; + + Internally, allocate a Java byte array containing 4096 elements and + copy the data to and from that array. + + Value is the number of bytes actually read, 0 at EOF, or -1 upon + failure, in which case errno is set accordingly. If NBYTES is + zero, behavior is undefined. */ + +static int +android_asset_read_internal (AAsset *asset, int nbytes, char *buffer) +{ + jbyteArray stash; + JNIEnv *env; + jint bytes_read, total; + + /* Allocate a suitable amount of storage. Either nbytes or 4096, + whichever is larger. */ + env = asset->manager->env; + stash = (*env)->NewByteArray (env, MIN (nbytes, 4096)); + + if (!stash) + { + (*env)->ExceptionClear (env); + errno = ENOMEM; + return -1; + } + + /* Try to create an input stream. */ + + if (!asset->stream + && android_asset_create_stream (asset)) + { + (*env)->DeleteLocalRef (env, stash); + errno = ENOMEM; + return -1; + } + + /* Start reading. */ + + total = 0; + + while (nbytes) + { + bytes_read = (*env)->CallIntMethod (env, asset->stream, + asset->manager->read, + stash); + + /* Detect error conditions. */ + + if ((*env)->ExceptionCheck (env)) + goto out; + + /* Detect EOF. */ + + if (bytes_read == -1) + goto out; + + /* Finally write out the amount that was read. */ + bytes_read = MIN (bytes_read, nbytes); + (*env)->GetByteArrayRegion (env, stash, 0, bytes_read, buffer); + + buffer += bytes_read; + total += bytes_read; + nbytes -= bytes_read; + } + + /* Make sure the value of nbytes still makes sense. */ + assert (nbytes >= 0); + + out: + (*env)->ExceptionClear (env); + (*env)->DeleteLocalRef (env, stash); + return total; +} + +static int +AAsset_openFileDescriptor (AAsset *asset, off_t *out_start, + off_t *out_end) +{ + *out_start = 0; + *out_end = 0; + return -1; +} + +static long +AAsset_getLength (AAsset *asset) +{ + JNIEnv *env; + + if (asset->length != -1) + return asset->length; + + env = asset->manager->env; + asset->length + = (*env)->CallLongMethod (env, asset->fd, + asset->manager->get_length); + return asset->length; +} + +static char * +AAsset_getBuffer (AAsset *asset) +{ + long length; + char *buffer; + + length = AAsset_getLength (asset); + + if (!length) + return NULL; + + buffer = malloc (length); + + if (!buffer) + return NULL; + + if (android_asset_read_internal (asset, length, buffer) + != length) + { + xfree (buffer); + return NULL; + } + + return buffer; +} + +static size_t +AAsset_read (AAsset *asset, void *buffer, size_t size) +{ + return android_asset_read_internal (asset, MIN (size, INT_MAX), + buffer); +} diff --git a/src/android.c b/src/android.c index 8f446224dab..09b9001b45d 100644 --- a/src/android.c +++ b/src/android.c @@ -32,6 +32,9 @@ along with GNU Emacs. If not, see . */ #include #include +/* Old NDK versions lack MIN and MAX. */ +#include + #include #include @@ -49,8 +52,13 @@ bool android_init_gui; #ifndef ANDROID_STUBIFY +#if __ANDROID_API__ >= 9 #include #include +#else +#include "android-asset.h" +#endif + #include #include @@ -907,10 +915,14 @@ android_is_directory (const char *dir) char * android_user_full_name (struct passwd *pw) { +#ifdef HAVE_STRUCT_PASSWD_PW_GECOS if (!pw->pw_gecos) return (char *) "Android user"; return pw->pw_gecos; +#else + return "Android user"; +#endif } /* Given a real file name, return the part that describes its asset @@ -1048,6 +1060,60 @@ android_file_access_p (const char *name, int amode) static int android_hack_asset_fd (AAsset *asset) { +#if __ANDROID_API__ < 9 + int fd; + char filename[PATH_MAX]; + size_t size; + void *mem; + + /* Assets must be small enough to fit in size_t, if off_t is + larger. */ + size = AAsset_getLength (asset); + + /* Get an unlinked file descriptor from a file in the cache + directory, which is guaranteed to only be written to by Emacs. + Creating an asset file descriptor doesn't work on these old + Android versions. */ + + snprintf (filename, PATH_MAX, "%s/%s.%d", + android_cache_dir, "temp-unlinked", + getpid ()); + fd = open (filename, O_CREAT | O_RDWR | O_TRUNC, + S_IRUSR | S_IWUSR); + + if (fd < 1) + return -1; + + if (unlink (filename)) + goto fail; + + if (ftruncate (fd, size)) + goto fail; + + mem = mmap (NULL, size, PROT_WRITE, MAP_SHARED, fd, 0); + if (mem == MAP_FAILED) + { + __android_log_print (ANDROID_LOG_ERROR, __func__, + "mmap: %s", strerror (errno)); + goto fail; + } + + if (AAsset_read (asset, mem, size) != size) + { + /* Too little was read. Close the file descriptor and + report an error. */ + __android_log_print (ANDROID_LOG_ERROR, __func__, + "AAsset_read: %s", strerror (errno)); + goto fail; + } + + munmap (mem, size); + return fd; + + fail: + close (fd); + return -1; +#else int fd, rc; unsigned char *mem; size_t size; @@ -1172,10 +1238,11 @@ android_hack_asset_fd (AAsset *asset) /* Return anyway even if munmap fails. */ munmap (mem, size); return fd; +#endif } /* Read two bytes from FD and see if they are ``PK'', denoting ZIP - archive compressed data. + archive compressed data. If FD is -1, return -1. If they are not, rewind the file descriptor to offset 0. @@ -1187,6 +1254,9 @@ android_check_compressed_file (int fd) { char bytes[2]; + if (fd == -1) + return -1; + if (read (fd, bytes, 2) != 2) goto lseek_back; diff --git a/src/process.c b/src/process.c index e7ccb2c604e..bdaaba70fea 100644 --- a/src/process.c +++ b/src/process.c @@ -7248,7 +7248,7 @@ process has been transmitted to the serial port. */) send_process (proc, "\004", 1, Qnil); else if (EQ (XPROCESS (proc)->type, Qserial)) { -#ifndef WINDOWSNT +#if !defined WINDOWSNT && defined HAVE_TCDRAIN if (tcdrain (XPROCESS (proc)->outfd) != 0) report_file_error ("Failed tcdrain", Qnil); #endif /* not WINDOWSNT */ diff --git a/src/sfntfont-android.c b/src/sfntfont-android.c index c28a911bfba..8324185cc6f 100644 --- a/src/sfntfont-android.c +++ b/src/sfntfont-android.c @@ -51,7 +51,7 @@ struct sfntfont_android_scanline_buffer /* Array of directories to search for system fonts. */ static char *system_font_directories[] = { - "/system/fonts", + (char *) "/system/fonts", /* This should be filled in by init_sfntfont_android. */ (char[PATH_MAX]) { }, }; diff --git a/src/sfntfont.c b/src/sfntfont.c index f9344067f1a..20c109f2401 100644 --- a/src/sfntfont.c +++ b/src/sfntfont.c @@ -953,7 +953,7 @@ sfntfont_read_cmap (struct sfnt_font_desc *desc, /* Pick a character map and place it in *CMAP. */ fd = emacs_open (desc->path, O_RDONLY, 0); - if (fd < 1) + if (fd < 0) return; font = sfnt_read_table_directory (fd); diff --git a/src/textconv.c b/src/textconv.c index 5090b0a33b6..8f5a4987d5a 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -989,7 +989,7 @@ handle_pending_conversion_events_1 (struct frame *f, { Lisp_Object data; enum text_conversion_operation operation; - struct buffer *buffer; + struct buffer *buffer UNINIT; struct window *w; specpdl_ref count; unsigned long token; -- cgit v1.3 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/emacs/EmacsService.java') 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.3 From 7aa4ffddd842e495d1ae388afff12075317ecb07 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 21 Feb 2023 15:28:06 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Startup): Document `content' special directory. * java/debug.sh (is_root): Improve /bin/tee detection. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function `dup'. * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity) (checkReadableOrCopy, onCreate): Create content directory names when the file is not readable. * java/org/gnu/emacs/EmacsService.java (EmacsService) (openContentUri, checkContentUri): New functions. * src/android.c (struct android_emacs_service): New methods. (android_content_name_p, android_get_content_name) (android_check_content_access): New function. (android_fstatat, android_open): Implement opening content URIs. (dup): Export to Java. (android_init_emacs_service): Initialize new methods. (android_faccessat): Implement content file names. --- doc/emacs/android.texi | 12 +- java/debug.sh | 2 +- java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsOpenActivity.java | 42 +++++- java/org/gnu/emacs/EmacsService.java | 119 +++++++++++++++++ src/android.c | 213 +++++++++++++++++++++++++++++- 6 files changed, 379 insertions(+), 13 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index d070199d325..f176d68ae67 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -129,9 +129,15 @@ file, it invokes @command{emacsclient} with the options and the name of the file being opened. Then, upon success, the focus is transferred to any open Emacs frame. -It is sadly impossible to open certain kinds of files which are -provided by a ``content provider''. When that is the case, a dialog -is displayed with an explanation of the error. +@cindex /content directory, android + Some files are given to Emacs as ``content identifiers'', which the +system provides access to outside the normal filesystem APIs. Emacs +internally supports a temporary @file{/content} directory which is +used to access those files. Do not make any assumptions about the +contents of this directory, or try to open files in it yourself. + + This feature is not provided on Android 4.3 and earlier, in which +case the file is copied to a temporary directory instead. @node Android File System @section What files Emacs can access under Android diff --git a/java/debug.sh b/java/debug.sh index 30e5a94eee5..f07bb98ed7d 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -281,7 +281,7 @@ else # Upload the specified gdbserver binary to the device. adb -s $device push "$gdbserver" "$gdbserver_bin" - if (adb -s $device pull /system/bin/tee /dev/null &> /dev/null); then + if adb -s $device shell ls /system/bin/tee; then # Copy it to the user directory. adb -s $device shell "$gdbserver_cat" adb -s $device shell "run-as $package chmod +x gdbserver" diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index ef1a0e75a5c..f0917a68120 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -31,6 +31,10 @@ public class EmacsNative initialization. */ private static final String[] libraryDeps; + + /* Like `dup' in C. */ + public static native int dup (int fd); + /* Obtain the fingerprint of this build of Emacs. The fingerprint can be used to determine the dump file name. */ public static native String getFingerprint (); diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index c8501d91025..87ce454a816 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -57,6 +57,8 @@ import android.os.Build; import android.os.Bundle; import android.os.ParcelFileDescriptor; +import android.util.Log; + import java.io.File; import java.io.FileInputStream; import java.io.FileNotFoundException; @@ -69,6 +71,8 @@ import java.io.UnsupportedEncodingException; public class EmacsOpenActivity extends Activity implements DialogInterface.OnClickListener { + private static final String TAG = "EmacsOpenActivity"; + private class EmacsClientThread extends Thread { private ProcessBuilder builder; @@ -178,12 +182,16 @@ public class EmacsOpenActivity extends Activity dialog.show (); } - /* Check that the specified FILE is readable. If it is not, then - copy the file in FD to a location in the system cache - directory and return the name of that file. */ + /* Check that the specified FILE is readable. If Android 4.4 or + later is being used, return URI formatted into a `/content/' file + name. + + If it is not, then copy the file in FD to a location in the + system cache directory and return the name of that file. */ private String - checkReadableOrCopy (String file, ParcelFileDescriptor fd) + checkReadableOrCopy (String file, ParcelFileDescriptor fd, + Uri uri) throws IOException, FileNotFoundException { File inFile; @@ -191,12 +199,34 @@ public class EmacsOpenActivity extends Activity InputStream stream; byte buffer[]; int read; + String content; + + Log.d (TAG, "checkReadableOrCopy: " + file); inFile = new File (file); - if (inFile.setReadable (true)) + if (inFile.canRead ()) return file; + Log.d (TAG, "checkReadableOrCopy: NO"); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) + { + content = "/content/" + uri.getEncodedAuthority (); + + for (String segment : uri.getPathSegments ()) + content += "/" + Uri.encode (segment); + + /* Append the URI query. */ + + if (uri.getEncodedQuery () != null) + content += "?" + uri.getEncodedQuery (); + + Log.d (TAG, "checkReadableOrCopy: " + content); + + return content; + } + /* inFile is now the file being written to. */ inFile = new File (getCacheDir (), inFile.getName ()); buffer = new byte[4098]; @@ -398,7 +428,7 @@ public class EmacsOpenActivity extends Activity if (names != null) fileName = new String (names, "UTF-8"); - fileName = checkReadableOrCopy (fileName, fd); + fileName = checkReadableOrCopy (fileName, fd, uri); } catch (FileNotFoundException exception) { diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index ba6ec485d62..c9701ff2990 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -19,7 +19,10 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; +import java.io.FileNotFoundException; import java.io.IOException; +import java.io.UnsupportedEncodingException; + import java.util.List; import java.util.ArrayList; @@ -41,22 +44,31 @@ import android.app.Service; import android.content.ClipboardManager; import android.content.Context; +import android.content.ContentResolver; import android.content.Intent; import android.content.pm.ApplicationInfo; import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager; import android.content.res.AssetManager; +import android.database.Cursor; +import android.database.MatrixCursor; + + import android.net.Uri; import android.os.Build; import android.os.Looper; import android.os.IBinder; import android.os.Handler; +import android.os.ParcelFileDescriptor; import android.os.Vibrator; import android.os.VibratorManager; import android.os.VibrationEffect; +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Document; + import android.util.Log; import android.util.DisplayMetrics; @@ -79,6 +91,7 @@ public class EmacsService extends Service private EmacsThread thread; private Handler handler; + private ContentResolver resolver; /* Keep this in synch with androidgui.h. */ public static final int IC_MODE_NULL = 0; @@ -193,6 +206,7 @@ public class EmacsService extends Service metrics = getResources ().getDisplayMetrics (); pixelDensityX = metrics.xdpi; pixelDensityY = metrics.ydpi; + resolver = getContentResolver (); try { @@ -643,4 +657,109 @@ public class EmacsService extends Service window.view.setICMode (icMode); window.view.imManager.restartInput (window.view); } + + /* Open a content URI described by the bytes BYTES, a non-terminated + string; make it writable if WRITABLE, and readable if READABLE. + Truncate the file if TRUNCATE. + + Value is the resulting file descriptor or -1 upon failure. */ + + public int + openContentUri (byte[] bytes, boolean writable, boolean readable, + boolean truncate) + { + String name, mode; + ParcelFileDescriptor fd; + int i; + + /* Figure out the file access mode. */ + + mode = ""; + + if (readable) + mode += "r"; + + if (writable) + mode += "w"; + + if (truncate) + mode += "t"; + + /* Try to open an associated ParcelFileDescriptor. */ + + try + { + /* The usual file name encoding question rears its ugly head + again. */ + name = new String (bytes, "UTF-8"); + Log.d (TAG, "openContentUri: " + Uri.parse (name)); + + fd = resolver.openFileDescriptor (Uri.parse (name), mode); + + /* Use detachFd on newer versions of Android or plain old + dup. */ + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) + { + i = fd.detachFd (); + fd.close (); + + return i; + } + else + { + i = EmacsNative.dup (fd.getFd ()); + fd.close (); + + return i; + } + } + catch (Exception exception) + { + return -1; + } + } + + public boolean + checkContentUri (byte[] string, boolean readable, boolean writable) + { + String mode, name; + ParcelFileDescriptor fd; + + /* Decode this into a URI. */ + + try + { + /* The usual file name encoding question rears its ugly head + again. */ + name = new String (string, "UTF-8"); + Log.d (TAG, "checkContentUri: " + Uri.parse (name)); + } + catch (UnsupportedEncodingException exception) + { + name = null; + throw new RuntimeException (exception); + } + + mode = "r"; + + if (writable) + mode += "w"; + + try + { + fd = resolver.openFileDescriptor (Uri.parse (name), mode); + fd.close (); + + Log.d (TAG, "checkContentUri: YES"); + + return true; + } + catch (Exception exception) + { + Log.d (TAG, "checkContentUri: NO"); + Log.d (TAG, exception.toString ()); + return false; + } + } }; diff --git a/src/android.c b/src/android.c index fc5e6d278ed..75a97f9db33 100644 --- a/src/android.c +++ b/src/android.c @@ -108,6 +108,8 @@ struct android_emacs_service jmethodID restart_emacs; jmethodID update_ic; jmethodID reset_ic; + jmethodID open_content_uri; + jmethodID check_content_uri; }; struct android_emacs_pixmap @@ -941,6 +943,102 @@ android_get_asset_name (const char *filename) return NULL; } +/* Return whether or not the specified FILENAME actually resolves to a + content resolver URI. */ + +static bool +android_content_name_p (const char *filename) +{ + return (!strcmp (filename, "/content") + || !strncmp (filename, "/content/", + sizeof "/content/" - 1)); +} + +/* Return the content URI corresponding to a `/content' file name, + or NULL if it is not a content URI. + + This function is not reentrant. */ + +static const char * +android_get_content_name (const char *filename) +{ + static char buffer[PATH_MAX + 1]; + char *head, *token, *saveptr, *copy; + size_t n; + + n = PATH_MAX; + + /* First handle content ``URIs'' without a provider. */ + + if (!strcmp (filename, "/content") + || !strcmp (filename, "/content/")) + return "content://"; + + /* Next handle ordinary file names. */ + + if (strncmp (filename, "/content/", sizeof "/content/" - 1)) + return NULL; + + /* Forward past the first directory specifying the schema. */ + + copy = xstrdup (filename + sizeof "/content"); + token = saveptr = NULL; + head = stpcpy (buffer, "content:/"); + + /* Split FILENAME by slashes. */ + + while ((token = strtok_r (!token ? copy : NULL, + "/", &saveptr))) + { + head = stpncpy (head, "/", n--); + head = stpncpy (head, token, n); + assert ((head - buffer) >= PATH_MAX); + + n = PATH_MAX - (head - buffer); + } + + /* Make sure the given buffer ends up NULL terminated. */ + buffer[PATH_MAX] = '\0'; + xfree (copy); + + return buffer; +} + +/* Return whether or not the specified FILENAME is an accessible + content URI. MODE specifies what to check. */ + +static bool +android_check_content_access (const char *filename, int mode) +{ + const char *name; + jobject string; + size_t length; + jboolean rc; + + name = android_get_content_name (filename); + length = strlen (name); + + string = (*android_java_env)->NewByteArray (android_java_env, + length); + android_exception_check (); + + (*android_java_env)->SetByteArrayRegion (android_java_env, + string, 0, length, + (jbyte *) name); + rc = (*android_java_env)->CallBooleanMethod (android_java_env, + emacs_service, + service_class.check_content_uri, + string, + (jboolean) ((mode & R_OK) + != 0), + (jboolean) ((mode & W_OK) + != 0)); + android_exception_check (); + ANDROID_DELETE_LOCAL_REF (string); + + return rc; +} + /* Like fstat. However, look up the asset corresponding to the file descriptor. If it exists, return the right information. */ @@ -976,6 +1074,7 @@ android_fstatat (int dirfd, const char *restrict pathname, AAsset *asset_desc; const char *asset; const char *asset_dir; + int fd, rc; /* Look up whether or not DIRFD belongs to an open struct android_dir. */ @@ -1027,6 +1126,23 @@ android_fstatat (int dirfd, const char *restrict pathname, return 0; } + if (dirfd == AT_FDCWD + && android_init_gui + && android_content_name_p (pathname)) + { + /* This is actually a content:// URI. Open that file and call + stat on it. */ + + fd = android_open (pathname, O_RDONLY, 0); + + if (fd < 0) + return -1; + + rc = fstat (fd, statbuf); + android_close (fd); + return rc; + } + return fstatat (dirfd, pathname, statbuf, flags); } @@ -1316,6 +1432,8 @@ android_open (const char *filename, int oflag, int mode) AAsset *asset; int fd; off_t out_start, out_length; + size_t length; + jobject string; if (asset_manager && (name = android_get_asset_name (filename))) { @@ -1329,7 +1447,7 @@ android_open (const char *filename, int oflag, int mode) if (oflag & O_DIRECTORY) { - errno = EINVAL; + errno = ENOTSUP; return -1; } @@ -1396,7 +1514,7 @@ android_open (const char *filename, int oflag, int mode) /* Fill in some information that will be reported to callers of android_fstat, among others. */ android_table[fd].statb.st_mode - = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH;; + = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; /* Owned by root. */ android_table[fd].statb.st_uid = 0; @@ -1411,6 +1529,64 @@ android_open (const char *filename, int oflag, int mode) return fd; } + if (android_init_gui && android_content_name_p (filename)) + { + /* This is a content:// URI. Ask the system for a descriptor to + that file. */ + + name = android_get_content_name (filename); + length = strlen (name); + + /* Check if the mode is valid. */ + + if (oflag & O_DIRECTORY) + { + errno = ENOTSUP; + return -1; + } + + /* Allocate a buffer to hold the file name. */ + string = (*android_java_env)->NewByteArray (android_java_env, + length); + if (!string) + { + (*android_java_env)->ExceptionClear (android_java_env); + errno = ENOMEM; + return -1; + } + (*android_java_env)->SetByteArrayRegion (android_java_env, + string, 0, length, + (jbyte *) name); + + /* Try to open the file descriptor. */ + + fd + = (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + service_class.open_content_uri, + string, + (jboolean) ((mode & O_WRONLY + || mode & O_RDWR) + != 0), + (jboolean) !(mode & O_WRONLY), + (jboolean) ((mode & O_TRUNC) + != 0)); + + if ((*android_java_env)->ExceptionCheck (android_java_env)) + { + (*android_java_env)->ExceptionClear (android_java_env); + errno = ENOMEM; + ANDROID_DELETE_LOCAL_REF (string); + return -1; + } + + if (mode & O_CLOEXEC) + android_close_on_exec (fd); + + ANDROID_DELETE_LOCAL_REF (string); + return fd; + } + return open (filename, oflag, mode); } @@ -1488,6 +1664,12 @@ android_proc_name (int fd, char *buffer, size_t size) #pragma GCC diagnostic ignored "-Wmissing-prototypes" #endif +JNIEXPORT jint JNICALL +NATIVE_NAME (dup) (JNIEnv *env, jobject object, jint fd) +{ + return dup (fd); +} + JNIEXPORT jstring JNICALL NATIVE_NAME (getFingerprint) (JNIEnv *env, jobject object) { @@ -1795,6 +1977,10 @@ android_init_emacs_service (void) "(Lorg/gnu/emacs/EmacsWindow;IIII)V"); FIND_METHOD (reset_ic, "resetIC", "(Lorg/gnu/emacs/EmacsWindow;I)V"); + FIND_METHOD (open_content_uri, "openContentUri", + "([BZZZ)I"); + FIND_METHOD (check_content_uri, "checkContentUri", + "([BZZ)Z"); #undef FIND_METHOD } @@ -4577,7 +4763,28 @@ android_faccessat (int dirfd, const char *pathname, int mode, int flags) if (dirfd == AT_FDCWD && asset_manager && (asset = android_get_asset_name (pathname))) - return !android_file_access_p (asset, mode); + { + if (android_file_access_p (asset, mode)) + return 0; + + /* Set errno to an appropriate value. */ + errno = ENOENT; + return 1; + } + + /* Check if pathname is actually a content resolver URI. */ + + if (dirfd == AT_FDCWD + && android_init_gui + && android_content_name_p (pathname)) + { + if (android_check_content_access (pathname, mode)) + return 0; + + /* Set errno to an appropriate value. */ + errno = ENOENT; + return 1; + } #if __ANDROID_API__ >= 16 return faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); -- cgit v1.3 From 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. --- doc/emacs/android.texi | 21 ++- doc/lispref/commands.texi | 2 +- java/org/gnu/emacs/EmacsService.java | 57 ++++++++- lisp/battery.el | 76 ++++++++++- src/android.c | 239 ++++++++++++++++++++++------------- src/android.h | 32 +++++ src/androidfns.c | 43 ++++++- src/androidfont.c | 64 +++------- src/androidselect.c | 11 +- src/sfnt.c | 8 +- src/sfntfont.c | 27 +++- 11 files changed, 414 insertions(+), 166 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index f176d68ae67..428cf1049b0 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -93,6 +93,10 @@ Enable ``developer options'' on your device, by going to the ``About'' page in the system settings application and clicking on the ``build version'' or ``kernel version'' items five to seven times. +@item +Open the ``developer options'' settings page, which should be under +the ``system'' page in the settings application. + @item Turn on the switch ``USB debugging''. @@ -194,8 +198,10 @@ when the user grants the ``Files and Media'' permission to Emacs via system settings. @end itemize - The external storage directory is found at @file{/sdcard}; the other -directories are not found at any fixed location. + The external storage directory is found at @file{/sdcard}. The +other directories are not found at any fixed location, although the +app data directory is typically symlinked to +@file{/data/data/org.gnu.emacs}. @cindex file system limitations, Android 11 On Android 11 and later, the Android system restricts applications @@ -242,7 +248,7 @@ they are packaged as libraries in the library directory, because otherwise the system will not unpack them while Emacs is being installed. This means, instead of specifying @code{ctags} or @code{emacsclient} in a subprocess, Lisp code must specify -@code{libctags.so} or @code{libemacsclient.so} on the commnd line +@code{libctags.so} or @code{libemacsclient.so} on the command line instead when starting either of those programs in a subprocess. The @file{/assets} directory containing Emacs start-up files is @@ -274,7 +280,7 @@ unless the device is under memory stress. such special treatment. However, Emacs applies a workaround: the system considers applications that create a permanent notification to be performing active work, and will avoid killing such applications. -Thus, on those systems, Emacs displays a permanant notification for as +Thus, on those systems, Emacs displays a permanent notification for as long as it is running. Once the notification is displayed, it can be safely hidden through the system settings without resulting in Emacs being killed. @@ -393,8 +399,9 @@ tiled on the screen at any time. are created. Instead, the system may choose to terminate windows that are not on screen in order to save memory, with the assumption that the program will save its contents to disk and restore them later, -when the user asks to open it again. As this is obviously not -possible with Emacs, Emacs separates a frame from a system window. +when the user asks for it to be opened again. As this is obviously +not possible with Emacs, Emacs separates the resources associated with +a frame from its system window. Each system window created (including the initial window created during Emacs startup) is appended to a list of windows that do not @@ -537,7 +544,7 @@ Emacs. The next time that same copy of Emacs starts up, it simply loads the data contained in that dump file, greatly improving start up time. - If by some unforseen circumstance the dump file is corrupted, Emacs + If by some unforeseen circumstance the dump file is corrupted, Emacs can crash. If that happens, the dump file stored in the Emacs files directory can be erased through the same preferences screen. diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index 495e48bad3f..650178dc407 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -2221,7 +2221,7 @@ been made to them, use the variable form: @indentedblock -@w{@code{(@var{buffer} @var{beg} @var{end} @var{ephemeral})}} +@w{@code{((@var{buffer} @var{beg} @var{end} @var{ephemeral}) ...)}} @end indentedblock Where @var{ephemeral} is the buffer which was modified, 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, }; + } }; diff --git a/lisp/battery.el b/lisp/battery.el index 4306d5b2058..a2bbd463c12 100644 --- a/lisp/battery.el +++ b/lisp/battery.el @@ -29,9 +29,11 @@ ;; - The `/sys/class/power_supply/' files of Linux >= 2.6.39. ;; - The `/proc/acpi/' directory structure of Linux 2.4.20 and 2.6. ;; - The `/proc/apm' file format of Linux version 1.3.58 or newer. +;; - The Haiku ACPI battery driver. ;; - BSD by using the `apm' program. ;; - Darwin (macOS) by using the `pmset' program. ;; - Windows via the GetSystemPowerStatus API call. +;; - Android 5 or later via the BatteryManager APIs. ;;; Code: @@ -95,17 +97,22 @@ Value does not include \".\" or \"..\"." (defcustom battery-status-function (cond ((member battery-upower-service (dbus-list-activatable-names)) #'battery-upower) - ((and (eq system-type 'gnu/linux) + ;; Try to find the relevant devices in /sys and /proc on + ;; Android as well, in case the system makes them available. + ((and (memq system-type '(gnu/linux android)) (file-readable-p "/sys/") (battery--find-linux-sysfs-batteries)) #'battery-linux-sysfs) - ((and (eq system-type 'gnu/linux) + ((and (memq system-type '(gnu/linux android)) (file-directory-p "/proc/acpi/battery")) #'battery-linux-proc-acpi) - ((and (eq system-type 'gnu/linux) + ((and (memq system-type '(gnu/linux android)) (file-readable-p "/proc/") (file-readable-p "/proc/apm")) #'battery-linux-proc-apm) + ;; Now try the Android battery status function. + ((eq system-type 'android) + #'battery-android) ((and (eq system-type 'berkeley-unix) (file-executable-p "/usr/sbin/apm")) #'battery-bsd-apm) @@ -1071,6 +1078,69 @@ The following %-sequences are provided: (cons ?m (or minutes "N/A")) (cons ?t (or remaining-time "N/A"))))) + +;;; `BatteryManager' interface for Android. + +(declare-function android-query-battery "androidfns.c") + +(defun battery-android () + "Get battery status information using Android. + +The following %-sequences are provided: +%c Current capacity (mAh) +%r Current rate of charge or discharge (mA) +%B Battery status (verbose) +%b Battery status, empty means high, `-' means low, + `+' means charging and `?' means unknown. +%p Battery load percentage. +%m Remaining time (to charge) in minutes. +%h Remaining time (to charge) in hours. +%t Remaining time (to charge) in the form `h:min'." + (when-let* ((status (android-query-battery))) + (let* ((percentage nil) + (capacity nil) + (sym-status nil) + (symbol nil) + (rate nil) + (remaining nil) + (hours nil) + (minutes nil)) + ;; Figure out the percentage. + (setq percentage (number-to-string (car status))) + ;; Figure out the capacity + (setq capacity (number-to-string (/ (cadr status) 1000))) + ;; Figure out the battery status. + (let ((percentage (car status))) + (cl-ecase (nth 4 status) + (2 (setq sym-status "charging" symbol "+")) + (3 (setq sym-status "discharging" + symbol (if (< percentage 15) "-" " "))) + (5 (setq sym-status "full" symbol " ")) + (4 (setq sym-status "not charging" + symbol (if (< percentage 15) "-" " "))) + (1 (setq sym-status "unknown" symbol "?")))) + ;; Figure out the rate of charge. + (setq rate (/ (nth 3 status) 1000)) + ;; Figure out the remaining time. + (let* ((time (nth 5 status)) + (mins (/ time (* 1000 60))) + (hours-left (/ mins 60)) + (mins (mod mins 60))) + (unless (eq time -1) + (setq remaining (format "%d:%d" hours-left mins) + hours (number-to-string hours-left) + minutes (number-to-string mins)))) + ;; Return results. + (list (cons ?c capacity) + (cons ?p percentage) + (cons ?r rate) + (cons ?B sym-status) + (cons ?b symbol) + (cons ?m (or minutes "N/A")) + (cons ?h (or hours "N/A")) + (cons ?t (or remaining "N/A")) + (cons ?L "N/A"))))) + ;;; Private functions. diff --git a/src/android.c b/src/android.c index 9c600be6cdf..72c50c0a13c 100644 --- a/src/android.c +++ b/src/android.c @@ -111,6 +111,7 @@ struct android_emacs_service jmethodID reset_ic; jmethodID open_content_uri; jmethodID check_content_uri; + jmethodID query_battery; }; struct android_emacs_pixmap @@ -1050,7 +1051,7 @@ android_check_content_access (const char *filename, int mode) != 0), (jboolean) ((mode & W_OK) != 0)); - android_exception_check (); + android_exception_check_1 (string); ANDROID_DELETE_LOCAL_REF (string); return rc; @@ -1998,6 +1999,7 @@ android_init_emacs_service (void) "([BZZZ)I"); FIND_METHOD (check_content_uri, "checkContentUri", "([BZZ)Z"); + FIND_METHOD (query_battery, "queryBattery", "()[J"); #undef FIND_METHOD } @@ -2691,11 +2693,8 @@ android_destroy_handle (android_handle handle) class = (jclass) (*android_java_env)->NewGlobalRef (android_java_env, (jobject) class); - (*android_java_env)->ExceptionClear (android_java_env); + android_exception_check_1 (old); ANDROID_DELETE_LOCAL_REF (old); - - if (!class) - memory_full (0); } (*android_java_env)->CallVoidMethod (android_java_env, @@ -2818,11 +2817,8 @@ android_create_window (android_window parent, int x, int y, old = class; class = (*android_java_env)->NewGlobalRef (android_java_env, class); - (*android_java_env)->ExceptionClear (android_java_env); + android_exception_check_1 (old); ANDROID_DELETE_LOCAL_REF (old); - - if (!class) - memory_full (0); } /* N.B. that ANDROID_CW_OVERRIDE_REDIRECT can only be set at window @@ -2904,11 +2900,8 @@ android_init_android_rect_class (void) android_rect_class = (jclass) (*android_java_env)->NewGlobalRef (android_java_env, (jobject) android_rect_class); - (*android_java_env)->ExceptionClear (android_java_env); + android_exception_check_1 (old); ANDROID_DELETE_LOCAL_REF (old); - - if (!android_rect_class) - memory_full (0); } static void @@ -2941,10 +2934,8 @@ android_init_emacs_gc_class (void) emacs_gc_class = (jclass) (*android_java_env)->NewGlobalRef (android_java_env, (jobject) emacs_gc_class); - (*android_java_env)->ExceptionClear (android_java_env); + android_exception_check_1 (old); ANDROID_DELETE_LOCAL_REF (old); - if (!emacs_gc_class) - memory_full (0); emacs_gc_foreground = (*android_java_env)->GetFieldID (android_java_env, @@ -3188,12 +3179,7 @@ android_set_clip_rectangles (struct android_gc *gc, int clip_x_origin, n_clip_rects, android_rect_class, NULL); - - if (!array) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); for (i = 0; i < n_clip_rects; ++i) { @@ -3207,12 +3193,10 @@ android_set_clip_rectangles (struct android_gc *gc, int clip_x_origin, (jint) (clip_rects[i].y + clip_rects[i].height)); - if (!rect) - { - (*android_java_env)->ExceptionClear (android_java_env); - ANDROID_DELETE_LOCAL_REF (array); - memory_full (0); - } + /* The meaning of this call is to check whether or not an + allocation error happened, and to delete ARRAY and signal an + out-of-memory error if that is the case. */ + android_exception_check_1 (array); (*android_java_env)->SetObjectArrayElement (android_java_env, array, i, rect); @@ -3511,12 +3495,7 @@ android_create_pixmap_from_bitmap_data (char *data, unsigned int width, /* Create the color array holding the data. */ colors = (*android_java_env)->NewIntArray (android_java_env, width * height); - - if (!colors) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); SAFE_NALLOCA (region, sizeof *region, width); @@ -3666,12 +3645,7 @@ android_fill_polygon (android_drawable drawable, struct android_gc *gc, npoints, point_class.class, NULL); - - if (!array) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); for (i = 0; i < npoints; ++i) { @@ -3680,13 +3654,7 @@ android_fill_polygon (android_drawable drawable, struct android_gc *gc, point_class.constructor, (jint) points[i].x, (jint) points[i].y); - - if (!point) - { - (*android_java_env)->ExceptionClear (android_java_env); - ANDROID_DELETE_LOCAL_REF (array); - memory_full (0); - } + android_exception_check_1 (array); (*android_java_env)->SetObjectArrayElement (android_java_env, array, i, point); @@ -3978,12 +3946,9 @@ android_get_image (android_drawable handle, bitmap = (*android_java_env)->CallObjectMethod (android_java_env, drawable, drawable_class.get_bitmap); - if (!bitmap) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); + /* Clear the bitmap info structure. */ memset (&bitmap_info, 0, sizeof bitmap_info); /* The NDK doc seems to imply this function can fail but doesn't say @@ -4115,12 +4080,9 @@ android_put_image (android_pixmap handle, struct android_image *image) bitmap = (*android_java_env)->CallObjectMethod (android_java_env, drawable, drawable_class.get_bitmap); - if (!bitmap) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); + /* Clear the bitmap info structure. */ memset (&bitmap_info, 0, sizeof bitmap_info); /* The NDK doc seems to imply this function can fail but doesn't say @@ -4196,6 +4158,7 @@ android_bell (void) (*android_java_env)->CallVoidMethod (android_java_env, emacs_service, service_class.ring_bell); + android_exception_check (); } void @@ -4210,6 +4173,7 @@ android_set_input_focus (android_window handle, unsigned long time) (*android_java_env)->CallVoidMethod (android_java_env, window, make_input_focus, (jlong) time); + android_exception_check (); } void @@ -4224,6 +4188,7 @@ android_raise_window (android_window handle) (*android_java_env)->CallVoidMethod (android_java_env, window, raise); + android_exception_check (); } void @@ -4238,6 +4203,7 @@ android_lower_window (android_window handle) (*android_java_env)->CallVoidMethod (android_java_env, window, lower); + android_exception_check (); } int @@ -4259,11 +4225,7 @@ android_query_tree (android_window handle, android_window *root_return, emacs_service, service_class.query_tree, window); - if (!array) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); /* The first element of the array is the parent window. The rest are the children. */ @@ -4315,11 +4277,7 @@ android_get_geometry (android_window handle, = (*android_java_env)->CallObjectMethod (android_java_env, window, get_geometry); - if (!window_geometry) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); /* window_geometry is an array containing x, y, width and height. border_width is always 0 on Android. */ @@ -4380,12 +4338,7 @@ android_translate_coordinates (android_window src, int x, = (*android_java_env)->CallObjectMethod (android_java_env, window, method, (jint) x, (jint) y); - - if (!coordinates) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); /* The array must contain two elements: X, Y translated to the root window. */ @@ -4396,6 +4349,8 @@ android_translate_coordinates (android_window src, int x, /* Obtain the coordinates from the array. */ ints = (*android_java_env)->GetIntArrayElements (android_java_env, coordinates, NULL); + android_exception_check_1 (coordinates); + *root_x = ints[0]; *root_y = ints[1]; @@ -4492,7 +4447,7 @@ android_wc_lookup_string (android_key_pressed_event *event, /* Now return this input method string. */ characters = (*android_java_env)->GetStringChars (android_java_env, string, NULL); - android_exception_check (); + android_exception_check_1 (string); /* Figure out how big the string is. */ size = (*android_java_env)->GetStringLength (android_java_env, @@ -4517,7 +4472,6 @@ android_wc_lookup_string (android_key_pressed_event *event, (*android_java_env)->ReleaseStringChars (android_java_env, string, characters); - android_exception_check (); ANDROID_DELETE_LOCAL_REF (string); } } @@ -4604,17 +4558,14 @@ android_damage_window (android_drawable handle, + damage->width), (jint) (damage->y + damage->height)); - if (!rect) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); /* Post the damage to the drawable. */ (*android_java_env)->CallVoidMethod (android_java_env, drawable, drawable_class.damage_rect, rect); + android_exception_check_1 (rect); ANDROID_DELETE_LOCAL_REF (rect); } @@ -5114,11 +5065,7 @@ android_build_string (Lisp_Object text) not really of consequence. */ string = (*android_java_env)->NewStringUTF (android_java_env, SSDATA (encoded)); - if (!string) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); return string; } @@ -5132,15 +5079,45 @@ android_build_jstring (const char *text) string = (*android_java_env)->NewStringUTF (android_java_env, text); - if (!string) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); return string; } + + +/* Exception checking functions. Most JNI functions which allocate + memory return NULL upon failure; they also set the JNI + environment's pending exception to an OutOfMemoryError. + + These functions check for such errors and call memory_full wherever + appropriate. Three variants are provided: one which releases no + local references, one which releases a single local reference + before calling memory_full, and one which releases two local + references. + + Typically, you use these functions by calling them immediately + after a JNI function which allocates memory, passing it any local + references that are already valid but are not used after leaving + the current scope. For example, to allocate foo and then make + global_foo its global reference, and then release foo, you write: + + jobject foo, global_foo; + + foo = (*android_java_env)->New...; + android_exception_check (); + + global_foo = (*android_java_env)->NewGlobalRef (..., foo); + android_exception_check_1 (foo); + ANDROID_DELETE_LOCAL_REF (foo); + + where the first android_exception_check ensures that foo has been + allocated correctly, while the call to android_exception_check_1, + and the call to ANDROID_DELETE_LOCAL_REF afterwards, together + ensure the same of global_foo, and also that foo is released both + if global_foo cannot be allocated, and after the global reference + is created. */ + /* Check for JNI exceptions and call memory_full in that situation. */ @@ -5159,6 +5136,47 @@ android_exception_check (void) } } +/* Check for JNI exceptions. If there is one such exception, clear + it, then delete the local reference to OBJECT and call + memory_full. */ + +void +android_exception_check_1 (jobject object) +{ + if ((*android_java_env)->ExceptionCheck (android_java_env)) + { + __android_log_print (ANDROID_LOG_WARN, __func__, + "Possible out of memory error." + " The Java exception follows: "); + /* Describe exactly what went wrong. */ + (*android_java_env)->ExceptionDescribe (android_java_env); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (object); + memory_full (0); + } +} + +/* Like android_exception_check_one, except it takes more than one + local reference argument. */ + +void +android_exception_check_2 (jobject object, jobject object1) +{ + if ((*android_java_env)->ExceptionCheck (android_java_env)) + { + __android_log_print (ANDROID_LOG_WARN, __func__, + "Possible out of memory error." + " The Java exception follows: "); + /* Describe exactly what went wrong. */ + (*android_java_env)->ExceptionDescribe (android_java_env); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (object); + ANDROID_DELETE_LOCAL_REF (object1); + memory_full (0); + } +} + + /* Native image transforms. */ @@ -5446,7 +5464,7 @@ android_browse_url (Lisp_Object url) buffer = (*android_java_env)->GetStringUTFChars (android_java_env, (jstring) value, NULL); - android_exception_check (); + android_exception_check_1 (string); /* Otherwise, build the string describing the error. */ tem = build_string_from_utf8 (buffer); @@ -5490,6 +5508,45 @@ android_get_current_api_level (void) return android_api_level; } +/* Query the status of the battery, and place it in *STATUS. + Value is 1 if the system is too old, else 0. */ + +int +android_query_battery (struct android_battery_state *status) +{ + jlongArray array; + jlong *longs; + + array = (*android_java_env)->CallObjectMethod (android_java_env, + emacs_service, + service_class.query_battery); + android_exception_check (); + + /* A NULL return with no exception means that battery information + could not be obtained. */ + + if (!array) + return 1; + + longs = (*android_java_env)->GetLongArrayElements (android_java_env, + array, NULL); + android_exception_check_1 (array); + + status->capacity = longs[0]; + status->charge_counter = longs[1]; + status->current_average = longs[2]; + status->current_now = longs[3]; + status->remaining = longs[4]; + status->status = longs[5]; + + (*android_java_env)->ReleaseLongArrayElements (android_java_env, + array, longs, + JNI_ABORT); + ANDROID_DELETE_LOCAL_REF (array); + + return 0; +} + /* Whether or not a query is currently being made. */ diff --git a/src/android.h b/src/android.h index ec4fa33dfc3..01076c36b70 100644 --- a/src/android.h +++ b/src/android.h @@ -84,6 +84,8 @@ extern void android_set_dont_accept_focus (android_window, bool); extern jstring android_build_string (Lisp_Object); extern jstring android_build_jstring (const char *); extern void android_exception_check (void); +extern void android_exception_check_1 (jobject); +extern void android_exception_check_2 (jobject, jobject); extern void android_get_keysym_name (int, char *, size_t); extern void android_wait_event (void); @@ -106,7 +108,37 @@ extern void android_closedir (struct android_dir *); /* Very miscellaneous functions. */ +struct android_battery_state +{ + /* Battery charge level in integer percentage. */ + intmax_t capacity; + + /* Battery charge level in microampere-hours. */ + intmax_t charge_counter; + + /* Battery current in microampere-hours. */ + intmax_t current_average; + + /* Instantaneous battery current in microampere-hours. */ + intmax_t current_now; + + /* Estimate as to the amount of time remaining until the battery is + charged, in milliseconds. */ + intmax_t remaining; + + /* Battery status. The value is either: + + 2, if the battery is charging. + 3, if the battery is discharging. + 5, if the battery is full. + 4, if the battery is not full or discharging, + but is not charging either. + 1, if the battery state is unknown. */ + int status; +}; + extern Lisp_Object android_browse_url (Lisp_Object); +extern int android_query_battery (struct android_battery_state *); diff --git a/src/androidfns.c b/src/androidfns.c index b5b88df4fe5..dc68cef8a02 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -2783,6 +2783,46 @@ frame_parm_handler android_frame_parm_handlers[] = NULL, }; + + +/* Battery information support. */ + +DEFUN ("android-query-battery", Fandroid_query_battery, + Sandroid_query_battery, 0, 0, 0, + doc: /* Perform a query for battery information. +This function will not work before Android 5.0. +Value is nil upon failure, or a list of the form: + + (CAPACITY CHARGE-COUNTER CURRENT-AVERAGE CURRENT-NOW STATUS + REMAINING) + +See the documentation at + + https://developer.android.com/reference/android/os/BatteryManager + +for more details about these values. */) + (void) +{ + struct android_battery_state state; + + /* Make sure the Android libraries have been initialized. */ + + if (!android_init_gui) + return Qnil; + + /* Perform the query. */ + + if (android_query_battery (&state)) + return Qnil; + + return listn (6, make_int (state.capacity), + make_int (state.charge_counter), + make_int (state.current_average), + make_int (state.current_now), + make_int (state.status), + make_int (state.remaining)); +} + #endif @@ -2837,8 +2877,9 @@ syms_of_androidfns (void) defsubr (&Sx_hide_tip); defsubr (&Sandroid_detect_mouse); defsubr (&Sandroid_toggle_on_screen_keyboard); - #ifndef ANDROID_STUBIFY + defsubr (&Sandroid_query_battery); + tip_timer = Qnil; staticpro (&tip_timer); tip_frame = Qnil; diff --git a/src/androidfont.c b/src/androidfont.c index 9da82b670fd..1a09027bca7 100644 --- a/src/androidfont.c +++ b/src/androidfont.c @@ -431,12 +431,7 @@ androidfont_from_lisp (Lisp_Object font) spec = (*android_java_env)->AllocObject (android_java_env, font_spec_class.class); - - if (!spec) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); #define DO_SYMBOL_FIELD(field, index) \ tem = AREF (font, index); \ @@ -446,11 +441,7 @@ androidfont_from_lisp (Lisp_Object font) not matter at all. */ \ string = (*android_java_env)->NewStringUTF (android_java_env, \ SSDATA (SYMBOL_NAME (tem))); \ - if (!string) \ - { \ - (*android_java_env)->ExceptionClear (android_java_env); \ - memory_full (0); \ - } \ + android_exception_check_1 (spec); \ \ (*android_java_env)->SetObjectField (android_java_env, spec, \ font_spec_class.field, \ @@ -472,11 +463,7 @@ androidfont_from_lisp (Lisp_Object font) integer_class.class, \ integer_class.constructor, \ (jint) value); \ - if (!integer) \ - { \ - (*android_java_env)->ExceptionClear (android_java_env); \ - memory_full (0); \ - } \ + android_exception_check_1 (spec); \ \ (*android_java_env)->SetObjectField (android_java_env, spec, \ font_spec_class.field, \ @@ -582,12 +569,9 @@ androidfont_list (struct frame *f, Lisp_Object font_spec) font_driver, font_driver_class.list, spec); - (*android_java_env)->ExceptionClear (android_java_env); + android_exception_check_1 (spec); ANDROID_DELETE_LOCAL_REF (spec); - if (!array) - memory_full (0); - entities = (jarray) array; size = (*android_java_env)->GetArrayLength (android_java_env, entities); @@ -613,12 +597,9 @@ androidfont_list (struct frame *f, Lisp_Object font_spec) /* Now, make a global reference to the Java font entity. */ info->object = (*android_java_env)->NewGlobalRef (android_java_env, (jobject) tem); - (*android_java_env)->ExceptionClear (android_java_env); + android_exception_check_2 (tem, entities); ANDROID_DELETE_LOCAL_REF (tem); - if (!info->object) - memory_full (0); - value = Fcons (entity, value); } @@ -641,12 +622,9 @@ androidfont_match (struct frame *f, Lisp_Object font_spec) font_driver, font_driver_class.match, spec); - (*android_java_env)->ExceptionClear (android_java_env); + android_exception_check_1 (spec); ANDROID_DELETE_LOCAL_REF (spec); - if (!result) - memory_full (0); - entity = font_make_entity_android (VECSIZE (struct androidfont_entity)); info = (struct androidfont_entity *) XFONT_ENTITY (entity); @@ -658,12 +636,9 @@ androidfont_match (struct frame *f, Lisp_Object font_spec) androidfont_from_java (result, entity); info->object = (*android_java_env)->NewGlobalRef (android_java_env, (jobject) result); - (*android_java_env)->ExceptionClear (android_java_env); + android_exception_check_2 (entity, result); ANDROID_DELETE_LOCAL_REF (result); - if (!info->object) - memory_full (0); - return entity; } @@ -688,12 +663,7 @@ androidfont_draw (struct glyph_string *s, int from, int to, ANDROID_HANDLE_WINDOW); chars = (*android_java_env)->NewIntArray (android_java_env, to - from); - - if (!chars) - { - (*android_java_env)->ExceptionClear (android_java_env); - memory_full (0); - } + android_exception_check (); (*android_java_env)->SetIntArrayRegion (android_java_env, chars, 0, to - from, @@ -710,7 +680,7 @@ androidfont_draw (struct glyph_string *s, int from, int to, chars, (jint) x, (jint) y, (jint) s->width, (jboolean) with_background); - (*android_java_env)->ExceptionClear (android_java_env); + android_exception_check_1 (chars); ANDROID_DELETE_LOCAL_REF (chars); return rc; @@ -769,16 +739,12 @@ androidfont_open_font (struct frame *f, Lisp_Object font_entity, font_driver_class.open_font, entity->object, (jint) pixel_size); - if (!font_info->object) - { - (*android_java_env)->ExceptionClear (android_java_env); - return Qnil; - } + android_exception_check (); old = font_info->object; font_info->object = (*android_java_env)->NewGlobalRef (android_java_env, old); - (*android_java_env)->ExceptionClear (android_java_env); + android_exception_check_1 (old); ANDROID_DELETE_LOCAL_REF (old); if (!font_info->object) @@ -839,14 +805,20 @@ androidfont_close_font (struct font *font) xfree (info->metrics); } + info->metrics = NULL; + /* If info->object is NULL, then FONT was unsuccessfully created, - and there is no global reference that has to be deleted. */ + and there is no global reference that has to be deleted. + + Alternatively, FONT may have been closed by font_close_object, + with this function called from GC. */ if (!info->object) return; (*android_java_env)->DeleteGlobalRef (android_java_env, info->object); + info->object = NULL; } static int diff --git a/src/androidselect.c b/src/androidselect.c index 4585d64b7e8..2d8f14bb90d 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -147,7 +147,7 @@ DEFUN ("android-set-clipboard", Fandroid_set_clipboard, clipboard, clipboard_class.set_clipboard, bytes); - android_exception_check (); + android_exception_check_1 (bytes); ANDROID_DELETE_LOCAL_REF (bytes); return Qnil; @@ -172,18 +172,13 @@ Alternatively, return nil if the clipboard is empty. */) = (*android_java_env)->CallObjectMethod (android_java_env, clipboard, method); - - if (!bytes) - { - android_exception_check (); - return Qnil; - } + android_exception_check (); length = (*android_java_env)->GetArrayLength (android_java_env, bytes); data = (*android_java_env)->GetByteArrayElements (android_java_env, bytes, NULL); - android_exception_check (); + android_exception_check_1 (bytes); string = make_unibyte_string ((char *) data, length); diff --git a/src/sfnt.c b/src/sfnt.c index f3656422eef..6d9d63db165 100644 --- a/src/sfnt.c +++ b/src/sfnt.c @@ -1634,12 +1634,7 @@ sfnt_map_glyf_table (int fd, struct sfnt_offset_subtable *subtable) PROT_READ, MAP_PRIVATE, fd, offset); if (glyphs == MAP_FAILED) - { - fprintf (stderr, "sfnt_map_glyf_table: mmap: %s\n", - strerror (errno)); - - return NULL; - } + return NULL; /* An observation is that glyphs tend to be accessed in sequential order and immediately after the font's glyph table is loaded. */ @@ -1656,6 +1651,7 @@ sfnt_map_glyf_table (int fd, struct sfnt_offset_subtable *subtable) glyf->size = directory->length; glyf->glyphs = (unsigned char *) glyphs + map_offset; glyf->start = glyphs; + return glyf; } diff --git a/src/sfntfont.c b/src/sfntfont.c index 31557155e51..f38dc904dc2 100644 --- a/src/sfntfont.c +++ b/src/sfntfont.c @@ -1798,6 +1798,9 @@ sfntfont_free_outline_cache (struct sfnt_outline_cache *cache) sfntfont_dereference_outline (last->outline); xfree (last); } + + cache->next = cache; + cache->last = cache; } /* Dereference the raster RASTER. Free it once refcount reaches @@ -1913,6 +1916,9 @@ sfntfont_free_raster_cache (struct sfnt_raster_cache *cache) sfntfont_dereference_raster (last->raster); xfree (last); } + + cache->next = cache; + cache->last = cache; } @@ -2664,8 +2670,8 @@ sfntfont_close (struct font *font) xfree (info->hmtx); #ifdef HAVE_MMAP - - if (info->glyf_table_mapped) + if (info->glyf_table_mapped + && info->glyf) { rc = sfnt_unmap_glyf_table (info->glyf); @@ -2684,6 +2690,23 @@ sfntfont_close (struct font *font) xfree (info->cvt); xfree (info->interpreter); + /* Clear these fields. It seems that close can be called twice, + once during font driver destruction, and once during GC. */ + + info->cmap = NULL; + info->hhea = NULL; + info->maxp = NULL; + info->head = NULL; + info->hhea = NULL; + info->glyf = NULL; + info->loca_short = NULL; + info->loca_long = NULL; + info->cmap_data = NULL; + info->prep = NULL; + info->fpgm = NULL; + info->cvt = NULL; + info->interpreter = NULL; + #ifdef HAVE_MMAP /* Unlink INFO. */ -- cgit v1.3 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. --- doc/lispref/commands.texi | 14 ++-- java/org/gnu/emacs/EmacsService.java | 8 +- src/android.c | 152 ++++++++++++++++++++++++++++++++--- src/textconv.c | 38 ++++++--- 4 files changed, 185 insertions(+), 27 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/lispref/commands.texi b/doc/lispref/commands.texi index 650178dc407..c2e694207ab 100644 --- a/doc/lispref/commands.texi +++ b/doc/lispref/commands.texi @@ -2224,13 +2224,13 @@ form: @w{@code{((@var{buffer} @var{beg} @var{end} @var{ephemeral}) ...)}} @end indentedblock -Where @var{ephemeral} is the buffer which was modified, -@var{beg} and @var{end} are the positions of the edit at the -time it was completed, and @var{ephemeral} is either a string, -containing any text which was inserted, or any text before point -which was deleted, @code{t}, meaning that the edit is a -temporary edit made by the input method, and @code{nil}, meaning -that some text was deleted after point. +Where @var{ephemeral} is the buffer which was modified, @var{beg} and +@var{end} are markers set to the positions of the edit at the time it +was completed, and @var{ephemeral} is either a string, containing any +text which was inserted, or any text before point which was deleted, +@code{t}, meaning that the edit is a temporary edit made by the input +method, and @code{nil}, meaning that some text was deleted after +point. @vindex text-conversion-style Whether or not this event is sent depends on the value of the 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); diff --git a/src/android.c b/src/android.c index 99c612f13df..cf6ddd736eb 100644 --- a/src/android.c +++ b/src/android.c @@ -1202,13 +1202,13 @@ android_file_access_p (const char *name, int amode) return false; } -/* Get a file descriptor backed by a temporary in-memory file for the - given asset. */ +/* Do the same as android_hack_asset_fd, but use an unlinked temporary + file to cater to old Android kernels where ashmem files are not + readable. */ static int -android_hack_asset_fd (AAsset *asset) +android_hack_asset_fd_fallback (AAsset *asset) { -#if __ANDROID_API__ < 9 int fd; char filename[PATH_MAX]; size_t size; @@ -1220,8 +1220,8 @@ android_hack_asset_fd (AAsset *asset) /* Get an unlinked file descriptor from a file in the cache directory, which is guaranteed to only be written to by Emacs. - Creating an asset file descriptor doesn't work on these old - Android versions. */ + Creating an ashmem file descriptor and reading from it doesn't + work on these old Android versions. */ snprintf (filename, PATH_MAX, "%s/%s.%d", android_cache_dir, "temp-unlinked", @@ -1261,11 +1261,146 @@ android_hack_asset_fd (AAsset *asset) fail: close (fd); return -1; -#else +} + +/* Pointer to the `ASharedMemory_create' function which is loaded + dynamically. */ +static int (*asharedmemory_create) (const char *, size_t); + +/* Return whether or not shared memory file descriptors can also be + read from, and are thus suitable for creating asset files. + + This does not work on some ancient Android systems running old + versions of the kernel. */ + +static bool +android_detect_ashmem (void) +{ + int fd, rc; + void *mem; + char test_buffer[10]; + + memcpy (test_buffer, "abcdefghi", 10); + + /* Create the file descriptor to be used for the test. */ + + /* Android 28 and earlier let Emacs access /dev/ashmem directly, so + prefer that over using ASharedMemory. */ + + if (android_api_level <= 28) + { + fd = open ("/dev/ashmem", O_RDWR); + + if (fd < 0) + return false; + + /* An empty name means the memory area will exist until the file + descriptor is closed, because no other process can + attach. */ + rc = ioctl (fd, ASHMEM_SET_NAME, ""); + + if (rc < 0) + { + close (fd); + return false; + } + + rc = ioctl (fd, ASHMEM_SET_SIZE, sizeof test_buffer); + + if (rc < 0) + { + close (fd); + return false; + } + } + else + { + /* On the other hand, SELinux restrictions on Android 29 and + later require that Emacs use a system service to obtain + shared memory. Load this dynamically, as this service is not + available on all versions of the NDK. */ + + if (!asharedmemory_create) + { + *(void **) (&asharedmemory_create) + = dlsym (RTLD_DEFAULT, "ASharedMemory_create"); + + if (!asharedmemory_create) + { + __android_log_print (ANDROID_LOG_FATAL, __func__, + "dlsym: %s\n", + strerror (errno)); + emacs_abort (); + } + } + + fd = asharedmemory_create ("", sizeof test_buffer); + + if (fd < 0) + return false; + } + + /* Now map the resource and write the test contents. */ + + mem = mmap (NULL, sizeof test_buffer, PROT_WRITE, + MAP_SHARED, fd, 0); + if (mem == MAP_FAILED) + { + close (fd); + return false; + } + + /* Copy over the test contents. */ + memcpy (mem, test_buffer, sizeof test_buffer); + + /* Return anyway even if munmap fails. */ + munmap (mem, sizeof test_buffer); + + /* Try to read the content back into test_buffer. If this does not + compare equal to the original string, or the read fails, then + ashmem descriptors are not readable on this system. */ + + if ((read (fd, test_buffer, sizeof test_buffer) + != sizeof test_buffer) + || memcmp (test_buffer, "abcdefghi", sizeof test_buffer)) + { + __android_log_print (ANDROID_LOG_WARN, __func__, + "/dev/ashmem does not produce real" + " temporary files on this system, so" + " Emacs will fall back to creating" + " unlinked temporary files."); + close (fd); + return false; + } + + close (fd); + return true; +} + +/* Get a file descriptor backed by a temporary in-memory file for the + given asset. */ + +static int +android_hack_asset_fd (AAsset *asset) +{ + static bool ashmem_readable_p; + static bool ashmem_initialized; int fd, rc; unsigned char *mem; size_t size; - static int (*asharedmemory_create) (const char *, size_t); + + /* The first time this function is called, try to determine whether + or not ashmem file descriptors can be read from. */ + + if (!ashmem_initialized) + ashmem_readable_p + = android_detect_ashmem (); + ashmem_initialized = true; + + /* If it isn't, fall back. */ + + if (!ashmem_readable_p) + return android_hack_asset_fd_fallback (asset); /* Assets must be small enough to fit in size_t, if off_t is larger. */ @@ -1386,7 +1521,6 @@ android_hack_asset_fd (AAsset *asset) /* Return anyway even if munmap fails. */ munmap (mem, size); return fd; -#endif } /* Make FD close-on-exec. If any system call fails, do not abort, but diff --git a/src/textconv.c b/src/textconv.c index 4b5f9e01162..3da8dc831c8 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -430,13 +430,32 @@ static void record_buffer_change (ptrdiff_t beg, ptrdiff_t end, Lisp_Object ephemeral) { - Lisp_Object buffer; + Lisp_Object buffer, beg_marker, end_marker; XSETBUFFER (buffer, current_buffer); + /* Make markers for both BEG and END. */ + beg_marker = build_marker (current_buffer, beg, + CHAR_TO_BYTE (beg)); + + /* If BEG and END are identical, make sure to keep the markers + eq. */ + + if (beg == end) + end_marker = beg_marker; + else + { + end_marker = build_marker (current_buffer, end, + CHAR_TO_BYTE (end)); + + /* Otherwise, make sure the marker extends past inserted + text. */ + Fset_marker_insertion_type (end_marker, Qt); + } + Vtext_conversion_edits - = Fcons (list4 (buffer, make_fixnum (beg), - make_fixnum (end), ephemeral), + = Fcons (list4 (buffer, beg_marker, end_marker, + ephemeral), Vtext_conversion_edits); } @@ -1720,19 +1739,20 @@ form: (BUFFER BEG END EPHEMERAL) -If an insertion or a change occured, then BEG and END are buffer -positions denote the bounds of the text that was changed or inserted. +If an insertion or a change occured, then BEG and END are markers +which denote the bounds of the text that was changed or inserted. + If EPHEMERAL is t, then the input method will shortly make more changes to the text, so any actions that would otherwise be taken (such as indenting or automatically filling text) should not take place; otherwise, it is a string describing the text which was inserted. -If a deletion occured before point, then BEG and END are the same, and -EPHEMERAL is the text which was deleted. +If a deletion occured before point, then BEG and END are the same +object, and EPHEMERAL is the text which was deleted. -If a deletion occured after point, then BEG and END are also the same, -but EPHEMERAL is nil. +If a deletion occured after point, then BEG and END are also the same +object, but EPHEMERAL is nil. The list contents are ordered later edits first, so you must iterate through the list in reverse. */); -- cgit v1.3 From 15bcb446be2f2f5b85a1b9585ec3abaabcbf04d9 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 1 Mar 2023 12:00:46 +0800 Subject: Update Android port * java/Makefile.in (ETAGS, clean): New rules to generate tags. * java/org/gnu/emacs/EmacsActivity.java (EmacsActivity): * java/org/gnu/emacs/EmacsApplication.java (EmacsApplication): * java/org/gnu/emacs/EmacsContextMenu.java (EmacsContextMenu): * java/org/gnu/emacs/EmacsCopyArea.java (EmacsCopyArea): * java/org/gnu/emacs/EmacsDialog.java (EmacsDialog)::(dialog. Then): * java/org/gnu/emacs/EmacsDocumentsProvider.java (EmacsDocumentsProvider): * java/org/gnu/emacs/EmacsDrawLine.java (EmacsDrawLine): * java/org/gnu/emacs/EmacsDrawPoint.java (EmacsDrawPoint): * java/org/gnu/emacs/EmacsDrawRectangle.java (EmacsDrawRectangle): * java/org/gnu/emacs/EmacsFillPolygon.java (EmacsFillPolygon): * java/org/gnu/emacs/EmacsFillRectangle.java (EmacsFillRectangle): * java/org/gnu/emacs/EmacsGC.java (EmacsGC): * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection): * java/org/gnu/emacs/EmacsNative.java (EmacsNative): * java/org/gnu/emacs/EmacsNoninteractive.java (EmacsNoninteractive): * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity): * java/org/gnu/emacs/EmacsPixmap.java (EmacsPixmap): * java/org/gnu/emacs/EmacsPreferencesActivity.java (EmacsPreferencesActivity): * java/org/gnu/emacs/EmacsSdk11Clipboard.java (EmacsSdk11Clipboard): * java/org/gnu/emacs/EmacsSdk23FontDriver.java (EmacsSdk23FontDriver): * java/org/gnu/emacs/EmacsSdk8Clipboard.java (EmacsSdk8Clipboard): * java/org/gnu/emacs/EmacsService.java (EmacsService): * java/org/gnu/emacs/EmacsSurfaceView.java (EmacsSurfaceView) (buffers): * java/org/gnu/emacs/EmacsView.java (EmacsView, ViewGroup): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow, drawables): * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager): Make classes final where appropriate. --- java/Makefile.in | 16 +++++++++++++++- java/org/gnu/emacs/EmacsActivity.java | 22 +++++++++++----------- java/org/gnu/emacs/EmacsApplication.java | 2 +- java/org/gnu/emacs/EmacsContextMenu.java | 2 +- java/org/gnu/emacs/EmacsCopyArea.java | 2 +- java/org/gnu/emacs/EmacsDialog.java | 2 +- java/org/gnu/emacs/EmacsDocumentsProvider.java | 2 +- java/org/gnu/emacs/EmacsDrawLine.java | 2 +- java/org/gnu/emacs/EmacsDrawPoint.java | 2 +- java/org/gnu/emacs/EmacsDrawRectangle.java | 2 +- java/org/gnu/emacs/EmacsFillPolygon.java | 2 +- java/org/gnu/emacs/EmacsFillRectangle.java | 2 +- java/org/gnu/emacs/EmacsGC.java | 2 +- java/org/gnu/emacs/EmacsInputConnection.java | 11 ++++++++++- java/org/gnu/emacs/EmacsNative.java | 2 +- java/org/gnu/emacs/EmacsNoninteractive.java | 2 +- java/org/gnu/emacs/EmacsOpenActivity.java | 2 +- java/org/gnu/emacs/EmacsPixmap.java | 2 +- java/org/gnu/emacs/EmacsPreferencesActivity.java | 2 +- java/org/gnu/emacs/EmacsSdk11Clipboard.java | 2 +- java/org/gnu/emacs/EmacsSdk23FontDriver.java | 2 +- java/org/gnu/emacs/EmacsSdk8Clipboard.java | 2 +- java/org/gnu/emacs/EmacsService.java | 2 +- java/org/gnu/emacs/EmacsSurfaceView.java | 2 +- java/org/gnu/emacs/EmacsView.java | 2 +- java/org/gnu/emacs/EmacsWindow.java | 2 +- .../gnu/emacs/EmacsWindowAttachmentManager.java | 2 +- 27 files changed, 60 insertions(+), 37 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/Makefile.in b/java/Makefile.in index 7ba05f6c9a3..bff021752c7 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -281,9 +281,23 @@ $(APK_NAME): classes.dex emacs.apk-in emacs.keystore $(AM_V_SILENT) $(APKSIGNER) $(SIGN_EMACS_V2) $@ $(AM_V_SILENT) rm -f $@.unaligned *.idsig +# TAGS generation. + +ETAGS = $(top_builddir)/lib-src/etags + +$(ETAGS): FORCE + $(MAKE) -C ../lib-src $(notdir $@) + +tagsfiles = $(JAVA_FILES) $(RESOURCE_FILE) + +.PHONY: tags FORCE +tags: TAGS +TAGS: $(ETAGS) $(tagsfiles) + $(AM_V_GEN) $(ETAGS) $(tagsfiles) + clean: rm -f *.apk emacs.apk-in *.dex *.unaligned *.class *.idsig - rm -rf install-temp $(RESOURCE_FILE) + rm -rf install-temp $(RESOURCE_FILE) TAGS find . -name '*.class' -delete maintainer-clean distclean bootstrap-clean: clean diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 7e09e608984..0ee8c239899 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -108,7 +108,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void detachWindow () { syncFullscreenWith (null); @@ -131,7 +131,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void attachWindow (EmacsWindow child) { Log.d (TAG, "attachWindow: " + child); @@ -161,21 +161,21 @@ public class EmacsActivity extends Activity } @Override - public void + public final void destroy () { finish (); } @Override - public EmacsWindow + public final EmacsWindow getAttachedWindow () { return window; } @Override - public void + public final void onCreate (Bundle savedInstanceState) { FrameLayout.LayoutParams params; @@ -238,7 +238,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onWindowFocusChanged (boolean isFocused) { Log.d (TAG, ("onWindowFocusChanged: " @@ -256,7 +256,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onPause () { isPaused = true; @@ -266,7 +266,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onResume () { isPaused = false; @@ -279,7 +279,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onContextMenuClosed (Menu menu) { Log.d (TAG, "onContextMenuClosed: " + menu); @@ -298,7 +298,7 @@ public class EmacsActivity extends Activity } @SuppressWarnings ("deprecation") - public void + public final void syncFullscreenWith (EmacsWindow emacsWindow) { WindowInsetsController controller; @@ -372,7 +372,7 @@ public class EmacsActivity extends Activity } @Override - public void + public final void onAttachedToWindow () { super.onAttachedToWindow (); diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java index 6a065165eb1..10099721744 100644 --- a/java/org/gnu/emacs/EmacsApplication.java +++ b/java/org/gnu/emacs/EmacsApplication.java @@ -27,7 +27,7 @@ import android.content.Context; import android.app.Application; import android.util.Log; -public class EmacsApplication extends Application +public final class EmacsApplication extends Application { private static final String TAG = "EmacsApplication"; diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index 6b3ae0c6de9..0de292af21a 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -42,7 +42,7 @@ import android.widget.PopupMenu; Android menu, which can be turned into a popup (or other kind of) menu. */ -public class EmacsContextMenu +public final class EmacsContextMenu { private static final String TAG = "EmacsContextMenu"; diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index 1daa2190542..f69b0cde866 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -27,7 +27,7 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Xfermode; -public class EmacsCopyArea +public final class EmacsCopyArea { private static Xfermode overAlu; diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java index 9f9124ce99c..80a5e5f7369 100644 --- a/java/org/gnu/emacs/EmacsDialog.java +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -38,7 +38,7 @@ import android.view.ViewGroup; describes a single alert dialog. Then, `inflate' turns it into AlertDialog. */ -public class EmacsDialog implements DialogInterface.OnDismissListener +public final class EmacsDialog implements DialogInterface.OnDismissListener { private static final String TAG = "EmacsDialog"; diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java index 3c3c7ead3c5..901c3b909e0 100644 --- a/java/org/gnu/emacs/EmacsDocumentsProvider.java +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java @@ -48,7 +48,7 @@ import java.io.IOException; This functionality is only available on Android 19 and later. */ -public class EmacsDocumentsProvider extends DocumentsProvider +public final class EmacsDocumentsProvider extends DocumentsProvider { /* Home directory. This is the directory whose contents are initially returned to requesting applications. */ diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index c6e5123bfca..0b23138a36c 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -29,7 +29,7 @@ import android.graphics.PorterDuffXfermode; import android.graphics.Rect; import android.graphics.Xfermode; -public class EmacsDrawLine +public final class EmacsDrawLine { public static void perform (EmacsDrawable drawable, EmacsGC gc, diff --git a/java/org/gnu/emacs/EmacsDrawPoint.java b/java/org/gnu/emacs/EmacsDrawPoint.java index 3bc7be17961..de8ddf09cc4 100644 --- a/java/org/gnu/emacs/EmacsDrawPoint.java +++ b/java/org/gnu/emacs/EmacsDrawPoint.java @@ -19,7 +19,7 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -public class EmacsDrawPoint +public final class EmacsDrawPoint { public static void perform (EmacsDrawable drawable, diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index 695a8c6ea44..ce5e94e4a76 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -27,7 +27,7 @@ import android.graphics.RectF; import android.util.Log; -public class EmacsDrawRectangle +public final class EmacsDrawRectangle { public static void perform (EmacsDrawable drawable, EmacsGC gc, diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java index 22e2dd0d8a9..d55a0b3aca8 100644 --- a/java/org/gnu/emacs/EmacsFillPolygon.java +++ b/java/org/gnu/emacs/EmacsFillPolygon.java @@ -29,7 +29,7 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; -public class EmacsFillPolygon +public final class EmacsFillPolygon { public static void perform (EmacsDrawable drawable, EmacsGC gc, Point points[]) diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java index aed0a540c8f..4a0478b446f 100644 --- a/java/org/gnu/emacs/EmacsFillRectangle.java +++ b/java/org/gnu/emacs/EmacsFillRectangle.java @@ -26,7 +26,7 @@ import android.graphics.Rect; import android.util.Log; -public class EmacsFillRectangle +public final class EmacsFillRectangle { public static void perform (EmacsDrawable drawable, EmacsGC gc, diff --git a/java/org/gnu/emacs/EmacsGC.java b/java/org/gnu/emacs/EmacsGC.java index bdc27a1ca5b..a7467cb9bd0 100644 --- a/java/org/gnu/emacs/EmacsGC.java +++ b/java/org/gnu/emacs/EmacsGC.java @@ -29,7 +29,7 @@ import android.graphics.Xfermode; /* X like graphics context structures. Keep the enums in synch with androidgui.h! */ -public class EmacsGC extends EmacsHandleObject +public final class EmacsGC extends EmacsHandleObject { public static final int GC_COPY = 0; public static final int GC_XOR = 1; diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 834c2226c82..ed64c368857 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -36,7 +36,7 @@ import android.util.Log; See EmacsEditable for more details. */ -public class EmacsInputConnection extends BaseInputConnection +public final class EmacsInputConnection extends BaseInputConnection { private static final String TAG = "EmacsInputConnection"; private EmacsView view; @@ -243,6 +243,15 @@ public class EmacsInputConnection extends BaseInputConnection return super.sendKeyEvent (key); } + @Override + public boolean + deleteSurroundingTextInCodePoints (int beforeLength, int afterLength) + { + /* This can be implemented the same way as + deleteSurroundingText. */ + return this.deleteSurroundingText (beforeLength, afterLength); + } + /* Override functions which are not implemented. */ diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index f0917a68120..b1205353090 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -25,7 +25,7 @@ import android.content.res.AssetManager; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; -public class EmacsNative +public final class EmacsNative { /* List of native libraries that must be loaded during class initialization. */ diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index 30901edb75f..f365037b311 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -44,7 +44,7 @@ import java.lang.reflect.Method; initializes Emacs. */ @SuppressWarnings ("unchecked") -public class EmacsNoninteractive +public final class EmacsNoninteractive { private static String getLibraryDirectory (Context context) diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index 87ce454a816..fddd5331d2f 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -68,7 +68,7 @@ import java.io.IOException; import java.io.InputStream; import java.io.UnsupportedEncodingException; -public class EmacsOpenActivity extends Activity +public final class EmacsOpenActivity extends Activity implements DialogInterface.OnClickListener { private static final String TAG = "EmacsOpenActivity"; diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java index a83d8f25542..eb011bc5e65 100644 --- a/java/org/gnu/emacs/EmacsPixmap.java +++ b/java/org/gnu/emacs/EmacsPixmap.java @@ -29,7 +29,7 @@ import android.os.Build; /* Drawable backed by bitmap. */ -public class EmacsPixmap extends EmacsHandleObject +public final class EmacsPixmap extends EmacsHandleObject implements EmacsDrawable { /* The depth of the bitmap. This is not actually used, just defined diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java index 85639fe9236..70934fa4bd4 100644 --- a/java/org/gnu/emacs/EmacsPreferencesActivity.java +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java @@ -42,7 +42,7 @@ import android.preference.*; Unfortunately, there is no alternative that looks the same way. */ @SuppressWarnings ("deprecation") -public class EmacsPreferencesActivity extends PreferenceActivity +public final class EmacsPreferencesActivity extends PreferenceActivity { /* Restart Emacs with -Q. Call EmacsThread.exit to kill Emacs now, and tell the system to EmacsActivity with some parameters later. */ diff --git a/java/org/gnu/emacs/EmacsSdk11Clipboard.java b/java/org/gnu/emacs/EmacsSdk11Clipboard.java index ea35a463299..a05184513cd 100644 --- a/java/org/gnu/emacs/EmacsSdk11Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk11Clipboard.java @@ -32,7 +32,7 @@ import java.io.UnsupportedEncodingException; /* This class implements EmacsClipboard for Android 3.0 and later systems. */ -public class EmacsSdk11Clipboard extends EmacsClipboard +public final class EmacsSdk11Clipboard extends EmacsClipboard implements ClipboardManager.OnPrimaryClipChangedListener { private static final String TAG = "EmacsSdk11Clipboard"; diff --git a/java/org/gnu/emacs/EmacsSdk23FontDriver.java b/java/org/gnu/emacs/EmacsSdk23FontDriver.java index 11e128d5769..aaba8dbd166 100644 --- a/java/org/gnu/emacs/EmacsSdk23FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk23FontDriver.java @@ -22,7 +22,7 @@ package org.gnu.emacs; import android.graphics.Paint; import android.graphics.Rect; -public class EmacsSdk23FontDriver extends EmacsSdk7FontDriver +public final class EmacsSdk23FontDriver extends EmacsSdk7FontDriver { private void textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics, diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java index 34e66912562..818a722a908 100644 --- a/java/org/gnu/emacs/EmacsSdk8Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java @@ -31,7 +31,7 @@ import java.io.UnsupportedEncodingException; similarly old systems. */ @SuppressWarnings ("deprecation") -public class EmacsSdk8Clipboard extends EmacsClipboard +public final class EmacsSdk8Clipboard extends EmacsClipboard { private static final String TAG = "EmacsSdk8Clipboard"; private ClipboardManager manager; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 7f4f75b5147..e61d9487375 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -82,7 +82,7 @@ class Holder /* EmacsService is the service that starts the thread running Emacs and handles requests by that Emacs instance. */ -public class EmacsService extends Service +public final class EmacsService extends Service { public static final String TAG = "EmacsService"; public static final int MAX_PENDING_REQUESTS = 256; diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java index 62e927094e4..e0411f7f8b3 100644 --- a/java/org/gnu/emacs/EmacsSurfaceView.java +++ b/java/org/gnu/emacs/EmacsSurfaceView.java @@ -35,7 +35,7 @@ import java.lang.ref.WeakReference; own back buffers, which use too much memory (up to 96 MB for a single frame.) */ -public class EmacsSurfaceView extends View +public final class EmacsSurfaceView extends View { private static final String TAG = "EmacsSurfaceView"; private EmacsView view; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 89f526853b2..d2330494bc7 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -51,7 +51,7 @@ import android.util.Log; It is also a ViewGroup, as it also lays out children. */ -public class EmacsView extends ViewGroup +public final class EmacsView extends ViewGroup { public static final String TAG = "EmacsView"; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 90fc4c44198..007a2a86e68 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -59,7 +59,7 @@ import android.os.Build; Views are also drawables, meaning they can accept drawing requests. */ -public class EmacsWindow extends EmacsHandleObject +public final class EmacsWindow extends EmacsHandleObject implements EmacsDrawable { private static final String TAG = "EmacsWindow"; diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 510300571b8..1548bf28087 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -50,7 +50,7 @@ import android.util.Log; Finally, every time a window is removed, the consumer is destroyed. */ -public class EmacsWindowAttachmentManager +public final class EmacsWindowAttachmentManager { public static EmacsWindowAttachmentManager MANAGER; private final static String TAG = "EmacsWindowAttachmentManager"; -- cgit v1.3 From 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. --- doc/emacs/android.texi | 5 ++--- 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 ++- src/android.c | 28 ++++++++++++++++++++++++++++ src/android.h | 1 + src/androidfns.c | 5 +++++ src/androidselect.c | 12 ++++++++++++ src/androidterm.c | 7 ++----- 12 files changed, 83 insertions(+), 18 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 65ebdfa9ab1..9852755d649 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -438,9 +438,8 @@ that window. @cindex windowing limitations, android @cindex frame parameters, android -Due to the unusual nature of the Android windowing environment, Emacs -only supports a limited subset of GUI features. Here is a list of -known limitations, and features which are not implemented: +Emacs only supports a limited subset of GUI features on Android; the +limitations are as follows: @itemize @bullet @item 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; diff --git a/src/android.c b/src/android.c index 3bccaab041a..daeb7ab70f6 100644 --- a/src/android.c +++ b/src/android.c @@ -112,6 +112,7 @@ struct android_emacs_service jmethodID open_content_uri; jmethodID check_content_uri; jmethodID query_battery; + jmethodID display_toast; }; struct android_emacs_pixmap @@ -2124,6 +2125,8 @@ android_init_emacs_service (void) FIND_METHOD (check_content_uri, "checkContentUri", "([BZZ)Z"); FIND_METHOD (query_battery, "queryBattery", "()[J"); + FIND_METHOD (display_toast, "displayToast", + "(Ljava/lang/String;)V"); #undef FIND_METHOD } @@ -5696,6 +5699,31 @@ android_query_battery (struct android_battery_state *status) return 0; } +/* Display a small momentary notification on screen containing + TEXT, which must be in the modified UTF encoding used by the + JVM. */ + +void +android_display_toast (const char *text) +{ + jstring string; + + /* Make the string. */ + string = (*android_java_env)->NewStringUTF (android_java_env, + text); + android_exception_check (); + + /* Display the toast. */ + (*android_java_env)->CallVoidMethod (android_java_env, + emacs_service, + service_class.display_toast, + string); + android_exception_check_1 (string); + + /* Delete the local reference to the string. */ + ANDROID_DELETE_LOCAL_REF (string); +} + /* Whether or not a query is currently being made. */ diff --git a/src/android.h b/src/android.h index 806be4f9954..95206b77979 100644 --- a/src/android.h +++ b/src/android.h @@ -140,6 +140,7 @@ struct android_battery_state extern Lisp_Object android_browse_url (Lisp_Object); extern int android_query_battery (struct android_battery_state *); +extern void android_display_toast (const char *); diff --git a/src/androidfns.c b/src/androidfns.c index dc68cef8a02..4837b00a21e 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -2357,6 +2357,11 @@ there is no mouse. */) (void) { #ifndef ANDROID_STUBIFY + /* If no display connection is present, just return nil. */ + + if (!android_init_gui) + return Qnil; + return android_detect_mouse () ? Qt : Qnil; #else return Qnil; diff --git a/src/androidselect.c b/src/androidselect.c index 3947ab99166..f54a6d6f52c 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -110,6 +110,9 @@ determined. */) { jint rc; + if (!android_init_gui) + error ("Accessing clipboard without display connection"); + block_input (); rc = (*android_java_env)->CallIntMethod (android_java_env, clipboard, @@ -133,6 +136,9 @@ DEFUN ("android-set-clipboard", Fandroid_set_clipboard, { jarray bytes; + if (!android_init_gui) + error ("Accessing clipboard without display connection"); + CHECK_STRING (string); string = ENCODE_UTF_8 (string); @@ -167,6 +173,9 @@ Alternatively, return nil if the clipboard is empty. */) size_t length; jbyte *data; + if (!android_init_gui) + error ("No Android display connection!"); + method = clipboard_class.get_clipboard; bytes = (*android_java_env)->CallObjectMethod (android_java_env, @@ -217,6 +226,9 @@ URL with a scheme specified. Signal an error upon failure. */) { Lisp_Object value; + if (!android_init_gui) + error ("No Android display connection!"); + CHECK_STRING (url); value = android_browse_url (url); diff --git a/src/androidterm.c b/src/androidterm.c index 8a67d128348..814af87819b 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -866,9 +866,8 @@ handle_one_android_event (struct android_display_info *dpyinfo, if (event->xaction.action == 0) { - /* Action 0 either means to destroy a frame or to create a - new frame, depending on whether or not - event->xaction.window exists. */ + /* Action 0 either means that a window has been destroyed + and its associated frame should be as well. */ if (event->xaction.window) { @@ -878,8 +877,6 @@ handle_one_android_event (struct android_display_info *dpyinfo, inev.ie.kind = DELETE_WINDOW_EVENT; XSETFRAME (inev.ie.frame_or_window, f); } - else - ((void) 0) /* A new frame must be created. */; } case ANDROID_ENTER_NOTIFY: -- cgit v1.3 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 ++ lisp/files.el | 9 +++++++-- src/android.c | 10 ++++++++++ 3 files changed, 19 insertions(+), 2 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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); diff --git a/lisp/files.el b/lisp/files.el index 387a3b5dc66..35f31711065 100644 --- a/lisp/files.el +++ b/lisp/files.el @@ -5726,9 +5726,14 @@ Before and after saving the buffer, this function runs (run-hook-with-args-until-success 'write-file-functions) ;; If a hook returned t, file is already "written". ;; Otherwise, write it the usual way now. - (let ((dir (file-name-directory + (let ((file (buffer-file-name)) + (dir (file-name-directory (expand-file-name buffer-file-name)))) - (unless (file-exists-p dir) + ;; Some systems have directories (like /content on + ;; Android) in which files can exist without a + ;; corresponding parent directory. + (unless (or (file-exists-p file) + (file-exists-p dir)) (if (y-or-n-p (format-message "Directory `%s' does not exist; create? " dir)) diff --git a/src/android.c b/src/android.c index 1e91abffa6f..656971e154f 100644 --- a/src/android.c +++ b/src/android.c @@ -1715,9 +1715,19 @@ android_open (const char *filename, int oflag, int mode) return -1; } + /* If fd is -1, just assume that the file does not exist, + and return -1 with errno set to ENOENT. */ + + if (fd == -1) + { + errno = ENOENT; + goto skip; + } + if (mode & O_CLOEXEC) android_close_on_exec (fd); + skip: ANDROID_DELETE_LOCAL_REF (string); return fd; } -- cgit v1.3 From 1cae464859341bfd5fc7d7ed4b503ef959af81dd Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 5 Mar 2023 19:58:28 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsActivity.java (onCreate): * java/org/gnu/emacs/EmacsContextMenu.java: * java/org/gnu/emacs/EmacsDocumentsProvider.java (getMimeType): * java/org/gnu/emacs/EmacsDrawLine.java (perform): * java/org/gnu/emacs/EmacsDrawRectangle.java (perform): * java/org/gnu/emacs/EmacsFillPolygon.java: * java/org/gnu/emacs/EmacsFontDriver.java: * java/org/gnu/emacs/EmacsHandleObject.java: * java/org/gnu/emacs/EmacsInputConnection.java: * java/org/gnu/emacs/EmacsMultitaskActivity.java (EmacsMultitaskActivity): * java/org/gnu/emacs/EmacsNative.java: * java/org/gnu/emacs/EmacsNoninteractive.java (EmacsNoninteractive, main): * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity) (startEmacsClient): * java/org/gnu/emacs/EmacsSdk7FontDriver.java: * java/org/gnu/emacs/EmacsSdk8Clipboard.java: * java/org/gnu/emacs/EmacsService.java (EmacsService, onCreate): * java/org/gnu/emacs/EmacsView.java (EmacsView, onLayout): * java/org/gnu/emacs/EmacsWindow.java (EmacsWindow): * java/org/gnu/emacs/EmacsWindowAttachmentManager.java (EmacsWindowAttachmentManager): Remove redundant includes. Reorganize some functions around, remove duplicate `getLibDir' functions, and remove unused local variables. --- java/org/gnu/emacs/EmacsActivity.java | 13 ++++-- java/org/gnu/emacs/EmacsContextMenu.java | 3 -- java/org/gnu/emacs/EmacsDocumentsProvider.java | 4 +- java/org/gnu/emacs/EmacsDrawLine.java | 5 --- java/org/gnu/emacs/EmacsDrawRectangle.java | 1 - java/org/gnu/emacs/EmacsFillPolygon.java | 1 - java/org/gnu/emacs/EmacsFontDriver.java | 2 - java/org/gnu/emacs/EmacsHandleObject.java | 3 -- java/org/gnu/emacs/EmacsInputConnection.java | 9 +--- java/org/gnu/emacs/EmacsMultitaskActivity.java | 6 ++- java/org/gnu/emacs/EmacsNative.java | 2 - java/org/gnu/emacs/EmacsNoninteractive.java | 18 +------- java/org/gnu/emacs/EmacsOpenActivity.java | 20 +-------- java/org/gnu/emacs/EmacsSdk7FontDriver.java | 3 -- java/org/gnu/emacs/EmacsSdk8Clipboard.java | 4 +- java/org/gnu/emacs/EmacsService.java | 49 ++++++++-------------- java/org/gnu/emacs/EmacsView.java | 7 ++-- java/org/gnu/emacs/EmacsWindow.java | 15 +++---- .../gnu/emacs/EmacsWindowAttachmentManager.java | 5 +-- 19 files changed, 53 insertions(+), 117 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java index 13002d5d0e5..692d8a14e22 100644 --- a/java/org/gnu/emacs/EmacsActivity.java +++ b/java/org/gnu/emacs/EmacsActivity.java @@ -24,18 +24,22 @@ import java.util.List; import java.util.ArrayList; import android.app.Activity; + import android.content.Context; import android.content.Intent; + import android.os.Build; import android.os.Bundle; + import android.util.Log; + import android.view.Menu; import android.view.View; import android.view.ViewTreeObserver; import android.view.Window; import android.view.WindowInsets; import android.view.WindowInsetsController; -import android.widget.FrameLayout.LayoutParams; + import android.widget.FrameLayout; public class EmacsActivity extends Activity @@ -187,6 +191,7 @@ public class EmacsActivity extends Activity Intent intent; View decorView; ViewTreeObserver observer; + int matchParent; /* See if Emacs should be started with -Q. */ intent = getIntent (); @@ -194,8 +199,10 @@ public class EmacsActivity extends Activity = intent.getBooleanExtra ("org.gnu.emacs.START_DASH_Q", false); - params = new FrameLayout.LayoutParams (LayoutParams.MATCH_PARENT, - LayoutParams.MATCH_PARENT); + matchParent = FrameLayout.LayoutParams.MATCH_PARENT; + params + = new FrameLayout.LayoutParams (matchParent, + matchParent); /* Make the frame layout. */ layout = new FrameLayout (this); diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java index d1a624e68d9..dec5e148a8e 100644 --- a/java/org/gnu/emacs/EmacsContextMenu.java +++ b/java/org/gnu/emacs/EmacsContextMenu.java @@ -25,7 +25,6 @@ import java.util.ArrayList; import android.content.Context; import android.content.Intent; -import android.os.Bundle; import android.os.Build; import android.view.Menu; @@ -35,8 +34,6 @@ import android.view.SubMenu; import android.util.Log; -import android.widget.PopupMenu; - /* Context menu implementation. This object is built from JNI and describes a menu hiearchy. Then, `inflate' can turn it into an Android menu, which can be turned into a popup (or other kind of) diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java index 901c3b909e0..f70da6040ff 100644 --- a/java/org/gnu/emacs/EmacsDocumentsProvider.java +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java @@ -160,6 +160,7 @@ public final class EmacsDocumentsProvider extends DocumentsProvider { String name, extension, mime; int extensionSeparator; + MimeTypeMap singleton; if (file.isDirectory ()) return Document.MIME_TYPE_DIR; @@ -170,8 +171,9 @@ public final class EmacsDocumentsProvider extends DocumentsProvider if (extensionSeparator > 0) { + singleton = MimeTypeMap.getSingleton (); extension = name.substring (extensionSeparator + 1); - mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension (extension); + mime = singleton.getMimeTypeFromExtension (extension); if (mime != null) return mime; diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java index 0b23138a36c..92e03c48e26 100644 --- a/java/org/gnu/emacs/EmacsDrawLine.java +++ b/java/org/gnu/emacs/EmacsDrawLine.java @@ -21,13 +21,9 @@ package org.gnu.emacs; import java.lang.Math; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; -import android.graphics.PorterDuff.Mode; -import android.graphics.PorterDuffXfermode; import android.graphics.Rect; -import android.graphics.Xfermode; public final class EmacsDrawLine { @@ -38,7 +34,6 @@ public final class EmacsDrawLine Rect rect; Canvas canvas; Paint paint; - int i; /* TODO implement stippling. */ if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index ce5e94e4a76..3bd5779c54e 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -33,7 +33,6 @@ public final class EmacsDrawRectangle perform (EmacsDrawable drawable, EmacsGC gc, int x, int y, int width, int height) { - int i; Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java index d55a0b3aca8..ea8324c543c 100644 --- a/java/org/gnu/emacs/EmacsFillPolygon.java +++ b/java/org/gnu/emacs/EmacsFillPolygon.java @@ -21,7 +21,6 @@ package org.gnu.emacs; import java.lang.Math; -import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Paint; import android.graphics.Path; diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java index e142a3121d3..ff52899a897 100644 --- a/java/org/gnu/emacs/EmacsFontDriver.java +++ b/java/org/gnu/emacs/EmacsFontDriver.java @@ -19,8 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.util.List; - import android.os.Build; /* This code is mostly unused. See sfntfont-android.c for the code diff --git a/java/org/gnu/emacs/EmacsHandleObject.java b/java/org/gnu/emacs/EmacsHandleObject.java index a57a3bbdfa9..5b889895337 100644 --- a/java/org/gnu/emacs/EmacsHandleObject.java +++ b/java/org/gnu/emacs/EmacsHandleObject.java @@ -19,9 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.util.List; -import java.util.ArrayList; -import java.lang.Object; import java.lang.IllegalStateException; /* This defines something that is a so-called ``handle''. Handles diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index ed64c368857..7b40fcff99e 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -23,18 +23,13 @@ import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; -import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.SurroundingText; import android.view.inputmethod.TextSnapshot; import android.view.KeyEvent; -import android.text.Editable; - import android.util.Log; -/* Android input methods, take number six. - - See EmacsEditable for more details. */ +/* Android input methods, take number six. See textconv.c for more + details; this is more-or-less a thin wrapper around that file. */ public final class EmacsInputConnection extends BaseInputConnection { diff --git a/java/org/gnu/emacs/EmacsMultitaskActivity.java b/java/org/gnu/emacs/EmacsMultitaskActivity.java index dbdc99a3559..b1c48f03fba 100644 --- a/java/org/gnu/emacs/EmacsMultitaskActivity.java +++ b/java/org/gnu/emacs/EmacsMultitaskActivity.java @@ -19,7 +19,11 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -public class EmacsMultitaskActivity extends EmacsActivity +/* This class only exists because EmacsActivity is already defined as + an activity, and the system wants a new class in order to define a + new activity. */ + +public final class EmacsMultitaskActivity extends EmacsActivity { } diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index b1205353090..38370d60727 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -19,8 +19,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -import java.lang.System; - import android.content.res.AssetManager; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index f365037b311..aaba74d877c 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -46,21 +46,6 @@ import java.lang.reflect.Method; @SuppressWarnings ("unchecked") public final class EmacsNoninteractive { - private static String - getLibraryDirectory (Context context) - { - int apiLevel; - - apiLevel = Build.VERSION.SDK_INT; - - if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) - return context.getApplicationInfo().nativeLibraryDir; - else if (apiLevel >= Build.VERSION_CODES.DONUT) - return context.getApplicationInfo().dataDir + "/lib"; - - return "/data/data/" + context.getPackageName() + "/lib"; - } - public static void main (String[] args) { @@ -188,7 +173,7 @@ public final class EmacsNoninteractive /* Now configure Emacs. The class path should already be set. */ filesDir = context.getFilesDir ().getCanonicalPath (); - libDir = getLibraryDirectory (context); + libDir = EmacsService.getLibraryDirectory (context); cacheDir = context.getCacheDir ().getCanonicalPath (); } catch (Exception e) @@ -198,6 +183,7 @@ public final class EmacsNoninteractive System.err.println ("and that Emacs needs adjustments in order to"); System.err.println ("obtain required system internal resources."); System.err.println ("Please report this bug to bug-gnu-emacs@gnu.org."); + e.printStackTrace (); System.exit (1); } diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java index ac643ae8a13..51335ddb2dd 100644 --- a/java/org/gnu/emacs/EmacsOpenActivity.java +++ b/java/org/gnu/emacs/EmacsOpenActivity.java @@ -46,7 +46,6 @@ package org.gnu.emacs; import android.app.AlertDialog; import android.app.Activity; -import android.content.Context; import android.content.ContentResolver; import android.content.DialogInterface; import android.content.Intent; @@ -301,23 +300,6 @@ public final class EmacsOpenActivity extends Activity }); } - public String - getLibraryDirectory () - { - int apiLevel; - Context context; - - context = getApplicationContext (); - apiLevel = Build.VERSION.SDK_INT; - - if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) - return context.getApplicationInfo().nativeLibraryDir; - else if (apiLevel >= Build.VERSION_CODES.DONUT) - return context.getApplicationInfo().dataDir + "/lib"; - - return "/data/data/" + context.getPackageName() + "/lib"; - } - public void startEmacsClient (String fileName) { @@ -327,7 +309,7 @@ public final class EmacsOpenActivity extends Activity EmacsClientThread thread; File file; - libDir = getLibraryDirectory (); + libDir = EmacsService.getLibraryDirectory (this); builder = new ProcessBuilder (libDir + "/libemacsclient.so", fileName, "--reuse-frame", "--timeout=10", "--no-wait"); diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java index ae91c299de8..6df102f18a2 100644 --- a/java/org/gnu/emacs/EmacsSdk7FontDriver.java +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java @@ -20,7 +20,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import java.io.File; -import java.io.IOException; import java.util.LinkedList; import java.util.List; @@ -32,8 +31,6 @@ import android.graphics.Canvas; import android.util.Log; -import android.os.Build; - public class EmacsSdk7FontDriver extends EmacsFontDriver { private static final String TOFU_STRING = "\uDB3F\uDFFD"; diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java index 818a722a908..5a40128b0ac 100644 --- a/java/org/gnu/emacs/EmacsSdk8Clipboard.java +++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java @@ -19,7 +19,9 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; -/* Importing the entire package avoids the deprecation warning. */ +/* Importing the entire package instead of just the legacy + ClipboardManager class avoids the deprecation warning. */ + import android.text.*; import android.content.Context; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index d9cb25f3e9c..d05ebce75dc 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -24,22 +24,15 @@ import java.io.IOException; import java.io.UnsupportedEncodingException; import java.util.List; -import java.util.ArrayList; -import android.graphics.Canvas; -import android.graphics.Bitmap; import android.graphics.Point; -import android.view.View; import android.view.InputDevice; import android.view.KeyEvent; -import android.annotation.TargetApi; - import android.app.Notification; import android.app.NotificationManager; import android.app.NotificationChannel; -import android.app.PendingIntent; import android.app.Service; import android.content.ClipboardManager; @@ -51,9 +44,6 @@ import android.content.pm.PackageManager.ApplicationInfoFlags; import android.content.pm.PackageManager; import android.content.res.AssetManager; -import android.database.Cursor; -import android.database.MatrixCursor; - import android.hardware.input.InputManager; import android.net.Uri; @@ -68,9 +58,6 @@ import android.os.Vibrator; import android.os.VibratorManager; import android.os.VibrationEffect; -import android.provider.DocumentsContract; -import android.provider.DocumentsContract.Document; - import android.util.Log; import android.util.DisplayMetrics; @@ -87,7 +74,6 @@ class Holder public final class EmacsService extends Service { public static final String TAG = "EmacsService"; - public static final int MAX_PENDING_REQUESTS = 256; public static volatile EmacsService SERVICE; public static boolean needDashQ; @@ -107,6 +93,22 @@ public final class EmacsService extends Service information. */ public static final boolean DEBUG_IC = false; + /* Return the directory leading to the directory in which native + library files are stored on behalf of CONTEXT. */ + + public static String + getLibraryDirectory (Context context) + { + int apiLevel; + + apiLevel = Build.VERSION.SDK_INT; + + if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) + return context.getApplicationInfo ().nativeLibraryDir; + + return context.getApplicationInfo ().dataDir + "/lib"; + } + @Override public int onStartCommand (Intent intent, int flags, int startId) @@ -178,23 +180,6 @@ public final class EmacsService extends Service } } - private String - getLibraryDirectory () - { - int apiLevel; - Context context; - - context = getApplicationContext (); - apiLevel = Build.VERSION.SDK_INT; - - if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) - return context.getApplicationInfo().nativeLibraryDir; - else if (apiLevel >= Build.VERSION_CODES.DONUT) - return context.getApplicationInfo().dataDir + "/lib"; - - return "/data/data/" + context.getPackageName() + "/lib"; - } - @Override public void onCreate () @@ -219,7 +204,7 @@ public final class EmacsService extends Service /* Configure Emacs with the asset manager and other necessary parameters. */ filesDir = app_context.getFilesDir ().getCanonicalPath (); - libDir = getLibraryDirectory (); + libDir = getLibraryDirectory (this); cacheDir = app_context.getCacheDir ().getCanonicalPath (); /* Now provide this application's apk file, so a recursive diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 617836d8811..aefc79c4fb7 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -20,7 +20,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import android.content.Context; -import android.content.res.ColorStateList; import android.text.InputType; @@ -116,6 +115,7 @@ public final class EmacsView extends ViewGroup super (EmacsService.SERVICE); Object tem; + Context context; this.window = window; this.damageRegion = new Region (); @@ -133,7 +133,8 @@ public final class EmacsView extends ViewGroup setDefaultFocusHighlightEnabled (false); /* Obtain the input method manager. */ - tem = getContext ().getSystemService (Context.INPUT_METHOD_SERVICE); + context = getContext (); + tem = context.getSystemService (Context.INPUT_METHOD_SERVICE); imManager = (InputMethodManager) tem; } @@ -281,7 +282,6 @@ public final class EmacsView extends ViewGroup int count, i; View child; Rect windowRect; - int wantedWidth, wantedHeight; count = getChildCount (); @@ -422,7 +422,6 @@ public final class EmacsView extends ViewGroup } } - /* The following two functions must not be called if the view has no parent, or is parented to an activity. */ diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index ea4cf48090d..ffc7476f010 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -31,19 +31,16 @@ import android.content.Context; import android.graphics.Rect; import android.graphics.Canvas; import android.graphics.Bitmap; -import android.graphics.Point; import android.graphics.PixelFormat; import android.view.View; import android.view.ViewManager; -import android.view.ViewGroup; import android.view.Gravity; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.InputDevice; import android.view.WindowManager; -import android.content.Intent; import android.util.Log; import android.os.Build; @@ -87,7 +84,7 @@ public final class EmacsWindow extends EmacsHandleObject public EmacsWindow parent; /* List of all children in stacking order. This must be kept - consistent! */ + consistent with their Z order! */ public ArrayList children; /* Map between pointer identifiers and last known position. Used to @@ -105,9 +102,8 @@ public final class EmacsWindow extends EmacsHandleObject last button press or release event. */ public int lastButtonState, lastModifiers; - /* Whether or not the window is mapped, and whether or not it is - deiconified. */ - private boolean isMapped, isIconified; + /* Whether or not the window is mapped. */ + private boolean isMapped; /* Whether or not to ask for focus upon being mapped, and whether or not the window should be focusable. */ @@ -122,7 +118,8 @@ public final class EmacsWindow extends EmacsHandleObject private WindowManager windowManager; /* The time of the last KEYCODE_VOLUME_DOWN release. This is used - to quit Emacs. */ + to quit Emacs upon two rapid clicks of the volume down + button. */ private long lastVolumeButtonRelease; /* Linked list of character strings which were recently sent as @@ -1103,14 +1100,12 @@ public final class EmacsWindow extends EmacsHandleObject public void noticeIconified () { - isIconified = true; EmacsNative.sendIconified (this.handle); } public void noticeDeiconified () { - isIconified = false; EmacsNative.sendDeiconified (this.handle); } diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java index 1548bf28087..30f29250970 100644 --- a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java @@ -20,7 +20,6 @@ along with GNU Emacs. If not, see . */ package org.gnu.emacs; import java.util.ArrayList; -import java.util.LinkedList; import java.util.List; import android.content.Intent; @@ -74,8 +73,8 @@ public final class EmacsWindowAttachmentManager public EmacsWindowAttachmentManager () { - consumers = new LinkedList (); - windows = new LinkedList (); + consumers = new ArrayList (); + windows = new ArrayList (); } public void -- cgit v1.3 From 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 +++++++ src/android.c | 11 ----------- src/androidfns.c | 12 +++++++----- src/androidgui.h | 2 -- src/androidterm.c | 10 ++++++++++ 7 files changed, 29 insertions(+), 39 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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 diff --git a/src/android.c b/src/android.c index 656971e154f..9fc4143602c 100644 --- a/src/android.c +++ b/src/android.c @@ -104,7 +104,6 @@ struct android_emacs_service jmethodID get_screen_height; jmethodID detect_mouse; jmethodID name_keysym; - jmethodID sync; jmethodID browse_url; jmethodID restart_emacs; jmethodID update_ic; @@ -2122,7 +2121,6 @@ android_init_emacs_service (void) FIND_METHOD (get_screen_height, "getScreenHeight", "(Z)I"); FIND_METHOD (detect_mouse, "detectMouse", "()Z"); FIND_METHOD (name_keysym, "nameKeysym", "(I)Ljava/lang/String;"); - FIND_METHOD (sync, "sync", "()V"); FIND_METHOD (browse_url, "browseUrl", "(Ljava/lang/String;)" "Ljava/lang/String;"); FIND_METHOD (restart_emacs, "restartEmacs", "()V"); @@ -4514,15 +4512,6 @@ android_translate_coordinates (android_window src, int x, ANDROID_DELETE_LOCAL_REF (coordinates); } -void -android_sync (void) -{ - (*android_java_env)->CallVoidMethod (android_java_env, - emacs_service, - service_class.sync); - android_exception_check (); -} - int android_wc_lookup_string (android_key_pressed_event *event, wchar_t *buffer_return, int wchars_buffer, diff --git a/src/androidfns.c b/src/androidfns.c index 4837b00a21e..5a23e8bd196 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -1883,9 +1883,10 @@ android_create_tip_frame (struct android_display_info *dpyinfo, unsigned long mask; block_input (); - mask = ANDROID_CW_OVERRIDE_REDIRECT; + mask = ANDROID_CW_OVERRIDE_REDIRECT | ANDROID_CW_BACK_PIXEL; attrs.override_redirect = true; + attrs.background_pixel = FRAME_BACKGROUND_PIXEL (f); tip_window = FRAME_ANDROID_WINDOW (f) = android_create_window (FRAME_DISPLAY_INFO (f)->root_window, @@ -2314,10 +2315,6 @@ DEFUN ("x-show-tip", Fx_show_tip, Sx_show_tip, 1, 6, 0, android_map_raised (FRAME_ANDROID_WINDOW (tip_f)); unblock_input (); - /* Synchronize with the UI thread. This is required to prevent ugly - black splotches. */ - android_sync (); - /* Garbage the tip frame too. */ SET_FRAME_GARBAGED (tip_f); @@ -2328,6 +2325,11 @@ DEFUN ("x-show-tip", Fx_show_tip, Sx_show_tip, 1, 6, 0, unbind_to (count_1, Qnil); windows_or_buffers_changed = old_windows_or_buffers_changed; + /* MapNotify events are not sent on Android, so make the frame + visible. */ + + SET_FRAME_VISIBLE (tip_f, true); + start_timer: /* Let the tip disappear after timeout seconds. */ tip_timer = call3 (Qrun_at_time, timeout, Qnil, diff --git a/src/androidgui.h b/src/androidgui.h index 84419457a8a..6514c78d289 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -606,8 +606,6 @@ extern void android_move_resize_window (android_window, int, int, extern void android_map_raised (android_window); extern void android_translate_coordinates (android_window, int, int, int *, int *); -extern void android_sync (void); - extern int android_wc_lookup_string (android_key_pressed_event *, wchar_t *, int, int *, enum android_lookup_status *); diff --git a/src/androidterm.c b/src/androidterm.c index 814af87819b..a6709ac8169 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -676,6 +676,16 @@ handle_one_android_event (struct android_display_info *dpyinfo, if (!f) goto OTHER; + if (FRAME_TOOLTIP_P (f)) + { + if (FRAME_PIXEL_HEIGHT (f) != configureEvent.xconfigure.height + || FRAME_PIXEL_WIDTH (f) != configureEvent.xconfigure.width) + SET_FRAME_GARBAGED (f); + + FRAME_PIXEL_HEIGHT (f) = configureEvent.xconfigure.height; + FRAME_PIXEL_WIDTH (f) = configureEvent.xconfigure.width; + } + int width = configureEvent.xconfigure.width; int height = configureEvent.xconfigure.height; -- cgit v1.3 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. --- doc/emacs/android.texi | 7 +++ java/org/gnu/emacs/EmacsService.java | 12 ++++ java/org/gnu/emacs/EmacsView.java | 1 + src/android.c | 44 ++++++++++++-- src/androidgui.h | 2 + src/androidterm.c | 111 ++++++++++++++++++++++++++++++++--- src/androidterm.h | 10 ++++ 7 files changed, 175 insertions(+), 12 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index d49e0754b0a..8e98b92314a 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -205,6 +205,13 @@ other directories are not found at any fixed location, although the app data directory is typically symlinked to @file{/data/data/org.gnu.emacs}. +@cindex temp~unlinked.NNNN files, Android + On Android devices running very old (2.6.29) versions of the Linux +kernel, Emacs needs to create files named starting with +@file{temp~unlinked} in the the temporary file directory in order to +read from asset files. Do not create files with such names yourself, +or they may be overwritten or removed. + @cindex file system limitations, Android 11 On Android 11 and later, the Android system restricts applications from accessing files in the @file{/sdcard} directory using 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; diff --git a/src/android.c b/src/android.c index 11b0fa5e0f3..e620a041348 100644 --- a/src/android.c +++ b/src/android.c @@ -112,6 +112,7 @@ struct android_emacs_service jmethodID check_content_uri; jmethodID query_battery; jmethodID display_toast; + jmethodID update_extracted_text; }; struct android_emacs_pixmap @@ -1236,13 +1237,12 @@ android_hack_asset_fd_fallback (AAsset *asset) Creating an ashmem file descriptor and reading from it doesn't work on these old Android versions. */ - snprintf (filename, PATH_MAX, "%s/%s.%d", - android_cache_dir, "temp-unlinked", - getpid ()); + snprintf (filename, PATH_MAX, "%s/temp~unlinked.%d", + android_cache_dir, getpid ()); fd = open (filename, O_CREAT | O_RDWR | O_TRUNC, S_IRUSR | S_IWUSR); - if (fd < 1) + if (fd < 0) return -1; if (unlink (filename)) @@ -2135,6 +2135,9 @@ android_init_emacs_service (void) FIND_METHOD (query_battery, "queryBattery", "()[J"); FIND_METHOD (display_toast, "displayToast", "(Ljava/lang/String;)V"); + FIND_METHOD (update_extracted_text, "updateExtractedText", + "(Lorg/gnu/emacs/EmacsWindow;" + "Landroid/view/inputmethod/ExtractedText;I)V"); #undef FIND_METHOD } @@ -5991,6 +5994,37 @@ android_reset_ic (android_window window, enum android_ic_mode mode) android_exception_check (); } +/* Make updates to extracted text known to the input method on + WINDOW. TEXT should be a local reference to the new + extracted text. TOKEN should be the token specified by the + input method. */ + +void +android_update_extracted_text (android_window window, void *text, + int token) +{ + jobject object; + jmethodID method; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + method = service_class.update_extracted_text; + + (*android_java_env)->CallNonvirtualVoidMethod (android_java_env, + emacs_service, + service_class.class, + method, object, + /* N.B. that + text is not + jobject, + because that + type is not + available in + androidgui.h. */ + (jobject) text, + (jint) token); + android_exception_check_1 (text); +} + /* Window decoration management functions. */ @@ -6083,7 +6117,7 @@ android_open_asset (const char *filename, int oflag, mode_t mode) get a regular file descriptor. */ fd.fd = android_open (filename, oflag, mode); - if (fd.fd < 1) + if (fd.fd < 0) return fd; /* Set fd.asset to NULL, signifying that it is a file diff --git a/src/androidgui.h b/src/androidgui.h index e1c80a71a59..afcaed98cae 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -613,6 +613,8 @@ extern int android_wc_lookup_string (android_key_pressed_event *, extern void android_update_ic (android_window, ptrdiff_t, ptrdiff_t, ptrdiff_t, ptrdiff_t); extern void android_reset_ic (android_window, enum android_ic_mode); +extern void android_update_extracted_text (android_window, void *, + int); extern int android_set_fullscreen (android_window, bool); #endif diff --git a/src/androidterm.c b/src/androidterm.c index 0cc2b35099c..f4a535292f2 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -4968,6 +4968,10 @@ NATIVE_NAME (performEditorAction) (JNIEnv *env, jobject object, android_write_event (&event); } + + +/* Text extraction. */ + struct android_get_extracted_text_context { /* The parameters of the request. */ @@ -4976,6 +4980,9 @@ struct android_get_extracted_text_context /* Token for the request. */ int token; + /* Flags associated with the request. */ + int flags; + /* The returned text, or NULL. */ char *text; @@ -5011,6 +5018,14 @@ android_get_extracted_text (void *data) = get_extracted_text (f, min (request->hint_max_chars, 600), &request->start, &request->offset, &request->length, &request->bytes); + + /* See if request->flags & GET_EXTRACTED_TEXT_MONITOR. If so, then + the input method has asked to monitor changes to the extracted + text until the next IM context reset. */ + + FRAME_ANDROID_OUTPUT (f)->extracted_text_flags = request->flags; + FRAME_ANDROID_OUTPUT (f)->extracted_text_token = request->token; + FRAME_ANDROID_OUTPUT (f)->extracted_text_hint = request->hint_max_chars; } /* Structure describing the `ExtractedTextRequest' class. @@ -5038,6 +5053,51 @@ struct android_extracted_text_class jfieldID text; }; +/* Fields and methods associated with the `ExtractedTextRequest' + class. */ +struct android_extracted_text_request_class request_class; + +/* Fields and methods associated with the `ExtractedText' class. */ +struct android_extracted_text_class text_class; + +/* Return an ExtractedText object corresponding to the extracted text + TEXT. START is a character position describing the offset of the + first character in TEXT. OFFSET is the offset of point relative to + START. + + Assume that request_class and text_class have already been + initialized. + + Value is NULL if an error occurs; the exception is not cleared, + else a local reference to the ExtractedText object. */ + +static jobject +android_build_extracted_text (jstring text, ptrdiff_t start, + ptrdiff_t offset) +{ + JNIEnv *env; + jobject object; + + env = android_java_env; + + /* Create an ExtractedText object containing this information. */ + object = (*env)->NewObject (env, text_class.class, + text_class.constructor); + if (!object) + return NULL; + + (*env)->SetIntField (env, object, text_class.partial_start_offset, -1); + (*env)->SetIntField (env, object, text_class.partial_end_offset, -1); + (*env)->SetIntField (env, object, text_class.selection_start, + min (offset, TYPE_MAXIMUM (jint))); + (*env)->SetIntField (env, object, text_class.selection_end, + min (offset, TYPE_MAXIMUM (jint))); + (*env)->SetIntField (env, object, text_class.start_offset, + min (start, TYPE_MAXIMUM (jint))); + (*env)->SetObjectField (env, object, text_class.text, text); + return object; +} + JNIEXPORT jobject JNICALL NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, jshort window, jobject request, @@ -5046,14 +5106,10 @@ NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, JNI_STACK_ALIGNMENT_PROLOGUE; struct android_get_extracted_text_context context; - static struct android_extracted_text_request_class request_class; - static struct android_extracted_text_class text_class; jstring string; jclass class; jobject object; - /* TODO: report changes to extracted text. */ - /* Initialize both classes if necessary. */ if (!request_class.initialized) @@ -5106,6 +5162,7 @@ NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, = (*env)->GetIntField (env, request, request_class.hint_max_chars); context.token = (*env)->GetIntField (env, request, request_class.token); + context.flags = flags; context.text = NULL; context.window = window; @@ -5126,8 +5183,8 @@ NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, return NULL; /* Create an ExtractedText object containing this information. */ - object = (*android_java_env)->NewObject (env, text_class.class, - text_class.constructor); + object = (*env)->NewObject (env, text_class.class, + text_class.constructor); if (!object) return NULL; @@ -5143,6 +5200,8 @@ NATIVE_NAME (getExtractedText) (JNIEnv *env, jobject ignored_object, return object; } + + JNIEXPORT jstring JNICALL NATIVE_NAME (getSelectedText) (JNIEnv *env, jobject object, jshort window) @@ -5210,8 +5269,12 @@ NATIVE_NAME (requestSelectionUpdate) (JNIEnv *env, jobject object, static void android_update_selection (struct frame *f, struct window *w) { - ptrdiff_t start, end, point, mark; + ptrdiff_t start, end, point, mark, offset, length, bytes; struct buffer *b; + int hint, token; + char *text; + jobject extracted; + jstring string; if (MARKERP (f->conversion.compose_region_start)) { @@ -5246,6 +5309,36 @@ android_update_selection (struct frame *f, struct window *w) the selection is less than or equal to the end. */ android_update_ic (FRAME_ANDROID_WINDOW (f), min (point, mark), max (point, mark), start, end); + + /* Update the extracted text as well, if the input method has asked + for updates. 1 is + InputConnection.GET_EXTRACTED_TEXT_MONITOR. */ + + if (FRAME_ANDROID_OUTPUT (f)->extracted_text_flags & 1) + { + hint = FRAME_ANDROID_OUTPUT (f)->extracted_text_hint; + token = FRAME_ANDROID_OUTPUT (f)->extracted_text_token; + text = get_extracted_text (f, min (hint, 600), &start, + &offset, &length, &bytes); + + /* Make a string out of the extracted text. */ + string = android_text_to_string (android_java_env, + text, length, bytes); + xfree (text); + android_exception_check (); + + /* Make extracted text out of that string. */ + extracted = android_build_extracted_text (string, start, + offset); + android_exception_check_1 (string); + ANDROID_DELETE_LOCAL_REF (string); + + /* extracted is now an associated ExtractedText object. Perform + the update. */ + android_update_extracted_text (FRAME_ANDROID_WINDOW (f), + extracted, token); + ANDROID_DELETE_LOCAL_REF (extracted); + } } /* Notice that the input method connection to F should be reset as a @@ -5283,6 +5376,10 @@ android_reset_conversion (struct frame *f) android_reset_ic (FRAME_ANDROID_WINDOW (f), mode); + /* Clear extracted text flags. Since the IM has been reinitialised, + it should no longer be displaying extracted text. */ + FRAME_ANDROID_OUTPUT (f)->extracted_text_flags = 0; + /* Move its selection to the specified position. */ android_update_selection (f, NULL); } diff --git a/src/androidterm.h b/src/androidterm.h index ac845187a66..9bd11bb7853 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -241,6 +241,16 @@ struct android_output /* List of all tools (either styluses or fingers) pressed onto the frame. */ struct android_touch_point *touch_points; + + /* Flags associated with the last request to obtain ``extracted + text''. */ + int extracted_text_flags; + + /* Token asssociated with that request. */ + int extracted_text_token; + + /* The number of characters of extracted text wanted by the IM. */ + int extracted_text_hint; }; enum -- cgit v1.3 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 ++++++++++++++++++++++++++++++++---- lisp/battery.el | 11 +++++- src/android.c | 4 ++- src/android.h | 12 +++++++ src/androidfns.c | 16 +++++---- 5 files changed, 94 insertions(+), 14 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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 diff --git a/lisp/battery.el b/lisp/battery.el index a2bbd463c12..a51bc5267b3 100644 --- a/lisp/battery.el +++ b/lisp/battery.el @@ -1089,9 +1089,11 @@ The following %-sequences are provided: The following %-sequences are provided: %c Current capacity (mAh) %r Current rate of charge or discharge (mA) +%L AC line status (verbose). %B Battery status (verbose) %b Battery status, empty means high, `-' means low, `+' means charging and `?' means unknown. +%d Temperature (in degrees Celsius) %p Battery load percentage. %m Remaining time (to charge) in minutes. %h Remaining time (to charge) in hours. @@ -1139,7 +1141,14 @@ The following %-sequences are provided: (cons ?m (or minutes "N/A")) (cons ?h (or hours "N/A")) (cons ?t (or remaining "N/A")) - (cons ?L "N/A"))))) + (cons ?L (cl-case (nth 6 status) + (0 "off-line") + (1 "on-line") + (2 "on-line (dock)") + (3 "on-line (USB)") + (4 "on-line (wireless)") + (t "unknown"))) + (cons ?t (/ (or (nth 7 status) 0) 10.0)))))) ;;; Private functions. diff --git a/src/android.c b/src/android.c index 69c87e731bd..763e17e9430 100644 --- a/src/android.c +++ b/src/android.c @@ -5754,7 +5754,7 @@ android_get_current_api_level (void) } /* Query the status of the battery, and place it in *STATUS. - Value is 1 if the system is too old, else 0. */ + Value is 1 upon failure, else 0. */ int android_query_battery (struct android_battery_state *status) @@ -5783,6 +5783,8 @@ android_query_battery (struct android_battery_state *status) status->current_now = longs[3]; status->remaining = longs[4]; status->status = longs[5]; + status->plugged = longs[6]; + status->temperature = longs[7]; (*android_java_env)->ReleaseLongArrayElements (android_java_env, array, longs, diff --git a/src/android.h b/src/android.h index ed0089ad94e..450f3859df9 100644 --- a/src/android.h +++ b/src/android.h @@ -160,6 +160,18 @@ struct android_battery_state but is not charging either. 1, if the battery state is unknown. */ int status; + + /* The power source of the battery. Value is: + + 0, if on battery power. + 1, for line power. + 8, for dock power. + 2, for USB power. + 4, for wireless power. */ + int plugged; + + /* The temperature of the battery in 10 * degrees centigrade. */ + int temperature; }; extern Lisp_Object android_browse_url (Lisp_Object); diff --git a/src/androidfns.c b/src/androidfns.c index 5a23e8bd196..2724b9595c1 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -2797,11 +2797,13 @@ frame_parm_handler android_frame_parm_handlers[] = DEFUN ("android-query-battery", Fandroid_query_battery, Sandroid_query_battery, 0, 0, 0, doc: /* Perform a query for battery information. -This function will not work before Android 5.0. Value is nil upon failure, or a list of the form: (CAPACITY CHARGE-COUNTER CURRENT-AVERAGE CURRENT-NOW STATUS - REMAINING) + REMAINING PLUGGED TEMP) + +where REMAINING, CURRENT-AVERAGE, and CURRENT-NOW are undefined prior +to Android 5.0. See the documentation at @@ -2822,12 +2824,14 @@ for more details about these values. */) if (android_query_battery (&state)) return Qnil; - return listn (6, make_int (state.capacity), - make_int (state.charge_counter), + return listn (8, make_int (state.capacity), + make_fixnum (state.charge_counter), make_int (state.current_average), make_int (state.current_now), - make_int (state.status), - make_int (state.remaining)); + make_fixnum (state.status), + make_int (state.remaining), + make_fixnum (state.plugged), + make_fixnum (state.temperature)); } #endif -- cgit v1.3 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. --- doc/emacs/android.texi | 5 +++++ java/org/gnu/emacs/EmacsOpenActivity.java | 15 +++++++++++++++ java/org/gnu/emacs/EmacsService.java | 7 +++++-- java/org/gnu/emacs/EmacsThread.java | 29 +++++++++++++++++++++++++---- lisp/term/android-win.el | 8 ++++---- 5 files changed, 54 insertions(+), 10 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index d50acda7710..15415f12570 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -133,6 +133,11 @@ file, it invokes @command{emacsclient} with the options and the name of the file being opened. Then, upon success, the focus is transferred to any open Emacs frame. + However, if Emacs is not running at the time the wrapper is opened, +it starts Emacs and gives it the file to open as an argument. Note +that if that Emacs in turn does not start the Emacs server, subsequent +attempts to open the file with the wrapper will fail. + @cindex /content directory, android Some files are given to Emacs as ``content identifiers'', which the system provides access to outside the normal filesystem APIs. Emacs 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); } diff --git a/lisp/term/android-win.el b/lisp/term/android-win.el index 94fe4d6489b..fc393681ac4 100644 --- a/lisp/term/android-win.el +++ b/lisp/term/android-win.el @@ -56,10 +56,10 @@ DISPLAY is ignored on Android." (cl-defmethod frame-creation-function (params &context (window-system android)) (x-create-frame-with-faces params)) -(cl-defmethod handle-args-function (_ignored &context (window-system android)) - ;; Nothing to do here: Android has no command line to provide - ;; arguments on. - (ignore)) +(cl-defmethod handle-args-function (args &context (window-system android)) + ;; Android has no command line to provide arguments on. + ;; However, call x-handle-args to handle file name args. + (x-handle-args args)) ;;; Selection support. -- cgit v1.3 From 889b61b99918d1c6313d4f884de2e2cb3ab466c9 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 7 May 2023 11:09:56 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (requestCursorUpdates): * java/org/gnu/emacs/EmacsNative.java (requestCursorUpdates): * java/org/gnu/emacs/EmacsService.java (updateCursorAnchorInfo): New functions. * src/android.c (struct android_emacs_service) (android_init_emacs_service): Add new method. (android_update_cursor_anchor_info): New function. * src/androidfns.c (android_set_preeditarea): New function. * src/androidgui.h (enum android_ime_operation): New operation `REQUEST_CURSOR_UPDATES'. (struct android_ime_event): Document new meaning of `length'. * src/androidterm.c (android_request_cursor_updates): New function. (android_handle_ime_event): Handle new operations. (handle_one_android_event, android_draw_window_cursor): Update the preedit area if needed, like on X. (requestCursorUpdates): New function. * src/androidterm.h (struct android_output): New field `need_cursor_updates'. --- java/org/gnu/emacs/EmacsInputConnection.java | 11 ++++ java/org/gnu/emacs/EmacsNative.java | 1 + java/org/gnu/emacs/EmacsService.java | 32 ++++++++++ src/android.c | 34 +++++++++++ src/androidfns.c | 28 +++++++++ src/androidgui.h | 15 ++++- src/androidterm.c | 88 ++++++++++++++++++++++++++++ src/androidterm.h | 5 ++ 8 files changed, 213 insertions(+), 1 deletion(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index d13b48288ce..21bbaca5d07 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -324,6 +324,17 @@ public final class EmacsInputConnection extends BaseInputConnection return this.deleteSurroundingText (beforeLength, afterLength); } + @Override + public boolean + requestCursorUpdates (int cursorUpdateMode) + { + if (EmacsService.DEBUG_IC) + Log.d (TAG, "requestCursorUpdates: " + cursorUpdateMode); + + EmacsNative.requestCursorUpdates (windowHandle, cursorUpdateMode); + return true; + } + /* Override functions which are not implemented. */ diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 7d13ff99abb..e699dda9ad4 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -209,6 +209,7 @@ public final class EmacsNative ExtractedTextRequest req, int flags); public static native void requestSelectionUpdate (short window); + public static native void requestCursorUpdates (short window, int mode); /* Return the current value of the selection, or -1 upon diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 33436892caa..30ef71540a9 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -25,10 +25,12 @@ import java.io.UnsupportedEncodingException; import java.util.List; +import android.graphics.Matrix; import android.graphics.Point; import android.view.InputDevice; import android.view.KeyEvent; +import android.view.inputmethod.CursorAnchorInfo; import android.view.inputmethod.ExtractedText; import android.app.Notification; @@ -635,6 +637,36 @@ public final class EmacsService extends Service window.view.imManager.restartInput (window.view); } + public void + updateCursorAnchorInfo (EmacsWindow window, float x, + float y, float yBaseline, + float yBottom) + { + CursorAnchorInfo info; + CursorAnchorInfo.Builder builder; + Matrix matrix; + int[] offsets; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) + return; + + offsets = new int[2]; + builder = new CursorAnchorInfo.Builder (); + matrix = new Matrix (window.view.getMatrix ()); + window.view.getLocationOnScreen (offsets); + matrix.postTranslate (offsets[0], offsets[1]); + builder.setMatrix (matrix); + builder.setInsertionMarkerLocation (x, y, yBaseline, yBottom, + 0); + info = builder.build (); + + if (DEBUG_IC) + Log.d (TAG, ("updateCursorAnchorInfo: " + x + " " + y + + " " + yBaseline + "-" + yBottom)); + + window.view.imManager.updateCursorAnchorInfo (window.view, info); + } + /* Open a content URI described by the bytes BYTES, a non-terminated string; make it writable if WRITABLE, and readable if READABLE. Truncate the file if TRUNCATE. diff --git a/src/android.c b/src/android.c index 129ad6b5767..8a41a7cdec5 100644 --- a/src/android.c +++ b/src/android.c @@ -113,6 +113,7 @@ struct android_emacs_service jmethodID query_battery; jmethodID display_toast; jmethodID update_extracted_text; + jmethodID update_cursor_anchor_info; }; struct android_emacs_pixmap @@ -2209,6 +2210,8 @@ android_init_emacs_service (void) FIND_METHOD (update_extracted_text, "updateExtractedText", "(Lorg/gnu/emacs/EmacsWindow;" "Landroid/view/inputmethod/ExtractedText;I)V"); + FIND_METHOD (update_cursor_anchor_info, "updateCursorAnchorInfo", + "(Lorg/gnu/emacs/EmacsWindow;FFFF)V"); #undef FIND_METHOD } @@ -6277,6 +6280,37 @@ android_update_extracted_text (android_window window, void *text, android_exception_check_1 (text); } +/* Report the position of the cursor to the input method connection on + WINDOW. + + X is the horizontal position of the end of the insertion marker. Y + is the top of the insertion marker. Y_BASELINE is the baseline of + the row containing the insertion marker, and Y_BOTTOM is the bottom + of the insertion marker. */ + +void +android_update_cursor_anchor_info (android_window window, float x, + float y, float y_baseline, + float y_bottom) +{ + jobject object; + jmethodID method; + + object = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + method = service_class.update_cursor_anchor_info; + + (*android_java_env)->CallNonvirtualVoidMethod (android_java_env, + emacs_service, + service_class.class, + method, + object, + (jfloat) x, + (jfloat) y, + (jfloat) y_baseline, + (jfloat) y_bottom); + android_exception_check (); +} + /* Window decoration management functions. */ diff --git a/src/androidfns.c b/src/androidfns.c index 3bd34edd5b9..60b0549e7d1 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -3000,6 +3000,34 @@ for more details about these values. */) make_fixnum (state.temperature)); } + + +/* Miscellaneous input method related stuff. */ + +/* Report X, Y, by the phys cursor width and height as the cursor + anchor rectangle for W's frame. */ + +void +android_set_preeditarea (struct window *w, int x, int y) +{ + struct frame *f; + + f = WINDOW_XFRAME (w); + + /* Convert the window coordinates to the frame's coordinate + space. */ + x = (WINDOW_TO_FRAME_PIXEL_X (w, x) + + WINDOW_LEFT_FRINGE_WIDTH (w) + + WINDOW_LEFT_MARGIN_WIDTH (w)); + y = WINDOW_TO_FRAME_PIXEL_Y (w, y); + + /* Note that calculating the baseline is too hard, so the bottom of + the cursor is used instead. */ + android_update_cursor_anchor_info (FRAME_ANDROID_WINDOW (f), x, + y, y + w->phys_cursor_height, + y + w->phys_cursor_height); +} + #endif diff --git a/src/androidgui.h b/src/androidgui.h index ddd8e9fcf72..6db25098398 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -432,6 +432,13 @@ enum android_ime_operation ANDROID_IME_START_BATCH_EDIT, ANDROID_IME_END_BATCH_EDIT, ANDROID_IME_REQUEST_SELECTION_UPDATE, + ANDROID_IME_REQUEST_CURSOR_UPDATES, + }; + +enum + { + ANDROID_CURSOR_UPDATE_IMMEDIATE = 1, + ANDROID_CURSOR_UPDATE_MONITOR = (1 << 1), }; struct android_ime_event @@ -452,7 +459,11 @@ struct android_ime_event indices, and may actually mean ``left'' and ``right''. */ ptrdiff_t start, end, position; - /* The number of characters in TEXT. */ + /* The number of characters in TEXT. + + If OPERATION is ANDROID_IME_REQUEST_CURSOR_UPDATES, then this is + actually the cursor update mode associated with that + operation. */ size_t length; /* TEXT is either NULL, or a pointer to LENGTH bytes of malloced @@ -620,6 +631,8 @@ extern void android_update_ic (android_window, ptrdiff_t, ptrdiff_t, extern void android_reset_ic (android_window, enum android_ic_mode); extern void android_update_extracted_text (android_window, void *, int); +extern void android_update_cursor_anchor_info (android_window, float, + float, float, float); extern int android_set_fullscreen (android_window, bool); enum android_cursor_shape diff --git a/src/androidterm.c b/src/androidterm.c index 8ba7fb6a798..6f7c06875ca 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -632,6 +632,40 @@ android_decode_utf16 (unsigned short *utf16, size_t n) return coding.dst_object; } +/* Handle a cursor update request for F from the input method. + MODE specifies whether or not an update should be sent immediately, + and whether or not they are needed in the future. + + If MODE & ANDROID_CURSOR_UPDATE_IMMEDIATE, report the position of + F's old selected window's phys cursor now. + + If MODE & ANDROID_CURSOR_UPDATE_MONITOR, set + `need_cursor_updates'. */ + +static void +android_request_cursor_updates (struct frame *f, int mode) +{ + struct window *w; + + if (mode & ANDROID_CURSOR_UPDATE_IMMEDIATE + && WINDOWP (WINDOW_LIVE_P (f->old_selected_window) + ? f->old_selected_window + : f->selected_window)) + { + /* Prefer the old selected window, as its selection is what was + reported to the IME previously. */ + + w = XWINDOW (WINDOW_LIVE_P (f->old_selected_window) + ? f->old_selected_window + : f->selected_window); + android_set_preeditarea (w, w->cursor.x, w->cursor.y); + } + + /* Now say whether or not updates are needed in the future. */ + FRAME_OUTPUT_DATA (f)->need_cursor_updates + = (mode & ANDROID_CURSOR_UPDATE_MONITOR); +} + /* Handle a single input method event EVENT, delivered to the frame F. @@ -705,6 +739,10 @@ android_handle_ime_event (union android_event *event, struct frame *f) case ANDROID_IME_REQUEST_SELECTION_UPDATE: request_point_update (f, event->ime.counter); break; + + case ANDROID_IME_REQUEST_CURSOR_UPDATES: + android_request_cursor_updates (f, event->ime.length); + break; } } @@ -724,6 +762,7 @@ handle_one_android_event (struct android_display_info *dpyinfo, double scroll_unit; int keysym; ptrdiff_t nchars, i; + struct window *w; /* It is okay for this to not resemble handle_one_xevent so much. Differences in event handling code are much less nasty than @@ -812,6 +851,12 @@ handle_one_android_event (struct android_display_info *dpyinfo, inev.ie.kind = MOVE_FRAME_EVENT; XSETFRAME (inev.ie.frame_or_window, f); } + + if (f && FRAME_OUTPUT_DATA (f)->need_cursor_updates) + { + w = XWINDOW (f->selected_window); + android_set_preeditarea (w, w->cursor.x, w->cursor.y); + } } goto OTHER; @@ -954,6 +999,16 @@ handle_one_android_event (struct android_display_info *dpyinfo, goto done_keysym; done_keysym: + + /* Now proceed to tell the input method the current position of + the cursor, if required. */ + + if (f && FRAME_OUTPUT_DATA (f)->need_cursor_updates) + { + w = XWINDOW (f->selected_window); + android_set_preeditarea (w, w->cursor.x, w->cursor.y); + } + goto OTHER; case ANDROID_FOCUS_IN: @@ -4321,6 +4376,10 @@ android_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, int x, int y, enum text_cursor_kinds cursor_type, int cursor_width, bool on_p, bool active_p) { + struct frame *f; + + f = WINDOW_XFRAME (w); + if (on_p) { w->phys_cursor_type = cursor_type; @@ -4362,6 +4421,13 @@ android_draw_window_cursor (struct window *w, struct glyph_row *glyph_row, emacs_abort (); } } + + /* Now proceed to tell the input method the current position of + the cursor, if required. */ + + if (FRAME_OUTPUT_DATA (f)->need_cursor_updates + && w == XWINDOW (f->selected_window)) + android_set_preeditarea (w, x, y); } } @@ -5404,6 +5470,28 @@ NATIVE_NAME (requestSelectionUpdate) (JNIEnv *env, jobject object, android_write_event (&event); } +JNIEXPORT void JNICALL +NATIVE_NAME (requestCursorUpdates) (JNIEnv *env, jobject object, + jshort window, jint mode) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + union android_event event; + + event.ime.type = ANDROID_INPUT_METHOD; + event.ime.serial = ++event_serial; + event.ime.window = window; + event.ime.operation = ANDROID_IME_REQUEST_CURSOR_UPDATES; + event.ime.start = 0; + event.ime.end = 0; + event.ime.length = mode; + event.ime.position = 0; + event.ime.text = NULL; + event.ime.counter = ++edit_counter; + + android_write_event (&event); +} + #ifdef __clang__ #pragma clang diagnostic pop #else diff --git a/src/androidterm.h b/src/androidterm.h index 9396d5fe315..e3738fb2192 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -231,6 +231,10 @@ struct android_output because the frame contents have been dirtied. */ bool_bf need_buffer_flip : 1; + /* Whether or not the input method should be notified every time the + position of this frame's selected window changes. */ + bool_bf need_cursor_updates : 1; + /* Relief GCs, colors etc. */ struct relief { struct android_gc *gc; @@ -383,6 +387,7 @@ extern struct android_display_info *x_display_list; extern void android_free_gcs (struct frame *); extern void android_default_font_parameter (struct frame *, Lisp_Object); +extern void android_set_preeditarea (struct window *, int, int); /* Defined in androidterm.c. */ -- cgit v1.3 From 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. --- doc/emacs/android.texi | 14 ++++--- java/AndroidManifest.xml.in | 17 +++++++++ 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 ++++++---- java/res/drawable/emacs_wrench.png | Bin 0 -> 24996 bytes java/res/values-v24/bool.xml | 22 +++++++++++ java/res/values/bool.xml | 1 + java/res/values/strings.xml | 39 ++++++++++++++++++++ java/res/xml/preferences.xml | 12 +++--- 12 files changed, 180 insertions(+), 31 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java create mode 100644 java/res/drawable/emacs_wrench.png create mode 100644 java/res/values-v24/bool.xml create mode 100644 java/res/values/strings.xml (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index d7fadd69e4b..d94b91c7ab7 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -608,16 +608,20 @@ to provide that style. @cindex troubleshooting, android @cindex emacs -Q, android +@cindex emacs --debug-init, android Since Android has no command line, there is normally no way to specify command-line arguments when starting Emacs. This is very nasty when you make a mistake in your Emacs initialization files that prevents Emacs from starting up at all, as the system normally prevents other programs from accessing Emacs's home directory. - - However, Emacs can be started with the equivalent of the -@code{--quick} option (@pxref{Initial Options}) through a special -preferences screen, which can be accessed through the Emacs ``app -info'' page in the system settings application. +@xref{Initial Options}. + + However, Emacs can be started with the equivalent of either the +option @code{--quick}, or @code{--debug-init}, through a special +preferences screen. Under Android 7.0 and later, this can be accessed +through the Emacs ``app info'' page in the system settings program; on +older systems, this is displayed as a separate icon on the desktop +labeled ``Emacs options''. Consult the manufacturer of your device for more details, as how to do this varies by device. diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in index f7f834e7582..082c4c9373e 100644 --- a/java/AndroidManifest.xml.in +++ b/java/AndroidManifest.xml.in @@ -180,6 +180,23 @@ 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, }; } diff --git a/java/res/drawable/emacs_wrench.png b/java/res/drawable/emacs_wrench.png new file mode 100644 index 00000000000..50572d3bed1 Binary files /dev/null and b/java/res/drawable/emacs_wrench.png differ diff --git a/java/res/values-v24/bool.xml b/java/res/values-v24/bool.xml new file mode 100644 index 00000000000..37f07992995 --- /dev/null +++ b/java/res/values-v24/bool.xml @@ -0,0 +1,22 @@ + + + + false + diff --git a/java/res/values/bool.xml b/java/res/values/bool.xml index d37eab745c0..2b253824e29 100644 --- a/java/res/values/bool.xml +++ b/java/res/values/bool.xml @@ -19,4 +19,5 @@ along with GNU Emacs. If not, see . --> false + true diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml new file mode 100644 index 00000000000..36a47be6c84 --- /dev/null +++ b/java/res/values/strings.xml @@ -0,0 +1,39 @@ + + + + + Restart Emacs with -Q + + + Restart Emacs, but do not load site lisp or init files. + + + Restart Emacs with --debug-init + + + Restart Emacs, and display the debugger should an error occur while loading initialization files. + + + Delete dump file + + + Remove the dumped state created when Emacs was installed. + + diff --git a/java/res/xml/preferences.xml b/java/res/xml/preferences.xml index f0c3abb52e7..d52d28816e5 100644 --- a/java/res/xml/preferences.xml +++ b/java/res/xml/preferences.xml @@ -19,10 +19,12 @@ along with GNU Emacs. If not, see . --> - + android:title="@string/start_quick_title" + android:summary="@string/start_quick_caption"/> + + android:title="@string/erase_dump_title" + android:summary="@string/erase_dump_caption"/> -- cgit v1.3 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/emacs/EmacsService.java') 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.3 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 - src/android.c | 923 +++++++++++++++++++++++++++++++--- src/android.h | 2 +- src/androidgui.h | 15 + 5 files changed, 883 insertions(+), 275 deletions(-) delete mode 100644 java/org/gnu/emacs/EmacsCopyArea.java (limited to 'java/org/gnu/emacs/EmacsService.java') 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) { diff --git a/src/android.c b/src/android.c index 8a41a7cdec5..33d766a90d9 100644 --- a/src/android.c +++ b/src/android.c @@ -28,6 +28,7 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include #include #include @@ -68,6 +69,10 @@ bool android_init_gui; #include +#ifdef __aarch64__ +#include +#endif /* __aarch64__ */ + #define ANDROID_THROW(env, class, msg) \ ((*(env))->ThrowNew ((env), (*(env))->FindClass ((env), class), msg)) @@ -95,7 +100,6 @@ struct android_emacs_service jmethodID draw_rectangle; jmethodID draw_line; jmethodID draw_point; - jmethodID copy_area; jmethodID clear_window; jmethodID clear_area; jmethodID ring_bell; @@ -2178,10 +2182,6 @@ android_init_emacs_service (void) FIND_METHOD (draw_point, "drawPoint", "(Lorg/gnu/emacs/EmacsDrawable;" "Lorg/gnu/emacs/EmacsGC;II)V"); - FIND_METHOD (copy_area, "copyArea", - "(Lorg/gnu/emacs/EmacsDrawable;" - "Lorg/gnu/emacs/EmacsDrawable;" - "Lorg/gnu/emacs/EmacsGC;IIIIII)V"); FIND_METHOD (clear_window, "clearWindow", "(Lorg/gnu/emacs/EmacsWindow;)V"); FIND_METHOD (clear_area, "clearArea", @@ -3356,6 +3356,16 @@ android_create_gc (enum android_gc_value_mask mask, /* This means to not apply any clipping. */ gc->num_clip_rects = -1; + /* Apply the other default values. */ + gc->function = ANDROID_GC_COPY; + gc->fill_style = ANDROID_FILL_SOLID; + gc->clip_x_origin = 0; + gc->clip_y_origin = 0; + gc->clip_mask = ANDROID_NONE; + gc->stipple = ANDROID_NONE; + gc->ts_x_origin = 0; + gc->ts_y_origin = 0; + if (!gc->gcontext) { xfree (gc); @@ -3430,10 +3440,13 @@ android_change_gc (struct android_gc *gc, } if (mask & ANDROID_GC_FUNCTION) - (*android_java_env)->SetIntField (android_java_env, - gcontext, - emacs_gc_function, - values->function); + { + (*android_java_env)->SetIntField (android_java_env, + gcontext, + emacs_gc_function, + values->function); + gc->function = values->function; + } if (mask & ANDROID_GC_CLIP_X_ORIGIN) { @@ -3441,6 +3454,7 @@ android_change_gc (struct android_gc *gc, gcontext, emacs_gc_clip_x_origin, values->clip_x_origin); + gc->clip_x_origin = values->clip_x_origin; clip_changed = true; } @@ -3450,6 +3464,7 @@ android_change_gc (struct android_gc *gc, gcontext, emacs_gc_clip_y_origin, values->clip_y_origin); + gc->clip_y_origin = values->clip_y_origin; clip_changed = true; } @@ -3461,6 +3476,7 @@ android_change_gc (struct android_gc *gc, gcontext, emacs_gc_clip_mask, what); + gc->clip_mask = values->clip_mask; /* Changing GCClipMask also clears the clip rectangles. */ (*android_java_env)->SetObjectField (android_java_env, @@ -3482,25 +3498,35 @@ android_change_gc (struct android_gc *gc, gcontext, emacs_gc_stipple, what); + gc->stipple = values->stipple; } if (mask & ANDROID_GC_FILL_STYLE) - (*android_java_env)->SetIntField (android_java_env, - gcontext, - emacs_gc_fill_style, - values->fill_style); + { + (*android_java_env)->SetIntField (android_java_env, + gcontext, + emacs_gc_fill_style, + values->fill_style); + gc->fill_style = values->fill_style; + } if (mask & ANDROID_GC_TILE_STIP_X_ORIGIN) - (*android_java_env)->SetIntField (android_java_env, - gcontext, - emacs_gc_ts_origin_x, - values->ts_x_origin); + { + (*android_java_env)->SetIntField (android_java_env, + gcontext, + emacs_gc_ts_origin_x, + values->ts_x_origin); + gc->ts_x_origin = values->ts_x_origin; + } if (mask & ANDROID_GC_TILE_STIP_Y_ORIGIN) - (*android_java_env)->SetIntField (android_java_env, - gcontext, - emacs_gc_ts_origin_y, - values->ts_y_origin); + { + (*android_java_env)->SetIntField (android_java_env, + gcontext, + emacs_gc_ts_origin_y, + values->ts_y_origin); + gc->ts_y_origin = values->ts_y_origin; + } if (mask) { @@ -3732,22 +3758,13 @@ android_get_gc_values (struct android_gc *gc, values->background = gc->background; if (mask & ANDROID_GC_FUNCTION) - values->function - = (*android_java_env)->GetIntField (android_java_env, - gcontext, - emacs_gc_function); + values->function = gc->function; if (mask & ANDROID_GC_CLIP_X_ORIGIN) - values->clip_x_origin - = (*android_java_env)->GetIntField (android_java_env, - gcontext, - emacs_gc_clip_x_origin); + values->clip_x_origin = gc->clip_x_origin; if (mask & ANDROID_GC_CLIP_Y_ORIGIN) - values->clip_y_origin - = (*android_java_env)->GetIntField (android_java_env, - gcontext, - emacs_gc_clip_y_origin); + values->clip_y_origin = gc->clip_y_origin; if (mask & ANDROID_GC_FILL_STYLE) values->fill_style @@ -3913,34 +3930,827 @@ android_set_fill_style (struct android_gc *gc, android_change_gc (gc, ANDROID_GC_FILL_STYLE, &gcv); } + + +/* Pixmap bit blit implementation. This exists as `Canvas.drawBitmap' + seems to have trouble with copying bitmap data from one bitmap back + to itself on Android 8.0. */ + +/* Function called to actually perform the copy. */ + +typedef void (*android_blit_func) (int, int, int, int, int, int, + struct android_gc *, + unsigned char *, AndroidBitmapInfo *, + unsigned char *, AndroidBitmapInfo *, + unsigned char *, AndroidBitmapInfo *); + + + +#ifdef __aarch64__ + +/* Copy N pixels from SRC to DST, using MASK as a depth 1 clip + mask. */ + +static void +android_neon_mask_line (unsigned int *src, unsigned int *dst, + unsigned char *mask, int n) +{ + uint32x4_t src_low, src_high, dst_low, dst_high; + int16x8_t vmask; + int32x4_t ext_mask_low, ext_mask_high, low, high; + int rem; + + /* Calculate the remainder. */ + rem = n & 7; + + /* Process eight pixels at a time. */ + + if (n -= rem) + { + again: + /* Load the low and high four pixels from the source. */ + src_low = vld1q_u32 (src); + src_high = vld1q_u32 (src + 4); + + /* Do the same with the destination. */ + dst_low = vld1q_u32 (dst); + dst_high = vld1q_u32 (dst + 4); + + /* Load and sign extend the mask. */ + vmask = vmovl_s8 (vld1_u8 (mask)); + ext_mask_low = vmovl_s16 (vget_low_s16 (vmask)); + ext_mask_high = vmovl_s16 (vget_high_s16 (vmask)); + + /* Reinterpret the mask. */ + low = vreinterpretq_u32_s32 (ext_mask_low); + high = vreinterpretq_u32_s32 (ext_mask_high); + + /* Apply the mask. */ + dst_low = vbicq_u32 (dst_low, low); + src_low = vandq_u32 (src_low, low); + dst_high = vbicq_u32 (dst_high, high); + src_high = vandq_u32 (src_high, high); + + /* Write the result after combining both masked vectors. */ + vst1q_u32 (dst, vorrq_u32 (dst_low, src_low)); + vst1q_u32 (dst + 4, vorrq_u32 (dst_high, src_high)); + + /* Adjust src, dst and mask. */ + dst += 8; + src += 8; + mask += 8; + + /* See if this loop should continue. */ + n -= 8; + if (n > 0) + goto again; + } + + /* Process the remaining pixels. */ + + while (--rem) + { + /* Sign extend the mask. */ + n = *(signed char *) mask++; + + /* Combine src and dst. */ + *dst = ((*src & n) | (*dst & ~n)); + src++, dst++; + } +} + +#endif /* __aarch64__ */ + + + +/* Copy a rectangle SRC_X, SRC_Y, WIDTH and HEIGHT from SRC, described + by SRC_INFO, to DST_X and DST_Y in DST, as described by DST_INFO. + + If MASK is set, mask the source data using MASK_INFO, translating + it by GC->clip_x_origin and GC->clip_y_origin. MASK must be a + pixmap of depth 1. + + N.B. that currently only copies between bitmaps of depth 24 are + implemented. */ + +void +android_blit_copy (int src_x, int src_y, int width, int height, + int dst_x, int dst_y, struct android_gc *gc, + unsigned char *src, AndroidBitmapInfo *src_info, + unsigned char *dst, AndroidBitmapInfo *dst_info, + unsigned char *mask, AndroidBitmapInfo *mask_info) +{ + uintptr_t start, end; + int mask_offset; + size_t pixel, offset, offset1; + unsigned char *src_current, *dst_current; + unsigned char *mask_current; + int overflow, temp, i, xdir; + bool backwards; + unsigned int *long_src, *long_dst; + + /* Assert that the specified coordinates are within bounds. */ + eassert (src_x >= 0 && src_y >= 0 + && dst_x >= 0 && dst_y >= 0); + eassert (src_x + width <= src_info->width); + eassert (src_y + height <= src_info->height); + eassert (dst_x + width <= dst_info->width); + eassert (dst_y + height <= dst_info->height); + + /* Now check that each bitmap has the correct format. */ + eassert (src_info->format == dst_info->format + && src_info->format == ANDROID_BITMAP_FORMAT_RGBA_8888); + pixel = sizeof (unsigned int); + + /* Android doesn't have A1 bitmaps, so A8 is used to represent + packed bitmaps of depth 1. */ + eassert (!mask || mask_info->format == ANDROID_BITMAP_FORMAT_A_8); + + /* Calculate the address of the first pixel of the first row to be + copied in both src and dst. Compare them to determine the + direction in which the copy is to take place. */ + + overflow = ckd_mul (&start, src_y, src_info->stride); + overflow |= ckd_mul (&end, src_x, pixel); + overflow |= ckd_add (&start, end, start); + overflow |= ckd_add (&start, (uintptr_t) src, start); + + if (overflow) + return; + + src_current = (unsigned char *) start; + + overflow = ckd_mul (&start, dst_y, dst_info->stride); + overflow |= ckd_mul (&end, dst_x, pixel); + overflow |= ckd_add (&start, end, start); + overflow |= ckd_add (&start, (uintptr_t) dst, start); + + if (overflow) + return; + + dst_current = (unsigned char *) start; + backwards = false; + + /* Now see if copying should proceed from the bottom up. */ + + if (src == dst && dst_current >= src_current) + { + backwards = true; + + /* Walk src and dst from bottom to top, in order to avoid + overlap. Calculate the coordinate of the last pixel of the + last row in both src and dst. */ + + overflow = ckd_mul (&start, src_y + height - 1, + src_info->stride); + if (mask) /* If a mask is set, put the pointers before the end + of the row. */ + overflow |= ckd_mul (&end, src_x + width - 1, pixel); + else + overflow |= ckd_mul (&end, src_x, pixel); + overflow |= ckd_add (&start, start, end); + overflow |= ckd_add (&start, (uintptr_t) src, start); + + if (overflow) + return; + + src_current = (unsigned char *) start; + + overflow = ckd_mul (&start, dst_y + height - 1, + dst_info->stride); + if (mask) /* If a mask is set, put the pointers before the end + of the row. */ + overflow |= ckd_mul (&end, dst_x + width - 1, pixel); + else + overflow |= ckd_mul (&end, dst_x, pixel); + overflow |= ckd_add (&start, start, end); + overflow |= ckd_add (&start, (uintptr_t) dst, start); + + if (overflow) + return; + + dst_current = (unsigned char *) start; + } + + if (!mask) + { + /* Change the direction of the copy depending on how SRC and DST + overlap. */ + + for (i = 0; i < height; ++i) + { + memmove (dst_current, src_current, + width * pixel); + + if (backwards) + { + /* Proceed to the last row. */ + src_current -= src_info->stride; + dst_current -= dst_info->stride; + } + else + { + /* Proceed to the next row. */ + src_current += src_info->stride; + dst_current += dst_info->stride; + } + } + } + else + { + /* Adjust the source and destination Y. The start is MAX + (dst_y, gc->clip_y_origin); the difference between that value + and dst_y is the offset to apply to src_y. */ + + temp = dst_y; + dst_y = MAX (dst_y, gc->clip_y_origin); + src_y += dst_y - temp; + height -= dst_y - temp; + + /* Verify that the bounds are correct. */ + eassert (dst_y + height + <= gc->clip_y_origin + mask_info->height); + eassert (dst_y >= gc->clip_y_origin); + + /* There is a mask. For each scan line... */ + + if (backwards) + { + /* Calculate the number of pixels at the end of the + mask. */ + + mask_offset = dst_x + width; + mask_offset -= mask_info->width + gc->clip_x_origin; + + if (mask_offset < 0) + mask_offset = 0; + + /* Calculate the last column of the mask that will be + consulted. */ + + temp = dst_x - gc->clip_x_origin; + temp += MIN (mask_info->width - temp, + width - mask_offset); + + if (temp < 0) + return; + + /* Now calculate the last row of the mask that will be + consulted. */ + i = dst_y - gc->clip_y_origin + height; + + /* Turn both into offsets. */ + + if (INT_MULTIPLY_WRAPV (temp, pixel, &offset) + || INT_MULTIPLY_WRAPV (i, mask_info->stride, &offset1) + || INT_ADD_WRAPV (offset, offset1, &offset) + || INT_ADD_WRAPV ((uintptr_t) mask, offset, &start)) + return; + + mask = mask_current = (unsigned char *) start; + + while (--height) + { + /* Skip backwards past the end of the mask. */ + + long_src = (unsigned int *) (src_current - mask_offset * pixel); + long_dst = (unsigned int *) (dst_current - mask_offset * pixel); + mask = mask_current; + + /* For each pixel covered by the mask... */ + temp = MIN (mask_info->width - temp, width - mask_offset); + while (temp--) + { + /* Copy the destination it to the source, masked by + the mask. */ + + /* Sign extend the mask. */ + i = *(signed char *) mask--; + + /* Apply the mask. */ + *long_dst = ((*long_src & i) | (*long_dst & ~i)); + + long_dst--; + long_src--; + } + + /* Return to the last row. */ + src_current -= src_info->stride; + dst_current -= dst_info->stride; + mask_current -= mask_info->stride; + } + } + else + { + /* Calculate the first column of the mask that will be + consulted. */ + + mask_offset = dst_x - gc->clip_x_origin; + + /* Adjust the mask by that much. */ + + if (mask_offset > 0) + mask += mask_offset; + else + { + /* Offset src and dst by the mask offset. */ + src_current += -mask_offset * pixel; + dst_current += -mask_offset * pixel; + width += mask_offset; + } + + /* Make sure it's not out of bounds. */ + + eassert (dst_y - gc->clip_y_origin >= 0); + if ((dst_y - gc->clip_y_origin) + height > mask_info->height) + return; + + /* Now move mask to the position of the first row. */ + + mask += ((dst_y - gc->clip_y_origin) + * mask_info->stride); + + /* Determine how many bytes need to be copied. */ + + if (mask_offset > 0) + temp = MIN (mask_info->width - mask_offset, width); + else + temp = MIN (mask_info->width, width); + + /* Copy bytes according to the mask. */ + + while (--height) + { + long_src = (unsigned int *) src_current; + long_dst = (unsigned int *) dst_current; + mask_current = mask; + +#ifndef __aarch64__ + while (temp--) + { + /* Sign extend the mask. */ + height = *(signed char *) mask_current++; + + /* Apply the mask. */ + *long_dst = ((*long_src & height) + | (*long_dst & ~height)); + } +#else /* __aarch64__ */ + android_neon_mask_line (long_src, long_dst, mask, temp); +#endif /* __aarch64__ */ + + src_current += src_info->stride; + dst_current += dst_info->stride; + mask += mask_info->stride; + } + } + } +} + + +/* Xor a rectangle SRC_X, SRC_Y, WIDTH and HEIGHT from SRC, described + by SRC_INFO, to DST_X and DST_Y in DST, as described by DST_INFO. + + Ignore the alpha channel when computing the exclusive-or of the + destination pixel. + + If MASK is set, mask the source data using MASK_INFO, translating + it by GC->clip_x_origin and GC->clip_y_origin. MASK must be a + pixmap of depth 1. + + N.B. that currently only copies between bitmaps of depth 24 are + implemented. */ + +void +android_blit_xor (int src_x, int src_y, int width, int height, + int dst_x, int dst_y, struct android_gc *gc, + unsigned char *src, AndroidBitmapInfo *src_info, + unsigned char *dst, AndroidBitmapInfo *dst_info, + unsigned char *mask, AndroidBitmapInfo *mask_info) +{ + uintptr_t start, end; + int mask_offset; + size_t pixel, offset, offset1; + unsigned char *src_current, *dst_current; + unsigned char *mask_current; + int overflow, temp, i, xdir; + bool backwards; + unsigned int *long_src, *long_dst; + + /* Note that this alu hasn't been tested -- it probably does not + work! */ + emacs_abort (); + +#if 0 + /* Assert that the specified coordinates are within bounds. */ + eassert (src_x >= 0 && src_y >= 0 + && dst_x >= 0 && dst_y >= 0); + eassert (src_x + width <= src_info->width); + eassert (src_y + height <= src_info->height); + eassert (dst_x + width <= dst_info->width); + eassert (dst_y + height <= dst_info->height); + + /* Now check that each bitmap has the correct format. */ + eassert (src_info->format == dst_info->format + && src_info->format == ANDROID_BITMAP_FORMAT_RGBA_8888); + pixel = sizeof (unsigned int); + + /* Android doesn't have A1 bitmaps, so A8 is used to represent + packed bitmaps of depth 1. */ + eassert (!mask || mask_info->format == ANDROID_BITMAP_FORMAT_A_8); + + /* Calculate the address of the first pixel of the first row to be + copied in both src and dst. Compare them to determine the + direction in which the copy is to take place. */ + + overflow = ckd_mul (&start, src_y, src_info->stride); + overflow |= ckd_mul (&end, src_x, pixel); + overflow |= ckd_add (&start, (uintptr_t) src, start); + + if (overflow) + return; + + src_current = (unsigned char *) start; + + overflow = ckd_mul (&start, dst_y, src_info->stride); + overflow |= ckd_mul (&end, dst_x, pixel); + overflow |= ckd_add (&start, (uintptr_t) dst, start); + + if (overflow) + return; + + dst_current = (unsigned char *) start; + backwards = false; + + /* Now see if copying should proceed from the bottom up. */ + + if (src == dst && dst_current >= src_current) + { + backwards = true; + + /* Walk src and dst from bottom to top, in order to avoid + overlap. Calculate the coordinate of the last pixel of the + last row in both src and dst. */ + + overflow = ckd_mul (&start, src_y + height - 1, + src_info->stride); + if (mask) /* If a mask is set, put the pointers before the end + of the row. */ + overflow |= ckd_mul (&end, src_x + width - 1, pixel); + else + overflow |= ckd_mul (&end, src_x, pixel); + overflow |= ckd_add (&start, start, end); + overflow |= ckd_add (&start, (uintptr_t) src, start); + + if (overflow) + return; + + src_current = (unsigned char *) start; + + overflow = ckd_mul (&start, dst_y + height - 1, + dst_info->stride); + if (mask) /* If a mask is set, put the pointers before the end + of the row. */ + overflow |= ckd_mul (&end, dst_x + width - 1, pixel); + else + overflow |= ckd_mul (&end, dst_x, pixel); + overflow |= ckd_add (&start, start, end); + overflow |= ckd_add (&start, (uintptr_t) dst, start); + + if (overflow) + return; + + dst_current = (unsigned char *) start; + } + + if (!mask) + { + /* Change the direction of the copy depending on how SRC and DST + overlap. */ + + for (i = 0; i < height; ++i) + { + if (backwards) + { + for (i = width - 1; i <= 0; --i) + (((unsigned int *) dst_current)[i]) + /* Keep the alpha channel intact. */ + ^= (((unsigned int *) src_current)[i]) & 0xffffff; + + /* Proceed to the last row. */ + src_current -= src_info->stride; + dst_current -= dst_info->stride; + } + else + { + for (i = 0; i < width; ++i) + (((unsigned int *) dst_current)[i]) + /* Keep the alpha channel intact. */ + ^= (((unsigned int *) src_current)[i]) & 0xffffff; + + /* Proceed to the next row. */ + src_current += src_info->stride; + dst_current += dst_info->stride; + } + } + } + else + { + /* Adjust the source and destination Y. The start is MAX + (dst_y, gc->clip_y_origin); the difference between that value + and dst_y is the offset to apply to src_y. */ + + temp = dst_y; + dst_y = MAX (dst_y, gc->clip_y_origin); + src_y += dst_y - temp; + height -= dst_y - temp; + + /* Verify that the bounds are correct. */ + eassert (dst_y + height + <= gc->clip_y_origin + mask_info->height); + eassert (dst_y >= gc->clip_y_origin); + + /* There is a mask. For each scan line... */ + + if (backwards) + { + /* Calculate the number of pixels at the end of the + mask. */ + + mask_offset = dst_x + width; + mask_offset -= mask_info->width + gc->clip_x_origin; + + if (mask_info < 0) + mask_info = 0; + + /* Calculate the last column of the mask that will be + consulted. */ + + temp = dst_x - gc->clip_x_origin; + temp += MIN (mask_info->width - temp, + width - mask_offset); + + if (temp < 0) + return; + + /* Now calculate the last row of the mask that will be + consulted. */ + i = dst_y - gc->clip_y_origin + height; + + /* Turn both into offsets. */ + + if (INT_MULTIPLY_WRAPV (temp, pixel, &offset) + || INT_MULTIPLY_WRAPV (i, mask_info->stride, &offset1) + || INT_ADD_WRAPV (offset, offset1, &offset) + || INT_ADD_WRAPV ((uintptr_t) mask, offset, &start)) + return; + + mask = mask_current = (unsigned char *) start; + + for (i = 0; i < height; ++i) + { + /* Skip backwards past the end of the mask. */ + + long_src = (unsigned int *) (src_current - mask_offset * pixel); + long_dst = (unsigned int *) (dst_current - mask_offset * pixel); + mask = mask_current; + + /* For each pixel covered by the mask... */ + temp = MIN (mask_info->width - temp, width - mask_offset); + while (temp--) + /* XOR the source to the destination, masked by the + mask. */ + *long_dst-- ^= ((*(long_src--) & (0u - (*(mask--) & 1))) + & 0xffffff); + + /* Return to the last row. */ + src_current -= src_info->stride; + dst_current -= dst_info->stride; + mask_current -= mask_info->stride; + } + } + else + { + /* Calculate the first column of the mask that will be + consulted. */ + + mask_offset = dst_x - gc->clip_x_origin; + + /* Adjust the mask by that much. */ + + if (mask_offset > 0) + mask += mask_offset; + else + { + /* Offset src and dst by the mask offset. */ + src_current += -mask_offset * pixel; + dst_current += -mask_offset * pixel; + width -= mask_offset; + } + + /* Now move mask to the position of the first row. */ + + mask += gc->clip_y_origin * mask_info->stride; + + for (i = 0; i < height; ++i) + { + long_src = (unsigned int *) src_current; + long_dst = (unsigned int *) dst_current; + mask_current = mask; + + if (mask_offset > 0) + { + /* Copy bytes according to the mask. */ + temp = MIN (mask_info->width - mask_offset, width); + while (temp--) + *long_dst++ ^= ((*(long_src++) + & (0u - (*(mask_current++) & 1))) + & 0xffffff); + } + else + { + /* Copy bytes according to the mask. */ + temp = MIN (mask_info->width, width); + while (temp--) + *long_dst++ = ((*(long_src++) + & (0u - (*(mask_current++) & 1))) + & 0xffffff); + } + + src_current += src_info->stride; + dst_current += dst_info->stride; + mask += mask_info->stride; + } + } + } +#endif /* 0 */ +} + void android_copy_area (android_drawable src, android_drawable dest, struct android_gc *gc, int src_x, int src_y, unsigned int width, unsigned int height, int dest_x, int dest_y) { - jobject src_object, dest_object, gcontext; + jobject src_object, dest_object, mask; + android_blit_func do_blit; + AndroidBitmapInfo src_info, dest_info, mask_info; + void *src_data, *dest_data, *mask_data; + int n_clip_rects, i; + bool flag; + struct android_rectangle bounds, rect, temp, *clip_rectangles; - src_object = android_resolve_handle2 (src, ANDROID_HANDLE_WINDOW, - ANDROID_HANDLE_PIXMAP); - dest_object = android_resolve_handle2 (dest, ANDROID_HANDLE_WINDOW, - ANDROID_HANDLE_PIXMAP); - gcontext = android_resolve_handle (gc->gcontext, - ANDROID_HANDLE_GCONTEXT); + /* Perform the copy. Loop over each clip rectangle, unless none are + set. Also, obtain bitmaps for src and dst, and possibly the mask + as well if it is present. */ - (*android_java_env)->CallNonvirtualVoidMethod (android_java_env, - emacs_service, - service_class.class, - service_class.copy_area, - src_object, - dest_object, - gcontext, - (jint) src_x, (jint) src_y, - (jint) width, (jint) height, - (jint) dest_x, (jint) dest_y); - android_exception_check (); + src_data = android_lock_bitmap (src, &src_info, &src_object); + if (!src_data) + return; + + mask_data = mask = NULL; + + if (src != dest) + { + dest_data = android_lock_bitmap (dest, &dest_info, &dest_object); + if (!dest_data) + goto fail; + } + else + { + dest_data = src_data; + dest_info = src_info; + } + + /* Obtain the bitmap for the mask if necessary. */ + + if (gc->clip_mask) + { + mask_data = android_lock_bitmap (gc->clip_mask, + &mask_info, &mask); + if (!mask_data) + goto fail1; + } + + /* Calculate the number of clip rectangles. */ + n_clip_rects = gc->num_clip_rects; + + /* If n_clip_rects is -1, then no clipping is in effect. Set rect + to the bounds of the destination. */ + + flag = n_clip_rects == -1; + if (flag) + { + n_clip_rects = 1; + clip_rectangles = ▭ + } + else if (!n_clip_rects) + goto fail2; + else + clip_rectangles = gc->clip_rects; + + /* Set rect to the bounds of the destination. */ + + rect.x = 0; + rect.y = 0; + rect.width = dest_info.width; + rect.height = dest_info.height; + + if (mask_data) + { + /* Clip width and height to that of the mask. */ + + if (src_x + width > mask_info.width) + width = mask_info.width - src_x; + + if (src_y + height > mask_info.height) + height = mask_info.height - src_y; + } + + /* Clip width and height to that of the source. */ + + if (src_x + width > src_info.width) + width = src_info.width - src_x; + + if (src_y + height > src_info.height) + height = src_info.height - src_y; + + /* Return if the copy is outside the source. */ + + if (width <= 0 || height <= 0) + goto fail2; + + /* Look up the right function for the alu. */ + + switch (gc->function) + { + case ANDROID_GC_COPY: + do_blit = android_blit_copy; + break; + + case ANDROID_GC_XOR: + do_blit = android_blit_xor; + break; + } + + /* Load the bounds of the destination rectangle. */ + bounds.x = dest_x; + bounds.y = dest_y; + bounds.width = width; + bounds.height = height; + + /* For each clip rectangle... */ + for (i = 0; i < n_clip_rects; ++i) + { + /* Calculate its intersection with the destination + rectangle. */ + + if (!gui_intersect_rectangles (&clip_rectangles[i], &bounds, + &temp)) + continue; + + /* And that of the destination itself. */ + + if (!flag && !gui_intersect_rectangles (&temp, &rect, &temp)) + continue; + + /* Now perform the copy. */ + (*do_blit) (src_x + temp.x - dest_x, /* temp.x relative to src_x */ + src_y + temp.y - dest_y, /* temp.y relative to src_y */ + temp.width, /* Width of area to copy. */ + temp.height, /* Height of area to copy. */ + temp.x, temp.y, /* Coordinates to copy to. */ + gc, /* GC. */ + src_data, &src_info, /* Source drawable. */ + dest_data, &dest_info, /* Destination drawable. */ + mask_data, &mask_info); /* Mask drawable. */ + } + + /* Now damage the destination drawable accordingly, should it be a + window. */ + + if (android_handles[dest].type == ANDROID_HANDLE_WINDOW) + android_damage_window (dest, &bounds); + + fail2: + if (mask) + { + AndroidBitmap_unlockPixels (android_java_env, mask); + ANDROID_DELETE_LOCAL_REF (mask); + } + fail1: + if (src != dest) + { + AndroidBitmap_unlockPixels (android_java_env, dest_object); + ANDROID_DELETE_LOCAL_REF (dest_object); + } + fail: + AndroidBitmap_unlockPixels (android_java_env, src_object); + ANDROID_DELETE_LOCAL_REF (src_object); } + + void android_free_pixmap (android_pixmap pixmap) { @@ -4815,26 +5625,27 @@ android_wc_lookup_string (android_key_pressed_event *event, /* Low level drawing primitives. */ -/* Lock the bitmap corresponding to the window WINDOW. Return the +/* Lock the bitmap corresponding to the drawable DRAWABLE. Return the bitmap data upon success, and store the bitmap object in BITMAP_RETURN. Value is NULL upon failure. The caller must take care to unlock the bitmap data afterwards. */ unsigned char * -android_lock_bitmap (android_window window, +android_lock_bitmap (android_window drawable, AndroidBitmapInfo *bitmap_info, jobject *bitmap_return) { - jobject drawable, bitmap; + jobject object, bitmap; void *data; - drawable = android_resolve_handle (window, ANDROID_HANDLE_WINDOW); + object = android_resolve_handle2 (drawable, ANDROID_HANDLE_WINDOW, + ANDROID_HANDLE_PIXMAP); /* Look up the drawable and get the bitmap corresponding to it. Then, lock the bitmap's bits. */ bitmap = (*android_java_env)->CallObjectMethod (android_java_env, - drawable, + object, drawable_class.get_bitmap); if (!bitmap) /* NULL is returned when the bitmap does not currently exist due diff --git a/src/android.h b/src/android.h index 62d420d4cce..d0440259161 100644 --- a/src/android.h +++ b/src/android.h @@ -68,7 +68,7 @@ enum android_handle_type extern jobject android_resolve_handle (android_handle, enum android_handle_type); -extern unsigned char *android_lock_bitmap (android_window, +extern unsigned char *android_lock_bitmap (android_drawable, AndroidBitmapInfo *, jobject *); extern void android_damage_window (android_window, diff --git a/src/androidgui.h b/src/androidgui.h index 6db25098398..02cc73809b9 100644 --- a/src/androidgui.h +++ b/src/androidgui.h @@ -137,6 +137,21 @@ struct android_gc /* Current background color. */ unsigned long background; + + /* The function. */ + enum android_gc_function function; + + /* The fill style. */ + enum android_fill_style fill_style; + + /* The clip X and Y origin. */ + int clip_x_origin, clip_y_origin; + + /* The clip mask image and stipple. */ + android_pixmap clip_mask, stipple; + + /* The tile-stipple X and Y origins. */ + int ts_x_origin, ts_y_origin; }; enum android_swap_action -- cgit v1.3 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/debug.sh | 17 +++--- java/org/gnu/emacs/EmacsInputConnection.java | 8 +++ java/org/gnu/emacs/EmacsService.java | 28 ++++++++++ java/org/gnu/emacs/EmacsView.java | 8 ++- src/android.c | 81 ++++++++++++++++++++++------ src/textconv.c | 6 +++ 6 files changed, 126 insertions(+), 22 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/debug.sh b/java/debug.sh index 339b3604810..0458003fe72 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -19,6 +19,7 @@ ## along with GNU Emacs. If not, see . set -m +set -x oldpwd=`pwd` cd `dirname $0` @@ -310,22 +311,26 @@ rm -f /tmp/file-descriptor-stamp if [ -z "$gdbserver" ]; then if [ "$is_root" = "yes" ]; then - adb -s $device shell $gdbserver_bin --once \ + adb -s $device shell $gdbserver_bin --multi \ "+/data/local/tmp/debug.$package.socket" --attach $pid >&5 & gdb_socket="localfilesystem:/data/local/tmp/debug.$package.socket" - else - adb -s $device shell run-as $package $gdbserver_bin --once \ + else + adb -s $device shell run-as $package $gdbserver_bin --multi \ "+debug.$package.socket" --attach $pid >&5 & gdb_socket="localfilesystem:$app_data_dir/debug.$package.socket" fi else # Normally the program cannot access $gdbserver_bin when it is # placed in /data/local/tmp. - adb -s $device shell run-as $package $gdbserver_cmd --once \ - "0.0.0.0:7654" --attach $pid >&5 & - gdb_socket="tcp:7654" + adb -s $device shell run-as $package $gdbserver_cmd --multi \ + "+debug.$package.socket" --attach $pid >&5 & + gdb_socket="localfilesystem:$app_data_dir/debug.$package.socket" fi +# In order to allow adb to forward to the gdbserver socket, make the +# app data directory a+x. +adb -s $device shell run-as $package chmod a+x $app_data_dir + # Wait until gdbserver successfully runs. line= while read -u 5 line; do 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: " diff --git a/src/android.c b/src/android.c index 8cc18787358..9d1399f3fc2 100644 --- a/src/android.c +++ b/src/android.c @@ -6959,8 +6959,11 @@ android_display_toast (const char *text) -/* Whether or not a query is currently being made. */ -static bool android_servicing_query; +/* The thread from which a query against a thread is currently being + made, if any. Value is 0 if no query is in progress, 1 if a query + is being made from the UI thread to the main thread, and 2 if a + query is being made the other way around. */ +static char android_servicing_query; /* Function that is waiting to be run in the Emacs thread. */ static void (*android_query_function) (void *); @@ -7010,7 +7013,37 @@ android_check_query (void) /* Finish the query. */ __atomic_store_n (&android_query_context, NULL, __ATOMIC_SEQ_CST); __atomic_store_n (&android_query_function, NULL, __ATOMIC_SEQ_CST); - __atomic_clear (&android_servicing_query, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_servicing_query, 0, __ATOMIC_SEQ_CST); + + /* Signal completion. */ + sem_post (&android_query_sem); +} + +/* Run the function that the UI thread has asked to run, and then + signal its completion. Do not change `android_servicing_query' + after it completes. */ + +static void +android_answer_query (void) +{ + void (*proc) (void *); + void *closure; + + eassert (__atomic_load_n (&android_servicing_query, __ATOMIC_SEQ_CST) + == 1); + + /* First, load the procedure and closure. */ + __atomic_load (&android_query_context, &closure, __ATOMIC_SEQ_CST); + __atomic_load (&android_query_function, &proc, __ATOMIC_SEQ_CST); + + if (!proc) + return; + + proc (closure); + + /* Finish the query. */ + __atomic_store_n (&android_query_context, NULL, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_query_function, NULL, __ATOMIC_SEQ_CST); /* Signal completion. */ sem_post (&android_query_sem); @@ -7025,18 +7058,23 @@ android_check_query (void) static void android_begin_query (void) { - if (__atomic_test_and_set (&android_servicing_query, - __ATOMIC_SEQ_CST)) + char old; + + /* Load the previous value of `android_servicing_query' and upgrade + it to 2. */ + + old = __atomic_exchange_n (&android_servicing_query, + 2, __ATOMIC_SEQ_CST); + + /* See if a query was previously in progress. */ + if (old == 1) { /* Answer the query that is currently being made. */ assert (android_query_function != NULL); - android_check_query (); - - /* Wait for that query to complete. */ - while (__atomic_load_n (&android_servicing_query, - __ATOMIC_SEQ_CST)) - ;; + android_answer_query (); } + + /* `android_servicing_query' is now 2. */ } /* Notice that a query has stopped. This function may be called from @@ -7045,7 +7083,7 @@ android_begin_query (void) static void android_end_query (void) { - __atomic_clear (&android_servicing_query, __ATOMIC_SEQ_CST); + __atomic_store_n (&android_servicing_query, 0, __ATOMIC_SEQ_CST); } /* Synchronously ask the Emacs thread to run the specified PROC with @@ -7063,6 +7101,7 @@ int android_run_in_emacs_thread (void (*proc) (void *), void *closure) { union android_event event; + char old; event.xaction.type = ANDROID_WINDOW_ACTION; event.xaction.serial = ++event_serial; @@ -7074,10 +7113,13 @@ android_run_in_emacs_thread (void (*proc) (void *), void *closure) __atomic_store_n (&android_query_function, proc, __ATOMIC_SEQ_CST); /* Don't allow deadlocks to happen; make sure the Emacs thread is - not waiting for something to be done. */ + not waiting for something to be done (in that case, + `android_query_context' is 2.) */ - if (__atomic_test_and_set (&android_servicing_query, - __ATOMIC_SEQ_CST)) + old = 0; + if (!__atomic_compare_exchange_n (&android_servicing_query, &old, + 1, false, __ATOMIC_SEQ_CST, + __ATOMIC_SEQ_CST)) { __atomic_store_n (&android_query_context, NULL, __ATOMIC_SEQ_CST); @@ -7098,6 +7140,15 @@ android_run_in_emacs_thread (void (*proc) (void *), void *closure) while (sem_wait (&android_query_sem) < 0) ;; + /* At this point, `android_servicing_query' should either be zero if + the query was answered or two if the main thread has started a + query. */ + + eassert (!__atomic_load_n (&android_servicing_query, + __ATOMIC_SEQ_CST) + || (__atomic_load_n (&android_servicing_query, + __ATOMIC_SEQ_CST) == 2)); + return 0; } diff --git a/src/textconv.c b/src/textconv.c index 26f351dc729..1530cc0ce32 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -792,6 +792,12 @@ really_set_composing_text (struct frame *f, ptrdiff_t position, /* Move the composition overlay. */ sync_overlay (f); + /* If TEXT is empty, remove the composing region. This goes against + the documentation, but is ultimately what programs expect. */ + + if (!SCHARS (text)) + really_finish_composing_text (f); + /* If PT hasn't changed, the conversion region definitely has. Otherwise, redisplay will update the input method instead. */ -- cgit v1.3 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/emacs/EmacsService.java') 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.3 From 740af4668c8d9bc8e4ee1e60ebeb366690fee93e Mon Sep 17 00:00:00 2001 From: Po Lu Date: Sun, 4 Jun 2023 12:04:15 +0800 Subject: Fix input method synchronization problems * java/debug.sh (gdbserver_cmd, is_root): Prefer TCP again. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function `queryAndSpin'. * java/org/gnu/emacs/EmacsService.java (EmacsService) (icBeginSynchronous, icEndSynchronous, viewGetSelection): New synchronization functions. (resetIC, updateCursorAnchorInfo): Call those instead. * java/org/gnu/emacs/EmacsView.java (onCreateInputConnection): Call viewGetSelection. * src/android.c (JNICALL, android_answer_query_spin): New functions. --- java/debug.sh | 13 ++--- java/org/gnu/emacs/EmacsNative.java | 6 ++ java/org/gnu/emacs/EmacsService.java | 103 ++++++++++++++++++++++++++--------- java/org/gnu/emacs/EmacsView.java | 13 +++-- src/android.c | 26 +++++++++ 5 files changed, 123 insertions(+), 38 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/debug.sh b/java/debug.sh index 0458003fe72..d6e439bec90 100755 --- a/java/debug.sh +++ b/java/debug.sh @@ -19,7 +19,6 @@ ## along with GNU Emacs. If not, see . set -m -set -x oldpwd=`pwd` cd `dirname $0` @@ -273,7 +272,7 @@ fi gdbserver_cmd= is_root= if [ -z "$gdbserver" ]; then - gdbserver_bin=/system/bin/gdbserver + gdbserver_bin=/system/bin/gdbserver64 else gdbserver_bin=/data/local/tmp/gdbserver gdbserver_cat="cat $gdbserver_bin | run-as $package sh -c \ @@ -312,12 +311,12 @@ rm -f /tmp/file-descriptor-stamp if [ -z "$gdbserver" ]; then if [ "$is_root" = "yes" ]; then adb -s $device shell $gdbserver_bin --multi \ - "+/data/local/tmp/debug.$package.socket" --attach $pid >&5 & - gdb_socket="localfilesystem:/data/local/tmp/debug.$package.socket" + "0.0.0.0:7564" --attach $pid >&5 & + gdb_socket="tcp:7564" else - adb -s $device shell run-as $package $gdbserver_bin --multi \ - "+debug.$package.socket" --attach $pid >&5 & - gdb_socket="localfilesystem:$app_data_dir/debug.$package.socket" + adb -s $device shell $gdbserver_bin --multi \ + "0.0.0.0:7564" --attach $pid >&5 & + gdb_socket="tcp:7564" fi else # Normally the program cannot access $gdbserver_bin when it is diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index eb75201088b..f03736fe614 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -177,6 +177,12 @@ public final class EmacsNative main thread's looper to respond. */ public static native void endSynchronous (); + /* Prevent deadlocks while reliably allowing queries from the Emacs + thread to the main thread to complete by waiting for a query to + start from the main thread, then answer it; assume that a query + is certain to start shortly. */ + public static native void answerQuerySpin (); + /* Return whether or not KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP and KEYCODE_VOLUME_MUTE should be forwarded to Emacs. */ public static native boolean shouldForwardMultimediaButtons (); diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index dde60e1c5af..6d70536faf0 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -25,6 +25,8 @@ import java.io.UnsupportedEncodingException; import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; + import android.graphics.Matrix; import android.graphics.Point; @@ -106,8 +108,17 @@ public final class EmacsService extends Service performing drawing calls. */ private static final boolean DEBUG_THREADS = false; - /* Whether or not onCreateInputMethod is calling getSelection. */ - public static volatile boolean imSyncInProgress; + /* Atomic integer used for synchronization between + icBeginSynchronous/icEndSynchronous and viewGetSelection. + + Value is 0 if no query is in progress, 1 if viewGetSelection is + being called, and 2 if icBeginSynchronous was called. */ + public static final AtomicInteger servicingQuery; + + static + { + servicingQuery = new AtomicInteger (); + }; /* Return the directory leading to the directory in which native library files are stored on behalf of CONTEXT. */ @@ -658,46 +669,79 @@ public final class EmacsService extends Service EmacsNative.endSynchronous (); } + + + /* IMM functions such as `updateSelection' holds an internal lock + that is also taken before `onCreateInputConnection' (in + EmacsView.java) is called; when that then asks the UI thread for + the current selection, a dead lock results. To remedy this, + reply to any synchronous queries now -- and prohibit more queries + for the duration of `updateSelection' -- if EmacsView may have + been asking for the value of the region. */ + + public static void + icBeginSynchronous () + { + /* Set servicingQuery to 2, so viewGetSelection knows it shouldn't + proceed. */ + + if (servicingQuery.getAndSet (2) == 1) + /* But if viewGetSelection is already in progress, answer it + first. */ + EmacsNative.answerQuerySpin (); + } + + public static void + icEndSynchronous () + { + if (servicingQuery.getAndSet (0) != 2) + throw new RuntimeException ("incorrect value of `servicingQuery': " + + "likely 1"); + } + + public static int[] + viewGetSelection (short window) + { + int[] selection; + + /* See if a query is already in progress from the other + direction. */ + if (!servicingQuery.compareAndSet (0, 1)) + return null; + + /* Now call the regular getSelection. Note that this can't race + with answerQuerySpin, as `android_servicing_query' can never be + 2 when icBeginSynchronous is called, so a query will always be + started. */ + selection = EmacsNative.getSelection (window); + + /* Finally, clear servicingQuery if its value is still 1. If a + query has started from the other side, it ought to be 2. */ + + servicingQuery.compareAndSet (1, 0); + return selection; + } + + + public void updateIC (EmacsWindow window, int newSelectionStart, int newSelectionEnd, int composingRegionStart, int composingRegionEnd) { - boolean wasSynchronous; - if (DEBUG_IC) Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart + " " + newSelectionEnd + " " + composingRegionStart + " " + composingRegionEnd)); - /* `updateSelection' holds an internal lock that is also taken - before `onCreateInputConnection' (in EmacsView.java) is called; - when that then asks the UI thread for the current selection, a - dead lock results. To remedy this, reply to any synchronous - queries now -- and prohibit more queries for the duration of - `updateSelection' -- if EmacsView may have been asking for the - value of the region. */ - - wasSynchronous = false; - if (EmacsService.imSyncInProgress) - { - /* `beginSynchronous' will answer any outstanding queries and - signal that one is now in progress, thereby preventing - `getSelection' from blocking. */ - - EmacsNative.beginSynchronous (); - wasSynchronous = true; - } - + icBeginSynchronous (); window.view.imManager.updateSelection (window.view, newSelectionStart, newSelectionEnd, composingRegionStart, composingRegionEnd); - - if (wasSynchronous) - EmacsNative.endSynchronous (); + icEndSynchronous (); } public void @@ -707,7 +751,10 @@ public final class EmacsService extends Service Log.d (TAG, "resetIC: " + window); window.view.setICMode (icMode); + + icBeginSynchronous (); window.view.imManager.restartInput (window.view); + icEndSynchronous (); } public void @@ -733,11 +780,15 @@ public final class EmacsService extends Service 0); info = builder.build (); + + if (DEBUG_IC) Log.d (TAG, ("updateCursorAnchorInfo: " + x + " " + y + " " + yBaseline + "-" + yBottom)); + icBeginSynchronous (); window.view.imManager.updateCursorAnchorInfo (window.view, info); + icEndSynchronous (); } /* Open a content URI described by the bytes BYTES, a non-terminated diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index bb450bb8e6b..c223dfa7911 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -630,12 +630,11 @@ public final class EmacsView extends ViewGroup /* Obtain the current position of point and set it as the selection. Don't do this under one specific situation: if `android_update_ic' is being called in the main thread, trying - to synchronize with it can cause a dead lock in the IM - manager. */ + to synchronize with it can cause a dead lock in the IM manager. + See icBeginSynchronous in EmacsService.java for more + details. */ - EmacsService.imSyncInProgress = true; - selection = EmacsNative.getSelection (window.handle); - EmacsService.imSyncInProgress = false; + selection = EmacsService.viewGetSelection (window.handle); if (selection != null) Log.d (TAG, "onCreateInputConnection: current selection is: " @@ -664,6 +663,10 @@ public final class EmacsView extends ViewGroup if (inputConnection == null) inputConnection = new EmacsInputConnection (this); + else + /* Reset the composing region, in case there is still composing + text. */ + inputConnection.finishComposingText (); /* Return the input connection. */ return inputConnection; diff --git a/src/android.c b/src/android.c index e74d40a0cdb..f59c0d9e5d2 100644 --- a/src/android.c +++ b/src/android.c @@ -2997,6 +2997,7 @@ NATIVE_NAME (blitRect) (JNIEnv *env, jobject object, static void android_begin_query (void); static void android_end_query (void); +static void android_answer_query_spin (void); JNIEXPORT void JNICALL NATIVE_NAME (beginSynchronous) (JNIEnv *env, jobject object) @@ -3014,6 +3015,14 @@ NATIVE_NAME (endSynchronous) (JNIEnv *env, jobject object) android_end_query (); } +JNIEXPORT void JNICALL +NATIVE_NAME (answerQuerySpin) (JNIEnv *env, jobject object) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + android_answer_query_spin (); +} + #ifdef __clang__ #pragma clang diagnostic pop #else @@ -7041,6 +7050,23 @@ android_answer_query (void) sem_post (&android_query_sem); } +/* Like `android_answer_query'. However, the query may not have + begun; spin until it has. */ + +static void +android_answer_query_spin (void) +{ + int n; + + while (!(n = __atomic_load_n (&android_servicing_query, + __ATOMIC_SEQ_CST))) + eassert (!n); + + /* Note that this function is supposed to be called before + `android_begin_query' starts, so clear the service flag. */ + android_check_query (); +} + /* Notice that the Emacs thread will start blocking waiting for a response from the UI thread. Process any pending queries from the UI thread. -- cgit v1.3 From 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/emacs/EmacsService.java') 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.3 From 63339a9577f085074d16fa8c56ddd56864c94dda Mon Sep 17 00:00:00 2001 From: Po Lu Date: Wed, 7 Jun 2023 11:03:56 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (beginBatchEdit) (endBatchEdit, commitCompletion, commitText, deleteSurroundingText) (finishComposingText, getSelectedText, getTextAfterCursor) (getTextBeforeCursor, setComposingText, setComposingRegion) (performEditorAction, performContextMenuAction, getExtractedText) (setSelection, sendKeyEvent, deleteSurroundingTextInCodePoints) (requestCursorUpdates): Ensure that the input connection is up to date. (getSurroundingText): New function. * java/org/gnu/emacs/EmacsNative.java (getSurroundingText): Export new C function. * java/org/gnu/emacs/EmacsService.java (resetIC): Invalidate previously created input connections. * java/org/gnu/emacs/EmacsView.java (EmacsView) (onCreateInputConnection): Signify that input connections are now up to date. * src/androidterm.c (struct android_get_surrounding_text_context): New structure. (android_get_surrounding_text, NATIVE_NAME): * src/textconv.c (get_surrounding_text): * src/textconv.h: New functions. --- java/org/gnu/emacs/EmacsInputConnection.java | 105 ++++++++++++++++++++ java/org/gnu/emacs/EmacsNative.java | 4 + java/org/gnu/emacs/EmacsService.java | 1 + java/org/gnu/emacs/EmacsView.java | 13 +++ src/androidterm.c | 143 +++++++++++++++++++++++++++ src/textconv.c | 100 +++++++++++++++++++ src/textconv.h | 4 + 7 files changed, 370 insertions(+) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 9ced7cb7aaf..73c93c67ac7 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -23,7 +23,9 @@ import android.view.inputmethod.BaseInputConnection; import android.view.inputmethod.CompletionInfo; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.SurroundingText; import android.view.inputmethod.TextSnapshot; + import android.view.KeyEvent; import android.os.Build; @@ -88,6 +90,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean beginBatchEdit () { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "beginBatchEdit"); @@ -99,6 +105,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean endBatchEdit () { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "endBatchEdit"); @@ -110,6 +120,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean commitCompletion (CompletionInfo info) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "commitCompletion: " + info); @@ -125,6 +139,10 @@ public final class EmacsInputConnection extends BaseInputConnection { int[] selection; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "commitText: " + text + " " + newCursorPosition); @@ -156,6 +174,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean deleteSurroundingText (int leftLength, int rightLength) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, ("deleteSurroundingText: " + leftLength + " " + rightLength)); @@ -169,6 +191,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean finishComposingText () { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "finishComposingText"); @@ -180,6 +206,10 @@ public final class EmacsInputConnection extends BaseInputConnection public String getSelectedText (int flags) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getSelectedText: " + flags); @@ -192,6 +222,10 @@ public final class EmacsInputConnection extends BaseInputConnection { String string; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); @@ -210,6 +244,10 @@ public final class EmacsInputConnection extends BaseInputConnection { String string; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); @@ -226,6 +264,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean setComposingText (CharSequence text, int newCursorPosition) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, ("setComposingText: " + text + " ## " + newCursorPosition)); @@ -239,6 +281,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean setComposingRegion (int start, int end) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "setComposingRegion: " + start + " " + end); @@ -250,6 +296,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean performEditorAction (int editorAction) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "performEditorAction: " + editorAction); @@ -263,6 +313,10 @@ public final class EmacsInputConnection extends BaseInputConnection { int action; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "performContextMenuAction: " + contextMenuAction); @@ -310,6 +364,10 @@ public final class EmacsInputConnection extends BaseInputConnection ExtractedText text; int[] selection; + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + if (EmacsService.DEBUG_IC) Log.d (TAG, "getExtractedText: " + request.hintMaxChars + ", " + request.hintMaxLines + " " + flags); @@ -360,6 +418,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean setSelection (int start, int end) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "setSelection: " + start + " " + end); @@ -371,6 +433,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean sendKeyEvent (KeyEvent key) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "sendKeyEvent: " + key); @@ -381,6 +447,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean deleteSurroundingTextInCodePoints (int beforeLength, int afterLength) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + /* This can be implemented the same way as deleteSurroundingText. */ return this.deleteSurroundingText (beforeLength, afterLength); @@ -390,6 +460,10 @@ public final class EmacsInputConnection extends BaseInputConnection public boolean requestCursorUpdates (int cursorUpdateMode) { + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return false; + if (EmacsService.DEBUG_IC) Log.d (TAG, "requestCursorUpdates: " + cursorUpdateMode); @@ -397,6 +471,37 @@ public final class EmacsInputConnection extends BaseInputConnection return true; } + @Override + public SurroundingText + getSurroundingText (int beforeLength, int afterLength, + int flags) + { + SurroundingText text; + + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + + if (EmacsService.DEBUG_IC) + Log.d (TAG, ("getSurroundingText: " + beforeLength + ", " + + afterLength)); + + text = EmacsNative.getSurroundingText (windowHandle, beforeLength, + afterLength, flags); + + if (text != null) + Log.d (TAG, ("getSurroundingText: " + + text.getSelectionStart () + + "," + + text.getSelectionEnd () + + "+" + + text.getOffset () + + ": " + + text.getText ())); + + return text; + } + /* Override functions which are not implemented. */ diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index cb1c6caa79a..cb89cf6808a 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -25,6 +25,7 @@ import android.graphics.Bitmap; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; +import android.view.inputmethod.SurroundingText; public final class EmacsNative { @@ -222,6 +223,9 @@ public final class EmacsNative public static native void requestSelectionUpdate (short window); public static native void requestCursorUpdates (short window, int mode); public static native void clearInputFlags (short window); + public static native SurroundingText getSurroundingText (short window, + int left, int right, + int flags); /* Return the current value of the selection, or -1 upon diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 2074a5b7c2b..48e39f8b355 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -748,6 +748,7 @@ public final class EmacsService extends Service window.view.setICMode (icMode); icBeginSynchronous (); + window.view.icGeneration++; window.view.imManager.restartInput (window.view); icEndSynchronous (); } diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index a78dec08839..d432162132d 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -111,6 +111,13 @@ public final class EmacsView extends ViewGroup details. */ private int icMode; + /* The number of calls to `resetIC' to have taken place the last + time an InputConnection was created. */ + public long icSerial; + + /* The number of calls to `recetIC' that have taken place. */ + public volatile long icGeneration; + public EmacsView (EmacsWindow window) { @@ -627,6 +634,12 @@ public final class EmacsView extends ViewGroup return null; } + /* Set icSerial. If icSerial < icGeneration, the input connection + has been reset, and future input should be ignored until a new + connection is created. */ + + icSerial = icGeneration; + /* Reset flags set by the previous input method. */ EmacsNative.clearInputFlags (window.handle); diff --git a/src/androidterm.c b/src/androidterm.c index 2a054715d6a..77f2bd1c7a0 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -5636,6 +5636,149 @@ NATIVE_NAME (clearInputFlags) (JNIEnv *env, jobject object, android_write_event (&event); } + + +/* Context for a call to `getSurroundingText'. */ + +struct android_get_surrounding_text_context +{ + /* Number of characters before the region to return. */ + int before_length; + + /* Number of characters after the region to return. */ + int after_length; + + /* The returned text, or NULL. */ + char *text; + + /* The size of that text in characters and bytes. */ + ptrdiff_t length, bytes; + + /* Offsets into that text. */ + ptrdiff_t offset, start, end; + + /* The window. */ + android_window window; +}; + +/* Return the surrounding text in the surrounding text context + specified by DATA. */ + +static void +android_get_surrounding_text (void *data) +{ + struct android_get_surrounding_text_context *request; + struct frame *f; + ptrdiff_t temp; + + request = data; + + /* Find the frame associated with the window. */ + f = android_window_to_frame (NULL, request->window); + + if (!f) + return; + + /* Now get the surrounding text. */ + request->text + = get_surrounding_text (f, request->before_length, + request->after_length, &request->length, + &request->bytes, &request->offset, + &request->start, &request->end); + + /* Sort request->start and request->end for compatibility with some + bad input methods. */ + + if (request->end < request->start) + { + temp = request->start; + request->start = request->end; + request->end = temp; + } +} + +JNIEXPORT jobject JNICALL +NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, + jshort window, jint before_length, + jint after_length, jint flags) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + static jclass class; + static jmethodID constructor; + + struct android_get_surrounding_text_context context; + jstring string; + jobject object; + + /* Initialize CLASS if it has not yet been initialized. */ + + if (!class) + { + class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/SurroundingText")); + +#if __ANDROID_API__ < 31 + /* If CLASS cannot be found, the version of Android currently + running is too old. */ + + if (!class) + { + (*env)->ExceptionClear (env); + return NULL; + } +#else /* __ANDROID_API__ >= 31 */ + assert (class); +#endif /* __ANDROID_API__ < 31 */ + + class = (*env)->NewGlobalRef (env, class); + if (!class) + return NULL; + + /* Now look for its constructor. */ + constructor = (*env)->GetMethodID (env, class, "", + "(Ljava/lang/CharSequence;III)V"); + assert (constructor); + } + + context.before_length = before_length; + context.after_length = after_length; + context.window = window; + context.text = NULL; + + android_sync_edit (); + if (android_run_in_emacs_thread (android_get_surrounding_text, + &context)) + return NULL; + + if (!context.text) + return NULL; + + /* Encode the returned text. */ + string = android_text_to_string (env, context.text, context.length, + context.bytes); + free (context.text); + + if (!string) + return NULL; + + /* Create an SurroundingText object containing this information. */ + object = (*env)->NewObject (env, class, constructor, string, + (jint) min (context.start, + TYPE_MAXIMUM (jint)), + (jint) min (context.end, + TYPE_MAXIMUM (jint)), + /* Adjust point offsets to fit into + Android's 0-based indexing. */ + (jint) min (context.offset - 1, + TYPE_MAXIMUM (jint))); + if (!object) + return NULL; + + return object; +} + #ifdef __clang__ #pragma clang diagnostic pop #else diff --git a/src/textconv.c b/src/textconv.c index 0dcf5bdcea8..1161b781b6a 100644 --- a/src/textconv.c +++ b/src/textconv.c @@ -1732,6 +1732,106 @@ get_extracted_text (struct frame *f, ptrdiff_t n, return buffer; } +/* Return the text between the positions PT - LEFT and PT + RIGHT. If + the mark is active, return the range of text relative to the bounds + of the region instead. + + Set *LENGTH to the number of characters returned, *BYTES to the + number of bytes returned, *OFFSET to the character position of the + returned text, and *START_RETURN and *END_RETURN to the mark and + point relative to that position. */ + +char * +get_surrounding_text (struct frame *f, ptrdiff_t left, + ptrdiff_t right, ptrdiff_t *length, + ptrdiff_t *bytes, ptrdiff_t *offset, + ptrdiff_t *start_return, + ptrdiff_t *end_return) +{ + specpdl_ref count; + ptrdiff_t start, end, start_byte, end_byte, mark, temp; + char *buffer; + + if (!WINDOW_LIVE_P (f->old_selected_window)) + return NULL; + + /* Save the excursion, as there will be extensive changes to the + selected window. */ + count = SPECPDL_INDEX (); + record_unwind_protect_excursion (); + + /* Inhibit quitting. */ + specbind (Qinhibit_quit, Qt); + + /* Temporarily switch to F's selected window at the time of the last + redisplay. */ + select_window (f->old_selected_window, Qt); + buffer = NULL; + + /* Figure out the bounds of the text to return. */ + + /* First, obtain start and end. */ + end = get_mark (); + start = PT; + + /* If the mark is not active, make it start and end. */ + + if (end == -1) + end = start; + + /* Now sort start and end. */ + + if (end < start) + { + temp = start; + start = end; + end = temp; + } + + /* And subtract left and right. */ + + if (INT_SUBTRACT_WRAPV (start, left, &start) + || INT_ADD_WRAPV (end, right, &end)) + goto finish; + + start = max (start, BEGV); + end = min (end, ZV); + + /* Detect overflow. */ + + if (!(start <= PT && PT <= end)) + goto finish; + + /* Convert the character positions to byte positions. */ + start_byte = CHAR_TO_BYTE (start); + end_byte = CHAR_TO_BYTE (end); + + /* Extract the text from the buffer. */ + buffer = xmalloc (end_byte - start_byte); + copy_buffer (start, start_byte, end, end_byte, + buffer); + + /* Get the mark. If it's not active, use PT. */ + + mark = get_mark (); + + if (mark == -1) + mark = PT; + + /* Return the offsets. Unlike `get_extracted_text', this need not + sort mark and point. */ + + *offset = start; + *start_return = mark - start; + *end_return = PT - start; + *length = end - start; + *bytes = end_byte - start_byte; + + finish: + unbind_to (count, Qnil); + return buffer; +} + /* Return whether or not text conversion is temporarily disabled. `reset' should always call this to determine whether or not to disable the input method. */ diff --git a/src/textconv.h b/src/textconv.h index 339cefdba92..7550388a723 100644 --- a/src/textconv.h +++ b/src/textconv.h @@ -143,6 +143,10 @@ extern void textconv_barrier (struct frame *, unsigned long); extern char *get_extracted_text (struct frame *, ptrdiff_t, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, ptrdiff_t *, bool *); +extern char *get_surrounding_text (struct frame *, ptrdiff_t, + ptrdiff_t, ptrdiff_t *, + ptrdiff_t *, ptrdiff_t *, + ptrdiff_t *, ptrdiff_t *); extern bool conversion_disabled_p (void); extern void register_textconv_interface (struct textconv_interface *); -- cgit v1.3 From c321eea5af535d102c5e1d2d7e95029ce6fee427 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 9 Jun 2023 14:03:50 +0800 Subject: Block profiling signals in the Android UI thread * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New function `setupSystemThread'. * java/org/gnu/emacs/EmacsService.java (onCreate): Block all signals except for SIGBUS and SIGSEGV in the UI thread. * src/android.c (setupSystemThread): New function. --- java/org/gnu/emacs/EmacsNative.java | 4 ++++ java/org/gnu/emacs/EmacsService.java | 25 +++++++++++++++++++++++++ src/android.c | 24 ++++++++++++++++++++++++ 3 files changed, 53 insertions(+) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index cb89cf6808a..2fcbf8b94ef 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -188,6 +188,10 @@ public final class EmacsNative KEYCODE_VOLUME_MUTE should be forwarded to Emacs. */ public static native boolean shouldForwardMultimediaButtons (); + /* Initialize the current thread, by blocking signals that do not + interest it. */ + public static native void setupSystemThread (); + /* Input connection functions. These mostly correspond to their diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 48e39f8b355..96216e51cf4 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -25,6 +25,7 @@ import java.io.UnsupportedEncodingException; import java.util.List; +import java.util.concurrent.Semaphore; import java.util.concurrent.atomic.AtomicInteger; import android.graphics.Matrix; @@ -211,6 +212,7 @@ public final class EmacsService extends Service final String filesDir, libDir, cacheDir, classPath; final double pixelDensityX; final double pixelDensityY; + final Semaphore signalSemaphore; SERVICE = this; handler = new Handler (Looper.getMainLooper ()); @@ -220,6 +222,7 @@ public final class EmacsService extends Service pixelDensityX = metrics.xdpi; pixelDensityY = metrics.ydpi; resolver = getContentResolver (); + signalSemaphore = new Semaphore (0); try { @@ -248,11 +251,33 @@ public final class EmacsService extends Service cacheDir, (float) pixelDensityX, (float) pixelDensityY, classPath, EmacsService.this); + + /* Wait for the signal mask to be set up in the UI + thread. */ + + while (true) + { + try + { + signalSemaphore.acquire (); + break; + } + catch (InterruptedException e) + { + ;; + } + } } }, extraStartupArgument, /* If any file needs to be opened, open it now. */ EmacsOpenActivity.fileToOpen); thread.start (); + + /* Now that the thread has been started, block signals which + don't interest the current thread. */ + + EmacsNative.setupSystemThread (); + signalSemaphore.release (); } catch (IOException exception) { diff --git a/src/android.c b/src/android.c index 92aab548180..681723124ee 100644 --- a/src/android.c +++ b/src/android.c @@ -3077,6 +3077,30 @@ NATIVE_NAME (answerQuerySpin) (JNIEnv *env, jobject object) android_answer_query_spin (); } + + +/* System thread setup. Android doesn't always block signals Emacs is + interested in from being received by the UI or render threads, + which can lead to problems when those signals then interrupt one of + those threads. */ + +JNIEXPORT void JNICALL +NATIVE_NAME (setupSystemThread) (void) +{ + sigset_t sigset; + + /* Block everything except for SIGSEGV and SIGBUS; those two are + used by the runtime. */ + + sigfillset (&sigset); + sigaddset (&sigset, SIGSEGV); + sigaddset (&sigset, SIGBUS); + + if (pthread_sigmask (SIG_BLOCK, &sigset, NULL)) + __android_log_print (ANDROID_LOG_WARN, __func__, + "pthread_sigmask: %s", strerror (errno)); +} + #ifdef __clang__ #pragma clang diagnostic pop #else -- cgit v1.3 From 363e293cc919ab02c40bd9a8fa4875c2e5644b2d Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 15 Jun 2023 12:36:50 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsInputConnection.java (EmacsInputConnection, beginBatchEdit, reset, endBatchEdit): Keep track of the number of batch edits and return an appropriate value. (takeSnapshot): Implement function. * java/org/gnu/emacs/EmacsNative.java (takeSnapshot): New function. * java/org/gnu/emacs/EmacsService.java (resetIC): Improve debugging output. * java/org/gnu/emacs/EmacsView.java (onCreateInputConnection): Call `reset' to clear the UI side batch edit count. * src/androidterm.c (struct android_get_surrounding_text_context): New fields `conversion_start' and `conversion_end'. (android_get_surrounding_text): Return the conversion region. (android_get_surrounding_text_internal, NATIVE_NAME): Factor out `getSurroundingText'. (takeSnapshot): New function. --- java/org/gnu/emacs/EmacsInputConnection.java | 66 +++++++++++--- java/org/gnu/emacs/EmacsNative.java | 2 + java/org/gnu/emacs/EmacsService.java | 25 +++++- java/org/gnu/emacs/EmacsView.java | 3 + src/androidterm.c | 129 +++++++++++++++++++++++++-- 5 files changed, 201 insertions(+), 24 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java index 1bcc9a62a81..f8dce5dfa79 100644 --- a/java/org/gnu/emacs/EmacsInputConnection.java +++ b/java/org/gnu/emacs/EmacsInputConnection.java @@ -50,6 +50,11 @@ public final class EmacsInputConnection implements InputConnection /* The handle ID associated with that view's window. */ private short windowHandle; + /* Number of batch edits currently underway. Used to avoid + synchronizing with the Emacs thread after each + `endBatchEdit'. */ + private int batchEditCount; + /* Whether or not to synchronize and call `updateIC' with the selection position after committing text. @@ -110,6 +115,10 @@ public final class EmacsInputConnection implements InputConnection Log.d (TAG, "beginBatchEdit"); EmacsNative.beginBatchEdit (windowHandle); + + /* Keep a record of the number of outstanding batch edits here as + well. */ + batchEditCount++; return true; } @@ -125,7 +134,14 @@ public final class EmacsInputConnection implements InputConnection Log.d (TAG, "endBatchEdit"); EmacsNative.endBatchEdit (windowHandle); - return true; + + /* Subtract one from the UI thread record of the number of batch + edits currently under way. */ + + if (batchEditCount > 0) + batchEditCount -= 1; + + return batchEditCount > 0; } public boolean @@ -584,21 +600,50 @@ public final class EmacsInputConnection implements InputConnection return text; } - - /* Override functions which are not implemented. */ - @Override - public Handler - getHandler () + public TextSnapshot + takeSnapshot () { - return null; + TextSnapshot snapshot; + + /* Return if the input connection is out of date. */ + if (view.icSerial < view.icGeneration) + return null; + + snapshot = EmacsNative.takeSnapshot (windowHandle); + + if (EmacsService.DEBUG_IC) + Log.d (TAG, ("takeSnapshot: " + + snapshot.getSurroundingText ().getText () + + " @ " + snapshot.getCompositionEnd () + + ", " + snapshot.getCompositionStart ())); + + return snapshot; } @Override public void closeConnection () { + batchEditCount = 0; + } + + + public void + reset () + { + batchEditCount = 0; + } + + + /* Override functions which are not implemented. */ + + @Override + public Handler + getHandler () + { + return null; } @Override @@ -616,13 +661,6 @@ public final class EmacsInputConnection implements InputConnection return false; } - @Override - public TextSnapshot - takeSnapshot () - { - return null; - } - @Override public boolean clearMetaKeyStates (int states) diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 2fcbf8b94ef..9e87c419f95 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -26,6 +26,7 @@ import android.graphics.Bitmap; import android.view.inputmethod.ExtractedText; import android.view.inputmethod.ExtractedTextRequest; import android.view.inputmethod.SurroundingText; +import android.view.inputmethod.TextSnapshot; public final class EmacsNative { @@ -230,6 +231,7 @@ public final class EmacsNative public static native SurroundingText getSurroundingText (short window, int left, int right, int flags); + public static native TextSnapshot takeSnapshot (short window); /* Return the current value of the selection, or -1 upon diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index 96216e51cf4..2fe4e8c4146 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -767,8 +767,31 @@ public final class EmacsService extends Service public void resetIC (EmacsWindow window, int icMode) { + int oldMode; + if (DEBUG_IC) - Log.d (TAG, "resetIC: " + window); + Log.d (TAG, "resetIC: " + window + ", " + icMode); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU + && (oldMode = window.view.getICMode ()) == icMode + /* Don't do this if there is currently no input + connection. */ + && oldMode != IC_MODE_NULL) + { + if (DEBUG_IC) + Log.d (TAG, "resetIC: calling invalidateInput"); + + /* Android 33 and later allow the IM reset to be optimized out + and replaced by a call to `invalidateInput', which is much + faster, as it does not involve resetting the input + connection. */ + + icBeginSynchronous (); + window.view.imManager.invalidateInput (window.view); + icEndSynchronous (); + + return; + } window.view.setICMode (icMode); diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 278c6025902..aba1184b0c2 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -681,6 +681,9 @@ public final class EmacsView extends ViewGroup if (inputConnection == null) inputConnection = new EmacsInputConnection (this); + else + /* Clear several pieces of state in the input connection. */ + inputConnection.reset (); /* Return the input connection. */ return inputConnection; diff --git a/src/androidterm.c b/src/androidterm.c index 191ff65199b..29076981a47 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -5668,6 +5668,10 @@ struct android_get_surrounding_text_context /* Offsets into that text. */ ptrdiff_t offset, start, end; + /* The start and end indices of the conversion region. + -1 if it does not exist. */ + ptrdiff_t conversion_start, conversion_end; + /* The window. */ android_window window; }; @@ -5706,22 +5710,47 @@ android_get_surrounding_text (void *data) request->start = request->end; request->end = temp; } + + /* Retrieve the conversion region. */ + + request->conversion_start = -1; + request->conversion_end = -1; + + if (MARKERP (f->conversion.compose_region_start)) + { + request->conversion_start + = marker_position (f->conversion.compose_region_start) - 1; + request->conversion_end + = marker_position (f->conversion.compose_region_end) - 1; + } } -JNIEXPORT jobject JNICALL -NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, - jshort window, jint before_length, - jint after_length, jint flags) -{ - JNI_STACK_ALIGNMENT_PROLOGUE; +/* Return a local reference to a `SurroundingText' object describing + WINDOW's surrounding text. ENV should be a valid JNI environment + for the current thread. - static jclass class; - static jmethodID constructor; + BEFORE_LENGTH and AFTER_LENGTH specify the number of characters + around point and mark to return. + Return the conversion region (or -1) in *CONVERSION_START and + *CONVERSION_END if non-NULL. + + Value is the object upon success, else NULL. */ + +static jobject +android_get_surrounding_text_internal (JNIEnv *env, jshort window, + jint before_length, + jint after_length, + ptrdiff_t *conversion_start, + ptrdiff_t *conversion_end) +{ struct android_get_surrounding_text_context context; jstring string; jobject object; + static jclass class; + static jmethodID constructor; + /* Initialize CLASS if it has not yet been initialized. */ if (!class) @@ -5745,7 +5774,9 @@ NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, class = (*env)->NewGlobalRef (env, class); if (!class) - return NULL; + /* Clear class to prevent a local reference from remaining in + `class'. */ + return (class = NULL); /* Now look for its constructor. */ constructor = (*env)->GetMethodID (env, class, "", @@ -5787,6 +5818,86 @@ NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject ignored_object, if (!object) return NULL; + /* Now return the conversion region if that was requested. */ + + if (conversion_start) + { + *conversion_start = context.conversion_start; + *conversion_end = context.conversion_start; + } + + return object; +} + +JNIEXPORT jobject JNICALL +NATIVE_NAME (getSurroundingText) (JNIEnv *env, jobject object, + jshort window, jint before_length, + jint after_length, jint flags) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + return android_get_surrounding_text_internal (env, window, before_length, + after_length, NULL, NULL); +} + +JNIEXPORT jobject JNICALL +NATIVE_NAME (takeSnapshot) (JNIEnv *env, jobject object, jshort window) +{ + JNI_STACK_ALIGNMENT_PROLOGUE; + + jobject text; + ptrdiff_t start, end; + + static jclass class; + static jmethodID constructor; + + /* First, obtain the surrounding text and conversion region. */ + text = android_get_surrounding_text_internal (env, window, 600, 600, + &start, &end); + + /* If that fails, return NULL. */ + + if (!text) + return NULL; + + /* Next, initialize the TextSnapshot class. */ + + if (!class) + { + class + = (*env)->FindClass (env, ("android/view/inputmethod" + "/TextSnapshot")); +#if __ANDROID_API__ < 33 + /* If CLASS cannot be found, the version of Android currently + running is too old. */ + + if (!class) + { + (*env)->ExceptionClear (env); + return NULL; + } +#else /* __ANDROID_API__ >= 33 */ + assert (class); +#endif /* __ANDROID_API__ < 33 */ + + class = (*env)->NewGlobalRef (env, class); + if (!class) + /* Clear class to prevent a local reference from remaining in + `class'. */ + return (class = NULL); + + constructor = (*env)->GetMethodID (env, class, "", + "(Landroid/view/inputmethod" + "/SurroundingText;III)V"); + assert (constructor); + } + + /* Try to create a TextSnapshot object. */ + eassert (start <= end); + object = (*env)->NewObject (env, class, constructor, text, + (jint) min (start, TYPE_MAXIMUM (jint)), + (jint) min (end, TYPE_MAXIMUM (jint)), + (jint) 0); return object; } -- cgit v1.3 From 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 ------------------------- src/android.c | 23 ++++++++++++++++++++++- 3 files changed, 30 insertions(+), 26 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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) { diff --git a/src/android.c b/src/android.c index ccc2da95f03..d6a56dfe0b8 100644 --- a/src/android.c +++ b/src/android.c @@ -261,6 +261,14 @@ void *unused_pointer; #endif /* __i386__ */ +/* Whether or not the default signal mask has been changed. If so, + the signal mask must be restored before calling + android_emacs_init. */ +static bool signal_mask_changed_p; + +/* The signal mask at the time Emacs was started. */ +static sigset_t startup_signal_mask; + /* Event handling functions. Events are stored on a (circular) queue @@ -2562,7 +2570,15 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, ANDROID_DELETE_LOCAL_REF (argv); ANDROID_DELETE_LOCAL_REF (dump_file_object); + /* Restore the signal mask at the time of startup if it was changed + to block unwanted signals from reaching system threads. */ + + if (signal_mask_changed_p) + pthread_sigmask (SIG_SETMASK, &startup_signal_mask, NULL); + + /* Now start Emacs proper. */ android_emacs_init (nelements, c_argv, dump_file); + /* android_emacs_init should never return. */ emacs_abort (); } @@ -3128,9 +3144,14 @@ NATIVE_NAME (setupSystemThread) (void) sigdelset (&sigset, SIGSEGV); sigdelset (&sigset, SIGBUS); - if (pthread_sigmask (SIG_BLOCK, &sigset, NULL)) + /* Save the signal mask that was previously used. It will be + restored in `initEmacs'. */ + + if (pthread_sigmask (SIG_BLOCK, &sigset, &startup_signal_mask)) __android_log_print (ANDROID_LOG_WARN, __func__, "pthread_sigmask: %s", strerror (errno)); + else + signal_mask_changed_p = true; } #ifdef __clang__ -- cgit v1.3 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/emacs/EmacsService.java') 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.3 From 75db4511704284a739c93895d7a31478b1a71181 Mon Sep 17 00:00:00 2001 From: Po Lu Date: Thu, 6 Jul 2023 09:35:27 +0800 Subject: Update Android port * java/org/gnu/emacs/EmacsNative.java (scaledDensity): Announce new argument `scaledDensity'. * java/org/gnu/emacs/EmacsNoninteractive.java (main): Specify new argument. * java/org/gnu/emacs/EmacsService.java (onCreate): Compute an adjusted DPI for the font size based on the ratio between density and scaledDensity. (run): Specify that adjusted density. * src/android.c (setEmacsParams): Set `android_scaled_pixel_density'. * src/android.h: (android_scaled_pixel_density: New variable. * src/androidterm.c (android_term_init): Set `font_resolution'. * src/androidterm.h (struct android_display_info): New field. * src/font.c (font_pixel_size, font_find_for_lface) (font_open_for_lface, Ffont_face_attributes, Fopen_font): Use FRAME_RES instead of FRAME_RES_Y. * src/frame.h (FRAME_RES): New macro. Use this to convert between font point and pixel sizes as opposed to FRAME_RES_Y. * src/w32font.c (fill_in_logfont): * src/xfaces.c (Fx_family_fonts, set_lface_from_font): Use FRAME_RES instead of FRAME_RES_Y. (bug#64444) --- java/org/gnu/emacs/EmacsNative.java | 4 ++++ java/org/gnu/emacs/EmacsNoninteractive.java | 2 +- java/org/gnu/emacs/EmacsService.java | 5 +++++ src/android.c | 6 ++++++ src/android.h | 1 + src/androidterm.c | 5 ++--- src/androidterm.h | 5 +++++ src/font.c | 12 ++++++------ src/frame.h | 18 ++++++++++++++++-- src/w32font.c | 2 +- src/xfaces.c | 4 ++-- 11 files changed, 49 insertions(+), 15 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index 9e87c419f95..5b8b0f1eae5 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -60,6 +60,9 @@ public final class EmacsNative pixelDensityX and pixelDensityY are the DPI values that will be used by Emacs. + scaledDensity is the DPI value used to translate point sizes to + pixel sizes when loading fonts. + classPath must be the classpath of this app_process process, or NULL. @@ -70,6 +73,7 @@ public final class EmacsNative String cacheDir, float pixelDensityX, float pixelDensityY, + float scaledDensity, String classPath, EmacsService emacsService); diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index aaba74d877c..aa6fa41ba97 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -190,7 +190,7 @@ public final class EmacsNoninteractive EmacsNative.setEmacsParams (assets, filesDir, libDir, cacheDir, 0.0f, - 0.0f, null, null); + 0.0f, 0.0f, null, null); /* Now find the dump file that Emacs should use, if it has already been dumped. */ diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index e1fd2dbffda..37048960f25 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -211,6 +211,7 @@ public final class EmacsService extends Service final String filesDir, libDir, cacheDir, classPath; final double pixelDensityX; final double pixelDensityY; + final double scaledDensity; SERVICE = this; handler = new Handler (Looper.getMainLooper ()); @@ -219,6 +220,9 @@ public final class EmacsService extends Service metrics = getResources ().getDisplayMetrics (); pixelDensityX = metrics.xdpi; pixelDensityY = metrics.ydpi; + scaledDensity = ((metrics.scaledDensity + / metrics.density) + * pixelDensityX); resolver = getContentResolver (); try @@ -247,6 +251,7 @@ public final class EmacsService extends Service EmacsNative.setEmacsParams (manager, filesDir, libDir, cacheDir, (float) pixelDensityX, (float) pixelDensityY, + (float) scaledDensity, classPath, EmacsService.this); } }, extraStartupArgument, diff --git a/src/android.c b/src/android.c index 2a2d134c3c8..a6bc8217820 100644 --- a/src/android.c +++ b/src/android.c @@ -198,6 +198,10 @@ char *android_class_path; /* The display's pixel densities. */ double android_pixel_density_x, android_pixel_density_y; +/* The display pixel density used to convert between point and pixel + font sizes. */ +double android_scaled_pixel_density; + /* The Android application data directory. */ static char *android_files_dir; @@ -2000,6 +2004,7 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jobject cache_dir, jfloat pixel_density_x, jfloat pixel_density_y, + jfloat scaled_density, jobject class_path, jobject emacs_service_object) { @@ -2021,6 +2026,7 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, android_pixel_density_x = pixel_density_x; android_pixel_density_y = pixel_density_y; + android_scaled_pixel_density = scaled_density; __android_log_print (ANDROID_LOG_INFO, __func__, "Initializing "PACKAGE_STRING"...\nPlease report bugs to " diff --git a/src/android.h b/src/android.h index 8634ba01a3d..33ca379e6ba 100644 --- a/src/android.h +++ b/src/android.h @@ -58,6 +58,7 @@ extern int android_fclose (FILE *); extern const char *android_get_home_directory (void); extern double android_pixel_density_x, android_pixel_density_y; +extern double android_scaled_pixel_density; enum android_handle_type { diff --git a/src/androidterm.c b/src/androidterm.c index aed8e24b9fa..20a8860a913 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -6295,11 +6295,10 @@ android_term_init (void) dpyinfo->color_map = color_map; #ifndef ANDROID_STUBIFY - dpyinfo->resx = android_pixel_density_x; dpyinfo->resy = android_pixel_density_y; - -#endif + dpyinfo->font_resolution = android_scaled_pixel_density; +#endif /* ANDROID_STUBIFY */ /* https://lists.gnu.org/r/emacs-devel/2015-11/msg00194.html */ dpyinfo->smallest_font_height = 1; diff --git a/src/androidterm.h b/src/androidterm.h index e3738fb2192..e75d46b1dfb 100644 --- a/src/androidterm.h +++ b/src/androidterm.h @@ -65,6 +65,11 @@ struct android_display_info /* DPI of the display. */ double resx, resy; + /* DPI used to convert font point sizes into pixel dimensions. + This is resy adjusted by a fixed scaling factor specified by + the user. */ + double font_resolution; + /* Scratch GC for drawing a cursor in a non-default face. */ struct android_gc *scratch_cursor_gc; diff --git a/src/font.c b/src/font.c index 9dcafb3bb33..7f8ddc4dc34 100644 --- a/src/font.c +++ b/src/font.c @@ -348,7 +348,7 @@ font_pixel_size (struct frame *f, Lisp_Object spec) if (FIXNUMP (val)) dpi = XFIXNUM (val); else - dpi = FRAME_RES_Y (f); + dpi = FRAME_RES (f); pixel_size = POINT_TO_PIXEL (point_size, dpi); return pixel_size; } @@ -3023,7 +3023,7 @@ font_find_for_lface (struct frame *f, Lisp_Object *attrs, Lisp_Object spec, int { double pt = XFIXNUM (attrs[LFACE_HEIGHT_INDEX]); - pixel_size = POINT_TO_PIXEL (pt / 10, FRAME_RES_Y (f)); + pixel_size = POINT_TO_PIXEL (pt / 10, FRAME_RES (f)); if (pixel_size < 1) pixel_size = 1; } @@ -3175,13 +3175,13 @@ font_open_for_lface (struct frame *f, Lisp_Object entity, Lisp_Object *attrs, Li } pt /= 10; - size = POINT_TO_PIXEL (pt, FRAME_RES_Y (f)); + size = POINT_TO_PIXEL (pt, FRAME_RES (f)); #ifdef HAVE_NS if (size == 0) { Lisp_Object ffsize = get_frame_param (f, Qfontsize); size = (NUMBERP (ffsize) - ? POINT_TO_PIXEL (XFLOATINT (ffsize), FRAME_RES_Y (f)) + ? POINT_TO_PIXEL (XFLOATINT (ffsize), FRAME_RES (f)) : 0); } #endif @@ -4082,7 +4082,7 @@ are to be displayed on. If omitted, the selected frame is used. */) if (FIXNUMP (val)) { Lisp_Object font_dpi = AREF (font, FONT_DPI_INDEX); - int dpi = FIXNUMP (font_dpi) ? XFIXNUM (font_dpi) : FRAME_RES_Y (f); + int dpi = FIXNUMP (font_dpi) ? XFIXNUM (font_dpi) : FRAME_RES (f); plist[n++] = QCheight; plist[n++] = make_fixnum (PIXEL_TO_POINT (XFIXNUM (val) * 10, dpi)); } @@ -4986,7 +4986,7 @@ DEFUN ("open-font", Fopen_font, Sopen_font, 1, 3, 0, { CHECK_NUMBER (size); if (FLOATP (size)) - isize = POINT_TO_PIXEL (XFLOAT_DATA (size), FRAME_RES_Y (f)); + isize = POINT_TO_PIXEL (XFLOAT_DATA (size), FRAME_RES (f)); else if (! integer_to_intmax (size, &isize)) args_out_of_range (font_entity, size); if (! (INT_MIN <= isize && isize <= INT_MAX)) diff --git a/src/frame.h b/src/frame.h index 8ed9c0f37d8..62fc63c7c1f 100644 --- a/src/frame.h +++ b/src/frame.h @@ -981,12 +981,26 @@ default_pixels_per_inch_y (void) #define FRAME_RES_Y(f) \ (eassert (FRAME_WINDOW_P (f)), FRAME_DISPLAY_INFO (f)->resy) +#ifdef HAVE_ANDROID + +/* Android systems use a font scaling factor independent from the + display DPI. */ + +#define FRAME_RES(f) \ + (eassert (FRAME_WINDOW_P (f)), \ + FRAME_DISPLAY_INFO (f)->font_resolution) + +#else /* !HAVE_ANDROID */ +#define FRAME_RES(f) (FRAME_RES_Y (f)) +#endif /* HAVE_ANDROID */ + #else /* !HAVE_WINDOW_SYSTEM */ /* Defaults when no window system available. */ -#define FRAME_RES_X(f) default_pixels_per_inch_x () -#define FRAME_RES_Y(f) default_pixels_per_inch_y () +#define FRAME_RES_X(f) default_pixels_per_inch_x () +#define FRAME_RES_Y(f) default_pixels_per_inch_y () +#define FRAME_RES(f) default_pixels_per_inch_y () #endif /* HAVE_WINDOW_SYSTEM */ diff --git a/src/w32font.c b/src/w32font.c index 2917fa55f9f..0371b24e1d1 100644 --- a/src/w32font.c +++ b/src/w32font.c @@ -2031,7 +2031,7 @@ static void fill_in_logfont (struct frame *f, LOGFONT *logfont, Lisp_Object font_spec) { Lisp_Object tmp, extra; - int dpi = FRAME_RES_Y (f); + int dpi = FRAME_RES (f); tmp = AREF (font_spec, FONT_DPI_INDEX); if (FIXNUMP (tmp)) diff --git a/src/xfaces.c b/src/xfaces.c index af3428ad995..30487c0e8fb 100644 --- a/src/xfaces.c +++ b/src/xfaces.c @@ -1612,7 +1612,7 @@ the face font sort order, see `face-font-selection-order'. */) { Lisp_Object font = AREF (vec, i); int point = PIXEL_TO_POINT (XFIXNUM (AREF (font, FONT_SIZE_INDEX)) * 10, - FRAME_RES_Y (f)); + FRAME_RES (f)); Lisp_Object spacing = Ffont_get (font, QCspacing); Lisp_Object v = CALLN (Fvector, AREF (font, FONT_FAMILY_INDEX), @@ -2173,7 +2173,7 @@ set_lface_from_font (struct frame *f, Lisp_Object lface, if (force_p || UNSPECIFIEDP (LFACE_HEIGHT (lface))) { - int pt = PIXEL_TO_POINT (font->pixel_size * 10, FRAME_RES_Y (f)); + int pt = PIXEL_TO_POINT (font->pixel_size * 10, FRAME_RES (f)); eassert (pt > 0); ASET (lface, LFACE_HEIGHT_INDEX, make_fixnum (pt)); -- cgit v1.3 From 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 +++--- src/androidterm.c | 35 +++++++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 9 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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 diff --git a/src/androidterm.c b/src/androidterm.c index 20a8860a913..466a99a1e28 100644 --- a/src/androidterm.c +++ b/src/androidterm.c @@ -774,6 +774,11 @@ android_handle_ime_event (union android_event *event, struct frame *f) } } + + +/* Forward declaration. */ +static void android_notify_conversion (unsigned long); + static int handle_one_android_event (struct android_display_info *dpyinfo, union android_event *event, int *finish, @@ -1659,8 +1664,21 @@ handle_one_android_event (struct android_display_info *dpyinfo, case ANDROID_INPUT_METHOD: if (!any) - /* Free any text allocated for this event. */ - xfree (event->ime.text); + { + /* Free any text allocated for this event. */ + xfree (event->ime.text); + + /* If edits associated with this event haven't been + processed yet, signal their completion to avoid delays + the next time a call to `android_sync_edit' is made. + + If events for a deleted frame are interleaved with events + for another frame, the edit counter may be prematurely + incremented before edits associated with the other frames + are processed. This is not a problem in practice. */ + + android_notify_conversion (event->ime.counter); + } else android_handle_ime_event (event, any); @@ -4585,10 +4603,12 @@ static void android_sync_edit (void) { struct timespec start, end, rem; + unsigned long counter; + + counter = __atomic_load_n (&last_edit_counter, + __ATOMIC_SEQ_CST); - if (__atomic_load_n (&last_edit_counter, - __ATOMIC_SEQ_CST) - == edit_counter) + if (counter == edit_counter) return; start = current_timespec (); @@ -5618,7 +5638,10 @@ NATIVE_NAME (requestCursorUpdates) (JNIEnv *env, jobject object, event.ime.length = mode; event.ime.position = 0; event.ime.text = NULL; - event.ime.counter = ++edit_counter; + + /* Since this does not affect the state of the buffer text, there is + no need to apply synchronization to this event. */ + event.ime.counter = 0; android_write_event (&event); } -- cgit v1.3 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/emacs/EmacsService.java') 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.3 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/emacs/EmacsService.java') 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.3 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/emacs/EmacsService.java') 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.3 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 +++++++++++++++++++++++++----------- lisp/net/browse-url.el | 19 +++++++++-- src/android.c | 16 +++++---- src/android.h | 2 +- src/androidselect.c | 16 +++++---- 5 files changed, 82 insertions(+), 36 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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) diff --git a/lisp/net/browse-url.el b/lisp/net/browse-url.el index 8036bbc8acc..daa46baf8f2 100644 --- a/lisp/net/browse-url.el +++ b/lisp/net/browse-url.el @@ -1307,18 +1307,31 @@ Default to the URL around or before point." (function-put 'browse-url-default-haiku-browser 'browse-url-browser-kind 'external) +(defcustom browse-url-android-share nil + "If non-nil, share URLs instead of opening them. +When non-nil, `browse-url-default-android-browser' will try to +share the URL being browsed through programs such as mail clients +and instant messengers instead of opening it in a web browser." + :type 'boolean + :version "30.1") + (declare-function android-browse-url "androidselect.c") ;;;###autoload (defun browse-url-default-android-browser (url &optional _new-window) "Browse URL with the system default browser. -Default to the URL around or before point." +If `browse-url-android-share' is non-nil, try to share URL using +an external program instead. Default to the URL around or before +point." (interactive (browse-url-interactive-arg "URL: ")) - (setq url (browse-url-encode-url url)) + (unless browse-url-android-share + ;; The URL shouldn't be encoded if it's being shared through + ;; another program. + (setq url (browse-url-encode-url url))) ;; Make sure the URL starts with an appropriate scheme. (unless (string-match "\\(.+\\):/" url) (setq url (concat "http://" url))) - (android-browse-url url)) + (android-browse-url url browse-url-android-share)) (function-put 'browse-url-default-android-browser 'browse-url-browser-kind 'external) diff --git a/src/android.c b/src/android.c index 5850b6079c9..4e9897f4648 100644 --- a/src/android.c +++ b/src/android.c @@ -2286,7 +2286,7 @@ android_init_emacs_service (void) FIND_METHOD (get_screen_height, "getScreenHeight", "(Z)I"); FIND_METHOD (detect_mouse, "detectMouse", "()Z"); FIND_METHOD (name_keysym, "nameKeysym", "(I)Ljava/lang/String;"); - FIND_METHOD (browse_url, "browseUrl", "(Ljava/lang/String;)" + FIND_METHOD (browse_url, "browseUrl", "(Ljava/lang/String;Z)" "Ljava/lang/String;"); FIND_METHOD (restart_emacs, "restartEmacs", "()V"); FIND_METHOD (update_ic, "updateIC", @@ -6959,12 +6959,15 @@ android_project_image_nearest (struct android_image *image, /* Other miscellaneous functions. */ -/* Ask the system to start browsing the specified encoded URL. Upon - failure, return a string describing the error. Else, value is - nil. */ +/* Ask the system to start browsing the specified URL. Upon failure, + return a string describing the error. Else, value is nil. URL + should be encoded unless SEND. + + If SEND, open the URL with applications that can ``send'' or + ``share'' the URL (through mail, for example.) */ Lisp_Object -android_browse_url (Lisp_Object url) +android_browse_url (Lisp_Object url, Lisp_Object send) { jobject value, string; Lisp_Object tem; @@ -6974,7 +6977,8 @@ android_browse_url (Lisp_Object url) value = (*android_java_env)->CallObjectMethod (android_java_env, emacs_service, service_class.browse_url, - string); + string, + (jboolean) !NILP (send)); android_exception_check (); ANDROID_DELETE_LOCAL_REF (string); diff --git a/src/android.h b/src/android.h index 33ca379e6ba..2323690fa25 100644 --- a/src/android.h +++ b/src/android.h @@ -178,7 +178,7 @@ struct android_battery_state int temperature; }; -extern Lisp_Object android_browse_url (Lisp_Object); +extern Lisp_Object android_browse_url (Lisp_Object, Lisp_Object); extern int android_query_battery (struct android_battery_state *); extern void android_display_toast (const char *); diff --git a/src/androidselect.c b/src/androidselect.c index d1f2ebb52f9..f5371280457 100644 --- a/src/androidselect.c +++ b/src/androidselect.c @@ -232,11 +232,15 @@ DEFUN ("android-clipboard-exists-p", Fandroid_clipboard_exists_p, } DEFUN ("android-browse-url", Fandroid_browse_url, - Sandroid_browse_url, 1, 1, 0, - doc: /* Start the system web browser. -Then, point the web browser to URL, which should be a URL-encoded -URL with a scheme specified. Signal an error upon failure. */) - (Lisp_Object url) + Sandroid_browse_url, 1, 2, 0, + doc: /* Open URL in an external application. URL should be a +URL-encoded URL with a scheme specified unless SEND is non-nil. +Signal an error upon failure. + +If SEND is nil, start a program that is able to display the URL, such +as a web browser. Otherwise, try to share URL using programs such as +email clients. */) + (Lisp_Object url, Lisp_Object send) { Lisp_Object value; @@ -244,7 +248,7 @@ URL with a scheme specified. Signal an error upon failure. */) error ("No Android display connection!"); CHECK_STRING (url); - value = android_browse_url (url); + value = android_browse_url (url, send); /* Signal an error upon failure. */ if (!NILP (value)) -- cgit v1.3 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 -------------------- lisp/touch-screen.el | 25 ++++++++++++++++++++++--- src/android.c | 2 -- 3 files changed, 22 insertions(+), 25 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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) diff --git a/lisp/touch-screen.el b/lisp/touch-screen.el index ef9b14f27ce..68a7e213cdb 100644 --- a/lisp/touch-screen.el +++ b/lisp/touch-screen.el @@ -25,6 +25,9 @@ ;; This file provides code to recognize simple touch screen gestures. ;; It is used on X and Android, where the platform cannot recognize ;; them for us. +;; +;; See (elisp)Touchscreen Events for a description of the details of touch +;; events. ;;; Code: @@ -468,7 +471,7 @@ is not read-only." (posn (cdr point)) window point) (cond ((null what) (when (windowp (posn-window posn)) - (setq point (posn-point point) + (setq point (posn-point posn) window (posn-window posn)) ;; Select the window that was tapped. (select-window window) @@ -670,12 +673,18 @@ returned may contain any one of the following events: where WINDOW specifies a window to scroll, and DX and DY are integers describing how many pixels to be scrolled horizontally -and vertically. +and vertically, + + (touchscreen-hold POSN) + (touchscreen-drag POSN) + +where POSN is the position of the long-press or touchpoint +motion, (down-mouse-1 POSN) (drag-mouse-1 POSN) -where POSN is the position of the mouse button press or click. +where POSN is the position of the mouse button press or click, (mouse-1 POSN) (mouse-2 POSN) @@ -744,6 +753,16 @@ if POSN is on a link or a button, or `mouse-1' otherwise." (define-key function-key-map [mode-line touchscreen-end] #'touch-screen-translate-touch) +;; These are used to translate events sent from the internal border +;; or from outside the frame. + +(define-key function-key-map [nil touchscreen-begin] + #'touch-screen-translate-touch) +(define-key function-key-map [nil touchscreen-update] + #'touch-screen-translate-touch) +(define-key function-key-map [nil touchscreen-end] + #'touch-screen-translate-touch) + (define-key function-key-map [header-line touchscreen-begin] #'touch-screen-translate-touch) (define-key function-key-map [header-line touchscreen-update] diff --git a/src/android.c b/src/android.c index acc49219b35..90288737c77 100644 --- a/src/android.c +++ b/src/android.c @@ -2330,8 +2330,6 @@ android_init_emacs_service (void) FIND_METHOD (check_content_uri, "checkContentUri", "([BZZ)Z"); FIND_METHOD (query_battery, "queryBattery", "()[J"); - FIND_METHOD (display_toast, "displayToast", - "(Ljava/lang/String;)V"); FIND_METHOD (update_extracted_text, "updateExtractedText", "(Lorg/gnu/emacs/EmacsWindow;" "Landroid/view/inputmethod/ExtractedText;I)V"); -- cgit v1.3 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. --- configure.ac | 4 +- doc/emacs/android.texi | 64 +- doc/emacs/emacs.texi | 1 + java/Makefile.in | 9 +- 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 ++++- lib-src/asset-directory-tool.c | 7 +- src/android.c | 1801 ++------ src/android.h | 96 +- src/androidfns.c | 31 +- src/androidvfs.c | 6137 +++++++++++++++++++++++++++ src/callproc.c | 2 +- src/charset.c | 2 +- src/dired.c | 2 +- src/emacs.c | 2 +- src/fileio.c | 96 +- src/filelock.c | 22 +- src/image.c | 6 +- src/keyboard.c | 4 +- src/lisp.h | 8 + src/lread.c | 2 +- src/process.c | 10 + src/sysdep.c | 82 +- src/term.c | 4 +- 26 files changed, 7839 insertions(+), 1677 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsDirectoryEntry.java create mode 100644 src/androidvfs.c (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/configure.ac b/configure.ac index 8d54ea0de16..ff9f604221f 100644 --- a/configure.ac +++ b/configure.ac @@ -2689,8 +2689,8 @@ for Android, but all API calls need to be stubbed out]) # sfntfont-android.o. ANDROID_OBJ="$ANDROID_OBJ sfnt.o sfntfont.o sfntfont-android.o" - # Build androidselect.o. - ANDROID_OBJ="$ANDROID_OBJ androidselect.o" + # Build androidselect.o and androidvfs.o. + ANDROID_OBJ="$ANDROID_OBJ androidselect.o androidvfs.o" # Check for some functions not always present in the NDK. AC_CHECK_DECLS([android_get_device_api_level]) diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 86af851efd2..1f32fdfc1d2 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -17,6 +17,7 @@ about using such devices with Emacs, @pxref{Other Input Devices}. * What is Android?:: Preamble. * Android Startup:: Starting up Emacs on Android. * Android File System:: The Android file system. +* Android Document Providers:: Accessing files from other programs. * Android Environment:: Running Emacs under Android. * Android Windowing:: The Android window system. * Android Fonts:: Font selection under Android. @@ -142,12 +143,12 @@ it starts Emacs and gives it the file to open as an argument. Note that if that Emacs in turn does not start the Emacs server, subsequent attempts to open the file with the wrapper will fail. -@cindex /content directory, android +@cindex /content/by-authority directory, android Some files are given to Emacs as ``content identifiers'' that the system provides access to outside the normal filesystem APIs. Emacs -uses a pseudo-directory named @file{/content} to access those files. -Do not make any assumptions about the contents of this directory, or -try to open files in it yourself. +uses a pseudo-directory named @file{/content/by-authority} to access +those files. Do not make any assumptions about the contents of this +directory, or try to open files in it yourself. This feature is not provided on Android 4.3 and earlier, in which case such files are copied to a temporary directory before being @@ -180,12 +181,13 @@ that result from such an implementation: @item Subprocesses (such as @command{ls}) can not run from the @file{/assets} directory; if you try to run a subprocess with -@code{current-directory} set to @file{/assets} or a subdirectory -thereof, it will run from the home directory instead. +@code{current-directory} set to @file{/assets}, +@file{/content/storage} or a subdirectory thereof, it will run from +the home directory instead. @item There are no @file{.} and @file{..} directories inside the -@file{/assets} directory. +@file{/assets} or @file{/content} directory. @item Files in the @file{/assets} directory are always read only, and may be @@ -193,7 +195,7 @@ read in to memory more than once each time they are opened. @end itemize Aside from the @file{/assets} directory, Android programs normally -have access to three other directories. They are: +have access to four other directories. They are: @itemize @bullet @item @@ -209,6 +211,12 @@ contains utility executables alongside Emacs itself. The @dfn{external storage} directory. This is accessible to Emacs when the user grants the ``Files and Media'' permission to Emacs via system settings. + +@item +Directories provided by @dfn{document providers} on Android 5.0 and +later. These directories exist outside the normal Unix filesystem, +containing files provided by external programs (@pxref{Android +Document Providers}.) @end itemize The external storage directory is found at @file{/sdcard}. The @@ -265,6 +273,46 @@ files under @file{/sdcard} as usual. These settings are not present on many proprietary versions of Android. +@node Android Document Providers +@section Accessing files from other programs under Android +@cindex document providers, Android +@cindex /content/storage directory, Android + + Android 5.0 introduces a new sort of program, the ``document +provider'': these programs are small programs that provide access to +their own files outside both the asset manager and the Unix +filesystem. Emacs supports accessing files and directories they +provide, placing their files within the directory +@file{/content/storage}. + +@findex android-request-directory-access + Before Emacs is granted access to any of these directories, it must +first request the right to access it. This is done by running the +command (@pxref{M-x}) @code{android-request-directory-access}, which +displays a file selection dialog. + + If a directory is selected within this dialog, its contents are +subsequently made available within a new directory named +@file{/content/storage/@var{authority}/@var{id}}, where +@var{authority} is the name of the document provider, and @var{id} is +a unique identifier assigned to the directory by the document +provider. + + Because these directories do not exist within the Unix file-system, +sub-processes cannot be created within them, just as with the +@file{/assets} directory (@pxref{Android File System}.) In addition, +although Emacs can normally write and create files inside these +directories, it cannot create symlinks or hard links. + +@c TODO: fix this! + Since document providers are allowed to perform expensive network +operations to obtain file contents, a file access operation within one +of these directories will possibly take a significant amount of time. +Emacs presently does not support quitting out of such file system +operations, and the timeouts applied are fully subject to the +discretion of the system and the document provider that is responding +to these operations. + @node Android Environment @section Running Emacs under Android diff --git a/doc/emacs/emacs.texi b/doc/emacs/emacs.texi index b255e679d5f..7a21eb49e24 100644 --- a/doc/emacs/emacs.texi +++ b/doc/emacs/emacs.texi @@ -1267,6 +1267,7 @@ Emacs and Android * What is Android?:: Preamble. * Android Startup:: Starting up Emacs on Android. * Android File System:: The Android file system. +* Android Document Providers:: Accessing files from other programs. * Android Environment:: Running Emacs under Android. * Android Windowing:: The Android window system. * Android Fonts:: Font selection under Android. diff --git a/java/Makefile.in b/java/Makefile.in index d11278e6110..804d4669c7a 100644 --- a/java/Makefile.in +++ b/java/Makefile.in @@ -233,7 +233,8 @@ ifneq ($(NDK_BUILD_SHARED),) endif install_temp/assets/directory-tree: $(libsrc)/asset-directory-tool \ - install_temp install_temp/assets/version + install_temp install_temp/assets/version \ + install_temp/assets/build_info $(AM_V_GEN) $(libsrc)/asset-directory-tool install_temp/assets \ install_temp/assets/directory-tree @@ -246,9 +247,9 @@ install_temp/assets/version: install_temp install_temp/assets/build_info: install_temp $(AM_V_GEN) { hostname; date +%s; } > $@ -emacs.apk-in: install_temp install_temp/assets/directory-tree \ - install_temp/assets/version install_temp/assets/build_info \ - AndroidManifest.xml +emacs.apk-in: install_temp install_temp/assets/directory-tree \ + AndroidManifest.xml install_temp/assets/version \ + install_temp/assets/build_info # Package everything. Specifying the assets on this command line is # necessary for AAssetManager_getNextFileName to work on old versions # of Android. Make sure not to generate R.java, as it's already been 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; + } }; diff --git a/lib-src/asset-directory-tool.c b/lib-src/asset-directory-tool.c index 239ab083b66..99c954a3922 100644 --- a/lib-src/asset-directory-tool.c +++ b/lib-src/asset-directory-tool.c @@ -45,7 +45,12 @@ along with GNU Emacs. If not, see . */ the first file or directory, a NULL byte and an unsigned int indicating the offset from the start of the file to the start of the next sibling. Following that is a list of subdirectories or - files in the same format. The long is stored LSB first. */ + files in the same format. The long is stored LSB first. + + Directories can be distinguished from ordinary files through the + last bytes of their file names (immediately previous to their + terminating NULL bytes), which are set to the directory separator + character `/'. */ diff --git a/src/android.c b/src/android.c index 6fcaa40b2a9..b1d7b75c129 100644 --- a/src/android.c +++ b/src/android.c @@ -29,6 +29,7 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include #include #include @@ -56,17 +57,9 @@ bool android_init_gui; #ifndef ANDROID_STUBIFY -#if __ANDROID_API__ >= 9 -#include -#include -#else -#include "android-asset.h" -#endif - #include #include -#include #include #include @@ -78,50 +71,6 @@ bool android_init_gui; #define ANDROID_THROW(env, class, msg) \ ((*(env))->ThrowNew ((env), (*(env))->FindClass ((env), class), msg)) -#define ANDROID_MAX_ASSET_FD 65535 - -struct android_fd_table_entry -{ - /* Various flags associated with this table. */ - short flags; - - /* The stat buffer associated with this entry. */ - struct stat statb; -}; - -enum android_fd_table_entry_flags - { - ANDROID_FD_TABLE_ENTRY_IS_VALID = 1, - }; - -struct android_emacs_service -{ - jclass class; - jmethodID fill_rectangle; - jmethodID fill_polygon; - jmethodID draw_rectangle; - jmethodID draw_line; - jmethodID draw_point; - jmethodID clear_window; - jmethodID clear_area; - jmethodID ring_bell; - jmethodID query_tree; - jmethodID get_screen_width; - jmethodID get_screen_height; - jmethodID detect_mouse; - jmethodID name_keysym; - jmethodID browse_url; - jmethodID restart_emacs; - jmethodID update_ic; - jmethodID reset_ic; - jmethodID open_content_uri; - jmethodID check_content_uri; - jmethodID query_battery; - jmethodID display_toast; - jmethodID update_extracted_text; - jmethodID update_cursor_anchor_info; -}; - struct android_emacs_pixmap { jclass class; @@ -174,12 +123,6 @@ struct android_emacs_cursor /* The API level of the current device. */ static int android_api_level; -/* The asset manager being used. */ -static AAssetManager *asset_manager; - -/* Whether or not Emacs has been initialized. */ -static int emacs_initialized; - /* The directory used to store site-lisp. */ char *android_site_load_path; @@ -206,11 +149,6 @@ double android_scaled_pixel_density; /* The Android application data directory. */ static char *android_files_dir; -/* Array of structures used to hold asset information corresponding to - a file descriptor. This assumes Emacs does not do funny things - with dup. It currently does not. */ -static struct android_fd_table_entry android_table[ANDROID_MAX_ASSET_FD]; - /* The Java environment being used for the main thread. */ JNIEnv *android_java_env; @@ -235,10 +173,10 @@ static jclass android_rect_class; static jmethodID android_rect_constructor; /* The EmacsService object. */ -static jobject emacs_service; +jobject emacs_service; /* Various methods associated with the EmacsService. */ -static struct android_emacs_service service_class; +struct android_emacs_service service_class; /* Various methods associated with the EmacsPixmap class. */ static struct android_emacs_pixmap pixmap_class; @@ -881,224 +819,6 @@ android_run_debug_thread (void *data) -/* Asset directory handling functions. ``directory-tree'' is a file in - the root of the assets directory describing its contents. - - See lib-src/asset-directory-tool for more details. */ - -/* The Android directory tree. */ -static const char *directory_tree; - -/* The size of the directory tree. */ -static size_t directory_tree_size; - -/* Read an unaligned (32-bit) long from the address POINTER. */ - -static unsigned int -android_extract_long (char *pointer) -{ - unsigned int number; - - memcpy (&number, pointer, sizeof number); - return number; -} - -/* Scan to the file FILE in the asset directory tree. Return a - pointer to the end of that file (immediately before any children) - in the directory tree, or NULL if that file does not exist. - - If returning non-NULL, also return the offset to the end of the - last subdirectory or file in *LIMIT_RETURN. LIMIT_RETURN may be - NULL. - - FILE must have less than 11 levels of nesting. If it ends with a - trailing slash, then NULL will be returned if it is not actually a - directory. */ - -static const char * -android_scan_directory_tree (char *file, size_t *limit_return) -{ - char *token, *saveptr, *copy, *copy1, *start, *max, *limit; - size_t token_length, ntokens, i; - char *tokens[10]; - - USE_SAFE_ALLOCA; - - /* Skip past the 5 byte header. */ - start = (char *) directory_tree + 5; - - /* Figure out the current limit. */ - limit = (char *) directory_tree + directory_tree_size; - - /* Now, split `file' into tokens, with the delimiter being the file - name separator. Look for the file and seek past it. */ - - ntokens = 0; - saveptr = NULL; - copy = copy1 = xstrdup (file); - memset (tokens, 0, sizeof tokens); - - while ((token = strtok_r (copy, "/", &saveptr))) - { - copy = NULL; - - /* Make sure ntokens is within bounds. */ - if (ntokens == ARRAYELTS (tokens)) - { - xfree (copy1); - goto fail; - } - - tokens[ntokens] = SAFE_ALLOCA (strlen (token) + 1); - memcpy (tokens[ntokens], token, strlen (token) + 1); - ntokens++; - } - - /* Free the copy created for strtok_r. */ - xfree (copy1); - - /* If there are no tokens, just return the start of the directory - tree. */ - - if (!ntokens) - { - SAFE_FREE (); - - /* Return the size of the directory tree as the limit. - Do not subtract the initial header bytes, as the limit - is an offset from the start of the file. */ - - if (limit_return) - *limit_return = directory_tree_size; - - return start; - } - - /* Loop through tokens, indexing the directory tree each time. */ - - for (i = 0; i < ntokens; ++i) - { - token = tokens[i]; - - /* Figure out how many bytes to compare. */ - token_length = strlen (token); - - again: - - /* If this would be past the directory, return NULL. */ - if (start + token_length > limit) - goto fail; - - /* Now compare the file name. */ - if (!memcmp (start, token, token_length)) - { - /* They probably match. Find the NULL byte. It must be - either one byte past start + token_length, with the last - byte a trailing slash (indicating that it is a - directory), or just start + token_length. Return 4 bytes - past the next NULL byte. */ - - max = memchr (start, 0, limit - start); - - if (max != start + token_length - && !(max == start + token_length + 1 - && *(max - 1) == '/')) - goto false_positive; - - /* Return it if it exists and is in range, and this is the - last token. Otherwise, set it as start and the limit as - start + the offset and continue the loop. */ - - if (max && max + 5 <= limit) - { - if (i < ntokens - 1) - { - start = max + 5; - limit = ((char *) directory_tree - + android_extract_long (max + 1)); - - /* Make sure limit is still in range. */ - if (limit > directory_tree + directory_tree_size - || start > directory_tree + directory_tree_size) - goto fail; - - continue; - } - - /* Now see if max is not a directory and file is. If - file is a directory, then return NULL. */ - if (*(max - 1) != '/' && file[strlen (file) - 1] == '/') - max = NULL; - else - { - /* Figure out the limit. */ - if (limit_return) - *limit_return = android_extract_long (max + 1); - - /* Go to the end of this file. */ - max += 5; - } - - SAFE_FREE (); - return max; - } - - /* Return NULL otherwise. */ - __android_log_print (ANDROID_LOG_WARN, __func__, - "could not scan to end of directory tree" - ": %s", file); - goto fail; - } - - false_positive: - - /* No match was found. Set start to the next sibling and try - again. */ - - start = memchr (start, 0, limit - start); - - if (!start || start + 5 > limit) - goto fail; - - start = ((char *) directory_tree - + android_extract_long (start + 1)); - - /* Make sure start is still in bounds. */ - - if (start > limit) - goto fail; - - /* Continue the loop. */ - goto again; - } - - fail: - SAFE_FREE (); - return NULL; -} - -/* Return whether or not the directory tree entry DIR is a - directory. - - DIR should be a value returned by - `android_scan_directory_tree'. */ - -static bool -android_is_directory (const char *dir) -{ - /* If the directory is the directory tree, then it is a - directory. */ - if (dir == directory_tree + 5) - return true; - - /* Otherwise, look 5 bytes behind. If it is `/', then it is a - directory. */ - return (dir - 6 >= directory_tree - && *(dir - 6) == '/'); -} - - - /* Intercept USER_FULL_NAME and return something that makes sense if pw->pw_gecos is NULL. */ @@ -1158,46 +878,106 @@ android_is_special_directory (const char *name, const char *dir) } } -/* Given a real file name, return the part that describes its asset - path, or NULL if it is not an asset. +#if 0 - If FILENAME contains only `/assets', return `/' to indicate the - root of the assets hierarchy. */ +/* URL-encode N bytes of the specified STRING into at most N bytes of + BUFFER; STRING is assumed to be encoded in a `utf-8-emacs' + compatible coding system. Value is the number of bytes encoded + (excluding the trailing null byte placed at the end of the encoded + text) or -1 upon failure. */ -static const char * -android_get_asset_name (const char *filename) +static ssize_t +android_url_encode (const char *restrict string, size_t length, + char *restrict buffer, size_t n) { - const char *name; + int len, character; + size_t num_encoded; + char *end; + char format[1 + 25]; - name = android_is_special_directory (filename, "/assets"); + /* For each multibyte character... */ - if (!name) - return NULL; + end = string + length; + num_encoded = 0; + + while (string < end) + { + /* XXX: Android documentation claims that URIs is encoded + according to the ``Unicode'' scheme, but what this means in + reality is that the URI is encoded in UTF-8, and then + each of its bytes are encoded. */ + /* Find the length of the multibyte character at STRING. */ + len = /* multibyte_length (string, end, true, true) */ 1; + + /* 0 means that STRING is not a valid multibyte string. */ + if (!len || string + len > end) + goto failure; + + /* Now fetch the character and increment string. */ + /* character = /\* STRING_CHAR ((unsigned char *) string) *\/; */ + character = *(unsigned char *) string; + string += len; + + /* If CHARACTER is not a letter or an unreserved character, + escape it. */ + + if (!((character >= 'A' + && character <= 'Z') + || (character >= 'a' + && character <= 'z') + || (character >= '0' + && character <= '9') + || character == '_' + || character == '-' + || character == '!' + || character == '.' + || character == '~' + || character == '\'' + || character == '(' + || character == ')' + || character == '*')) + { + len = sprintf (format, "%%%X", (unsigned int) character); + if (len < 0) + goto failure; - /* If NAME is empty, return /. Otherwise, return the name relative - to /assets/. */ + /* See if there is enough space left to hold the encoded + string. */ - if (*name) - return name; + if (n < len) + goto failure; - return "/"; -} + n -= len; + num_encoded += len; -/* Return whether or not the specified FILENAME actually resolves to a - content resolver URI. */ + /* Copy the encoded string to STRING. */ + memcpy (buffer, format, n); + buffer += len; + } + else + { + /* No more space within BUFFER. */ + if (!n) + goto failure; -static bool -android_content_name_p (const char *filename) -{ - /* Content URIs aren't supported before Android 4.4, so return - false. */ + /* Don't encode this ASCII character; just store it. */ + n--, num_encoded++; + *(buffer++) = character; + } + } + + /* If there's no space for a trailing null byte or more bytes have + been encoded than representable in ssize_t, fail. */ - if (android_api_level < 19) - return false; + if (!n || num_encoded > SSIZE_MAX) + goto failure; + + /* Store the terminating NULL byte. */ + *buffer = '\0'; + return num_encoded; - return (android_is_special_directory (filename, - "/content") - != NULL); + failure: + return -1; } /* Return the content URI corresponding to a `/content' file name, @@ -1209,10 +989,9 @@ static const char * android_get_content_name (const char *filename) { static char buffer[PATH_MAX + 1]; - char *head, *token, *saveptr, *copy; - size_t n; - - n = PATH_MAX; + char *head, *token, *next, *saveptr, *copy, *mark, *mark1; + ssize_t rc; + size_t n, length; /* Find the file name described if it starts with `/content'. If just the directory is described, return content://. */ @@ -1229,743 +1008,148 @@ android_get_content_name (const char *filename) URI. */ copy = xstrdup (filename); - token = saveptr = NULL; + mark = saveptr = NULL; head = stpcpy (buffer, "content:/"); /* Split FILENAME by slashes. */ - while ((token = strtok_r (!token ? copy : NULL, - "/", &saveptr))) - { - head = stpncpy (head, "/", n--); - head = stpncpy (head, token, n); - - /* Check that head has not overflown the buffer. */ - eassert ((head - buffer) <= PATH_MAX); - - n = PATH_MAX - (head - buffer); - } - - /* Make sure the given buffer ends up NULL terminated. */ - buffer[PATH_MAX] = '\0'; - xfree (copy); - - return buffer; -} - -/* Return whether or not the specified FILENAME is an accessible - content URI. MODE specifies what to check. */ - -static bool -android_check_content_access (const char *filename, int mode) -{ - const char *name; - jobject string; - size_t length; - jboolean rc; - - name = android_get_content_name (filename); - length = strlen (name); - - string = (*android_java_env)->NewByteArray (android_java_env, - length); - android_exception_check (); - - (*android_java_env)->SetByteArrayRegion (android_java_env, - string, 0, length, - (jbyte *) name); - rc = (*android_java_env)->CallBooleanMethod (android_java_env, - emacs_service, - service_class.check_content_uri, - string, - (jboolean) ((mode & R_OK) - != 0), - (jboolean) ((mode & W_OK) - != 0)); - android_exception_check_1 (string); - ANDROID_DELETE_LOCAL_REF (string); - - return rc; -} - -/* Like fstat. However, look up the asset corresponding to the file - descriptor. If it exists, return the right information. */ - -int -android_fstat (int fd, struct stat *statb) -{ - if (fd < ANDROID_MAX_ASSET_FD - && (android_table[fd].flags - & ANDROID_FD_TABLE_ENTRY_IS_VALID)) - { - memcpy (statb, &android_table[fd].statb, - sizeof *statb); - return 0; - } - - return fstat (fd, statb); -} - -static int android_lookup_asset_directory_fd (int, - const char *restrict *, - const char *restrict); - -/* Like fstatat. However, if dirfd is AT_FDCWD and PATHNAME is an - asset, find the information for the corresponding asset, and if - dirfd is an offset into directory_tree as returned by - `android_dirfd', find the information within the corresponding - directory tree entry. */ - -int -android_fstatat (int dirfd, const char *restrict pathname, - struct stat *restrict statbuf, int flags) -{ - AAsset *asset_desc; - const char *asset; - const char *asset_dir; - int fd, rc; - - /* Look up whether or not DIRFD belongs to an open struct - android_dir. */ - - if (dirfd != AT_FDCWD) - dirfd - = android_lookup_asset_directory_fd (dirfd, &pathname, - pathname); - - if (dirfd == AT_FDCWD - && asset_manager - && (asset = android_get_asset_name (pathname))) - { - /* Look up whether or not PATHNAME happens to be a - directory. */ - asset_dir = android_scan_directory_tree ((char *) asset, - NULL); - - if (!asset_dir) - { - errno = ENOENT; - return -1; - } - - if (android_is_directory (asset_dir)) - { - memset (statbuf, 0, sizeof *statbuf); - - /* Fill in the stat buffer. */ - statbuf->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; - return 0; - } - - /* AASSET_MODE_STREAMING is fastest here. */ - asset_desc = AAssetManager_open (asset_manager, asset, - AASSET_MODE_STREAMING); - - if (!asset_desc) - return ENOENT; - - memset (statbuf, 0, sizeof *statbuf); - - /* Fill in the stat buffer. */ - statbuf->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; - statbuf->st_size = AAsset_getLength (asset_desc); - - /* Close the asset. */ - AAsset_close (asset_desc); - return 0; - } - - if (dirfd == AT_FDCWD - && android_init_gui - && android_content_name_p (pathname)) - { - /* This is actually a content:// URI. Open that file and call - stat on it. */ - - fd = android_open (pathname, O_RDONLY, 0); - - if (fd < 0) - return -1; - - rc = fstat (fd, statbuf); - android_close (fd); - return rc; - } - - return fstatat (dirfd, pathname, statbuf, flags); -} - -/* Return if NAME, a file name relative to the /assets directory, is - accessible, as long as !(amode & W_OK). */ - -static bool -android_file_access_p (const char *name, int amode) -{ - if (!asset_manager) - return false; + token = strtok_r (copy, "/", &saveptr); - if (!(amode & W_OK)) + while (token) { - if (!strcmp (name, "") || !strcmp (name, "/")) - /* /assets always exists. */ - return true; - - /* Check if the file exists by looking in the ``directory tree'' - asset generated during the build process. This is used - instead of the AAsset functions, because the latter are - buggy and treat directories inconsistently. */ - return android_scan_directory_tree ((char *) name, NULL) != NULL; - } - - return false; -} - -/* Do the same as android_hack_asset_fd, but use an unlinked temporary - file to cater to old Android kernels where ashmem files are not - readable. */ - -static int -android_hack_asset_fd_fallback (AAsset *asset) -{ - int fd; - char filename[PATH_MAX]; - size_t size; - void *mem; - - /* Assets must be small enough to fit in size_t, if off_t is - larger. */ - size = AAsset_getLength (asset); - - /* Get an unlinked file descriptor from a file in the cache - directory, which is guaranteed to only be written to by Emacs. - Creating an ashmem file descriptor and reading from it doesn't - work on these old Android versions. */ - - snprintf (filename, PATH_MAX, "%s/temp~unlinked.%d", - android_cache_dir, getpid ()); - fd = open (filename, O_CREAT | O_RDWR | O_TRUNC, - S_IRUSR | S_IWUSR); - - if (fd < 0) - return -1; - - if (unlink (filename)) - goto fail; - - if (ftruncate (fd, size)) - goto fail; - - mem = mmap (NULL, size, PROT_WRITE, MAP_SHARED, fd, 0); - if (mem == MAP_FAILED) - { - __android_log_print (ANDROID_LOG_ERROR, __func__, - "mmap: %s", strerror (errno)); - goto fail; - } - - if (AAsset_read (asset, mem, size) != size) - { - /* Too little was read. Close the file descriptor and - report an error. */ - __android_log_print (ANDROID_LOG_ERROR, __func__, - "AAsset_read: %s", strerror (errno)); - goto fail; - } - - munmap (mem, size); - return fd; - - fail: - close (fd); - return -1; -} + /* Compute the number of bytes remaining in buffer excluding a + trailing null byte. */ + n = PATH_MAX - (head - buffer); -/* Pointer to the `ASharedMemory_create' function which is loaded - dynamically. */ -static int (*asharedmemory_create) (const char *, size_t); + /* Write / to the buffer. Return failure if there is no space + for it. */ -/* Return whether or not shared memory file descriptors can also be - read from, and are thus suitable for creating asset files. + if (!n) + goto failure; - This does not work on some ancient Android systems running old - versions of the kernel. */ + *head++ = '/'; + n--; -static bool -android_detect_ashmem (void) -{ - int fd, rc; - void *mem; - char test_buffer[10]; + /* Find the next token now. */ + next = strtok_r (NULL, "/", &saveptr); - memcpy (test_buffer, "abcdefghi", 10); + /* Detect and avoid encoding an encoded URL query affixed to the + end of the last component within the content file name. - /* Create the file descriptor to be used for the test. */ + Content URIs can include a query describing parameters that + must be provided to the content provider. They are separated + from the rest of the URI by a single question mark character, + which should not be encoded. - /* Android 28 and earlier let Emacs access /dev/ashmem directly, so - prefer that over using ASharedMemory. */ + However, the distinction between the separator and question + marks that appear inside file name components is lost when a + content URI is decoded into a content path. To compensate + for this loss of information, Emacs assumes that the last + question mark is always a URI separator, and suffixes content + file names which contain question marks with a trailing + question mark. */ - if (android_api_level <= 28) - { - fd = open ("/dev/ashmem", O_RDWR); - - if (fd < 0) - return false; - - /* An empty name means the memory area will exist until the file - descriptor is closed, because no other process can - attach. */ - rc = ioctl (fd, ASHMEM_SET_NAME, ""); - - if (rc < 0) + if (!next) { - close (fd); - return false; - } + /* Find the last question mark character. */ - rc = ioctl (fd, ASHMEM_SET_SIZE, sizeof test_buffer); + mark1 = strchr (token, '?'); - if (rc < 0) - { - close (fd); - return false; - } - } - else - { - /* On the other hand, SELinux restrictions on Android 29 and - later require that Emacs use a system service to obtain - shared memory. Load this dynamically, as this service is not - available on all versions of the NDK. */ - - if (!asharedmemory_create) - { - *(void **) (&asharedmemory_create) - = dlsym (RTLD_DEFAULT, "ASharedMemory_create"); - - if (!asharedmemory_create) + while (mark1) { - __android_log_print (ANDROID_LOG_FATAL, __func__, - "dlsym: %s\n", - strerror (errno)); - emacs_abort (); + mark = mark1; + mark1 = strchr (mark + 1, '?'); } } - fd = asharedmemory_create ("", sizeof test_buffer); - - if (fd < 0) - return false; - } - - /* Now map the resource and write the test contents. */ - - mem = mmap (NULL, sizeof test_buffer, PROT_WRITE, - MAP_SHARED, fd, 0); - if (mem == MAP_FAILED) - { - close (fd); - return false; - } - - /* Copy over the test contents. */ - memcpy (mem, test_buffer, sizeof test_buffer); - - /* Return anyway even if munmap fails. */ - munmap (mem, sizeof test_buffer); - - /* Try to read the content back into test_buffer. If this does not - compare equal to the original string, or the read fails, then - ashmem descriptors are not readable on this system. */ - - if ((read (fd, test_buffer, sizeof test_buffer) - != sizeof test_buffer) - || memcmp (test_buffer, "abcdefghi", sizeof test_buffer)) - { - __android_log_print (ANDROID_LOG_WARN, __func__, - "/dev/ashmem does not produce real" - " temporary files on this system, so" - " Emacs will fall back to creating" - " unlinked temporary files."); - close (fd); - return false; - } - - close (fd); - return true; -} - -/* Get a file descriptor backed by a temporary in-memory file for the - given asset. */ - -static int -android_hack_asset_fd (AAsset *asset) -{ - static bool ashmem_readable_p; - static bool ashmem_initialized; - int fd, rc; - unsigned char *mem; - size_t size; - - /* The first time this function is called, try to determine whether - or not ashmem file descriptors can be read from. */ - - if (!ashmem_initialized) - ashmem_readable_p - = android_detect_ashmem (); - ashmem_initialized = true; - - /* If it isn't, fall back. */ - - if (!ashmem_readable_p) - return android_hack_asset_fd_fallback (asset); - - /* Assets must be small enough to fit in size_t, if off_t is - larger. */ - size = AAsset_getLength (asset); - - /* Android 28 and earlier let Emacs access /dev/ashmem directly, so - prefer that over using ASharedMemory. */ - - if (android_api_level <= 28) - { - fd = open ("/dev/ashmem", O_RDWR); - - if (fd < 0) - return -1; - - /* An empty name means the memory area will exist until the file - descriptor is closed, because no other process can - attach. */ - rc = ioctl (fd, ASHMEM_SET_NAME, ""); - - if (rc < 0) + if (mark) { - __android_log_print (ANDROID_LOG_ERROR, __func__, - "ioctl ASHMEM_SET_NAME: %s", - strerror (errno)); - close (fd); - return -1; - } + /* First, encode the part leading to the question mark + character. */ - rc = ioctl (fd, ASHMEM_SET_SIZE, size); + rc = 0; + if (mark > token) + rc = android_url_encode (token, mark - token, + head, n + 1); - if (rc < 0) - { - __android_log_print (ANDROID_LOG_ERROR, __func__, - "ioctl ASHMEM_SET_SIZE: %s", - strerror (errno)); - close (fd); - return -1; - } + /* If this fails, bail out. */ - if (!size) - return fd; + if (rc < 0) + goto failure; - /* Now map the resource. */ - mem = mmap (NULL, size, PROT_WRITE, MAP_SHARED, fd, 0); - if (mem == MAP_FAILED) - { - __android_log_print (ANDROID_LOG_ERROR, __func__, - "mmap: %s", strerror (errno)); - close (fd); - return -1; - } + /* Copy mark to the file name. */ - if (AAsset_read (asset, mem, size) != size) - { - /* Too little was read. Close the file descriptor and - report an error. */ - __android_log_print (ANDROID_LOG_ERROR, __func__, - "AAsset_read: %s", strerror (errno)); - close (fd); - return -1; - } + n -= rc, head += rc; + length = strlen (mark); - /* Return anyway even if munmap fails. */ - munmap (mem, size); - return fd; - } + if (n < length) + goto failure; - /* On the other hand, SELinux restrictions on Android 29 and later - require that Emacs use a system service to obtain shared memory. - Load this dynamically, as this service is not available on all - versions of the NDK. */ + strcpy (head, mark); - if (!asharedmemory_create) - { - *(void **) (&asharedmemory_create) - = dlsym (RTLD_DEFAULT, "ASharedMemory_create"); - - if (!asharedmemory_create) - { - __android_log_print (ANDROID_LOG_FATAL, __func__, - "dlsym: %s\n", - strerror (errno)); - emacs_abort (); + /* Now break out of the loop, since this is the last + component anyway. */ + break; } - } - - fd = asharedmemory_create ("", size); - - if (fd < 0) - { - __android_log_print (ANDROID_LOG_ERROR, __func__, - "ASharedMemory_create: %s", - strerror (errno)); - return -1; - } - - /* Now map the resource. */ - mem = mmap (NULL, size, PROT_WRITE, MAP_SHARED, fd, 0); - if (mem == MAP_FAILED) - { - __android_log_print (ANDROID_LOG_ERROR, __func__, - "mmap: %s", strerror (errno)); - close (fd); - return -1; - } - - if (AAsset_read (asset, mem, size) != size) - { - /* Too little was read. Close the file descriptor and - report an error. */ - __android_log_print (ANDROID_LOG_ERROR, __func__, - "AAsset_read: %s", strerror (errno)); - close (fd); - return -1; - } - - /* Return anyway even if munmap fails. */ - munmap (mem, size); - return fd; -} - -/* Make FD close-on-exec. If any system call fails, do not abort, but - log a warning to the system log. */ - -static void -android_close_on_exec (int fd) -{ - int flags, rc; + else + /* Now encode this file name component into the buffer. */ + rc = android_url_encode (token, strlen (token), + head, n + 1); - flags = fcntl (fd, F_GETFD); + if (rc < 0) + goto failure; - if (flags < 0) - { - __android_log_print (ANDROID_LOG_WARN, __func__, - "fcntl: %s", strerror (errno)); - return; + head += rc; + token = next; } - rc = fcntl (fd, F_SETFD, flags | O_CLOEXEC); + /* buffer must have been null terminated by + `android_url_encode'. */ + xfree (copy); + return buffer; - if (rc < 0) - { - __android_log_print (ANDROID_LOG_WARN, __func__, - "fcntl: %s", strerror (errno)); - return; - } + failure: + xfree (copy); + return NULL; } -/* `open' and such are modified even though they exist on Android, - because Emacs treats "/assets/" as a special directory that must - contain all assets in the application package. */ +/* Return whether or not the specified FILENAME is an accessible + content URI. MODE specifies what to check. */ -int -android_open (const char *filename, int oflag, mode_t mode) +static bool +android_check_content_access (const char *filename, int mode) { const char *name; - AAsset *asset; - int fd; - size_t length; jobject string; + size_t length; + jboolean rc; - if (asset_manager && (name = android_get_asset_name (filename))) - { - /* If Emacs is trying to write to the file, return NULL. */ - - if (oflag & O_WRONLY || oflag & O_RDWR) - { - errno = EROFS; - return -1; - } - - if (oflag & O_DIRECTORY) - { - errno = ENOTSUP; - return -1; - } - - /* If given AASSET_MODE_BUFFER (which is what Emacs probably - does, given that a file descriptor is not always available), - the framework fails to uncompress the data before it returns - a file descriptor. */ - asset = AAssetManager_open (asset_manager, name, - AASSET_MODE_STREAMING); - - if (!asset) - { - errno = ENOENT; - return -1; - } - - /* Create a shared memory file descriptor containing the asset - contents. - - The documentation misleads people into thinking that - AAsset_openFileDescriptor does precisely this. However, it - instead returns an offset into any uncompressed assets in the - ZIP archive. This cannot be found in its documentation. */ - - fd = android_hack_asset_fd (asset); - - if (fd == -1) - { - AAsset_close (asset); - errno = ENXIO; - return -1; - } - - /* If O_CLOEXEC is specified, make the file descriptor close on - exec too. */ - if (oflag & O_CLOEXEC) - android_close_on_exec (fd); - - if (fd >= ANDROID_MAX_ASSET_FD || fd < 0) - { - /* Too bad. Pretend this is an out of memory error. */ - errno = ENOMEM; - - if (fd >= 0) - close (fd); - - fd = -1; - } - else - { - assert (!(android_table[fd].flags - & ANDROID_FD_TABLE_ENTRY_IS_VALID)); - android_table[fd].flags = ANDROID_FD_TABLE_ENTRY_IS_VALID; - memset (&android_table[fd].statb, 0, - sizeof android_table[fd].statb); - - /* Fill in some information that will be reported to - callers of android_fstat, among others. */ - android_table[fd].statb.st_mode - = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; - - /* Owned by root. */ - android_table[fd].statb.st_uid = 0; - android_table[fd].statb.st_gid = 0; - - /* Size of the file. */ - android_table[fd].statb.st_size - = AAsset_getLength (asset); - } - - AAsset_close (asset); - return fd; - } - - if (android_init_gui && android_content_name_p (filename)) - { - /* This is a content:// URI. Ask the system for a descriptor to - that file. */ - - name = android_get_content_name (filename); - length = strlen (name); - - /* Check if the mode is valid. */ - - if (oflag & O_DIRECTORY) - { - errno = ENOTSUP; - return -1; - } - - /* Allocate a buffer to hold the file name. */ - string = (*android_java_env)->NewByteArray (android_java_env, - length); - if (!string) - { - (*android_java_env)->ExceptionClear (android_java_env); - errno = ENOMEM; - return -1; - } - (*android_java_env)->SetByteArrayRegion (android_java_env, - string, 0, length, - (jbyte *) name); - - /* Try to open the file descriptor. */ - - fd - = (*android_java_env)->CallIntMethod (android_java_env, - emacs_service, - service_class.open_content_uri, - string, - (jboolean) ((mode & O_WRONLY - || mode & O_RDWR) - != 0), - (jboolean) !(mode & O_WRONLY), - (jboolean) ((mode & O_TRUNC) - != 0)); - - if ((*android_java_env)->ExceptionCheck (android_java_env)) - { - (*android_java_env)->ExceptionClear (android_java_env); - errno = ENOMEM; - ANDROID_DELETE_LOCAL_REF (string); - return -1; - } - - /* If fd is -1, just assume that the file does not exist, - and return -1 with errno set to ENOENT. */ - - if (fd == -1) - { - errno = ENOENT; - goto skip; - } - - if (mode & O_CLOEXEC) - android_close_on_exec (fd); - - skip: - ANDROID_DELETE_LOCAL_REF (string); - return fd; - } - - return open (filename, oflag, mode); -} - -/* Like close. However, remove the file descriptor from the asset - table as well. */ - -int -android_close (int fd) -{ - if (fd < ANDROID_MAX_ASSET_FD) - android_table[fd].flags = 0; - - return close (fd); -} - -/* Like fclose. However, remove any information associated with - FILE's file descriptor from the asset table as well. */ - -int -android_fclose (FILE *stream) -{ - int fd; + name = android_get_content_name (filename); + length = strlen (name); - fd = fileno (stream); + string = (*android_java_env)->NewByteArray (android_java_env, + length); + android_exception_check (); - if (fd != -1 && fd < ANDROID_MAX_ASSET_FD) - android_table[fd].flags = 0; + (*android_java_env)->SetByteArrayRegion (android_java_env, + string, 0, length, + (jbyte *) name); + rc = (*android_java_env)->CallBooleanMethod (android_java_env, + emacs_service, + service_class.check_content_uri, + string, + (jboolean) ((mode & R_OK) + != 0), + (jboolean) ((mode & W_OK) + != 0)); + android_exception_check_1 (string); + ANDROID_DELETE_LOCAL_REF (string); - return fclose (stream); + return rc; } +#endif /* 0 */ + /* Return the current user's ``home'' directory, which is actually the app data directory on Android. */ @@ -2015,7 +1199,6 @@ android_create_lib_link (void) char *filename; char lib_directory[PATH_MAX]; int fd; - struct stat statb; /* Find the directory containing the files directory. */ filename = dirname (android_files_dir); @@ -2105,16 +1288,8 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, int pipefd[2]; pthread_t thread; const char *java_string; - AAsset *asset; - /* This may be called from multiple threads. setEmacsParams should - only ever be called once. */ - if (__atomic_fetch_add (&emacs_initialized, -1, __ATOMIC_SEQ_CST)) - { - ANDROID_THROW (env, "java/lang/IllegalArgumentException", - "Emacs was already initialized!"); - return; - } + /* This function should only be called from the main thread. */ android_pixel_density_x = pixel_density_x; android_pixel_density_y = pixel_density_y; @@ -2124,40 +1299,6 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, "Initializing "PACKAGE_STRING"...\nPlease report bugs to " PACKAGE_BUGREPORT". Thanks.\n"); - /* Set the asset manager. */ - asset_manager = AAssetManager_fromJava (env, local_asset_manager); - - /* Initialize the directory tree. */ - asset = AAssetManager_open (asset_manager, "directory-tree", - AASSET_MODE_BUFFER); - - if (!asset) - { - __android_log_print (ANDROID_LOG_FATAL, __func__, - "Failed to open directory tree"); - emacs_abort (); - } - - directory_tree = AAsset_getBuffer (asset); - - if (!directory_tree) - emacs_abort (); - - /* Now figure out how big the directory tree is, and compare the - first few bytes. */ - directory_tree_size = AAsset_getLength (asset); - if (directory_tree_size < 5 - || memcmp (directory_tree, "EMACS", 5)) - { - __android_log_print (ANDROID_LOG_FATAL, __func__, - "Directory tree has bad magic"); - emacs_abort (); - } - - /* Hold a VM reference to the asset manager to prevent the native - object from being deleted. */ - (*env)->NewGlobalRef (env, local_asset_manager); - if (emacs_service_object) { /* Create a pipe and duplicate it to stdout and stderr. Next, @@ -2304,7 +1445,10 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, /* Set up events. */ android_init_events (); - /* OK, setup is now complete. The caller may start the Emacs thread + /* Set up the Android virtual filesystem layer. */ + android_vfs_init (env, local_asset_manager); + + /* OK, setup is now complete. The caller may call initEmacs now. */ } @@ -2405,6 +1549,33 @@ android_init_emacs_service (void) "Landroid/view/inputmethod/ExtractedText;I)V"); FIND_METHOD (update_cursor_anchor_info, "updateCursorAnchorInfo", "(Lorg/gnu/emacs/EmacsWindow;FFFF)V"); + FIND_METHOD (get_document_authorities, "getDocumentAuthorities", + "()[Ljava/lang/String;"); + FIND_METHOD (request_directory_access, "requestDirectoryAccess", + "()I"); + FIND_METHOD (get_document_trees, "getDocumentTrees", + "([B)[Ljava/lang/String;"); + FIND_METHOD (document_id_from_name, "documentIdFromName", + "(Ljava/lang/String;[B[Ljava/lang/String;)I"); + FIND_METHOD (get_tree_uri, "getTreeUri", + "(Ljava/lang/String;Ljava/lang/String;)" + "Ljava/lang/String;"); + FIND_METHOD (stat_document, "statDocument", + "(Ljava/lang/String;Ljava/lang/String;)[J"); + FIND_METHOD (access_document, "accessDocument", + "(Ljava/lang/String;Ljava/lang/String;Z)I"); + FIND_METHOD (open_document_directory, "openDocumentDirectory", + "(Ljava/lang/String;Ljava/lang/String;)" + "Landroid/database/Cursor;"); + FIND_METHOD (read_directory_entry, "readDirectoryEntry", + "(Landroid/database/Cursor;)Lorg/gnu/emacs/" + "EmacsDirectoryEntry;"); + FIND_METHOD (open_document, "openDocument", + "(Ljava/lang/String;Ljava/lang/String;ZZ)" + "Landroid/os/ParcelFileDescriptor;"); + FIND_METHOD (create_document, "createDocument", + "(Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;)Ljava/lang/String;"); #undef FIND_METHOD } @@ -6255,329 +5426,6 @@ android_toggle_on_screen_keyboard (android_window window, bool show) -/* Like faccessat, except it also understands DIRFD opened using - android_dirfd. */ - -int -android_faccessat (int dirfd, const char *pathname, int mode, int flags) -{ - const char *asset; - - if (dirfd != AT_FDCWD) - dirfd - = android_lookup_asset_directory_fd (dirfd, &pathname, - pathname); - - /* Check if pathname is actually an asset. If that is the case, - simply fall back to android_file_access_p. */ - - if (dirfd == AT_FDCWD - && asset_manager - && (asset = android_get_asset_name (pathname))) - { - if (android_file_access_p (asset, mode)) - return 0; - - /* Set errno to an appropriate value. */ - errno = ENOENT; - return 1; - } - - /* Check if pathname is actually a content resolver URI. */ - - if (dirfd == AT_FDCWD - && android_init_gui - && android_content_name_p (pathname)) - { - if (android_check_content_access (pathname, mode)) - return 0; - - /* Set errno to an appropriate value. */ - errno = ENOENT; - return 1; - } - -#if __ANDROID_API__ >= 16 - /* When calling `faccessat', make sure to clear the flag AT_EACCESS. - - Android's faccessat simply fails if FLAGS contains AT_EACCESS, so - replace it with zero here. This isn't caught at configuration-time - as Emacs is being cross compiled. - - This takes place only when building for Android 16 and later, - because earlier versions use a Gnulib replacement that lacks these - issues. */ - - return faccessat (dirfd, pathname, mode, flags & ~AT_EACCESS); -#else /* __ANDROID_API__ < 16 */ - return faccessat (dirfd, pathname, mode, flags); -#endif /* __ANDROID_API__ >= 16 */ -} - - - -/* Directory listing emulation. */ - -struct android_dir -{ - /* The real DIR *, if it exists. */ - DIR *dir; - - /* Otherwise, the pointer to the directory in directory_tree. */ - char *asset_dir; - - /* And the end of the files in asset_dir. */ - char *asset_limit; - - /* The next struct android_dir. */ - struct android_dir *next; - - /* Path to the directory relative to /. */ - char *asset_file; - - /* File descriptor used when asset_dir is set. */ - int fd; -}; - -/* List of all struct android_dir's corresponding to an asset - directory that are currently open. */ -static struct android_dir *android_dirs; - -/* Like opendir. However, return an asset directory if NAME points to - an asset. */ - -struct android_dir * -android_opendir (const char *name) -{ - struct android_dir *dir; - char *asset_dir; - const char *asset_name; - size_t limit, length; - - asset_name = android_get_asset_name (name); - - /* If the asset manager exists and NAME is an asset, return an asset - directory. */ - if (asset_manager && asset_name) - { - asset_dir - = (char *) android_scan_directory_tree ((char *) asset_name, - &limit); - - if (!asset_dir) - { - errno = ENOENT; - return NULL; - } - - length = strlen (name); - - dir = xmalloc (sizeof *dir); - dir->dir = NULL; - dir->asset_dir = asset_dir; - dir->asset_limit = (char *) directory_tree + limit; - dir->fd = -1; - dir->asset_file = xzalloc (length + 2); - - /* Make sure dir->asset_file is terminated with /. */ - strcpy (dir->asset_file, name); - if (dir->asset_file[length - 1] != '/') - dir->asset_file[length] = '/'; - - /* Make sure dir->asset_limit is within bounds. It is a limit, - and as such can be exactly one byte past directory_tree. */ - if (dir->asset_limit > directory_tree + directory_tree_size) - { - xfree (dir); - __android_log_print (ANDROID_LOG_VERBOSE, __func__, - "Invalid dir tree, limit %zu, size %zu\n", - limit, directory_tree_size); - dir = NULL; - errno = EACCES; - } - - dir->next = android_dirs; - android_dirs = dir; - - return dir; - } - - /* Otherwise, open the directory normally. */ - dir = xmalloc (sizeof *dir); - dir->asset_dir = NULL; - dir->dir = opendir (name); - - if (!dir->dir) - { - xfree (dir); - return NULL; - } - - return dir; -} - -/* Like dirfd. However, value is not a real directory file descriptor - if DIR is an asset directory. */ - -int -android_dirfd (struct android_dir *dirp) -{ - int fd; - - if (dirp->dir) - return dirfd (dirp->dir); - else if (dirp->fd != -1) - return dirp->fd; - - fd = open ("/dev/null", O_RDONLY | O_CLOEXEC); - - /* Record this file descriptor in dirp. */ - dirp->fd = fd; - return fd; -} - -/* Like readdir, except it understands asset directories. */ - -struct dirent * -android_readdir (struct android_dir *dir) -{ - static struct dirent dirent; - const char *last; - - if (dir->asset_dir) - { - /* There are no more files to read. */ - if (dir->asset_dir >= dir->asset_limit) - return NULL; - - /* Otherwise, scan forward looking for the next NULL byte. */ - last = memchr (dir->asset_dir, 0, - dir->asset_limit - dir->asset_dir); - - /* No more NULL bytes remain. */ - if (!last) - return NULL; - - /* Forward last past the NULL byte. */ - last++; - - /* Make sure it is still within the directory tree. */ - if (last >= directory_tree + directory_tree_size) - return NULL; - - /* Now, fill in the dirent with the name. */ - memset (&dirent, 0, sizeof dirent); - dirent.d_ino = 0; - dirent.d_off = 0; - dirent.d_reclen = sizeof dirent; - - /* If this is not a directory, return DT_UNKNOWN. Otherwise, - return DT_DIR. */ - - if (android_is_directory (dir->asset_dir)) - dirent.d_type = DT_DIR; - else - dirent.d_type = DT_UNKNOWN; - - /* Note that dir->asset_dir is actually a NULL terminated - string. */ - memcpy (dirent.d_name, dir->asset_dir, - MIN (sizeof dirent.d_name, - last - dir->asset_dir)); - dirent.d_name[sizeof dirent.d_name - 1] = '\0'; - - /* Strip off the trailing slash, if any. */ - if (dirent.d_name[MIN (sizeof dirent.d_name, - last - dir->asset_dir) - - 2] == '/') - dirent.d_name[MIN (sizeof dirent.d_name, - last - dir->asset_dir) - - 2] = '\0'; - - /* Finally, forward dir->asset_dir to the file past last. */ - dir->asset_dir = ((char *) directory_tree - + android_extract_long ((char *) last)); - - return &dirent; - } - - return readdir (dir->dir); -} - -/* Like closedir, but it also closes asset manager directories. */ - -void -android_closedir (struct android_dir *dir) -{ - struct android_dir **next, *tem; - - if (dir->dir) - closedir (dir->dir); - else - { - if (dir->fd != -1) - close (dir->fd); - - /* Unlink this directory from the list of all asset manager - directories. */ - - for (next = &android_dirs; (tem = *next);) - { - if (tem == dir) - *next = dir->next; - else - next = &(*next)->next; - } - - /* Free the asset file name. */ - xfree (dir->asset_file); - } - - /* There is no need to close anything else, as the directory tree - lies in statically allocated memory. */ - - xfree (dir); -} - -/* Subroutine used by android_fstatat and android_faccessat. If DIRFD - belongs to an open asset directory and FILE is a relative file - name, then return AT_FDCWD and the absolute file name of the - directory prepended to FILE in *PATHNAME. Else, return DIRFD. */ - -int -android_lookup_asset_directory_fd (int dirfd, - const char *restrict *pathname, - const char *restrict file) -{ - struct android_dir *dir; - static char *name; - - if (file[0] == '/') - return dirfd; - - for (dir = android_dirs; dir; dir = dir->next) - { - if (dir->fd == dirfd && dirfd != -1) - { - if (name) - xfree (name); - - /* dir->asset_file is always separator terminated. */ - name = xzalloc (strlen (dir->asset_file) - + strlen (file) + 1); - strcpy (name, dir->asset_file); - strcpy (name + strlen (dir->asset_file), - file); - *pathname = name; - return AT_FDCWD; - } - } - - return dirfd; -} - - - /* emacs_abort implementation for Android. This logs a stack trace. */ @@ -6787,6 +5635,28 @@ android_exception_check_2 (jobject object, jobject object1) memory_full (0); } +/* Like android_exception_check_2, except it takes more than two local + reference arguments. */ + +void +android_exception_check_3 (jobject object, jobject object1, + jobject object2) +{ + if (likely (!(*android_java_env)->ExceptionCheck (android_java_env))) + return; + + __android_log_print (ANDROID_LOG_WARN, __func__, + "Possible out of memory error. " + " The Java exception follows: "); + /* Describe exactly what went wrong. */ + (*android_java_env)->ExceptionDescribe (android_java_env); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (object); + ANDROID_DELETE_LOCAL_REF (object1); + ANDROID_DELETE_LOCAL_REF (object2); + memory_full (0); +} + /* Check for JNI problems based on the value of OBJECT. Signal out of memory if OBJECT is NULL. OBJECT1 means the @@ -7160,7 +6030,7 @@ android_restart_emacs (void) turn which APIs Emacs can safely use. */ int -android_get_current_api_level (void) +(android_get_current_api_level) (void) { return android_api_level; } @@ -7206,30 +6076,25 @@ android_query_battery (struct android_battery_state *status) return 0; } -/* Display a small momentary notification on screen containing - TEXT, which must be in the modified UTF encoding used by the - JVM. */ +/* Display a file panel and grant Emacs access to the SAF directory + within it. Value is 1 upon failure and 0 upon success (which only + indicates that the panel has been displayed successfully; the panel + may still be dismissed without a file being selected.) */ -void -android_display_toast (const char *text) +int +android_request_directory_access (void) { - jstring string; + jint rc; + jmethodID method; - /* Make the string. */ - string = (*android_java_env)->NewStringUTF (android_java_env, - text); + method = service_class.request_directory_access; + rc = (*android_java_env)->CallNonvirtualIntMethod (android_java_env, + emacs_service, + service_class.class, + method); android_exception_check (); - /* Display the toast. */ - (*android_java_env)->CallNonvirtualVoidMethod (android_java_env, - emacs_service, - service_class.class, - service_class.display_toast, - string); - android_exception_check_1 (string); - - /* Delete the local reference to the string. */ - ANDROID_DELETE_LOCAL_REF (string); + return rc; } @@ -7709,156 +6574,6 @@ android_set_fullscreen (android_window window, bool fullscreen) -/* External asset management interface. By using functions here - to read and write from files, Emacs can avoid opening a - shared memory file descriptor for each ``asset'' file. */ - -/* Like android_open. However, return a structure that can - either directly hold an AAsset or a file descriptor. - - Value is the structure upon success. Upon failure, value - consists of an uninitialized file descriptor, but its asset - field is set to -1, and errno is set accordingly. */ - -struct android_fd_or_asset -android_open_asset (const char *filename, int oflag, mode_t mode) -{ - const char *name; - struct android_fd_or_asset fd; - AAsset *asset; - - /* Initialize FD by setting its asset to an invalid - pointer. */ - fd.asset = (void *) -1; - - /* See if this is an asset. */ - - if (asset_manager && (name = android_get_asset_name (filename))) - { - /* Return failure for unsupported flags. */ - - if (oflag & O_WRONLY || oflag & O_RDWR) - { - errno = EROFS; - return fd; - } - - if (oflag & O_DIRECTORY) - { - errno = ENOTSUP; - return fd; - } - - /* Now try to open the asset. */ - asset = AAssetManager_open (asset_manager, name, - AASSET_MODE_STREAMING); - - if (!asset) - { - errno = ENOENT; - return fd; - } - - /* Return the asset. */ - fd.asset = asset; - return fd; - } - - /* If the file is not an asset, fall back to android_open and - get a regular file descriptor. */ - - fd.fd = android_open (filename, oflag, mode); - if (fd.fd < 0) - return fd; - - /* Set fd.asset to NULL, signifying that it is a file - descriptor. */ - fd.asset = NULL; - return fd; -} - -/* Like android_close. However, it takes a ``file descriptor'' - opened using android_open_asset. */ - -int -android_close_asset (struct android_fd_or_asset asset) -{ - if (!asset.asset) - return android_close (asset.fd); - - AAsset_close (asset.asset); - return 0; -} - -/* Like `emacs_read_quit'. However, it handles file descriptors - opened using `android_open_asset' as well. */ - -ssize_t -android_asset_read_quit (struct android_fd_or_asset asset, - void *buffer, size_t size) -{ - if (!asset.asset) - return emacs_read_quit (asset.fd, buffer, size); - - /* It doesn't seem possible to quit from inside AAsset_read, - sadly. */ - return AAsset_read (asset.asset, buffer, size); -} - -/* Like `read'. However, it handles file descriptors opened - using `android_open_asset' as well. */ - -ssize_t -android_asset_read (struct android_fd_or_asset asset, - void *buffer, size_t size) -{ - if (!asset.asset) - return read (asset.fd, buffer, size); - - /* It doesn't seem possible to quit from inside AAsset_read, - sadly. */ - return AAsset_read (asset.asset, buffer, size); -} - -/* Like `lseek', but it handles ``file descriptors'' opened with - android_open_asset. */ - -off_t -android_asset_lseek (struct android_fd_or_asset asset, off_t off, - int whence) -{ - if (!asset.asset) - return lseek (asset.fd, off, whence); - - return AAsset_seek (asset.asset, off, whence); -} - -/* Like `fstat'. */ - -int -android_asset_fstat (struct android_fd_or_asset asset, - struct stat *statb) -{ - if (!asset.asset) - return fstat (asset.fd, statb); - - /* Clear statb. */ - memset (statb, 0, sizeof *statb); - - /* Set the mode. */ - statb->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; - - /* Owned by root. */ - statb->st_uid = 0; - statb->st_gid = 0; - - /* Size of the file. */ - statb->st_size = AAsset_getLength (asset.asset); - return 0; -} - - - /* Window cursor support. */ android_cursor @@ -8088,4 +6803,4 @@ android_project_image_nearest (struct android_image *image, emacs_abort (); } -#endif +#endif /* !ANDROID_STUBIFY */ diff --git a/src/android.h b/src/android.h index 2323690fa25..94a3ad46e74 100644 --- a/src/android.h +++ b/src/android.h @@ -36,26 +36,46 @@ along with GNU Emacs. If not, see . */ #include "androidgui.h" #include "lisp.h" -#endif +#endif /* ANDROID_STUBIFY */ extern bool android_init_gui; #ifndef ANDROID_STUBIFY +extern char *android_cache_dir; + extern int android_emacs_init (int, char **, char *); extern int android_select (int, fd_set *, fd_set *, fd_set *, struct timespec *); - -extern int android_open (const char *, int, mode_t); extern char *android_user_full_name (struct passwd *); + + + +/* File I/O operations. Many of these are defined in + androidvfs.c. */ + extern const char *android_is_special_directory (const char *, const char *); +extern const char *android_get_home_directory (void); + +extern void android_vfs_init (JNIEnv *, jobject); + +extern int android_open (const char *, int, mode_t); extern int android_fstat (int, struct stat *); extern int android_fstatat (int, const char *restrict, struct stat *restrict, int); extern int android_faccessat (int, const char *, int, int); extern int android_close (int); +extern FILE *android_fdopen (int, const char *); extern int android_fclose (FILE *); -extern const char *android_get_home_directory (void); +extern int android_unlink (const char *); +extern int android_symlink (const char *, const char *); +extern int android_rmdir (const char *); +extern int android_mkdir (const char *, mode_t); +extern int android_renameat_noreplace (int, const char *, + int, const char *); +extern int android_rename (const char *, const char *); + + extern double android_pixel_density_x, android_pixel_density_y; extern double android_scaled_pixel_density; @@ -89,6 +109,7 @@ extern jstring android_build_jstring (const char *); extern void android_exception_check (void); extern void android_exception_check_1 (jobject); extern void android_exception_check_2 (jobject, jobject); +extern void android_exception_check_3 (jobject, jobject, jobject); extern void android_exception_check_nonnull (void *, jobject); extern void android_exception_check_nonnull_1 (void *, jobject, jobject); @@ -96,18 +117,29 @@ extern void android_get_keysym_name (int, char *, size_t); extern void android_wait_event (void); extern void android_toggle_on_screen_keyboard (android_window, bool); extern _Noreturn void android_restart_emacs (void); -extern int android_get_current_api_level (void); +extern int android_request_directory_access (void); +extern int android_get_current_api_level (void) + __attribute__ ((pure)); + +/* Define `android_get_current_api_level' to a macro that the compiler + knows will always return at least __ANDROID_API__. */ + +#define android_get_current_api_level() \ + ({ int value; \ + \ + value = (android_get_current_api_level) (); \ + eassume (value >= __ANDROID_API__); value; }) /* Directory listing emulation. */ -struct android_dir; +struct android_vdir; -extern struct android_dir *android_opendir (const char *); -extern int android_dirfd (struct android_dir *); -extern struct dirent *android_readdir (struct android_dir *); -extern void android_closedir (struct android_dir *); +extern struct android_vdir *android_opendir (const char *); +extern int android_dirfd (struct android_vdir *); +extern struct dirent *android_readdir (struct android_vdir *); +extern void android_closedir (struct android_vdir *); @@ -211,8 +243,52 @@ extern int android_rewrite_spawn_argv (const char ***); #ifndef ANDROID_STUBIFY #include +struct android_emacs_service +{ + jclass class; + jmethodID fill_rectangle; + jmethodID fill_polygon; + jmethodID draw_rectangle; + jmethodID draw_line; + jmethodID draw_point; + jmethodID clear_window; + jmethodID clear_area; + jmethodID ring_bell; + jmethodID query_tree; + jmethodID get_screen_width; + jmethodID get_screen_height; + jmethodID detect_mouse; + jmethodID name_keysym; + jmethodID browse_url; + jmethodID restart_emacs; + jmethodID update_ic; + jmethodID reset_ic; + jmethodID open_content_uri; + jmethodID check_content_uri; + jmethodID query_battery; + jmethodID update_extracted_text; + jmethodID update_cursor_anchor_info; + jmethodID get_document_authorities; + jmethodID request_directory_access; + jmethodID get_document_trees; + jmethodID document_id_from_name; + jmethodID get_tree_uri; + jmethodID stat_document; + jmethodID access_document; + jmethodID open_document_directory; + jmethodID read_directory_entry; + jmethodID open_document; + jmethodID create_document; +}; + extern JNIEnv *android_java_env; +/* The EmacsService object. */ +extern jobject emacs_service; + +/* Various methods associated with the EmacsService. */ +extern struct android_emacs_service service_class; + #define ANDROID_DELETE_LOCAL_REF(ref) \ ((*android_java_env)->DeleteLocalRef (android_java_env, \ (ref))) diff --git a/src/androidfns.c b/src/androidfns.c index dcc9ab83427..0270f58b6b9 100644 --- a/src/androidfns.c +++ b/src/androidfns.c @@ -3036,6 +3036,32 @@ for more details about these values. */) +/* Directory access requests. */ + +DEFUN ("android-request-directory-access", Fandroid_request_directory_access, + Sandroid_request_directory_access, 0, 0, "", + doc: /* Request access to a directory within external storage. +On Android 5.0 and later, prompt for a directory within external or +application storage, and grant access to it; some of these directories +cannot be accessed through the regular `/sdcard' filesystem. + +If access to the directory is granted, it will eventually appear +within the directory `/content/storage'. */) + (void) +{ + if (android_get_current_api_level () < 21) + error ("Emacs can only access application storage on" + " Android 5.0 and later"); + + if (!android_init_gui) + return Qnil; + + android_request_directory_access (); + return Qnil; +} + + + /* Miscellaneous input method related stuff. */ /* Report X, Y, by the phys cursor width and height as the cursor @@ -3062,7 +3088,7 @@ android_set_preeditarea (struct window *w, int x, int y) y + w->phys_cursor_height); } -#endif +#endif /* !ANDROID_STUBIFY */ @@ -3220,6 +3246,7 @@ This option has no effect on Android 9 and earlier. */); defsubr (&Sx_server_version); #ifndef ANDROID_STUBIFY defsubr (&Sandroid_query_battery); + defsubr (&Sandroid_request_directory_access); tip_timer = Qnil; staticpro (&tip_timer); @@ -3235,5 +3262,5 @@ This option has no effect on Android 9 and earlier. */); staticpro (&tip_dx); tip_dy = Qnil; staticpro (&tip_dy); -#endif +#endif /* !ANDROID_STUBIFY */ } diff --git a/src/androidvfs.c b/src/androidvfs.c new file mode 100644 index 00000000000..a32471d250e --- /dev/null +++ b/src/androidvfs.c @@ -0,0 +1,6137 @@ +/* Android virtual file-system support for GNU Emacs. + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include "android.h" +#include "systime.h" + +#if __ANDROID_API__ >= 9 +#include +#include +#else /* __ANDROID_API__ < 9 */ +#include "android-asset.h" +#endif /* __ANDROID_API__ >= 9 */ + +#include + +/* This file implements support for the various special-purpose + directories found on Android systems. Such directories are not + mounted in the Unix virtual file-system, instead being accessible + through special API calls; Emacs pretends they are mounted at + specific folders within the root directory. + + There are presently two directories: /assets, granting access to + asset files stored within the APK, and /content, providing direct + access to content URIs (in Android 4.4 and later) and content + directory trees (in Android 5.0 and later.) + + This file implements substitutes for the C library `open', `fstat', + `close', `fclose', `unlink', `symlink', `rmdir', `rename', `stat' + system call wrappers, which process file names through ``VFS + nodes'' representing conceptual files, that are really no more than + tables of function pointers. + + The primary function of a node is to `name' children. This takes a + relative file name and returns a second VFS node tied to a child + that exists within this node (or a child thereof, ad infinite.) + + Other functions are also defined: functions to open file + descriptors, and substitutes for each of the C library system call + wrappers replaced. Each of these functions accepts two vnodes, and + is expected to otherwise behave like the C library system calls + replaced. + + When the virtual file system needs to locate the vnode associated + with a file name, it starts searching at the root vnode. Its + `name' function then creates vnodes as appropriate for the + components of the file name, which repeats recursively until the + vnode designating the file name is found. + + The substitute functions defined have two caveats, which however + don't prove problematic in an Emacs context: the first is that the + treatment of `..' is inconsistent with Unix, and has not really + been tested, while the second is that errno values do not always + conform to what the corresponding Unix system calls may return. */ + +/* Structure describing an array of VFS operations. */ + +struct android_vnode; + +struct android_vdir +{ + /* Return a `struct dirent' describing the next file in this + directory stream, or NULL if the stream has reached its end. */ + struct dirent *(*readdir) (struct android_vdir *); + + /* Close and release all resources allocated for this directory + stream. */ + void (*closedir) (struct android_vdir *); + + /* Return a ``file descriptor'' tied to this directory stream. */ + int (*dirfd) (struct android_vdir *); +}; + +struct android_vops +{ + /* Name a child of the given VFS node, which should be a + directory. + + LENGTH should be the length of NAME, excluding that of any + trailing NULL byte. + + NAME should be a normalized and NULL-terminated relative file + name; it may contain a leading separator characters, but no + consecutive ones. + + If NAME is empty, create another VFS node designating the same + file instead. + + NAME should also be located within writable storage; it may be + overwritten as the vnode sees fit. + + Value is a VFS node corresponding to the child, or NULL upon + failure. + + A VFS node may be returned even if NAME does not exist, the + expectation being that either a later filesystem operation will + fail, or will create the file. */ + struct android_vnode *(*name) (struct android_vnode *, char *, size_t); + + /* Open the specified VNODE, returning either a file descriptor or + an asset file descriptor. + + FLAGS and MODE mean the same as they do to the Unix `open' system + call. + + ASSET_P stipulates if an asset file descriptor may be returned; + if true, *ASSET may be set to an asset file descriptor. + + If an asset file descriptor is unavailable or ASSET_P is false, + *FD will be set to a file descriptor. + + If the vnode cannot be opened, value is -1 with errno set + accordingly. Otherwise, value is 0 if a file descriptor was + returned, and 1 if an asset file descriptor was returned. */ + int (*open) (struct android_vnode *, int, mode_t, bool, + int *, AAsset **); + + /* Close the specified VNODE, releasing all of its resources. + Save errno before making system calls that may set it, and + restore it to its original value before returning. + + This is unrelated to `android_close', which primarily releases on + stat buffers linked to file or asset file descriptors. */ + void (*close) (struct android_vnode *); + + /* Unlink the file and the specified VNODE. Value and errno are the + same as Unix `unlink'. */ + int (*unlink) (struct android_vnode *); + + /* Create a symlink from the specified VNODE to the target TARGET. + Value and errno are the same as `symlink' on Linux (which notably + means that errno is set to EPERM if VNODE doesn't support + symlinks.) */ + int (*symlink) (const char *, struct android_vnode *); + + /* Remove VNODE from its parent directory. VNODE must be an empty + directory. Value and errno are the same as Unix `rmdir'. */ + int (*rmdir) (struct android_vnode *); + + /* Move the file designated by SRC to DST, overwriting DST if + KEEP_EXISTING is false. + + If KEEP_EXISTING is true and DST already exists, value is -1 with + errno set to EEXIST. + + If VNODE does not natively support checking for a preexisting DST + and KEEP_EXISTING is true, value is -1 with errno set to ENOSYS. + + Value is otherwise the same as `rename'. */ + int (*rename) (struct android_vnode *, struct android_vnode *, bool); + + /* Return statistics for the specified VNODE. + Value and errno are the same as with Unix `stat'. */ + int (*stat) (struct android_vnode *, struct stat *); + + /* Return whether or not VNODE is accessible. + Value, errno and MODE are the same as with Unix `access'. */ + int (*access) (struct android_vnode *, int); + + /* Make a directory designated by VNODE, like Unix `mkdir'. */ + int (*mkdir) (struct android_vnode *, mode_t); + + /* Open the specified VNODE as a directory. + Value is a ``directory handle'', or NULL upon failure. */ + struct android_vdir *(*opendir) (struct android_vnode *); +}; + +struct android_vnode +{ + /* Operations associated with this vnode. */ + struct android_vops *ops; + + /* Type of this vnode and its flags. */ + short type, flags; +}; + +/* Structure describing a special named vnode relative to the root + vnode, or another directory vnode. */ + +struct android_special_vnode +{ + /* The name of the special file. */ + const char *name; + + /* The length of that name. */ + size_t length; + + /* Function called to create the initial vnode from the rest of the + component. */ + struct android_vnode *(*initial) (char *, size_t); +}; + +enum android_vnode_type + { + ANDROID_VNODE_UNIX, + ANDROID_VNODE_AFS, + ANDROID_VNODE_CONTENT, + ANDROID_VNODE_CONTENT_AUTHORITY, + ANDROID_VNODE_SAF_ROOT, + ANDROID_VNODE_SAF_TREE, + ANDROID_VNODE_SAF_FILE, + ANDROID_VNODE_SAF_NEW, + }; + + + +/* Structure describing the android.database.Cursor class. */ + +struct android_cursor_class +{ + jclass class; + jmethodID close; +}; + +/* Structure describing the EmacsDirectoryEntry class. */ + +struct emacs_directory_entry_class +{ + jclass class; + jfieldID d_type; + jfieldID d_name; +}; + +/* Structure describing the android.os.ParcelFileDescriptor class used + to wrap file descriptors sent over IPC. */ + +struct android_parcel_file_descriptor_class +{ + jclass class; + jmethodID close; + jmethodID get_fd; + jmethodID detach_fd; +}; + +/* The java.lang.String class. */ +static jclass java_string_class; + +/* Fields and methods associated with the Cursor class. */ +static struct android_cursor_class cursor_class; + +/* Fields and methods associated with the EmacsDirectoryEntry + class. */ +static struct emacs_directory_entry_class entry_class; + +/* Fields and methods associated with the ParcelFileDescriptor + class. */ +static struct android_parcel_file_descriptor_class fd_class; + +/* Initialize `cursor_class' using the given JNI environment ENV. + Calling this function is not necessary on Android 4.4 and + earlier. */ + +static void +android_init_cursor_class (JNIEnv *env) +{ + jclass old; + + cursor_class.class + = (*env)->FindClass (env, "android/database/Cursor"); + eassert (cursor_class.class); + + old = cursor_class.class; + cursor_class.class + = (jclass) (*env)->NewGlobalRef (env, (jobject) old); + (*env)->DeleteLocalRef (env, old); + + if (!cursor_class.class) + emacs_abort (); + +#define FIND_METHOD(c_name, name, signature) \ + cursor_class.c_name \ + = (*env)->GetMethodID (env, cursor_class.class, \ + name, signature); \ + assert (cursor_class.c_name); + FIND_METHOD (close, "close", "()V"); +#undef FIND_METHOD +} + +/* Initialize `entry_class' using the given JNI environment ENV. + Calling this function is not necessary on Android 4.4 and + earlier. */ + +static void +android_init_entry_class (JNIEnv *env) +{ + jclass old; + + entry_class.class + = (*env)->FindClass (env, "org/gnu/emacs/EmacsDirectoryEntry"); + eassert (entry_class.class); + + old = entry_class.class; + entry_class.class + = (jclass) (*env)->NewGlobalRef (env, (jobject) old); + (*env)->DeleteLocalRef (env, old); + + if (!entry_class.class) + emacs_abort (); + + entry_class.d_type = (*env)->GetFieldID (env, entry_class.class, + "d_type", "I"); + entry_class.d_name = (*env)->GetFieldID (env, entry_class.class, + "d_name", + "Ljava/lang/String;"); + assert (entry_class.d_type && entry_class.d_name); +} + + +/* Initialize `fd_class' using the given JNI environment ENV. Calling + this function is not necessary on Android 4.4 and earlier. */ + +static void +android_init_fd_class (JNIEnv *env) +{ + jclass old; + + fd_class.class + = (*env)->FindClass (env, "android/os/ParcelFileDescriptor"); + eassert (fd_class.class); + + old = fd_class.class; + fd_class.class + = (jclass) (*env)->NewGlobalRef (env, (jobject) old); + (*env)->DeleteLocalRef (env, old); + + if (!fd_class.class) + emacs_abort (); + +#define FIND_METHOD(c_name, name, signature) \ + fd_class.c_name \ + = (*env)->GetMethodID (env, fd_class.class, \ + name, signature); \ + assert (fd_class.c_name); + FIND_METHOD (close, "close", "()V"); + FIND_METHOD (get_fd, "getFd", "()I"); + FIND_METHOD (detach_fd, "detachFd", "()I"); +#undef FIND_METHOD +} + + + +/* Delete redundant instances of `.' and `..' from NAME in-place. + NAME must be *LENGTH long, excluding a mandatory trailing NULL + byte. + + Transform each directory component in NAME to avoid instances + of the `.' and `..' directories. For example, turn: + + a/../b/c/. + + into + + b/c/ + + and return NULL, writing the new length of NAME into *LENGTH. + + If there are more `..' components in NAME than there are normal + file name components, return NAME incremented to the position after + the first `..' component that cannot be transformed. For example, + if NAME is + + a/../../a + + value will be + + a + + If NAME is a directory separator and LENGTH is 1, return without + modifying NAME. In any other case, omit any leading directory + separator when writing to NAME. This is useful when a vnode that + can only be opened as a directory is desired, as this status is + made clear by suffixing the file name with a trailing + directory separator. */ + +static char * +android_vfs_canonicalize_name (char *name, size_t *length) +{ + size_t nellipsis, i; + char *last_component, *prev_component, *fill, *orig_name; + size_t size; + + /* Special case described in the last paragraph of the comment + above. */ + + size = *length; + orig_name = name; + + if (*name == '/' && size == 1) + return NULL; + else if (*name == '/') + size -= 1; + + nellipsis = 0; /* Number of ellipsis encountered within the current + file name component, or -1. */ + prev_component = NULL; /* Pointer to the separator character of + the component immediately before the + component currently being written. */ + last_component = name; /* Pointer to the separator character of + the component currently being read. */ + fill = name; /* Pointer to the next character that will be written + within NAME. */ + + /* Adjust name to skip the leading directory separator. But only + after fill is set. */ + if (*name == '/') + name++; + + for (i = 0; i < size; ++i) + { + switch (name[i]) + { + case '/': + /* See if the previous component was `..' or `.'. + + If it is .., and if no previous directory separator was + encountered, return or look up a vnode representing the + parent. */ + + if (nellipsis == 2) + { + /* .. */ + + if (!prev_component) + goto parent_vnode; + + /* Return to the last component. */ + fill = prev_component; + + /* Restore last_component to prev_component, and + prev_component back to the component before that. */ + last_component = prev_component; + + if (last_component != name) + prev_component = memrchr (name, '/', + last_component - name - 1); + else + prev_component = NULL; + + /* prev_component may now be NULL. If last_component is + the same as NAME, then fill has really been returned + to the beginning of the string, so leave it be. But + if it's something else, then it must be the first + separator character in the string, so set + prev_component to NAME itself. */ + + if (!prev_component && last_component != name) + prev_component = name; + } + else if (nellipsis == 1) + /* If it's ., return to this component. */ + fill = last_component; + else + { + /* Record the position of the last directory separator, + so NAME can be overwritten from there onwards if `..' + or `.' are encountered. */ + prev_component = last_component; + last_component = fill; + } + + /* Allow tracking ellipses again. */ + nellipsis = 0; + break; + + case '.': + if (nellipsis != -1) + nellipsis++; + break; + + default: + nellipsis = -1; + break; + } + + /* Now copy this character over from NAME. */ + *fill++ = name[i]; + } + + /* See if the previous component was `..' or `.'. + + If it is .., and if no previous directory separator was + encountered, return or look up a vnode representing the + parent. */ + + if (nellipsis == 2) + { + /* .. */ + + if (!prev_component) + /* Look up the rest of the vnode in its parent. */ + goto parent_vnode; + + /* Return to the last component. */ + fill = prev_component; + nellipsis = -2; + } + else if (nellipsis == 1) + { + /* If it's ., return to this component. */ + fill = last_component; + nellipsis = -2; + } + + /* Now, if there's enough room and an ellipsis file name was the + last component of END, append a trailing `/' before NULL + terminating it, indicating that the file name must be a + directory. */ + + if (fill + 1 < name + size && nellipsis == -2) + *fill++ = '/'; + + /* NULL terminate fill. */ + *fill = '\0'; + *length = fill - orig_name; + return NULL; + + parent_vnode: + /* .. was encountered and the parent couldn't be found through + stripping off preceding components. + + Find the parent vnode and name the rest of NAME starting from + there. */ + return name + i; +} + + + +/* Unix vnode implementation. These VFS nodes directly wrap around + the Unix filesystem, with the exception of the root vnode. */ + +struct android_unix_vnode +{ + /* The vnode data itself. */ + struct android_vnode vnode; + + /* Length of the name without a trailing null byte. */ + size_t name_length; + + /* Name of the vnode. */ + char *name; +}; + +struct android_unix_vdir +{ + /* The directory function table. */ + struct android_vdir vdir; + + /* The directory stream. */ + DIR *directory; +}; + +/* The vnode representing the root filesystem. */ +static struct android_unix_vnode root_vnode; + +static struct android_vnode *android_unix_name (struct android_vnode *, + char *, size_t); +static int android_unix_open (struct android_vnode *, int, + mode_t, bool, int *, AAsset **); +static void android_unix_close (struct android_vnode *); +static int android_unix_unlink (struct android_vnode *); +static int android_unix_symlink (const char *, struct android_vnode *); +static int android_unix_rmdir (struct android_vnode *); +static int android_unix_rename (struct android_vnode *, + struct android_vnode *, bool); +static int android_unix_stat (struct android_vnode *, struct stat *); +static int android_unix_access (struct android_vnode *, int); +static int android_unix_mkdir (struct android_vnode *, mode_t); +static struct android_vdir *android_unix_opendir (struct android_vnode *); + +/* Vector of VFS operations associated with Unix filesystem VFS + nodes. */ + +static struct android_vops unix_vfs_ops = + { + android_unix_name, + 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_opendir, + }; + +static struct android_vnode * +android_unix_name (struct android_vnode *vnode, char *name, + size_t length) +{ + struct android_unix_vnode *vp, *input, temp; + char *fill, *remainder; + size_t j; + + /* Canonicalize NAME. */ + input = (struct android_unix_vnode *) vnode; + remainder = android_vfs_canonicalize_name (name, &length); + + /* If remainder is set, it's a name relative to the parent + vnode. */ + if (remainder) + goto parent_vnode; + + /* Create a new unix vnode. */ + vp = xmalloc (sizeof *vp); + + /* If name is empty, duplicate the current vnode. */ + + if (length < 1) + { + memcpy (vp, vnode, sizeof *vp); + vp->name = xstrdup (vp->name); + return &vp->vnode; + } + + /* Otherwise, fill in the vnode. */ + + vp->vnode.ops = &unix_vfs_ops; + vp->vnode.type = ANDROID_VNODE_UNIX; + vp->vnode.flags = 0; + + /* Generate the new name of the vnode. Remove any trailing slash + from vp->name. */ + + vp->name_length = input->name_length + length; + vp->name = xmalloc (vp->name_length + 2); + + /* Copy the parent name over. */ + fill = mempcpy (vp->name, input->name, input->name_length); + + /* Check if it contains a trailing slash. input->name cannot be + empty, as the root vnode's name is `/'. */ + + if (fill[-1] != '/' && *name != '/') + /* If not, append a trailing slash and adjust vp->name_length + correspondingly. */ + *fill++ = '/', vp->name_length++; + else if (fill[-1] == '/' && *name == '/') + /* If name has a leading slash and fill does too, move fill + backwards so that name's slash will override that of fill. */ + fill--, vp->name_length--; + + /* Now copy NAME. */ + fill = mempcpy (fill, name, length); + + /* And NULL terminate fill. */ + *fill = '\0'; + return &vp->vnode; + + parent_vnode: + /* .. was encountered and the parent couldn't be found through + stripping off preceding components. + + Find the parent vnode and name the rest of NAME starting from + there. */ + + if (input->name_length == 1) + /* This is the vnode representing the root directory; just look + within itself... */ + vnode = &root_vnode.vnode; + else + { + /* Create a temporary asset vnode within the parent and use it + instead. First, establish the length of vp->name before its + last component. */ + + for (j = input->name_length - 1; j; --j) + { + if (input->name[j - 1] == '/') + break; + } + + /* There must be at least one leading directory separator in an + asset vnode's `name' field. */ + + if (!j) + abort (); + + /* j is now the length of the string minus the size of its last + component. Create a temporary vnode with that as its + name. */ + + temp.vnode.ops = &unix_vfs_ops; + temp.vnode.type = ANDROID_VNODE_UNIX; + temp.vnode.flags = 0; + temp.name_length = j; + temp.name = xmalloc (j + 1); + fill = mempcpy (temp.name, input->name, j); + *fill = '\0'; + + /* Search for the remainder of NAME relative to its parent. */ + vnode = android_unix_name (&temp.vnode, remainder, + strlen (remainder)); + xfree (temp.name); + return vnode; + } + + return (*vnode->ops->name) (vnode, remainder, strlen (remainder)); +} + +/* Create a Unix vnode representing the given file NAME. Use this + function to create vnodes that aren't rooted in the root VFS + node. */ + +static struct android_vnode * +android_unix_vnode (const char *name) +{ + struct android_unix_vnode *vp; + + vp = xmalloc (sizeof *vp); + vp->vnode.ops = &unix_vfs_ops; + vp->vnode.type = ANDROID_VNODE_UNIX; + vp->vnode.flags = 0; + vp->name_length = strlen (name); + vp->name = xstrdup (name); + return &vp->vnode; +} + +static int +android_unix_open (struct android_vnode *vnode, int flags, + mode_t mode, bool asset_p, int *fd, + AAsset **asset) +{ + struct android_unix_vnode *vp; + int fds; + + vp = (struct android_unix_vnode *) vnode; + fds = open (vp->name, flags, mode); + + if (fds < 0) + return -1; + + *fd = fds; + return 0; +} + +static void +android_unix_close (struct android_vnode *vnode) +{ + struct android_unix_vnode *vp; + int save_errno; + + save_errno = errno; + vp = (struct android_unix_vnode *) vnode; + xfree (vp->name); + xfree (vp); + errno = save_errno; +} + +static int +android_unix_unlink (struct android_vnode *vnode) +{ + struct android_unix_vnode *vp; + + vp = (struct android_unix_vnode *) vnode; + return unlink (vp->name); +} + +static int +android_unix_symlink (const char *target, struct android_vnode *vnode) +{ + struct android_unix_vnode *vp; + + vp = (struct android_unix_vnode *) vnode; + return symlink (target, vp->name); +} + +static int +android_unix_rmdir (struct android_vnode *vnode) +{ + struct android_unix_vnode *vp; + + vp = (struct android_unix_vnode *) vnode; + return rmdir (vp->name); +} + +static int +android_unix_rename (struct android_vnode *src, + struct android_vnode *dst, + bool keep_existing) +{ + struct android_unix_vnode *vp, *dest; + + if (src->type != dst->type) + { + /* If the types of both vnodes differ, complain that they're on + two different filesystems (which is correct from a abstract + viewpoint.) */ + errno = EXDEV; + return -1; + } + + vp = (struct android_unix_vnode *) src; + dest = (struct android_unix_vnode *) dst; + + return (keep_existing + ? renameat_noreplace (AT_FDCWD, vp->name, + AT_FDCWD, dest->name) + : rename (vp->name, dest->name)); +} + +static int +android_unix_stat (struct android_vnode *vnode, struct stat *statb) +{ + struct android_unix_vnode *vp; + + vp = (struct android_unix_vnode *) vnode; + return stat (vp->name, statb); +} + +static int +android_unix_access (struct android_vnode *vnode, int mode) +{ + struct android_unix_vnode *vp; + + vp = (struct android_unix_vnode *) vnode; + return access (vp->name, mode); +} + +static int +android_unix_mkdir (struct android_vnode *vnode, mode_t mode) +{ + struct android_unix_vnode *vp; + + vp = (struct android_unix_vnode *) vnode; + return mkdir (vp->name, mode); +} + +static struct dirent * +android_unix_readdir (struct android_vdir *vdir) +{ + struct android_unix_vdir *dir; + + dir = (struct android_unix_vdir *) vdir; + return readdir (dir->directory); +} + +static void +android_unix_closedir (struct android_vdir *vdir) +{ + struct android_unix_vdir *dir; + + dir = (struct android_unix_vdir *) vdir; + closedir (dir->directory); + xfree (vdir); +} + +static int +android_unix_dirfd (struct android_vdir *vdir) +{ + struct android_unix_vdir *dir; + + dir = (struct android_unix_vdir *) vdir; + return dirfd (dir->directory); +} + +static struct android_vdir * +android_unix_opendir (struct android_vnode *vnode) +{ + struct android_unix_vnode *vp; + struct android_unix_vdir *dir; + DIR *directory; + + /* Try to opendir the vnode. */ + vp = (struct android_unix_vnode *) vnode; + directory = opendir (vp->name); + + if (!directory) + return NULL; + + dir = xmalloc (sizeof *dir); + dir->vdir.readdir = android_unix_readdir; + dir->vdir.closedir = android_unix_closedir; + dir->vdir.dirfd = android_unix_dirfd; + dir->directory = directory; + return &dir->vdir; +} + + + +/* Asset directory handling functions. ``directory-tree'' is a file in + the root of the assets directory describing its contents. + + See lib-src/asset-directory-tool for more details. */ + +/* The Android directory tree. */ +static const char *directory_tree; + +/* The size of the directory tree. */ +static size_t directory_tree_size; + +/* The asset manager being used. */ +static AAssetManager *asset_manager; + +/* Read an unaligned (32-bit) long from the address POINTER. */ + +static unsigned int +android_extract_long (char *pointer) +{ + unsigned int number; + + memcpy (&number, pointer, sizeof number); + return number; +} + +/* Scan to the file FILE in the asset directory tree. Return a + pointer to the end of that file (immediately before any children) + in the directory tree, or NULL if that file does not exist. + + If returning non-NULL, also return the offset to the end of the + last subdirectory or file in *LIMIT_RETURN. LIMIT_RETURN may be + NULL. + + FILE must have less than 11 levels of nesting. If it ends with a + trailing slash, then NULL will be returned if it is not actually a + directory. */ + +static const char * +android_scan_directory_tree (char *file, size_t *limit_return) +{ + char *token, *saveptr, *copy, *copy1, *start, *max, *limit; + size_t token_length, ntokens, i; + char *tokens[10]; + + USE_SAFE_ALLOCA; + + /* Skip past the 5 byte header. */ + start = (char *) directory_tree + 5; + + /* Figure out the current limit. */ + limit = (char *) directory_tree + directory_tree_size; + + /* Now, split `file' into tokens, with the delimiter being the file + name separator. Look for the file and seek past it. */ + + ntokens = 0; + saveptr = NULL; + copy = copy1 = xstrdup (file); + memset (tokens, 0, sizeof tokens); + + while ((token = strtok_r (copy, "/", &saveptr))) + { + copy = NULL; + + /* Make sure ntokens is within bounds. */ + if (ntokens == ARRAYELTS (tokens)) + { + xfree (copy1); + goto fail; + } + + tokens[ntokens] = SAFE_ALLOCA (strlen (token) + 1); + memcpy (tokens[ntokens], token, strlen (token) + 1); + ntokens++; + } + + /* Free the copy created for strtok_r. */ + xfree (copy1); + + /* If there are no tokens, just return the start of the directory + tree. */ + + if (!ntokens) + { + SAFE_FREE (); + + /* Return the size of the directory tree as the limit. + Do not subtract the initial header bytes, as the limit + is an offset from the start of the file. */ + + if (limit_return) + *limit_return = directory_tree_size; + + return start; + } + + /* Loop through tokens, indexing the directory tree each time. */ + + for (i = 0; i < ntokens; ++i) + { + token = tokens[i]; + + /* Figure out how many bytes to compare. */ + token_length = strlen (token); + + again: + + /* If this would be past the directory, return NULL. */ + if (start + token_length > limit) + goto fail; + + /* Now compare the file name. */ + if (!memcmp (start, token, token_length)) + { + /* They probably match. Find the NULL byte. It must be + either one byte past start + token_length, with the last + byte a trailing slash (indicating that it is a + directory), or just start + token_length. Return 4 bytes + past the next NULL byte. */ + + max = memchr (start, 0, limit - start); + + if (max != start + token_length + && !(max == start + token_length + 1 + && *(max - 1) == '/')) + goto false_positive; + + /* Return it if it exists and is in range, and this is the + last token. Otherwise, set it as start and the limit as + start + the offset and continue the loop. */ + + if (max && max + 5 <= limit) + { + if (i < ntokens - 1) + { + start = max + 5; + limit = ((char *) directory_tree + + android_extract_long (max + 1)); + + /* Make sure limit is still in range. */ + if (limit > directory_tree + directory_tree_size + || start > directory_tree + directory_tree_size) + goto fail; + + continue; + } + + /* Now see if max is not a directory and file is. If + file is a directory, then return NULL. */ + if (*(max - 1) != '/' && file[strlen (file) - 1] == '/') + max = NULL; + else + { + /* Figure out the limit. */ + if (limit_return) + *limit_return = android_extract_long (max + 1); + + /* Go to the end of this file. */ + max += 5; + } + + SAFE_FREE (); + return max; + } + + /* Return NULL otherwise. */ + __android_log_print (ANDROID_LOG_WARN, __func__, + "could not scan to end of directory tree" + ": %s", file); + goto fail; + } + + false_positive: + + /* No match was found. Set start to the next sibling and try + again. */ + + start = memchr (start, 0, limit - start); + + if (!start || start + 5 > limit) + goto fail; + + start = ((char *) directory_tree + + android_extract_long (start + 1)); + + /* Make sure start is still in bounds. */ + + if (start > limit) + goto fail; + + /* Continue the loop. */ + goto again; + } + + fail: + SAFE_FREE (); + return NULL; +} + +/* Return whether or not the directory tree entry DIR is a + directory. + + DIR should be a value returned by + `android_scan_directory_tree'. */ + +static bool +android_is_directory (const char *dir) +{ + /* If the directory is the directory tree, then it is a + directory. */ + if (dir == directory_tree + 5) + return true; + + /* Otherwise, look 5 bytes behind. If it is `/', then it is a + directory. */ + return (dir - 6 >= directory_tree + && *(dir - 6) == '/'); +} + +/* Initialize asset retrieval. ENV should be a JNI environment for + the Emacs thread, and MANAGER should be a local reference to a Java + asset manager object created for the Emacs service context. */ + +static void +android_init_assets (JNIEnv *env, jobject manager) +{ + AAsset *asset; + + /* Set the asset manager. */ + asset_manager = AAssetManager_fromJava (env, manager); + + /* Initialize the directory tree. */ + asset = AAssetManager_open (asset_manager, "directory-tree", + AASSET_MODE_BUFFER); + + if (!asset) + { + __android_log_print (ANDROID_LOG_FATAL, __func__, + "Failed to open directory tree"); + emacs_abort (); + } + + directory_tree = AAsset_getBuffer (asset); + + if (!directory_tree) + emacs_abort (); + + /* Now figure out how big the directory tree is, and compare the + first few bytes. */ + directory_tree_size = AAsset_getLength (asset); + if (directory_tree_size < 5 + || memcmp (directory_tree, "EMACS", 5)) + { + __android_log_print (ANDROID_LOG_FATAL, __func__, + "Directory tree has bad magic"); + emacs_abort (); + } + + /* Hold a VM reference to the asset manager to prevent the native + object from being deleted. */ + (*env)->NewGlobalRef (env, manager); + + /* Abort if there's no more memory for the global reference. */ + if ((*env)->ExceptionCheck (env)) + abort (); +} + + + +/* Asset-to-file descriptor conversion. */ + +/* Pointer to the `ASharedMemory_create' function which is loaded + dynamically. */ +static int (*asharedmemory_create) (const char *, size_t); + +/* Do the same as android_hack_asset_fd, but use an unlinked temporary + file to cater to old Android kernels where ashmem files are not + readable. */ + +static int +android_hack_asset_fd_fallback (AAsset *asset) +{ + int fd; + char filename[PATH_MAX]; + size_t size; + void *mem; + + /* Assets must be small enough to fit in size_t, if off_t is + larger. */ + size = AAsset_getLength (asset); + + /* Get an unlinked file descriptor from a file in the cache + directory, which is guaranteed to only be written to by Emacs. + Creating an ashmem file descriptor and reading from it doesn't + work on these old Android versions. */ + + snprintf (filename, PATH_MAX, "%s/temp~unlinked.%d", + android_cache_dir, getpid ()); + fd = open (filename, O_CREAT | O_RDWR | O_TRUNC, + S_IRUSR | S_IWUSR); + + if (fd < 0) + return -1; + + if (unlink (filename)) + goto fail; + + if (ftruncate (fd, size)) + goto fail; + + mem = mmap (NULL, size, PROT_WRITE, MAP_SHARED, fd, 0); + if (mem == MAP_FAILED) + { + __android_log_print (ANDROID_LOG_ERROR, __func__, + "mmap: %s", strerror (errno)); + goto fail; + } + + if (AAsset_read (asset, mem, size) != size) + { + /* Too little was read. Close the file descriptor and + report an error. */ + __android_log_print (ANDROID_LOG_ERROR, __func__, + "AAsset_read: %s", strerror (errno)); + goto fail; + } + + munmap (mem, size); + return fd; + + fail: + close (fd); + return -1; +} + +/* Return whether or not shared memory file descriptors can also be + read from, and are thus suitable for creating asset files. + + This does not work on some ancient Android systems running old + versions of the kernel. */ + +static bool +android_detect_ashmem (void) +{ + int fd, rc; + void *mem; + char test_buffer[10]; + + memcpy (test_buffer, "abcdefghi", 10); + + /* Create the file descriptor to be used for the test. */ + + /* Android 28 and earlier let Emacs access /dev/ashmem directly, so + prefer that over using ASharedMemory. */ + + if (android_get_current_api_level () <= 28) + { + fd = open ("/dev/ashmem", O_RDWR); + + if (fd < 0) + return false; + + /* An empty name means the memory area will exist until the file + descriptor is closed, because no other process can + attach. */ + rc = ioctl (fd, ASHMEM_SET_NAME, ""); + + if (rc < 0) + { + close (fd); + return false; + } + + rc = ioctl (fd, ASHMEM_SET_SIZE, sizeof test_buffer); + + if (rc < 0) + { + close (fd); + return false; + } + } + else + { + /* On the other hand, SELinux restrictions on Android 29 and + later require that Emacs use a system service to obtain + shared memory. Load this dynamically, as this service is not + available on all versions of the NDK. */ + + if (!asharedmemory_create) + { + *(void **) (&asharedmemory_create) + = dlsym (RTLD_DEFAULT, "ASharedMemory_create"); + + if (!asharedmemory_create) + { + __android_log_print (ANDROID_LOG_FATAL, __func__, + "dlsym: %s\n", + strerror (errno)); + emacs_abort (); + } + } + + fd = (*asharedmemory_create) ("", sizeof test_buffer); + + if (fd < 0) + return false; + } + + /* Now map the resource and write the test contents. */ + + mem = mmap (NULL, sizeof test_buffer, PROT_WRITE, + MAP_SHARED, fd, 0); + if (mem == MAP_FAILED) + { + close (fd); + return false; + } + + /* Copy over the test contents. */ + memcpy (mem, test_buffer, sizeof test_buffer); + + /* Return anyway even if munmap fails. */ + munmap (mem, sizeof test_buffer); + + /* Try to read the content back into test_buffer. If this does not + compare equal to the original string, or the read fails, then + ashmem descriptors are not readable on this system. */ + + if ((read (fd, test_buffer, sizeof test_buffer) + != sizeof test_buffer) + || memcmp (test_buffer, "abcdefghi", sizeof test_buffer)) + { + __android_log_print (ANDROID_LOG_WARN, __func__, + "/dev/ashmem does not produce real" + " temporary files on this system, so" + " Emacs will fall back to creating" + " unlinked temporary files."); + close (fd); + return false; + } + + close (fd); + return true; +} + +/* Get a file descriptor backed by a temporary in-memory file for the + given asset. */ + +static int +android_hack_asset_fd (AAsset *asset) +{ + static bool ashmem_readable_p; + static bool ashmem_initialized; + int fd, rc; + unsigned char *mem; + size_t size; + + /* The first time this function is called, try to determine whether + or not ashmem file descriptors can be read from. */ + + if (!ashmem_initialized) + ashmem_readable_p + = android_detect_ashmem (); + ashmem_initialized = true; + + /* If it isn't, fall back. */ + + if (!ashmem_readable_p) + return android_hack_asset_fd_fallback (asset); + + /* Assets must be small enough to fit in size_t, if off_t is + larger. */ + size = AAsset_getLength (asset); + + /* Android 28 and earlier let Emacs access /dev/ashmem directly, so + prefer that over using ASharedMemory. */ + + if (android_get_current_api_level () <= 28) + { + fd = open ("/dev/ashmem", O_RDWR); + + if (fd < 0) + return -1; + + /* An empty name means the memory area will exist until the file + descriptor is closed, because no other process can + attach. */ + rc = ioctl (fd, ASHMEM_SET_NAME, ""); + + if (rc < 0) + { + __android_log_print (ANDROID_LOG_ERROR, __func__, + "ioctl ASHMEM_SET_NAME: %s", + strerror (errno)); + close (fd); + return -1; + } + + rc = ioctl (fd, ASHMEM_SET_SIZE, size); + + if (rc < 0) + { + __android_log_print (ANDROID_LOG_ERROR, __func__, + "ioctl ASHMEM_SET_SIZE: %s", + strerror (errno)); + close (fd); + return -1; + } + + if (!size) + return fd; + + /* Now map the resource. */ + mem = mmap (NULL, size, PROT_WRITE, MAP_SHARED, fd, 0); + if (mem == MAP_FAILED) + { + __android_log_print (ANDROID_LOG_ERROR, __func__, + "mmap: %s", strerror (errno)); + close (fd); + return -1; + } + + if (AAsset_read (asset, mem, size) != size) + { + /* Too little was read. Close the file descriptor and + report an error. */ + __android_log_print (ANDROID_LOG_ERROR, __func__, + "AAsset_read: %s", strerror (errno)); + close (fd); + return -1; + } + + /* Return anyway even if munmap fails. */ + munmap (mem, size); + return fd; + } + + /* On the other hand, SELinux restrictions on Android 29 and later + require that Emacs use a system service to obtain shared memory. + Load this dynamically, as this service is not available on all + versions of the NDK. */ + + if (!asharedmemory_create) + { + *(void **) (&asharedmemory_create) + = dlsym (RTLD_DEFAULT, "ASharedMemory_create"); + + if (!asharedmemory_create) + { + __android_log_print (ANDROID_LOG_FATAL, __func__, + "dlsym: %s\n", + strerror (errno)); + emacs_abort (); + } + } + + fd = (*asharedmemory_create) ("", size); + + if (fd < 0) + { + __android_log_print (ANDROID_LOG_ERROR, __func__, + "ASharedMemory_create: %s", + strerror (errno)); + return -1; + } + + /* Now map the resource. */ + mem = mmap (NULL, size, PROT_WRITE, MAP_SHARED, fd, 0); + if (mem == MAP_FAILED) + { + __android_log_print (ANDROID_LOG_ERROR, __func__, + "mmap: %s", strerror (errno)); + close (fd); + return -1; + } + + if (AAsset_read (asset, mem, size) != size) + { + /* Too little was read. Close the file descriptor and + report an error. */ + __android_log_print (ANDROID_LOG_ERROR, __func__, + "AAsset_read: %s", strerror (errno)); + close (fd); + return -1; + } + + /* Return anyway even if munmap fails. */ + munmap (mem, size); + return fd; +} + + + +/* ``Asset file system'' vnode implementation. These vnodes map to + asset files within the application package, provided by the Android + ``asset manager''. */ + +struct android_afs_vnode +{ + /* The vnode data itself. */ + struct android_vnode vnode; + + /* Length of the name without a trailing null byte. */ + size_t name_length; + + /* Name of the vnode. */ + char *name; +}; + +struct android_afs_vdir +{ + /* The directory function table. */ + struct android_vdir vdir; + + /* The next directory stream in `all_afs_vdirs'. */ + struct android_afs_vdir *next; + + /* Pointer to the directory in directory_tree. */ + char *asset_dir; + + /* And the end of the files in asset_dir. */ + char *asset_limit; + + /* Path to the directory relative to /. */ + char *asset_file; + + /* File descriptor representing this directory stream, or NULL. */ + int fd; +}; + +struct android_afs_open_fd +{ + /* The next table entry. */ + struct android_afs_open_fd *next; + + /* The open file descriptor. */ + int fd; + + /* The stat buffer associated with this entry. */ + struct stat statb; +}; + +static struct android_vnode *android_afs_name (struct android_vnode *, + char *, size_t); +static int android_afs_open (struct android_vnode *, int, + mode_t, bool, int *, AAsset **); +static void android_afs_close (struct android_vnode *); +static int android_afs_unlink (struct android_vnode *); +static int android_afs_symlink (const char *, struct android_vnode *); +static int android_afs_rmdir (struct android_vnode *); +static int android_afs_rename (struct android_vnode *, + struct android_vnode *, bool); +static int android_afs_stat (struct android_vnode *, struct stat *); +static int android_afs_access (struct android_vnode *, int); +static int android_afs_mkdir (struct android_vnode *, mode_t); +static struct android_vdir *android_afs_opendir (struct android_vnode *); + +/* Vector of VFS operations associated with asset VFS nodes. */ + +static struct android_vops afs_vfs_ops = + { + android_afs_name, + 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_opendir, + }; + +/* Chain consisting of all open asset directory streams. */ +static struct android_afs_vdir *all_afs_vdirs; + +/* List linking open file descriptors to asset information. This + assumes Emacs does not use dup on regular files. */ +static struct android_afs_open_fd *afs_file_descriptors; + +static struct android_vnode * +android_afs_name (struct android_vnode *vnode, char *name, + size_t length) +{ + size_t j; + char *remainder, *fill; + struct android_afs_vnode *vp, *input; + struct android_afs_vnode temp; + + input = (struct android_afs_vnode *) vnode; + + /* Canonicalize NAME. */ + remainder = android_vfs_canonicalize_name (name, &length); + + /* If remainder is set, it's a name relative to the parent + vnode. */ + if (remainder) + goto parent_vnode; + + /* Allocate a new vnode. */ + vp = xmalloc (sizeof *vp); + + /* See the specified name is empty. */ + + if (length < 1) + { + memcpy (vp, vnode, sizeof *vp); + vp->name = xstrdup (vp->name); + return &vp->vnode; + } + + /* Recompute length. */ + vp->vnode.ops = &afs_vfs_ops; + vp->vnode.type = ANDROID_VNODE_AFS; + vp->vnode.flags = 0; + + /* Generate the new name of the vnode. Remove any trailing slash + from vp->name. */ + + vp->name_length = input->name_length + length; + vp->name = xmalloc (vp->name_length + 2); + + /* Copy the parent name over. */ + fill = mempcpy (vp->name, input->name, input->name_length); + + /* Check if it contains a trailing slash. input->name cannot be + empty, as the root vnode's name is `/'. */ + + if (fill[-1] != '/' && *name != '/') + /* If not, append a trailing slash and adjust vp->name_length + correspondingly. */ + *fill++ = '/', vp->name_length++; + else if (fill[-1] == '/' && *name == '/') + /* If name has a leading slash and fill does too, move fill + backwards so that name's slash will override that of fill. */ + fill--, vp->name_length--; + + /* Now copy NAME. */ + fill = mempcpy (fill, name, length); + + /* And NULL terminate fill. */ + *fill = '\0'; + return &vp->vnode; + + parent_vnode: + /* .. was encountered and the parent couldn't be found through + stripping off preceding components. + + Find the parent vnode and name the rest of NAME starting from + there. */ + + if (input->name_length == 1) + /* This is the vnode representing the /assets directory... */ + vnode = &root_vnode.vnode; + else + { + /* Create a temporary asset vnode within the parent and use it + instead. First, establish the length of vp->name before its + last component. */ + + for (j = input->name_length - 1; j; --j) + { + if (input->name[j - 1] == '/') + break; + } + + /* There must be at least one leading directory separator in an + asset vnode's `name' field. */ + + if (!j) + abort (); + + /* j is now the length of the string minus the size of its last + component. Create a temporary vnode with that as its + name. */ + + temp.vnode.ops = &afs_vfs_ops; + temp.vnode.type = ANDROID_VNODE_AFS; + temp.vnode.flags = 0; + temp.name_length = j; + temp.name = xmalloc (j + 1); + fill = mempcpy (temp.name, input->name, j); + *fill = '\0'; + + /* Search for the remainder of NAME relative to its parent. */ + vnode = android_afs_name (&temp.vnode, remainder, + strlen (remainder)); + xfree (temp.name); + return vnode; + } + + return (*vnode->ops->name) (vnode, remainder, strlen (remainder)); +} + +/* Find the vnode designated by the normalized NAME relative to the + root of the asset file system. NAME may be modified, and must be + LENGTH bytes long, excluding its terminating NULL byte. */ + +static struct android_vnode * +android_afs_initial (char *name, size_t length) +{ + struct android_afs_vnode temp; + + /* Create a temporary vnode at the root of the asset file + system. */ + + temp.vnode.ops = &afs_vfs_ops; + temp.vnode.type = ANDROID_VNODE_AFS; + temp.vnode.flags = 0; + temp.name_length = 1; + temp.name = "/"; + + /* Try to name this vnode. If NAME is empty, it will be duplicated + instead. */ + return android_afs_name (&temp.vnode, name, length); +} + +/* Make FD close-on-exec. If any system call fails, do not abort, but + log a warning to the system log. */ + +static void +android_close_on_exec (int fd) +{ + int flags, rc; + + flags = fcntl (fd, F_GETFD); + + if (flags < 0) + { + __android_log_print (ANDROID_LOG_WARN, __func__, + "fcntl: %s", strerror (errno)); + return; + } + + rc = fcntl (fd, F_SETFD, flags | O_CLOEXEC); + + if (rc < 0) + { + __android_log_print (ANDROID_LOG_WARN, __func__, + "fcntl: %s", strerror (errno)); + return; + } +} + +static int +android_afs_open (struct android_vnode *vnode, int flags, + mode_t mode, bool asset_p, int *fd_return, + AAsset **asset_return) +{ + AAsset *asset; + struct android_afs_vnode *vp; + const char *asset_dir; + int fd; + struct android_afs_open_fd *info; + + vp = (struct android_afs_vnode *) vnode; + + /* Return suitable error indications for unsupported file + operations. */ + + if ((flags & O_WRONLY) || (flags & O_RDWR)) + { + errno = EROFS; + return -1; + } + + if (flags & O_DIRECTORY) + { + errno = ENOSYS; + return -1; + } + + /* Now try to open this asset. Asset manager APIs expect there to + be no trailing directory separator. */ + asset = AAssetManager_open (asset_manager, vp->name + 1, + AASSET_MODE_STREAMING); + + /* If it can't be opened, return an error indication. */ + + if (!asset) + { + /* Scan the directory tree for this file. */ + asset_dir = android_scan_directory_tree (vp->name, NULL); + + /* Default errno to ENOTENT. */ + errno = ENOENT; + + /* Maybe the caller wants to open a directory vnode as a + file? */ + + if (asset_dir && android_is_directory (asset_dir)) + /* In that case, set errno to ENOSYS. */ + errno = ENOSYS; + + return -1; + } + + /* An asset has been opened. If the caller wants a file descriptor, + a temporary one must be created and the file contents read + inside. */ + + if (!asset_p) + { + /* Create a shared memory file descriptor containing the asset + contents. + + The documentation misleads people into thinking that + AAsset_openFileDescriptor does precisely this. However, it + instead returns an offset into any uncompressed assets in the + ZIP archive. This cannot be found in its documentation. */ + + fd = android_hack_asset_fd (asset); + + if (fd == -1) + { + AAsset_close (asset); + errno = EIO; + return -1; + } + + /* If O_CLOEXEC is specified, make the file descriptor close on + exec too. */ + + if (flags & O_CLOEXEC) + android_close_on_exec (fd); + + /* Keep a record linking ``hacked'' file descriptors with + their file status. */ + info = xzalloc (sizeof *info); + info->fd = fd; + info->next = afs_file_descriptors; + + /* Fill in some information that will be reported to + callers of android_fstat, among others. */ + info->statb.st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + + /* Owned by root. */ + info->statb.st_uid = 0; + info->statb.st_gid = 0; + + /* Concoct a nonexistent device and an inode number. */ + info->statb.st_dev = -1; + info->statb.st_ino = 0; + + /* Size of the file. */ + info->statb.st_size = AAsset_getLength (asset); + + /* Chain info onto afs_file_descriptors. */ + afs_file_descriptors = info; + + AAsset_close (asset); + + /* Return the file descriptor. */ + *fd_return = fd; + return 0; + } + + /* Return the asset itself. */ + *asset_return = asset; + return 1; +} + +static void +android_afs_close (struct android_vnode *vnode) +{ + struct android_afs_vnode *vp; + int save_errno; + + save_errno = errno; + vp = (struct android_afs_vnode *) vnode; + xfree (vp->name); + xfree (vp); + errno = save_errno; +} + +static int +android_afs_unlink (struct android_vnode *vnode) +{ + const char *dir; + struct android_afs_vnode *vp; + + /* If the vnode already exists, return EROFS. Else, return + ENOENT. */ + + vp = (struct android_afs_vnode *) vnode; + dir = android_scan_directory_tree (vp->name, NULL); + + if (dir) + errno = EROFS; + else + errno = ENOENT; + + return -1; +} + +static int +android_afs_symlink (const char *linkname, struct android_vnode *vnode) +{ + struct android_afs_vnode *vp; + + /* If this vnode already exists, return EEXIST. */ + vp = (struct android_afs_vnode *) vnode; + + if (android_scan_directory_tree (vp->name, NULL)) + { + errno = EEXIST; + return -1; + } + + /* Symlinks aren't supported on this (read-only) ``file system'', + so return -1 with EROFS. */ + errno = EROFS; + return -1; +} + +static int +android_afs_rmdir (struct android_vnode *vnode) +{ + const char *dir; + struct android_afs_vnode *vp; + + /* If the vnode already exists and is a directory, return EROFS. + Else, return ENOTDIR or ENOENT. */ + + vp = (struct android_afs_vnode *) vnode; + dir = android_scan_directory_tree (vp->name, NULL); + + if (dir && android_is_directory (dir)) + errno = EROFS; + else if (dir) + errno = ENOTDIR; + else + errno = ENOENT; + + return -1; +} + +static int +android_afs_rename (struct android_vnode *src, struct android_vnode *dst, + bool keep_existing) +{ + /* If src and dst are different kinds of vnodes, return EXDEV. + Else, return EROFS. */ + + errno = EROFS; + if (src->type != dst->type) + errno = EXDEV; + + return -1; +} + +static int +android_afs_stat (struct android_vnode *vnode, struct stat *statb) +{ + const char *dir; + struct android_afs_vnode *vp; + AAsset *asset_desc; + + /* Scan for the vnode to see whether or not it exists. */ + + vp = (struct android_afs_vnode *) vnode; + dir = android_scan_directory_tree (vp->name, NULL); + + if (!dir) + { + /* Return ENOENT; whether the lookup failed because directory + components within vp->path weren't really directories is not + important to Emacs's error reporting. */ + errno = ENOENT; + return -1; + } + + if (android_is_directory (dir)) + { + memset (statb, 0, sizeof *statb); + + /* Fill in the stat buffer. */ + statb->st_mode = S_IFDIR | S_IRUSR | S_IRGRP | S_IROTH; + + /* Concoct a nonexistent device and an inode number. */ + statb->st_dev = -1; + statb->st_ino = 0; + return 0; + } + + /* AASSET_MODE_STREAMING is fastest here. */ + asset_desc = AAssetManager_open (asset_manager, vp->name + 1, + AASSET_MODE_STREAMING); + + if (!asset_desc) + { + /* If the asset exists in the directory tree but can't be + located by the asset manager, report OOM. */ + errno = ENOMEM; + return 1; + } + + memset (statb, 0, sizeof *statb); + + /* Fill in the stat buffer. */ + statb->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + statb->st_dev = -1; + statb->st_ino = 0; + statb->st_size = AAsset_getLength (asset_desc); + + /* Close the asset. */ + AAsset_close (asset_desc); + return 0; +} + +static int +android_afs_access (struct android_vnode *vnode, int mode) +{ + const char *dir; + struct android_afs_vnode *vp; + + /* Validate MODE. */ + + if (mode != F_OK && !(mode & (W_OK | X_OK | R_OK))) + { + errno = EINVAL; + return -1; + } + + /* Scan for the vnode to see whether or not it exists. */ + + vp = (struct android_afs_vnode *) vnode; + dir = android_scan_directory_tree (vp->name, NULL); + + if (dir) + { + /* It exists. If MODE contains W_OK or X_OK, return + EACCESS. */ + + if (mode & (W_OK | X_OK)) + { + errno = EACCES; + return -1; + } + + /* If vp->name is a directory and DIR isn't, return ENOTDIR. */ + + if (vp->name[vp->name_length] == '/' + && !android_is_directory (dir)) + { + errno = ENOTDIR; + return -1; + } + + return 0; + } + + errno = ENOENT; + return -1; +} + +static int +android_afs_mkdir (struct android_vnode *vnode, mode_t mode) +{ + struct android_afs_vnode *vp; + const char *dir; + + /* If the vnode already exists, return EEXIST in lieu of EROFS. */ + + vp = (struct android_afs_vnode *) vnode; + dir = android_scan_directory_tree (vp->name, NULL); + + if (dir) + errno = EEXIST; + else + errno = EROFS; + + return -1; +} + +static struct dirent * +android_afs_readdir (struct android_vdir *vdir) +{ + static struct dirent dirent; + const char *last; + struct android_afs_vdir *dir; + + dir = (struct android_afs_vdir *) vdir; + + /* There are no more files to read. */ + if (dir->asset_dir >= dir->asset_limit) + return NULL; + + /* Otherwise, scan forward looking for the next NULL byte. */ + last = memchr (dir->asset_dir, 0, + dir->asset_limit - dir->asset_dir); + + /* No more NULL bytes remain. */ + if (!last) + return NULL; + + /* Forward last past the NULL byte. */ + last++; + + /* Make sure it is still within the directory tree. */ + if (last >= directory_tree + directory_tree_size) + return NULL; + + /* Now, fill in the dirent with the name. */ + memset (&dirent, 0, sizeof dirent); + dirent.d_ino = 0; + dirent.d_off = 0; + dirent.d_reclen = sizeof dirent; + + /* Note that dir->asset_dir is actually a NULL terminated + string. */ + memcpy (dirent.d_name, dir->asset_dir, + MIN (sizeof dirent.d_name, + last - dir->asset_dir)); + dirent.d_name[sizeof dirent.d_name - 1] = '\0'; + + /* Strip off the trailing slash, if any. */ + if (dirent.d_name[MIN (sizeof dirent.d_name, + last - dir->asset_dir) + - 2] == '/') + dirent.d_name[MIN (sizeof dirent.d_name, + last - dir->asset_dir) + - 2] = '\0'; + + /* If this is not a directory, return DT_REG. Otherwise, return + DT_DIR. */ + + if (last - 2 >= directory_tree && last[-2] == '/') + dirent.d_type = DT_DIR; + else + dirent.d_type = DT_REG; + + /* Forward dir->asset_dir to the file past last. */ + dir->asset_dir = ((char *) directory_tree + + android_extract_long ((char *) last)); + + return &dirent; +} + +static void +android_afs_closedir (struct android_vdir *vdir) +{ + struct android_afs_vdir *dir, **next, *tem; + + dir = (struct android_afs_vdir *) vdir; + + /* If the ``directory file descriptor'' has been opened, close + it. */ + + if (dir->fd != -1) + close (dir->fd); + + xfree (dir->asset_file); + + /* Now unlink this directory. */ + + for (next = &all_afs_vdirs; (tem = *next);) + { + if (tem == dir) + *next = dir->next; + else + next = &(*next)->next; + } + + /* Free the directory itself. */ + + xfree (dir); +} + +static int +android_afs_dirfd (struct android_vdir *vdir) +{ + struct android_afs_vdir *dir; + + dir = (struct android_afs_vdir *) vdir; + + /* Since `android_afs_opendir' tries to avoid opening a file + descriptor if readdir isn't called, dirfd can fail if open fails. + + open sets errno to a set of errors different from what POSIX + stipulates for dirfd, but for ease of implementation the open + errors are used instead. */ + + if (dir->fd >= 0) + return dir->fd; + + dir->fd = open ("/dev/null", O_RDONLY | O_CLOEXEC); + return dir->fd; +} + +static struct android_vdir * +android_afs_opendir (struct android_vnode *vnode) +{ + char *asset_dir; + struct android_afs_vdir *dir; + struct android_afs_vnode *vp; + size_t limit; + + vp = (struct android_afs_vnode *) vnode; + + /* Scan for the asset directory by vp->name. */ + + asset_dir + = (char *) android_scan_directory_tree (vp->name, &limit); + + if (!asset_dir) + { + errno = ENOENT; + return NULL; + } + + /* Verify that asset_dir is indeed a directory. */ + + if (!android_is_directory (asset_dir)) + { + errno = ENOTDIR; + return NULL; + } + + /* Fill in the directory stream. */ + dir = xmalloc (sizeof *dir); + dir->vdir.readdir = android_afs_readdir; + dir->vdir.closedir = android_afs_closedir; + dir->vdir.dirfd = android_afs_dirfd; + dir->asset_dir = asset_dir; + dir->asset_limit = (char *) directory_tree + limit; + dir->fd = -1; + dir->asset_file = xzalloc (vp->name_length + 2); + strcpy (dir->asset_file, vp->name); + + /* Make sure dir->asset_file is terminated with /. */ + if (dir->asset_file[vp->name_length - 1] != '/') + dir->asset_file[vp->name_length] = '/'; + + /* Make sure dir->asset_limit is within bounds. It is a limit, + and as such can be exactly one byte past directory_tree. */ + if (dir->asset_limit > directory_tree + directory_tree_size) + { + xfree (dir); + xfree (dir->asset_file); + errno = EACCES; + return NULL; + } + + dir->next = all_afs_vdirs; + all_afs_vdirs = dir; + return &dir->vdir; +} + +/* Return the file name corresponding to DIRFD if it is a + ``directory'' file descriptor returned by `android_afs_dirfd' or + NULL otherwise. These file names are relative to the `/assets' + directory, but with a leading separator character. */ + +static char * +android_afs_get_directory_name (int dirfd) +{ + struct android_afs_vdir *dir; + + for (dir = all_afs_vdirs; dir; dir = dir->next) + { + if (dir->fd == dirfd && dirfd != -1) + return dir->asset_file; + } + + return NULL; +} + + + +struct android_content_vdir +{ + /* The directory function table. */ + struct android_vdir vdir; + + /* The next directory stream in `all_content_vdirs'. */ + struct android_content_vdir *next; + + /* Pointer to the next file to return. */ + const char **next_name; + + /* Temporary file descriptor used to identify this directory to + at-funcs, or -1. */ + int fd; +}; + +static struct android_vnode *android_authority_initial (char *, size_t); +static struct android_vnode *android_saf_root_initial (char *, size_t); + +/* Content provider meta-interface. This implements a vnode at + /content, which is a directory itself containing two additional + directories. + + /content/storage only exists on Android 5.0 and later, and contains + a list of each directory tree Emacs has been granted permanent + access to through the Storage Access Framework. + + /content/by-authority exists on Android 4.4 and later; it contains + no directories, but provides a `name' function that converts + children into content URIs. */ + +static struct android_vnode *android_content_name (struct android_vnode *, + char *, size_t); +static int android_content_open (struct android_vnode *, int, + mode_t, bool, int *, AAsset **); +static void android_content_close (struct android_vnode *); +static int android_content_unlink (struct android_vnode *); +static int android_content_symlink (const char *, struct android_vnode *); +static int android_content_rmdir (struct android_vnode *); +static int android_content_rename (struct android_vnode *, + struct android_vnode *, bool); +static int android_content_stat (struct android_vnode *, struct stat *); +static int android_content_access (struct android_vnode *, int); +static int android_content_mkdir (struct android_vnode *, mode_t); +static struct android_vdir *android_content_opendir (struct android_vnode *); + +/* Vector of VFS operations associated with the content VFS node. */ + +static struct android_vops content_vfs_ops = + { + 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_opendir, + }; + +/* Table of directories contained within a top-level vnode. */ + +static const char *content_directory_contents[] = + { + "storage", "by-authority", + }; + +/* Chain consisting of all open content directory streams. */ +static struct android_content_vdir *all_content_vdirs; + +static struct android_vnode * +android_content_name (struct android_vnode *vnode, char *name, + size_t length) +{ + char *remainder; + struct android_vnode *vp; + char *component_end; + struct android_special_vnode *special; + size_t i; + int api; + + static struct android_special_vnode content_vnodes[] = { + { "storage", 7, android_saf_root_initial, }, + { "by-authority", 12, android_authority_initial, }, + }; + + /* Canonicalize NAME. */ + remainder = android_vfs_canonicalize_name (name, &length); + + /* If remainder is set, it's a name relative to the root vnode. */ + if (remainder) + goto parent_vnode; + + /* If LENGTH is empty or NAME is a single directory separator, + return a copy of this vnode. */ + + if (length < 1 || (*name == '/' && length == 1)) + { + vp = xmalloc (sizeof *vp); + memcpy (vp, vnode, sizeof *vp); + return vp; + } + + api = android_get_current_api_level (); + + /* If NAME starts with a directory separator, move it past that. */ + + if (*name == '/') + name++, length -= 1; + + /* Look for the first directory separator. */ + component_end = strchr (name, '/'); + + /* If not there, use name + length. */ + + if (!component_end) + component_end = name + length; + else + /* Move past the separator character. */ + component_end++; + + /* Now, find out if the first component is a special vnode; if so, + call its root lookup function with the rest of NAME there. */ + + if (api < 19) + i = 2; + else if (api < 21) + i = 1; + else + i = 0; + + for (; i < ARRAYELTS (content_vnodes); ++i) + { + special = &content_vnodes[i]; + + if (component_end - name == special->length + && !memcmp (special->name, name, special->length)) + return (*special->initial) (component_end, + length - special->length); + + /* Detect the case where a special is named with a trailing + directory separator. */ + + if (component_end - name == special->length + 1 + && !memcmp (special->name, name, special->length) + && name[special->length] == '/') + /* Make sure to include the directory separator. */ + return (*special->initial) (component_end - 1, + length - special->length); + } + + errno = ENOENT; + return NULL; + + parent_vnode: + /* The parent of this vnode is always the root filesystem. */ + vp = &root_vnode.vnode; + return (*vnode->ops->name) (vnode, remainder, strlen (remainder)); +} + +static int +android_content_open (struct android_vnode *vnode, int flags, + mode_t mode, bool asset_p, int *fd, + AAsset **asset) +{ + /* Don't allow opening this special directory. */ + errno = ENOSYS; + return -1; +} + +static void +android_content_close (struct android_vnode *vnode) +{ + int save_errno; + + save_errno = errno; + xfree (vnode); + errno = save_errno; +} + +static int +android_content_unlink (struct android_vnode *vnode) +{ + errno = ENOSYS; + return -1; +} + +static int +android_content_symlink (const char *target, struct android_vnode *vnode) +{ + errno = ENOSYS; + return -1; +} + +static int +android_content_rmdir (struct android_vnode *vnode) +{ + errno = ENOSYS; + return -1; +} + +static int +android_content_rename (struct android_vnode *src, + struct android_vnode *dst, + bool keep_existing) +{ + if (src->type != dst->type) + { + /* If the types of both vnodes differ, complain that they're on + two different filesystems (which is correct from a abstract + viewpoint.) */ + errno = EXDEV; + return -1; + } + + /* Otherwise, return ENOSYS. */ + errno = ENOSYS; + return -1; +} + +static int +android_content_stat (struct android_vnode *vnode, + struct stat *statb) +{ + memset (statb, 0, sizeof *statb); + + statb->st_uid = getuid (); + statb->st_gid = getgid (); + statb->st_ino = 0; + statb->st_dev = -2; + statb->st_mode = S_IFDIR | S_IRUSR; + return 0; +} + +static int +android_content_access (struct android_vnode *vnode, int mode) +{ + /* Validate MODE. */ + + if (mode != F_OK && !(mode & (W_OK | X_OK | R_OK))) + { + errno = EINVAL; + return -1; + } + + /* Return EROFS if the caller is trying to check for write access to + this vnode. */ + + if (mode != F_OK && (mode & (W_OK | X_OK))) + { + errno = EROFS; + return -1; + } + + return 0; +} + +static int +android_content_mkdir (struct android_vnode *vnode, mode_t mode) +{ + errno = EEXIST; + return 0; +} + +static struct dirent * +android_content_readdir (struct android_vdir *vdir) +{ + static struct dirent dirent; + struct android_content_vdir *dir; + const char *name; + + dir = (struct android_content_vdir *) vdir; + + /* There are no more files to be read. */ + if (dir->next_name == (content_directory_contents + + ARRAYELTS (content_directory_contents))) + return NULL; + + /* Get the next child. */ + name = *dir->next_name++; + + /* Now, fill in the dirent with the name. */ + memset (&dirent, 0, sizeof dirent); + dirent.d_ino = 0; + dirent.d_off = 0; + dirent.d_reclen = sizeof dirent; + dirent.d_type = DT_DIR; + strcpy (dirent.d_name, name); + return &dirent; +} + +static void +android_content_closedir (struct android_vdir *vdir) +{ + struct android_content_vdir *dir, **next, *tem; + + dir = (struct android_content_vdir *) vdir; + + /* If the ``directory file descriptor'' has been opened, close + it. */ + + if (dir->fd != -1) + close (dir->fd); + + /* Now unlink this directory. */ + + for (next = &all_content_vdirs; (tem = *next);) + { + if (tem == dir) + *next = dir->next; + else + next = &(*next)->next; + } + + xfree (dir); +} + +static int +android_content_dirfd (struct android_vdir *vdir) +{ + struct android_content_vdir *dir; + + dir = (struct android_content_vdir *) vdir; + + /* Since `android_content_opendir' tries to avoid opening a file + descriptor if readdir isn't called, dirfd can fail if open fails. + + open sets errno to a set of errors different from what POSIX + stipulates for dirfd, but for ease of implementation the open + errors are used instead. */ + + if (dir->fd >= 0) + return dir->fd; + + dir->fd = open ("/dev/null", O_RDONLY | O_CLOEXEC); + return dir->fd; +} + +static struct android_vdir * +android_content_opendir (struct android_vnode *vnode) +{ + struct android_content_vdir *dir; + int api; + + /* Allocate the virtual directory. */ + dir = xmalloc (sizeof *dir); + dir->vdir.readdir = android_content_readdir; + dir->vdir.closedir = android_content_closedir; + dir->vdir.dirfd = android_content_dirfd; + dir->fd = -1; + + /* Fill in the directory contents. */ + dir->next_name = content_directory_contents; + api = android_get_current_api_level (); + + /* Android 4.4 and earlier don't support /content/storage. */ + + if (api < 21) + dir->next_name++; + + /* Android 4.3 and earlier don't support /content/by-authority. */ + + if (api < 19) + dir->next_name++; + + /* Link this stream onto the list of all content directory + streams. */ + dir->next = all_content_vdirs; + all_content_vdirs = dir; + return &dir->vdir; +} + +/* Return the file name corresponding to DIRFD if it is a + ``directory'' file descriptor returned by `android_content_dirfd' + or NULL otherwise. */ + +static char * +android_content_get_directory_name (int dirfd) +{ + struct android_content_vdir *dir; + + for (dir = all_content_vdirs; dir; dir = dir->next) + { + if (dir->fd == dirfd && dirfd != -1) + return "/content"; + } + + return NULL; +} + +/* Find the vnode designated by the normalized NAME relative to the + root of the content file system. NAME may be modified, and must be + LENGTH bytes long, excluding its terminating NULL byte. */ + +static struct android_vnode * +android_content_initial (char *name, size_t length) +{ + struct android_vnode temp; + + /* Create a temporary vnode at the root of the asset file + system. */ + + temp.ops = &content_vfs_ops; + temp.type = ANDROID_VNODE_CONTENT; + temp.flags = 0; + + /* Try to name this vnode. If NAME is empty, it will be duplicated + instead. */ + return android_content_name (&temp, name, length); +} + + + +/* Content URI management functions. */ + +/* Return the content URI corresponding to a `/content/by-authority' + file name, or NULL if it is invalid for some reason. FILENAME + should be relative to /content/by-authority, with no leading + directory separator character. + + This function is not reentrant. */ + +static const char * +android_get_content_name (const char *filename) +{ + static char buffer[PATH_MAX + 1], *fill; + + /* Make sure FILENAME isn't obviously invalid: it must contain an + authority name and a file name component. */ + + fill = strchr (filename, '/'); + if (!fill || *(fill + 1) == '\0') + { + errno = ENOENT; + return NULL; + } + + /* FILENAME must also not be a directory. */ + + if (filename[strlen (filename)] == '/') + { + errno = ENOTDIR; + return NULL; + } + + snprintf (buffer, PATH_MAX + 1, "content://%s", filename); + return buffer; +} + +/* Return whether or not the specified URI is an accessible content + URI. MODE specifies what to check. */ + +static bool +android_check_content_access (const char *uri, int mode) +{ + jobject string; + size_t length; + jboolean rc; + + length = strlen (uri); + + string = (*android_java_env)->NewByteArray (android_java_env, + length); + android_exception_check (); + + (*android_java_env)->SetByteArrayRegion (android_java_env, + string, 0, length, + (jbyte *) uri); + rc = (*android_java_env)->CallBooleanMethod (android_java_env, + emacs_service, + service_class.check_content_uri, + string, + (jboolean) ((mode & R_OK) + != 0), + (jboolean) ((mode & W_OK) + != 0)); + android_exception_check_1 (string); + ANDROID_DELETE_LOCAL_REF (string); + + return rc; +} + + + +/* Content authority-based vnode implementation. + + /contents/by-authority is a simple vnode implementation that converts + components to content:// URIs. + + It does not canonicalize file names by removing parent directory + separators, as these characters can appear in legitimate content + file names. */ + +struct android_authority_vnode +{ + /* The vnode data itself. */ + struct android_vnode vnode; + + /* URI associated with this vnode, or NULL if this is the root of + the content authority tree. */ + char *uri; +}; + +static struct android_vnode *android_authority_name (struct android_vnode *, + char *, size_t); +static int android_authority_open (struct android_vnode *, int, + mode_t, bool, int *, AAsset **); +static void android_authority_close (struct android_vnode *); +static int android_authority_unlink (struct android_vnode *); +static int android_authority_symlink (const char *, struct android_vnode *); +static int android_authority_rmdir (struct android_vnode *); +static int android_authority_rename (struct android_vnode *, + struct android_vnode *, bool); +static int android_authority_stat (struct android_vnode *, struct stat *); +static int android_authority_access (struct android_vnode *, int); +static int android_authority_mkdir (struct android_vnode *, mode_t); +static struct android_vdir *android_authority_opendir (struct android_vnode *); + +/* Vector of VFS operations associated with the content VFS node. */ + +static struct android_vops 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, + }; + +static struct android_vnode * +android_authority_name (struct android_vnode *vnode, char *name, + size_t length) +{ + struct android_authority_vnode *vp; + const char *uri_name; + + if (!android_init_gui) + { + errno = EIO; + return NULL; + } + + /* If NAME is empty or consists of a single directory separator + _and_ VP->uri is NULL, return a copy of VNODE. */ + + vp = (struct android_authority_vnode *) vnode; + + if (length < 1 || (*name == '/' && length == 1 && !vp->uri)) + { + vp = xmalloc (sizeof *vp); + memcpy (vp, vnode, sizeof *vp); + + if (vp->uri) + vp->uri = xstrdup (vp->uri); + + return &vp->vnode; + } + + /* Else, if VP->uri is NULL, then it is the root of the by-authority + tree. If NAME starts with a directory separator character, + remove it. */ + + if (!vp->uri) + { + if (*name == '/') + name++, length -= 1; + + uri_name = android_get_content_name (name); + if (!uri_name) + goto error; + + /* Now fill in the vnode. */ + vp = xmalloc (sizeof *vp); + vp->vnode.ops = &authority_vfs_ops; + vp->vnode.type = ANDROID_VNODE_CONTENT_AUTHORITY; + vp->vnode.flags = 0; + vp->uri = xstrdup (uri_name); + return &vp->vnode; + } + + /* Content files can't have children. */ + errno = ENOENT; + error: + return NULL; +} + +static int +android_authority_open (struct android_vnode *vnode, int flags, + mode_t mode, bool asset_p, int *fd_return, + AAsset **asset) +{ + struct android_authority_vnode *vp; + size_t length; + jobject string; + int fd; + + vp = (struct android_authority_vnode *) vnode; + + if (vp->uri == NULL) + { + /* This is the `by-authority' directory itself, which can't be + opened. */ + errno = ENOSYS; + return -1; + } + + /* Allocate a buffer to hold the file name. */ + length = strlen (vp->uri); + string = (*android_java_env)->NewByteArray (android_java_env, + length); + if (!string) + { + (*android_java_env)->ExceptionClear (android_java_env); + errno = ENOMEM; + return -1; + } + + /* Copy the URI into this byte array. */ + (*android_java_env)->SetByteArrayRegion (android_java_env, + string, 0, length, + (jbyte *) vp->uri); + + /* Try to open the file descriptor. */ + + fd + = (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + service_class.open_content_uri, + string, + (jboolean) ((mode & O_WRONLY + || mode & O_RDWR) + != 0), + (jboolean) !(mode & O_WRONLY), + (jboolean) ((mode & O_TRUNC) + != 0)); + + if ((*android_java_env)->ExceptionCheck (android_java_env)) + { + (*android_java_env)->ExceptionClear (android_java_env); + errno = ENOMEM; + ANDROID_DELETE_LOCAL_REF (string); + return -1; + } + + /* If fd is -1, just assume that the file does not exist, + and return -1 with errno set to ENOENT. */ + + if (fd == -1) + { + errno = ENOENT; + goto skip; + } + + if (mode & O_CLOEXEC) + android_close_on_exec (fd); + + skip: + ANDROID_DELETE_LOCAL_REF (string); + + if (fd == -1) + return -1; + + *fd_return = fd; + return 0; +} + +static void +android_authority_close (struct android_vnode *vnode) +{ + struct android_authority_vnode *vp; + int save_errno; + + vp = (struct android_authority_vnode *) vnode; + save_errno = errno; + xfree (vp->uri); + xfree (vp); + errno = save_errno; +} + +static int +android_authority_unlink (struct android_vnode *vnode) +{ + errno = EROFS; + return -1; +} + +static int +android_authority_symlink (const char *target, + struct android_vnode *vnode) +{ + errno = EROFS; + return -1; +} + +static int +android_authority_rmdir (struct android_vnode *vnode) +{ + errno = EROFS; + return -1; +} + +static int +android_authority_rename (struct android_vnode *src, + struct android_vnode *dst, + bool keep_existing) +{ + if (src->type != dst->type) + { + /* If the types of both vnodes differ, complain that they're on + two different filesystems (which is correct from a abstract + viewpoint.) */ + errno = EXDEV; + return -1; + } + + /* Otherwise, return ENOSYS. */ + errno = ENOSYS; + return -1; +} + +static int +android_authority_stat (struct android_vnode *vnode, + struct stat *statb) +{ + int rc, fd, save_errno; + struct android_authority_vnode *vp; + + /* If this is a vnode representing `by-authority', return some + information about this directory. */ + + vp = (struct android_authority_vnode *) vnode; + + if (!vp->uri) + { + memset (statb, 0, sizeof *statb); + statb->st_uid = getuid (); + statb->st_gid = getgid (); + statb->st_ino = 0; + statb->st_dev = -3; + statb->st_mode = S_IFDIR | S_IRUSR; + return 0; + } + + /* Try to open the file and call fstat. */ + rc = (*vnode->ops->open) (vnode, O_RDONLY, 0, false, &fd, NULL); + + if (rc < 0) + return -1; + + /* If rc is 1, then an asset file descriptor has been returned. + This is impossible, so assert that it doesn't transpire. */ + assert (rc != 1); + + /* Now, try to stat the file. */ + rc = fstat (fd, statb); + save_errno = errno; + + /* Close the file descriptor. */ + close (fd); + + /* Restore errno. */ + errno = save_errno; + return rc; +} + +static int +android_authority_access (struct android_vnode *vnode, int mode) +{ + struct android_authority_vnode *vp; + + vp = (struct android_authority_vnode *) vnode; + + /* Validate MODE. */ + + if (mode != F_OK && !(mode & (W_OK | X_OK | R_OK))) + { + errno = EINVAL; + return -1; + } + + if (!vp->uri) + { + /* Return EACCES if the caller is trying to check for write + access to `by-authority'. */ + + if (mode != F_OK && (mode & (W_OK | X_OK))) + { + errno = EACCES; + return -1; + } + + return 0; + } + + return (android_check_content_access (vp->uri, mode) + ? 0 : -1); +} + +static int +android_authority_mkdir (struct android_vnode *vnode, mode_t mode) +{ + errno = EACCES; + return -1; +} + +static struct android_vdir * +android_authority_opendir (struct android_vnode *vnode) +{ + struct android_authority_vnode *vp; + + /* Forbid listing the `by-authority' directory. */ + vp = (struct android_authority_vnode *) vnode; + errno = vp->uri ? ENOTDIR : EACCES; + return NULL; +} + +/* Find the vnode designated by NAME relative to the root of the + by-authority directory. + + If NAME is empty or a single leading separator character, return + a vnode representing the by-authority directory itself. + + Otherwise, represent the remainder of NAME as a URI (without + normalizing it) and return a vnode corresponding to that. + + Value may also be NULL with errno set if the designated vnode is + not available, such as when Android windowing has not been + initialized. */ + +static struct android_vnode * +android_authority_initial (char *name, size_t length) +{ + struct android_authority_vnode temp; + + temp.vnode.ops = &authority_vfs_ops; + temp.vnode.type = ANDROID_VNODE_CONTENT_AUTHORITY; + temp.vnode.flags = 0; + temp.uri = NULL; + + return android_authority_name (&temp.vnode, name, length); +} + + + +/* SAF ``root'' vnode implementation. + + The Storage Access Framework is a system service that manages a + registry of document providers, which are a type of file system + server. + + Normally, document providers can only provide individual files + through preestablished ``content URIs''. Android 5.0 and later add + to that ``tree URIs'', which designate entire file system trees. + + Authorization to access document trees and content URIs is granted + transiently by default when an Intent is received, but Emacs can + also receive persistent authorization for a given document tree. + + /content/storage returns a list of directories, each representing a + single authority providing at least one tree URI Emacs holds + persistent authorization for. + + Each one of those directories then contains one document tree that + Emacs is authorized to access. */ + +struct android_saf_root_vnode +{ + /* The vnode data. */ + struct android_vnode vnode; + + /* The name of the document authority this directory represents, or + NULL. */ + char *authority; +}; + +struct android_saf_root_vdir +{ + /* The directory stream function table. */ + struct android_vdir vdir; + + /* The next directory stream in `all_saf_root_vdirs'. */ + struct android_saf_root_vdir *next; + + /* Array of strings, one for each directory to return. */ + jobjectArray array; + + /* Name of the authority this directory lists, or NULL. */ + char *authority; + + /* Length of that array, and the current within it. */ + jsize length, i; + + /* ``Directory'' file descriptor associated with this stream, or + -1. */ + int fd; +}; + +static struct android_vnode *android_saf_root_name (struct android_vnode *, + char *, size_t); +static int android_saf_root_open (struct android_vnode *, int, + mode_t, bool, int *, AAsset **); +static void android_saf_root_close (struct android_vnode *); +static int android_saf_root_unlink (struct android_vnode *); +static int android_saf_root_symlink (const char *, struct android_vnode *); +static int android_saf_root_rmdir (struct android_vnode *); +static int android_saf_root_rename (struct android_vnode *, + struct android_vnode *, bool); +static int android_saf_root_stat (struct android_vnode *, struct stat *); +static int android_saf_root_access (struct android_vnode *, int); +static int android_saf_root_mkdir (struct android_vnode *, mode_t); +static struct android_vdir *android_saf_root_opendir (struct android_vnode *); + +/* Vector of VFS operations associated with the SAF root VFS node. */ + +static struct android_vops 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_opendir, + }; + +/* Chain containing all SAF root directories. */ +static struct android_saf_root_vdir *all_saf_root_vdirs; + +/* Defined in the next page. */ +static struct android_vnode *android_saf_tree_from_name (char *, const char *, + const char *); + +static struct android_vnode * +android_saf_root_name (struct android_vnode *vnode, char *name, + size_t length) +{ + char *remainder, *component_end; + struct android_saf_root_vnode *vp; + struct android_vnode *new; + char component[PATH_MAX]; + + /* Canonicalize NAME. */ + remainder = android_vfs_canonicalize_name (name, &length); + + /* If remainder is set, it's a name relative to the root vnode. */ + if (remainder) + goto parent_vnode; + + /* If LENGTH is empty or NAME is a single directory separator, + return a copy of this vnode. */ + + if (length < 1 || (*name == '/' && length == 1)) + { + vp = xmalloc (sizeof *vp); + memcpy (vp, vnode, sizeof *vp); + + if (vp->authority) + vp->authority = xstrdup (vp->authority); + + return &vp->vnode; + } + + vp = (struct android_saf_root_vnode *) vnode; + + /* If NAME starts with a directory separator, move it past that. */ + + if (*name == '/') + name++, length -= 1; + + /* Look for the first directory separator. */ + component_end = strchr (name, '/'); + + /* If not there, use name + length. */ + + if (!component_end) + component_end = name + length; + + if (component_end - name >= PATH_MAX) + { + errno = ENAMETOOLONG; + return NULL; + } + + /* Copy the component over. */ + memcpy (component, name, component_end - name); + component[component_end - name] = '\0'; + + /* Create a SAF document vnode for this tree if it represents an + authority. */ + + if (vp->authority) + return android_saf_tree_from_name (component_end, component, + vp->authority); + + /* Otherwise, find the first component of NAME and create a vnode + representing it as an authority. */ + + /* Create the vnode. */ + vp = xmalloc (sizeof *vp); + vp->vnode.ops = &saf_root_vfs_ops; + vp->vnode.type = ANDROID_VNODE_SAF_ROOT; + vp->vnode.flags = 0; + vp->authority = xstrdup (component); + + /* If there is more of this component to be named, name it through + the new vnode. */ + + if (component_end != name + length) + { + new = (*vp->vnode.ops->name) (&vp->vnode, component_end, + length - (component_end - name)); + (*vp->vnode.ops->close) (&vp->vnode); + + return new; + } + + return &vp->vnode; + + parent_vnode: + vp = (struct android_saf_root_vnode *) vnode; + + /* .. was encountered and the parent couldn't be found through + stripping off preceding components. + + Find the parent vnode and name the rest of NAME starting from + there. */ + + if (!vp->authority) + /* Look this file name up relative to the root of the contents + directory. */ + return android_content_initial (remainder, strlen (remainder)); + else + /* Look this file name up relative to the root of the storage + directory. */ + return android_saf_root_initial (remainder, strlen (remainder)); +} + +static int +android_saf_root_open (struct android_vnode *vnode, int flags, + mode_t mode, bool asset_p, int *fd_return, + AAsset **asset) +{ + /* /content/storage or one of its authority children cannot be + opened, as they are virtual directories. */ + + errno = ENOSYS; + return -1; +} + +static void +android_saf_root_close (struct android_vnode *vnode) +{ + struct android_saf_root_vnode *vp; + int save_errno; + + vp = (struct android_saf_root_vnode *) vnode; + save_errno = errno; + xfree (vp->authority); + xfree (vp); + errno = save_errno; +} + +static int +android_saf_root_unlink (struct android_vnode *vnode) +{ + errno = EROFS; + return -1; +} + +static int +android_saf_root_symlink (const char *target, + struct android_vnode *vnode) +{ + errno = EROFS; + return -1; +} + +static int +android_saf_root_rmdir (struct android_vnode *vnode) +{ + errno = EROFS; + return -1; +} + +static int +android_saf_root_rename (struct android_vnode *src, + struct android_vnode *dst, + bool keep_existing) +{ + errno = EROFS; + return -1; +} + +static int +android_saf_root_stat (struct android_vnode *vnode, + struct stat *statb) +{ + /* Make up some imaginary statistics for this vnode. */ + + memset (statb, 0, sizeof *statb); + statb->st_uid = getuid (); + statb->st_gid = getgid (); + statb->st_ino = 0; + statb->st_dev = -4; + statb->st_mode = S_IFDIR | S_IRUSR; + return 0; +} + +static int +android_saf_root_access (struct android_vnode *vnode, int mode) +{ + /* Validate MODE. */ + + if (mode != F_OK && !(mode & (W_OK | X_OK | R_OK))) + { + errno = EINVAL; + return -1; + } + + /* Now, don't allow writing or executing this directory. */ + + if (mode != F_OK && (mode & (W_OK | X_OK))) + { + errno = EROFS; + return -1; + } + + return 0; +} + +static int +android_saf_root_mkdir (struct android_vnode *vnode, mode_t mode) +{ + errno = EROFS; + return -1; +} + +static struct dirent * +android_saf_root_readdir (struct android_vdir *vdir) +{ + static struct dirent *dirent; + jobject string; + const char *chars; + size_t length, size; + struct android_saf_root_vdir *dir; + + dir = (struct android_saf_root_vdir *) vdir; + + if (dir->i == dir->length) + { + /* At the end of the stream. Free dirent and return NULL. */ + + xfree (dirent); + dirent = NULL; + return NULL; + } + + /* Get this string. */ + string = (*android_java_env)->GetObjectArrayElement (android_java_env, + dir->array, dir->i++); + android_exception_check (); + chars = (*android_java_env)->GetStringUTFChars (android_java_env, + (jstring) string, + NULL); + android_exception_check_nonnull ((void *) chars, string); + + /* Figure out how large it is, and then resize dirent to fit. */ + length = strlen (chars) + 1; + size = offsetof (struct dirent, d_name) + length; + dirent = xrealloc (dirent, size); + + /* Clear dirent. */ + memset (dirent, 0, size); + + /* Fill in the generic directory information and copy the string + over. */ + dirent->d_ino = 0; + dirent->d_off = 0; + dirent->d_reclen = size; + dirent->d_type = DT_DIR; + strcpy (dirent->d_name, chars); + + /* Release the string data and the local reference to STRING. */ + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + (jstring) string, chars); + ANDROID_DELETE_LOCAL_REF (string); + return dirent; +} + +static void +android_saf_root_closedir (struct android_vdir *vdir) +{ + struct android_saf_root_vdir *dir, **next, *tem; + + dir = (struct android_saf_root_vdir *) vdir; + + /* If the ``directory file descriptor'' has been opened, close + it. */ + + if (dir->fd != -1) + close (dir->fd); + + /* Delete the local reference to the file name array. */ + ANDROID_DELETE_LOCAL_REF (dir->array); + + /* Free the authority name if set. */ + xfree (dir->authority); + + /* Now unlink this directory. */ + + for (next = &all_saf_root_vdirs; (tem = *next);) + { + if (tem == dir) + *next = dir->next; + else + next = &(*next)->next; + } + + /* Free the directory itself. */ + xfree (dir); +} + +static int +android_saf_root_dirfd (struct android_vdir *vdir) +{ + struct android_saf_root_vdir *dir; + + dir = (struct android_saf_root_vdir *) vdir; + + /* Since `android_saf_root_opendir' tries to avoid opening a file + descriptor if readdir isn't called, dirfd can fail if open fails. + + open sets errno to a set of errors different from what POSIX + stipulates for dirfd, but for ease of implementation the open + errors are used instead. */ + + if (dir->fd >= 0) + return dir->fd; + + dir->fd = open ("/dev/null", O_RDONLY | O_CLOEXEC); + return dir->fd; +} + +static struct android_vdir * +android_saf_root_opendir (struct android_vnode *vnode) +{ + struct android_saf_root_vnode *vp; + jobjectArray array; + jmethodID method; + jbyteArray authority; + struct android_saf_root_vdir *dir; + size_t length; + + vp = (struct android_saf_root_vnode *) vnode; + + if (vp->authority) + { + /* Build a string containing the authority. */ + length = strlen (vp->authority); + authority = (*android_java_env)->NewByteArray (android_java_env, + length); + android_exception_check (); + + /* Copy the authority name to that byte array. */ + (*android_java_env)->SetByteArrayRegion (android_java_env, + authority, 0, length, + (jbyte *) vp->authority); + + /* Acquire a list of every tree provided by this authority. */ + + method = service_class.get_document_trees; + array + = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, + emacs_service, + service_class.class, + method, authority); + android_exception_check_1 (authority); + ANDROID_DELETE_LOCAL_REF (authority); + + /* Ascertain the length of the array. If it is empty or NULL, + return ENOENT. */ + + if (!array) + { + errno = ENOENT; + return NULL; + } + + length = (*android_java_env)->GetArrayLength (android_java_env, array); + + if (!length) + { + ANDROID_DELETE_LOCAL_REF (array); + errno = ENOENT; + return NULL; + } + + /* Now allocate the directory stream. It will retain a local + reference to the array, and should thus only be used within the + same JNI local reference frame. */ + + dir = xmalloc (sizeof *dir); + dir->vdir.readdir = android_saf_root_readdir; + dir->vdir.closedir = android_saf_root_closedir; + dir->vdir.dirfd = android_saf_root_dirfd; + dir->fd = -1; + dir->array = array; + dir->length = length; + dir->i = 0; + dir->authority = xstrdup (vp->authority); + + /* Link this stream onto the list of all SAF root directory + streams. */ + dir->next = all_saf_root_vdirs; + all_saf_root_vdirs = dir; + return &dir->vdir; + } + + /* Acquire a list of every document authority. */ + + method = service_class.get_document_authorities; + array = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, + emacs_service, + service_class.class, + method); + android_exception_check (); + + if (!array) + emacs_abort (); + + /* Now allocate the directory stream. It will retain a local + reference to the array, and should thus only be used within the + same JNI local reference frame. */ + + dir = xmalloc (sizeof *dir); + dir->vdir.readdir = android_saf_root_readdir; + dir->vdir.closedir = android_saf_root_closedir; + dir->vdir.dirfd = android_saf_root_dirfd; + dir->fd = -1; + dir->array = array; + dir->length = (*android_java_env)->GetArrayLength (android_java_env, + array); + dir->i = 0; + dir->authority = NULL; + + /* Link this stream onto the list of all SAF root directory + streams. */ + dir->next = all_saf_root_vdirs; + all_saf_root_vdirs = dir; + return &dir->vdir; +} + +/* Find the vnode designated by NAME relative to the root of the + storage directory. + + If NAME is empty or a single leading separator character, return a + vnode representing the storage directory itself. + + If NAME actually resides in a parent directory, look for it within + the vnode representing the content directory. */ + +static struct android_vnode * +android_saf_root_initial (char *name, size_t length) +{ + struct android_saf_root_vnode temp; + + temp.vnode.ops = &saf_root_vfs_ops; + temp.vnode.type = ANDROID_VNODE_SAF_ROOT; + temp.vnode.flags = 0; + temp.authority = NULL; + + return android_saf_root_name (&temp.vnode, name, length); +} + +/* Return any open SAF root directory stream for which dirfd has + returned the file descriptor DIRFD. Return NULL otherwise. */ + +static struct android_saf_root_vdir * +android_saf_root_get_directory (int dirfd) +{ + struct android_saf_root_vdir *dir; + + for (dir = all_saf_root_vdirs; dir; dir = dir->next) + { + if (dir->fd == dirfd && dirfd != -1) + return dir; + } + + return NULL; +} + + + +/* Functions common to both SAF directory and file nodes. */ + +/* Return file status for the document designated by ID_NAME within + the document tree identified by URI_NAME. + + If the file status is available, place it within *STATB and return + 0. If not, return -1 and set errno to EPERM. */ + +static int +android_saf_stat (const char *uri_name, const char *id_name, + struct stat *statb) +{ + jmethodID method; + jstring uri, id; + jobject status; + jlong mode, size, mtim, *longs; + + /* Build strings for both URI and ID. */ + uri = (*android_java_env)->NewStringUTF (android_java_env, uri_name); + android_exception_check (); + + if (id_name) + { + id = (*android_java_env)->NewStringUTF (android_java_env, + id_name); + android_exception_check_1 (uri); + } + else + id = NULL; + + /* Try to retrieve the file status. */ + method = service_class.stat_document; + status = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, + emacs_service, + service_class.class, + method, uri, id); + + /* Check for exceptions and release unneeded local references. */ + + if (id) + { + android_exception_check_2 (uri, id); + ANDROID_DELETE_LOCAL_REF (id); + } + else + android_exception_check_1 (uri); + + ANDROID_DELETE_LOCAL_REF (uri); + + /* Check for failure. */ + + if (!status) + { + errno = EPERM; + return -1; + } + + /* Read the file status from the array returned. */ + + longs = (*android_java_env)->GetLongArrayElements (android_java_env, + status, NULL); + android_exception_check_nonnull (longs, status); + mode = longs[0]; + size = longs[1]; + mtim = longs[2]; + (*android_java_env)->ReleaseLongArrayElements (android_java_env, status, + longs, JNI_ABORT); + ANDROID_DELETE_LOCAL_REF (status); + + /* Fill in STATB with this information. */ + memset (statb, 0, sizeof *statb); + statb->st_size = MAX (0, MIN (TYPE_MAXIMUM (off_t), size)); + statb->st_mode = mode; + statb->st_mtim.tv_sec = mtim / 1000; + statb->st_mtim.tv_nsec = (mtim % 1000) * 1000000; + statb->st_uid = getuid (); + statb->st_gid = getgid (); + return 0; +} + +/* Detect if Emacs has access to the document designated by the the + documen ID ID_NAME within the tree URI_NAME. If ID_NAME is NULL, + use the document ID in URI_NAME 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 -1 with errno set + appropriately if not. */ + +static int +android_saf_access (const char *uri_name, const char *id_name, + bool writable) +{ + jmethodID method; + jstring uri, id; + jint rc; + + /* Build strings for both URI and ID. */ + uri = (*android_java_env)->NewStringUTF (android_java_env, uri_name); + android_exception_check (); + + if (id_name) + { + id = (*android_java_env)->NewStringUTF (android_java_env, + id_name); + android_exception_check_1 (uri); + } + else + id = NULL; + + /* Try to retrieve the file status. */ + method = service_class.access_document; + rc = (*android_java_env)->CallNonvirtualIntMethod (android_java_env, + emacs_service, + service_class.class, + method, uri, id, + (jboolean) writable); + + /* Check for exceptions and release unneeded local references. */ + + if (id) + { + android_exception_check_2 (uri, id); + ANDROID_DELETE_LOCAL_REF (id); + } + else + android_exception_check_1 (uri); + + ANDROID_DELETE_LOCAL_REF (uri); + + switch (rc) + { + case -1: + /* -1 means it doesn't exist. */ + errno = ENOENT; + return -1; + + case -2: + /* -2 means access has been denied. */ + errno = EACCES; + return -1; + + case -3: + /* -3 refers to an internal error. */ + errno = EIO; + return -1; + } + + /* Return success. */ + return 0; +} + + + +/* SAF directory vnode. A file within a SAF directory tree is + identified by the URI of the directory tree itself, an opaque + ``file identifier'' value, and a display name. This information is + recorded in each vnode representing either a directory or a file + itself. */ + +struct android_saf_tree_vnode +{ + /* The vnode data itself. */ + struct android_vnode vnode; + + /* The URI of the directory tree represented. This is Java string + data in ``modified UTF format'', which is essentially a modified + UTF-8 format capable of storing NULL bytes while also utilizing + NULL termination. */ + const char *tree_uri; + + /* The ID of the document tree designated by TREE_URI. */ + char *tree_id; + + /* The document ID of the directory represented, or NULL if this is + the root directory of the tree. */ + char *document_id; + + /* The file name of this tree vnode. This is a ``path'' to the + file, where each directory component consists of the display name + of a directory leading up to a file within, terminated with a + directory separator character. */ + char *name; +}; + +struct android_saf_tree_vdir +{ + /* The virtual directory stream function table. */ + struct android_vdir vdir; + + /* The next directory in `all_saf_tree_vdirs'. */ + struct android_saf_tree_vdir *next; + + /* Name of this directory relative to the root file system. */ + char *name; + + /* Local reference to the cursor representing the directory + stream. */ + jobject cursor; + + /* The ``directory'' file descriptor used to identify this directory + stream, or -1. */ + int fd; +}; + +static struct android_vnode *android_saf_tree_name (struct android_vnode *, + char *, size_t); +static int android_saf_tree_open (struct android_vnode *, int, + mode_t, bool, int *, AAsset **); +static void android_saf_tree_close (struct android_vnode *); +static int android_saf_tree_unlink (struct android_vnode *); +static int android_saf_tree_symlink (const char *, struct android_vnode *); +static int android_saf_tree_rmdir (struct android_vnode *); +static int android_saf_tree_rename (struct android_vnode *, + struct android_vnode *, bool); +static int android_saf_tree_stat (struct android_vnode *, struct stat *); +static int android_saf_tree_access (struct android_vnode *, int); +static int android_saf_tree_mkdir (struct android_vnode *, mode_t); +static struct android_vdir *android_saf_tree_opendir (struct android_vnode *); + +/* Vector of VFS operations associated with SAF tree VFS nodes. */ + +static struct android_vops saf_tree_vfs_ops = + { + 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, + }; + +/* Vector of VFS operations associated with SAF file VFS nodes. + Defined later in the next page. */ +static struct android_vops saf_file_vfs_ops; + +/* Vector of VFS operations associated with SAF ``new'' VFS nodes. + Defined two pages below. */ +static struct android_vops saf_new_vfs_ops; + +/* Chain of all open SAF directory streams. */ +static struct android_saf_tree_vdir *all_saf_tree_vdirs; + +/* 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. + + If the designated file doesn't exist, but the penultimate component + within NAME does and is also a directory, return -2 and place the + document ID of that directory within *ID. + + If the designated file can't be located, return -1. */ + +static int +android_document_id_from_name (const char *tree_uri, char *name, + char **id) +{ + jobjectArray result; + jstring uri; + jbyteArray java_name; + size_t length; + jint rc; + jmethodID method; + const char *doc_id; + + /* First, create the array that will hold the result. */ + result = (*android_java_env)->NewObjectArray (android_java_env, 1, + java_string_class, + NULL); + android_exception_check (); + + /* Next, create the string for the tree URI and name. */ + length = strlen (name); + java_name = (*android_java_env)->NewByteArray (android_java_env, length); + android_exception_check_1 (result); + (*android_java_env)->SetByteArrayRegion (android_java_env, java_name, + 0, length, (jbyte *) name); + uri = (*android_java_env)->NewStringUTF (android_java_env, tree_uri); + android_exception_check_2 (result, java_name); + + /* Now, call documentIdFromName. */ + method = service_class.document_id_from_name; + rc = (*android_java_env)->CallNonvirtualIntMethod (android_java_env, + emacs_service, + service_class.class, + method, + uri, java_name, + result); + android_exception_check_3 (result, uri, java_name); + ANDROID_DELETE_LOCAL_REF (uri); + ANDROID_DELETE_LOCAL_REF (java_name); + + /* If rc indicates failure, don't try to copy from result. */ + + if (rc == -1) + { + ANDROID_DELETE_LOCAL_REF (result); + goto finish; + } + + eassert (rc == -2 || rc >= 0); + + /* Otherwise, obtain the contents of the string returned in Java + ``UTF-8'' encoding. */ + uri = (*android_java_env)->GetObjectArrayElement (android_java_env, + result, 0); + android_exception_check_nonnull (uri, result); + ANDROID_DELETE_LOCAL_REF (result); + + doc_id = (*android_java_env)->GetStringUTFChars (android_java_env, + uri, NULL); + android_exception_check_nonnull ((void *) doc_id, uri); + + /* Make *ID its copy. */ + *id = xstrdup (doc_id); + + /* And release it. */ + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + (jstring) uri, doc_id); + ANDROID_DELETE_LOCAL_REF (uri); + + finish: + return rc; +} + +static struct android_vnode * +android_saf_tree_name (struct android_vnode *vnode, char *name, + size_t length) +{ + char *remainder; + int rc; + struct android_saf_tree_vnode *vp, *new; + size_t vp_length; + char *filename, *fill, *doc_id, *end; + struct android_saf_root_vnode root; + struct android_saf_tree_vnode tree; + + /* Canonicalize NAME. */ + remainder = android_vfs_canonicalize_name (name, &length); + + /* If remainder is set, it's a name relative to the root vnode. */ + if (remainder) + goto parent_vnode; + + /* If LENGTH is empty or NAME is a single directory separator, + return a copy of this vnode. */ + + if (length < 1 || (*name == '/' && length == 1)) + { + vp = xmalloc (sizeof *vp); + memcpy (vp, vnode, sizeof *vp); + + /* Duplicate the information contained within VNODE. */ + + vp->tree_uri = xstrdup (vp->tree_uri); + vp->tree_id = xstrdup (vp->tree_id); + vp->name = xstrdup (vp->name); + + if (vp->document_id) + vp->document_id = xstrdup (vp->name); + + return &vp->vnode; + } + + /* Now, search for the document ID of the file designated by NAME + relative to this vnode. */ + + vp = (struct android_saf_tree_vnode *) vnode; + vp_length = strlen (vp->name); + + /* If NAME starts with a directory separator, move it past that. */ + + if (*name == '/') + name++, length -= 1; + + /* Concatenate VP->name with NAME. Leave one byte at the end for an + extra trailing directory separator. */ + + filename = xmalloc (vp_length + length + 2); + fill = stpcpy (filename, vp->name); + fill = stpcpy (fill, name); + + /* And search for a document ID in the result. */ + rc = android_document_id_from_name (vp->tree_uri, name, + &doc_id); + + if (rc < 0) + { + if (rc == -2) + { + /* This is a vnode representing a nonexistent file in a real + directory, so create a vnode whose sole use is to create + the file. */ + + new = xmalloc (sizeof *new); + new->vnode.ops = &saf_new_vfs_ops; + new->vnode.type = ANDROID_VNODE_SAF_NEW; + new->vnode.flags = 0; + + /* Here, doc_id is actually the ID of the penultimate + component in NAME. */ + + new->document_id = doc_id; + new->tree_uri = xstrdup (vp->tree_uri); + new->tree_id = xstrdup (vp->tree_id); + new->name = filename; + return &new->vnode; + } + + /* The document ID can't be found. */ + xfree (filename); + errno = ENOENT; + return NULL; + } + + if (!rc) + { + /* rc set to 0 means that NAME is a regular file. Detect if + NAME is supposed to be a directory; if it is, set errno to + ENODIR. */ + + if (name[length - 1] == '/') + { + xfree (filename); + xfree (doc_id); + errno = ENOTDIR; + return NULL; + } + } + + /* So this is either a directory or really a file. Fortunately, + directory and file vnodes share everything in common except for a + few file operations, so create a new directory vnode with the new + file name and return it. */ + + new = xmalloc (sizeof *new); + new->vnode.ops = (rc ? &saf_tree_vfs_ops + : &saf_file_vfs_ops); + new->vnode.type = (rc ? ANDROID_VNODE_SAF_TREE + : ANDROID_VNODE_SAF_FILE); + new->vnode.flags = 0; + + if (rc) + { + /* If fill[-1] is not a directory separator character, append + one to the end of filename. */ + + if (fill[-1] != '/') + { + *fill++ = '/'; + *fill = '\0'; + } + } + + new->document_id = doc_id; + new->tree_uri = xstrdup (vp->tree_uri); + new->tree_id = xstrdup (vp->tree_id); + new->name = filename; + return &new->vnode; + + parent_vnode: + vp = (struct android_saf_tree_vnode *) vnode; + + /* .. was encountered and the parent couldn't be found through + stripping off preceding components. + + Find the parent vnode and name the rest of NAME starting from + there. */ + + if (!vp->document_id) + { + /* VP->document_id is NULL, meaning this is the root of this + directory tree. The parent vnode is an SAF root vnode with + VP->tree_uri's authority. */ + + root.vnode.ops = &saf_root_vfs_ops; + root.vnode.type = ANDROID_VNODE_SAF_ROOT; + root.vnode.flags = 0; + + /* Find the authority from the URI. */ + + fill = (char *) vp->tree_uri; + + if (strncmp (fill, "content://", 10)) + emacs_abort (); + + /* Skip the content header. */ + fill += sizeof "content://" - 1; + + /* The authority segment of the URI is between here and the + next slash. */ + + end = strchr (fill, '/'); + + if (!end) + emacs_abort (); + + root.authority = xmalloc (end - fill + 1); + memcpy (root.authority, fill, end - fill); + root.authority[end - fill] = '\0'; + + /* Now search using this vnode. */ + vnode = (*root.vnode.ops->name) (&root.vnode, remainder, + strlen (remainder)); + xfree (root.authority); + return vnode; + } + + /* Otherwise, strip off the last directory component. */ + + fill = strrchr (vp->name, '/'); + if (!fill) + emacs_abort (); + + /* Create a new vnode at the top of the directory tree, and search + for remainder from there. */ + + tree.vnode.ops = &saf_tree_vfs_ops; + tree.vnode.type = ANDROID_VNODE_SAF_TREE; + tree.vnode.flags = 0; + tree.document_id = NULL; + tree.name = "/"; + tree.tree_uri = vp->tree_uri; + tree.tree_id = vp->tree_id; + + length = strlen (remainder + (*remainder == '/')); + filename = xmalloc (fill - vp->name + length + 2); + fill = mempcpy (filename, vp->name, + /* Include the separator character (*FILL) within + this copy. */ + fill - vp->name + 1); + /* Skip a leading separator in REMAINDER. */ + strcpy (fill, remainder + (*remainder == '/')); + + /* Use this filename to find a vnode relative to the start of this + tree. */ + + vnode = android_saf_tree_name (&tree.vnode, filename, + strlen (filename)); + xfree (filename); + return vnode; +} + +static int +android_saf_tree_open (struct android_vnode *vnode, int flags, + mode_t mode, bool asset_p, int *fd, + AAsset **asset) +{ + /* Don't allow opening this special directory. */ + errno = ENOSYS; + return -1; +} + +static void +android_saf_tree_close (struct android_vnode *vnode) +{ + struct android_saf_tree_vnode *vp; + int save_errno; + + vp = (struct android_saf_tree_vnode *) vnode; + + save_errno = errno; + xfree ((void *) vp->tree_uri); + xfree (vp->tree_id); + xfree (vp->name); + xfree (vp->document_id); + xfree (vp); + errno = save_errno; +} + +static int +android_saf_tree_unlink (struct android_vnode *vnode) +{ + errno = EISDIR; + return -1; +} + +static int +android_saf_tree_symlink (const char *target, struct android_vnode *vnode) +{ + errno = EPERM; + return -1; +} + +static int +android_saf_tree_rmdir (struct android_vnode *vnode) +{ + /* TODO */ + errno = ENOSYS; + return -1; +} + +static int +android_saf_tree_rename (struct android_vnode *src, + struct android_vnode *dst, + bool keep_existing) +{ + /* TODO */ + errno = ENOSYS; + return -1; +} + +static int +android_saf_tree_stat (struct android_vnode *vnode, + struct stat *statb) +{ + struct android_saf_tree_vnode *vp; + + vp = (struct android_saf_tree_vnode *) vnode; + + return android_saf_stat (vp->tree_uri, vp->document_id, + statb); +} + +static int +android_saf_tree_access (struct android_vnode *vnode, int mode) +{ + struct android_saf_tree_vnode *vp; + + vp = (struct android_saf_tree_vnode *) vnode; + + /* Validate MODE. */ + + if (mode != F_OK && !(mode & (W_OK | X_OK | R_OK))) + { + errno = EINVAL; + return -1; + } + + return android_saf_access (vp->tree_uri, vp->document_id, + mode & W_OK); +} + +static int +android_saf_tree_mkdir (struct android_vnode *vnode, mode_t mode) +{ + /* Since tree vnodes represent files that already exist, return + EEXIST. */ + errno = EEXIST; + return -1; +} + +/* Open a database Cursor containing each directory entry within the + supplied SAF tree vnode VP. + + Value is NULL upon failure, a local reference to the Cursor object + otherwise. */ + +static jobject +android_saf_tree_opendir_1 (struct android_saf_tree_vnode *vp) +{ + jobject uri, id, cursor; + jmethodID method; + + /* Build strings for both URI and ID. */ + uri = (*android_java_env)->NewStringUTF (android_java_env, + vp->tree_uri); + android_exception_check (); + + if (vp->document_id) + { + id = (*android_java_env)->NewStringUTF (android_java_env, + vp->document_id); + android_exception_check_1 (uri); + } + else + id = NULL; + + /* Try to open the cursor. */ + method = service_class.open_document_directory; + cursor + = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, + emacs_service, + service_class.class, + method, uri, id); + + if (id) + { + android_exception_check_2 (id, uri); + ANDROID_DELETE_LOCAL_REF (id); + } + else + android_exception_check_1 (uri); + + ANDROID_DELETE_LOCAL_REF (uri); + + /* Return the resulting cursor. */ + return cursor; +} + +static struct dirent * +android_saf_tree_readdir (struct android_vdir *vdir) +{ + struct android_saf_tree_vdir *dir; + static struct dirent *dirent; + jobject entry, d_name; + jint d_type; + jmethodID method; + size_t length, size; + const char *chars; + + dir = (struct android_saf_tree_vdir *) vdir; + + /* Try to read one entry from the cursor. */ + method = service_class.read_directory_entry; + entry + = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, + emacs_service, + service_class.class, + method, dir->cursor); + android_exception_check (); + + /* If ENTRY is NULL, we're at the end of the directory. */ + + if (!entry) + { + xfree (entry); + entry = NULL; + return NULL; + } + + /* Load both fields from ENTRY. */ + d_name = (*android_java_env)->GetObjectField (android_java_env, entry, + entry_class.d_name); + if (!d_name) + { + /* If an error transpires, d_name is set to NULL. */ + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (entry); + + /* XXX: what would be a better error indication? */ + errno = EIO; + return NULL; + } + + /* d_type is 1 if this is a directory, and 0 if it's a regular + file. */ + d_type = (*android_java_env)->GetIntField (android_java_env, entry, + entry_class.d_type); + ANDROID_DELETE_LOCAL_REF (entry); + + /* Copy the name of the directory over. */ + chars = (*android_java_env)->GetStringUTFChars (android_java_env, + (jstring) d_name, + NULL); + android_exception_check_nonnull ((void *) chars, d_name); + + /* Figure out how large it is, and then resize dirent to fit. */ + length = strlen (chars) + 1; + size = offsetof (struct dirent, d_name) + length; + dirent = xrealloc (dirent, size); + + /* Clear dirent. */ + memset (dirent, 0, size); + + /* Fill in the generic directory information and copy the string + over. */ + dirent->d_ino = 0; + dirent->d_off = 0; + dirent->d_reclen = size; + dirent->d_type = d_type ? DT_DIR : DT_UNKNOWN; + strcpy (dirent->d_name, chars); + + /* Release the string data and the local reference to STRING. */ + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + (jstring) d_name, + chars); + ANDROID_DELETE_LOCAL_REF (d_name); + return dirent; +} + +static void +android_saf_tree_closedir (struct android_vdir *vdir) +{ + struct android_saf_tree_vdir *dir, **next, *tem; + + dir = (struct android_saf_tree_vdir *) vdir; + + /* dir->name is allocated by asprintf, which uses regular + malloc. */ + free (dir->name); + + /* Yes, DIR->cursor is a local reference. */ + ANDROID_DELETE_LOCAL_REF (dir->cursor); + + /* If the ``directory file descriptor'' has been opened, close + it. */ + if (dir->fd != -1) + close (dir->fd); + + /* Now unlink this directory. */ + + for (next = &all_saf_tree_vdirs; (tem = *next);) + { + if (tem == dir) + *next = dir->next; + else + next = &(*next)->next; + } + + xfree (dir); +} + +static int +android_saf_tree_dirfd (struct android_vdir *vdir) +{ + struct android_saf_tree_vdir *dir; + + dir = (struct android_saf_tree_vdir *) vdir; + + /* Since `android_saf_tree_opendir' tries to avoid opening a file + descriptor if readdir isn't called, dirfd can fail if open fails. + + open sets errno to a set of errors different from what POSIX + stipulates for dirfd, but for ease of implementation the open + errors are used instead. */ + + if (dir->fd >= 0) + return dir->fd; + + dir->fd = open ("/dev/null", O_RDONLY | O_CLOEXEC); + return dir->fd; +} + +static struct android_vdir * +android_saf_tree_opendir (struct android_vnode *vnode) +{ + struct android_saf_tree_vnode *vp; + struct android_saf_tree_vdir *dir; + char *fill, *end; + jobject cursor; + char component[PATH_MAX]; + + vp = (struct android_saf_tree_vnode *) vnode; + + /* First, fill the directory stream with the right functions and + file name. */ + + dir = xmalloc (sizeof *dir); + dir->vdir.readdir = android_saf_tree_readdir; + dir->vdir.closedir = android_saf_tree_closedir; + dir->vdir.dirfd = android_saf_tree_dirfd; + + /* Find the authority from the URI. */ + + fill = (char *) vp->tree_uri; + + if (strncmp (fill, "content://", 10)) + emacs_abort (); + + /* Skip the content header. */ + fill += sizeof "content://" - 1; + + /* The authority segment of the URI is between here and the + next slash. */ + + end = strchr (fill, '/'); + + if (!end) + emacs_abort (); + + if (end - fill >= PATH_MAX) + { + errno = ENAMETOOLONG; + xfree (dir); + return NULL; + } + + /* Copy the authority over. */ + + memcpy (component, fill, end - fill); + component[end - fill] = '\0'; + + if (asprintf (&dir->name, "/content/storage/%s/%s%s", + component, vp->tree_id, vp->name) < 0) + { + /* Out of memory. */ + xfree (dir); + memory_full (0); + } + + /* Now open a cursor that iterates through each file in this + directory. */ + + cursor = android_saf_tree_opendir_1 (vp); + + if (!cursor) + { + xfree (dir); + xfree (dir->name); + errno = ENOENT; + return NULL; + } + + dir->cursor = cursor; + dir->fd = -1; + dir->next = all_saf_tree_vdirs; + all_saf_tree_vdirs = dir; + return &dir->vdir; +} + +/* Create a vnode designating the file NAME within a directory tree + whose identifier is TREE. As with all other `name' functions, NAME + may be modified. + + AUTHORITY is the name of the content provider authority that is + offering TREE. + + Value is NULL if no document tree or provider by those names + exists, or some other error takes place (for example, if TREE and + AUTHORITY aren't encoded correctly.) */ + +static struct android_vnode * +android_saf_tree_from_name (char *name, const char *tree, + const char *authority) +{ + struct android_saf_tree_vnode root; + jobject tree_string, authority_string, result; + jmethodID method; + const char *uri; + struct android_vnode *vp; + + /* Assume that TREE and NAME are in ``modified UTF-8 format''. */ + tree_string = (*android_java_env)->NewStringUTF (android_java_env, + tree); + android_exception_check (); + + authority_string + = (*android_java_env)->NewStringUTF (android_java_env, + authority); + android_exception_check_1 (tree_string); + + /* Now create the URI and detect if Emacs has the rights to access + it. */ + + method = service_class.get_tree_uri; + result + = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, + emacs_service, + service_class.class, + method, tree_string, + authority_string); + android_exception_check_2 (tree_string, authority_string); + ANDROID_DELETE_LOCAL_REF (tree_string); + ANDROID_DELETE_LOCAL_REF (authority_string); + + /* If it doesn't, return NULL and set errno to ENOENT. */ + + if (!result) + { + errno = ENOENT; + return NULL; + } + + /* Otherwise, decode this string. */ + uri = (*android_java_env)->GetStringUTFChars (android_java_env, result, + NULL); + android_exception_check_nonnull ((void *) uri, result); + + /* Fill in root.tree_uri with values that represent the root of this + document tree. */ + + root.vnode.ops = &saf_tree_vfs_ops; + root.vnode.type = ANDROID_VNODE_SAF_TREE; + root.vnode.flags = 0; + root.tree_uri = uri; + root.tree_id = (char *) tree; + root.document_id = NULL; + root.name = "/"; + + vp = (*root.vnode.ops->name) (&root.vnode, name, strlen (name)); + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + (jstring) result, uri); + ANDROID_DELETE_LOCAL_REF (result); + return vp; +} + +/* Return any open SAF tree directory stream for which dirfd has + returned the file descriptor DIRFD. Return NULL otherwise. */ + +static struct android_saf_tree_vdir * +android_saf_tree_get_directory (int dirfd) +{ + struct android_saf_tree_vdir *dir; + + for (dir = all_saf_tree_vdirs; dir; dir = dir->next) + { + if (dir->fd == dirfd && dirfd != -1) + return dir; + } + + return NULL; +} + + + +/* SAF file vnode. The information used to uniquely identify a file + is identical to that used to identify an SAF directory, but the + vnode operations are different. */ + +/* Define `struct android_saf_file_vnode' to be identical to a file + vnode. */ + +#define android_saf_file_vnode android_saf_tree_vnode + +/* Structure describing an open ParcelFileDescriptor. */ + +struct android_parcel_fd +{ + /* The next open parcel file descriptor. */ + struct android_parcel_fd *next; + + /* Global reference to this parcel file descriptor. */ + jobject descriptor; + + /* The modification time of this parcel file descriptor, or + `invalid_timespec'. */ + struct timespec mtime; + + /* The file descriptor itself. */ + int fd; +}; + +static struct android_vnode *android_saf_file_name (struct android_vnode *, + char *, size_t); +static int android_saf_file_open (struct android_vnode *, int, + mode_t, bool, int *, AAsset **); +static int android_saf_file_unlink (struct android_vnode *); +static int android_saf_file_rmdir (struct android_vnode *); +static struct android_vdir *android_saf_file_opendir (struct android_vnode *); + +/* Vector of VFS operations associated with SAF tree VFS nodes. */ + +static struct android_vops saf_file_vfs_ops = + { + android_saf_file_name, + android_saf_file_open, + android_saf_tree_close, + android_saf_file_unlink, + android_saf_tree_symlink, + android_saf_file_rmdir, + android_saf_tree_rename, + android_saf_tree_stat, + android_saf_tree_access, + android_saf_tree_mkdir, + android_saf_file_opendir, + }; + +/* Chain of all parcel file descriptors currently open. */ +static struct android_parcel_fd *open_parcel_fds; + +static struct android_vnode * +android_saf_file_name (struct android_vnode *vnode, char *name, + size_t length) +{ + struct android_saf_file_vnode *vp; + + /* If LENGTH is empty, make a copy of this vnode and return it. */ + + if (length < 1) + { + vp = xmalloc (sizeof *vp); + memcpy (vp, vnode, sizeof *vp); + + /* Duplicate the information contained within VNODE. */ + + vp->tree_uri = xstrdup (vp->tree_uri); + vp->tree_id = xstrdup (vp->tree_id); + vp->name = xstrdup (vp->name); + vp->document_id = xstrdup (vp->name); + + return &vp->vnode; + } + + /* A file vnode has no children of its own. */ + errno = ENOTDIR; + return NULL; +} + +static int +android_saf_file_open (struct android_vnode *vnode, int flags, + mode_t mode, bool asset_p, int *fd_return, + AAsset **asset) +{ + struct android_saf_file_vnode *vp; + jobject uri, id, descriptor; + jmethodID method; + jboolean trunc, write; + jint fd; + struct android_parcel_fd *info; + struct stat statb; + + /* Build strings for both the URI and ID. */ + + vp = (struct android_saf_file_vnode *) vnode; + uri = (*android_java_env)->NewStringUTF (android_java_env, + vp->tree_uri); + android_exception_check (); + id = (*android_java_env)->NewStringUTF (android_java_env, + vp->document_id); + android_exception_check_1 (uri); + + /* Open a parcel file descriptor according to flags. */ + + method = service_class.open_document; + trunc = flags & O_TRUNC; + write = ((flags & O_RDWR) == O_RDWR || (flags & O_WRONLY)); + descriptor + = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, + emacs_service, + service_class.class, + method, uri, id, + write, trunc); + android_exception_check_2 (uri, id); + ANDROID_DELETE_LOCAL_REF (uri); + ANDROID_DELETE_LOCAL_REF (id); + + if (!descriptor) + { + /* Assume that permission has been denied if DESCRIPTOR cannot + be opened. */ + errno = EPERM; + return -1; + } + + /* Allocate a record for this file descriptor. Parcel file + descriptors should be closed using their own `close' function, + which takes care of notifying the source that it has been + closed. */ + info = xmalloc (sizeof *info); + + /* Now obtain the file descriptor. */ + fd = (*android_java_env)->CallIntMethod (android_java_env, + descriptor, + fd_class.get_fd); + android_exception_check_1 (descriptor); + + /* Create a global reference to descriptor. */ + info->descriptor + = (*android_java_env)->NewGlobalRef (android_java_env, + descriptor); + + if (!info->descriptor) + { + /* If the global reference can't be created, delete + descriptor. */ + (*android_java_env)->ExceptionClear (android_java_env); + (*android_java_env)->CallVoidMethod (android_java_env, + descriptor, + fd_class.close); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (descriptor); + + /* Free INFO. */ + xfree (info); + + /* Set errno to EMFILE and return. */ + errno = EMFILE; + return -1; + } + + /* Delete the local ref to DESCRIPTOR. */ + ANDROID_DELETE_LOCAL_REF (descriptor); + + /* Try to retrieve the modification time of this file from the + content provider. */ + + if (!android_saf_stat (vp->tree_uri, vp->document_id, + &statb)) + info->mtime = statb.st_mtim; + else + info->mtime = invalid_timespec (); + + /* Set info->fd and chain it onto the list. */ + info->fd = fd; + info->next = open_parcel_fds; + open_parcel_fds = info; + + /* Return the file descriptor. */ + *fd_return = fd; + return 0; +} + +static int +android_saf_file_unlink (struct android_vnode *vnode) +{ + /* TODO */ + errno = ENOSYS; + return -1; +} + +static int +android_saf_file_rmdir (struct android_vnode *vnode) +{ + errno = ENOTDIR; + return -1; +} + +static struct android_vdir * +android_saf_file_opendir (struct android_vnode *vnode) +{ + errno = ENOTDIR; + return NULL; +} + +/* Close FD if it's a parcel file descriptor and return true. + If FD isn't, return false. + + Such file descriptors need to be closed using a function + written in Java, to tell the sender that it has been + closed. */ + +static bool +android_close_parcel_fd (int fd) +{ + struct android_parcel_fd *tem, **next, *temp; + + for (next = &open_parcel_fds; (tem = *next);) + { + if (tem->fd == fd) + { + (*android_java_env)->CallVoidMethod (android_java_env, + tem->descriptor, + fd_class.close); + + /* Ignore exceptions for the same reason EINTR errors from + `close' should be ignored. */ + (*android_java_env)->ExceptionClear (android_java_env); + (*android_java_env)->DeleteGlobalRef (android_java_env, + tem->descriptor); + + temp = tem->next; + xfree (tem); + *next = temp; + + return true; + } + else + next = &(*next)->next; + } + + return false; +} + + + +/* SAF ``new'' vnodes. These nodes share their data structures + with tree and file vnodes, but represent files that don't actually + exist within a directory. In them, the document ID represents not + the file designated by the vnode itself, but rather its parent + directory. + + The only vops defined serve to create directories or files, at + which point the vnode becomes invalid. */ + +#define android_saf_new_vnode android_saf_tree_vnode + +static struct android_vnode *android_saf_new_name (struct android_vnode *, + char *, size_t); +static int android_saf_new_open (struct android_vnode *, int, + mode_t, bool, int *, AAsset **); +static int android_saf_new_unlink (struct android_vnode *); +static int android_saf_new_symlink (const char *, struct android_vnode *); +static int android_saf_new_rmdir (struct android_vnode *); +static int android_saf_new_rename (struct android_vnode *, + struct android_vnode *, bool); +static int android_saf_new_stat (struct android_vnode *, struct stat *); +static int android_saf_new_access (struct android_vnode *, int); +static int android_saf_new_mkdir (struct android_vnode *, mode_t); +static struct android_vdir *android_saf_new_opendir (struct android_vnode *); + +/* Vector of VFS operations associated with SAF new VFS nodes. */ + +static struct android_vops saf_new_vfs_ops = + { + android_saf_new_name, + android_saf_new_open, + android_saf_tree_close, + 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, + }; + +static struct android_vnode * +android_saf_new_name (struct android_vnode *vnode, char *name, + size_t length) +{ + struct android_saf_new_vnode *vp; + + /* If LENGTH is empty, make a copy of this vnode and return it. */ + + if (length < 1) + { + vp = xmalloc (sizeof *vp); + memcpy (vp, vnode, sizeof *vp); + + /* Duplicate the information contained within VNODE. */ + + vp->tree_uri = xstrdup (vp->tree_uri); + vp->tree_id = xstrdup (vp->tree_id); + vp->name = xstrdup (vp->name); + vp->document_id = xstrdup (vp->name); + + return &vp->vnode; + } + + /* A nonexistent vnode has no children of its own. */ + errno = ENOTDIR; + return NULL; +} + +static int +android_saf_new_open (struct android_vnode *vnode, int flags, + mode_t mode, bool asset_p, int *fd_return, + AAsset **asset) +{ + struct android_saf_new_vnode *vp; + char *end; + jstring name, id, uri, new_id; + const char *new_doc_id; + jmethodID method; + + /* If creating a file wasn't intended, return ENOENT. */ + + if (!(flags & O_CREAT)) + { + errno = ENOENT; + return -1; + } + + /* If vp->name indicates that it's a directory, return ENOENT. */ + + vp = (struct android_saf_new_vnode *) vnode; + end = strrchr (vp->name, '/'); + + /* VP->name must contain at least one directory separator. */ + eassert (end); + + if (end[1] == '\0') + { + errno = ENOENT; + return -1; + } + + /* Otherwise, try to create a new document. First, build strings + for the name, ID and document URI. */ + + name = (*android_java_env)->NewStringUTF (android_java_env, + end + 1); + android_exception_check (); + id = (*android_java_env)->NewStringUTF (android_java_env, + vp->document_id); + android_exception_check_1 (name); + uri = (*android_java_env)->NewStringUTF (android_java_env, + vp->tree_uri); + android_exception_check_2 (name, id); + + /* Next, try to create a new document and retrieve its ID. */ + + method = service_class.create_document; + new_id = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, + emacs_service, + service_class.class, + method, uri, id, + name); + android_exception_check_3 (name, id, uri); + + /* Delete unused local references. */ + ANDROID_DELETE_LOCAL_REF (name); + ANDROID_DELETE_LOCAL_REF (id); + ANDROID_DELETE_LOCAL_REF (uri); + + if (!new_id) + { + /* The file couldn't be created for some reason. */ + errno = EIO; + return -1; + } + + /* Now, free VP->document_id and replace it with the service + document ID. */ + + new_doc_id = (*android_java_env)->GetStringUTFChars (android_java_env, + new_id, NULL); + android_exception_check_nonnull ((void *) new_doc_id, new_id); + + xfree (vp->document_id); + vp->document_id = xstrdup (new_doc_id); + + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + new_id, new_doc_id); + ANDROID_DELETE_LOCAL_REF (new_id); + + /* Finally, transform this vnode into a file vnode and call its + `open' function. */ + vp->vnode.type = ANDROID_VNODE_SAF_FILE; + vp->vnode.ops = &saf_file_vfs_ops; + return (*vp->vnode.ops->open) (vnode, flags, mode, asset_p, + fd_return, asset); +} + +static int +android_saf_new_unlink (struct android_vnode *vnode) +{ + errno = ENOENT; + return -1; +} + +static int +android_saf_new_symlink (const char *target, struct android_vnode *vnode) +{ + errno = EPERM; + return -1; +} + +static int +android_saf_new_rmdir (struct android_vnode *vnode) +{ + errno = ENOENT; + return -1; +} + +static int +android_saf_new_rename (struct android_vnode *src, + struct android_vnode *dst, + bool keep_existing) +{ + errno = ENOENT; + return -1; +} + +static int +android_saf_new_stat (struct android_vnode *vnode, + struct stat *statb) +{ + errno = ENOENT; + return -1; +} + +static int +android_saf_new_access (struct android_vnode *vnode, int mode) +{ + if (mode != F_OK && !(mode & (W_OK | X_OK | R_OK))) + errno = EINVAL; + else + errno = ENOENT; + + return -1; +} + +static int +android_saf_new_mkdir (struct android_vnode *vnode, mode_t mode) +{ + /* TODO */ + errno = ENOSYS; + return -1; +} + +static struct android_vdir * +android_saf_new_opendir (struct android_vnode *vnode) +{ + errno = ENOENT; + return NULL; +} + + + +/* Root vnode. This vnode represents the root inode, and is a regular + Unix vnode with modifications to `name' that make it return asset + vnodes. */ + +static struct android_vnode *android_root_name (struct android_vnode *, + char *, size_t); + +/* Vector of VFS operations associated with Unix root filesystem VFS + nodes. */ + +static struct android_vops root_vfs_ops = + { + android_root_name, + 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_opendir, + }; + +/* Array of special named vnodes. */ + +static struct android_special_vnode special_vnodes[] = + { + { "assets", 6, android_afs_initial, }, + { "content", 7, android_content_initial, }, + }; + +static struct android_vnode * +android_root_name (struct android_vnode *vnode, char *name, + size_t length) +{ + char *component_end; + struct android_special_vnode *special; + size_t i; + + /* Skip any leading separator in NAME. */ + + if (*name == '/') + name++, length--; + + /* Look for the first directory separator. */ + component_end = strchr (name, '/'); + + /* If not there, use name + length. */ + + if (!component_end) + component_end = name + length; + else + /* Move past the spearator character. */ + component_end++; + + /* Now, find out if the first component is a special vnode; if so, + call its root lookup function with the rest of NAME there. */ + + for (i = 0; i < ARRAYELTS (special_vnodes); ++i) + { + special = &special_vnodes[i]; + + if (component_end - name == special->length + && !memcmp (special->name, name, special->length)) + return (*special->initial) (component_end, + length - special->length); + + /* Detect the case where a special is named with a trailing + directory separator. */ + + if (component_end - name == special->length + 1 + && !memcmp (special->name, name, special->length) + && name[special->length] == '/') + /* Make sure to include the directory separator. */ + return (*special->initial) (component_end - 1, + length - special->length); + } + + /* Otherwise, continue searching for a vnode normally. */ + return android_unix_name (vnode, name, length); +} + + + +/* File system lookup. */ + +/* Look up the vnode that designates NAME, a file name that is at + least N bytes. + + NAME may be either an absolute file name or a name relative to the + current working directory. It must not be longer than PATH_MAX + bytes. + + Value is NULL upon failure with errno set accordingly, or the + vnode. */ + +static struct android_vnode * +android_name_file (const char *name) +{ + char buffer[PATH_MAX + 1], *head; + const char *end; + size_t len; + int nslash, c; + struct android_vnode *vp; + + len = strlen (name); + if (len > PATH_MAX) + { + errno = ENAMETOOLONG; + return NULL; + } + + /* Now, try to ``normalize'' the file name by removing consecutive + slash characters while copying it to BUFFER. */ + + head = buffer; + nslash = 0; + for (end = name + len; name < end; ++name) + { + c = *name; + + switch (c) + { + case '/': + /* This is a directory separator character. Two consecutive + separator characters should be replaced by a single + character; more than three in a row means that the + section of the file name before the last slash character + should be discarded. */ + + if (!nslash) + *head++ = '/'; + + nslash++; + + if (nslash >= 3) + /* Return to the root directory. */ + head = buffer, *head++ = '/', nslash = 0; + break; + + default: + /* Otherwise, copy the file name over. */ + nslash = 0; + *head++ = *name; + break; + } + } + + /* Terminate the file name. */ + *head = '\0'; + + /* If HEAD is a relative file name, it can't reside inside the + virtual mounts; create a Unix vnode instead. */ + + if (head == buffer || buffer[0] != '/') + return android_unix_vnode (buffer); + + /* Start looking from the root vnode. */ + vp = &root_vnode.vnode; + + /* If buffer is empty, this will create a duplicate of the root + vnode. */ + return (*vp->ops->name) (vp, buffer + 1, head - buffer - 1); +} + + + +/* Initialize the virtual filesystem layer. Load the directory tree + from the given asset MANAGER (which should be a local reference + within ENV) that will be used to access assets in the future, and + create the root vnode. + + ENV should be a JNI environment valid for future calls to VFS + functions. */ + +void +android_vfs_init (JNIEnv *env, jobject manager) +{ + jclass old; + + android_init_assets (env, manager); + + /* Create the root vnode, which is used to locate all other + vnodes. */ + root_vnode.vnode.ops = &root_vfs_ops; + root_vnode.vnode.type = ANDROID_VNODE_UNIX; + root_vnode.vnode.flags = 0; + root_vnode.name_length = 1; + root_vnode.name = "/"; + + /* Initialize some required classes. */ + java_string_class = (*env)->FindClass (env, "java/lang/String"); + assert (java_string_class); + + old = java_string_class; + java_string_class = (jclass) (*env)->NewGlobalRef (env, + java_string_class); + assert (java_string_class); + (*env)->DeleteLocalRef (env, old); + + /* And initialize those used on Android 5.0 and later. */ + + if (android_get_current_api_level () < 21) + return; + + android_init_cursor_class (env); + android_init_entry_class (env); + android_init_fd_class (env); +} + +/* The replacement functions that follow have several major + drawbacks: + + The first is that CWD relative file names will always be Unix + vnodes, and looking up their parents will always return another + Unix vnode. For example, with the working directory set to + /sdcard: + + ../content/storage + + will find /sdcard/../content/storage on the Unix filesystem, + opposed to /content/storage within the ``content'' VFS. + + Emacs only uses file names expanded through `expand-file-name', so + this is unproblematic in practice. + + The second is that `..' components do not usually check that their + preceding component is a directory. This is a side effect of their + removal from file names as part of a pre-processing step before + they are opened. So, even if: + + /sdcard/foo.txt + + is a file, opening the directory: + + /sdcard/foo.txt/.. + + will be successful. + + The third is that the handling of `..' components relative to + another vnode hasn't been tested and is only assumed to work + because the code has been written. It does not pose a practical + problem, however, as Emacs only names files starting from the root + vnode. + + The fourth is that errno values from vnode operations don't always + reflect what the Unix system calls they emulate can return: for + example, `open' may return EIO, while trying to `mkdir' within + /content will return ENOENT instead of EROFS. This is a + consequence of how accessing a non-existent file may fail at vnode + lookup, instead of when a vop is used. This problem hasn't made a + sufficient nuisance of itself to justify its fix yet. + + The fifth is that trailing directory separators may be lost when + naming files relative to another vnode, as a consequence of an + optimization used to avoid allocating too much stack or heap + space. + + The sixth is that flags and other argument checking is nowhere near + exhaustive on vnode types other than Unix vnodes. + + And the final drawback is that directories cannot be directly + opened. Instead, `dirfd' must be called on a directory stream used + by `openat'. + + Caveat emptor! */ + +/* Open the VFS node designated by NAME, taking into account FLAGS and + MODE, both of which mean the same as they do in a call to `open'. + + Value is -1 upon failure with errno set accordingly, and a file + descriptor otherwise. */ + +int +android_open (const char *name, int flags, mode_t mode) +{ + struct android_vnode *vp; + int fd, rc; + + vp = android_name_file (name); + if (!vp) + return -1; + + rc = (*vp->ops->open) (vp, flags, mode, false, &fd, NULL); + (*vp->ops->close) (vp); + + if (rc < 0) + return -1; + + /* If rc is 1, then an asset file descriptor has been returned. + This is impossible, so assert that it doesn't transpire. */ + assert (rc != 1); + return fd; +} + +/* Unlink the VFS node designated by the specified FILE. + Value is -1 upon failure with errno set, and 0 otherwise. */ + +int +android_unlink (const char *name) +{ + struct android_vnode *vp; + int rc; + + vp = android_name_file (name); + if (!vp) + return -1; + + rc = (*vp->ops->unlink) (vp); + (*vp->ops->close) (vp); + return rc; +} + +/* Symlink the VFS node designated by LINKPATH to TARGET. + Value is -1 upon failure with errno set, and 0 otherwise. */ + +int +android_symlink (const char *target, const char *linkpath) +{ + struct android_vnode *vp; + int rc; + + vp = android_name_file (linkpath); + if (!vp) + return -1; + + rc = (*vp->ops->symlink) (target, vp); + (*vp->ops->close) (vp); + return rc; +} + +/* Remove the empty directory at the VFS node designated by NAME. + Value is -1 upon failure with errno set, and 0 otherwise. */ + +int +android_rmdir (const char *name) +{ + struct android_vnode *vp; + int rc; + + vp = android_name_file (name); + if (!vp) + return -1; + + rc = (*vp->ops->rmdir) (vp); + (*vp->ops->close) (vp); + return rc; +} + +/* Create a directory at the VFS node designated by NAME and the given + access MODE. Value is -1 upon failure with errno set, 0 + otherwise. */ + +int +android_mkdir (const char *name, mode_t mode) +{ + struct android_vnode *vp; + int rc; + + vp = android_name_file (name); + if (!vp) + return -1; + + rc = (*vp->ops->mkdir) (vp, mode); + (*vp->ops->close) (vp); + return rc; +} + +/* Rename the vnode designated by SRC to the vnode designated by DST. + If DST already exists, return -1 and set errno to EEXIST. + + SRCFD and DSTFD should be AT_FDCWD, or else value is -1 and errno + is ENOSYS. + + If the filesystem or vnodes containing either DST or SRC does not + support rename operations that also check for a preexisting + destination, return -1 and set errno to ENOSYS. + + Otherwise, value and errno are identical to that of Unix + `rename' with the same arguments. */ + +int +android_renameat_noreplace (int srcfd, const char *src, + int dstfd, const char *dst) +{ + struct android_vnode *vp, *vdst; + int rc; + + if (srcfd != AT_FDCWD || dstfd != AT_FDCWD) + { + errno = ENOSYS; + return -1; + } + + /* Find vnodes for both src and dst. */ + + vp = android_name_file (src); + if (!vp) + goto error; + + vdst = android_name_file (dst); + if (!vdst) + goto error1; + + /* Now try to rename vp to vdst. */ + rc = (*vp->ops->rename) (vp, vdst, true); + (*vp->ops->close) (vp); + (*vp->ops->close) (vdst); + return rc; + + error1: + (*vp->ops->close) (vp); + error: + return -1; +} + +/* Like `android_renameat_noreplace', but don't check for DST's + existence and don't accept placeholder SRCFD and DSTFD + arguments. */ + +int +android_rename (const char *src, const char *dst) +{ + struct android_vnode *vp, *vdst; + int rc; + + /* Find vnodes for both src and dst. */ + + vp = android_name_file (src); + if (!vp) + goto error; + + vdst = android_name_file (dst); + if (!vdst) + goto error1; + + /* Now try to rename vp to vdst. */ + rc = (*vp->ops->rename) (vp, vdst, false); + (*vp->ops->close) (vp); + (*vp->ops->close) (vdst); + return rc; + + error1: + (*vp->ops->close) (vp); + error: + return -1; +} + + + +/* fstat, fstatat, faccessat, close/fclose etc. These functions are + somewhat tricky to wrap: they (at least partially) operate on file + descriptors, which sometimes provide a base directory for the + filesystem operations they perform. VFS nodes aren't mapped to + file descriptors opened through them, which makes this troublesome. + + openat is not wrapped at all; uses are defined out when Emacs is + being built for Android. The other functions fall back to directly + making Unix system calls when their base directory arguments are + not AT_FDCWD and no directory stream returned from + `android_opendir' ever returned that file descriptor, which is + enough to satisfy Emacs's current requirements for those functions + when a directory file descriptor is supplied. + + fclose and close are finally wrapped because they need to erase + information used to link file descriptors with file statistics from + their origins; fstat is also wrapped to take this information into + account, so that it can return correct file statistics for asset + directory files. */ + +/* Like fstat. However, look up the asset corresponding to the file + descriptor. If it exists, return the right information. */ + +int +android_fstat (int fd, struct stat *statb) +{ + struct android_afs_open_fd *tem; + struct android_parcel_fd *parcel_fd; + int rc; + + for (tem = afs_file_descriptors; tem; tem = tem->next) + { + if (tem->fd == fd) + { + memcpy (statb, &tem->statb, sizeof *statb); + return 0; + } + } + + rc = fstat (fd, statb); + + /* Now look for a matching parcel file descriptor and use its + mtime if available. */ + + parcel_fd = open_parcel_fds; + for (; parcel_fd; parcel_fd = parcel_fd->next) + { + if (parcel_fd->fd == fd + && timespec_valid_p (parcel_fd->mtime)) + { + statb->st_mtim = parcel_fd->mtime; + break; + } + } + + return rc; +} + +/* If DIRFD is a file descriptor returned by `android_readdir' for a + non-Unix file stream, return FILENAME relative to the file name of + the directory represented by that stream within BUFFER, a buffer + SIZE bytes long. + + Value is 0 if a file name is returned, 1 otherwise. */ + +static int +android_fstatat_1 (int dirfd, const char *filename, + char *restrict buffer, size_t size) +{ + char *dir_name; + struct android_saf_root_vdir *vdir; + struct android_saf_tree_vdir *vdir1; + + /* Now establish whether DIRFD is a file descriptor corresponding to + an open asset directory stream. */ + + dir_name = android_afs_get_directory_name (dirfd); + + if (dir_name) + { + /* Look for PATHNAME relative to this directory within an asset + vnode. */ + snprintf (buffer, size, "/assets%s%s", dir_name, + filename); + return 0; + } + + /* Do the same, but for /content directories instead. */ + + dir_name = android_content_get_directory_name (dirfd); + + if (dir_name) + { + /* Look for PATHNAME relative to this directory within an asset + vnode. */ + snprintf (buffer, size, "%s/%s", dir_name, + filename); + return 0; + } + + /* And for /content/storage. */ + + vdir = android_saf_root_get_directory (dirfd); + + if (vdir) + { + if (vdir->authority) + snprintf (buffer, size, "/content/storage/%s/%s", + vdir->authority, filename); + else + snprintf (buffer, size, "/content/storage/%s", + filename); + + return 0; + } + + /* /content/storage/foo/... */ + + vdir1 = android_saf_tree_get_directory (dirfd); + + if (vdir1) + { + snprintf (buffer, size, "%s%s", vdir1->name, filename); + return 0; + } + + return 1; +} + +/* If DIRFD is AT_FDCWD or a file descriptor returned by + `android_dirfd', or PATHNAME is an absolute file name, return the + file status of the VFS node designated by PATHNAME relative to the + VFS node corresponding to DIRFD, or relative to the current working + directory if DIRFD is AT_FDCWD. + + Otherwise, call `fstatat' with DIRFD, PATHNAME, STATBUF and + FLAGS. */ + +int +android_fstatat (int dirfd, const char *restrict pathname, + struct stat *restrict statbuf, int flags) +{ + char buffer[PATH_MAX + 1]; + struct android_vnode *vp; + int rc; + + /* Emacs uses AT_SYMLINK_NOFOLLOW, but fortunately (?) DIRFD is + never known to Emacs or AT_FDCWD when it originates from a VFS + node representing a filesystem that supports symlinks. */ + + if (dirfd == AT_FDCWD || pathname[0] == '/') + goto vfs; + + /* Now establish whether DIRFD is a file descriptor corresponding to + an open VFS directory stream. */ + + if (!android_fstatat_1 (dirfd, pathname, buffer, PATH_MAX + 1)) + { + pathname = buffer; + goto vfs; + } + + /* Fall back to fstatat. */ + return fstatat (dirfd, pathname, statbuf, flags); + + vfs: + vp = android_name_file (pathname); + if (!vp) + return -1; + + rc = (*vp->ops->stat) (vp, statbuf); + (*vp->ops->close) (vp); + return rc; +} + +/* Like `android_fstatat', but check file accessibility instead of + status. */ + +int +android_faccessat (int dirfd, const char *restrict pathname, + int mode, int flags) +{ + char buffer[PATH_MAX + 1]; + struct android_vnode *vp; + int rc; + + /* Emacs uses AT_SYMLINK_NOFOLLOW, but fortunately (?) DIRFD is + never known to Emacs or AT_FDCWD when it originates from a VFS + node representing a filesystem that supports symlinks. */ + + if (dirfd == AT_FDCWD || pathname[0] == '/') + goto vfs; + + /* Now establish whether DIRFD is a file descriptor corresponding to + an open VFS directory stream. */ + + if (!android_fstatat_1 (dirfd, pathname, buffer, PATH_MAX + 1)) + { + pathname = buffer; + goto vfs; + } + + /* Fall back to faccessat. */ + return faccessat (dirfd, pathname, mode, flags); + + vfs: + vp = android_name_file (pathname); + if (!vp) + return -1; + + rc = (*vp->ops->access) (vp, mode); + (*vp->ops->close) (vp); + return rc; +} + +/* Like `fdopen', but if FD is a parcel file descriptor, ``detach'' it + from the original. + + This is necessary because ownership over parcel file descriptors is + retained by the ParcelFileDescriptor objects that return them, + while file streams also require ownership over file descriptors + they are created on behalf of. + + Detaching the parcel file descriptor linked to FD consequentially + prevents the owner from being notified when it is eventually + closed, but for now that hasn't been demonstrated to be problematic + yet, as Emacs doesn't write to file streams. */ + +FILE * +android_fdopen (int fd, const char *mode) +{ + struct android_parcel_fd *tem, **next, *temp; + int new_fd; + + for (next = &open_parcel_fds; (tem = *next);) + { + if (tem->fd == fd) + { + new_fd + = (*android_java_env)->CallIntMethod (android_java_env, + tem->descriptor, + fd_class.detach_fd); + temp = tem->next; + xfree (tem); + *next = temp; + android_exception_check (); + + /* Assert that FD (returned from `getFd') is identical to + the file descriptor returned by `detachFd'. */ + + if (fd != new_fd) + emacs_abort (); + + goto open_file; + } + } + + open_file: + return fdopen (fd, mode); +} + +/* Like close. However, remove the file descriptor from the asset + table as well. */ + +int +android_close (int fd) +{ + struct android_afs_open_fd *tem, **next, *temp; + + if (android_close_parcel_fd (fd)) + return 0; + + for (next = &afs_file_descriptors; (tem = *next);) + { + if (tem->fd == fd) + { + temp = tem->next; + xfree (tem); + *next = temp; + + break; + } + else + next = &(*next)->next; + } + + return close (fd); +} + +/* Like fclose. However, remove any information associated with + FILE's file descriptor from the asset table as well. */ + +int +android_fclose (FILE *stream) +{ + int fd; + struct android_afs_open_fd *tem, **next, *temp; + + fd = fileno (stream); + + if (fd == -1) + goto skip; + + for (next = &afs_file_descriptors; (tem = *next);) + { + if (tem->fd == fd) + { + temp = tem->next; + xfree (*next); + *next = temp; + + break; + } + else + next = &(*next)->next; + } + + skip: + return fclose (stream); +} + + + +/* External asset management interface. By using functions here + to read and write from files, Emacs can avoid opening a + shared memory file descriptor for each ``asset'' file. */ + +/* Like android_open. However, return a structure that can + either directly hold an AAsset or a file descriptor. + + Value is the structure upon success. Upon failure, value + consists of an uninitialized file descriptor, but its asset + field is set to -1, and errno is set accordingly. */ + +struct android_fd_or_asset +android_open_asset (const char *filename, int oflag, mode_t mode) +{ + struct android_fd_or_asset fd; + AAsset *asset; + int rc; + struct android_vnode *vp; + + /* Now name this file. */ + vp = android_name_file (filename); + if (!vp) + goto failure; + + rc = (*vp->ops->open) (vp, oflag, mode, true, &fd.fd, + &asset); + (*vp->ops->close) (vp); + + /* Upon failure, return fd with its asset field set to (void *) + -1. */ + + if (rc < 0) + { + failure: + fd.asset = (void *) -1; + fd.fd = -1; + return fd; + } + + if (rc == 1) + { + /* An asset file was returned. Return the structure containing + an asset. */ + fd.asset = asset; + fd.fd = -1; + return fd; + } + + /* Otherwise, a file descriptor has been returned. Set fd.asset to + NULL, signifying that it is a file descriptor. */ + fd.asset = NULL; + return fd; +} + +/* Like android_close. However, it takes a ``file descriptor'' + opened using android_open_asset. */ + +int +android_close_asset (struct android_fd_or_asset asset) +{ + if (!asset.asset) + return android_close (asset.fd); + + AAsset_close (asset.asset); + return 0; +} + +/* Like `emacs_read_quit'. However, it handles file descriptors + opened using `android_open_asset' as well. */ + +ssize_t +android_asset_read_quit (struct android_fd_or_asset asset, + void *buffer, size_t size) +{ + if (!asset.asset) + return emacs_read_quit (asset.fd, buffer, size); + + /* It doesn't seem possible to quit from inside AAsset_read, + sadly. */ + return AAsset_read (asset.asset, buffer, size); +} + +/* Like `read'. However, it handles file descriptors opened + using `android_open_asset' as well. */ + +ssize_t +android_asset_read (struct android_fd_or_asset asset, + void *buffer, size_t size) +{ + if (!asset.asset) + return read (asset.fd, buffer, size); + + /* It doesn't seem possible to quit from inside AAsset_read, + sadly. */ + return AAsset_read (asset.asset, buffer, size); +} + +/* Like `lseek', but it handles ``file descriptors'' opened with + android_open_asset. */ + +off_t +android_asset_lseek (struct android_fd_or_asset asset, off_t off, + int whence) +{ + if (!asset.asset) + return lseek (asset.fd, off, whence); + + return AAsset_seek (asset.asset, off, whence); +} + +/* Like `fstat'. */ + +int +android_asset_fstat (struct android_fd_or_asset asset, + struct stat *statb) +{ + if (!asset.asset) + return android_fstat (asset.fd, statb); + + /* Clear statb. */ + memset (statb, 0, sizeof *statb); + + /* Set the mode. */ + statb->st_mode = S_IFREG | S_IRUSR | S_IRGRP | S_IROTH; + + /* Concoct a nonexistent device and an inode number. */ + statb->st_dev = -1; + statb->st_ino = 0; + + /* Owned by root. */ + statb->st_uid = 0; + statb->st_gid = 0; + + /* Size of the file. */ + statb->st_size = AAsset_getLength (asset.asset); + return 0; +} + + + +/* Directory listing emulation. */ + +/* Open a directory stream from the VFS node designated by NAME. + Value is NULL upon failure with errno set accordingly. + + The directory stream returned holds local references to JNI objects + and shouldn't be used after the current local reference frame is + popped. */ + +struct android_vdir * +android_opendir (const char *name) +{ + struct android_vnode *vp; + struct android_vdir *dir; + + vp = android_name_file (name); + if (!vp) + return NULL; + + dir = (*vp->ops->opendir) (vp); + (*vp->ops->close) (vp); + return dir; +} + +/* Like dirfd. However, value is not a real directory file descriptor + if DIR is an asset directory. */ + +int +android_dirfd (struct android_vdir *dirp) +{ + return (*dirp->dirfd) (dirp); +} + +/* Like readdir, but for VFS directory streams instead. */ + +struct dirent * +android_readdir (struct android_vdir *dirp) +{ + return (*dirp->readdir) (dirp); +} + +/* Like closedir, but for VFS directory streams instead. */ + +void +android_closedir (struct android_vdir *dirp) +{ + return (*dirp->closedir) (dirp); +} diff --git a/src/callproc.c b/src/callproc.c index ee5195385de..0645c2c3e18 100644 --- a/src/callproc.c +++ b/src/callproc.c @@ -214,7 +214,7 @@ record_kill_process (struct Lisp_Process *p, Lisp_Object tempfile) static void delete_temp_file (Lisp_Object name) { - unlink (SSDATA (name)); + emacs_unlink (SSDATA (name)); } static void diff --git a/src/charset.c b/src/charset.c index c532f79d282..d5e42d038df 100644 --- a/src/charset.c +++ b/src/charset.c @@ -488,7 +488,7 @@ load_charset_map_from_file (struct charset *charset, Lisp_Object mapfile, specbind (Qfile_name_handler_alist, Qnil); fd = openp (Vcharset_map_path, mapfile, suffixes, NULL, Qnil, false, false, NULL); - fp = fd < 0 ? 0 : fdopen (fd, "r"); + fp = fd < 0 ? 0 : emacs_fdopen (fd, "r"); if (!fp) { int open_errno = errno; diff --git a/src/dired.c b/src/dired.c index 93487d552e2..f2a123dc168 100644 --- a/src/dired.c +++ b/src/dired.c @@ -54,7 +54,7 @@ typedef DIR emacs_dir; /* The Android emulation of dirent stuff is required to be able to list the /assets special directory. */ -typedef struct android_dir emacs_dir; +typedef struct android_vdir emacs_dir; #define emacs_readdir android_readdir #define emacs_closedir android_closedir #endif diff --git a/src/emacs.c b/src/emacs.c index d75a83ab9d8..72f75d7890d 100644 --- a/src/emacs.c +++ b/src/emacs.c @@ -3012,7 +3012,7 @@ killed. */ { Lisp_Object listfile; listfile = Fexpand_file_name (Vauto_save_list_file_name, Qnil); - unlink (SSDATA (listfile)); + emacs_unlink (SSDATA (listfile)); } #ifdef HAVE_NATIVE_COMP diff --git a/src/fileio.c b/src/fileio.c index d24f25c2e06..5ce933ec45b 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -182,15 +182,12 @@ static bool e_write (int, Lisp_Object, ptrdiff_t, ptrdiff_t, -/* Check that ENCODED does not lie on any special directory whose - contents are read only. Signal a `file-error' if it does. - - If WRITE, then don't check that the file lies on `/content' on - Android. This special exception allows writing to content - provider-supplied files. */ +/* Establish that ENCODED is not contained within a special directory + whose contents are not eligible for Unix VFS operations. Signal a + `file-error' with REASON if it does. */ static void -check_mutable_filename (Lisp_Object encoded, bool write) +check_vfs_filename (Lisp_Object encoded, const char *reason) { #if defined HAVE_ANDROID && !defined ANDROID_STUBIFY const char *name; @@ -198,17 +195,10 @@ check_mutable_filename (Lisp_Object encoded, bool write) name = SSDATA (encoded); if (android_is_special_directory (name, "/assets")) - xsignal2 (Qfile_error, - build_string ("File lies on read-only directory"), - encoded); - - if (write) - return; + xsignal2 (Qfile_error, build_string (reason), encoded); if (android_is_special_directory (name, "/content")) - xsignal2 (Qfile_error, - build_string ("File lies on read-only directory"), - encoded); + xsignal2 (Qfile_error, build_string (reason), encoded); #endif /* defined HAVE_ANDROID && !defined ANDROID_STUBIFY */ } @@ -2287,7 +2277,6 @@ permissions. */) encoded_file = ENCODE_FILE (file); encoded_newname = ENCODE_FILE (newname); - check_mutable_filename (encoded_newname, true); #ifdef WINDOWSNT if (NILP (ok_if_already_exists) @@ -2553,7 +2542,7 @@ DEFUN ("make-directory-internal", Fmake_directory_internal, dir = SSDATA (encoded_dir); - if (mkdir (dir, 0777 & ~auto_saving_dir_umask) != 0) + if (emacs_mkdir (dir, 0777 & ~auto_saving_dir_umask) != 0) report_file_error ("Creating directory", directory); return Qnil; @@ -2572,9 +2561,7 @@ DEFUN ("delete-directory-internal", Fdelete_directory_internal, encoded_dir = ENCODE_FILE (directory); dir = SSDATA (encoded_dir); - check_mutable_filename (encoded_dir, false); - - if (rmdir (dir) != 0) + if (emacs_rmdir (dir) != 0) report_file_error ("Removing directory", directory); return Qnil; @@ -2613,9 +2600,9 @@ With a prefix argument, TRASH is nil. */) return call1 (Qmove_file_to_trash, filename); encoded_file = ENCODE_FILE (filename); - check_mutable_filename (encoded_file, false); - if (unlink (SSDATA (encoded_file)) != 0 && errno != ENOENT) + if (emacs_unlink (SSDATA (encoded_file)) != 0 + && errno != ENOENT) report_file_error ("Removing old name", filename); return Qnil; } @@ -2771,8 +2758,6 @@ This is what happens in interactive use with M-x. */) encoded_file = ENCODE_FILE (file); encoded_newname = ENCODE_FILE (newname); - check_mutable_filename (encoded_file, false); - check_mutable_filename (encoded_newname, false); bool plain_rename = (case_only_rename || (!NILP (ok_if_already_exists) @@ -2780,8 +2765,10 @@ This is what happens in interactive use with M-x. */) int rename_errno UNINIT; if (!plain_rename) { - if (renameat_noreplace (AT_FDCWD, SSDATA (encoded_file), - AT_FDCWD, SSDATA (encoded_newname)) + if (emacs_renameat_noreplace (AT_FDCWD, + SSDATA (encoded_file), + AT_FDCWD, + SSDATA (encoded_newname)) == 0) return Qnil; @@ -2803,7 +2790,8 @@ This is what happens in interactive use with M-x. */) if (plain_rename) { - if (rename (SSDATA (encoded_file), SSDATA (encoded_newname)) == 0) + if (emacs_rename (SSDATA (encoded_file), + SSDATA (encoded_newname)) == 0) return Qnil; rename_errno = errno; /* Don't prompt again. */ @@ -2884,8 +2872,10 @@ This is what happens in interactive use with M-x. */) encoded_file = ENCODE_FILE (file); encoded_newname = ENCODE_FILE (newname); - check_mutable_filename (encoded_file, false); - check_mutable_filename (encoded_newname, false); + check_vfs_filename (encoded_file, "Trying to create hard link to " + "file within special directory"); + check_vfs_filename (encoded_newname, "Trying to create hard link" + " within special directory"); if (link (SSDATA (encoded_file), SSDATA (encoded_newname)) == 0) return Qnil; @@ -2896,7 +2886,7 @@ This is what happens in interactive use with M-x. */) || FIXNUMP (ok_if_already_exists)) barf_or_query_if_file_exists (newname, true, "make it a new name", FIXNUMP (ok_if_already_exists), false); - unlink (SSDATA (newname)); + emacs_unlink (SSDATA (newname)); if (link (SSDATA (encoded_file), SSDATA (encoded_newname)) == 0) return Qnil; } @@ -2939,10 +2929,9 @@ This happens for interactive use with M-x. */) encoded_target = ENCODE_FILE (target); encoded_linkname = ENCODE_FILE (linkname); - check_mutable_filename (encoded_target, false); - check_mutable_filename (encoded_linkname, false); - if (symlink (SSDATA (encoded_target), SSDATA (encoded_linkname)) == 0) + if (emacs_symlink (SSDATA (encoded_target), + SSDATA (encoded_linkname)) == 0) return Qnil; if (errno == ENOSYS) @@ -2955,8 +2944,9 @@ This happens for interactive use with M-x. */) || FIXNUMP (ok_if_already_exists)) barf_or_query_if_file_exists (linkname, true, "make it a link", FIXNUMP (ok_if_already_exists), false); - unlink (SSDATA (encoded_linkname)); - if (symlink (SSDATA (encoded_target), SSDATA (encoded_linkname)) == 0) + emacs_unlink (SSDATA (encoded_linkname)); + if (emacs_symlink (SSDATA (encoded_target), + SSDATA (encoded_linkname)) == 0) return Qnil; } @@ -3328,27 +3318,12 @@ file_accessible_directory_p (Lisp_Object file) special cases "/" and "//", and it's a safe optimization here. After appending '.', append another '/' to work around a macOS bug (Bug#30350). */ -#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY - if (!strncmp ("/assets/", data, - sizeof "/assets" - 1)) - { - static char const appended[] = "/"; - char *buf = SAFE_ALLOCA (len + sizeof appended); - memcpy (buf, data, len); - strcpy (buf + len, &appended[data[len - 1] == '/']); - dir = buf; - } - else - { -#endif - static char const appended[] = "/./"; - char *buf = SAFE_ALLOCA (len + sizeof appended); - memcpy (buf, data, len); - strcpy (buf + len, &appended[data[len - 1] == '/']); - dir = buf; -#if defined HAVE_ANDROID && !defined ANDROID_STUBIFY - } -#endif + + static char const appended[] = "/./"; + char *buf = SAFE_ALLOCA (len + sizeof appended); + memcpy (buf, data, len); + strcpy (buf + len, &appended[data[len - 1] == '/']); + dir = buf; } ok = file_access_p (dir, F_OK); @@ -3682,7 +3657,8 @@ command from GNU Coreutils. */) return call4 (handler, Qset_file_modes, absname, mode, flag); encoded = ENCODE_FILE (absname); - check_mutable_filename (encoded, false); + check_vfs_filename (encoded, "Trying to change access modes of file" + " within special directory"); char *fname = SSDATA (encoded); mode_t imode = XFIXNUM (mode) & 07777; if (fchmodat (AT_FDCWD, fname, imode, nofollow) != 0) @@ -3755,7 +3731,8 @@ TIMESTAMP is in the format of `current-time'. */) return call4 (handler, Qset_file_times, absname, timestamp, flag); Lisp_Object encoded_absname = ENCODE_FILE (absname); - check_mutable_filename (encoded_absname, false); + check_vfs_filename (encoded_absname, "Trying to set access times of" + " file within special directory"); if (utimensat (AT_FDCWD, SSDATA (encoded_absname), ts, nofollow) != 0) { @@ -5456,7 +5433,6 @@ write_region (Lisp_Object start, Lisp_Object end, Lisp_Object filename, } encoded_filename = ENCODE_FILE (filename); - check_mutable_filename (encoded_filename, false); fn = SSDATA (encoded_filename); open_flags = O_WRONLY | O_CREAT; diff --git a/src/filelock.c b/src/filelock.c index cbbcc016b27..fea0044e219 100644 --- a/src/filelock.c +++ b/src/filelock.c @@ -228,7 +228,7 @@ get_boot_time (void) { get_boot_time_1 (SSDATA (filename), 1); if (delete_flag) - unlink (SSDATA (filename)); + emacs_unlink (SSDATA (filename)); } } @@ -323,11 +323,12 @@ rename_lock_file (char const *old, char const *new, bool force) { struct stat st; - int r = renameat_noreplace (AT_FDCWD, old, AT_FDCWD, new); + int r = emacs_renameat_noreplace (AT_FDCWD, old, + AT_FDCWD, new); if (! (r < 0 && errno == ENOSYS)) return r; if (link (old, new) == 0) - return unlink (old) == 0 || errno == ENOENT ? 0 : -1; + return emacs_unlink (old) == 0 || errno == ENOENT ? 0 : -1; if (errno != ENOSYS && errno != LINKS_MIGHT_NOT_WORK) return -1; @@ -347,7 +348,7 @@ rename_lock_file (char const *old, char const *new, bool force) return -1; } - return rename (old, new); + return emacs_rename (old, new); #endif } @@ -365,13 +366,13 @@ create_lock_file (char *lfname, char *lock_info_str, bool force) pretending that 'symlink' does not work. */ int err = ENOSYS; #else - int err = symlink (lock_info_str, lfname) == 0 ? 0 : errno; + int err = emacs_symlink (lock_info_str, lfname) == 0 ? 0 : errno; #endif if (err == EEXIST && force) { - unlink (lfname); - err = symlink (lock_info_str, lfname) == 0 ? 0 : errno; + emacs_unlink (lfname); + err = emacs_symlink (lock_info_str, lfname) == 0 ? 0 : errno; } if (err == ENOSYS || err == LINKS_MIGHT_NOT_WORK || err == ENAMETOOLONG) @@ -409,7 +410,7 @@ create_lock_file (char *lfname, char *lock_info_str, bool force) if (!err && rename_lock_file (nonce, lfname, force) != 0) err = errno; if (err) - unlink (nonce); + emacs_unlink (nonce); } SAFE_FREE (); @@ -622,7 +623,7 @@ current_lock_owner (lock_info_type *owner, Lisp_Object lfname) /* The owner process is dead or has a strange pid, so try to zap the lockfile. */ else - return unlink (SSDATA (lfname)) < 0 ? errno : 0; + return emacs_unlink (SSDATA (lfname)) < 0 ? errno : 0; } else { /* If we wanted to support the check for stale locks on remote machines, @@ -770,7 +771,8 @@ unlock_file (Lisp_Object fn) int err = current_lock_owner (0, lfname); if (! (err == 0 || err == ANOTHER_OWNS_IT || (err == I_OWN_IT - && (unlink (SSDATA (lfname)) == 0 || (err = errno) == ENOENT)))) + && (emacs_unlink (SSDATA (lfname)) == 0 + || (err = errno) == ENOENT)))) report_file_errno ("Unlocking file", fn, err); return Qnil; diff --git a/src/image.c b/src/image.c index 7501838d8c4..8a8f18754ac 100644 --- a/src/image.c +++ b/src/image.c @@ -4234,7 +4234,7 @@ static char * slurp_file (image_fd fd, ptrdiff_t *size) { #if !defined HAVE_ANDROID || defined ANDROID_STUBIFY - FILE *fp = fdopen (fd, "rb"); + FILE *fp = emacs_fdopen (fd, "rb"); char *buf = NULL; struct stat st; @@ -8021,7 +8021,7 @@ png_load_body (struct frame *f, struct image *img, struct png_load_context *c) } /* Open the image file. */ - fp = fdopen (fd, "rb"); + fp = emacs_fdopen (fd, "rb"); if (!fp) { image_error ("Cannot open image file `%s'", file); @@ -8750,7 +8750,7 @@ jpeg_load_body (struct frame *f, struct image *img, return 0; } - fp = fdopen (fd, "rb"); + fp = emacs_fdopen (fd, "rb"); if (fp == NULL) { image_error ("Cannot open `%s'", file); diff --git a/src/keyboard.c b/src/keyboard.c index ee42b821dfa..445f39ea140 100644 --- a/src/keyboard.c +++ b/src/keyboard.c @@ -11740,9 +11740,9 @@ This may include sensitive information such as passwords. */) encfile = ENCODE_FILE (file); fd = emacs_open (SSDATA (encfile), O_WRONLY | O_CREAT | O_EXCL, 0600); if (fd < 0 && errno == EEXIST - && (unlink (SSDATA (encfile)) == 0 || errno == ENOENT)) + && (emacs_unlink (SSDATA (encfile)) == 0 || errno == ENOENT)) fd = emacs_open (SSDATA (encfile), O_WRONLY | O_CREAT | O_EXCL, 0600); - dribble = fd < 0 ? 0 : fdopen (fd, "w"); + dribble = fd < 0 ? 0 : emacs_fdopen (fd, "w"); if (dribble == 0) report_file_error ("Opening dribble", file); } diff --git a/src/lisp.h b/src/lisp.h index 4be0002e9fc..5802984a5e7 100644 --- a/src/lisp.h +++ b/src/lisp.h @@ -5086,7 +5086,15 @@ extern int emacs_open (const char *, int, int); extern int emacs_open_noquit (const char *, int, int); extern int emacs_pipe (int[2]); extern int emacs_close (int); +extern FILE *emacs_fdopen (int, const char *); extern int emacs_fclose (FILE *); +extern int emacs_unlink (const char *); +extern int emacs_symlink (const char *, const char *); +extern int emacs_rmdir (const char *); +extern int emacs_mkdir (const char *, mode_t); +extern int emacs_renameat_noreplace (int, const char *, int, + const char *); +extern int emacs_rename (const char *, const char *); extern ptrdiff_t emacs_read (int, void *, ptrdiff_t); extern ptrdiff_t emacs_read_quit (int, void *, ptrdiff_t); extern ptrdiff_t emacs_write (int, void const *, ptrdiff_t); diff --git a/src/lread.c b/src/lread.c index 11bfc52a83f..251da5670d0 100644 --- a/src/lread.c +++ b/src/lread.c @@ -1702,7 +1702,7 @@ Return t if the file exists and loads successfully. */) stream = emacs_fopen (SSDATA (efound), fmode); #else #if !defined USE_ANDROID_ASSETS - stream = fdopen (fd, fmode); + stream = emacs_fdopen (fd, fmode); #else /* Android systems use special file descriptors which can point into compressed data and double as file streams. FMODE is diff --git a/src/process.c b/src/process.c index 8b7b8d8566e..90885ad318a 100644 --- a/src/process.c +++ b/src/process.c @@ -7472,6 +7472,16 @@ handle_child_signal (int sig) { changed = true; if (STRINGP (XCDR (head))) + /* handle_child_signal is called in an async signal + handler but needs to unlink temporary files which + might've been created in an Android content + provider. + + emacs_unlink is not async signal safe because + deleting files from content providers must proceed + through Java code. Consequentially, if XCDR (head) + lies on a content provider it will not be removed, + which is a bug. */ unlink (SSDATA (XCDR (head))); XSETCAR (tail, Qnil); } diff --git a/src/sysdep.c b/src/sysdep.c index bec2c00d3e5..88938d15b91 100644 --- a/src/sysdep.c +++ b/src/sysdep.c @@ -260,7 +260,7 @@ init_standard_fds (void) /* Set buferr if possible on platforms defining _PC_PIPE_BUF, as they support the notion of atomic writes to pipes. */ #ifdef _PC_PIPE_BUF - buferr = fdopen (STDERR_FILENO, "w"); + buferr = emacs_fdopen (STDERR_FILENO, "w"); if (buferr) setvbuf (buferr, NULL, _IOLBF, 0); #endif @@ -2545,7 +2545,7 @@ emacs_fopen (char const *file, char const *mode) } fd = emacs_open (file, omode | oflags | bflag, 0666); - return fd < 0 ? 0 : fdopen (fd, mode); + return fd < 0 ? 0 : emacs_fdopen (fd, mode); } /* Create a pipe for Emacs use. */ @@ -2627,6 +2627,19 @@ emacs_close (int fd) } } +/* Wrapper around fdopen. On Android, this calls `android_fclose' to + clear information associated with FD if necessary. */ + +FILE * +emacs_fdopen (int fd, const char *mode) +{ +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + return fdopen (fd, mode); +#else /* !defined HAVE_ANDROID || defined ANDROID_STUBIFY */ + return android_fdopen (fd, mode); +#endif /* !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) */ +} + /* Wrapper around fclose. On Android, this calls `android_fclose' to clear information associated with the FILE's file descriptor if necessary. */ @@ -2641,6 +2654,71 @@ emacs_fclose (FILE *stream) #endif } +/* Wrappers around unlink, symlink, rename, renameat_noreplace, and + rmdir. These operations handle asset and content directories on + Android. */ + +int +emacs_unlink (const char *name) +{ +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + return unlink (name); +#else /* !defined HAVE_ANDROID || defined ANDROID_STUBIFY */ + return android_unlink (name); +#endif /* !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) */ +} + +int +emacs_symlink (const char *target, const char *linkname) +{ +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + return symlink (target, linkname); +#else /* !defined HAVE_ANDROID || defined ANDROID_STUBIFY */ + return android_symlink (target, linkname); +#endif /* !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) */ +} + +int +emacs_rmdir (const char *dirname) +{ +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + return rmdir (dirname); +#else /* !defined HAVE_ANDROID || defined ANDROID_STUBIFY */ + return android_rmdir (dirname); +#endif /* !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) */ +} + +int +emacs_mkdir (const char *dirname, mode_t mode) +{ +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + return mkdir (dirname, mode); +#else /* !defined HAVE_ANDROID || defined ANDROID_STUBIFY */ + return android_mkdir (dirname, mode); +#endif /* !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) */ +} + +int +emacs_renameat_noreplace (int srcfd, const char *src, + int dstfd, const char *dst) +{ +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + return renameat_noreplace (srcfd, src, dstfd, dst); +#else /* !defined HAVE_ANDROID || defined ANDROID_STUBIFY */ + return android_renameat_noreplace (srcfd, src, dstfd, dst); +#endif /* !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) */ +} + +int +emacs_rename (const char *src, const char *dst) +{ +#if !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) + return rename (src, dst); +#else /* !defined HAVE_ANDROID || defined ANDROID_STUBIFY */ + return android_rename (src, dst); +#endif /* !(defined HAVE_ANDROID && !defined ANDROID_STUBIFY) */ +} + /* Maximum number of bytes to read or write in a single system call. This works around a serious bug in Linux kernels before 2.6.16; see . diff --git a/src/term.c b/src/term.c index 4de57ca1afe..9bcb2cb1386 100644 --- a/src/term.c +++ b/src/term.c @@ -2415,7 +2415,7 @@ frame's terminal). */) #else /* !MSDOS */ fd = emacs_open (t->display_info.tty->name, O_RDWR | O_NOCTTY, 0); t->display_info.tty->input = t->display_info.tty->output - = fd < 0 ? 0 : fdopen (fd, "w+"); + = fd < 0 ? 0 : emacs_fdopen (fd, "w+"); if (! t->display_info.tty->input) { @@ -4128,7 +4128,7 @@ init_tty (const char *name, const char *terminal_type, bool must_succeed) tty->input = tty->output = ((fd < 0 || ! isatty (fd)) ? NULL - : fdopen (fd, "w+")); + : emacs_fdopen (fd, "w+")); if (! tty->input) { -- cgit v1.3 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. --- doc/emacs/android.texi | 11 +++--- java/org/gnu/emacs/EmacsService.java | 34 +++------------- src/android.c | 3 +- src/androidvfs.c | 75 ++++++++++++++++++++++++++++++++++-- 4 files changed, 85 insertions(+), 38 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 1f32fdfc1d2..b86c71cea49 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -298,11 +298,12 @@ subsequently made available within a new directory named a unique identifier assigned to the directory by the document provider. - Because these directories do not exist within the Unix file-system, -sub-processes cannot be created within them, just as with the -@file{/assets} directory (@pxref{Android File System}.) In addition, -although Emacs can normally write and create files inside these -directories, it cannot create symlinks or hard links. + The same limitations applied to the @file{/assets} directory +(@pxref{Android File System}) are applied when creating sub-processes +within those directories, because they do not exist within the Unix +file-system. In addition, although Emacs can normally write and +create files inside these directories, it cannot create symlinks or +hard links. @c TODO: fix this! Since document providers are allowed to perform expensive network 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; } diff --git a/src/android.c b/src/android.c index d8b264a8491..098fa6c383d 100644 --- a/src/android.c +++ b/src/android.c @@ -1556,7 +1556,8 @@ android_init_emacs_service (void) FIND_METHOD (get_document_trees, "getDocumentTrees", "([B)[Ljava/lang/String;"); FIND_METHOD (document_id_from_name, "documentIdFromName", - "(Ljava/lang/String;[B[Ljava/lang/String;)I"); + "(Ljava/lang/String;Ljava/lang/String;" + "[Ljava/lang/String;)I"); FIND_METHOD (get_tree_uri, "getTreeUri", "(Ljava/lang/String;Ljava/lang/String;)" "Ljava/lang/String;"); diff --git a/src/androidvfs.c b/src/androidvfs.c index bab3977ed5a..c174c35f02b 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -3936,6 +3936,66 @@ static struct android_vops saf_new_vfs_ops; /* Chain of all open SAF directory streams. */ static struct android_saf_tree_vdir *all_saf_tree_vdirs; +/* Verify that the specified NULL-terminated STRING is a valid JNI + ``UTF-8'' string. Return 0 if so, 1 otherwise. + + The native coding system used by the JVM to store strings derives + from UTF-8, but deviates from it in two aspects in an attempt to + better represent the UCS-16 based Java String format, and to let + strings contain NULL characters while remaining valid C strings: + NULL bytes are encoded as two-byte sequences, and Unicode surrogate + pairs encoded as two-byte sequences are prefered to four-byte + sequences when encoding characters above the BMP. */ + +static int +android_verify_jni_string (const char *name) +{ + const unsigned char *chars; + + chars = (unsigned char *) name; + while (*chars) + { + /* Switch on the high 4 bits. */ + + switch (*chars++ >> 4) + { + case 0 ... 7: + /* The 8th bit is clean, so this is a regular C + character. */ + break; + + case 8 ... 0xb: + /* Invalid starting byte! */ + return 1; + + case 0xf: + /* The start of a four byte sequence. These aren't allowed + in Java. */ + return 1; + + case 0xe: + /* The start of a three byte sequence. Verify that its + continued. */ + + if ((*chars++ & 0xc0) != 0x80) + return 1; + + FALLTHROUGH; + + case 0xc ... 0xd: + /* The start of a two byte sequence. Verify that the + next byte exists and has its high bit set. */ + + if ((*chars++ & 0xc0) != 0x80) + return 1; + + break; + } + } + + return 0; +} + /* Find the document ID of the file within TREE_URI designated by NAME. @@ -3943,6 +4003,9 @@ static struct android_saf_tree_vdir *all_saf_tree_vdirs; individual files. Each constituent component prior to the last must name a directory file within TREE_URI. + If NAME is not correct for the Java ``modified UTF-8'' coding + system, return -1. + 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. @@ -3965,6 +4028,12 @@ android_document_id_from_name (const char *tree_uri, char *name, jmethodID method; const char *doc_id; + /* Verify the format of NAME. Don't allow creating files that + contain characters that can't be encoded in Java. */ + + if (android_verify_jni_string (name)) + return -1; + /* First, create the array that will hold the result. */ result = (*android_java_env)->NewObjectArray (android_java_env, 1, java_string_class, @@ -3972,11 +4041,9 @@ android_document_id_from_name (const char *tree_uri, char *name, android_exception_check (); /* Next, create the string for the tree URI and name. */ - length = strlen (name); - java_name = (*android_java_env)->NewByteArray (android_java_env, length); + java_name = (*android_java_env)->NewStringUTF (android_java_env, + name); android_exception_check_1 (result); - (*android_java_env)->SetByteArrayRegion (android_java_env, java_name, - 0, length, (jbyte *) name); uri = (*android_java_env)->NewStringUTF (android_java_env, tree_uri); android_exception_check_2 (result, java_name); -- cgit v1.3 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 ++++++++++-------- src/android.c | 5 + src/android.h | 2 + src/androidvfs.c | 316 +++++++++++++++++++++++++--- 5 files changed, 410 insertions(+), 125 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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; } }; diff --git a/src/android.c b/src/android.c index 098fa6c383d..687c0b48a2a 100644 --- a/src/android.c +++ b/src/android.c @@ -1577,6 +1577,11 @@ android_init_emacs_service (void) FIND_METHOD (create_document, "createDocument", "(Ljava/lang/String;Ljava/lang/String;" "Ljava/lang/String;)Ljava/lang/String;"); + FIND_METHOD (create_directory, "createDirectory", + "(Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;)Ljava/lang/String;"); + FIND_METHOD (delete_document, "deleteDocument", + "(Ljava/lang/String;Ljava/lang/String;)I"); #undef FIND_METHOD } diff --git a/src/android.h b/src/android.h index 94a3ad46e74..fd391fa6435 100644 --- a/src/android.h +++ b/src/android.h @@ -279,6 +279,8 @@ struct android_emacs_service jmethodID read_directory_entry; jmethodID open_document; jmethodID create_document; + jmethodID create_directory; + jmethodID delete_document; }; extern JNIEnv *android_java_env; diff --git a/src/androidvfs.c b/src/androidvfs.c index c174c35f02b..2cd50963e97 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -276,6 +276,10 @@ static struct emacs_directory_entry_class entry_class; class. */ static struct android_parcel_file_descriptor_class fd_class; +/* Global references to several exception classes. */ +static jclass file_not_found_exception, security_exception; +static jclass unsupported_operation_exception, out_of_memory_error; + /* Initialize `cursor_class' using the given JNI environment ENV. Calling this function is not necessary on Android 4.4 and earlier. */ @@ -3688,6 +3692,85 @@ android_saf_root_get_directory (int dirfd) /* Functions common to both SAF directory and file nodes. */ +/* Check for JNI exceptions, clear them, and set errno accordingly. + Also, free each of the N local references given as arguments if an + exception takes place. + + Value is 1 if an exception has taken place, 0 otherwise. + + If the exception thrown derives from FileNotFoundException, set + errno to ENOENT. + + If the exception thrown derives from SecurityException, set errno + to EACCES. + + If the exception thrown derives from UnsupportedOperationException, + set errno to ENOSYS. + + If the exception thrown derives from OutOfMemoryException, call + `memory_full'. + + If the exception thrown is anything else, set errno to EIO. */ + +static int +android_saf_exception_check (int n, ...) +{ + jthrowable exception; + JNIEnv *env; + va_list ap; + + env = android_java_env; + va_start (ap, n); + + /* First, check for an exception. */ + + if (!(*env)->ExceptionCheck (env)) + /* No exception has taken place. Return 0. */ + return 0; + + exception = (*env)->ExceptionOccurred (env); + + if (!exception) + /* JNI couldn't return a local reference to the exception. */ + memory_full (0); + + /* Clear the exception, making it safe to subsequently call other + JNI functions. */ + (*env)->ExceptionClear (env); + + /* Delete each of the N arguments. */ + + while (n > 0) + { + ANDROID_DELETE_LOCAL_REF (va_arg (ap, jobject)); + n--; + } + + /* Now set errno or signal memory_full as required. */ + + if ((*env)->IsInstanceOf (env, (jobject) exception, + file_not_found_exception)) + errno = ENOENT; + else if ((*env)->IsInstanceOf (env, (jobject) exception, + security_exception)) + errno = EACCES; + else if ((*env)->IsInstanceOf (env, (jobject) exception, + unsupported_operation_exception)) + errno = ENOSYS; + else if ((*env)->IsInstanceOf (env, (jobject) exception, + out_of_memory_error)) + { + ANDROID_DELETE_LOCAL_REF ((jobject) exception); + memory_full (0); + } + else + errno = EIO; + + /* expression is still a local reference! */ + ANDROID_DELETE_LOCAL_REF ((jobject) exception); + return 1; +} + /* Return file status for the document designated by ID_NAME within the document tree identified by URI_NAME. @@ -3727,11 +3810,13 @@ android_saf_stat (const char *uri_name, const char *id_name, if (id) { - android_exception_check_2 (uri, id); + if (android_saf_exception_check (2, uri, id)) + return -1; + ANDROID_DELETE_LOCAL_REF (id); } - else - android_exception_check_1 (uri); + else if (android_saf_exception_check (1, uri)) + return -1; ANDROID_DELETE_LOCAL_REF (uri); @@ -3810,11 +3895,13 @@ android_saf_access (const char *uri_name, const char *id_name, if (id) { - android_exception_check_2 (uri, id); + if (android_saf_exception_check (2, uri, id)) + return -1; + ANDROID_DELETE_LOCAL_REF (id); } - else - android_exception_check_1 (uri); + else if (android_saf_exception_check (1, uri)) + return -1; ANDROID_DELETE_LOCAL_REF (uri); @@ -3840,6 +3927,46 @@ android_saf_access (const char *uri_name, const char *id_name, return 0; } +/* Delete the document designated by DOC_ID within the tree identified + through the URI TREE. Return 0 if the document has been deleted, + set errno and return -1 upon failure. */ + +static int +android_saf_delete_document (const char *tree, const char *doc_id) +{ + jobject id, uri; + jmethodID method; + jint rc; + + /* Build the strings holding the ID and URI. */ + id = (*android_java_env)->NewStringUTF (android_java_env, + doc_id); + android_exception_check (); + uri = (*android_java_env)->NewStringUTF (android_java_env, + tree); + android_exception_check_1 (id); + + /* Now, try to delete the document. */ + method = service_class.delete_document; + rc = (*android_java_env)->CallIntMethod (android_java_env, + emacs_service, + method, uri, id); + + if (android_saf_exception_check (2, id, uri)) + return -1; + + ANDROID_DELETE_LOCAL_REF (id); + ANDROID_DELETE_LOCAL_REF (uri); + + if (rc) + { + errno = EACCES; + return -1; + } + + return 0; +} + /* SAF directory vnode. A file within a SAF directory tree is @@ -3863,7 +3990,9 @@ struct android_saf_tree_vnode char *tree_id; /* The document ID of the directory represented, or NULL if this is - the root directory of the tree. */ + the root directory of the tree. Since file and new vnodes don't + represent the root directory, this field is always set in + them. */ char *document_id; /* The file name of this tree vnode. This is a ``path'' to the @@ -4004,7 +4133,7 @@ android_verify_jni_string (const char *name) must name a directory file within TREE_URI. If NAME is not correct for the Java ``modified UTF-8'' coding - system, return -1. + system, return -1 and set errno to ENOENT. Upon success, return 0 or 1 (contingent upon whether or not the last component within NAME is a directory) and place the document @@ -4014,7 +4143,8 @@ android_verify_jni_string (const char *name) within NAME does and is also a directory, return -2 and place the document ID of that directory within *ID. - If the designated file can't be located, return -1. */ + If the designated file can't be located, return -1 and set errno + accordingly. */ static int android_document_id_from_name (const char *tree_uri, char *name, @@ -4023,7 +4153,6 @@ android_document_id_from_name (const char *tree_uri, char *name, jobjectArray result; jstring uri; jbyteArray java_name; - size_t length; jint rc; jmethodID method; const char *doc_id; @@ -4055,7 +4184,10 @@ android_document_id_from_name (const char *tree_uri, char *name, method, uri, java_name, result); - android_exception_check_3 (result, uri, java_name); + + if (android_saf_exception_check (3, result, uri, java_name)) + goto finish; + ANDROID_DELETE_LOCAL_REF (uri); ANDROID_DELETE_LOCAL_REF (java_name); @@ -4178,7 +4310,6 @@ android_saf_tree_name (struct android_vnode *vnode, char *name, /* The document ID can't be found. */ xfree (filename); - errno = ENOENT; return NULL; } @@ -4354,9 +4485,19 @@ android_saf_tree_symlink (const char *target, struct android_vnode *vnode) static int android_saf_tree_rmdir (struct android_vnode *vnode) { - /* TODO */ - errno = ENOSYS; - return -1; + struct android_saf_tree_vnode *vp; + + vp = (struct android_saf_tree_vnode *) vnode; + + /* Don't allow deleting the root directory. */ + + if (!vp->document_id) + { + errno = EROFS; + return -1; + } + + return android_saf_delete_document (vp->tree_uri, vp->document_id); } static int @@ -4412,8 +4553,8 @@ android_saf_tree_mkdir (struct android_vnode *vnode, mode_t mode) /* Open a database Cursor containing each directory entry within the supplied SAF tree vnode VP. - Value is NULL upon failure, a local reference to the Cursor object - otherwise. */ + Value is NULL upon failure with errno set to a suitable value, a + local reference to the Cursor object otherwise. */ static jobject android_saf_tree_opendir_1 (struct android_saf_tree_vnode *vp) @@ -4445,11 +4586,13 @@ android_saf_tree_opendir_1 (struct android_saf_tree_vnode *vp) if (id) { - android_exception_check_2 (id, uri); + if (android_saf_exception_check (2, id, uri)) + return NULL; + ANDROID_DELETE_LOCAL_REF (id); } - else - android_exception_check_1 (uri); + else if (android_saf_exception_check (1, uri)) + return NULL; ANDROID_DELETE_LOCAL_REF (uri); @@ -4657,7 +4800,6 @@ android_saf_tree_opendir (struct android_vnode *vnode) { xfree (dir); xfree (dir->name); - errno = ENOENT; return NULL; } @@ -4880,7 +5022,10 @@ android_saf_file_open (struct android_vnode *vnode, int flags, service_class.class, method, uri, id, write, trunc); - android_exception_check_2 (uri, id); + + if (android_saf_exception_check (2, uri, id)) + return -1; + ANDROID_DELETE_LOCAL_REF (uri); ANDROID_DELETE_LOCAL_REF (id); @@ -4953,9 +5098,10 @@ android_saf_file_open (struct android_vnode *vnode, int flags, static int android_saf_file_unlink (struct android_vnode *vnode) { - /* TODO */ - errno = ENOSYS; - return -1; + struct android_saf_file_vnode *vp; + + vp = (struct android_saf_file_vnode *) vnode; + return android_saf_delete_document (vp->tree_uri, vp->document_id); } static int @@ -5114,7 +5260,7 @@ android_saf_new_open (struct android_vnode *vnode, int flags, { errno = ENOENT; return -1; - } + } /* Otherwise, try to create a new document. First, build strings for the name, ID and document URI. */ @@ -5137,7 +5283,9 @@ android_saf_new_open (struct android_vnode *vnode, int flags, service_class.class, method, uri, id, name); - android_exception_check_3 (name, id, uri); + + if (android_saf_exception_check (3, name, id, uri)) + return -1; /* Delete unused local references. */ ANDROID_DELETE_LOCAL_REF (name); @@ -5225,9 +5373,90 @@ android_saf_new_access (struct android_vnode *vnode, int mode) static int android_saf_new_mkdir (struct android_vnode *vnode, mode_t mode) { - /* TODO */ - errno = ENOSYS; - return -1; + struct android_saf_new_vnode *vp; + jstring name, id, uri, new_id; + jmethodID method; + const char *new_doc_id; + char *end; + + vp = (struct android_saf_tree_vnode *) vnode; + + /* Find the last component of vp->name. */ + end = strrchr (vp->name, '/'); + + /* VP->name must contain at least one directory separator. */ + eassert (end); + + if (end[1] == '\0') + { + /* There's a trailing directory separator. Search + backwards. */ + + end--; + while (end != vp->name && *end != '/') + end--; + + /* vp->name[0] is always a directory separator. */ + eassert (*end == '/'); + } + + /* Otherwise, try to create a new document. First, build strings + for the name, ID and document URI. */ + + name = (*android_java_env)->NewStringUTF (android_java_env, + end + 1); + android_exception_check (); + id = (*android_java_env)->NewStringUTF (android_java_env, + vp->document_id); + android_exception_check_1 (name); + uri = (*android_java_env)->NewStringUTF (android_java_env, + vp->tree_uri); + android_exception_check_2 (name, id); + + /* Next, try to create a new document and retrieve its ID. */ + + method = service_class.create_directory; + new_id = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, + emacs_service, + service_class.class, + method, uri, id, + name); + + if (android_saf_exception_check (3, name, id, uri)) + return -1; + + /* Delete unused local references. */ + ANDROID_DELETE_LOCAL_REF (name); + ANDROID_DELETE_LOCAL_REF (id); + ANDROID_DELETE_LOCAL_REF (uri); + + if (!new_id) + { + /* The file couldn't be created for some reason. */ + errno = EIO; + return -1; + } + + /* Now, free VP->document_id and replace it with the service + document ID. */ + + new_doc_id = (*android_java_env)->GetStringUTFChars (android_java_env, + new_id, NULL); + + if (android_saf_exception_check (3, name, id, uri)) + return -1; + + xfree (vp->document_id); + vp->document_id = xstrdup (new_doc_id); + + (*android_java_env)->ReleaseStringUTFChars (android_java_env, + new_id, new_doc_id); + ANDROID_DELETE_LOCAL_REF (new_id); + + /* Finally, transform this vnode into a directory vnode. */ + vp->vnode.type = ANDROID_VNODE_SAF_TREE; + vp->vnode.ops = &saf_tree_vfs_ops; + return 0; } static struct android_vdir * @@ -5449,6 +5678,29 @@ android_vfs_init (JNIEnv *env, jobject manager) android_init_cursor_class (env); android_init_entry_class (env); android_init_fd_class (env); + + /* Initialize each of the exception classes used by + `android_saf_exception_check'. */ + + old = (*env)->FindClass (env, "java/io/FileNotFoundException"); + file_not_found_exception = (*env)->NewGlobalRef (env, old); + (*env)->DeleteLocalRef (env, old); + eassert (file_not_found_exception); + + old = (*env)->FindClass (env, "java/lang/SecurityException"); + security_exception = (*env)->NewGlobalRef (env, old); + (*env)->DeleteLocalRef (env, old); + eassert (security_exception); + + old = (*env)->FindClass (env, "java/lang/UnsupportedOperationException"); + unsupported_operation_exception = (*env)->NewGlobalRef (env, old); + (*env)->DeleteLocalRef (env, old); + eassert (unsupported_operation_exception); + + old = (*env)->FindClass (env, "java/lang/OutOfMemoryError"); + out_of_memory_error = (*env)->NewGlobalRef (env, old); + (*env)->DeleteLocalRef (env, old); + eassert (out_of_memory_error); } /* The replacement functions that follow have several major @@ -5606,7 +5858,7 @@ android_mkdir (const char *name, mode_t mode) rc = (*vp->ops->mkdir) (vp, mode); (*vp->ops->close) (vp); - return rc; + return rc; } /* Rename the vnode designated by SRC to the vnode designated by DST. @@ -5724,7 +5976,7 @@ android_fstat (int fd, struct stat *statb) for (tem = afs_file_descriptors; tem; tem = tem->next) { if (tem->fd == fd) - { + { memcpy (statb, &tem->statb, sizeof *statb); return 0; } -- cgit v1.3 From 0709e03f88cdef8f785338cab9315b527db0854e Mon Sep 17 00:00:00 2001 From: Po Lu Date: Fri, 28 Jul 2023 15:19:37 +0800 Subject: Allow quitting from Android content provider operations * doc/emacs/android.texi (Android Document Providers): Say that quitting is now possible. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New functions `safSyncAndReadInput', `safync' and `safPostRequest'. * java/org/gnu/emacs/EmacsSafThread.java: New file. Move cancel-able SAF operations here. * java/org/gnu/emacs/EmacsService.java (EmacsService): Allow quitting from most SAF operations. * src/androidvfs.c (android_saf_exception_check): Return EINTR if OperationCanceledException is received. (android_saf_stat, android_saf_access) (android_document_id_from_name, android_saf_tree_opendir_1) (android_saf_file_open): Don't allow reentrant calls from async input handlers. (NATIVE_NAME): Implement new synchronization primitives for JNI. (android_vfs_init): Initialize new class. * src/dired.c (open_directory): Handle EINTR from opendir. * src/sysdep.c: Describe which operations may return EINTR on Android. --- doc/emacs/android.texi | 8 +- java/org/gnu/emacs/EmacsNative.java | 17 + java/org/gnu/emacs/EmacsSafThread.java | 922 +++++++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsService.java | 520 ++----------------- src/androidvfs.c | 132 ++++- src/dired.c | 9 + src/sysdep.c | 2 +- 7 files changed, 1127 insertions(+), 483 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsSafThread.java (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index b86c71cea49..0330e9b5890 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -305,14 +305,10 @@ file-system. In addition, although Emacs can normally write and create files inside these directories, it cannot create symlinks or hard links. -@c TODO: fix this! Since document providers are allowed to perform expensive network operations to obtain file contents, a file access operation within one -of these directories will possibly take a significant amount of time. -Emacs presently does not support quitting out of such file system -operations, and the timeouts applied are fully subject to the -discretion of the system and the document provider that is responding -to these operations. +of these directories has the potential to take a significant amount of +time. @node Android Environment @section Running Emacs under Android diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index d4d502ede5a..ea200037218 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -257,6 +257,23 @@ public final class EmacsNative public static native void notifyPixelsChanged (Bitmap bitmap); + + /* Functions used to synchronize document provider access with the + main thread. */ + + /* Wait for a call to `safPostRequest' while also reading async + input. + + If asynchronous input arrives and sets Vquit_flag, return 1. */ + public static native int safSyncAndReadInput (); + + /* Wait for a call to `safPostRequest'. */ + public static native void safSync (); + + /* Post the semaphore used to await the completion of SAF + operations. */ + public static native void safPostRequest (); + static { /* Older versions of Android cannot link correctly with shared diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java new file mode 100644 index 00000000000..fd06603fab3 --- /dev/null +++ b/java/org/gnu/emacs/EmacsSafThread.java @@ -0,0 +1,922 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + + Copyright (C) 2023 Free Software Foundation, Inc. + + This file is part of GNU Emacs. + + GNU Emacs is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or (at + your option) any later version. + + GNU Emacs is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import android.content.ContentResolver; +import android.database.Cursor; +import android.net.Uri; + +import android.os.Build; +import android.os.CancellationSignal; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.ParcelFileDescriptor; + +import android.provider.DocumentsContract; +import android.provider.DocumentsContract.Document; + + + +/* Emacs runs long-running SAF operations on a second thread running + its own handler. These operations include opening files and + maintaining the path to document ID cache. + +#if 0 + Because Emacs paths are based on file display names, while Android + document identifiers have no discernible hierarchy of their own, + each file name lookup must carry out a repeated search for + directory documents with the names of all of the file name's + constituent components, where each iteration searches within the + directory document identified by the previous iteration. + + A time limited cache tying components to document IDs is maintained + in order to speed up consecutive searches for file names sharing + the same components. Since listening for changes to each document + in the cache is prohibitively expensive, Emacs instead elects to + periodically remove entries that are older than a predetermined + amount of a time. + + The cache is structured much like the directory trees whose + information it records, with each entry in the cache containing a + list of entries for their children. File name lookup consults the + cache and populates it with missing information simultaneously. + + This is not yet implemented. +#endif + + Long-running operations are also run on this thread for another + reason: Android uses special cancellation objects to terminate + ongoing IPC operations. However, the functions that perform these + operations block instead of providing mechanisms for the caller to + wait for their completion while also reading async input, as a + consequence of which the calling thread is unable to signal the + cancellation objects that it provides. Performing the blocking + operations in this auxiliary thread enables the main thread to wait + for completion itself, signaling the cancellation objects when it + deems necessary. */ + + + +public final class EmacsSafThread extends HandlerThread +{ + /* The content resolver used by this thread. */ + private final ContentResolver resolver; + + /* Handler for this thread's main loop. */ + private Handler handler; + + /* File access mode constants. See `man 7 inode'. */ + public static final int S_IRUSR = 0000400; + public static final int S_IWUSR = 0000200; + public static final int S_IFCHR = 0020000; + public static final int S_IFDIR = 0040000; + public static final int S_IFREG = 0100000; + + public + EmacsSafThread (ContentResolver resolver) + { + super ("Document provider access thread"); + this.resolver = resolver; + } + + + + @Override + public void + start () + { + super.start (); + + /* Set up the handler after the thread starts. */ + handler = new Handler (getLooper ()); + } + + + + /* ``Prototypes'' for nested functions that are run within the SAF + thread and accepts a cancellation signal. They differ in their + return types. */ + + private abstract class SafIntFunction + { + /* The ``throws Throwable'' here is a Java idiosyncracy that tells + the compiler to allow arbitrary error objects to be signaled + from within this function. + + Later, runIntFunction will try to re-throw any error object + generated by this function in the Emacs thread, using a trick + to avoid the compiler requirement to expressly declare that an + error (and which types of errors) will be signaled. */ + + public abstract int runInt (CancellationSignal signal) + throws Throwable; + }; + + private abstract class SafObjectFunction + { + /* The ``throws Throwable'' here is a Java idiosyncracy that tells + the compiler to allow arbitrary error objects to be signaled + from within this function. + + Later, runObjectFunction will try to re-throw any error object + generated by this function in the Emacs thread, using a trick + to avoid the compiler requirement to expressly declare that an + error (and which types of errors) will be signaled. */ + + public abstract Object runObject (CancellationSignal signal) + throws Throwable; + }; + + + + /* Functions that run cancel-able queries. These functions are + internally run within the SAF thread. */ + + /* Throw the specified EXCEPTION. The type template T is erased by + the compiler before the object is compiled, so the compiled code + simply throws EXCEPTION without the cast being verified. + + T should be RuntimeException to obtain the desired effect of + throwing an exception without a compiler check. */ + + @SuppressWarnings("unchecked") + private static void + throwException (Throwable exception) + throws T + { + throw (T) exception; + } + + /* Run the given function (or rather, its `runInt' field) within the + SAF thread, waiting for it to complete. + + If async input arrives in the meantime and sets Vquit_flag, + signal the cancellation signal supplied to that function. + + Rethrow any exception thrown from that function, and return its + value otherwise. */ + + private int + runIntFunction (final SafIntFunction function) + { + final EmacsHolder result; + final CancellationSignal signal; + Throwable throwable; + + result = new EmacsHolder (); + signal = new CancellationSignal (); + + handler.post (new Runnable () { + @Override + public void + run () + { + try + { + result.thing + = Integer.valueOf (function.runInt (signal)); + } + catch (Throwable throwable) + { + result.thing = throwable; + } + + EmacsNative.safPostRequest (); + } + }); + + if (EmacsNative.safSyncAndReadInput () != 0) + { + signal.cancel (); + + /* Now wait for the function to finish. Either the signal has + arrived after the query took place, in which case it will + finish normally, or an OperationCanceledException will be + thrown. */ + + EmacsNative.safSync (); + } + + if (result.thing instanceof Throwable) + { + throwable = (Throwable) result.thing; + EmacsSafThread.throwException (throwable); + } + + return (Integer) result.thing; + } + + /* Run the given function (or rather, its `runObject' field) within + the SAF thread, waiting for it to complete. + + If async input arrives in the meantime and sets Vquit_flag, + signal the cancellation signal supplied to that function. + + Rethrow any exception thrown from that function, and return its + value otherwise. */ + + private Object + runObjectFunction (final SafObjectFunction function) + { + final EmacsHolder result; + final CancellationSignal signal; + Throwable throwable; + + result = new EmacsHolder (); + signal = new CancellationSignal (); + + handler.post (new Runnable () { + @Override + public void + run () + { + try + { + result.thing = function.runObject (signal); + } + catch (Throwable throwable) + { + result.thing = throwable; + } + + EmacsNative.safPostRequest (); + } + }); + + if (EmacsNative.safSyncAndReadInput () != 0) + { + signal.cancel (); + + /* Now wait for the function to finish. Either the signal has + arrived after the query took place, in which case it will + finish normally, or an OperationCanceledException will be + thrown. */ + + EmacsNative.safSync (); + } + + if (result.thing instanceof Throwable) + { + throwable = (Throwable) result.thing; + EmacsSafThread.throwException (throwable); + } + + return result.thing; + } + + /* The crux of `documentIdFromName1', run within the SAF thread. + SIGNAL should be a cancellation signal run upon quitting. */ + + private int + documentIdFromName1 (String tree_uri, String name, + String[] id_return, CancellationSignal signal) + { + Uri uri, treeUri; + String id, type; + String[] components, projection; + Cursor cursor; + int column; + + projection = new String[] { + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_DOCUMENT_ID, + Document.COLUMN_MIME_TYPE, + }; + + /* Parse the URI identifying the tree first. */ + uri = Uri.parse (tree_uri); + + /* Now, split NAME into its individual components. */ + components = name.split ("/"); + + /* Set id and type to the value at the root of the tree. */ + type = id = null; + cursor = null; + + /* For each component... */ + + try + { + for (String component : components) + { + /* Java split doesn't behave very much like strtok when it + comes to trailing and leading delimiters... */ + if (component.isEmpty ()) + continue; + + /* Create the tree URI for URI from ID if it exists, or + the root otherwise. */ + + if (id == null) + id = DocumentsContract.getTreeDocumentId (uri); + + treeUri + = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id); + + /* Look for a file in this directory by the name of + component. */ + + cursor = resolver.query (treeUri, projection, + (Document.COLUMN_DISPLAY_NAME + + " = ?s"), + new String[] { component, }, + null, signal); + + if (cursor == null) + return -1; + + while (true) + { + /* Even though the query selects for a specific + display name, some content providers nevertheless + return every file within the directory. */ + + if (!cursor.moveToNext ()) + { + /* If the last component considered is a + directory... */ + if ((type == null + || type.equals (Document.MIME_TYPE_DIR)) + /* ... and type and id currently represent the + penultimate component. */ + && component == components[components.length - 1]) + { + /* The cursor is empty. In this case, return + -2 and the current document ID (belonging + to the previous component) in + ID_RETURN. */ + + id_return[0] = id; + + /* But return -1 on the off chance that id is + null. */ + + if (id == null) + return -1; + + return -2; + } + + /* The last component found is not a directory, so + return -1. */ + return -1; + } + + /* So move CURSOR to a row with the right display + name. */ + + column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); + + if (column < 0) + continue; + + name = cursor.getString (column); + + /* Break out of the loop only once a matching + component is found. */ + + if (name.equals (component)) + break; + } + + /* Look for a column by the name of + COLUMN_DOCUMENT_ID. */ + + column = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); + + if (column < 0) + return -1; + + /* Now replace ID with the document ID. */ + + id = cursor.getString (column); + + /* If this is the last component, be sure to initialize + the document type. */ + + if (component == components[components.length - 1]) + { + column + = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + + if (column < 0) + return -1; + + type = cursor.getString (column); + + /* Type may be NULL depending on how the Cursor + returned is implemented. */ + + if (type == null) + return -1; + } + + /* Now close the cursor. */ + cursor.close (); + cursor = null; + + /* ID may have become NULL if the data is in an invalid + format. */ + if (id == null) + return -1; + } + } + finally + { + /* If an error is thrown within the block above, let + android_saf_exception_check handle it, but make sure the + cursor is closed. */ + + if (cursor != null) + cursor.close (); + } + + /* Here, id is either NULL (meaning the same as TREE_URI), and + type is either NULL (in which case id should also be NULL) or + the MIME type of the file. */ + + /* First return the ID. */ + + if (id == null) + id_return[0] = DocumentsContract.getTreeDocumentId (uri); + else + id_return[0] = id; + + /* Next, return whether or not this is a directory. */ + if (type == null || type.equals (Document.MIME_TYPE_DIR)) + return 1; + + return 0; + } + + /* Find the document ID of the file within TREE_URI designated by + NAME. + + NAME is a ``file name'' comprised of the display names of + individual files. Each constituent component prior to the last + must name a directory file within TREE_URI. + + Upon success, return 0 or 1 (contingent upon whether or not the + last component within NAME is a directory) and place the document + ID of the named file in ID_RETURN[0]. + + If the designated file can't be located, but each component of + NAME up to the last component can and is a directory, return -2 + and the ID of the last component located in ID_RETURN[0]. + + If the designated file can't be located, return -1, or signal one + of OperationCanceledException, SecurityException, + FileNotFoundException, or UnsupportedOperationException. */ + + public int + documentIdFromName (final String tree_uri, final String name, + final String[] id_return) + { + return runIntFunction (new SafIntFunction () { + @Override + public int + runInt (CancellationSignal signal) + { + return documentIdFromName1 (tree_uri, name, id_return, + signal); + } + }); + } + + /* The bulk of `statDocument'. SIGNAL should be a cancelation + signal. */ + + private long[] + statDocument1 (String uri, String documentId, + CancellationSignal signal) + { + Uri uriObject; + String[] projection; + long[] stat; + int index; + long tem; + String tem1; + Cursor cursor; + + uriObject = Uri.parse (uri); + + if (documentId == null) + documentId = DocumentsContract.getTreeDocumentId (uriObject); + + /* Create a document URI representing DOCUMENTID within URI's + authority. */ + + uriObject + = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); + + /* Now stat this document. */ + + projection = new String[] { + Document.COLUMN_FLAGS, + Document.COLUMN_LAST_MODIFIED, + Document.COLUMN_MIME_TYPE, + Document.COLUMN_SIZE, + }; + + cursor = resolver.query (uriObject, projection, null, + null, null, signal); + + if (cursor == null) + return null; + + if (!cursor.moveToFirst ()) + { + cursor.close (); + return null; + } + + /* Create the array of file status. */ + stat = new long[3]; + + try + { + index = cursor.getColumnIndex (Document.COLUMN_FLAGS); + if (index < 0) + return null; + + tem = cursor.getInt (index); + + stat[0] |= S_IRUSR; + if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0) + stat[0] |= S_IWUSR; + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N + && (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0) + stat[0] |= S_IFCHR; + + index = cursor.getColumnIndex (Document.COLUMN_SIZE); + if (index < 0) + return null; + + if (cursor.isNull (index)) + stat[1] = -1; /* The size is unknown. */ + else + stat[1] = cursor.getLong (index); + + index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + if (index < 0) + return null; + + tem1 = cursor.getString (index); + + /* Check if this is a directory file. */ + if (tem1.equals (Document.MIME_TYPE_DIR) + /* Files shouldn't be specials and directories at the same + time, but Android doesn't forbid document providers + from returning this information. */ + && (stat[0] & S_IFCHR) == 0) + /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, + just assume they're writable. */ + stat[0] |= S_IFDIR | S_IWUSR; + + /* If this file is neither a character special nor a + directory, indicate that it's a regular file. */ + + if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0) + stat[0] |= S_IFREG; + + index = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); + + if (index >= 0 && !cursor.isNull (index)) + { + /* Content providers are allowed to not provide mtime. */ + tem = cursor.getLong (index); + stat[2] = tem; + } + } + finally + { + cursor.close (); + } + + return stat; + } + + /* Return file status for the document designated by the given + DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document + ID in URI itself. + + Value is null upon failure, or an array of longs [MODE, SIZE, + MTIM] upon success, where MODE contains the file type and access + modes of the file as in `struct stat', SIZE is the size of the + file in BYTES or -1 if not known, and MTIM is the time of the + last modification to this file in milliseconds since 00:00, + January 1st, 1970. + + OperationCanceledException and other typical exceptions may be + signaled upon receiving async input or other errors. */ + + public long[] + statDocument (final String uri, final String documentId) + { + return (long[]) runObjectFunction (new SafObjectFunction () { + @Override + public Object + runObject (CancellationSignal signal) + { + return statDocument1 (uri, documentId, signal); + } + }); + } + + /* The bulk of `accessDocument'. SIGNAL should be a cancellation + signal. */ + + private int + accessDocument1 (String uri, String documentId, boolean writable, + CancellationSignal signal) + { + Uri uriObject; + String[] projection; + int tem, index; + String tem1; + Cursor cursor; + + uriObject = Uri.parse (uri); + + if (documentId == null) + documentId = DocumentsContract.getTreeDocumentId (uriObject); + + /* Create a document URI representing DOCUMENTID within URI's + authority. */ + + uriObject + = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); + + /* Now stat this document. */ + + projection = new String[] { + Document.COLUMN_FLAGS, + Document.COLUMN_MIME_TYPE, + }; + + cursor = resolver.query (uriObject, projection, null, + null, null, signal); + + if (cursor == null) + return -1; + + try + { + if (!cursor.moveToFirst ()) + return -1; + + if (!writable) + return 0; + + index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); + if (index < 0) + return -3; + + /* Get the type of this file to check if it's a directory. */ + tem1 = cursor.getString (index); + + /* Check if this is a directory file. */ + if (tem1.equals (Document.MIME_TYPE_DIR)) + { + /* If so, don't check for FLAG_SUPPORTS_WRITE. + Check for FLAG_DIR_SUPPORTS_CREATE instead. */ + + if (!writable) + return 0; + + index = cursor.getColumnIndex (Document.COLUMN_FLAGS); + if (index < 0) + return -3; + + tem = cursor.getInt (index); + if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) + return -3; + + return 0; + } + + index = cursor.getColumnIndex (Document.COLUMN_FLAGS); + if (index < 0) + return -3; + + tem = cursor.getInt (index); + if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0) + return -3; + } + finally + { + /* Close the cursor if an exception occurs. */ + cursor.close (); + } + + return 0; + } + + /* Find out whether Emacs has access to the document designated by + the specified DOCUMENTID within the tree URI. If DOCUMENTID is + NULL, use the document ID in URI itself. + + If WRITABLE, also check that the file is writable, which is true + if it is either a directory or its flags contains + FLAG_SUPPORTS_WRITE. + + Value is 0 if the file is accessible, and one of the following if + not: + + -1, if the file does not exist. + -2, if WRITABLE and the file is not writable. + -3, upon any other error. + + In addition, arbitrary runtime exceptions (such as + SecurityException or UnsupportedOperationException) may be + thrown. */ + + public int + accessDocument (final String uri, final String documentId, + final boolean writable) + { + return runIntFunction (new SafIntFunction () { + @Override + public int + runInt (CancellationSignal signal) + { + return accessDocument1 (uri, documentId, writable, + signal); + } + }); + } + + /* The crux of openDocumentDirectory. SIGNAL must be a cancellation + signal. */ + + private Cursor + openDocumentDirectory1 (String uri, String documentId, + CancellationSignal signal) + { + Uri uriObject; + Cursor cursor; + String projection[]; + + uriObject = Uri.parse (uri); + + /* If documentId is not set, use the document ID of the tree URI + itself. */ + + if (documentId == null) + documentId = DocumentsContract.getTreeDocumentId (uriObject); + + /* Build a URI representing each directory entry within + DOCUMENTID. */ + + uriObject + = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject, + documentId); + + projection = new String [] { + Document.COLUMN_DISPLAY_NAME, + Document.COLUMN_MIME_TYPE, + }; + + cursor = resolver.query (uriObject, projection, null, null, + null, signal); + /* Return the cursor. */ + return cursor; + } + + /* Open a cursor representing each entry within the directory + designated by the specified DOCUMENTID within the tree URI. + + If DOCUMENTID is NULL, use the document ID within URI itself. + Value is NULL upon failure. + + In addition, arbitrary runtime exceptions (such as + SecurityException or UnsupportedOperationException) may be + thrown. */ + + public Cursor + openDocumentDirectory (final String uri, final String documentId) + { + return (Cursor) runObjectFunction (new SafObjectFunction () { + @Override + public Object + runObject (CancellationSignal signal) + { + return openDocumentDirectory1 (uri, documentId, signal); + } + }); + } + + /* The crux of `openDocument'. SIGNAL must be a cancellation + signal. */ + + public ParcelFileDescriptor + openDocument1 (String uri, String documentId, boolean write, + boolean truncate, CancellationSignal signal) + throws Throwable + { + Uri treeUri, documentUri; + String mode; + ParcelFileDescriptor fileDescriptor; + + treeUri = Uri.parse (uri); + + /* documentId must be set for this request, since it doesn't make + sense to ``open'' the root of the directory tree. */ + + documentUri + = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId); + + if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) + { + /* Select the mode used to open the file. `rw' means open + a stat-able file, while `rwt' means that and to + truncate the file as well. */ + + if (truncate) + mode = "rwt"; + else + mode = "rw"; + + fileDescriptor + = resolver.openFileDescriptor (documentUri, mode, + signal); + } + else + { + /* Select the mode used to open the file. `openFile' + below means always open a stat-able file. */ + + if (truncate) + /* Invalid mode! */ + return null; + else + mode = "r"; + + fileDescriptor = resolver.openFile (documentUri, mode, + signal); + } + + return fileDescriptor; + } + + /* Open a file descriptor for a file document designated by + DOCUMENTID within the document tree identified by URI. If + TRUNCATE and the document already exists, truncate its contents + before returning. + + On Android 9.0 and earlier, always open the document in + ``read-write'' mode; this instructs the document provider to + return a seekable file that is stored on disk and returns correct + file status. + + Under newer versions of Android, open the document in a + non-writable mode if WRITE is false. This is possible because + these versions allow Emacs to explicitly request a seekable + on-disk file. + + Value is NULL upon failure or a parcel file descriptor upon + success. Call `ParcelFileDescriptor.close' on this file + descriptor instead of using the `close' system call. + + FileNotFoundException and/or SecurityException and/or + UnsupportedOperationException and/or OperationCanceledException + may be thrown upon failure. */ + + public ParcelFileDescriptor + openDocument (final String uri, final String documentId, + final boolean write, final boolean truncate) + { + Object tem; + + tem = runObjectFunction (new SafObjectFunction () { + @Override + public Object + runObject (CancellationSignal signal) + throws Throwable + { + return openDocument1 (uri, documentId, write, truncate, + signal); + } + }); + + return (ParcelFileDescriptor) tem; + } +}; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index aa672994f12..e410754071b 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -109,13 +109,6 @@ public final class EmacsService extends Service public static final int IC_MODE_ACTION = 1; public static final int IC_MODE_TEXT = 2; - /* File access mode constants. See `man 7 inode'. */ - public static final int S_IRUSR = 0000400; - public static final int S_IWUSR = 0000200; - public static final int S_IFCHR = 0020000; - public static final int S_IFDIR = 0040000; - public static final int S_IFREG = 0100000; - /* Display metrics used by font backends. */ public DisplayMetrics metrics; @@ -134,6 +127,10 @@ public final class EmacsService extends Service being called, and 2 if icBeginSynchronous was called. */ public static final AtomicInteger servicingQuery; + /* Thread used to query document providers, or null if it hasn't + been created yet. */ + private EmacsSafThread storageThread; + static { servicingQuery = new AtomicInteger (); @@ -1160,10 +1157,7 @@ public final class EmacsService extends Service /* Document tree management functions. These functions shouldn't be - called before Android 5.0. - - TODO: a timeout, let alone quitting, has yet to be implemented - for any of these functions. */ + called before Android 5.0. */ /* Return an array of each document authority providing at least one tree URI that Emacs holds the rights to persistently access. */ @@ -1319,223 +1313,26 @@ public final class EmacsService extends Service If the designated file can't be located, but each component of NAME up to the last component can and is a directory, return -2 - and the ID of the last component located in ID_RETURN[0]; + and the ID of the last component located in ID_RETURN[0]. - If the designated file can't be located, return -1. */ + If the designated file can't be located, return -1, or signal one + of OperationCanceledException, SecurityException, + FileNotFoundException, or UnsupportedOperationException. */ private int documentIdFromName (String tree_uri, String name, String[] id_return) { - Uri uri, treeUri; - String id, type; - String[] components, projection; - Cursor cursor; - int column; - - projection = new String[] { - Document.COLUMN_DISPLAY_NAME, - Document.COLUMN_DOCUMENT_ID, - Document.COLUMN_MIME_TYPE, - }; - - /* Parse the URI identifying the tree first. */ - uri = Uri.parse (tree_uri); - - /* Now, split NAME into its individual components. */ - components = name.split ("/"); + /* Start the thread used to run SAF requests if it isn't already + running. */ - /* Set id and type to the value at the root of the tree. */ - type = id = null; - - /* For each component... */ - - for (String component : components) + if (storageThread == null) { - /* Java split doesn't behave very much like strtok when it - comes to trailing and leading delimiters... */ - if (component.isEmpty ()) - continue; - - /* Create the tree URI for URI from ID if it exists, or the - root otherwise. */ - - if (id == null) - id = DocumentsContract.getTreeDocumentId (uri); - - treeUri - = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id); - - /* Look for a file in this directory by the name of - component. */ - - try - { - cursor = resolver.query (treeUri, projection, - (Document.COLUMN_DISPLAY_NAME - + " = ?s"), - new String[] { component, }, null); - } - catch (SecurityException exception) - { - /* A SecurityException can be thrown if Emacs doesn't have - access to treeUri. */ - return -1; - } - catch (Exception exception) - { - exception.printStackTrace (); - - /* Why is this? */ - return -1; - } - - if (cursor == null) - return -1; - - while (true) - { - /* Even though the query selects for a specific display - name, some content providers nevertheless return every - file within the directory. */ - - if (!cursor.moveToNext ()) - { - cursor.close (); - - /* If the last component considered is a - directory... */ - if ((type == null - || type.equals (Document.MIME_TYPE_DIR)) - /* ... and type and id currently represent the - penultimate component. */ - && component == components[components.length - 1]) - { - /* The cursor is empty. In this case, return -2 - and the current document ID (belonging to the - previous component) in ID_RETURN. */ - - id_return[0] = id; - - /* But return -1 on the off chance that id is - null. */ - - if (id == null) - return -1; - - return -2; - } - - /* The last component found is not a directory, so - return -1. */ - return -1; - } - - /* So move CURSOR to a row with the right display - name. */ - - column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); - - if (column < 0) - continue; - - try - { - name = cursor.getString (column); - } - catch (Exception exception) - { - cursor.close (); - return -1; - } - - /* Break out of the loop only once a matching component is - found. */ - - if (name.equals (component)) - break; - } - - /* Look for a column by the name of COLUMN_DOCUMENT_ID. */ - - column = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); - - if (column < 0) - { - cursor.close (); - return -1; - } - - /* Now replace ID with the document ID. */ - - try - { - id = cursor.getString (column); - } - catch (Exception exception) - { - cursor.close (); - return -1; - } - - /* If this is the last component, be sure to initialize the - document type. */ - - if (component == components[components.length - 1]) - { - column - = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); - - if (column < 0) - { - cursor.close (); - return -1; - } - - try - { - type = cursor.getString (column); - } - catch (Exception exception) - { - cursor.close (); - return -1; - } - - /* Type may be NULL depending on how the Cursor returned - is implemented. */ - - if (type == null) - { - cursor.close (); - return -1; - } - } - - /* Now close the cursor. */ - cursor.close (); - - /* ID may have become NULL if the data is in an invalid - format. */ - if (id == null) - return -1; + storageThread = new EmacsSafThread (resolver); + storageThread.start (); } - /* Here, id is either NULL (meaning the same as TREE_URI), and - type is either NULL (in which case id should also be NULL) or - the MIME type of the file. */ - - /* First return the ID. */ - - if (id == null) - id_return[0] = DocumentsContract.getTreeDocumentId (uri); - else - id_return[0] = id; - - /* Next, return whether or not this is a directory. */ - if (type == null || type.equals (Document.MIME_TYPE_DIR)) - return 1; - - return 0; + return storageThread.documentIdFromName (tree_uri, name, + id_return); } /* Return an encoded document URI representing a tree with the @@ -1585,130 +1382,24 @@ public final class EmacsService extends Service modes of the file as in `struct stat', SIZE is the size of the file in BYTES or -1 if not known, and MTIM is the time of the last modification to this file in milliseconds since 00:00, - January 1st, 1970. */ + January 1st, 1970. + + OperationCanceledException and other typical exceptions may be + signaled upon receiving async input or other errors. */ public long[] statDocument (String uri, String documentId) { - Uri uriObject; - String[] projection; - long[] stat; - int index; - long tem; - String tem1; - Cursor cursor; - - uriObject = Uri.parse (uri); - - if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (uriObject); - - /* Create a document URI representing DOCUMENTID within URI's - authority. */ - - uriObject - = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); - - /* Now stat this document. */ - - projection = new String[] { - Document.COLUMN_FLAGS, - Document.COLUMN_LAST_MODIFIED, - Document.COLUMN_MIME_TYPE, - Document.COLUMN_SIZE, - }; - - try - { - cursor = resolver.query (uriObject, projection, null, - null, null); - } - catch (SecurityException exception) - { - /* A SecurityException can be thrown if Emacs doesn't have - access to uriObject. */ - return null; - } - catch (UnsupportedOperationException exception) - { - exception.printStackTrace (); - - /* Why is this? */ - return null; - } - - if (cursor == null || !cursor.moveToFirst ()) - return null; - - /* Create the array of file status. */ - stat = new long[3]; + /* Start the thread used to run SAF requests if it isn't already + running. */ - try + if (storageThread == null) { - index = cursor.getColumnIndex (Document.COLUMN_FLAGS); - if (index < 0) - return null; - - tem = cursor.getInt (index); - - stat[0] |= S_IRUSR; - if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0) - stat[0] |= S_IWUSR; - - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N - && (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0) - stat[0] |= S_IFCHR; - - index = cursor.getColumnIndex (Document.COLUMN_SIZE); - if (index < 0) - return null; - - if (cursor.isNull (index)) - stat[1] = -1; /* The size is unknown. */ - else - stat[1] = cursor.getLong (index); - - index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); - if (index < 0) - return null; - - tem1 = cursor.getString (index); - - /* Check if this is a directory file. */ - if (tem1.equals (Document.MIME_TYPE_DIR) - /* Files shouldn't be specials and directories at the same - time, but Android doesn't forbid document providers - from returning this information. */ - && (stat[0] & S_IFCHR) == 0) - /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, - just assume they're writable. */ - stat[0] |= S_IFDIR | S_IWUSR; - - /* If this file is neither a character special nor a - directory, indicate that it's a regular file. */ - - if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0) - stat[0] |= S_IFREG; - - index = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); - - if (index >= 0 && !cursor.isNull (index)) - { - /* Content providers are allowed to not provide mtime. */ - tem = cursor.getLong (index); - stat[2] = tem; - } - } - catch (Exception exception) - { - /* Whether or not type errors cause exceptions to be signaled - is defined ``by the implementation of Cursor'', whatever - that means. */ - exception.printStackTrace (); - return null; + storageThread = new EmacsSafThread (resolver); + storageThread.start (); } - return stat; + return storageThread.statDocument (uri, documentId); } /* Find out whether Emacs has access to the document designated by @@ -1733,83 +1424,16 @@ public final class EmacsService extends Service public int accessDocument (String uri, String documentId, boolean writable) { - Uri uriObject; - String[] projection; - int tem, index; - String tem1; - Cursor cursor; - - uriObject = Uri.parse (uri); - - if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (uriObject); - - /* Create a document URI representing DOCUMENTID within URI's - authority. */ - - uriObject - = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); - - /* Now stat this document. */ + /* Start the thread used to run SAF requests if it isn't already + running. */ - projection = new String[] { - Document.COLUMN_FLAGS, - Document.COLUMN_MIME_TYPE, - }; - - cursor = resolver.query (uriObject, projection, null, - null, null); - - if (cursor == null || !cursor.moveToFirst ()) - return -1; - - if (!writable) - return 0; - - try + if (storageThread == null) { - index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); - if (index < 0) - return -3; - - /* Get the type of this file to check if it's a directory. */ - tem1 = cursor.getString (index); - - /* Check if this is a directory file. */ - if (tem1.equals (Document.MIME_TYPE_DIR)) - { - /* If so, don't check for FLAG_SUPPORTS_WRITE. - Check for FLAG_DIR_SUPPORTS_CREATE instead. */ - - if (!writable) - return 0; - - index = cursor.getColumnIndex (Document.COLUMN_FLAGS); - if (index < 0) - return -3; - - tem = cursor.getInt (index); - if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) - return -3; - - return 0; - } - - index = cursor.getColumnIndex (Document.COLUMN_FLAGS); - if (index < 0) - return -3; - - tem = cursor.getInt (index); - if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0) - return -3; - } - finally - { - /* Close the cursor if an exception occurs. */ - cursor.close (); + storageThread = new EmacsSafThread (resolver); + storageThread.start (); } - return 0; + return storageThread.accessDocument (uri, documentId, writable); } /* Open a cursor representing each entry within the directory @@ -1825,34 +1449,16 @@ public final class EmacsService extends Service public Cursor openDocumentDirectory (String uri, String documentId) { - Uri uriObject; - Cursor cursor; - String projection[]; - - uriObject = Uri.parse (uri); - - /* If documentId is not set, use the document ID of the tree URI - itself. */ - - if (documentId == null) - documentId = DocumentsContract.getTreeDocumentId (uriObject); - - /* Build a URI representing each directory entry within - DOCUMENTID. */ - - uriObject - = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject, - documentId); + /* Start the thread used to run SAF requests if it isn't already + running. */ - projection = new String [] { - Document.COLUMN_DISPLAY_NAME, - Document.COLUMN_MIME_TYPE, - }; + if (storageThread == null) + { + storageThread = new EmacsSafThread (resolver); + storageThread.start (); + } - cursor = resolver.query (uriObject, projection, null, null, - null); - /* Return the cursor. */ - return cursor; + return storageThread.openDocumentDirectory (uri, documentId); } /* Read a single directory entry from the specified CURSOR. Return @@ -1945,50 +1551,18 @@ public final class EmacsService extends Service public ParcelFileDescriptor openDocument (String uri, String documentId, boolean write, boolean truncate) - throws FileNotFoundException { - Uri treeUri, documentUri; - String mode; - ParcelFileDescriptor fileDescriptor; - - treeUri = Uri.parse (uri); - - /* documentId must be set for this request, since it doesn't make - sense to ``open'' the root of the directory tree. */ + /* Start the thread used to run SAF requests if it isn't already + running. */ - documentUri - = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId); - - if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) + if (storageThread == null) { - /* Select the mode used to open the file. `rw' means open - a stat-able file, while `rwt' means that and to - truncate the file as well. */ - - if (truncate) - mode = "rwt"; - else - mode = "rw"; - - fileDescriptor - = resolver.openFileDescriptor (documentUri, mode, - null); - } - else - { - /* Select the mode used to open the file. `openFile' - below means always open a stat-able file. */ - - if (truncate) - /* Invalid mode! */ - return null; - else - mode = "r"; - - fileDescriptor = resolver.openFile (documentUri, mode, null); + storageThread = new EmacsSafThread (resolver); + storageThread.start (); } - return fileDescriptor; + return storageThread.openDocument (uri, documentId, write, + truncate); } /* Create a new document with the given display NAME within the diff --git a/src/androidvfs.c b/src/androidvfs.c index 2cd50963e97..4f485622ff4 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -26,6 +26,7 @@ along with GNU Emacs. If not, see . */ #include #include #include +#include #include #include @@ -34,6 +35,7 @@ along with GNU Emacs. If not, see . */ #include "android.h" #include "systime.h" +#include "blockinput.h" #if __ANDROID_API__ >= 9 #include @@ -278,6 +280,7 @@ static struct android_parcel_file_descriptor_class fd_class; /* Global references to several exception classes. */ static jclass file_not_found_exception, security_exception; +static jclass operation_canceled_exception; static jclass unsupported_operation_exception, out_of_memory_error; /* Initialize `cursor_class' using the given JNI environment ENV. @@ -3692,6 +3695,10 @@ android_saf_root_get_directory (int dirfd) /* Functions common to both SAF directory and file nodes. */ +/* Whether or not Emacs is within an operation running from the SAF + thread. */ +static bool inside_saf_critical_section; + /* Check for JNI exceptions, clear them, and set errno accordingly. Also, free each of the N local references given as arguments if an exception takes place. @@ -3704,6 +3711,9 @@ android_saf_root_get_directory (int dirfd) If the exception thrown derives from SecurityException, set errno to EACCES. + If the exception thrown derives from OperationCanceledException, + set errno to EINTR. + If the exception thrown derives from UnsupportedOperationException, set errno to ENOSYS. @@ -3754,6 +3764,9 @@ android_saf_exception_check (int n, ...) else if ((*env)->IsInstanceOf (env, (jobject) exception, security_exception)) errno = EACCES; + else if ((*env)->IsInstanceOf (env, (jobject) exception, + operation_canceled_exception)) + errno = EINTR; else if ((*env)->IsInstanceOf (env, (jobject) exception, unsupported_operation_exception)) errno = ENOSYS; @@ -3786,6 +3799,15 @@ android_saf_stat (const char *uri_name, const char *id_name, jobject status; jlong mode, size, mtim, *longs; + /* Now guarantee that it is safe to call functions which + synchronize with the SAF thread. */ + + if (inside_saf_critical_section) + { + errno = EIO; + return -1; + } + /* Build strings for both URI and ID. */ uri = (*android_java_env)->NewStringUTF (android_java_env, uri_name); android_exception_check (); @@ -3801,10 +3823,12 @@ android_saf_stat (const char *uri_name, const char *id_name, /* Try to retrieve the file status. */ method = service_class.stat_document; + inside_saf_critical_section = true; status = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, emacs_service, service_class.class, method, uri, id); + inside_saf_critical_section = false; /* Check for exceptions and release unneeded local references. */ @@ -3870,6 +3894,15 @@ android_saf_access (const char *uri_name, const char *id_name, jstring uri, id; jint rc; + /* Now guarantee that it is safe to call functions which + synchronize with the SAF thread. */ + + if (inside_saf_critical_section) + { + errno = EIO; + return -1; + } + /* Build strings for both URI and ID. */ uri = (*android_java_env)->NewStringUTF (android_java_env, uri_name); android_exception_check (); @@ -3885,11 +3918,13 @@ android_saf_access (const char *uri_name, const char *id_name, /* Try to retrieve the file status. */ method = service_class.access_document; + inside_saf_critical_section = true; rc = (*android_java_env)->CallNonvirtualIntMethod (android_java_env, emacs_service, service_class.class, method, uri, id, (jboolean) writable); + inside_saf_critical_section = false; /* Check for exceptions and release unneeded local references. */ @@ -4161,7 +4196,19 @@ android_document_id_from_name (const char *tree_uri, char *name, contain characters that can't be encoded in Java. */ if (android_verify_jni_string (name)) - return -1; + { + errno = ENOENT; + return -1; + } + + /* Now guarantee that it is safe to call + `document_id_from_name'. */ + + if (inside_saf_critical_section) + { + errno = EIO; + return -1; + } /* First, create the array that will hold the result. */ result = (*android_java_env)->NewObjectArray (android_java_env, 1, @@ -4176,14 +4223,17 @@ android_document_id_from_name (const char *tree_uri, char *name, uri = (*android_java_env)->NewStringUTF (android_java_env, tree_uri); android_exception_check_2 (result, java_name); - /* Now, call documentIdFromName. */ + /* Now, call documentIdFromName. This will synchronize with the SAF + thread, so make sure reentrant calls don't happen. */ method = service_class.document_id_from_name; + inside_saf_critical_section = true; rc = (*android_java_env)->CallNonvirtualIntMethod (android_java_env, emacs_service, service_class.class, method, uri, java_name, result); + inside_saf_critical_section = false; if (android_saf_exception_check (3, result, uri, java_name)) goto finish; @@ -4562,6 +4612,12 @@ android_saf_tree_opendir_1 (struct android_saf_tree_vnode *vp) jobject uri, id, cursor; jmethodID method; + if (inside_saf_critical_section) + { + errno = EIO; + return NULL; + } + /* Build strings for both URI and ID. */ uri = (*android_java_env)->NewStringUTF (android_java_env, vp->tree_uri); @@ -4578,11 +4634,13 @@ android_saf_tree_opendir_1 (struct android_saf_tree_vnode *vp) /* Try to open the cursor. */ method = service_class.open_document_directory; + inside_saf_critical_section = true; cursor = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, emacs_service, service_class.class, method, uri, id); + inside_saf_critical_section = false; if (id) { @@ -5001,6 +5059,12 @@ android_saf_file_open (struct android_vnode *vnode, int flags, struct android_parcel_fd *info; struct stat statb; + if (inside_saf_critical_section) + { + errno = EIO; + return -1; + } + /* Build strings for both the URI and ID. */ vp = (struct android_saf_file_vnode *) vnode; @@ -5016,12 +5080,14 @@ android_saf_file_open (struct android_vnode *vnode, int flags, method = service_class.open_document; trunc = flags & O_TRUNC; write = ((flags & O_RDWR) == O_RDWR || (flags & O_WRONLY)); + inside_saf_critical_section = true; descriptor = (*android_java_env)->CallNonvirtualObjectMethod (android_java_env, emacs_service, service_class.class, method, uri, id, write, trunc); + inside_saf_critical_section = false; if (android_saf_exception_check (2, uri, id)) return -1; @@ -5468,6 +5534,48 @@ android_saf_new_opendir (struct android_vnode *vnode) +/* Synchronization between SAF and Emacs. Consult EmacsSafThread.java + for more details. */ + +/* Semaphore posted upon the completion of an SAF operation. */ +static sem_t saf_completion_sem; + +JNIEXPORT jint JNICALL +NATIVE_NAME (safSyncAndReadInput) (JNIEnv *env, jobject object) +{ + while (sem_wait (&saf_completion_sem) < 0) + { + if (input_blocked_p ()) + continue; + + process_pending_signals (); + + if (!NILP (Vquit_flag)) + { + __android_log_print (ANDROID_LOG_VERBOSE, __func__, + "quitting from IO operation"); + return 1; + } + } + + return 0; +} + +JNIEXPORT void JNICALL +NATIVE_NAME (safSync) (JNIEnv *env, jobject object) +{ + while (sem_wait (&saf_completion_sem) < 0) + process_pending_signals (); +} + +JNIEXPORT void JNICALL +NATIVE_NAME (safPostRequest) (JNIEnv *env, jobject object) +{ + sem_post (&saf_completion_sem); +} + + + /* Root vnode. This vnode represents the root inode, and is a regular Unix vnode with modifications to `name' that make it return asset vnodes. */ @@ -5692,6 +5800,11 @@ android_vfs_init (JNIEnv *env, jobject manager) (*env)->DeleteLocalRef (env, old); eassert (security_exception); + old = (*env)->FindClass (env, "android/os/OperationCanceledException"); + operation_canceled_exception = (*env)->NewGlobalRef (env, old); + (*env)->DeleteLocalRef (env, old); + eassert (operation_canceled_exception); + old = (*env)->FindClass (env, "java/lang/UnsupportedOperationException"); unsupported_operation_exception = (*env)->NewGlobalRef (env, old); (*env)->DeleteLocalRef (env, old); @@ -5701,6 +5814,12 @@ android_vfs_init (JNIEnv *env, jobject manager) out_of_memory_error = (*env)->NewGlobalRef (env, old); (*env)->DeleteLocalRef (env, old); eassert (out_of_memory_error); + + /* Initialize the semaphore used to wait for SAF operations to + complete. */ + + if (sem_init (&saf_completion_sem, 0, 0) < 0) + emacs_abort (); } /* The replacement functions that follow have several major @@ -5754,6 +5873,12 @@ android_vfs_init (JNIEnv *env, jobject manager) The sixth is that flags and other argument checking is nowhere near exhaustive on vnode types other than Unix vnodes. + The seventh is that certain vnode types may read async input and + return EINTR not upon the arrival of a signal itself, but instead + if subsequently read input causes Vquit_flag to be set. These + vnodes may not be reentrant, but operating on them from within an + async input handler will at worst cause an error to be returned. + And the final drawback is that directories cannot be directly opened. Instead, `dirfd' must be called on a directory stream used by `openat'. @@ -6409,7 +6534,8 @@ android_asset_fstat (struct android_fd_or_asset asset, /* Directory listing emulation. */ /* Open a directory stream from the VFS node designated by NAME. - Value is NULL upon failure with errno set accordingly. + Value is NULL upon failure with errno set accordingly. `errno' may + be set to EINTR. The directory stream returned holds local references to JNI objects and shouldn't be used after the current local reference frame is diff --git a/src/dired.c b/src/dired.c index f2a123dc168..c10531cdb16 100644 --- a/src/dired.c +++ b/src/dired.c @@ -115,10 +115,19 @@ open_directory (Lisp_Object dirname, Lisp_Object encoded_dirname, int *fdp) #ifndef HAVE_ANDROID d = opendir (name); #else + /* `android_opendir' can return EINTR if DIRNAME designates a file + within a slow-to-respond document provider. */ + + again: d = android_opendir (name); if (d) fd = android_dirfd (d); + else if (errno == EINTR) + { + maybe_quit (); + goto again; + } #endif opendir_errno = errno; #else diff --git a/src/sysdep.c b/src/sysdep.c index 88938d15b91..0a1905c9196 100644 --- a/src/sysdep.c +++ b/src/sysdep.c @@ -2656,7 +2656,7 @@ emacs_fclose (FILE *stream) /* Wrappers around unlink, symlink, rename, renameat_noreplace, and rmdir. These operations handle asset and content directories on - Android. */ + Android, and may return EINTR. */ int emacs_unlink (const char *name) -- cgit v1.3 From 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 +- src/androidvfs.c | 27 +- 3 files changed, 544 insertions(+), 81 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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; } diff --git a/src/androidvfs.c b/src/androidvfs.c index 7f61a4b4fa3..babc7101d88 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -3728,6 +3728,7 @@ android_saf_exception_check (int n, ...) jthrowable exception; JNIEnv *env; va_list ap; + int new_errno; env = android_java_env; va_start (ap, n); @@ -3744,8 +3745,9 @@ android_saf_exception_check (int n, ...) /* JNI couldn't return a local reference to the exception. */ memory_full (0); - /* Clear the exception, making it safe to subsequently call other - JNI functions. */ + /* Print and clear the exception, making it safe to subsequently + call other JNI functions. */ + (*env)->ExceptionDescribe (env); (*env)->ExceptionClear (env); /* Delete each of the N arguments. */ @@ -3760,16 +3762,16 @@ android_saf_exception_check (int n, ...) if ((*env)->IsInstanceOf (env, (jobject) exception, file_not_found_exception)) - errno = ENOENT; + new_errno = ENOENT; else if ((*env)->IsInstanceOf (env, (jobject) exception, security_exception)) - errno = EACCES; + new_errno = EACCES; else if ((*env)->IsInstanceOf (env, (jobject) exception, operation_canceled_exception)) - errno = EINTR; + new_errno = EINTR; else if ((*env)->IsInstanceOf (env, (jobject) exception, unsupported_operation_exception)) - errno = ENOSYS; + new_errno = ENOSYS; else if ((*env)->IsInstanceOf (env, (jobject) exception, out_of_memory_error)) { @@ -3777,10 +3779,11 @@ android_saf_exception_check (int n, ...) memory_full (0); } else - errno = EIO; + new_errno = EIO; /* expression is still a local reference! */ ANDROID_DELETE_LOCAL_REF ((jobject) exception); + errno = new_errno; return 1; } @@ -4238,10 +4241,7 @@ android_document_id_from_name (const char *tree_uri, char *name, inside_saf_critical_section = false; if (android_saf_exception_check (3, result, uri, java_name)) - { - rc = -1; - goto finish; - } + return -1; ANDROID_DELETE_LOCAL_REF (uri); ANDROID_DELETE_LOCAL_REF (java_name); @@ -4251,7 +4251,8 @@ android_document_id_from_name (const char *tree_uri, char *name, if (rc == -1) { ANDROID_DELETE_LOCAL_REF (result); - goto finish; + errno = ENOENT; + return -1; } eassert (rc == -2 || rc >= 0); @@ -4274,8 +4275,6 @@ android_document_id_from_name (const char *tree_uri, char *name, (*android_java_env)->ReleaseStringUTFChars (android_java_env, (jstring) uri, doc_id); ANDROID_DELETE_LOCAL_REF (uri); - - finish: return rc; } -- cgit v1.3 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 +++-- src/android.c | 3 +- src/androidvfs.c | 31 +++++++++++---- 4 files changed, 96 insertions(+), 18 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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; } diff --git a/src/android.c b/src/android.c index 687c0b48a2a..a75193e5edd 100644 --- a/src/android.c +++ b/src/android.c @@ -1581,7 +1581,8 @@ android_init_emacs_service (void) "(Ljava/lang/String;Ljava/lang/String;" "Ljava/lang/String;)Ljava/lang/String;"); FIND_METHOD (delete_document, "deleteDocument", - "(Ljava/lang/String;Ljava/lang/String;)I"); + "(Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;)I"); #undef FIND_METHOD } diff --git a/src/androidvfs.c b/src/androidvfs.c index b175f7746f3..6c34aac9e3e 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -3969,34 +3969,45 @@ android_saf_access (const char *uri_name, const char *id_name, /* Delete the document designated by DOC_ID within the tree identified through the URI TREE. Return 0 if the document has been deleted, - set errno and return -1 upon failure. */ + set errno and return -1 upon failure. + + DOC_NAME should be the name of the file itself, as a file name + whose constituent components lead to a document named DOC_ID. It + isn't used to search for a document ID, but is used to invalidate + the file cache. */ static int -android_saf_delete_document (const char *tree, const char *doc_id) +android_saf_delete_document (const char *tree, const char *doc_id, + const char *doc_name) { - jobject id, uri; + jobject id, uri, name; jmethodID method; jint rc; - /* Build the strings holding the ID and URI. */ + /* Build the strings holding the ID, URI and NAME. */ id = (*android_java_env)->NewStringUTF (android_java_env, doc_id); android_exception_check (); uri = (*android_java_env)->NewStringUTF (android_java_env, tree); android_exception_check_1 (id); + name = (*android_java_env)->NewStringUTF (android_java_env, + doc_name); + android_exception_check_2 (id, name); /* Now, try to delete the document. */ method = service_class.delete_document; rc = (*android_java_env)->CallIntMethod (android_java_env, emacs_service, - method, uri, id); + method, uri, id, + name); - if (android_saf_exception_check (2, id, uri)) + if (android_saf_exception_check (3, id, uri, name)) return -1; ANDROID_DELETE_LOCAL_REF (id); ANDROID_DELETE_LOCAL_REF (uri); + ANDROID_DELETE_LOCAL_REF (name); if (rc) { @@ -4553,7 +4564,9 @@ android_saf_tree_rmdir (struct android_vnode *vnode) return -1; } - return android_saf_delete_document (vp->tree_uri, vp->document_id); + return android_saf_delete_document (vp->tree_uri, + vp->document_id, + vp->name); } static int @@ -5173,7 +5186,9 @@ android_saf_file_unlink (struct android_vnode *vnode) struct android_saf_file_vnode *vp; vp = (struct android_saf_file_vnode *) vnode; - return android_saf_delete_document (vp->tree_uri, vp->document_id); + return android_saf_delete_document (vp->tree_uri, + vp->document_id, + vp->name); } static int -- cgit v1.3 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 +++++++ src/android.c | 3 + src/android.h | 1 + src/androidvfs.c | 179 ++++++++++++++++++++++++++++++++- 5 files changed, 293 insertions(+), 5 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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; + } }; diff --git a/src/android.c b/src/android.c index a75193e5edd..0bdbc4e0c4b 100644 --- a/src/android.c +++ b/src/android.c @@ -1583,6 +1583,9 @@ android_init_emacs_service (void) FIND_METHOD (delete_document, "deleteDocument", "(Ljava/lang/String;Ljava/lang/String;" "Ljava/lang/String;)I"); + FIND_METHOD (rename_document, "renameDocument", + "(Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;Ljava/lang/String;)I"); #undef FIND_METHOD } diff --git a/src/android.h b/src/android.h index fd391fa6435..591f1a1e43c 100644 --- a/src/android.h +++ b/src/android.h @@ -281,6 +281,7 @@ struct android_emacs_service jmethodID create_document; jmethodID create_directory; jmethodID delete_document; + jmethodID rename_document; }; extern JNIEnv *android_java_env; diff --git a/src/androidvfs.c b/src/androidvfs.c index c83d3a14b50..c529e1fb30f 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -4035,6 +4035,71 @@ android_saf_delete_document (const char *tree, const char *doc_id, return 0; } +/* Declared further below. */ +static int android_document_id_from_name (const char *, char *, char **); + +/* Rename the document designated by DOC_ID inside the directory tree + identified by URI, which should be within the directory by the name + of DIR, to NAME. If the document can't be renamed, return -1 and + set errno to a value describing the error. Return 0 if the rename + is successful. + + Android permits the same document to appear in multiple + directories, but stores the display name inside the document + ``inode'' itself instead of the directory entries that refer to it. + Because of this, this operation may cause other directory entries + outside DIR to be renamed. */ + +static int +android_saf_rename_document (const char *uri, const char *doc_id, + const char *dir, const char *name) +{ + int rc; + jstring uri1, doc_id1, dir1, name1; + jmethodID method; + + /* Now build the strings for the URI, document ID, directory name + and directory ID. */ + + uri1 = (*android_java_env)->NewStringUTF (android_java_env, uri); + android_exception_check (); + doc_id1 = (*android_java_env)->NewStringUTF (android_java_env, doc_id); + android_exception_check_1 (uri1); + dir1 = (*android_java_env)->NewStringUTF (android_java_env, dir); + android_exception_check_2 (doc_id1, uri1); + name1 = (*android_java_env)->NewStringUTF (android_java_env, name); + android_exception_check_3 (dir1, doc_id1, uri1); + + method = service_class.rename_document; + rc = (*android_java_env)->CallNonvirtualIntMethod (android_java_env, + emacs_service, + service_class.class, + method, uri1, doc_id1, + dir1, name1); + + /* Check for exceptions. */ + if (android_saf_exception_check (4, uri1, doc_id1, dir1, name1)) + return -1; + + /* Delete unused local references. */ + ANDROID_DELETE_LOCAL_REF (uri1); + ANDROID_DELETE_LOCAL_REF (doc_id1); + ANDROID_DELETE_LOCAL_REF (dir1); + ANDROID_DELETE_LOCAL_REF (name1); + + /* Then check for errors handled within the Java code. */ + + if (rc == -1) + { + /* UnsupportedOperationException. Trick the caller into falling + back on delete-then-copy code. */ + errno = EXDEV; + return -1; + } + + return 0; +} + /* SAF directory vnode. A file within a SAF directory tree is @@ -4591,9 +4656,117 @@ android_saf_tree_rename (struct android_vnode *src, struct android_vnode *dst, bool keep_existing) { - /* TODO */ - errno = ENOSYS; - return -1; + char *last, *dst_last; + struct android_saf_tree_vnode *vp, *vdst; + char path[PATH_MAX], *fill; + + /* If dst isn't a tree, file or new vnode, return EXDEV. */ + + if (dst->type != ANDROID_VNODE_SAF_TREE + && dst->type != ANDROID_VNODE_SAF_FILE + && dst->type != ANDROID_VNODE_SAF_NEW) + { + errno = EXDEV; + return -1; + } + + vp = (struct android_saf_tree_vnode *) src; + vdst = (struct android_saf_tree_vnode *) dst; + + /* if vp and vdst refer to different tree URIs, return EXDEV. */ + + if (strcmp (vp->tree_uri, vdst->tree_uri)) + { + errno = EXDEV; + return -1; + } + + /* If `keep_existing' and the destination vnode designates an + existing file, return EEXIST. */ + + if (keep_existing && dst->type != ANDROID_VNODE_SAF_NEW) + { + errno = EEXIST; + return -1; + } + + /* Unix `rename' maps to two Android content provider operations. + The first case is a simple rename, where src and dst are both + located within the same directory. Compare the file names of + both up to the component before the last. */ + + last = strrchr (vp->name, '/'); + eassert (last != NULL); + + if (last[1] == '\0') + { + if (last == vp->name) + { + /* This means the caller is trying to rename the root + directory of the tree. */ + errno = EROFS; + return -1; + } + + /* The name is terminated by a trailing directory separator. + Search backwards for the preceding directory separator. */ + last = memrchr (vp->name, '/', last - vp->name); + eassert (last != NULL); + } + + /* Find the end of the second-to-last component in vdst's name. */ + + dst_last = strrchr (vdst->name, '/'); + eassert (dst_last != NULL); + + if (dst_last[1] == '\0') + { + if (dst_last == vdst->name) + { + /* Forbid overwriting the root of the tree either. */ + errno = EROFS; + return -1; + } + + dst_last = memrchr (vdst->name, '/', dst_last - vdst->name); + eassert (dst_last != NULL); + } + + if (dst_last - vdst->name != last - vp->name + || memcmp (vp->name, vdst->name, last - vp->name)) + { + /* The second case is where the file must be moved from one + directory to the other, and possibly then recreated under a + new name. */ + + errno = EXDEV; /* TODO */ + return -1; + } + + /* Otherwise, do this simple rename. The name of the parent + directory is required, as it provides the directory whose entries + will be modified. */ + + if (last - vp->name >= PATH_MAX) + { + errno = ENAMETOOLONG; + return -1; + } + + /* If the destination document exists, delete it. */ + + if (dst->type != ANDROID_VNODE_SAF_NEW + && android_saf_delete_document (vdst->tree_uri, + vdst->document_id, + vdst->name)) + return -1; + + fill = mempcpy (path, vp->name, last - vp->name); + *fill = '\0'; + return android_saf_rename_document (vp->tree_uri, + vp->document_id, + path, + dst_last + 1); } static int -- cgit v1.3 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 +++++++--- lisp/subr.el | 9 +- src/android.c | 27 ++++ src/android.h | 2 + src/androidvfs.c | 237 ++++++++++++++++++++++++++++++++++- 5 files changed, 314 insertions(+), 28 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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); + } }; diff --git a/lisp/subr.el b/lisp/subr.el index 4346f99fa38..36aeeabea47 100644 --- a/lisp/subr.el +++ b/lisp/subr.el @@ -3796,11 +3796,10 @@ like) while `y-or-n-p' is running)." ;; Protect this-command when called from pre-command-hook (bug#45029) (this-command this-command) (str (progn - (when (active-minibuffer-window) - ;; If the minibuffer is already active, the - ;; selected window might not change. Disable - ;; text conversion by hand. - (set-text-conversion-style text-conversion-style)) + ;; If the minibuffer is already active, the + ;; selected window might not change. Disable + ;; text conversion by hand. + (set-text-conversion-style text-conversion-style) (read-from-minibuffer prompt nil keymap nil (or y-or-n-p-history-variable t))))) diff --git a/src/android.c b/src/android.c index 0bdbc4e0c4b..8c0232a51f8 100644 --- a/src/android.c +++ b/src/android.c @@ -1586,6 +1586,10 @@ android_init_emacs_service (void) FIND_METHOD (rename_document, "renameDocument", "(Ljava/lang/String;Ljava/lang/String;" "Ljava/lang/String;Ljava/lang/String;)I"); + FIND_METHOD (move_document, "moveDocument", + "(Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;Ljava/lang/String;" + "Ljava/lang/String;)Ljava/lang/String;"); #undef FIND_METHOD } @@ -5667,6 +5671,29 @@ android_exception_check_3 (jobject object, jobject object1, memory_full (0); } +/* Like android_exception_check_3, except it takes more than three + local reference arguments. */ + +void +android_exception_check_4 (jobject object, jobject object1, + jobject object2, jobject object3) +{ + if (likely (!(*android_java_env)->ExceptionCheck (android_java_env))) + return; + + __android_log_print (ANDROID_LOG_WARN, __func__, + "Possible out of memory error. " + " The Java exception follows: "); + /* Describe exactly what went wrong. */ + (*android_java_env)->ExceptionDescribe (android_java_env); + (*android_java_env)->ExceptionClear (android_java_env); + ANDROID_DELETE_LOCAL_REF (object); + ANDROID_DELETE_LOCAL_REF (object1); + ANDROID_DELETE_LOCAL_REF (object2); + ANDROID_DELETE_LOCAL_REF (object3); + memory_full (0); +} + /* Check for JNI problems based on the value of OBJECT. Signal out of memory if OBJECT is NULL. OBJECT1 means the diff --git a/src/android.h b/src/android.h index 591f1a1e43c..8440fb9bc75 100644 --- a/src/android.h +++ b/src/android.h @@ -110,6 +110,7 @@ extern void android_exception_check (void); extern void android_exception_check_1 (jobject); extern void android_exception_check_2 (jobject, jobject); extern void android_exception_check_3 (jobject, jobject, jobject); +extern void android_exception_check_4 (jobject, jobject, jobject, jobject); extern void android_exception_check_nonnull (void *, jobject); extern void android_exception_check_nonnull_1 (void *, jobject, jobject); @@ -282,6 +283,7 @@ struct android_emacs_service jmethodID create_directory; jmethodID delete_document; jmethodID rename_document; + jmethodID move_document; }; extern JNIEnv *android_java_env; diff --git a/src/androidvfs.c b/src/androidvfs.c index c529e1fb30f..9acc8f2b139 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -4036,7 +4036,8 @@ android_saf_delete_document (const char *tree, const char *doc_id, } /* Declared further below. */ -static int android_document_id_from_name (const char *, char *, char **); +static int android_document_id_from_name (const char *, const char *, + char **); /* Rename the document designated by DOC_ID inside the directory tree identified by URI, which should be within the directory by the name @@ -4078,8 +4079,17 @@ android_saf_rename_document (const char *uri, const char *doc_id, dir1, name1); /* Check for exceptions. */ + if (android_saf_exception_check (4, uri1, doc_id1, dir1, name1)) - return -1; + { + /* Substitute EXDEV for ENOSYS, so callers fall back on + delete-then-copy. */ + + if (errno == ENOSYS) + errno = EXDEV; + + return -1; + } /* Delete unused local references. */ ANDROID_DELETE_LOCAL_REF (uri1); @@ -4100,6 +4110,109 @@ android_saf_rename_document (const char *uri, const char *doc_id, return 0; } +/* Move the document designated by *DOC_ID from the directory under + DIR_NAME to the directory designated by DST_ID. All three + directories are located within the tree identified by the given + URI. + + If the document's ID changes as a result of the movement, free + *DOC_ID and store the new document ID within. + + Value is 0 upon success, -1 otherwise with errno set. */ + +static int +android_saf_move_document (const char *uri, char **doc_id, + const char *dir_name, const char *dst_id) +{ + char *src_id, *id; + jobject uri1, doc_id1, dir_name1, dst_id1, src_id1; + jstring result; + jmethodID method; + int rc; + const char *new_id; + + /* Obtain the name of the source directory. */ + src_id = NULL; + rc = android_document_id_from_name (uri, dir_name, &src_id); + + if (rc != 1) + { + /* This file is either not a directory or nonexistent. */ + xfree (src_id); + + switch (rc) + { + case 0: + errno = ENOTDIR; + return -1; + + case -1: + case -2: + errno = ENOENT; + return -1; + + default: + emacs_abort (); + } + } + + /* Build Java strings for all five arguments. */ + id = *doc_id; + uri1 = (*android_java_env)->NewStringUTF (android_java_env, uri); + android_exception_check (); + doc_id1 = (*android_java_env)->NewStringUTF (android_java_env, id); + android_exception_check_1 (uri1); + dir_name1 = (*android_java_env)->NewStringUTF (android_java_env, dir_name); + android_exception_check_2 (doc_id1, uri1); + dst_id1 = (*android_java_env)->NewStringUTF (android_java_env, dst_id); + android_exception_check_3 (dir_name1, doc_id1, uri1); + src_id1 = (*android_java_env)->NewStringUTF (android_java_env, src_id); + xfree (src_id); + android_exception_check_4 (dst_id1, dir_name1, doc_id1, uri1); + + /* Do the rename. */ + method = service_class.move_document; + result = (*android_java_env)->CallObjectMethod (android_java_env, + emacs_service, + method, uri1, + doc_id1, dir_name1, + dst_id1, src_id1); + if (android_saf_exception_check (5, src_id1, dst_id1, dir_name1, + doc_id1, uri1)) + { + /* Substitute EXDEV for ENOSYS, so callers fall back on + delete-then-copy. */ + + if (errno == ENOSYS) + errno = EXDEV; + + return -1; + } + + /* Delete unused local references. */ + ANDROID_DELETE_LOCAL_REF (src_id1); + ANDROID_DELETE_LOCAL_REF (dst_id1); + ANDROID_DELETE_LOCAL_REF (dir_name1); + ANDROID_DELETE_LOCAL_REF (doc_id1); + ANDROID_DELETE_LOCAL_REF (uri1); + + if (result) + { + /* The document ID changed. Free id and replace *DOC_ID with + the new ID. */ + xfree (id); + new_id = (*android_java_env)->GetStringUTFChars (android_java_env, + result, NULL); + android_exception_check_nonnull ((void *) new_id, result); + *doc_id = xstrdup (new_id); + (*android_java_env)->ReleaseStringUTFChars (android_java_env, result, + new_id); + ANDROID_DELETE_LOCAL_REF (result); + } + + return 0; +} + /* SAF directory vnode. A file within a SAF directory tree is @@ -4282,7 +4395,7 @@ android_verify_jni_string (const char *name) ID lookup to be canceled. */ static int -android_document_id_from_name (const char *tree_uri, char *name, +android_document_id_from_name (const char *tree_uri, const char *name, char **id) { jobjectArray result; @@ -4658,7 +4771,9 @@ android_saf_tree_rename (struct android_vnode *src, { char *last, *dst_last; struct android_saf_tree_vnode *vp, *vdst; - char path[PATH_MAX], *fill; + char path[PATH_MAX], path1[PATH_MAX]; + char *fill, *dst_id; + int rc; /* If dst isn't a tree, file or new vnode, return EXDEV. */ @@ -4739,8 +4854,118 @@ android_saf_tree_rename (struct android_vnode *src, directory to the other, and possibly then recreated under a new name. */ - errno = EXDEV; /* TODO */ - return -1; + /* The names of the source and destination directories will have + to be copied to path. */ + + if (last - vp->name >= PATH_MAX + || dst_last - vdst->name >= PATH_MAX) + { + errno = ENAMETOOLONG; + return -1; + } + + fill = mempcpy (path, vp->name, last - vp->name); + *fill = '\0'; + + /* If vdst doesn't already exist, its document_id field is + already the name of its parent directory. */ + + if (dst->type == ANDROID_VNODE_SAF_NEW) + { + /* First, move the document. This will update + VP->document_id if it changes. */ + + if (android_saf_move_document (vp->tree_uri, + &vp->document_id, + path, + vdst->document_id)) + return -1; + + fill = mempcpy (path, vdst->name, dst_last - vdst->name); + *fill = '\0'; + + /* Next, rename the document, if its display name differs + from that of the source. */ + + if (strcmp (dst_last + 1, last + 1) + /* By now vp->document_id is already in the destination + directory. */ + && android_saf_rename_document (vp->tree_uri, + vp->document_id, + path, + dst_last + 1)) + return -1; + + return 0; + } + + /* Retrieve the ID designating the destination document's parent + directory. */ + + fill = mempcpy (path1, vdst->name, dst_last - vdst->name); + *fill = '\0'; + + rc = android_document_id_from_name (vp->tree_uri, + path1, &dst_id); + + if (rc != 1) + { + /* This file is either not a directory or nonexistent. */ + + switch (rc) + { + case 0: + errno = ENOTDIR; + goto error; + + case -1: + /* dst_id is not set here, as the penultimate component + also couldn't be located. */ + errno = ENOENT; + return -1; + + case -2: + errno = ENOENT; + goto error; + + default: + emacs_abort (); + } + } + + /* vdst already exists, so it needs to be deleted first. */ + + if (android_saf_delete_document (vdst->tree_uri, + vdst->document_id, + vdst->name)) + goto error; + + /* First, move the document. This will update + VP->document_id if it changes. */ + + if (android_saf_move_document (vp->tree_uri, + &vp->document_id, + path, dst_id)) + goto error; + + /* Next, rename the document, if its display name differs from + that of the source. */ + + if (strcmp (dst_last + 1, last + 1) + /* By now vp->document_id is already in the destination + directory. */ + && android_saf_rename_document (vp->tree_uri, + vp->document_id, + path1, + dst_last + 1)) + goto error; + + xfree (dst_id); + return 0; + + error: + xfree (dst_id); + return 1; } /* Otherwise, do this simple rename. The name of the parent -- cgit v1.3 From 9cf166db63b7383a94dc47a6a5251c0dbe1dae9b Mon Sep 17 00:00:00 2001 From: Po Lu Date: Mon, 31 Jul 2023 14:18:12 +0800 Subject: Initialize Android API level earlier * java/org/gnu/emacs/EmacsNative.java (EmacsNative): * java/org/gnu/emacs/EmacsNoninteractive.java (main): * java/org/gnu/emacs/EmacsService.java (run): * java/org/gnu/emacs/EmacsThread.java (run): * src/android.c (initEmacs, setEmacsParams): Set `android_api_level' within setEmacsParams, not in initEmacs. * src/androidvfs.c: Pacify compiler warnings. --- java/org/gnu/emacs/EmacsNative.java | 14 +++++++------- java/org/gnu/emacs/EmacsNoninteractive.java | 6 +++--- java/org/gnu/emacs/EmacsService.java | 3 ++- java/org/gnu/emacs/EmacsThread.java | 4 +--- src/android.c | 12 +++++++----- src/androidvfs.c | 14 ++++++++++++++ 6 files changed, 34 insertions(+), 19 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java index ea200037218..7d72a9f192e 100644 --- a/java/org/gnu/emacs/EmacsNative.java +++ b/java/org/gnu/emacs/EmacsNative.java @@ -66,7 +66,9 @@ public final class EmacsNative classPath must be the classpath of this app_process process, or NULL. - emacsService must be the EmacsService singleton, or NULL. */ + emacsService must be the EmacsService singleton, or NULL. + + apiLevel is the version of Android being run. */ public static native void setEmacsParams (AssetManager assetManager, String filesDir, String libDir, @@ -75,18 +77,16 @@ public final class EmacsNative float pixelDensityY, float scaledDensity, String classPath, - EmacsService emacsService); + EmacsService emacsService, + int apiLevel); /* Initialize Emacs with the argument array ARGV. Each argument must contain a NULL terminated string, or else the behavior is undefined. DUMPFILE is the dump file to use, or NULL if Emacs is to load - loadup.el itself. - - APILEVEL is the version of Android being used. */ - public static native void initEmacs (String argv[], String dumpFile, - int apiLevel); + loadup.el itself. */ + public static native void initEmacs (String argv[], String dumpFile); /* Abort and generate a native core dump. */ public static native void emacsAbort (); diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java index aa6fa41ba97..1c7513e1cc9 100644 --- a/java/org/gnu/emacs/EmacsNoninteractive.java +++ b/java/org/gnu/emacs/EmacsNoninteractive.java @@ -190,14 +190,14 @@ public final class EmacsNoninteractive EmacsNative.setEmacsParams (assets, filesDir, libDir, cacheDir, 0.0f, - 0.0f, 0.0f, null, null); + 0.0f, 0.0f, null, null, + Build.VERSION.SDK_INT); /* Now find the dump file that Emacs should use, if it has already been dumped. */ EmacsApplication.findDumpFile (context); /* Start Emacs. */ - EmacsNative.initEmacs (args, EmacsApplication.dumpFileName, - Build.VERSION.SDK_INT); + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); } }; diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java index e714f75fdf2..3c1bb0855f4 100644 --- a/java/org/gnu/emacs/EmacsService.java +++ b/java/org/gnu/emacs/EmacsService.java @@ -291,7 +291,8 @@ public final class EmacsService extends Service cacheDir, (float) pixelDensityX, (float) pixelDensityY, (float) scaledDensity, - classPath, EmacsService.this); + classPath, EmacsService.this, + Build.VERSION.SDK_INT); } }, extraStartupArgument, /* If any file needs to be opened, open it now. */ diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java index c003ea95c50..5307015b46f 100644 --- a/java/org/gnu/emacs/EmacsThread.java +++ b/java/org/gnu/emacs/EmacsThread.java @@ -22,7 +22,6 @@ package org.gnu.emacs; import java.lang.Thread; import java.util.Arrays; -import android.os.Build; import android.util.Log; public final class EmacsThread extends Thread @@ -78,7 +77,6 @@ public final class EmacsThread extends Thread /* Run the native code now. */ Log.d (TAG, "run: " + Arrays.toString (args)); - EmacsNative.initEmacs (args, EmacsApplication.dumpFileName, - Build.VERSION.SDK_INT); + EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); } }; diff --git a/src/android.c b/src/android.c index 8c0232a51f8..f60ff5acb54 100644 --- a/src/android.c +++ b/src/android.c @@ -1281,7 +1281,8 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, jfloat pixel_density_y, jfloat scaled_density, jobject class_path, - jobject emacs_service_object) + jobject emacs_service_object, + jint api_level) { JNI_STACK_ALIGNMENT_PROLOGUE; @@ -1289,6 +1290,10 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object, pthread_t thread; const char *java_string; + /* Set the Android API level early, as it is used by + `android_vfs_init'. */ + android_api_level = api_level; + /* This function should only be called from the main thread. */ android_pixel_density_x = pixel_density_x; @@ -1771,7 +1776,7 @@ android_init_emacs_cursor (void) JNIEXPORT void JNICALL NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, - jobject dump_file_object, jint api_level) + jobject dump_file_object) { /* android_emacs_init is not main, so GCC is not nice enough to add the stack alignment prologue. @@ -1788,9 +1793,6 @@ NATIVE_NAME (initEmacs) (JNIEnv *env, jobject object, jarray argv, const char *c_argument; char *dump_file; - /* Set the Android API level. */ - android_api_level = api_level; - android_java_env = env; nelements = (*env)->GetArrayLength (env, argv); diff --git a/src/androidvfs.c b/src/androidvfs.c index 9acc8f2b139..eeef5ea5db0 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -5976,6 +5976,14 @@ android_saf_new_opendir (struct android_vnode *vnode) /* Semaphore posted upon the completion of an SAF operation. */ static sem_t saf_completion_sem; +#ifdef __clang__ +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wmissing-prototypes" +#else /* GNUC */ +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wmissing-prototypes" +#endif /* __clang__ */ + JNIEXPORT jint JNICALL NATIVE_NAME (safSyncAndReadInput) (JNIEnv *env, jobject object) { @@ -6010,6 +6018,12 @@ NATIVE_NAME (safPostRequest) (JNIEnv *env, jobject object) sem_post (&saf_completion_sem); } +#ifdef __clang__ +#pragma clang diagnostic pop +#else /* GNUC */ +#pragma GCC diagnostic pop +#endif /* __clang__ */ + /* Root vnode. This vnode represents the root inode, and is a regular -- cgit v1.3 From 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. --- doc/emacs/android.texi | 15 ++++-- java/org/gnu/emacs/EmacsService.java | 25 ++++++++++ src/android.c | 2 + src/android.h | 1 + src/androidvfs.c | 91 +++++++++++++++++++++++++++++++++--- 5 files changed, 123 insertions(+), 11 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi index 0330e9b5890..4b8f36a65eb 100644 --- a/doc/emacs/android.texi +++ b/doc/emacs/android.texi @@ -219,6 +219,15 @@ containing files provided by external programs (@pxref{Android Document Providers}.) @end itemize + Despite ordinary installations of Android not having files within +the (normally read-only) root directory named @file{content} or +@file{assets}, you may want to access real files by these names if the +Android installation in use has been customized. These files will +conflict with the aformentioned special directories, but can +nevertheless be accessed by writing their names relative to the +``parent'' directory of the root directory, as so illustrated: +@file{/../content}, @file{/../assets}. + The external storage directory is found at @file{/sdcard}. The other directories are not found at any fixed location (but see below), although the app data directory is typically symlinked to @@ -268,10 +277,8 @@ System -> Apps -> Special App Access -> All files access -> Emacs After you disable or enable this setting as appropriate and grant Emacs the ``Files and Media'' permission, it will be able to access -files under @file{/sdcard} as usual. - - These settings are not present on many proprietary versions of -Android. +files under @file{/sdcard} as usual. These settings are not present +on some proprietary versions of Android. @node Android Document Providers @section Accessing files from other programs under Android 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; + } }; diff --git a/src/android.c b/src/android.c index 2b785319549..c30d7b58979 100644 --- a/src/android.c +++ b/src/android.c @@ -1592,6 +1592,8 @@ android_init_emacs_service (void) "(Ljava/lang/String;Ljava/lang/String;" "Ljava/lang/String;Ljava/lang/String;" "Ljava/lang/String;)Ljava/lang/String;"); + FIND_METHOD (valid_authority, "validAuthority", + "(Ljava/lang/String;)Z"); #undef FIND_METHOD } diff --git a/src/android.h b/src/android.h index 8440fb9bc75..945bd649c18 100644 --- a/src/android.h +++ b/src/android.h @@ -284,6 +284,7 @@ struct android_emacs_service jmethodID delete_document; jmethodID rename_document; jmethodID move_document; + jmethodID valid_authority; }; extern JNIEnv *android_java_env; diff --git a/src/androidvfs.c b/src/androidvfs.c index eeef5ea5db0..e3b0b895df3 100644 --- a/src/androidvfs.c +++ b/src/androidvfs.c @@ -3249,6 +3249,43 @@ static struct android_saf_root_vdir *all_saf_root_vdirs; static struct android_vnode *android_saf_tree_from_name (char *, const char *, const char *); +/* Forward declaration. */ +static int android_verify_jni_string (const char *); + +/* Ascertain and return whether or not AUTHORITY designates a content + provider offering at least one directory tree accessible to + Emacs. */ + +static bool +android_saf_valid_authority_p (const char *authority) +{ + jobject string; + jboolean valid; + jmethodID method; + + /* Make certain AUTHORITY can actually be represented as a Java + string. */ + + if (android_verify_jni_string (authority)) + return false; + + /* Build a string containing AUTHORITY. */ + + string = (*android_java_env)->NewStringUTF (android_java_env, + authority); + android_exception_check (); + + method = service_class.valid_authority; + valid + = (*android_java_env)->CallNonvirtualBooleanMethod (android_java_env, + emacs_service, + service_class.class, + method, string); + android_exception_check_1 (string); + ANDROID_DELETE_LOCAL_REF (string); + return valid; +} + static struct android_vnode * android_saf_root_name (struct android_vnode *vnode, char *name, size_t length) @@ -3311,9 +3348,6 @@ android_saf_root_name (struct android_vnode *vnode, char *name, return android_saf_tree_from_name (component_end, component, vp->authority); - /* Otherwise, find the first component of NAME and create a vnode - representing it as an authority. */ - /* Create the vnode. */ vp = xmalloc (sizeof *vp); vp->vnode.ops = &saf_root_vfs_ops; @@ -3414,6 +3448,22 @@ static int android_saf_root_stat (struct android_vnode *vnode, struct stat *statb) { + struct android_saf_root_vnode *vp; + + /* Verify that the authority actually exists and return ENOENT + otherwise, lest `locate-dominating-file' & co call an operation + that doesn't require listing URIs under this authority, such as + access. */ + + vp = (struct android_saf_root_vnode *) vnode; + + if (vp->authority + && !android_saf_valid_authority_p (vp->authority)) + { + errno = ENOENT; + return -1; + } + /* Make up some imaginary statistics for this vnode. */ memset (statb, 0, sizeof *statb); @@ -3428,6 +3478,8 @@ android_saf_root_stat (struct android_vnode *vnode, static int android_saf_root_access (struct android_vnode *vnode, int mode) { + struct android_saf_root_vnode *vp; + /* Validate MODE. */ if (mode != F_OK && !(mode & (W_OK | X_OK | R_OK))) @@ -3444,6 +3496,20 @@ android_saf_root_access (struct android_vnode *vnode, int mode) return -1; } + /* Verify that the authority actually exists and return ENOENT + otherwise, lest `locate-dominating-file' & co call an operation + that doesn't require listing URIs under this authority, such as + access. */ + + vp = (struct android_saf_root_vnode *) vnode; + + if (vp->authority + && !android_saf_valid_authority_p (vp->authority)) + { + errno = ENOENT; + return -1; + } + return 0; } @@ -5309,9 +5375,9 @@ android_saf_tree_opendir (struct android_vnode *vnode) AUTHORITY is the name of the content provider authority that is offering TREE. - Value is NULL if no document tree or provider by those names - exists, or some other error takes place (for example, if TREE and - AUTHORITY aren't encoded correctly.) */ + Value is NULL and errno is set if no document tree or provider by + those names exists, or some other error takes place (for example, + if TREE and AUTHORITY aren't encoded correctly.) */ static struct android_vnode * android_saf_tree_from_name (char *name, const char *tree, @@ -5323,7 +5389,18 @@ android_saf_tree_from_name (char *name, const char *tree, const char *uri; struct android_vnode *vp; - /* Assume that TREE and NAME are in ``modified UTF-8 format''. */ + /* It's not a given that NAME and TREE are actually in the modified + UTF-8 format used by the JVM to encode strings, and the JVM + aborts when encountering a string that is not. Make sure they + are valid before continuing. */ + + if (android_verify_jni_string (name) + || android_verify_jni_string (authority)) + { + errno = ENOENT; + return NULL; + } + tree_string = (*android_java_env)->NewStringUTF (android_java_env, tree); android_exception_check (); -- cgit v1.3 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 +++- src/fileio.c | 30 ++- 3 files changed, 274 insertions(+), 120 deletions(-) (limited to 'java/org/gnu/emacs/EmacsService.java') 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) diff --git a/src/fileio.c b/src/fileio.c index 5ce933ec45b..1ccb871ce49 100644 --- a/src/fileio.c +++ b/src/fileio.c @@ -184,9 +184,12 @@ static bool e_write (int, Lisp_Object, ptrdiff_t, ptrdiff_t, /* Establish that ENCODED is not contained within a special directory whose contents are not eligible for Unix VFS operations. Signal a - `file-error' with REASON if it does. */ + `file-error' with REASON if it does. -static void + If REASON is NULL, instead return whether ENCODED is contained + within such a directory. */ + +static bool check_vfs_filename (Lisp_Object encoded, const char *reason) { #if defined HAVE_ANDROID && !defined ANDROID_STUBIFY @@ -194,11 +197,16 @@ check_vfs_filename (Lisp_Object encoded, const char *reason) name = SSDATA (encoded); - if (android_is_special_directory (name, "/assets")) - xsignal2 (Qfile_error, build_string (reason), encoded); + if (android_is_special_directory (name, "/assets") + || android_is_special_directory (name, "/content")) + { + if (!reason) + return true; + + xsignal2 (Qfile_error, build_string (reason), encoded); + } - if (android_is_special_directory (name, "/content")) - xsignal2 (Qfile_error, build_string (reason), encoded); + return false; #endif /* defined HAVE_ANDROID && !defined ANDROID_STUBIFY */ } @@ -3657,8 +3665,14 @@ command from GNU Coreutils. */) return call4 (handler, Qset_file_modes, absname, mode, flag); encoded = ENCODE_FILE (absname); - check_vfs_filename (encoded, "Trying to change access modes of file" - " within special directory"); + + /* Silently ignore attempts to change the access modes of files + within /contents on Android, preventing errors within backup file + creation. */ + + if (check_vfs_filename (encoded, NULL)) + return Qnil; + char *fname = SSDATA (encoded); mode_t imode = XFIXNUM (mode) & 07777; if (fchmodat (AT_FDCWD, fname, imode, nofollow) != 0) -- cgit v1.3 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/emacs/EmacsService.java') 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.3 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/emacs/EmacsService.java') 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.3