aboutsummaryrefslogtreecommitdiffstats
path: root/java/org
diff options
context:
space:
mode:
authorPo Lu2023-08-07 08:51:11 +0800
committerPo Lu2023-08-07 08:51:11 +0800
commitc71a520d1da636a722cf87b46534ca3b5aafbc7b (patch)
tree95a3099c065800f60602e0a403b551a6d6dba139 /java/org
parent18e7bc87521e3c48b819cfe4a113f532ba905561 (diff)
parent9a9f73041d09d2da7ed562c7ffae0d9519562fba (diff)
downloademacs-c71a520d1da636a722cf87b46534ca3b5aafbc7b.tar.gz
emacs-c71a520d1da636a722cf87b46534ca3b5aafbc7b.zip
Introduce an Android window system port for GNU Emacs
* src/xterm.h: New fields `quit_keysym' and `quit_keysym_time'. * src/xterm.c (handle_one_xevent): Check for the quit keysym, and set Vquit_flag upon witnessing two clicks in rapid succession. (x_term_init): Set `quit_keysym'. (init_xterm): Fix typo in name of `register_textconv_interface'. (syms_of_xterm) <Vx_toolkit_scroll_bars>: Describe its default value on android. * src/xfns.c (xic_string_conversion_callback): Pass `0' as the last argument to textconv_query. (Fx_server_vendor, Fx_server_version): Document return values on Android. * src/xfaces.c (Fx_family_fonts, set_lface_from_font): Use FRAME_RES instead of FRAME_RES_Y, respecting user preferences on window systems that have distinct display and font scaling factors. (Fx_load_color_file): Call `emacs_fclose', not fclose. * src/xdisp.c (tab_bar_item_info): Allow `close_p' to be NULL. (get_tab_bar_item): Update commentary to reflect that change. (get_tab_bar_item_kbd): New function, resembling get_tab_bar_item. (build_desired_tool_bar_string): Clear `f->tool_bar_wraps_p'; insert new line characters if a QCwrap item is encountered, and set f->tool_bar_wrap_p. Replace characters beyond the end of the tool bar with spaces. (display_tool_bar_line): Move iterator to the next line if in contact with an explicit line-wrap item. (redisplay_internal): If there are newline characters in the tool bar, refrain from coercing each row into being identically tall. Don't call `set_tty_color_mode' on Android. (mark_window_display_accurate_1): Report changes to the point and mark to input methods. (display_menu_bar): Adjust ifdefs to allow non-X window systems to use the built-in menu bar. (draw_row_with_mouse_face): Don't call TTY functions on Android. (note_mouse_highlight): Call `popup_activated' on Android. (expose_frame): Correctly work on the menu bar window. (gui_union_rectangles): New function. * src/window.h (struct window): New fields for recording the last window and point positions, along with an ephemeral position used during IM text conversion. (WINDOW_MENU_BAR_P): Correct definition for non-X window systems without external menu bars. * src/window.c (replace_buffer_in_windows): Call Qreplace_buffer_in_windows only when bound. * src/w32proc.c (sys_spawnve): Pass extra argument to openp. * src/w32font.c (fill_in_logfont): Use font scaling factor, not the display scaling factor. * src/w32.c (check_windows_init_file): Pass extra argument to openp. * src/verbose.mk.in (AM_V_JAVAC, AM_V_DX, AM_V_AAPT) (AM_V_ZIPALIGN, AM_V_SILENT): New variables. * src/textconv.h (struct textconv_interface): New `point_changed', `compose_region_changed' and `notify_conversion'. Add declarations for new functions. * src/textconv.c (TEXTCONV_DEBUG): New macro. (suppress_conversion_count): New variable. (enum textconv_batch_edit_flags): New flag. (copy_buffer): Don't overwrite text before the gap with the text after. (get_mark, select_window): New functions. (textconv_query): New argument FLAGS. Contingent upon its value, use the previous point or mark or skip the conversion region. (sync_overlay, record_buffer_change, reset_frame_state) (detect_conversion_events, restore_selected_window) (really_commit_text, really_finish_composing_text) (really_set_composing_region, really_delete_composing_text) (really_request_point_update, really_set_point_and_mark) (complete_edit): New functions. (struct complete_edit_check_context): New structure; store in it the result of editing operations. (complete_edit_check, handle_pending_conversion_events_1) (decrement_inside, handle_pending_conversion_events) (start_batch_edit, end_batch_edit, commit_text) (set_composing_text, textconv_set_point_and_mark) (request_point_update, textconv_barrier, get_extracted_text) (get_surrounding_text, conversion_disabled_p) (report_selected_window_change, report_point_change) (disable_text_conversion, resume_text_conversion) (register_textconv_interface, check_postponed_buffers) (postponed_buffers, Fset_text_conversion_style) (syms_of_textconv) <Qaction, Qtext_conversion, Qpush_mark, Qunderline, Qoverriding_text_conversion_style, Vtext_conversion_edits, Voverriding_text_conversion_style, Vtext_conversion_face>: New functions, symbols and variables. * src/terminal.c (Fterminal_live_p): Return Qandroid if type is output_android. * src/termhooks.h (enum output_method): Add `output_android'. (struct terminal) <display_info>: Add union constituent field for `android'. <query_colors>: Define on Android as well. (TERMINAL_FONT_CACHE) [HAVE_ANDROID]: Return the inappropriately named font cache field on Android. * src/term.c (string_cost, string_cost_one_line, per_line_cost) (calculate_costs, produce_glyphs, produce_glyphs, tty_capable_p) (tty_capable_p, device, init_tty, maybe_fatal) (delete_tty) [HAVE_ANDROID]: Exclude or turn these functions into vestiges. (Fsuspend_tty, Fresume_tty): Call `emacs_fclose' and always signal on Android. (Fresume_tty): Call `emacs_fdopen'. (Ftty__set_output_buffer_size) [HAVE_ANDROID]: Remove this function. (encode_terminal_code): Replace with a stub. (init_tty, delete_tty, maybe_fatal): Call `emacs_fclose'. (syms_of_term): Remove most unnecessary code on Android. <system_uses_terminfo>: Always set this option on Android. * src/sysdep.c (init_standard_fds): Call emacs_fopen. (reset_sigio, widen_foreground_group): Define out on Android. (reset_sys_modes): Don't call either function on Android. (init_sigbus, handle_sigbus): New functions. (init_signals): Don't add user signals on Android. Register signal handlers for SIGBUS, and refrain from handling SIGSEGV. (emacs_fstatat): Wrap android_fstatat on Android. (sys_fstat, sys_faccessat): New function. (emacs_openat): Exclude this function when building libemacs.so. (emacs_open, emacs_open_noquit, emacs_fopen, emacs_close): Wrap functions defined in the Android filesystem emulation code. (emacs_fdopen, emacs_fclose, emacs_unlink, emacs_symlink) (emacs_rmdir, emacs_mkdir, emacs_renameat_noreplace, emacs_rename) (emacs_fchmodat): New wrappers for more of those functions. (close_output_streams): Placate the file descriptor sanitizer that's included with android. * src/sound.c (Fplay_sound_internal): Pass extra argument to openp. * src/sfntfont.h: * src/sfntfont.c: * src/sfntfont-android.c: * src/sfnt.h: * src/sfnt.c: New files. * src/scroll.c: Exclude the entire file on Android. * src/process.c: (allocate_pty): Call sys_faccessat, not faccessat. (Fmake_process): Call openp with an extra argument. (wait_reading_process_output): Call android_select. (Fprocess_send_eof): Don't call tcdrain if not present. (handle_child_signal): Write a comment describing a small, seldom encountered issue. * src/print.c (print_vectorlike): Don't print FONT_EXTRA_INDEX for font entities. * src/pdumper.c (Fdump_emacs_portable): Allow dumping in interactive Emacs's on Android, as this is performed within loadup.el. (dump_discard_mem): Use madvise if posix_advise is not present. (pdumper_load): Call sys_fstat, not fstat. (syms_of_pdumper) <Vpdumper_fingerprint>: Calculate the fingerprint for this Emacs executable and store it there. * src/menu.c (have_boxes): Android has boxes. (push_submenu_start, push_submenu_end): Define on Android. (single_menu_item): Produce submenus on Android as well. (x_popup_menu_1): Call EVENT_START, in contrast to duplicating its old functionality with calls to Fcar and XCDR. (Fx_popup_menu): Update documentation to reflect that touch screen events are now accepted as POSITION. * src/marker.c (set_marker_internal): Redisplay buffers when their mark changes, enabling changes to be reported to the IME. * src/lread.c (lread_fd, lread_fd_cmp, lread_fd_p, lread_close) (lread_fstat, lread_read_quit, lread_lseek, file_stream) (file_seek, file_stream_valid_p, file_stream_close) (file_stream_invalid, getc): New macros. Define to an implementation with file descriptors and file streams on systems other than Android 2.3+, and one using Android file descriptors on those systems. (USE_ANDROID_ASSETS): Define on Android 2.3+; (file_get_char): New function. (infile, skip_dyn_bytes, skip_dyn_eof, readbyte_from_stdio) (read_filtered_event, safe_to_load_version, close_infile_unwind): Implement in terms of those macros. (close_file_unwind_android_fd): New function. (Fload): Pass extra argument to `openp' and use Android file descriptors where possible. (Flocate_file_internal): Pass extra argument to `openp'. (maybe_swap_for_eln1): Call sys_fstat, not fstat. (openp): New arg PLATFORM; if supplied and opening a platform-specific file descriptor replacement is possible, place one there. (build_load_history): Fix typos in comments. (skip_lazy_string): Implement in terms of the aformentioned macros. * src/lisp.h: Add declarations for new functions. * src/keyboard.h (reading_key_sequence): Declare here. (EVENT_START): Treat touch screen events specially by returning the posn of their touch point. * src/keyboard.c (reading_key_sequence, menu_bar_touch_id): New variables. (command_loop_1): (read_menu_command): Pass false to read_key_sequence. (read_char): Update commentary. (readable_events): If text conversion events (edits from an input method) are queued, return 1. (kbd_buffer_get_event): If text conversion events exist, carry out the edits contained within. Then, generate a Qtext_conversion event. (lispy_function_keys, FUNCTION_KEY_OFFSET): Define function key array on Android. (coords_in_tab_bar_window): New function. (make_lispy_event) <TOUCHSCREEN_BEGIN_EVENT>: Keep track of touches that fall into the confines of the tab bar, and include the tab bar item in their position lists. Moreover, retain and track the touch in C code if it's taking place within the menu bar. <TOUCHSCREEN_END_EVENT>: Likewise for the tab bar; generate menu bar events if the touch ends on a menu item and was previously singled out for tracking. <TOUCHSCREEN_UPDATE_EVENT>: Don't deliver this event if the frame is dead, or if it was identified for tracking since the only touch sequence that changed begun inside the menu bar. (handle_async_input): Call android_check_query_urgent. (handle_input_available_signal): Add memory fence. (parse_tool_bar_item): Handle `wrap' properties within tool bar items moving subsequent items onto a new row. (access_keymap_keyremap): New arguments START, END, KEYBUF. Set Qcurrent_key_remap_sequence around calls to the remap function. (keyremap_step): Pass the necessary information to access_keymap_keyremap. (restore_reading_key_sequence): New function. (read_key_sequence): Set `reading_key_sequence'. New arg DISABLE_TEXT_CONVERSION_P, which causes text conversion to be disabled as long as the key sequence is being read. Disable text conversion as well if a menu or function key prefix is read, insert imaginary prefix keys before touchscreen events within special areas of a frame. Don't insert prefix keys if input is being mocked, which transpires if the input is in actuality originating from a key translation map. (read_key_sequence_vs): New argument DISABLE_TEXT_CONVERSION. (Fread_key_sequence): New argument DISABLE_TEXT_CONVERSION. (Fopen_dribble_file): Use emacs_fclose. (head_table): Make touchscreen-begin and touchscreen-end events touchscreen events. (syms_of_keyboard) <QCwrap, Qtouchscreen, Qtext_conversion>: New symbols. <disable_inhibit_text_conversion, Vcurrent_key_remap_sequence>: New variables. * src/inotify.c (Finotify_add_watch): Detect and avoid watching special files that don't exist from the POV of inotify. * src/image.c (image_create_bitmap_from_data) (image_create_bitmap_from_file, free_bitmap_record) (prepare_image_for_display, image_clear_image_1) (image_clear_image_1, image_size_in_bytes, image_set_transform): (Create_Pixmap_From_Bitmap_Data, lookup_rgb_color) (image_to_emacs_colors, image_from_emacs_colors) (image_pixmap_draw_cross, image_disable_image): Implement on Android, reusing much of the X11 code. (matrix_identity, matrix_rotate, matrix_mirror_horizontal) (matrix_translate): New functions. (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_get_x_image, image_unget_x_image): Implement on Android. (image_find_image_fd): Return an Android file descriptor if possible. (close_android_fd): New function. (slurp_file): Accept `image_fds', defined to Android file descriptors. (xpm_load): Enable built-in XPM support on Android. (xbm_load, pbm_load, png_load_body, jpeg_load_body, gif_load) (webp_load, imagemagick_load_image, svg_load): Use image file descriptors on Android; these file descriptors may in fact represent compressed asset streams, and obviate the necessity of creating a new file descriptor for each asset image opened. (Fimage_transforms_p): Report rotate90 on Android. (image_types, syms_of_image): Enable built-in XPM support on Android. * src/fringe.c (init_fringe_bitmap): Bit swap bitmaps on Android, as on X. * src/frame.h (enum text_conversion_operation): New enumerator. (struct text_conversion_action, struct text_conversion_state): New variable. (struct frame): New fields `tool_bar_wraps_p' and `conversion'. Increase the width of `output_method'. <output_data>: Add `android' field. <wait_event_type>: Define on Android as well. (fset_menu_bar_window): Define correctly, so that it's declared on non-X builds without external menu bars. (FRAME_ANDROID_P): Define macro. (FRAME_WINDOW_P) [HAVE_ANDROID]: Define to FRAME_ANDROID_P. (FRAME_RES): New macro. (MOUSE_HL_INFO): Define without referencing tty output data on Android, which doesn't have them. * src/frame.c (Fframep): Return `android' on Android systems. (Fwindow_system): Likewise. (make_frame): Clear text conversion state and `tool_bar_wraps_p'. (Fmake_terminal_frame): Signal that Android doesn't support text terminals. (delete_frame): Reset text conversion state prior to deleting the frame. (gui_display_get_resource): Don't call the resource hook on Android. (Fx_parse_geometry): Pacify compiler warning. (make_monitor_attribute_list): Don't always use SOURCE if nil. (syms_of_frame) <Qandroid>: New symbol. <Vdefault_frame_scroll_bars>: Don't default scroll bars to an enabled state on Android. * src/fontset.c (fontset_find_font): Tackle an unusual problem. * src/font.h (struct font_entity): New field `is_android'. (PT_PER_INCH): Define to 160.00 on Android. * src/font.c (font_make_entity): New function. (font_make_entity_android): New variant that sets `is_android' to true. (font_pixel_size, font_find_for_lface, font_open_for_lface) (Ffont_face_attributes, Fopen_font): Respect the distinction between frame text and display scales. * src/fns.c (Flocale_info): Silence compiler warning. * src/filelock.c (BOOT_TIME): Undefine BOOT_TIME when building libemacs.so (get_boot_time, rename_lock_file, create_lock_file) (current_lock_owner, make_lock_file_name, unlock_file): Employ wrappers for Android filesystem operations. * src/fileio.c (emacs_fd, emacs_fd_open, emacs_fd_close) (emacs_fd_read, emacs_fd_lseek, emacs_fd_fstat, emacs_fd_valid_p): New type and macros; define them to suitable values, akin to those in lread.c (check_vfs_filename): New function. (file_access_p): Call `sys_faccessat'. (close_file_unwind_emacs_fd): New function. (fclose_unwind): Call `emacs_fclose', not fclose. (file_name_directory): Export this function. (user_homedir): If PW->pw_dir is not set and its uid is the current user, call `android_get_home_directory'. (get_homedir): Call `android_get_home_directory' if PW->pw_dir is not set. (Fcopy_file, Fmake_directory_internal, Fdelete_directory_internal) (Fdelete_file, Frename_file, Fmake_symbolic_link, Faccess_file) (file_directory_p, file_accessible_directory_p, Fset_file_modes) (Fset_file_times, Ffile_newer_than_file_p, read_non_regular) (Finsert_file_contents, write_region) (Fverify_visited_file_modtime, Fset_visited_file_modtime) (do_auto_save_unwind): Make use of Android filesystem wrappers and file descriptors where possible. (Fadd_name_to_file): Prohibit creating links to and from files residing on Android special directories. (Ffile_system_info): Avoid compilation failure on Android, where Gnulib can't find out how to implement statfs. * src/epaths.in [HAVE_ANDROID && !ANDROID_STUBIFY]: Deface this file, so Makefile cannot change the hard-coded values within. * src/emacs.c (using_utf8): Correctly initialize mbstate_t on Android. (init_cmdargs): Pass extra argument to openp. (load_pdump): When building libemacs.so, use solely the file provided on the command line or as an argument to `android_emacs_init'. (load_seccomp): Call sys_fstat, not fstat. (main, android_emacs_init): Name `main' `android_emacs_init' when building libemacs.so, and accept an argument designating the dump file. (main): Initialize text conversion and Android. Don't presume that argv is NULL terminated. (Fkill_emacs, shut_down_emacs): Properly implement RESTART on Android. (syms_of_emacs) <Vsystem_type>: Describe the possible value `android'. * src/emacs-module.c (MODULE_HANDLE_NONLOCAL_EXIT): Cease relying on GCC clean-up attribute extension. (MODULE_INTERNAL_CLEANUP): New macro. (module_make_global_ref, module_free_global_ref) (module_make_function, module_get_function_finalizer) (module_make_interactive, module_funcall, module_extract_integer) (module_extract_float, module_copy_string_contents) (module_get_user_ptr, module_set_user_ptr) (module_get_user_finalizer, module_set_user_finalizer) (module_vec_set, module_vec_size, module_process_input) (module_extract_big_integer, module_make_big_integer): Carry out necessary clean-up tasks using MODULE_HANDLE_NONLOCAL_EXIT. * src/editfns.c (Fuser_full_name): Call `android_user_full_name', as USER_FULL_NAME doesn't always work. * src/doc.c (doc_fd, doc_fd_p, doc_open, doc_read_quit) (doc_lseek): New types and macros, resembling those in lread.c. (get_doc_string, Fsnarf_documentation): Implement in terms of those macros, so as to use Android asset streams directly. * src/dispnew.c (clear_current_matrices, clear_desired_matrices) (allocate_matrices_for_window_redisplay, free_glyphs) (redraw_frame, update_frame, scrolling, update_frame_line): Disable support for text terminals when building for Android. (Fopen_termscript): Use emacs_fclose. (init_display_interactive): Set Vinitial_window_system to Qandroid, and lose if Emacs needs to create a text terminal. * src/dispextern.h (No_Cursor, Emacs_Rectangle, struct gui_box): New definitions. (struct glyph_string) <gc>: Define to the Android GC type. (HAVE_NATIVE_TRANSPHORMS): Define on Android. (struct image): New fields `ximg', `mask_img', as on X. (enum tool_bar_item_idx): New tool bar item property TOOL_BAR_ITEM_WRAP. * src/dired.c (emacs_dir, emacs_closedir, emacs_readdir): New typedef and definitions. (open_directory): Return emacs_dir; use android_opendir on Android, instead of at-funcs. (directory_files_internal_unwind): Call emacs_closedir. (read_dirent): Call emacs_readdir. (directory_files_internal, file_name_completion) (file_name_completion_dirp): Use Android wrappers for directories and files. (file_attributes): Abstain from openat on Android. * src/conf_post.h (MB_CUR_MAX): Define to REPLACEMENT_MB_CUR_MAX if necessary to counteract inept LLVM headers. * src/coding.h (from_unicode_buffer): Define if HAVE_ANDROID as well. * src/coding.c (from_unicode_buffer): Define on Android, creating a variant that understands UCS-16 extended into wchar_t. (syms_of_coding) <Qutf_16le>: Define on Android. * src/charset.c (load_charset_map_from_file): Supply extra argument to openp, and call Emacs wrappers for fdopen and fclose. * src/callproc.c (get_current_directory): Return the home directory if ENCODED is a special directory. (delete_temp_file): Call emacs_unlink in lieu of unlink. (call_process): Use openp. (emacs_spawn): Use Android executable loader binary if needed and enabled. (init_callproc): Set Vshell_file_name to /system/bin/sh if libemacs.so. (syms_of_callproc) <Vctags_program_name, Vetags_program_name, Vhexl_program_name, Vemacsclient_program_name, Vmovemail_program_name>: New variables. Define to the names of the programs they respectively stand for. * src/callint.c (Fcall_interactively): Supply new argument in calls to Fread_key_sequence and Fread_key_sequence_vector. * src/buffer.h (struct buffer) <text_conversion_style_>: New bvar. (bset_text_conversion_style): New bvar setter. * src/buffer.c (init_buffer_once): Set the text conversion style. (syms_of_buffer) <BVAR (current_buffer, text_conversion_style)>: Define new BLV. * src/androidvfs.c: * src/androidterm.h: * src/androidterm.c: * src/androidselect.c: * src/androidmenu.c: * src/androidgui.h: * src/androidfont.c: * src/androidfns.c: * src/android.h: * src/android.c: * src/android-emacs.c: * src/android-asset.h: New function. * src/alloc.c (cleanup_vector): Finalize Android font entities. (find_string_data_in_pure) [__i386__ && !__clang__]: On Android, compensate for a bug in the latest NDK GCC. (mark_pinned_symbols, android_make_lisp_symbol): Elude another bug in debuginfo generation with an almost nonsensical fix. (garbage_collect): Mark androidterm and sfntfont. (mark_frame): Mark text conversion actions and info. * src/Makefile.in (XCONFIGURE): New variable. If set, add srcdir to vpath. (hostlib): New variable, always defined to libgnu.a on the build machine. (GIF_CFLAGS, JPEG_CFLAGS, TIFF_CFLAGS, SQLITE3_CFLAGS) (LIBSELINUX_CFLAGS, ANDROID_OBJ, ANDROID_LIBS, ANDROID_LDFLAGS) (ANDROID_BUILD_CFLAGS, LIBGMP_CFLAGS): New variables. (CM_OBJ): Update commentary. (EMACS_CFLAGS): Add new compiler flags variables. (base_obj): Add ANDROID_OBJ. (SOME_MACHINE_OBJECTS): Add Android-related objects. (lisp.mk): Generate from its absolute file name. ($(lispsource)/international/charprop.el): Don't generate when building libemacs.so. ($(libsrc)/make-docfile$(EXEEXT) $(libsrc)/make-fingerprint$(EXEEXT)): Depend on libgnu.a on the build machine. (mostlyclean): Remove libemacs.so. (build-counter.c, libemacs.so, android-emacs): New targets. These targets are made from this Makefile copied to a subdirectory of `cross', and provide the Emacs library and an ancillary binary used by the Android port. * nt/mingw-cfg.site: * nt/gnulib-cfg.mk: Impede building Gnulib's vasnprintf* code. * msdos/sedlibmk.inp: * msdos/sedlibcf.inp: * msdos/sed3v2.inp: * msdos/sed1v2.inp: Fix the DJGPP build. * make-dist (possibly_non_vc_files): Add exec/configure and exec/config.h.in. * m4/ndk-build.m4: New file. * m4/getline.m4: * m4/getdelim.m4: * m4/asm-underscore.m4: Update from Gnulib. * lisp/wid-edit.el (widget-event-point): Treat touch screen events correctly. (widget-keymap): Map touchscreen-begin to widget-button-click. (widget-event-start): New function. (widget-button--check-and-call-button): (widget-button-click): Behave correctly when confronted by touch screen events. * lisp/version.el (android-read-build-system) (android_read_build_time): New functions. (emacs-build-system, emacs-repository-version-android) (emacs-repository-get-version): (emacs-repository-get-branch): Implement properly on Android, by reading a file generated during the packaging process. * lisp/touch-screen.el: New file, supplying support for translating raw touch screen events into gestures. * lisp/tool-bar.el (secondary-tool-bar-map): New defvar. (tool-bar--cache-key, tool-bar--secondary-cache-key): Make defsubsts. (tool-bar--flush-key): Flush caches for the secondary tool bar as well. (tool-bar-make-keymap, tool-bar-make-keymap-1): Append the secondary tool bar map below the primary tool bar map. (modifier-bar-modifier-list): New variable. (tool-bar-apply-modifiers, modifier-bar-button) (tool-bar-event-apply-alt-modifier) (tool-bar-event-apply-super-modifier) (tool-bar-event-apply-hyper-modifier) (tool-bar-event-apply-shift-modifier) (tool-bar-event-apply-control-modifier) (tool-bar-event-apply-meta-modifier, modifier-bar-available-p) (modifier-bar-mode): New functions. * lisp/textmodes/text-mode.el (text-mode): Set text-conversion-style to t. * lisp/textmodes/reftex-global.el (reftex-create-tags-file): Use etags-program-name to provide the name of the etags program. * lisp/textmodes/conf-mode.el (conf-mode-initialize): Enable text conversion. * lisp/textmodes/artist.el (artist-figlet-get-font-list): Use /system/bin/sh on Android. * lisp/term/android-win.el: New file. * lisp/term.el (term-mode): Always display the on screen keyboard. (term-exec-1): Use /system/bin/sh on Android. * lisp/tab-line.el (tab-line-tab-map) (tab-line-new-tab) (tab-line-select-tab) (tab-line-close-tab) (tab-line-track-tap) (tab-line-event-start): Improve support for touch screen events. * lisp/tab-bar.el (tab-bar-mouse-context-menu): (tab-bar-map): Likewise. (tab-bar-handle-timeout, tab-bar-touchscreen-begin): New functions. * lisp/subr.el (event-start): Don't return nonsense if EVENT is a touchscreen event. (event-end): Likewise. (read-key): Disable text conversion within read-key-sequence-vector. (read-char-choice-with-read-key): Display the on screen keyboard. (read-char-from-minibuffer): Disable text conversion. (use-dialog-box-p): Prefer dialog boxes on Android. (y-or-n-p): Disable text conversion properly under all three modes of operation. * lisp/startup.el (android-fonts-enumerated): New variable. (normal-top-level): Load system fonts on Android. * lisp/speedbar.el (speedbar-fetch-etags-command): Use etags-program-name instead of hard-coding `etags'. * lisp/simple.el (normal-erase-is-backspace-setup-frame): Return true on Android. (event-apply-modifier): Correctly apply Shift and Control modifiers to keys with other modifiers. (undo-auto-amalgamate): Mention analyze-text-conversion wrt being an amalgamating command. * lisp/shell.el (shell--command-completion-data): Don't lose if PATH contains an inaccessible directory. * lisp/progmodes/prog-mode.el (prog-mode): Enable text conversion. * lisp/progmodes/cperl-mode.el (cperl-etags): Don't hard-code etags, employ etags-program-name instead. * lisp/progmodes/cc-mode.el (c-initialize-cc-mode): Initialize text conversion hook. * lisp/progmodes/cc-cmds.el (c-post-text-conversion): New function. Do electric characters. * lisp/play/gamegrid.el (gamegrid-setup-default-font): Don't crash if the display resolution is too high. * lisp/play/dunnet.el (text-conversion-style): * lisp/play/doctor.el (doctor-mode): Enable text conversion. * lisp/pixel-scroll.el (pixel-scroll-precision-scroll-down-page) (pixel-scroll-precision-scroll-Up-page): Make autoloads. * lisp/org/org-ctags.el (org-ctags-path-to-ctags): Use ctags-program-name, not ctags. * lisp/obsolete/terminal.el (terminal-emulator): Start /system/bin/sh, not /bin/sh. * lisp/net/tramp.el (tramp-encoding-shell): Use /system/bin/sh on Android. * lisp/net/eww.el (eww-form-submit, eww-form-file) (eww-form-checkbox, eww-form-select): Define these faces on Android as well. * lisp/net/browse-url.el (browse-url-default-browser) (browse-url--browser-defcustom-type): Specify on Android. (browse-url-android-share, browse-url-default-android-browser): New option and function. * lisp/mwheel.el (mouse-wheel-down-event, mouse-wheel-up-event) (mouse-wheel-left-event, mouse-wheel-right-event): Define suitably on Android. * lisp/mouse.el (minor-mode-menu-from-indicator): New argument EVENT. Use it for positioning the menu. (mouse-minor-mode-menu): Pass EVENT to that function. * lisp/minibuffer.el (clear-minibuffer-message): Don't clear the message if `touch-screen-preview-select' may be underway. (minibuffer-mode): Enable text conversion. (minibuffer-setup-on-screen-keyboard) (minibuffer-exit-on-screen-keyboard): New functions. * lisp/menu-bar.el (menu-bar-close-window): New option. (menu-bar-edit-menu): Bind execute-extended-command to a menu item. (kill-this-buffer, kill-this-buffer-enabled-p): Respect menu-bar-close-window. * lisp/mail/rmail.el (rmail-autodetect, rmail-insert-inbox-text): Don't hard-code the name of movemail; rather, use movemail-program-name. * lisp/mail/emacsbug.el (emacs-build-description): Insert the Android version and manufacturer. * lisp/ls-lisp.el (ls-lisp-use-insert-directory-program): Default to off on Android. * lisp/loadup.el: Set load-list to empty load list after startup; dump the first time Emacs starts, and load Android related miscellanea. * lisp/isearch.el (isearch-text-conversion-style): New variable. (isearch-mode, isearch-done): Display the OSK, then temporarily disable and restore the on screen keyboard. * lisp/international/mule-cmds.el (set-coding-system-map): Update menu definition for Android. * lisp/international/fontset.el (script-representative-chars) (setup-default-fontset): Improve detection of CJK fonts. * lisp/image/wallpaper.el: Fix compiler warning. * lisp/ielm.el (inferior-emacs-lisp-mode): Don't hard-code name of hexl, replacing that with hexl-program-name. * lisp/htmlfontify.el (hfy-etags-bin): Replace hard-coded Emacs with etags-program-name. * lisp/hexl.el (hexl-program): Replace hard-coded hexl. * lisp/help-macro.el (make-help-screen): Display the on screen keyboard and disable text conversion prior to reading options. * lisp/gnus/mail-source.el (mail-source-movemail-program): Replace hard-coded movemail with movemail-program-name. * lisp/gnus/gnus-score.el (gnus-read-char): New function. (gnus-summary-increase-score): Use a dialog box to display these options on Android. * 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-popup-menus-p, display-graphic-p, display-symbol-keys-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, display-monitor-attributes-list): Implement window system specific functions on Android. * lisp/files.el (basic-save-buffer): Allow files to exist without a parent directory. * lisp/faces.el (tool-bar): Use default definition on Android. * lisp/emacs-lisp/eldoc.el (eldoc-add-command-completions): Add touch-screen-handle-touch and analyze-text-conversion. * lisp/elec-pair.el (electric-pair-analyze-conversion): New function. * lisp/doc-view.el (doc-view-menu): Improve menu. (doc-view-tool-bar-map): Add a new tool bar for Doc View. (doc-view-new-search): New command. (doc-view-mode): Enable that new tool bar. * lisp/dired-aux.el (dired-do-chxxx, dired-do-chmod) (dired-do-print, dired-do-shell-command, dired-do-compress-to) (dired-do-create-files, dired-do-rename, dired-do-isearch) (dired-do-isearch-regexp, dired-do-search) (dired-do-query-replace-regexp, dired-do-find-regexp) (dired-vc-next-action): Disable ``click to select'' after running this command. * lisp/dired.el (dired-insert-set-properties): Attach click-to-select keymap to file names if necessary. (dired-mode-map): Bind `touchscreen-hold' to click to select mode. (dired-post-do-command): New function. (dired-do-delete): Call it. (dired-mark-for-click, dired-enable-click-to-select-mode): New functions. (dired-click-to-select-mode): New minor mode. * lisp/cus-edit.el (custom-button-mouse, custom-button-pressed) (custom-display): Define faces to their default values on Android. * lisp/comint.el (comint-mode): Enable text conversion. * lisp/cedet/semantic/db-ebrowse.el (semanticdb-create-ebrowse-database): Replace fixed ebrowse with ebrowse-program-name. * lisp/calc/calc.el (calc-mode): Display the on screen keyboard. (calc): Insist on displaying the on screen keyboard. * lisp/button.el (button-map): Bind touch screen events to push-button. (push-button): Deal with touch screen events. * lisp/bindings.el (cut, paste, cut, text-conversion): New bindings. * lisp/battery.el (battery-status-function): Use `battery-android'. (battery-android): New function. * lib/gnulib.mk.in: * lib/getline.c: * lib/getdelim.c: * lib/Makefile.in: Update from Gnulib. * lib-src/emacsclient.c (decode_options): Set `alt_display' to `android'. * lib-src/asset-directory-tool.c: New file. * lib-src/Makefile.in: Adapt for cross-compilation. * java/res/xml/preferences.xml: * java/res/values/style.xml: * java/res/values/strings.xml: * java/res/values/bool.xml: * java/res/values-v29/style.xml: * java/res/values-v24/bool.xml: * java/res/values-v19/bool.xml: * java/res/values-v14/style.xml: * java/res/values-v11/style.xml: * java/org/gnu/emacs/EmacsWindowAttachmentManager.java: * java/org/gnu/emacs/EmacsWindow.java: * java/org/gnu/emacs/EmacsView.java: * java/org/gnu/emacs/EmacsThread.java: * java/org/gnu/emacs/EmacsSurfaceView.java: * java/org/gnu/emacs/EmacsService.java: * java/org/gnu/emacs/EmacsSdk8Clipboard.java: * java/org/gnu/emacs/EmacsSdk7FontDriver.java: * java/org/gnu/emacs/EmacsSdk23FontDriver.java: * java/org/gnu/emacs/EmacsSdk11Clipboard.java: * java/org/gnu/emacs/EmacsSafThread.java: * java/org/gnu/emacs/EmacsPreferencesActivity.java: * java/org/gnu/emacs/EmacsPixmap.java: * java/org/gnu/emacs/EmacsOpenActivity.java: * java/org/gnu/emacs/EmacsNoninteractive.java: * java/org/gnu/emacs/EmacsNative.java: * java/org/gnu/emacs/EmacsMultitaskActivity.java: * java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java: * java/org/gnu/emacs/EmacsInputConnection.java: * java/org/gnu/emacs/EmacsHolder.java: * java/org/gnu/emacs/EmacsHandleObject.java: * java/org/gnu/emacs/EmacsGC.java: * java/org/gnu/emacs/EmacsFontDriver.java: * java/org/gnu/emacs/EmacsFillRectangle.java: * java/org/gnu/emacs/EmacsFillPolygon.java: * java/org/gnu/emacs/EmacsDrawable.java: * java/org/gnu/emacs/EmacsDrawRectangle.java: * java/org/gnu/emacs/EmacsDrawPoint.java: * java/org/gnu/emacs/EmacsDrawLine.java: * java/org/gnu/emacs/EmacsDocumentsProvider.java: * java/org/gnu/emacs/EmacsDirectoryEntry.java: * java/org/gnu/emacs/EmacsDialogButtonLayout.java: * java/org/gnu/emacs/EmacsDialog.java: * java/org/gnu/emacs/EmacsCursor.java: * java/org/gnu/emacs/EmacsContextMenu.java: * java/org/gnu/emacs/EmacsClipboard.java: * java/org/gnu/emacs/EmacsApplication.java: * java/org/gnu/emacs/EmacsActivity.java: * java/debug.sh: * java/README: * java/Makefile.in: * java/INSTALL: * java/AndroidManifest.xml.in: * exec/trace.c: * exec/test.c: * exec/mipsfpu.h: * exec/mipsfpu.c: * exec/mipsel-user.h: * exec/loader-x86_64.s: * exec/loader-x86.s: * exec/loader-mipsel.s: * exec/loader-mips64el.s: * exec/loader-armeabi.s: * exec/loader-aarch64.s: * exec/install-sh: * exec/exec1.c: * exec/exec.h: * exec/exec.c: * exec/deps.mk: * exec/configure.ac: * exec/config.sub: * exec/config.h.in: * exec/config.guess: * exec/config-mips.m4.in: * exec/README: * exec/Makefile.in: * etc/images/last-page.xpm: New files. * etc/PROBLEMS: Expound upon problems with font instructing on Android. * etc/NEWS: Announce changes. * etc/MACHINES: Describe support for Android. * etc/DEBUG: Illustrate the steps to debug Emacs on Android. * doc/lispref/processes.texi (Subprocess Creation): * doc/lispref/os.texi (System Environment): * doc/lispref/keymaps.texi (Translation Keymaps): (Extended Menu Items): (Tool Bar): * doc/lispref/frames.texi (Frames): (Frame Layout): (Font and Color Parameters): (Pop-Up Menus): (Window System Selections): * doc/lispref/elisp.texi (Top): * doc/lispref/display.texi (Defining Faces): (Window Systems): * doc/lispref/commands.texi (Touchscreen Events): (Touchscreen Events): (Misc Events): (Key Sequence Input): * doc/emacs/windows.texi (Tab Line): * doc/emacs/input.texi: * doc/emacs/frames.texi (Tool Bars): (Tab Bars): * doc/emacs/emacs.texi (Top): * doc/emacs/dired.texi (Marks vs Flags): * doc/emacs/android.texi: * doc/emacs/Makefile.in (EMACSSOURCES): Update the documentation to properly describe changes effected. * cross/verbose.mk.android: * cross/ndk-build/ndk-resolve.mk: * cross/ndk-build/ndk-prebuilt-static-library.mk: * cross/ndk-build/ndk-prebuilt-shared-library.mk: * cross/ndk-build/ndk-clear-vars.mk: * cross/ndk-build/ndk-build.mk.in: * cross/ndk-build/ndk-build-static-library.mk: * cross/ndk-build/ndk-build-shared-library.mk: * cross/ndk-build/ndk-build-executable.mk: * cross/ndk-build/README: * cross/ndk-build/Makefile.in: * cross/langinfo.h: * cross/README: * cross/Makefile.in: New files. * configure.ac: Configure Emacs for cross-compilation on Android. * build-aux/ndk-module-extract.awk: * build-aux/ndk-build-helper.mk: * build-aux/ndk-build-helper-4.mk: * build-aux/ndk-build-helper-3.mk: * build-aux/ndk-build-helper-2.mk: * build-aux/ndk-build-helper-1.mk: * build-aux/makecounter.sh: New file. * autogen.sh: Autogen in exec as well. * admin/merge-gnulib (GNULIB_MODULES): Add getline, stpncpy and strnlen. Clean lib. * README: * Makefile.in: * INSTALL: Update for Android. * .dir-locals.el (c-mode): Add a few new types.
Diffstat (limited to 'java/org')
-rw-r--r--java/org/gnu/emacs/EmacsActivity.java481
-rw-r--r--java/org/gnu/emacs/EmacsApplication.java92
-rw-r--r--java/org/gnu/emacs/EmacsClipboard.java47
-rw-r--r--java/org/gnu/emacs/EmacsContextMenu.java393
-rw-r--r--java/org/gnu/emacs/EmacsCursor.java47
-rw-r--r--java/org/gnu/emacs/EmacsDialog.java427
-rw-r--r--java/org/gnu/emacs/EmacsDialogButtonLayout.java152
-rw-r--r--java/org/gnu/emacs/EmacsDirectoryEntry.java33
-rw-r--r--java/org/gnu/emacs/EmacsDocumentsProvider.java578
-rw-r--r--java/org/gnu/emacs/EmacsDrawLine.java79
-rw-r--r--java/org/gnu/emacs/EmacsDrawPoint.java34
-rw-r--r--java/org/gnu/emacs/EmacsDrawRectangle.java120
-rw-r--r--java/org/gnu/emacs/EmacsDrawable.java32
-rw-r--r--java/org/gnu/emacs/EmacsFillPolygon.java80
-rw-r--r--java/org/gnu/emacs/EmacsFillRectangle.java116
-rw-r--r--java/org/gnu/emacs/EmacsFontDriver.java173
-rw-r--r--java/org/gnu/emacs/EmacsGC.java121
-rw-r--r--java/org/gnu/emacs/EmacsHandleObject.java59
-rw-r--r--java/org/gnu/emacs/EmacsHolder.java30
-rw-r--r--java/org/gnu/emacs/EmacsInputConnection.java698
-rw-r--r--java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java31
-rw-r--r--java/org/gnu/emacs/EmacsMultitaskActivity.java29
-rw-r--r--java/org/gnu/emacs/EmacsNative.java316
-rw-r--r--java/org/gnu/emacs/EmacsNoninteractive.java203
-rw-r--r--java/org/gnu/emacs/EmacsOpenActivity.java552
-rw-r--r--java/org/gnu/emacs/EmacsPixmap.java192
-rw-r--r--java/org/gnu/emacs/EmacsPreferencesActivity.java168
-rw-r--r--java/org/gnu/emacs/EmacsSafThread.java1687
-rw-r--r--java/org/gnu/emacs/EmacsSdk11Clipboard.java290
-rw-r--r--java/org/gnu/emacs/EmacsSdk23FontDriver.java114
-rw-r--r--java/org/gnu/emacs/EmacsSdk7FontDriver.java539
-rw-r--r--java/org/gnu/emacs/EmacsSdk8Clipboard.java147
-rw-r--r--java/org/gnu/emacs/EmacsService.java1820
-rw-r--r--java/org/gnu/emacs/EmacsSurfaceView.java223
-rw-r--r--java/org/gnu/emacs/EmacsThread.java82
-rw-r--r--java/org/gnu/emacs/EmacsView.java777
-rw-r--r--java/org/gnu/emacs/EmacsWindow.java1445
-rw-r--r--java/org/gnu/emacs/EmacsWindowAttachmentManager.java230
38 files changed, 12637 insertions, 0 deletions
diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java
new file mode 100644
index 00000000000..4ddf51fbb20
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsActivity.java
@@ -0,0 +1,481 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.lang.IllegalStateException;
23import java.util.List;
24import java.util.ArrayList;
25
26import android.app.Activity;
27
28import android.content.ContentResolver;
29import android.content.Context;
30import android.content.Intent;
31
32import android.os.Build;
33import android.os.Bundle;
34
35import android.util.Log;
36
37import android.net.Uri;
38
39import android.view.Menu;
40import android.view.View;
41import android.view.ViewTreeObserver;
42import android.view.Window;
43import android.view.WindowInsets;
44import android.view.WindowInsetsController;
45
46import android.widget.FrameLayout;
47
48public class EmacsActivity extends Activity
49 implements EmacsWindowAttachmentManager.WindowConsumer,
50 ViewTreeObserver.OnGlobalLayoutListener
51{
52 public static final String TAG = "EmacsActivity";
53
54 /* ID for URIs from a granted document tree. */
55 public static final int ACCEPT_DOCUMENT_TREE = 1;
56
57 /* The currently attached EmacsWindow, or null if none. */
58 private EmacsWindow window;
59
60 /* The frame layout associated with the activity. */
61 private FrameLayout layout;
62
63 /* List of activities with focus. */
64 public static final List<EmacsActivity> focusedActivities;
65
66 /* The last activity to have been focused. */
67 public static EmacsActivity lastFocusedActivity;
68
69 /* The currently focused window. */
70 public static EmacsWindow focusedWindow;
71
72 /* Whether or not this activity is paused. */
73 private boolean isPaused;
74
75 /* Whether or not this activity is fullscreen. */
76 private boolean isFullscreen;
77
78 /* The last context menu to be closed. */
79 private static Menu lastClosedMenu;
80
81 static
82 {
83 focusedActivities = new ArrayList<EmacsActivity> ();
84 };
85
86 public static void
87 invalidateFocus1 (EmacsWindow window)
88 {
89 if (window.view.isFocused ())
90 focusedWindow = window;
91
92 for (EmacsWindow child : window.children)
93 invalidateFocus1 (child);
94 }
95
96 public static void
97 invalidateFocus ()
98 {
99 EmacsWindow oldFocus;
100
101 /* Walk through each focused activity and assign the window focus
102 to the bottom-most focused window within. Record the old focus
103 as well. */
104 oldFocus = focusedWindow;
105 focusedWindow = null;
106
107 for (EmacsActivity activity : focusedActivities)
108 {
109 if (activity.window != null)
110 invalidateFocus1 (activity.window);
111 }
112
113 /* Send focus in- and out- events to the previous and current
114 focus. */
115
116 if (oldFocus != null)
117 EmacsNative.sendFocusOut (oldFocus.handle,
118 System.currentTimeMillis ());
119
120 if (focusedWindow != null)
121 EmacsNative.sendFocusIn (focusedWindow.handle,
122 System.currentTimeMillis ());
123 }
124
125 @Override
126 public final void
127 detachWindow ()
128 {
129 syncFullscreenWith (null);
130
131 if (window == null)
132 Log.w (TAG, "detachWindow called, but there is no window");
133 else
134 {
135 /* Clear the window's pointer to this activity and remove the
136 window's view. */
137 window.setConsumer (null);
138
139 /* The window can't be iconified any longer. */
140 window.noticeDeiconified ();
141 layout.removeView (window.view);
142 window = null;
143
144 invalidateFocus ();
145 }
146 }
147
148 @Override
149 public final void
150 attachWindow (EmacsWindow child)
151 {
152 Log.d (TAG, "attachWindow: " + child);
153
154 if (window != null)
155 throw new IllegalStateException ("trying to attach window when one"
156 + " already exists");
157
158 syncFullscreenWith (child);
159
160 /* Record and attach the view. */
161
162 window = child;
163 layout.addView (window.view);
164 child.setConsumer (this);
165
166 /* If the window isn't no-focus-on-map, focus its view. */
167 if (!child.getDontFocusOnMap ())
168 window.view.requestFocus ();
169
170 /* If the activity is iconified, send that to the window. */
171 if (isPaused)
172 window.noticeIconified ();
173
174 /* Invalidate the focus. */
175 invalidateFocus ();
176 }
177
178 @Override
179 public final void
180 destroy ()
181 {
182 finish ();
183 }
184
185 @Override
186 public final EmacsWindow
187 getAttachedWindow ()
188 {
189 return window;
190 }
191
192 @Override
193 public final void
194 onCreate (Bundle savedInstanceState)
195 {
196 FrameLayout.LayoutParams params;
197 Intent intent;
198 View decorView;
199 ViewTreeObserver observer;
200 int matchParent;
201
202 /* See if Emacs should be started with any extra arguments, such
203 as `--quick'. */
204 intent = getIntent ();
205 EmacsService.extraStartupArgument
206 = intent.getStringExtra ("org.gnu.emacs.STARTUP_ARGUMENT");
207
208 matchParent = FrameLayout.LayoutParams.MATCH_PARENT;
209 params
210 = new FrameLayout.LayoutParams (matchParent,
211 matchParent);
212
213 /* Make the frame layout. */
214 layout = new FrameLayout (this);
215 layout.setLayoutParams (params);
216
217 /* Set it as the content view. */
218 setContentView (layout);
219
220 /* Maybe start the Emacs service if necessary. */
221 EmacsService.startEmacsService (this);
222
223 /* Add this activity to the list of available activities. */
224 EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this);
225
226 /* Start observing global layout changes between Jelly Bean and Q.
227 This is required to restore the fullscreen state whenever the
228 on screen keyboard is displayed, as there is otherwise no way
229 to determine when the on screen keyboard becomes visible. */
230
231 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
232 && Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
233 {
234 decorView = getWindow ().getDecorView ();
235 observer = decorView.getViewTreeObserver ();
236 observer.addOnGlobalLayoutListener (this);
237 }
238
239 super.onCreate (savedInstanceState);
240 }
241
242 @Override
243 public final void
244 onGlobalLayout ()
245 {
246 syncFullscreenWith (window);
247 }
248
249 @Override
250 public final void
251 onDestroy ()
252 {
253 EmacsWindowAttachmentManager manager;
254 boolean isMultitask;
255
256 manager = EmacsWindowAttachmentManager.MANAGER;
257
258 /* The activity will die shortly hereafter. If there is a window
259 attached, close it now. */
260 Log.d (TAG, "onDestroy " + this);
261 isMultitask = this instanceof EmacsMultitaskActivity;
262 manager.removeWindowConsumer (this, isMultitask || isFinishing ());
263 focusedActivities.remove (this);
264 invalidateFocus ();
265
266 /* Remove this activity from the static field, lest it leak. */
267 if (lastFocusedActivity == this)
268 lastFocusedActivity = null;
269
270 super.onDestroy ();
271 }
272
273 @Override
274 public final void
275 onWindowFocusChanged (boolean isFocused)
276 {
277 Log.d (TAG, ("onWindowFocusChanged: "
278 + (isFocused ? "YES" : "NO")));
279
280 if (isFocused && !focusedActivities.contains (this))
281 {
282 focusedActivities.add (this);
283 lastFocusedActivity = this;
284
285 /* Update the window insets as the focus change may have
286 changed the window insets as well, and the system does not
287 automatically restore visibility flags. */
288
289 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
290 && Build.VERSION.SDK_INT < Build.VERSION_CODES.R
291 && isFullscreen)
292 syncFullscreenWith (window);
293 }
294 else
295 focusedActivities.remove (this);
296
297 invalidateFocus ();
298 }
299
300 @Override
301 public final void
302 onPause ()
303 {
304 isPaused = true;
305
306 EmacsWindowAttachmentManager.MANAGER.noticeIconified (this);
307 super.onPause ();
308 }
309
310 @Override
311 public final void
312 onResume ()
313 {
314 isPaused = false;
315
316 EmacsWindowAttachmentManager.MANAGER.noticeDeiconified (this);
317 super.onResume ();
318 }
319
320 @Override
321 public final void
322 onContextMenuClosed (Menu menu)
323 {
324 int serial;
325
326 Log.d (TAG, "onContextMenuClosed: " + menu);
327
328 /* See the comment inside onMenuItemClick. */
329
330 if (((EmacsContextMenu.wasSubmenuSelected == -2)
331 || (EmacsContextMenu.wasSubmenuSelected >= 0
332 && ((System.currentTimeMillis ()
333 - EmacsContextMenu.wasSubmenuSelected)
334 <= 300)))
335 || menu == lastClosedMenu)
336 {
337 EmacsContextMenu.wasSubmenuSelected = -1;
338 lastClosedMenu = menu;
339 return;
340 }
341
342 /* lastClosedMenu is set because Android apparently calls this
343 function twice. */
344
345 lastClosedMenu = null;
346
347 /* Send a context menu event given that no menu item has already
348 been selected. */
349 if (!EmacsContextMenu.itemAlreadySelected)
350 {
351 serial = EmacsContextMenu.lastMenuEventSerial;
352 EmacsNative.sendContextMenu ((short) 0, 0,
353 serial);
354 }
355
356 super.onContextMenuClosed (menu);
357 }
358
359 @SuppressWarnings ("deprecation")
360 public final void
361 syncFullscreenWith (EmacsWindow emacsWindow)
362 {
363 WindowInsetsController controller;
364 Window window;
365 int behavior, flags;
366 View view;
367
368 if (emacsWindow != null)
369 isFullscreen = emacsWindow.fullscreen;
370 else
371 isFullscreen = false;
372
373 /* On Android 11 or later, use the window insets controller to
374 control whether or not the view is fullscreen. */
375
376 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
377 {
378 window = getWindow ();
379
380 /* If there is no attached window, return immediately. */
381 if (window == null)
382 return;
383
384 behavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
385 controller = window.getInsetsController ();
386 controller.setSystemBarsBehavior (behavior);
387
388 if (isFullscreen)
389 controller.hide (WindowInsets.Type.statusBars ()
390 | WindowInsets.Type.navigationBars ());
391 else
392 controller.show (WindowInsets.Type.statusBars ()
393 | WindowInsets.Type.navigationBars ());
394
395 return;
396 }
397
398 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
399 {
400 /* On Android 4.1 or later, use `setSystemUiVisibility'. */
401
402 window = getWindow ();
403
404 if (window == null)
405 return;
406
407 view = window.getDecorView ();
408
409 if (isFullscreen)
410 {
411 flags = 0;
412 flags |= View.SYSTEM_UI_FLAG_FULLSCREEN;
413
414 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
415 {
416 /* These flags means that Emacs will be full screen as
417 long as the state flag is set. */
418 flags |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
419 flags |= View.SYSTEM_UI_FLAG_IMMERSIVE;
420 flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
421 }
422
423 /* Apply the given flags. */
424 view.setSystemUiVisibility (flags);
425 }
426 else
427 view.setSystemUiVisibility (View.SYSTEM_UI_FLAG_VISIBLE);
428 }
429 }
430
431 @Override
432 public final void
433 onAttachedToWindow ()
434 {
435 super.onAttachedToWindow ();
436
437 /* Update the window insets. */
438 syncFullscreenWith (window);
439 }
440
441
442
443 @Override
444 public final void
445 onActivityResult (int requestCode, int resultCode, Intent data)
446 {
447 ContentResolver resolver;
448 Uri uri;
449 int flags;
450
451 switch (requestCode)
452 {
453 case ACCEPT_DOCUMENT_TREE:
454
455 /* A document granted through
456 EmacsService.requestDirectoryAccess. */
457
458 if (resultCode == RESULT_OK)
459 {
460 resolver = getContentResolver ();
461 uri = data.getData ();
462 flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION
463 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
464
465 try
466 {
467 if (uri != null)
468 resolver.takePersistableUriPermission (uri, flags);
469 }
470 catch (Exception exception)
471 {
472 /* Permission to access URI might've been revoked in
473 between selecting the file and this callback being
474 invoked. Don't crash in such cases. */
475 }
476 }
477
478 break;
479 }
480 }
481};
diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java
new file mode 100644
index 00000000000..8afa5bcedb4
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsApplication.java
@@ -0,0 +1,92 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.io.File;
23import java.io.FileFilter;
24
25import android.content.Context;
26
27import android.app.Application;
28import android.util.Log;
29
30public final class EmacsApplication extends Application
31{
32 private static final String TAG = "EmacsApplication";
33
34 /* The name of the dump file to use. */
35 public static String dumpFileName;
36
37 public static void
38 findDumpFile (Context context)
39 {
40 File filesDirectory;
41 File[] allFiles;
42 String wantedDumpFile;
43 int i;
44
45 wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint ()
46 + ".pdmp");
47
48 /* Obtain a list of all files ending with ``.pdmp''. Then, look
49 for a file named ``emacs-<fingerprint>.pdmp'' and delete the
50 rest. */
51 filesDirectory = context.getFilesDir ();
52
53 allFiles = filesDirectory.listFiles (new FileFilter () {
54 @Override
55 public boolean
56 accept (File file)
57 {
58 return (!file.isDirectory ()
59 && file.getName ().endsWith (".pdmp"));
60 }
61 });
62
63 if (allFiles == null)
64 return;
65
66 /* Now try to find the right dump file. */
67 for (i = 0; i < allFiles.length; ++i)
68 {
69 if (allFiles[i].getName ().equals (wantedDumpFile))
70 dumpFileName = allFiles[i].getAbsolutePath ();
71 else
72 /* Delete this outdated dump file. */
73 allFiles[i].delete ();
74 }
75 }
76
77 @Override
78 public void
79 onCreate ()
80 {
81 /* Block signals which don't interest the current thread and its
82 descendants created by the system. The original signal mask
83 will be restored for the Emacs thread in `initEmacs'. */
84 EmacsNative.setupSystemThread ();
85
86 /* Locate a suitable dump file. */
87 findDumpFile (this);
88
89 /* Start the rest of the application. */
90 super.onCreate ();
91 }
92};
diff --git a/java/org/gnu/emacs/EmacsClipboard.java b/java/org/gnu/emacs/EmacsClipboard.java
new file mode 100644
index 00000000000..5cd48af6e3a
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsClipboard.java
@@ -0,0 +1,47 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.os.Build;
23
24/* This class provides helper code for accessing the clipboard,
25 abstracting between the different interfaces on API 8 and 11. */
26
27public abstract class EmacsClipboard
28{
29 public abstract void setClipboard (byte[] bytes);
30 public abstract int ownsClipboard ();
31 public abstract boolean clipboardExists ();
32 public abstract byte[] getClipboard ();
33
34 public abstract byte[][] getClipboardTargets ();
35 public abstract long[] getClipboardData (byte[] target);
36
37 /* Create the correct kind of clipboard for this system. */
38
39 public static EmacsClipboard
40 makeClipboard ()
41 {
42 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
43 return new EmacsSdk11Clipboard ();
44 else
45 return new EmacsSdk8Clipboard ();
46 }
47};
diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java
new file mode 100644
index 00000000000..46eddeeda3d
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsContextMenu.java
@@ -0,0 +1,393 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.util.List;
23import java.util.ArrayList;
24
25import android.content.Context;
26import android.content.Intent;
27
28import android.os.Build;
29
30import android.view.ContextMenu;
31import android.view.Menu;
32import android.view.MenuItem;
33import android.view.View;
34import android.view.SubMenu;
35
36import android.util.Log;
37
38/* Context menu implementation. This object is built from JNI and
39 describes a menu hiearchy. Then, `inflate' can turn it into an
40 Android menu, which can be turned into a popup (or other kind of)
41 menu. */
42
43public final class EmacsContextMenu
44{
45 private static final String TAG = "EmacsContextMenu";
46
47 /* Whether or not an item was selected. */
48 public static boolean itemAlreadySelected;
49
50 /* Whether or not a submenu was selected.
51 Value is -1 if no; value is -2 if yes, and a context menu
52 close event will definitely be sent. Any other value is
53 the timestamp when the submenu was selected. */
54 public static long wasSubmenuSelected;
55
56 /* The serial ID of the last context menu to be displayed. */
57 public static int lastMenuEventSerial;
58
59 /* The last group ID used for a menu item. */
60 public int lastGroupId;
61
62 private static final class Item implements MenuItem.OnMenuItemClickListener
63 {
64 public int itemID;
65 public String itemName, tooltip;
66 public EmacsContextMenu subMenu;
67 public boolean isEnabled, isCheckable, isChecked;
68 public EmacsView inflatedView;
69 public boolean isRadio;
70
71 @Override
72 public boolean
73 onMenuItemClick (MenuItem item)
74 {
75 Log.d (TAG, "onMenuItemClick: " + itemName + " (" + itemID + ")");
76
77 if (subMenu != null)
78 {
79 /* Android 6.0 and earlier don't support nested submenus
80 properly, so display the submenu popup by hand. */
81
82 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
83 {
84 Log.d (TAG, "onMenuItemClick: displaying submenu " + subMenu);
85
86 /* Still set wasSubmenuSelected -- if not set, the
87 dismissal of this context menu will result in a
88 context menu event being sent. */
89 wasSubmenuSelected = -2;
90
91 /* Running a popup menu from inside a click handler
92 doesn't work, so make sure it is displayed
93 outside. */
94
95 inflatedView.post (new Runnable () {
96 @Override
97 public void
98 run ()
99 {
100 inflatedView.popupMenu (subMenu, 0, 0, true);
101 }
102 });
103
104 return true;
105 }
106
107 /* After opening a submenu within a submenu, Android will
108 send onContextMenuClosed for a ContextMenuBuilder. This
109 will normally confuse Emacs into thinking that the
110 context menu has been dismissed. Wrong!
111
112 Setting this flag makes EmacsActivity to only handle
113 SubMenuBuilder being closed, which always means the menu
114 has actually been dismissed.
115
116 However, these extraneous events aren't sent on devices
117 where submenus display without dismissing their parents.
118 Thus, only ignore the close event if it happens within
119 300 milliseconds of the submenu being selected. */
120 wasSubmenuSelected = System.currentTimeMillis ();
121 return false;
122 }
123
124 /* Send a context menu event. */
125 EmacsNative.sendContextMenu ((short) 0, itemID,
126 lastMenuEventSerial);
127
128 /* Say that an item has already been selected. */
129 itemAlreadySelected = true;
130 return true;
131 }
132 };
133
134 /* List of menu items contained in this menu. */
135 public List<Item> menuItems;
136
137 /* The parent context menu, or NULL if none. */
138 private EmacsContextMenu parent;
139
140 /* The title of this context menu, or NULL if none. */
141 private String title;
142
143
144
145 /* Create a context menu with no items inside and the title TITLE,
146 which may be NULL. */
147
148 public static EmacsContextMenu
149 createContextMenu (String title)
150 {
151 EmacsContextMenu menu;
152
153 menu = new EmacsContextMenu ();
154 menu.title = title;
155 menu.menuItems = new ArrayList<Item> ();
156
157 return menu;
158 }
159
160 /* Add a normal menu item to the context menu with the id ITEMID and
161 the name ITEMNAME. Enable it if ISENABLED, else keep it
162 disabled.
163
164 If this is not a submenu and ISCHECKABLE is set, make the item
165 checkable. Likewise, if ISCHECKED is set, make the item
166 checked.
167
168 If TOOLTIP is non-NULL, set the menu item tooltip to TOOLTIP.
169
170 If ISRADIO, then display the check mark as a radio button. */
171
172 public void
173 addItem (int itemID, String itemName, boolean isEnabled,
174 boolean isCheckable, boolean isChecked,
175 String tooltip, boolean isRadio)
176 {
177 Item item;
178
179 item = new Item ();
180 item.itemID = itemID;
181 item.itemName = itemName;
182 item.isEnabled = isEnabled;
183 item.isCheckable = isCheckable;
184 item.isChecked = isChecked;
185 item.tooltip = tooltip;
186 item.isRadio = isRadio;
187
188 menuItems.add (item);
189 }
190
191 /* Create a disabled menu item with the name ITEMNAME. */
192
193 public void
194 addPane (String itemName)
195 {
196 Item item;
197
198 item = new Item ();
199 item.itemName = itemName;
200
201 menuItems.add (item);
202 }
203
204 /* Add a submenu to the context menu with the specified title and
205 item name. */
206
207 public EmacsContextMenu
208 addSubmenu (String itemName, String tooltip)
209 {
210 EmacsContextMenu submenu;
211 Item item;
212
213 item = new Item ();
214 item.itemID = 0;
215 item.itemName = itemName;
216 item.tooltip = tooltip;
217 item.subMenu = createContextMenu (itemName);
218 item.subMenu.parent = this;
219
220 menuItems.add (item);
221 return item.subMenu;
222 }
223
224 /* Add the contents of this menu to MENU. Assume MENU will be
225 displayed in INFLATEDVIEW. */
226
227 private void
228 inflateMenuItems (Menu menu, EmacsView inflatedView)
229 {
230 Intent intent;
231 MenuItem menuItem;
232 SubMenu submenu;
233
234 for (Item item : menuItems)
235 {
236 if (item.subMenu != null)
237 {
238 /* This is a submenu. On versions of Android which
239 support doing so, create the submenu and add the
240 contents of the menu to it.
241
242 Note that Android 4.0 and later technically supports
243 having multiple layers of nested submenus, but if they
244 are used, onContextMenuClosed becomes unreliable. */
245
246 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
247 {
248 submenu = menu.addSubMenu (item.itemName);
249 item.subMenu.inflateMenuItems (submenu, inflatedView);
250
251 /* This is still needed to set wasSubmenuSelected. */
252 menuItem = submenu.getItem ();
253 }
254 else
255 menuItem = menu.add (item.itemName);
256
257 item.inflatedView = inflatedView;
258 menuItem.setOnMenuItemClickListener (item);
259 }
260 else
261 {
262 if (item.isRadio)
263 menuItem = menu.add (++lastGroupId, Menu.NONE, Menu.NONE,
264 item.itemName);
265 else
266 menuItem = menu.add (item.itemName);
267 menuItem.setOnMenuItemClickListener (item);
268
269 /* If the item ID is zero, then disable the item. */
270 if (item.itemID == 0 || !item.isEnabled)
271 menuItem.setEnabled (false);
272
273 /* Now make the menu item display a checkmark as
274 appropriate. */
275
276 if (item.isCheckable)
277 menuItem.setCheckable (true);
278
279 if (item.isChecked)
280 menuItem.setChecked (true);
281
282 /* Define an exclusively checkable group if the item is a
283 radio button. */
284
285 if (item.isRadio)
286 menu.setGroupCheckable (lastGroupId, true, true);
287
288 /* If the tooltip text is set and the system is new enough
289 to support menu item tooltips, set it on the item. */
290
291 if (item.tooltip != null
292 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
293 menuItem.setTooltipText (item.tooltip);
294 }
295 }
296 }
297
298 /* Enter the items in this context menu to MENU.
299 Assume that MENU will be displayed in VIEW; this may lead to
300 popupMenu being called on VIEW if a submenu is selected.
301
302 If MENU is a ContextMenu, set its header title to the one
303 contained in this object. */
304
305 public void
306 expandTo (Menu menu, EmacsView view)
307 {
308 inflateMenuItems (menu, view);
309
310 /* See if menu is a ContextMenu and a title is set. */
311 if (title == null || !(menu instanceof ContextMenu))
312 return;
313
314 /* Set its title to this.title. */
315 ((ContextMenu) menu).setHeaderTitle (title);
316 }
317
318 /* Return the parent or NULL. */
319
320 public EmacsContextMenu
321 parent ()
322 {
323 return this.parent;
324 }
325
326 /* Like display, but does the actual work and runs in the main
327 thread. */
328
329 private boolean
330 display1 (EmacsWindow window, int xPosition, int yPosition)
331 {
332 /* Set this flag to false. It is used to decide whether or not to
333 send 0 in response to the context menu being closed. */
334 itemAlreadySelected = false;
335
336 /* No submenu has been selected yet. */
337 wasSubmenuSelected = -1;
338
339 return window.view.popupMenu (this, xPosition, yPosition,
340 false);
341 }
342
343 /* Display this context menu on WINDOW, at xPosition and yPosition.
344 SERIAL is a number that will be returned in any menu event
345 generated to identify this context menu. */
346
347 public boolean
348 display (final EmacsWindow window, final int xPosition,
349 final int yPosition, final int serial)
350 {
351 Runnable runnable;
352 final EmacsHolder<Boolean> rc;
353
354 rc = new EmacsHolder<Boolean> ();
355 rc.thing = false;
356
357 runnable = new Runnable () {
358 @Override
359 public void
360 run ()
361 {
362 synchronized (this)
363 {
364 lastMenuEventSerial = serial;
365 rc.thing = display1 (window, xPosition, yPosition);
366 notify ();
367 }
368 }
369 };
370
371 EmacsService.syncRunnable (runnable);
372 return rc.thing;
373 }
374
375 /* Dismiss this context menu. WINDOW is the window where the
376 context menu is being displayed. */
377
378 public void
379 dismiss (final EmacsWindow window)
380 {
381 Runnable runnable;
382
383 EmacsService.SERVICE.runOnUiThread (new Runnable () {
384 @Override
385 public void
386 run ()
387 {
388 window.view.cancelPopupMenu ();
389 itemAlreadySelected = false;
390 }
391 });
392 }
393};
diff --git a/java/org/gnu/emacs/EmacsCursor.java b/java/org/gnu/emacs/EmacsCursor.java
new file mode 100644
index 00000000000..c14c6f2a11b
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsCursor.java
@@ -0,0 +1,47 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.view.PointerIcon;
23import android.os.Build;
24
25/* Cursor wrapper. Note that pointer icons are not supported prior to
26 Android 24. */
27
28public final class EmacsCursor extends EmacsHandleObject
29{
30 /* The pointer icon associated with this cursor. */
31 public final PointerIcon icon;
32
33 public
34 EmacsCursor (short handle, int glyph)
35 {
36 super (handle);
37
38 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
39 {
40 icon = null;
41 return;
42 }
43
44 icon = PointerIcon.getSystemIcon (EmacsService.SERVICE,
45 glyph);
46 }
47};
diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java
new file mode 100644
index 00000000000..e4ed2271741
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDialog.java
@@ -0,0 +1,427 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.util.List;
23import java.util.ArrayList;
24
25import android.app.AlertDialog;
26
27import android.content.Context;
28import android.content.DialogInterface;
29
30import android.content.res.Resources.NotFoundException;
31import android.content.res.Resources.Theme;
32import android.content.res.TypedArray;
33
34import android.os.Build;
35
36import android.provider.Settings;
37
38import android.util.Log;
39
40import android.widget.Button;
41import android.widget.LinearLayout;
42import android.widget.FrameLayout;
43
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.Window;
47import android.view.WindowManager;
48
49/* Toolkit dialog implementation. This object is built from JNI and
50 describes a single alert dialog. Then, `inflate' turns it into
51 AlertDialog. */
52
53public final class EmacsDialog implements DialogInterface.OnDismissListener
54{
55 private static final String TAG = "EmacsDialog";
56
57 /* List of buttons in this dialog. */
58 private List<EmacsButton> buttons;
59
60 /* Dialog title. */
61 private String title;
62
63 /* Dialog text. */
64 private String text;
65
66 /* Whether or not a selection has already been made. */
67 private boolean wasButtonClicked;
68
69 /* Dialog to dismiss after click. */
70 private AlertDialog dismissDialog;
71
72 /* The menu serial associated with this dialog box. */
73 private int menuEventSerial;
74
75 private final class EmacsButton implements View.OnClickListener,
76 DialogInterface.OnClickListener
77 {
78 /* Name of this button. */
79 public String name;
80
81 /* ID of this button. */
82 public int id;
83
84 /* Whether or not the button is enabled. */
85 public boolean enabled;
86
87 @Override
88 public void
89 onClick (View view)
90 {
91 Log.d (TAG, "onClicked " + this);
92
93 wasButtonClicked = true;
94 EmacsNative.sendContextMenu ((short) 0, id, menuEventSerial);
95 dismissDialog.dismiss ();
96 }
97
98 @Override
99 public void
100 onClick (DialogInterface dialog, int which)
101 {
102 Log.d (TAG, "onClicked " + this);
103
104 wasButtonClicked = true;
105 EmacsNative.sendContextMenu ((short) 0, id, menuEventSerial);
106 }
107 };
108
109 /* Create a popup dialog with the title TITLE and the text TEXT.
110 TITLE may be NULL. MENUEVENTSERIAL is a number which will
111 identify this popup dialog inside events it sends. */
112
113 public static EmacsDialog
114 createDialog (String title, String text, int menuEventSerial)
115 {
116 EmacsDialog dialog;
117
118 dialog = new EmacsDialog ();
119 dialog.buttons = new ArrayList<EmacsButton> ();
120 dialog.title = title;
121 dialog.text = text;
122 dialog.menuEventSerial = menuEventSerial;
123
124 return dialog;
125 }
126
127 /* Add a button named NAME, with the identifier ID. If DISABLE,
128 disable the button. */
129
130 public void
131 addButton (String name, int id, boolean disable)
132 {
133 EmacsButton button;
134
135 button = new EmacsButton ();
136 button.name = name;
137 button.id = id;
138 button.enabled = !disable;
139 buttons.add (button);
140 }
141
142 /* Turn this dialog into an AlertDialog for the specified
143 CONTEXT.
144
145 Upon a button being selected, the dialog will send an
146 ANDROID_CONTEXT_MENU event with the id of that button.
147
148 Upon the dialog being dismissed, an ANDROID_CONTEXT_MENU event
149 will be sent with an id of 0. */
150
151 public AlertDialog
152 toAlertDialog (Context context)
153 {
154 AlertDialog dialog;
155 int size, styleId, flag;
156 int[] attrs;
157 EmacsButton button;
158 EmacsDialogButtonLayout layout;
159 Button buttonView;
160 ViewGroup.LayoutParams layoutParams;
161 Theme theme;
162 TypedArray attributes;
163 Window window;
164
165 size = buttons.size ();
166 styleId = -1;
167
168 if (size <= 3)
169 {
170 dialog = new AlertDialog.Builder (context).create ();
171 dialog.setMessage (text);
172 dialog.setCancelable (true);
173 dialog.setOnDismissListener (this);
174
175 if (title != null)
176 dialog.setTitle (title);
177
178 /* There are less than 4 buttons. Add the buttons the way
179 Android intends them to be added. */
180
181 if (size >= 1)
182 {
183 button = buttons.get (0);
184 dialog.setButton (DialogInterface.BUTTON_POSITIVE,
185 button.name, button);
186 }
187
188 if (size >= 2)
189 {
190 button = buttons.get (1);
191 dialog.setButton (DialogInterface.BUTTON_NEGATIVE,
192 button.name, button);
193 }
194
195 if (size >= 3)
196 {
197 button = buttons.get (2);
198 dialog.setButton (DialogInterface.BUTTON_NEUTRAL,
199 button.name, button);
200 }
201 }
202 else
203 {
204 /* There are more than 3 buttons. Add them all to a special
205 container widget that handles wrapping. First, create the
206 layout. */
207
208 layout = new EmacsDialogButtonLayout (context);
209 layoutParams
210 = new FrameLayout.LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT,
211 ViewGroup.LayoutParams.WRAP_CONTENT);
212 layout.setLayoutParams (layoutParams);
213
214 /* Add that layout to the dialog's custom view.
215
216 android.R.id.custom is documented to work. But looking it
217 up returns NULL, so setView must be used instead. */
218
219 dialog = new AlertDialog.Builder (context).setView (layout).create ();
220 dialog.setMessage (text);
221 dialog.setCancelable (true);
222 dialog.setOnDismissListener (this);
223
224 if (title != null)
225 dialog.setTitle (title);
226
227 /* Now that the dialog has been created, set the style of each
228 custom button to match the usual dialog buttons found on
229 Android 5 and later. */
230
231 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
232 {
233 /* Obtain the Theme associated with the dialog. */
234 theme = dialog.getContext ().getTheme ();
235
236 /* Resolve the dialog button style. */
237 attrs
238 = new int [] { android.R.attr.buttonBarNeutralButtonStyle, };
239
240 try
241 {
242 attributes = theme.obtainStyledAttributes (attrs);
243
244 /* Look for the style ID. Default to -1 if it could
245 not be found. */
246 styleId = attributes.getResourceId (0, -1);
247
248 /* Now clean up the TypedAttributes object. */
249 attributes.recycle ();
250 }
251 catch (NotFoundException e)
252 {
253 /* Nothing to do here. */
254 }
255 }
256
257 /* Create each button and add it to the layout. Set the style
258 if necessary. */
259
260 for (EmacsButton emacsButton : buttons)
261 {
262 if (styleId == -1)
263 /* No specific style... */
264 buttonView = new Button (context);
265 else
266 /* Use the given styleId. */
267 buttonView = new Button (context, null, 0, styleId);
268
269 /* Set the text and on click handler. */
270 buttonView.setText (emacsButton.name);
271 buttonView.setOnClickListener (emacsButton);
272 buttonView.setEnabled (emacsButton.enabled);
273 layout.addView (buttonView);
274 }
275 }
276
277 return dialog;
278 }
279
280 /* Internal helper for display run on the main thread. */
281
282 @SuppressWarnings("deprecation")
283 private boolean
284 display1 ()
285 {
286 Context context;
287 int size, type;
288 Button buttonView;
289 EmacsButton button;
290 AlertDialog dialog;
291 Window window;
292
293 if (EmacsActivity.focusedActivities.isEmpty ())
294 {
295 /* If focusedActivities is empty then this dialog may have
296 been displayed immediately after another popup dialog was
297 dismissed. Or Emacs might legitimately be in the
298 background, possibly displaying this popup in response to
299 an Emacsclient request. Try the service context if it will
300 work, then any focused EmacsOpenActivity, and finally the
301 last EmacsActivity to be focused. */
302
303 Log.d (TAG, "display1: no focused activities...");
304 Log.d (TAG, ("display1: EmacsOpenActivity.currentActivity: "
305 + EmacsOpenActivity.currentActivity));
306
307 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
308 || Settings.canDrawOverlays (EmacsService.SERVICE))
309 context = EmacsService.SERVICE;
310 else if (EmacsOpenActivity.currentActivity != null)
311 context = EmacsOpenActivity.currentActivity;
312 else
313 context = EmacsActivity.lastFocusedActivity;
314
315 if (context == null)
316 return false;
317 }
318 else
319 /* Display using the activity context when Emacs is in the
320 foreground, as this allows the dialog to be dismissed more
321 consistently. */
322 context = EmacsActivity.focusedActivities.get (0);
323
324 Log.d (TAG, "display1: using context " + context);
325
326 dialog = dismissDialog = toAlertDialog (context);
327
328 try
329 {
330 if (context == EmacsService.SERVICE)
331 {
332 /* Apply the system alert window type to make sure this
333 dialog can be displayed. */
334
335 window = dialog.getWindow ();
336 type = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
337 ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
338 : WindowManager.LayoutParams.TYPE_PHONE);
339 window.setType (type);
340 }
341
342 dismissDialog.show ();
343 }
344 catch (Exception exception)
345 {
346 /* This can happen when the system decides Emacs is not in the
347 foreground any longer. */
348 return false;
349 }
350
351 /* If there are less than four buttons, then they must be
352 individually enabled or disabled after the dialog is
353 displayed. */
354 size = buttons.size ();
355
356 if (size <= 3)
357 {
358 if (size >= 1)
359 {
360 button = buttons.get (0);
361 buttonView
362 = dialog.getButton (DialogInterface.BUTTON_POSITIVE);
363 buttonView.setEnabled (button.enabled);
364 }
365
366 if (size >= 2)
367 {
368 button = buttons.get (1);
369 buttonView
370 = dialog.getButton (DialogInterface.BUTTON_NEGATIVE);
371 buttonView.setEnabled (button.enabled);
372 }
373
374 if (size >= 3)
375 {
376 button = buttons.get (2);
377 buttonView
378 = dialog.getButton (DialogInterface.BUTTON_NEUTRAL);
379 buttonView.setEnabled (button.enabled);
380 }
381 }
382
383 return true;
384 }
385
386 /* Display this dialog for a suitable activity.
387 Value is false if the dialog could not be displayed,
388 and true otherwise. */
389
390 public boolean
391 display ()
392 {
393 Runnable runnable;
394 final EmacsHolder<Boolean> rc;
395
396 rc = new EmacsHolder<Boolean> ();
397 runnable = new Runnable () {
398 @Override
399 public void
400 run ()
401 {
402 synchronized (this)
403 {
404 rc.thing = display1 ();
405 notify ();
406 }
407 }
408 };
409
410 EmacsService.syncRunnable (runnable);
411 return rc.thing;
412 }
413
414
415
416 @Override
417 public void
418 onDismiss (DialogInterface dialog)
419 {
420 Log.d (TAG, "onDismiss: " + this);
421
422 if (wasButtonClicked)
423 return;
424
425 EmacsNative.sendContextMenu ((short) 0, 0, menuEventSerial);
426 }
427};
diff --git a/java/org/gnu/emacs/EmacsDialogButtonLayout.java b/java/org/gnu/emacs/EmacsDialogButtonLayout.java
new file mode 100644
index 00000000000..fd8d63d81d3
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDialogButtonLayout.java
@@ -0,0 +1,152 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22
23
24import android.content.Context;
25
26import android.view.View;
27import android.view.View.MeasureSpec;
28import android.view.ViewGroup;
29
30
31
32/* This ``view group'' implements a container widget for multiple
33 buttons of the type found in pop-up dialogs. It is used when
34 displaying a dialog box that contains more than three buttons, as
35 the default dialog box widget is not capable of holding more than
36 that many. */
37
38
39
40public final class EmacsDialogButtonLayout extends ViewGroup
41{
42 public
43 EmacsDialogButtonLayout (Context context)
44 {
45 super (context);
46 }
47
48 @Override
49 protected void
50 onMeasure (int widthMeasureSpec, int heightMeasureSpec)
51 {
52 int width, count, i, x, y, height, spec, tempSpec;
53 View view;
54
55 /* Obtain the width of this widget and create the measure
56 specification used to measure children. */
57
58 width = MeasureSpec.getSize (widthMeasureSpec);
59 spec = MeasureSpec.makeMeasureSpec (0, MeasureSpec.UNSPECIFIED);
60 tempSpec
61 = MeasureSpec.makeMeasureSpec (width, MeasureSpec.AT_MOST);
62 x = y = height = 0;
63
64 /* Run through each widget. */
65
66 count = getChildCount ();
67
68 for (i = 0; i < count; ++i)
69 {
70 view = getChildAt (i);
71
72 /* Measure this view. */
73 view.measure (spec, spec);
74
75 if (width - x < view.getMeasuredWidth ())
76 {
77 /* Move onto the next line, unless this line is empty. */
78
79 if (x != 0)
80 {
81 y += height;
82 height = x = 0;
83 }
84
85 if (view.getMeasuredWidth () > width)
86 /* Measure the view again, this time forcing it to be at
87 most width wide, if it is not already. */
88 view.measure (tempSpec, spec);
89 }
90
91 height = Math.max (height, view.getMeasuredHeight ());
92 x += view.getMeasuredWidth ();
93 }
94
95 /* Now set the measured size of this widget. */
96 setMeasuredDimension (width, y + height);
97 }
98
99 @Override
100 protected void
101 onLayout (boolean changed, int left, int top, int right,
102 int bottom)
103 {
104 int width, count, i, x, y, height, spec, tempSpec;
105 View view;
106
107 /* Obtain the width of this widget and create the measure
108 specification used to measure children. */
109
110 width = getMeasuredWidth ();
111 spec = MeasureSpec.makeMeasureSpec (0, MeasureSpec.UNSPECIFIED);
112 tempSpec
113 = MeasureSpec.makeMeasureSpec (width, MeasureSpec.AT_MOST);
114 x = y = height = 0;
115
116 /* Run through each widget. */
117
118 count = getChildCount ();
119
120 for (i = 0; i < count; ++i)
121 {
122 view = getChildAt (i);
123
124 /* Measure this view. */
125 view.measure (spec, spec);
126
127 if (width - x < view.getMeasuredWidth ())
128 {
129 /* Move onto the next line, unless this line is empty. */
130
131 if (x != 0)
132 {
133 y += height;
134 height = x = 0;
135 }
136
137 if (view.getMeasuredWidth () > width)
138 /* Measure the view again, this time forcing it to be at
139 most width wide, if it is not already. */
140 view.measure (tempSpec, spec);
141 }
142
143 /* Now assign this view its position. */
144 view.layout (x, y, x + view.getMeasuredWidth (),
145 y + view.getMeasuredHeight ());
146
147 /* And move on to the next widget. */
148 height = Math.max (height, view.getMeasuredHeight ());
149 x += view.getMeasuredWidth ();
150 }
151 }
152};
diff --git a/java/org/gnu/emacs/EmacsDirectoryEntry.java b/java/org/gnu/emacs/EmacsDirectoryEntry.java
new file mode 100644
index 00000000000..75c52e48002
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDirectoryEntry.java
@@ -0,0 +1,33 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22/* Structure holding a single ``directory entry'' from a document
23 provider. */
24
25public final class EmacsDirectoryEntry
26{
27 /* The type of this directory entry. 0 means a regular file and 1
28 means a directory. */
29 public int d_type;
30
31 /* The display name of the file represented. */
32 public String d_name;
33};
diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java
new file mode 100644
index 00000000000..96dc2bc6e14
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java
@@ -0,0 +1,578 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.content.Context;
23
24import android.database.Cursor;
25import android.database.MatrixCursor;
26
27import android.os.Build;
28import android.os.CancellationSignal;
29import android.os.ParcelFileDescriptor;
30
31import android.provider.DocumentsContract.Document;
32import android.provider.DocumentsContract.Root;
33import static android.provider.DocumentsContract.buildChildDocumentsUri;
34import android.provider.DocumentsProvider;
35
36import android.webkit.MimeTypeMap;
37
38import android.net.Uri;
39
40import java.io.File;
41import java.io.FileInputStream;
42import java.io.FileNotFoundException;
43import java.io.FileOutputStream;
44import java.io.IOException;
45
46/* ``Documents provider''. This allows Emacs's home directory to be
47 modified by other programs holding permissions to manage system
48 storage, which is useful to (for example) correct misconfigurations
49 which prevent Emacs from starting up.
50
51 This functionality is only available on Android 19 and later. */
52
53public final class EmacsDocumentsProvider extends DocumentsProvider
54{
55 /* Home directory. This is the directory whose contents are
56 initially returned to requesting applications. */
57 private File baseDir;
58
59 /* The default projection for requests for the root directory. */
60 private static final String[] DEFAULT_ROOT_PROJECTION;
61
62 /* The default projection for requests for a file. */
63 private static final String[] DEFAULT_DOCUMENT_PROJECTION;
64
65 static
66 {
67 DEFAULT_ROOT_PROJECTION = new String[] {
68 Root.COLUMN_ROOT_ID,
69 Root.COLUMN_MIME_TYPES,
70 Root.COLUMN_FLAGS,
71 Root.COLUMN_ICON,
72 Root.COLUMN_TITLE,
73 Root.COLUMN_SUMMARY,
74 Root.COLUMN_DOCUMENT_ID,
75 Root.COLUMN_AVAILABLE_BYTES,
76 };
77
78 DEFAULT_DOCUMENT_PROJECTION = new String[] {
79 Document.COLUMN_DOCUMENT_ID,
80 Document.COLUMN_MIME_TYPE,
81 Document.COLUMN_DISPLAY_NAME,
82 Document.COLUMN_LAST_MODIFIED,
83 Document.COLUMN_FLAGS,
84 Document.COLUMN_SIZE,
85 };
86 }
87
88 @Override
89 public boolean
90 onCreate ()
91 {
92 /* Set the base directory to Emacs's files directory. */
93 baseDir = getContext ().getFilesDir ();
94 return true;
95 }
96
97 @Override
98 public Cursor
99 queryRoots (String[] projection)
100 {
101 MatrixCursor result;
102 MatrixCursor.RowBuilder row;
103
104 /* If the requestor asked for nothing at all, then it wants some
105 data by default. */
106
107 if (projection == null)
108 projection = DEFAULT_ROOT_PROJECTION;
109
110 result = new MatrixCursor (projection);
111 row = result.newRow ();
112
113 /* Now create and add a row for each file in the base
114 directory. */
115 row.add (Root.COLUMN_ROOT_ID, baseDir.getAbsolutePath ());
116 row.add (Root.COLUMN_SUMMARY, "Emacs home directory");
117
118 /* Add the appropriate flags. */
119
120 row.add (Root.COLUMN_FLAGS, (Root.FLAG_SUPPORTS_CREATE
121 | Root.FLAG_SUPPORTS_IS_CHILD));
122 row.add (Root.COLUMN_ICON, R.drawable.emacs);
123 row.add (Root.FLAG_LOCAL_ONLY);
124 row.add (Root.COLUMN_TITLE, "Emacs");
125 row.add (Root.COLUMN_DOCUMENT_ID, baseDir.getAbsolutePath ());
126
127 return result;
128 }
129
130 private Uri
131 getNotificationUri (File file)
132 {
133 Uri updatedUri;
134
135 updatedUri
136 = buildChildDocumentsUri ("org.gnu.emacs",
137 file.getAbsolutePath ());
138
139 return updatedUri;
140 }
141
142 /* Inform the system that FILE's contents (or FILE itself) has
143 changed. */
144
145 private void
146 notifyChange (File file)
147 {
148 Uri updatedUri;
149 Context context;
150
151 context = getContext ();
152 updatedUri
153 = buildChildDocumentsUri ("org.gnu.emacs",
154 file.getAbsolutePath ());
155 context.getContentResolver ().notifyChange (updatedUri, null);
156 }
157
158 /* Inform the system that FILE's contents (or FILE itself) has
159 changed. FILE is a string describing containing the file name of
160 a directory as opposed to a File. */
161
162 private void
163 notifyChangeByName (String file)
164 {
165 Uri updatedUri;
166 Context context;
167
168 context = getContext ();
169 updatedUri
170 = buildChildDocumentsUri ("org.gnu.emacs", file);
171 context.getContentResolver ().notifyChange (updatedUri, null);
172 }
173
174 /* Return the MIME type of a file FILE. */
175
176 private String
177 getMimeType (File file)
178 {
179 String name, extension, mime;
180 int extensionSeparator;
181 MimeTypeMap singleton;
182
183 if (file.isDirectory ())
184 return Document.MIME_TYPE_DIR;
185
186 /* Abuse WebView stuff to get the file's MIME type. */
187 name = file.getName ();
188 extensionSeparator = name.lastIndexOf ('.');
189
190 if (extensionSeparator > 0)
191 {
192 singleton = MimeTypeMap.getSingleton ();
193 extension = name.substring (extensionSeparator + 1);
194 mime = singleton.getMimeTypeFromExtension (extension);
195
196 if (mime != null)
197 return mime;
198 }
199
200 return "application/octet-stream";
201 }
202
203 /* Append the specified FILE to the query result RESULT.
204 Handle both directories and ordinary files. */
205
206 private void
207 queryDocument1 (MatrixCursor result, File file)
208 {
209 MatrixCursor.RowBuilder row;
210 String fileName, displayName, mimeType;
211 int flags;
212
213 row = result.newRow ();
214 flags = 0;
215
216 /* fileName is a string that the system will ask for some time in
217 the future. Here, it is just the absolute name of the file. */
218 fileName = file.getAbsolutePath ();
219
220 /* If file is a directory, add the right flags for that. */
221
222 if (file.isDirectory ())
223 {
224 if (file.canWrite ())
225 {
226 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
227 flags |= Document.FLAG_SUPPORTS_DELETE;
228
229 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
230 flags |= Document.FLAG_SUPPORTS_RENAME;
231
232 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
233 flags |= Document.FLAG_SUPPORTS_MOVE;
234 }
235 }
236 else if (file.canWrite ())
237 {
238 /* Apply the correct flags for a writable file. */
239 flags |= Document.FLAG_SUPPORTS_WRITE;
240 flags |= Document.FLAG_SUPPORTS_DELETE;
241
242 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
243 flags |= Document.FLAG_SUPPORTS_RENAME;
244
245 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
246 {
247 flags |= Document.FLAG_SUPPORTS_REMOVE;
248 flags |= Document.FLAG_SUPPORTS_MOVE;
249 }
250 }
251
252 displayName = file.getName ();
253 mimeType = getMimeType (file);
254
255 row.add (Document.COLUMN_DOCUMENT_ID, fileName);
256 row.add (Document.COLUMN_DISPLAY_NAME, displayName);
257 row.add (Document.COLUMN_SIZE, file.length ());
258 row.add (Document.COLUMN_MIME_TYPE, mimeType);
259 row.add (Document.COLUMN_LAST_MODIFIED, file.lastModified ());
260 row.add (Document.COLUMN_FLAGS, flags);
261 }
262
263 @Override
264 public Cursor
265 queryDocument (String documentId, String[] projection)
266 throws FileNotFoundException
267 {
268 MatrixCursor result;
269 File file;
270 Context context;
271
272 file = new File (documentId);
273 context = getContext ();
274
275 if (projection == null)
276 projection = DEFAULT_DOCUMENT_PROJECTION;
277
278 result = new MatrixCursor (projection);
279 queryDocument1 (result, file);
280
281 /* Now allow interested applications to detect changes. */
282 result.setNotificationUri (context.getContentResolver (),
283 getNotificationUri (file));
284
285 return result;
286 }
287
288 @Override
289 public Cursor
290 queryChildDocuments (String parentDocumentId, String[] projection,
291 String sortOrder) throws FileNotFoundException
292 {
293 MatrixCursor result;
294 File directory;
295 File[] files;
296 Context context;
297
298 if (projection == null)
299 projection = DEFAULT_DOCUMENT_PROJECTION;
300
301 result = new MatrixCursor (projection);
302
303 /* Try to open the file corresponding to the location being
304 requested. */
305 directory = new File (parentDocumentId);
306
307 /* Look up each child. */
308 files = directory.listFiles ();
309
310 if (files != null)
311 {
312 /* Now add each child. */
313 for (File child : files)
314 queryDocument1 (result, child);
315 }
316
317 context = getContext ();
318
319 /* Now allow interested applications to detect changes. */
320 result.setNotificationUri (context.getContentResolver (),
321 getNotificationUri (directory));
322
323 return result;
324 }
325
326 @Override
327 public ParcelFileDescriptor
328 openDocument (String documentId, String mode,
329 CancellationSignal signal) throws FileNotFoundException
330 {
331 return ParcelFileDescriptor.open (new File (documentId),
332 ParcelFileDescriptor.parseMode (mode));
333 }
334
335 @Override
336 public String
337 createDocument (String documentId, String mimeType,
338 String displayName) throws FileNotFoundException
339 {
340 File file, parentFile;
341 boolean rc;
342
343 file = new File (documentId, displayName);
344
345 try
346 {
347 rc = false;
348
349 if (Document.MIME_TYPE_DIR.equals (mimeType))
350 {
351 file.mkdirs ();
352
353 if (file.isDirectory ())
354 rc = true;
355 }
356 else
357 {
358 file.createNewFile ();
359
360 if (file.isFile ()
361 && file.setWritable (true)
362 && file.setReadable (true))
363 rc = true;
364 }
365
366 if (!rc)
367 throw new FileNotFoundException ("rc != 1");
368 }
369 catch (IOException e)
370 {
371 throw new FileNotFoundException (e.toString ());
372 }
373
374 parentFile = file.getParentFile ();
375
376 if (parentFile != null)
377 notifyChange (parentFile);
378
379 return file.getAbsolutePath ();
380 }
381
382 private void
383 deleteDocument1 (File child)
384 {
385 File[] children;
386
387 /* Don't delete symlinks recursively.
388
389 Calling readlink or stat is problematic due to file name
390 encoding problems, so try to delete the file first, and only
391 try to delete files recursively afterword. */
392
393 if (child.delete ())
394 return;
395
396 children = child.listFiles ();
397
398 if (children != null)
399 {
400 for (File file : children)
401 deleteDocument1 (file);
402 }
403
404 child.delete ();
405 }
406
407 @Override
408 public void
409 deleteDocument (String documentId)
410 throws FileNotFoundException
411 {
412 File file, parent;
413 File[] children;
414
415 /* Java makes recursively deleting a file hard. File name
416 encoding issues also prevent easily calling into C... */
417
418 file = new File (documentId);
419 parent = file.getParentFile ();
420
421 if (parent == null)
422 throw new RuntimeException ("trying to delete file without"
423 + " parent!");
424
425 if (file.delete ())
426 {
427 /* Tell the system about the change. */
428 notifyChange (parent);
429 return;
430 }
431
432 children = file.listFiles ();
433
434 if (children != null)
435 {
436 for (File child : children)
437 deleteDocument1 (child);
438 }
439
440 if (file.delete ())
441 /* Tell the system about the change. */
442 notifyChange (parent);
443 }
444
445 @Override
446 public void
447 removeDocument (String documentId, String parentDocumentId)
448 throws FileNotFoundException
449 {
450 deleteDocument (documentId);
451 }
452
453 @Override
454 public String
455 getDocumentType (String documentId)
456 {
457 return getMimeType (new File (documentId));
458 }
459
460 @Override
461 public String
462 renameDocument (String documentId, String displayName)
463 throws FileNotFoundException
464 {
465 File file, newName;
466 File parent;
467
468 file = new File (documentId);
469 parent = file.getParentFile ();
470 newName = new File (parent, displayName);
471
472 if (parent == null)
473 throw new FileNotFoundException ("parent is null");
474
475 file = new File (documentId);
476
477 if (!file.renameTo (newName))
478 return null;
479
480 notifyChange (parent);
481 return newName.getAbsolutePath ();
482 }
483
484 @Override
485 public boolean
486 isChildDocument (String parentDocumentId, String documentId)
487 {
488 return documentId.startsWith (parentDocumentId);
489 }
490
491 @Override
492 public String
493 moveDocument (String sourceDocumentId,
494 String sourceParentDocumentId,
495 String targetParentDocumentId)
496 throws FileNotFoundException
497 {
498 File file, newName;
499 FileInputStream inputStream;
500 FileOutputStream outputStream;
501 byte buffer[];
502 int length;
503
504 file = new File (sourceDocumentId);
505
506 /* Now, create the file name of the parent document. */
507 newName = new File (targetParentDocumentId,
508 file.getName ());
509
510 /* Try to perform a simple rename, before falling back to
511 copying. */
512
513 if (file.renameTo (newName))
514 {
515 notifyChangeByName (file.getParent ());
516 notifyChangeByName (targetParentDocumentId);
517 return newName.getAbsolutePath ();
518 }
519
520 /* If that doesn't work, create the new file and copy over the old
521 file's contents. */
522
523 inputStream = null;
524 outputStream = null;
525
526 try
527 {
528 if (!newName.createNewFile ()
529 || !newName.setWritable (true)
530 || !newName.setReadable (true))
531 throw new FileNotFoundException ("failed to create new file");
532
533 /* Open the file in preparation for a copy. */
534
535 inputStream = new FileInputStream (file);
536 outputStream = new FileOutputStream (newName);
537
538 /* Allocate the buffer used to hold data. */
539
540 buffer = new byte[4096];
541
542 while ((length = inputStream.read (buffer)) > 0)
543 outputStream.write (buffer, 0, length);
544 }
545 catch (IOException e)
546 {
547 throw new FileNotFoundException ("IOException: " + e);
548 }
549 finally
550 {
551 try
552 {
553 if (inputStream != null)
554 inputStream.close ();
555 }
556 catch (IOException e)
557 {
558
559 }
560
561 try
562 {
563 if (outputStream != null)
564 outputStream.close ();
565 }
566 catch (IOException e)
567 {
568
569 }
570 }
571
572 file.delete ();
573 notifyChangeByName (file.getParent ());
574 notifyChangeByName (targetParentDocumentId);
575
576 return newName.getAbsolutePath ();
577 }
578}
diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java
new file mode 100644
index 00000000000..d367ccff9c4
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDrawLine.java
@@ -0,0 +1,79 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Canvas;
23import android.graphics.Paint;
24import android.graphics.Rect;
25
26public final class EmacsDrawLine
27{
28 public static void
29 perform (EmacsDrawable drawable, EmacsGC gc,
30 int x, int y, int x2, int y2)
31 {
32 Rect rect;
33 Canvas canvas;
34 Paint paint;
35 int x0, x1, y0, y1;
36
37 /* TODO implement stippling. */
38 if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED)
39 return;
40
41 /* Calculate the leftmost and rightmost points. */
42
43 x0 = Math.min (x, x2 + 1);
44 x1 = Math.max (x, x2 + 1);
45 y0 = Math.min (y, y2 + 1);
46 y1 = Math.max (y, y2 + 1);
47
48 /* And the clip rectangle. */
49
50 paint = gc.gcPaint;
51 rect = new Rect (x0, y0, x1, y1);
52 canvas = drawable.lockCanvas (gc);
53
54 if (canvas == null)
55 return;
56
57 paint.setStyle (Paint.Style.FILL);
58
59 /* Since drawLine has PostScript style behavior, adjust the
60 coordinates appropriately.
61
62 The left most pixel of a straight line is always partially
63 filled. Patch it in manually. */
64
65 if (gc.clip_mask == null)
66 {
67 canvas.drawLine ((float) x + 0.5f, (float) y + 0.5f,
68 (float) x2 + 0.5f, (float) y2 + 0.5f,
69 paint);
70
71 if (x2 > x)
72 canvas.drawRect (new Rect (x, y, x + 1, y + 1), paint);
73 }
74
75 /* DrawLine with clip mask not implemented; it is not used by
76 Emacs. */
77 drawable.damageRect (rect);
78 }
79}
diff --git a/java/org/gnu/emacs/EmacsDrawPoint.java b/java/org/gnu/emacs/EmacsDrawPoint.java
new file mode 100644
index 00000000000..6a1cb744d60
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDrawPoint.java
@@ -0,0 +1,34 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22public final class EmacsDrawPoint
23{
24 public static void
25 perform (EmacsDrawable drawable,
26 EmacsGC immutableGC, int x, int y)
27 {
28 /* Use EmacsFillRectangle instead of EmacsDrawRectangle, as the
29 latter actually draws a rectangle one pixel wider than
30 specified. */
31 EmacsFillRectangle.perform (drawable, immutableGC,
32 x, y, 1, 1);
33 }
34}
diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java
new file mode 100644
index 00000000000..e1261b4a2d2
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDrawRectangle.java
@@ -0,0 +1,120 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Bitmap;
23import android.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.Rect;
26import android.graphics.RectF;
27
28import android.util.Log;
29
30public final class EmacsDrawRectangle
31{
32 public static void
33 perform (EmacsDrawable drawable, EmacsGC gc,
34 int x, int y, int width, int height)
35 {
36 Paint maskPaint, paint;
37 Canvas maskCanvas;
38 Bitmap maskBitmap;
39 Rect maskRect, dstRect;
40 Canvas canvas;
41 Bitmap clipBitmap;
42
43 /* TODO implement stippling. */
44 if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED)
45 return;
46
47 canvas = drawable.lockCanvas (gc);
48
49 if (canvas == null)
50 return;
51
52 paint = gc.gcPaint;
53 paint.setStyle (Paint.Style.STROKE);
54
55 if (gc.clip_mask == null)
56 /* Use canvas.drawRect with a RectF. That seems to reliably
57 get PostScript behavior. */
58 canvas.drawRect (new RectF (x + 0.5f, y + 0.5f,
59 x + width + 0.5f,
60 y + height + 0.5f),
61 paint);
62 else
63 {
64 /* Drawing with a clip mask involves calculating the
65 intersection of the clip mask with the dst rect, and
66 extrapolating the corresponding part of the src rect. */
67 clipBitmap = gc.clip_mask.bitmap;
68 dstRect = new Rect (x, y, x + width, y + height);
69 maskRect = new Rect (gc.clip_x_origin,
70 gc.clip_y_origin,
71 (gc.clip_x_origin
72 + clipBitmap.getWidth ()),
73 (gc.clip_y_origin
74 + clipBitmap.getHeight ()));
75
76 if (!maskRect.setIntersect (dstRect, maskRect))
77 /* There is no intersection between the clip mask and the
78 dest rect. */
79 return;
80
81 /* Finally, create a temporary bitmap that is the size of
82 maskRect. */
83
84 maskBitmap
85 = Bitmap.createBitmap (maskRect.width (), maskRect.height (),
86 Bitmap.Config.ARGB_8888);
87
88 /* Draw the mask onto the maskBitmap. */
89 maskCanvas = new Canvas (maskBitmap);
90 maskRect.offset (-gc.clip_x_origin,
91 -gc.clip_y_origin);
92 maskCanvas.drawBitmap (gc.clip_mask.bitmap,
93 maskRect, new Rect (0, 0,
94 maskRect.width (),
95 maskRect.height ()),
96 paint);
97 maskRect.offset (gc.clip_x_origin,
98 gc.clip_y_origin);
99
100 /* Set the transfer mode to SRC_IN to preserve only the parts
101 of the source that overlap with the mask. */
102 maskPaint = new Paint ();
103 maskPaint.setXfermode (EmacsGC.srcInAlu);
104 maskPaint.setStyle (Paint.Style.STROKE);
105
106 /* Draw the source. */
107 maskCanvas.drawRect (maskRect, maskPaint);
108
109 /* Finally, draw the mask bitmap to the destination. */
110 paint.setXfermode (null);
111 canvas.drawBitmap (maskBitmap, null, maskRect, paint);
112
113 /* Recycle this unused bitmap. */
114 maskBitmap.recycle ();
115 }
116
117 drawable.damageRect (new Rect (x, y, x + width + 1,
118 y + height + 1));
119 }
120}
diff --git a/java/org/gnu/emacs/EmacsDrawable.java b/java/org/gnu/emacs/EmacsDrawable.java
new file mode 100644
index 00000000000..f2f8885e976
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDrawable.java
@@ -0,0 +1,32 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Rect;
23import android.graphics.Bitmap;
24import android.graphics.Canvas;
25
26public interface EmacsDrawable
27{
28 public Canvas lockCanvas (EmacsGC gc);
29 public void damageRect (Rect damageRect);
30 public Bitmap getBitmap ();
31 public boolean isDestroyed ();
32};
diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java
new file mode 100644
index 00000000000..4ae3882cab4
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsFillPolygon.java
@@ -0,0 +1,80 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Canvas;
23import android.graphics.Paint;
24import android.graphics.Path;
25import android.graphics.Point;
26import android.graphics.Rect;
27import android.graphics.RectF;
28
29public final class EmacsFillPolygon
30{
31 public static void
32 perform (EmacsDrawable drawable, EmacsGC gc, Point points[])
33 {
34 Canvas canvas;
35 Path path;
36 Paint paint;
37 Rect rect;
38 RectF rectF;
39 int i;
40
41 canvas = drawable.lockCanvas (gc);
42
43 if (canvas == null)
44 return;
45
46 paint = gc.gcPaint;
47
48 /* Build the path from the given array of points. */
49 path = new Path ();
50
51 if (points.length >= 1)
52 {
53 path.moveTo (points[0].x, points[0].y);
54
55 for (i = 1; i < points.length; ++i)
56 path.lineTo (points[i].x, points[i].y);
57
58 path.close ();
59 }
60
61 /* Compute the damage rectangle. */
62 rectF = new RectF (0, 0, 0, 0);
63 path.computeBounds (rectF, true);
64
65 rect = new Rect ((int) Math.floor (rectF.left),
66 (int) Math.floor (rectF.top),
67 (int) Math.ceil (rectF.right),
68 (int) Math.ceil (rectF.bottom));
69
70 paint.setStyle (Paint.Style.FILL);
71
72 if (gc.clip_mask == null)
73 canvas.drawPath (path, paint);
74
75 drawable.damageRect (rect);
76
77 /* FillPolygon with clip mask not implemented; it is not used by
78 Emacs. */
79 }
80}
diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java
new file mode 100644
index 00000000000..461fd3c639c
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsFillRectangle.java
@@ -0,0 +1,116 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Bitmap;
23import android.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.Rect;
26
27import android.util.Log;
28
29public final class EmacsFillRectangle
30{
31 public static void
32 perform (EmacsDrawable drawable, EmacsGC gc,
33 int x, int y, int width, int height)
34 {
35 Paint maskPaint, paint;
36 Canvas maskCanvas;
37 Bitmap maskBitmap;
38 Rect rect;
39 Rect maskRect, dstRect;
40 Canvas canvas;
41 Bitmap clipBitmap;
42
43 /* TODO implement stippling. */
44 if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED)
45 return;
46
47 canvas = drawable.lockCanvas (gc);
48
49 if (canvas == null)
50 return;
51
52 paint = gc.gcPaint;
53 rect = new Rect (x, y, x + width, y + height);
54
55 paint.setStyle (Paint.Style.FILL);
56
57 if (gc.clip_mask == null)
58 canvas.drawRect (rect, paint);
59 else
60 {
61 /* Drawing with a clip mask involves calculating the
62 intersection of the clip mask with the dst rect, and
63 extrapolating the corresponding part of the src rect. */
64
65 clipBitmap = gc.clip_mask.bitmap;
66 dstRect = new Rect (x, y, x + width, y + height);
67 maskRect = new Rect (gc.clip_x_origin,
68 gc.clip_y_origin,
69 (gc.clip_x_origin
70 + clipBitmap.getWidth ()),
71 (gc.clip_y_origin
72 + clipBitmap.getHeight ()));
73
74 if (!maskRect.setIntersect (dstRect, maskRect))
75 /* There is no intersection between the clip mask and the
76 dest rect. */
77 return;
78
79 /* Finally, create a temporary bitmap that is the size of
80 maskRect. */
81
82 maskBitmap
83 = Bitmap.createBitmap (maskRect.width (), maskRect.height (),
84 Bitmap.Config.ARGB_8888);
85
86 /* Draw the mask onto the maskBitmap. */
87 maskCanvas = new Canvas (maskBitmap);
88 maskRect.offset (-gc.clip_x_origin,
89 -gc.clip_y_origin);
90 maskCanvas.drawBitmap (gc.clip_mask.bitmap,
91 maskRect, new Rect (0, 0,
92 maskRect.width (),
93 maskRect.height ()),
94 paint);
95 maskRect.offset (gc.clip_x_origin,
96 gc.clip_y_origin);
97
98 /* Set the transfer mode to SRC_IN to preserve only the parts
99 of the source that overlap with the mask. */
100 maskPaint = new Paint ();
101 maskPaint.setXfermode (EmacsGC.srcInAlu);
102
103 /* Draw the source. */
104 maskCanvas.drawRect (maskRect, maskPaint);
105
106 /* Finally, draw the mask bitmap to the destination. */
107 paint.setXfermode (null);
108 canvas.drawBitmap (maskBitmap, null, maskRect, paint);
109
110 /* Recycle this unused bitmap. */
111 maskBitmap.recycle ();
112 }
113
114 drawable.damageRect (rect);
115 }
116}
diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java
new file mode 100644
index 00000000000..ff52899a897
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsFontDriver.java
@@ -0,0 +1,173 @@
1/* Font backend for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.os.Build;
23
24/* This code is mostly unused. See sfntfont-android.c for the code
25 that is actually used. */
26
27public abstract class EmacsFontDriver
28{
29 /* Font weights. */
30 public static final int THIN = 0;
31 public static final int ULTRA_LIGHT = 40;
32 public static final int LIGHT = 50;
33 public static final int SEMI_LIGHT = 55;
34 public static final int REGULAR = 80;
35 public static final int MEDIUM = 100;
36 public static final int SEMI_BOLD = 180;
37 public static final int BOLD = 200;
38 public static final int EXTRA_BOLD = 205;
39 public static final int BLACK = 210;
40 public static final int ULTRA_HEAVY = 250;
41
42 /* Font slants. */
43 public static final int REVERSE_OBLIQUE = 0;
44 public static final int REVERSE_ITALIC = 10;
45 public static final int NORMAL = 100;
46 public static final int ITALIC = 200;
47 public static final int OBLIQUE = 210;
48
49 /* Font widths. */
50 public static final int ULTRA_CONDENSED = 50;
51 public static final int EXTRA_CONDENSED = 63;
52 public static final int CONDENSED = 75;
53 public static final int SEMI_CONDENSED = 87;
54 public static final int UNSPECIFIED = 100;
55 public static final int SEMI_EXPANDED = 113;
56 public static final int EXPANDED = 125;
57 public static final int EXTRA_EXPANDED = 150;
58 public static final int ULTRA_EXPANDED = 200;
59
60 /* Font spacings. */
61 public static final int PROPORTIONAL = 0;
62 public static final int DUAL = 90;
63 public static final int MONO = 100;
64 public static final int CHARCELL = 110;
65
66 public static class FontSpec
67 {
68 /* The fields below mean the same as they do in enum
69 font_property_index in font.h. */
70
71 public String foundry;
72 public String family;
73 public String adstyle;
74 public String registry;
75 public Integer width;
76 public Integer weight;
77 public Integer slant;
78 public Integer size;
79 public Integer spacing;
80 public Integer avgwidth;
81 public Integer dpi;
82
83 @Override
84 public String
85 toString ()
86 {
87 return ("foundry: " + foundry
88 + " family: " + family
89 + " adstyle: " + adstyle
90 + " registry: " + registry
91 + " width: " + width
92 + " weight: " + weight
93 + " slant: " + slant
94 + " spacing: " + spacing
95 + " avgwidth: " + avgwidth
96 + " dpi: " + dpi);
97 }
98 };
99
100 public static class FontMetrics
101 {
102 public short lbearing;
103 public short rbearing;
104 public short width;
105 public short ascent;
106 public short descent;
107
108 @Override
109 public String
110 toString ()
111 {
112 return ("lbearing " + lbearing
113 + " rbearing " + rbearing
114 + " width " + width
115 + " ascent " + ascent
116 + " descent " + descent);
117 }
118 }
119
120 public static class FontEntity extends FontSpec
121 {
122 /* No extra fields here. */
123 };
124
125 public abstract class FontObject extends FontSpec
126 {
127 public int minWidth;
128 public int maxWidth;
129 public int pixelSize;
130 public int height;
131 public int spaceWidth;
132 public int averageWidth;
133 public int ascent;
134 public int descent;
135 public int underlineThickness;
136 public int underlinePosition;
137 public int baselineOffset;
138 public int relativeCompose;
139 public int defaultAscent;
140 public int encodingCharset;
141 public int repertoryCharset;
142
143 public
144 FontObject ()
145 {
146 encodingCharset = -1;
147 repertoryCharset = -1;
148 }
149 };
150
151 /* These mean the same as they do in struct font_driver. */
152 public abstract FontEntity[] list (FontSpec fontSpec);
153 public abstract FontEntity match (FontSpec fontSpec);
154 public abstract String[] listFamilies ();
155 public abstract FontObject openFont (FontEntity fontEntity, int pixelSize);
156 public abstract int hasChar (FontSpec font, char charCode);
157 public abstract void textExtents (FontObject font, int code[],
158 FontMetrics fontMetrics);
159 public abstract int encodeChar (FontObject fontObject, char charCode);
160 public abstract int draw (FontObject fontObject, EmacsGC gc,
161 EmacsDrawable drawable, int[] chars,
162 int x, int y, int backgroundWidth,
163 boolean withBackground);
164
165 public static EmacsFontDriver
166 createFontDriver ()
167 {
168 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
169 return new EmacsSdk23FontDriver ();
170
171 return new EmacsSdk7FontDriver ();
172 }
173};
diff --git a/java/org/gnu/emacs/EmacsGC.java b/java/org/gnu/emacs/EmacsGC.java
new file mode 100644
index 00000000000..a7467cb9bd0
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsGC.java
@@ -0,0 +1,121 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Rect;
23import android.graphics.Paint;
24
25import android.graphics.PorterDuff.Mode;
26import android.graphics.PorterDuffXfermode;
27import android.graphics.Xfermode;
28
29/* X like graphics context structures. Keep the enums in synch with
30 androidgui.h! */
31
32public final class EmacsGC extends EmacsHandleObject
33{
34 public static final int GC_COPY = 0;
35 public static final int GC_XOR = 1;
36
37 public static final int GC_FILL_SOLID = 0;
38 public static final int GC_FILL_OPAQUE_STIPPLED = 1;
39
40 public static final Xfermode xorAlu, srcInAlu;
41
42 public int function, fill_style;
43 public int foreground, background;
44 public int clip_x_origin, clip_y_origin;
45 public int ts_origin_x, ts_origin_y;
46 public Rect clip_rects[], real_clip_rects[];
47 public EmacsPixmap clip_mask, stipple;
48 public Paint gcPaint;
49
50 /* ID incremented every time the clipping rectangles of any GC
51 changes. */
52 private static long clip_serial;
53
54 /* The value of clipRectID after the last time this GCs clip
55 rectangles changed. 0 if there are no clip rectangles. */
56 public long clipRectID;
57
58 static
59 {
60 xorAlu = new PorterDuffXfermode (Mode.XOR);
61 srcInAlu = new PorterDuffXfermode (Mode.SRC_IN);
62 }
63
64 /* The following fields are only set on immutable GCs. */
65
66 public
67 EmacsGC (short handle)
68 {
69 /* For historical reasons the C code has an extra layer of
70 indirection above this GC handle. struct android_gc is the GC
71 used by Emacs code, while android_gcontext is the type of the
72 handle. */
73 super (handle);
74
75 fill_style = GC_FILL_SOLID;
76 function = GC_COPY;
77 foreground = 0;
78 background = 0xffffff;
79 gcPaint = new Paint ();
80 }
81
82 /* Mark this GC as dirty. Apply parameters to the paint and
83 recompute real_clip_rects. */
84
85 public void
86 markDirty (boolean clipRectsChanged)
87 {
88 int i;
89
90 if (clipRectsChanged)
91 {
92 if ((ts_origin_x != 0 || ts_origin_y != 0)
93 && clip_rects != null)
94 {
95 real_clip_rects = new Rect[clip_rects.length];
96
97 for (i = 0; i < clip_rects.length; ++i)
98 {
99 real_clip_rects[i] = new Rect (clip_rects[i]);
100 real_clip_rects[i].offset (ts_origin_x, ts_origin_y);
101 }
102 }
103 else
104 real_clip_rects = clip_rects;
105
106 clipRectID = ++clip_serial;
107 }
108
109 gcPaint.setStrokeWidth (1f);
110 gcPaint.setColor (foreground | 0xff000000);
111 gcPaint.setXfermode (function == GC_XOR
112 ? xorAlu : srcInAlu);
113 }
114
115 public void
116 resetXfermode ()
117 {
118 gcPaint.setXfermode (function == GC_XOR
119 ? xorAlu : srcInAlu);
120 }
121};
diff --git a/java/org/gnu/emacs/EmacsHandleObject.java b/java/org/gnu/emacs/EmacsHandleObject.java
new file mode 100644
index 00000000000..5b889895337
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsHandleObject.java
@@ -0,0 +1,59 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.lang.IllegalStateException;
23
24/* This defines something that is a so-called ``handle''. Handles
25 must be created by C code, and will remain existing until
26 destroyHandle is called. C code then refers to the handle by a
27 number which maps into the Java object representing the handle.
28
29 All handle operations must be done from the Emacs thread. */
30
31public abstract class EmacsHandleObject
32{
33 /* Whether or not this handle has been destroyed. */
34 volatile boolean destroyed;
35
36 /* The handle associated with this object. */
37 public short handle;
38
39 public
40 EmacsHandleObject (short handle)
41 {
42 this.handle = handle;
43 }
44
45 public void
46 destroyHandle () throws IllegalStateException
47 {
48 synchronized (this)
49 {
50 destroyed = true;
51 }
52 }
53
54 public boolean
55 isDestroyed ()
56 {
57 return destroyed;
58 }
59};
diff --git a/java/org/gnu/emacs/EmacsHolder.java b/java/org/gnu/emacs/EmacsHolder.java
new file mode 100644
index 00000000000..6cd48ba57ce
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsHolder.java
@@ -0,0 +1,30 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22
23
24/* This class serves as a simple reference to an object of type T.
25 Nothing could be found inside the standard library. */
26
27public final class EmacsHolder<T>
28{
29 T thing;
30};
diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java
new file mode 100644
index 00000000000..c3764a7b29f
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsInputConnection.java
@@ -0,0 +1,698 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.os.Build;
23import android.os.Bundle;
24import android.os.Handler;
25
26import android.view.KeyEvent;
27
28import android.view.inputmethod.CompletionInfo;
29import android.view.inputmethod.CorrectionInfo;
30import android.view.inputmethod.ExtractedText;
31import android.view.inputmethod.ExtractedTextRequest;
32import android.view.inputmethod.InputConnection;
33import android.view.inputmethod.InputContentInfo;
34import android.view.inputmethod.SurroundingText;
35import android.view.inputmethod.TextAttribute;
36import android.view.inputmethod.TextSnapshot;
37
38import android.util.Log;
39
40/* Android input methods, take number six. See textconv.c for more
41 details; this is more-or-less a thin wrapper around that file. */
42
43public final class EmacsInputConnection implements InputConnection
44{
45 private static final String TAG = "EmacsInputConnection";
46
47 /* View associated with this input connection. */
48 private EmacsView view;
49
50 /* The handle ID associated with that view's window. */
51 private short windowHandle;
52
53 /* Number of batch edits currently underway. Used to avoid
54 synchronizing with the Emacs thread after each
55 `endBatchEdit'. */
56 private int batchEditCount;
57
58 /* Whether or not to synchronize and call `updateIC' with the
59 selection position after committing text.
60
61 This helps with on screen keyboard programs found in some vendor
62 versions of Android, which rely on immediate updates to the point
63 position after text is commited in order to place the cursor
64 within that text. */
65
66 private static boolean syncAfterCommit;
67
68 /* Whether or not to return empty text with the offset set to zero
69 if a request arrives that has no flags set and has requested no
70 characters at all.
71
72 This is necessary with on screen keyboard programs found in some
73 vendor versions of Android which don't rely on the documented
74 meaning of `ExtractedText.startOffset', and instead take the
75 selection offset inside at face value. */
76
77 private static boolean extractAbsoluteOffsets;
78
79 static
80 {
81 if (Build.MANUFACTURER.equalsIgnoreCase ("Huawei")
82 || Build.MANUFACTURER.equalsIgnoreCase ("Honor"))
83 extractAbsoluteOffsets = syncAfterCommit = true;
84
85 /* The Samsung and Vivo keyboards take `selectionStart' at face
86 value if some text is returned, and also searches for words
87 solely within that text. However, when no text is returned, it
88 falls back to getTextAfterCursor and getTextBeforeCursor. */
89 if (Build.MANUFACTURER.equalsIgnoreCase ("Samsung")
90 || Build.MANUFACTURER.equalsIgnoreCase ("Vivo"))
91 extractAbsoluteOffsets = true;
92 };
93
94
95 public
96 EmacsInputConnection (EmacsView view)
97 {
98 this.view = view;
99 this.windowHandle = view.window.handle;
100 }
101
102
103 /* The functions below are called by input methods whenever they
104 need to perform an edit. */
105
106 @Override
107 public boolean
108 beginBatchEdit ()
109 {
110 /* Return if the input connection is out of date. */
111 if (view.icSerial < view.icGeneration)
112 return false;
113
114 if (EmacsService.DEBUG_IC)
115 Log.d (TAG, "beginBatchEdit");
116
117 EmacsNative.beginBatchEdit (windowHandle);
118
119 /* Keep a record of the number of outstanding batch edits here as
120 well. */
121 batchEditCount++;
122 return true;
123 }
124
125 @Override
126 public boolean
127 endBatchEdit ()
128 {
129 /* Return if the input connection is out of date. */
130 if (view.icSerial < view.icGeneration)
131 return false;
132
133 if (EmacsService.DEBUG_IC)
134 Log.d (TAG, "endBatchEdit");
135
136 EmacsNative.endBatchEdit (windowHandle);
137
138 /* Subtract one from the UI thread record of the number of batch
139 edits currently under way. */
140
141 if (batchEditCount > 0)
142 batchEditCount -= 1;
143
144 return batchEditCount > 0;
145 }
146
147 public boolean
148 commitCompletion (CompletionInfo info)
149 {
150 /* Return if the input connection is out of date. */
151 if (view.icSerial < view.icGeneration)
152 return false;
153
154 if (EmacsService.DEBUG_IC)
155 Log.d (TAG, "commitCompletion: " + info);
156
157 EmacsNative.commitCompletion (windowHandle,
158 info.getText ().toString (),
159 info.getPosition ());
160 return true;
161 }
162
163 @Override
164 public boolean
165 commitCorrection (CorrectionInfo info)
166 {
167 /* The input method calls this function not to commit text, but to
168 indicate that a subsequent edit will consist of a correction.
169 Emacs has no use for this information.
170
171 Of course this completely contradicts the provided
172 documentation, but this is how Android actually behaves. */
173 return false;
174 }
175
176 @Override
177 public boolean
178 commitText (CharSequence text, int newCursorPosition)
179 {
180 int[] selection;
181
182 /* Return if the input connection is out of date. */
183 if (view.icSerial < view.icGeneration)
184 return false;
185
186 if (EmacsService.DEBUG_IC)
187 Log.d (TAG, "commitText: " + text + " " + newCursorPosition);
188
189 EmacsNative.commitText (windowHandle, text.toString (),
190 newCursorPosition);
191
192 if (syncAfterCommit)
193 {
194 /* Synchronize with the Emacs thread, obtain the new
195 selection, and report it immediately. */
196
197 selection = EmacsNative.getSelection (windowHandle);
198
199 if (EmacsService.DEBUG_IC && selection != null)
200 Log.d (TAG, "commitText: new selection is " + selection[0]
201 + ", by " + selection[1]);
202
203 if (selection != null)
204 /* N.B. that the composing region is removed after text is
205 committed. */
206 view.imManager.updateSelection (view, selection[0],
207 selection[1], -1, -1);
208 }
209
210 return true;
211 }
212
213 @Override
214 public boolean
215 commitText (CharSequence text, int newCursorPosition,
216 TextAttribute textAttribute)
217 {
218 return commitText (text, newCursorPosition);
219 }
220
221 @Override
222 public boolean
223 deleteSurroundingText (int leftLength, int rightLength)
224 {
225 /* Return if the input connection is out of date. */
226 if (view.icSerial < view.icGeneration)
227 return false;
228
229 if (EmacsService.DEBUG_IC)
230 Log.d (TAG, ("deleteSurroundingText: "
231 + leftLength + " " + rightLength));
232
233 EmacsNative.deleteSurroundingText (windowHandle, leftLength,
234 rightLength);
235 return true;
236 }
237
238 @Override
239 public boolean
240 deleteSurroundingTextInCodePoints (int leftLength, int rightLength)
241 {
242 /* Emacs returns characters which cannot be represented in a Java
243 `char' as NULL characters, so code points always reflect
244 characters themselves. */
245 return deleteSurroundingText (leftLength, rightLength);
246 }
247
248 @Override
249 public boolean
250 finishComposingText ()
251 {
252 /* Return if the input connection is out of date. */
253 if (view.icSerial < view.icGeneration)
254 return false;
255
256 if (EmacsService.DEBUG_IC)
257 Log.d (TAG, "finishComposingText");
258
259 EmacsNative.finishComposingText (windowHandle);
260 return true;
261 }
262
263 @Override
264 public String
265 getSelectedText (int flags)
266 {
267 /* Return if the input connection is out of date. */
268 if (view.icSerial < view.icGeneration)
269 return null;
270
271 if (EmacsService.DEBUG_IC)
272 Log.d (TAG, "getSelectedText: " + flags);
273
274 return EmacsNative.getSelectedText (windowHandle, flags);
275 }
276
277 @Override
278 public String
279 getTextAfterCursor (int length, int flags)
280 {
281 String string;
282
283 /* Return if the input connection is out of date. */
284 if (view.icSerial < view.icGeneration)
285 return null;
286
287 if (EmacsService.DEBUG_IC)
288 Log.d (TAG, "getTextAfterCursor: " + length + " " + flags);
289
290 string = EmacsNative.getTextAfterCursor (windowHandle, length,
291 flags);
292
293 if (EmacsService.DEBUG_IC)
294 Log.d (TAG, " --> " + string);
295
296 return string;
297 }
298
299 @Override
300 public String
301 getTextBeforeCursor (int length, int flags)
302 {
303 String string;
304
305 /* Return if the input connection is out of date. */
306 if (view.icSerial < view.icGeneration)
307 return null;
308
309 if (EmacsService.DEBUG_IC)
310 Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags);
311
312 string = EmacsNative.getTextBeforeCursor (windowHandle, length,
313 flags);
314
315 if (EmacsService.DEBUG_IC)
316 Log.d (TAG, " --> " + string);
317
318 return string;
319 }
320
321 @Override
322 public boolean
323 setComposingText (CharSequence text, int newCursorPosition)
324 {
325 /* Return if the input connection is out of date. */
326 if (view.icSerial < view.icGeneration)
327 return false;
328
329 if (EmacsService.DEBUG_IC)
330 Log.d (TAG, ("setComposingText: "
331 + text + " ## " + newCursorPosition));
332
333 EmacsNative.setComposingText (windowHandle, text.toString (),
334 newCursorPosition);
335 return true;
336 }
337
338 @Override
339 public boolean
340 setComposingText (CharSequence text, int newCursorPosition,
341 TextAttribute textAttribute)
342 {
343 return setComposingText (text, newCursorPosition);
344 }
345
346 @Override
347 public boolean
348 setComposingRegion (int start, int end)
349 {
350 /* Return if the input connection is out of date. */
351 if (view.icSerial < view.icGeneration)
352 return false;
353
354 if (EmacsService.DEBUG_IC)
355 Log.d (TAG, "setComposingRegion: " + start + " " + end);
356
357 EmacsNative.setComposingRegion (windowHandle, start, end);
358 return true;
359 }
360
361 @Override
362 public boolean
363 setComposingRegion (int start, int end, TextAttribute textAttribute)
364 {
365 return setComposingRegion (start, end);
366 }
367
368 @Override
369 public boolean
370 performEditorAction (int editorAction)
371 {
372 /* Return if the input connection is out of date. */
373 if (view.icSerial < view.icGeneration)
374 return false;
375
376 if (EmacsService.DEBUG_IC)
377 Log.d (TAG, "performEditorAction: " + editorAction);
378
379 EmacsNative.performEditorAction (windowHandle, editorAction);
380 return true;
381 }
382
383 @Override
384 public boolean
385 performContextMenuAction (int contextMenuAction)
386 {
387 int action;
388
389 /* Return if the input connection is out of date. */
390 if (view.icSerial < view.icGeneration)
391 return false;
392
393 if (EmacsService.DEBUG_IC)
394 Log.d (TAG, "performContextMenuAction: " + contextMenuAction);
395
396 /* Translate the action in Java code. That way, a great deal of
397 JNI boilerplate can be avoided. */
398
399 switch (contextMenuAction)
400 {
401 case android.R.id.selectAll:
402 action = 0;
403 break;
404
405 case android.R.id.startSelectingText:
406 action = 1;
407 break;
408
409 case android.R.id.stopSelectingText:
410 action = 2;
411 break;
412
413 case android.R.id.cut:
414 action = 3;
415 break;
416
417 case android.R.id.copy:
418 action = 4;
419 break;
420
421 case android.R.id.paste:
422 action = 5;
423 break;
424
425 default:
426 return true;
427 }
428
429 EmacsNative.performContextMenuAction (windowHandle, action);
430 return true;
431 }
432
433 @Override
434 public ExtractedText
435 getExtractedText (ExtractedTextRequest request, int flags)
436 {
437 ExtractedText text;
438 int[] selection;
439
440 /* Return if the input connection is out of date. */
441 if (view.icSerial < view.icGeneration)
442 return null;
443
444 if (EmacsService.DEBUG_IC)
445 Log.d (TAG, "getExtractedText: " + request.hintMaxChars + ", "
446 + request.hintMaxLines + " " + flags);
447
448 /* If a request arrives with hintMaxChars, hintMaxLines and flags
449 set to 0, and the system is known to be buggy, return an empty
450 extracted text object with the absolute selection positions. */
451
452 if (extractAbsoluteOffsets
453 && request.hintMaxChars == 0
454 && request.hintMaxLines == 0
455 && flags == 0)
456 {
457 /* Obtain the selection. */
458 selection = EmacsNative.getSelection (windowHandle);
459 if (selection == null)
460 return null;
461
462 /* Create the workaround extracted text. */
463 text = new ExtractedText ();
464 text.partialStartOffset = -1;
465 text.partialEndOffset = -1;
466 text.text = "";
467 text.selectionStart = selection[0];
468 text.selectionEnd = selection[1];
469 }
470 else
471 text = EmacsNative.getExtractedText (windowHandle, request,
472 flags);
473
474 if (text == null)
475 {
476 if (EmacsService.DEBUG_IC)
477 Log.d (TAG, "getExtractedText: text is NULL");
478
479 return null;
480 }
481
482 if (EmacsService.DEBUG_IC)
483 Log.d (TAG, "getExtractedText: " + text.text + " @"
484 + text.startOffset + ":" + text.selectionStart
485 + ", " + text.selectionEnd);
486
487 return text;
488 }
489
490 @Override
491 public boolean
492 setSelection (int start, int end)
493 {
494 /* Return if the input connection is out of date. */
495 if (view.icSerial < view.icGeneration)
496 return false;
497
498 if (EmacsService.DEBUG_IC)
499 Log.d (TAG, "setSelection: " + start + " " + end);
500
501 EmacsNative.setSelection (windowHandle, start, end);
502 return true;
503 }
504
505 @Override
506 /* ACTION_MULTIPLE is apparently obsolete. */
507 @SuppressWarnings ("deprecation")
508 public boolean
509 sendKeyEvent (KeyEvent key)
510 {
511 /* Return if the input connection is out of date. */
512 if (view.icSerial < view.icGeneration)
513 return false;
514
515 if (EmacsService.DEBUG_IC)
516 Log.d (TAG, "sendKeyEvent: " + key);
517
518 /* Use the standard API if possible. */
519
520 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
521 view.imManager.dispatchKeyEventFromInputMethod (view, key);
522 else
523 {
524 /* Fall back to dispatching the event manually if not. */
525
526 switch (key.getAction ())
527 {
528 case KeyEvent.ACTION_DOWN:
529 view.onKeyDown (key.getKeyCode (), key);
530 break;
531
532 case KeyEvent.ACTION_UP:
533 view.onKeyUp (key.getKeyCode (), key);
534 break;
535
536 case KeyEvent.ACTION_MULTIPLE:
537 view.onKeyMultiple (key.getKeyCode (),
538 key.getRepeatCount (),
539 key);
540 break;
541 }
542 }
543
544 return true;
545 }
546
547 @Override
548 public boolean
549 requestCursorUpdates (int cursorUpdateMode)
550 {
551 /* Return if the input connection is out of date. */
552 if (view.icSerial < view.icGeneration)
553 return false;
554
555 if (EmacsService.DEBUG_IC)
556 Log.d (TAG, "requestCursorUpdates: " + cursorUpdateMode);
557
558 EmacsNative.requestCursorUpdates (windowHandle, cursorUpdateMode);
559 return true;
560 }
561
562 @Override
563 public boolean
564 requestCursorUpdates (int cursorUpdateMode, int filter)
565 {
566 if (filter != 0)
567 return false;
568
569 return requestCursorUpdates (cursorUpdateMode);
570 }
571
572 @Override
573 public SurroundingText
574 getSurroundingText (int beforeLength, int afterLength,
575 int flags)
576 {
577 SurroundingText text;
578
579 /* Return if the input connection is out of date. */
580 if (view.icSerial < view.icGeneration)
581 return null;
582
583 if (EmacsService.DEBUG_IC)
584 Log.d (TAG, ("getSurroundingText: " + beforeLength + ", "
585 + afterLength));
586
587 text = EmacsNative.getSurroundingText (windowHandle, beforeLength,
588 afterLength, flags);
589
590 if (EmacsService.DEBUG_IC && text != null)
591 Log.d (TAG, ("getSurroundingText: "
592 + text.getSelectionStart ()
593 + ","
594 + text.getSelectionEnd ()
595 + "+"
596 + text.getOffset ()
597 + ": "
598 + text.getText ()));
599
600 return text;
601 }
602
603 @Override
604 public TextSnapshot
605 takeSnapshot ()
606 {
607 TextSnapshot snapshot;
608
609 /* Return if the input connection is out of date. */
610 if (view.icSerial < view.icGeneration)
611 return null;
612
613 snapshot = EmacsNative.takeSnapshot (windowHandle);
614
615 if (EmacsService.DEBUG_IC)
616 Log.d (TAG, ("takeSnapshot: "
617 + snapshot.getSurroundingText ().getText ()
618 + " @ " + snapshot.getCompositionEnd ()
619 + ", " + snapshot.getCompositionStart ()));
620
621 return snapshot;
622 }
623
624 @Override
625 public void
626 closeConnection ()
627 {
628 batchEditCount = 0;
629 }
630
631
632
633 public void
634 reset ()
635 {
636 batchEditCount = 0;
637 }
638
639
640 /* Override functions which are not implemented. */
641
642 @Override
643 public Handler
644 getHandler ()
645 {
646 return null;
647 }
648
649 @Override
650 public boolean
651 commitContent (InputContentInfo inputContentInfo, int flags,
652 Bundle opts)
653 {
654 return false;
655 }
656
657 @Override
658 public boolean
659 setImeConsumesInput (boolean imeConsumesInput)
660 {
661 return false;
662 }
663
664 @Override
665 public boolean
666 clearMetaKeyStates (int states)
667 {
668 return false;
669 }
670
671 @Override
672 public boolean
673 reportFullscreenMode (boolean enabled)
674 {
675 return false;
676 }
677
678 @Override
679 public boolean
680 performSpellCheck ()
681 {
682 return false;
683 }
684
685 @Override
686 public boolean
687 performPrivateCommand (String action, Bundle data)
688 {
689 return false;
690 }
691
692 @Override
693 public int
694 getCursorCapsMode (int reqModes)
695 {
696 return 0;
697 }
698}
diff --git a/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java b/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java
new file mode 100644
index 00000000000..1e1e5d97631
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java
@@ -0,0 +1,31 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22/* This class only exists because EmacsPreferencesActivity is already
23 defined as an activity, the system wants a new class in order to
24 define a new activity, and only activities can be enabled or
25 disabled per the API level of the host. */
26
27public final class EmacsLauncherPreferencesActivity
28 extends EmacsPreferencesActivity
29{
30
31}
diff --git a/java/org/gnu/emacs/EmacsMultitaskActivity.java b/java/org/gnu/emacs/EmacsMultitaskActivity.java
new file mode 100644
index 00000000000..b1c48f03fba
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsMultitaskActivity.java
@@ -0,0 +1,29 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22/* This class only exists because EmacsActivity is already defined as
23 an activity, and the system wants a new class in order to define a
24 new activity. */
25
26public final class EmacsMultitaskActivity extends EmacsActivity
27{
28
29}
diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java
new file mode 100644
index 00000000000..fae0ba98f86
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsNative.java
@@ -0,0 +1,316 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.content.res.AssetManager;
23
24import android.graphics.Bitmap;
25
26import android.view.inputmethod.ExtractedText;
27import android.view.inputmethod.ExtractedTextRequest;
28import android.view.inputmethod.SurroundingText;
29import android.view.inputmethod.TextSnapshot;
30
31public final class EmacsNative
32{
33 /* List of native libraries that must be loaded during class
34 initialization. */
35 private static final String[] libraryDeps;
36
37
38 /* Like `dup' in C. */
39 public static native int dup (int fd);
40
41 /* Obtain the fingerprint of this build of Emacs. The fingerprint
42 can be used to determine the dump file name. */
43 public static native String getFingerprint ();
44
45 /* Set certain parameters before initializing Emacs.
46
47 assetManager must be the asset manager associated with the
48 context that is loading Emacs. It is saved and remains for the
49 remainder the lifetime of the Emacs process.
50
51 filesDir must be the package's data storage location for the
52 current Android user.
53
54 libDir must be the package's data storage location for native
55 libraries. It is used as PATH.
56
57 cacheDir must be the package's cache directory. It is used as
58 the `temporary-file-directory'.
59
60 pixelDensityX and pixelDensityY are the DPI values that will be
61 used by Emacs.
62
63 scaledDensity is the DPI value used to translate point sizes to
64 pixel sizes when loading fonts.
65
66 classPath must be the classpath of this app_process process, or
67 NULL.
68
69 emacsService must be the EmacsService singleton, or NULL.
70
71 apiLevel is the version of Android being run. */
72 public static native void setEmacsParams (AssetManager assetManager,
73 String filesDir,
74 String libDir,
75 String cacheDir,
76 float pixelDensityX,
77 float pixelDensityY,
78 float scaledDensity,
79 String classPath,
80 EmacsService emacsService,
81 int apiLevel);
82
83 /* Initialize Emacs with the argument array ARGV. Each argument
84 must contain a NULL terminated string, or else the behavior is
85 undefined.
86
87 DUMPFILE is the dump file to use, or NULL if Emacs is to load
88 loadup.el itself. */
89 public static native void initEmacs (String argv[], String dumpFile);
90
91 /* Abort and generate a native core dump. */
92 public static native void emacsAbort ();
93
94 /* Set Vquit_flag to t, resulting in Emacs quitting as soon as
95 possible. */
96 public static native void quit ();
97
98 /* Send an ANDROID_CONFIGURE_NOTIFY event. The values of all the
99 functions below are the serials of the events sent. */
100 public static native long sendConfigureNotify (short window, long time,
101 int x, int y, int width,
102 int height);
103
104 /* Send an ANDROID_KEY_PRESS event. */
105 public static native long sendKeyPress (short window, long time, int state,
106 int keyCode, int unicodeChar);
107
108 /* Send an ANDROID_KEY_RELEASE event. */
109 public static native long sendKeyRelease (short window, long time, int state,
110 int keyCode, int unicodeChar);
111
112 /* Send an ANDROID_FOCUS_IN event. */
113 public static native long sendFocusIn (short window, long time);
114
115 /* Send an ANDROID_FOCUS_OUT event. */
116 public static native long sendFocusOut (short window, long time);
117
118 /* Send an ANDROID_WINDOW_ACTION event. */
119 public static native long sendWindowAction (short window, int action);
120
121 /* Send an ANDROID_ENTER_NOTIFY event. */
122 public static native long sendEnterNotify (short window, int x, int y,
123 long time);
124
125 /* Send an ANDROID_LEAVE_NOTIFY event. */
126 public static native long sendLeaveNotify (short window, int x, int y,
127 long time);
128
129 /* Send an ANDROID_MOTION_NOTIFY event. */
130 public static native long sendMotionNotify (short window, int x, int y,
131 long time);
132
133 /* Send an ANDROID_BUTTON_PRESS event. */
134 public static native long sendButtonPress (short window, int x, int y,
135 long time, int state,
136 int button);
137
138 /* Send an ANDROID_BUTTON_RELEASE event. */
139 public static native long sendButtonRelease (short window, int x, int y,
140 long time, int state,
141 int button);
142
143 /* Send an ANDROID_TOUCH_DOWN event. */
144 public static native long sendTouchDown (short window, int x, int y,
145 long time, int pointerID,
146 int flags);
147
148 /* Send an ANDROID_TOUCH_UP event. */
149 public static native long sendTouchUp (short window, int x, int y,
150 long time, int pointerID,
151 int flags);
152
153 /* Send an ANDROID_TOUCH_MOVE event. */
154 public static native long sendTouchMove (short window, int x, int y,
155 long time, int pointerID,
156 int flags);
157
158 /* Send an ANDROID_WHEEL event. */
159 public static native long sendWheel (short window, int x, int y,
160 long time, int state,
161 float xDelta, float yDelta);
162
163 /* Send an ANDROID_ICONIFIED event. */
164 public static native long sendIconified (short window);
165
166 /* Send an ANDROID_DEICONIFIED event. */
167 public static native long sendDeiconified (short window);
168
169 /* Send an ANDROID_CONTEXT_MENU event. */
170 public static native long sendContextMenu (short window, int menuEventID,
171 int menuEventSerial);
172
173 /* Send an ANDROID_EXPOSE event. */
174 public static native long sendExpose (short window, int x, int y,
175 int width, int height);
176
177 /* Return the file name associated with the specified file
178 descriptor, or NULL if there is none. */
179 public static native byte[] getProcName (int fd);
180
181 /* Notice that the Emacs thread will now start waiting for the main
182 thread's looper to respond. */
183 public static native void beginSynchronous ();
184
185 /* Notice that the Emacs thread will has finished waiting for the
186 main thread's looper to respond. */
187 public static native void endSynchronous ();
188
189 /* Prevent deadlocks while reliably allowing queries from the Emacs
190 thread to the main thread to complete by waiting for a query to
191 start from the main thread, then answer it; assume that a query
192 is certain to start shortly. */
193 public static native void answerQuerySpin ();
194
195 /* Return whether or not KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP and
196 KEYCODE_VOLUME_MUTE should be forwarded to Emacs. */
197 public static native boolean shouldForwardMultimediaButtons ();
198
199 /* Initialize the current thread, by blocking signals that do not
200 interest it. */
201 public static native void setupSystemThread ();
202
203
204
205 /* Input connection functions. These mostly correspond to their
206 counterparts in Android's InputConnection. */
207
208 public static native void beginBatchEdit (short window);
209 public static native void endBatchEdit (short window);
210 public static native void commitCompletion (short window, String text,
211 int position);
212 public static native void commitText (short window, String text,
213 int position);
214 public static native void deleteSurroundingText (short window,
215 int leftLength,
216 int rightLength);
217 public static native void finishComposingText (short window);
218 public static native String getSelectedText (short window, int flags);
219 public static native String getTextAfterCursor (short window, int length,
220 int flags);
221 public static native String getTextBeforeCursor (short window, int length,
222 int flags);
223 public static native void setComposingText (short window, String text,
224 int newCursorPosition);
225 public static native void setComposingRegion (short window, int start,
226 int end);
227 public static native void setSelection (short window, int start, int end);
228 public static native void performEditorAction (short window,
229 int editorAction);
230 public static native void performContextMenuAction (short window,
231 int contextMenuAction);
232 public static native ExtractedText getExtractedText (short window,
233 ExtractedTextRequest req,
234 int flags);
235 public static native void requestSelectionUpdate (short window);
236 public static native void requestCursorUpdates (short window, int mode);
237 public static native void clearInputFlags (short window);
238 public static native SurroundingText getSurroundingText (short window,
239 int left, int right,
240 int flags);
241 public static native TextSnapshot takeSnapshot (short window);
242
243
244 /* Return the current value of the selection, or -1 upon
245 failure. */
246 public static native int[] getSelection (short window);
247
248
249 /* Graphics functions used as a replacement for potentially buggy
250 Android APIs. */
251
252 public static native void blitRect (Bitmap src, Bitmap dest, int x1,
253 int y1, int x2, int y2);
254
255 /* Increment the generation ID of the specified BITMAP, forcing its
256 texture to be re-uploaded to the GPU. */
257
258 public static native void notifyPixelsChanged (Bitmap bitmap);
259
260
261 /* Functions used to synchronize document provider access with the
262 main thread. */
263
264 /* Wait for a call to `safPostRequest' while also reading async
265 input.
266
267 If asynchronous input arrives and sets Vquit_flag, return 1. */
268 public static native int safSyncAndReadInput ();
269
270 /* Wait for a call to `safPostRequest'. */
271 public static native void safSync ();
272
273 /* Post the semaphore used to await the completion of SAF
274 operations. */
275 public static native void safPostRequest ();
276
277 /* Detect and return FD is writable. FD may be truncated to 0 bytes
278 in the process. */
279 public static native boolean ftruncate (int fd);
280
281 static
282 {
283 /* Older versions of Android cannot link correctly with shared
284 libraries that link with other shared libraries built along
285 Emacs unless all requisite shared libraries are explicitly
286 loaded from Java.
287
288 Every time you add a new shared library dependency to Emacs,
289 please add it here as well. */
290
291 libraryDeps = new String[] { "png_emacs", "selinux_emacs",
292 "crypto_emacs", "pcre_emacs",
293 "packagelistparser_emacs",
294 "gnutls_emacs", "gmp_emacs",
295 "nettle_emacs", "p11-kit_emacs",
296 "tasn1_emacs", "hogweed_emacs",
297 "jansson_emacs", "jpeg_emacs",
298 "tiff_emacs", "xml2_emacs",
299 "icuuc_emacs",
300 "tree-sitter_emacs", };
301
302 for (String dependency : libraryDeps)
303 {
304 try
305 {
306 System.loadLibrary (dependency);
307 }
308 catch (UnsatisfiedLinkError exception)
309 {
310 /* Ignore this exception. */
311 }
312 }
313
314 System.loadLibrary ("emacs");
315 };
316};
diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java
new file mode 100644
index 00000000000..1c7513e1cc9
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsNoninteractive.java
@@ -0,0 +1,203 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.os.Looper;
23import android.os.Build;
24
25import android.content.Context;
26import android.content.res.AssetManager;
27
28import java.lang.reflect.Constructor;
29import java.lang.reflect.Method;
30
31/* Noninteractive Emacs.
32
33 This is the class that libandroid-emacs.so starts.
34 libandroid-emacs.so figures out the system classpath, then starts
35 dalvikvm with the framework jars.
36
37 At that point, dalvikvm calls main, which sets up the main looper,
38 creates an ActivityThread and attaches it to the main thread.
39
40 Then, it obtains an application context for the LoadedApk in the
41 application thread.
42
43 Finally, it obtains the necessary context specific objects and
44 initializes Emacs. */
45
46@SuppressWarnings ("unchecked")
47public final class EmacsNoninteractive
48{
49 public static void
50 main (String[] args)
51 {
52 Object activityThread, loadedApk;
53 Class activityThreadClass, loadedApkClass, contextImplClass;
54 Class compatibilityInfoClass;
55 Method method;
56 Context context;
57 AssetManager assets;
58 String filesDir, libDir, cacheDir;
59
60 Looper.prepare ();
61 context = null;
62 assets = null;
63 filesDir = libDir = cacheDir = null;
64
65 try
66 {
67 /* Get the activity thread. */
68 activityThreadClass = Class.forName ("android.app.ActivityThread");
69
70 /* Get the systemMain method. */
71 method = activityThreadClass.getMethod ("systemMain");
72
73 /* Create and attach the activity thread. */
74 activityThread = method.invoke (null);
75 context = null;
76
77 /* Now get an LoadedApk. */
78
79 try
80 {
81 loadedApkClass = Class.forName ("android.app.LoadedApk");
82 }
83 catch (ClassNotFoundException exception)
84 {
85 /* Android 2.2 has no LoadedApk class, but fortunately it
86 does not need to be used, since contexts can be
87 directly created. */
88
89 loadedApkClass = null;
90 contextImplClass = Class.forName ("android.app.ContextImpl");
91
92 method = activityThreadClass.getDeclaredMethod ("getSystemContext");
93 context = (Context) method.invoke (activityThread);
94 method = contextImplClass.getDeclaredMethod ("createPackageContext",
95 String.class,
96 int.class);
97 method.setAccessible (true);
98 context = (Context) method.invoke (context, "org.gnu.emacs",
99 0);
100 }
101
102 /* If the context has not already been created, then do what
103 is appropriate for newer versions of Android. */
104
105 if (context == null)
106 {
107 /* Get a LoadedApk. How to do this varies by Android version.
108 On Android 2.3.3 and earlier, there is no
109 ``compatibilityInfo'' argument to getPackageInfo. */
110
111 if (Build.VERSION.SDK_INT
112 <= Build.VERSION_CODES.GINGERBREAD_MR1)
113 {
114 method
115 = activityThreadClass.getMethod ("getPackageInfo",
116 String.class,
117 int.class);
118 loadedApk = method.invoke (activityThread, "org.gnu.emacs",
119 0);
120 }
121 else
122 {
123 compatibilityInfoClass
124 = Class.forName ("android.content.res.CompatibilityInfo");
125
126 method
127 = activityThreadClass.getMethod ("getPackageInfo",
128 String.class,
129 compatibilityInfoClass,
130 int.class);
131 loadedApk = method.invoke (activityThread, "org.gnu.emacs",
132 null, 0);
133 }
134
135 if (loadedApk == null)
136 throw new RuntimeException ("getPackageInfo returned NULL");
137
138 /* Now, get a context. */
139 contextImplClass = Class.forName ("android.app.ContextImpl");
140
141 try
142 {
143 method
144 = contextImplClass.getDeclaredMethod ("createAppContext",
145 activityThreadClass,
146 loadedApkClass);
147 method.setAccessible (true);
148 context = (Context) method.invoke (null, activityThread,
149 loadedApk);
150 }
151 catch (NoSuchMethodException exception)
152 {
153 /* Older Android versions don't have createAppContext, but
154 instead require creating a ContextImpl, and then
155 calling createPackageContext. */
156 method
157 = activityThreadClass.getDeclaredMethod ("getSystemContext");
158 context = (Context) method.invoke (activityThread);
159 method
160 = contextImplClass.getDeclaredMethod ("createPackageContext",
161 String.class,
162 int.class);
163 method.setAccessible (true);
164 context = (Context) method.invoke (context, "org.gnu.emacs",
165 0);
166 }
167 }
168
169 /* Don't actually start the looper or anything. Instead, obtain
170 an AssetManager. */
171 assets = context.getAssets ();
172
173 /* Now configure Emacs. The class path should already be set. */
174
175 filesDir = context.getFilesDir ().getCanonicalPath ();
176 libDir = EmacsService.getLibraryDirectory (context);
177 cacheDir = context.getCacheDir ().getCanonicalPath ();
178 }
179 catch (Exception e)
180 {
181 System.err.println ("Internal error: " + e);
182 System.err.println ("This means that the Android platform changed,");
183 System.err.println ("and that Emacs needs adjustments in order to");
184 System.err.println ("obtain required system internal resources.");
185 System.err.println ("Please report this bug to bug-gnu-emacs@gnu.org.");
186 e.printStackTrace ();
187
188 System.exit (1);
189 }
190
191 EmacsNative.setEmacsParams (assets, filesDir,
192 libDir, cacheDir, 0.0f,
193 0.0f, 0.0f, null, null,
194 Build.VERSION.SDK_INT);
195
196 /* Now find the dump file that Emacs should use, if it has already
197 been dumped. */
198 EmacsApplication.findDumpFile (context);
199
200 /* Start Emacs. */
201 EmacsNative.initEmacs (args, EmacsApplication.dumpFileName);
202 }
203};
diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java
new file mode 100644
index 00000000000..3832cd2faab
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsOpenActivity.java
@@ -0,0 +1,552 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22/* This class makes the Emacs server work reasonably on Android.
23
24 There is no way to make the Unix socket publicly available on
25 Android.
26
27 Instead, this activity tries to connect to the Emacs server, to
28 make it open files the system asks Emacs to open, and to emulate
29 some reasonable behavior when Emacs has not yet started.
30
31 First, Emacs registers itself as an application that can open text
32 and image files.
33
34 Then, when the user is asked to open a file and selects ``Emacs''
35 as the application that will open the file, the system pops up a
36 window, this activity, and calls the `onCreate' function.
37
38 `onCreate' then tries very to find the file name of the file that
39 was selected, and give it to emacsclient.
40
41 If emacsclient successfully opens the file, then this activity
42 starts EmacsActivity (to bring it on to the screen); otherwise, it
43 displays the output of emacsclient or any error message that occurs
44 and exits. */
45
46import android.app.AlertDialog;
47import android.app.Activity;
48
49import android.content.ContentResolver;
50import android.content.DialogInterface;
51import android.content.Intent;
52
53import android.net.Uri;
54
55import android.os.Build;
56import android.os.Bundle;
57import android.os.ParcelFileDescriptor;
58
59import android.util.Log;
60
61import java.io.File;
62import java.io.FileInputStream;
63import java.io.FileNotFoundException;
64import java.io.FileOutputStream;
65import java.io.FileReader;
66import java.io.IOException;
67import java.io.InputStream;
68import java.io.UnsupportedEncodingException;
69
70public final class EmacsOpenActivity extends Activity
71 implements DialogInterface.OnClickListener,
72 DialogInterface.OnCancelListener
73{
74 private static final String TAG = "EmacsOpenActivity";
75
76 /* The name of any file that should be opened as EmacsThread starts
77 Emacs. This is never cleared, even if EmacsOpenActivity is
78 started a second time, as EmacsThread only starts once. */
79 public static String fileToOpen;
80
81 /* Any currently focused EmacsOpenActivity. Used to show pop ups
82 while the activity is active and Emacs doesn't have permission to
83 display over other programs. */
84 public static EmacsOpenActivity currentActivity;
85
86 private class EmacsClientThread extends Thread
87 {
88 private ProcessBuilder builder;
89
90 public
91 EmacsClientThread (ProcessBuilder processBuilder)
92 {
93 builder = processBuilder;
94 }
95
96 @Override
97 public void
98 run ()
99 {
100 Process process;
101 InputStream error;
102 String errorText;
103
104 try
105 {
106 /* Start emacsclient. */
107 process = builder.start ();
108 process.waitFor ();
109
110 /* Now figure out whether or not starting the process was
111 successful. */
112 if (process.exitValue () == 0)
113 finishSuccess ();
114 else
115 finishFailure ("Error opening file", null);
116 }
117 catch (IOException exception)
118 {
119 finishFailure ("Internal error", exception.toString ());
120 }
121 catch (InterruptedException exception)
122 {
123 finishFailure ("Internal error", exception.toString ());
124 }
125 }
126 }
127
128 @Override
129 public void
130 onClick (DialogInterface dialog, int which)
131 {
132 finish ();
133 }
134
135 @Override
136 public void
137 onCancel (DialogInterface dialog)
138 {
139 finish ();
140 }
141
142 public String
143 readEmacsClientLog ()
144 {
145 File file, cache;
146 FileReader reader;
147 char[] buffer;
148 int rc;
149 StringBuilder builder;
150
151 /* Because the ProcessBuilder functions necessary to redirect
152 process output are not implemented on Android 7 and earlier,
153 print a generic error message. */
154
155 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
156 return ("This is likely because the Emacs server"
157 + " is not running, or because you did"
158 + " not grant Emacs permission to access"
159 + " external storage.");
160
161 cache = getCacheDir ();
162 file = new File (cache, "emacsclient.log");
163 builder = new StringBuilder ();
164 reader = null;
165
166 try
167 {
168 reader = new FileReader (file);
169 buffer = new char[2048];
170
171 while ((rc = reader.read (buffer, 0, 2048)) != -1)
172 builder.append (buffer, 0, rc);
173
174 reader.close ();
175 return builder.toString ();
176 }
177 catch (IOException exception)
178 {
179 /* Close the reader if it's already been opened. */
180
181 try
182 {
183 if (reader != null)
184 reader.close ();
185 }
186 catch (IOException e)
187 {
188 /* Not sure what to do here. */
189 }
190
191 return ("Couldn't read emacsclient.log: "
192 + exception.toString ());
193 }
194 }
195
196 private void
197 displayFailureDialog (String title, String text)
198 {
199 AlertDialog.Builder builder;
200 AlertDialog dialog;
201
202 builder = new AlertDialog.Builder (this);
203 dialog = builder.create ();
204 dialog.setTitle (title);
205
206 if (text == null)
207 /* Read in emacsclient.log instead. */
208 text = readEmacsClientLog ();
209
210 dialog.setMessage (text);
211 dialog.setButton (DialogInterface.BUTTON_POSITIVE, "OK", this);
212 dialog.setOnCancelListener (this);
213 dialog.show ();
214 }
215
216 /* Check that the specified FILE is readable. If Android 4.4 or
217 later is being used, return URI formatted into a `/content/' file
218 name.
219
220 If it is not, then copy the file in FD to a location in the
221 system cache directory and return the name of that file. */
222
223 private String
224 checkReadableOrCopy (String file, ParcelFileDescriptor fd,
225 Uri uri)
226 throws IOException, FileNotFoundException
227 {
228 File inFile;
229 FileOutputStream outStream;
230 InputStream stream;
231 byte buffer[];
232 int read;
233 String content;
234
235 Log.d (TAG, "checkReadableOrCopy: " + file);
236
237 inFile = new File (file);
238
239 if (inFile.canRead ())
240 return file;
241
242 Log.d (TAG, "checkReadableOrCopy: NO");
243
244 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
245 {
246 content = EmacsService.buildContentName (uri);
247 Log.d (TAG, "checkReadableOrCopy: " + content);
248 return content;
249 }
250
251 /* inFile is now the file being written to. */
252 inFile = new File (getCacheDir (), inFile.getName ());
253 buffer = new byte[4098];
254
255 /* Initialize both streams to NULL. */
256 outStream = null;
257 stream = null;
258
259 try
260 {
261 outStream = new FileOutputStream (inFile);
262 stream = new FileInputStream (fd.getFileDescriptor ());
263
264 while ((read = stream.read (buffer)) >= 0)
265 outStream.write (buffer, 0, read);
266 }
267 finally
268 {
269 /* Note that this does not close FD.
270
271 Keep in mind that execution is transferred to ``finally''
272 even if an exception happens inside the while loop
273 above. */
274
275 if (stream != null)
276 stream.close ();
277
278 if (outStream != null)
279 outStream.close ();
280 }
281
282 return inFile.getCanonicalPath ();
283 }
284
285 /* Finish this activity in response to emacsclient having
286 successfully opened a file.
287
288 In the main thread, close this window, and open a window
289 belonging to an Emacs frame. */
290
291 public void
292 finishSuccess ()
293 {
294 runOnUiThread (new Runnable () {
295 @Override
296 public void
297 run ()
298 {
299 Intent intent;
300
301 intent = new Intent (EmacsOpenActivity.this,
302 EmacsActivity.class);
303
304 /* This means only an existing frame will be displayed. */
305 intent.addFlags (Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
306 startActivity (intent);
307
308 EmacsOpenActivity.this.finish ();
309 }
310 });
311 }
312
313 /* Finish this activity after displaying a dialog associated with
314 failure to open a file.
315
316 Use TITLE as the title of the dialog. If TEXT is non-NULL,
317 display that text in the dialog. Otherwise, use the contents of
318 emacsclient.log in the cache directory instead, or describe why
319 that file cannot be read. */
320
321 public void
322 finishFailure (final String title, final String text)
323 {
324 runOnUiThread (new Runnable () {
325 @Override
326 public void
327 run ()
328 {
329 displayFailureDialog (title, text);
330 }
331 });
332 }
333
334 public void
335 startEmacsClient (String fileName)
336 {
337 String libDir;
338 ProcessBuilder builder;
339 Process process;
340 EmacsClientThread thread;
341 File file;
342 Intent intent;
343
344 /* If the Emacs service is not running, then start Emacs and make
345 it open this file. */
346
347 if (EmacsService.SERVICE == null)
348 {
349 fileToOpen = fileName;
350 intent = new Intent (EmacsOpenActivity.this,
351 EmacsActivity.class);
352 finish ();
353 startActivity (intent);
354 return;
355 }
356
357 libDir = EmacsService.getLibraryDirectory (this);
358 builder = new ProcessBuilder (libDir + "/libemacsclient.so",
359 fileName, "--reuse-frame",
360 "--timeout=10", "--no-wait");
361
362 /* Redirection is unfortunately not possible in Android 7 and
363 earlier. */
364
365 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
366 {
367 file = new File (getCacheDir (), "emacsclient.log");
368
369 /* Redirect standard error to a file so that errors can be
370 meaningfully reported. */
371
372 if (file.exists ())
373 file.delete ();
374
375 builder.redirectError (file);
376 }
377
378 /* Track process output in a new thread, since this is the UI
379 thread and doing so here can cause deadlocks when EmacsService
380 decides to wait for something. */
381
382 thread = new EmacsClientThread (builder);
383 thread.start ();
384 }
385
386 /* Run emacsclient to open the file specified in the Intent that
387 caused this activity to start.
388
389 Determine the name of the file corresponding to the URI specified
390 in that intent; then, run emacsclient and wait for it to finish.
391
392 Finally, display any error message, transfer the focus to an
393 Emacs frame, and finish the activity. */
394
395 @Override
396 public void
397 onCreate (Bundle savedInstanceState)
398 {
399 String action, fileName;
400 Intent intent;
401 Uri uri;
402 ContentResolver resolver;
403 ParcelFileDescriptor fd;
404 byte[] names;
405 String errorBlurb;
406
407 super.onCreate (savedInstanceState);
408
409 /* Obtain the intent that started Emacs. */
410 intent = getIntent ();
411 action = intent.getAction ();
412
413 if (action == null)
414 {
415 finish ();
416 return;
417 }
418
419 /* Now see if the action specified is supported by Emacs. */
420
421 if (action.equals ("android.intent.action.VIEW")
422 || action.equals ("android.intent.action.EDIT")
423 || action.equals ("android.intent.action.PICK"))
424 {
425 /* Obtain the URI of the action. */
426 uri = intent.getData ();
427
428 if (uri == null)
429 {
430 finish ();
431 return;
432 }
433
434 /* Now, try to get the file name. */
435
436 if (uri.getScheme ().equals ("file"))
437 fileName = uri.getPath ();
438 else
439 {
440 fileName = null;
441
442 if (uri.getScheme ().equals ("content"))
443 {
444 /* This is one of the annoying Android ``content''
445 URIs. Most of the time, there is actually an
446 underlying file, but it cannot be found without
447 opening the file and doing readlink on its file
448 descriptor in /proc/self/fd. */
449 resolver = getContentResolver ();
450 fd = null;
451
452 try
453 {
454 fd = resolver.openFileDescriptor (uri, "r");
455 names = EmacsNative.getProcName (fd.getFd ());
456
457 /* What is the right encoding here? */
458
459 if (names != null)
460 fileName = new String (names, "UTF-8");
461
462 fileName = checkReadableOrCopy (fileName, fd, uri);
463 }
464 catch (FileNotFoundException exception)
465 {
466 /* Do nothing. */
467 }
468 catch (IOException exception)
469 {
470 /* Do nothing. */
471 }
472
473 if (fd != null)
474 {
475 try
476 {
477 fd.close ();
478 }
479 catch (IOException exception)
480 {
481 /* Do nothing. */
482 }
483 }
484 }
485
486 if (fileName == null)
487 {
488 errorBlurb = ("The URI: " + uri + " could not be opened"
489 + ", as it does not encode file name inform"
490 + "ation.");
491 displayFailureDialog ("Error opening file", errorBlurb);
492 return;
493 }
494 }
495
496 /* And start emacsclient. Set `currentActivity' to this now.
497 Presumably, it will shortly become capable of displaying
498 dialogs. */
499 currentActivity = this;
500 startEmacsClient (fileName);
501 }
502 else
503 finish ();
504 }
505
506
507
508 @Override
509 public void
510 onDestroy ()
511 {
512 Log.d (TAG, "onDestroy: " + this);
513
514 /* Clear `currentActivity' if it refers to the activity being
515 destroyed. */
516
517 if (currentActivity == this)
518 this.currentActivity = null;
519
520 super.onDestroy ();
521 }
522
523 @Override
524 public void
525 onWindowFocusChanged (boolean isFocused)
526 {
527 Log.d (TAG, "onWindowFocusChanged: " + this + ", is now focused: "
528 + isFocused);
529
530 if (isFocused)
531 currentActivity = this;
532 else if (currentActivity == this)
533 currentActivity = null;
534
535 super.onWindowFocusChanged (isFocused);
536 }
537
538 @Override
539 public void
540 onPause ()
541 {
542 Log.d (TAG, "onPause: " + this);
543
544 /* XXX: clear currentActivity here as well; I don't know whether
545 or not onWindowFocusChanged is always called prior to this. */
546
547 if (currentActivity == this)
548 currentActivity = null;
549
550 super.onPause ();
551 }
552}
diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java
new file mode 100644
index 00000000000..eb011bc5e65
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsPixmap.java
@@ -0,0 +1,192 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.lang.IllegalArgumentException;
23
24import android.graphics.Bitmap;
25import android.graphics.Canvas;
26import android.graphics.Rect;
27
28import android.os.Build;
29
30/* Drawable backed by bitmap. */
31
32public final class EmacsPixmap extends EmacsHandleObject
33 implements EmacsDrawable
34{
35 /* The depth of the bitmap. This is not actually used, just defined
36 in order to be consistent with X. */
37 public int depth, width, height;
38
39 /* The bitmap itself. */
40 public Bitmap bitmap;
41
42 /* The canvas used to draw to BITMAP. */
43 public Canvas canvas;
44
45 /* Whether or not GC should be explicitly triggered upon
46 release. */
47 private boolean needCollect;
48
49 /* ID used to determine whether or not the GC clip rects
50 changed. */
51 private long gcClipRectID;
52
53 public
54 EmacsPixmap (short handle, int colors[], int width,
55 int height, int depth)
56 {
57 super (handle);
58
59 if (depth != 1 && depth != 24)
60 throw new IllegalArgumentException ("Invalid depth specified"
61 + " for pixmap: " + depth);
62
63 switch (depth)
64 {
65 case 1:
66 bitmap = Bitmap.createBitmap (colors, width, height,
67 Bitmap.Config.ALPHA_8);
68 break;
69
70 case 24:
71 bitmap = Bitmap.createBitmap (colors, width, height,
72 Bitmap.Config.ARGB_8888);
73 bitmap.setHasAlpha (false);
74 break;
75 }
76
77 this.width = width;
78 this.height = height;
79 this.depth = depth;
80 }
81
82 public
83 EmacsPixmap (short handle, int width, int height, int depth)
84 {
85 super (handle);
86
87 if (depth != 1 && depth != 24)
88 throw new IllegalArgumentException ("Invalid depth specified"
89 + " for pixmap: " + depth);
90
91 switch (depth)
92 {
93 case 1:
94 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
95 bitmap = Bitmap.createBitmap (width, height,
96 Bitmap.Config.ALPHA_8,
97 false);
98 else
99 bitmap = Bitmap.createBitmap (width, height,
100 Bitmap.Config.ALPHA_8);
101 break;
102
103 case 24:
104
105 /* Emacs doesn't just use the first kind of `createBitmap'
106 because the latter allows specifying that the pixmap is
107 always opaque, which really increases efficiency. */
108 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
109 bitmap = Bitmap.createBitmap (width, height,
110 Bitmap.Config.ARGB_8888);
111 else
112 bitmap = Bitmap.createBitmap (width, height,
113 Bitmap.Config.ARGB_8888,
114 false);
115 break;
116 }
117
118 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1)
119 /* On these old versions of Android, Bitmap.recycle frees bitmap
120 contents immediately. */
121 needCollect = false;
122 else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
123 needCollect = (bitmap.getByteCount ()
124 >= 1024 * 512);
125 else
126 needCollect = (bitmap.getAllocationByteCount ()
127 >= 1024 * 512);
128
129 bitmap.eraseColor (0xff000000);
130
131 this.width = width;
132 this.height = height;
133 this.depth = depth;
134 }
135
136 @Override
137 public Canvas
138 lockCanvas (EmacsGC gc)
139 {
140 int i;
141
142 if (canvas == null)
143 {
144 canvas = new Canvas (bitmap);
145 canvas.save ();
146 }
147
148 /* Now see if clipping has to be redone. */
149 if (gc.clipRectID == gcClipRectID)
150 return canvas;
151
152 /* It does have to be redone. Reapply gc.real_clip_rects. */
153 canvas.restore ();
154 canvas.save ();
155
156 if (gc.real_clip_rects != null)
157 {
158 for (i = 0; i < gc.real_clip_rects.length; ++i)
159 canvas.clipRect (gc.real_clip_rects[i]);
160 }
161
162 /* Save the clip rect ID again. */
163 gcClipRectID = gc.clipRectID;
164 return canvas;
165 }
166
167 @Override
168 public void
169 damageRect (Rect damageRect)
170 {
171
172 }
173
174 @Override
175 public Bitmap
176 getBitmap ()
177 {
178 return bitmap;
179 }
180
181 @Override
182 public void
183 destroyHandle ()
184 {
185 bitmap.recycle ();
186 bitmap = null;
187
188 /* Collect the bitmap storage if the bitmap is big. */
189 if (needCollect)
190 Runtime.getRuntime ().gc ();
191 }
192};
diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java
new file mode 100644
index 00000000000..7e67cc3679b
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java
@@ -0,0 +1,168 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.io.File;
23
24import android.app.Activity;
25
26import android.content.Intent;
27
28import android.os.Bundle;
29import android.os.Build;
30
31import android.widget.Toast;
32
33import android.preference.*;
34
35/* This module provides a ``preferences'' display for Emacs. It is
36 supposed to be launched from inside the Settings application to
37 perform various actions, such as starting Emacs with the ``-Q''
38 option, which would not be possible otherwise, as there is no
39 command line on Android.
40
41 Android provides a preferences activity, but it is deprecated.
42 Unfortunately, there is no alternative that looks the same way. */
43
44@SuppressWarnings ("deprecation")
45public class EmacsPreferencesActivity extends PreferenceActivity
46{
47 /* Restart Emacs with -Q. Call EmacsThread.exit to kill Emacs now,
48 and tell the system to start EmacsActivity with some parameters
49 later. */
50
51 private void
52 startEmacsQ ()
53 {
54 Intent intent;
55
56 intent = new Intent (this, EmacsActivity.class);
57 intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK
58 | Intent.FLAG_ACTIVITY_CLEAR_TASK);
59 intent.putExtra ("org.gnu.emacs.STARTUP_ARGUMENT", "--quick");
60 startActivity (intent);
61 System.exit (0);
62 }
63
64 /* Restart Emacs with `--debug-init'. Call EmacsThread.exit to kill
65 Emacs now, and tell the system to EmacsActivity with some
66 parameters later. */
67
68 private void
69 startEmacsDebugInit ()
70 {
71 Intent intent;
72
73 intent = new Intent (this, EmacsActivity.class);
74 intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK
75 | Intent.FLAG_ACTIVITY_CLEAR_TASK);
76 intent.putExtra ("org.gnu.emacs.STARTUP_ARGUMENT", "--debug-init");
77 startActivity (intent);
78 System.exit (0);
79 }
80
81 /* Erase Emacs's dump file. */
82
83 private void
84 eraseDumpFile ()
85 {
86 String wantedDumpFile;
87 File file;
88 Toast toast;
89
90 wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint ()
91 + ".pdmp");
92 file = new File (getFilesDir (), wantedDumpFile);
93
94 if (file.exists ())
95 file.delete ();
96
97 /* Make sure to clear EmacsApplication.dumpFileName, or
98 starting Emacs without restarting this program will
99 make Emacs try to load a nonexistent dump file. */
100 EmacsApplication.dumpFileName = null;
101
102 /* Display a message stating that the dump file has been
103 erased. */
104 toast = Toast.makeText (this, "Dump file removed",
105 Toast.LENGTH_SHORT);
106 toast.show ();
107 }
108
109 @Override
110 public final void
111 onCreate (Bundle savedInstanceState)
112 {
113 Preference tem;
114 Preference.OnPreferenceClickListener listener;
115
116 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
117 setTheme (android.R.style.Theme_DeviceDefault_Settings);
118 else if (Build.VERSION.SDK_INT
119 >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
120 setTheme (android.R.style.Theme_DeviceDefault);
121
122 /* This must come before using any preference APIs. */
123 super.onCreate (savedInstanceState);
124
125 /* Add preferences from the XML file where they are defined. */
126 addPreferencesFromResource (R.xml.preferences);
127
128 /* Now, set up on click handlers for each of the preferences
129 items. */
130
131 tem = findPreference ("start_quick");
132 listener = new Preference.OnPreferenceClickListener () {
133 @Override
134 public boolean
135 onPreferenceClick (Preference preference)
136 {
137 startEmacsQ ();
138 return true;
139 }
140 };
141
142 tem.setOnPreferenceClickListener (listener);
143 tem = findPreference ("start_debug_init");
144 listener = new Preference.OnPreferenceClickListener () {
145 @Override
146 public boolean
147 onPreferenceClick (Preference preference)
148 {
149 startEmacsDebugInit ();
150 return true;
151 }
152 };
153
154 tem.setOnPreferenceClickListener (listener);
155 tem = findPreference ("erase_dump");
156 listener = new Preference.OnPreferenceClickListener () {
157 @Override
158 public boolean
159 onPreferenceClick (Preference preference)
160 {
161 eraseDumpFile ();
162 return true;
163 }
164 };
165
166 tem.setOnPreferenceClickListener (listener);
167 }
168};
diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java
new file mode 100644
index 00000000000..3ae3c0839ce
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSafThread.java
@@ -0,0 +1,1687 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.util.Collection;
23import java.util.HashMap;
24import java.util.Iterator;
25
26import java.io.FileNotFoundException;
27import java.io.IOException;
28
29import android.content.ContentResolver;
30import android.database.Cursor;
31import android.net.Uri;
32
33import android.os.Build;
34import android.os.CancellationSignal;
35import android.os.Handler;
36import android.os.HandlerThread;
37import android.os.OperationCanceledException;
38import android.os.ParcelFileDescriptor;
39import android.os.SystemClock;
40
41import android.util.Log;
42
43import android.provider.DocumentsContract;
44import android.provider.DocumentsContract.Document;
45
46
47
48/* Emacs runs long-running SAF operations on a second thread running
49 its own handler. These operations include opening files and
50 maintaining the path to document ID cache.
51
52 Because Emacs paths are based on file display names, while Android
53 document identifiers have no discernible hierarchy of their own,
54 each file name lookup must carry out a repeated search for
55 directory documents with the names of all of the file name's
56 constituent components, where each iteration searches within the
57 directory document identified by the previous iteration.
58
59 A time limited cache tying components to document IDs is maintained
60 in order to speed up consecutive searches for file names sharing
61 the same components. Since listening for changes to each document
62 in the cache is prohibitively expensive, Emacs instead elects to
63 periodically remove entries that are older than a predetermined
64 amount of a time.
65
66 The cache is split into two levels: the first caches the
67 relationships between display names and document IDs, while the
68 second caches individual document IDs and their contents (children,
69 type, etc.)
70
71 Long-running operations are also run on this thread for another
72 reason: Android uses special cancellation objects to terminate
73 ongoing IPC operations. However, the functions that perform these
74 operations block instead of providing mechanisms for the caller to
75 wait for their completion while also reading async input, as a
76 consequence of which the calling thread is unable to signal the
77 cancellation objects that it provides. Performing the blocking
78 operations in this auxiliary thread enables the main thread to wait
79 for completion itself, signaling the cancellation objects when it
80 deems necessary. */
81
82
83
84public final class EmacsSafThread extends HandlerThread
85{
86 private static final String TAG = "EmacsSafThread";
87
88 /* The content resolver used by this thread. */
89 private final ContentResolver resolver;
90
91 /* Map between tree URIs and the cache entry representing its
92 toplevel directory. */
93 private final HashMap<Uri, CacheToplevel> cacheToplevels;
94
95 /* Handler for this thread's main loop. */
96 private Handler handler;
97
98 /* File access mode constants. See `man 7 inode'. */
99 public static final int S_IRUSR = 0000400;
100 public static final int S_IWUSR = 0000200;
101 public static final int S_IXUSR = 0000100;
102 public static final int S_IFCHR = 0020000;
103 public static final int S_IFDIR = 0040000;
104 public static final int S_IFREG = 0100000;
105
106 /* Number of seconds in between each attempt to prune the storage
107 cache. */
108 public static final int CACHE_PRUNE_TIME = 10;
109
110 /* Number of seconds after which an entry in the cache is to be
111 considered invalid. */
112 public static final int CACHE_INVALID_TIME = 10;
113
114 public
115 EmacsSafThread (ContentResolver resolver)
116 {
117 super ("Document provider access thread");
118 this.resolver = resolver;
119 this.cacheToplevels = new HashMap<Uri, CacheToplevel> ();
120 }
121
122
123
124 @Override
125 public void
126 start ()
127 {
128 super.start ();
129
130 /* Set up the handler after the thread starts. */
131 handler = new Handler (getLooper ());
132
133 /* And start periodically pruning the cache. */
134 postPruneMessage ();
135 }
136
137
138 private static final class CacheToplevel
139 {
140 /* Map between document names and children. */
141 HashMap<String, DocIdEntry> children;
142
143 /* Map between document names and file status. */
144 HashMap<String, StatCacheEntry> statCache;
145
146 /* Map between document IDs and cache items. */
147 HashMap<String, CacheEntry> idCache;
148 };
149
150 private static final class StatCacheEntry
151 {
152 /* The time at which this cache entry was created. */
153 long time;
154
155 /* Flags, size, and modification time of this file. */
156 long flags, size, mtime;
157
158 /* Whether or not this file is a directory. */
159 boolean isDirectory;
160
161 public
162 StatCacheEntry ()
163 {
164 time = SystemClock.uptimeMillis ();
165 }
166
167 public boolean
168 isValid ()
169 {
170 return ((SystemClock.uptimeMillis () - time)
171 < CACHE_INVALID_TIME * 1000);
172 }
173 };
174
175 private static final class DocIdEntry
176 {
177 /* The document ID. */
178 String documentId;
179
180 /* The time this entry was created. */
181 long time;
182
183 public
184 DocIdEntry ()
185 {
186 time = SystemClock.uptimeMillis ();
187 }
188
189 /* Return a cache entry comprised of the state of the file
190 identified by `documentId'. TREE is the URI of the tree
191 containing this entry, and TOPLEVEL is the toplevel
192 representing it. SIGNAL is a cancellation signal.
193
194 RESOLVER is the content provider used to retrieve file
195 information.
196
197 Value is NULL if the file cannot be found. */
198
199 public CacheEntry
200 getCacheEntry (ContentResolver resolver, Uri tree,
201 CacheToplevel toplevel,
202 CancellationSignal signal)
203 {
204 Uri uri;
205 String[] projection;
206 String type;
207 Cursor cursor;
208 int column;
209 CacheEntry entry;
210
211 /* Create a document URI representing DOCUMENTID within URI's
212 authority. */
213
214 uri = DocumentsContract.buildDocumentUriUsingTree (tree,
215 documentId);
216 projection = new String[] {
217 Document.COLUMN_MIME_TYPE,
218 };
219
220 cursor = null;
221
222 try
223 {
224 cursor = resolver.query (uri, projection, null,
225 null, null, signal);
226
227 if (!cursor.moveToFirst ())
228 return null;
229
230 column = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
231
232 if (column < 0)
233 return null;
234
235 type = cursor.getString (column);
236
237 if (type == null)
238 return null;
239
240 entry = new CacheEntry ();
241 entry.type = type;
242 toplevel.idCache.put (documentId, entry);
243 return entry;
244 }
245 catch (OperationCanceledException e)
246 {
247 throw e;
248 }
249 catch (Throwable e)
250 {
251 return null;
252 }
253 finally
254 {
255 if (cursor != null)
256 cursor.close ();
257 }
258 }
259
260 public boolean
261 isValid ()
262 {
263 return ((SystemClock.uptimeMillis () - time)
264 < CACHE_INVALID_TIME * 1000);
265 }
266 };
267
268 private static final class CacheEntry
269 {
270 /* The type of this document. */
271 String type;
272
273 /* Map between document names and children. */
274 HashMap<String, DocIdEntry> children;
275
276 /* The time this entry was created. */
277 long time;
278
279 public
280 CacheEntry ()
281 {
282 children = new HashMap<String, DocIdEntry> ();
283 time = SystemClock.uptimeMillis ();
284 }
285
286 public boolean
287 isValid ()
288 {
289 return ((SystemClock.uptimeMillis () - time)
290 < CACHE_INVALID_TIME * 1000);
291 }
292 };
293
294 /* Create or return a toplevel for the given tree URI. */
295
296 private CacheToplevel
297 getCache (Uri uri)
298 {
299 CacheToplevel toplevel;
300
301 toplevel = cacheToplevels.get (uri);
302
303 if (toplevel != null)
304 return toplevel;
305
306 toplevel = new CacheToplevel ();
307 toplevel.children = new HashMap<String, DocIdEntry> ();
308 toplevel.statCache = new HashMap<String, StatCacheEntry> ();
309 toplevel.idCache = new HashMap<String, CacheEntry> ();
310 cacheToplevels.put (uri, toplevel);
311 return toplevel;
312 }
313
314 /* Remove each cache entry within COLLECTION older than
315 CACHE_INVALID_TIME. */
316
317 private void
318 pruneCache1 (Collection<DocIdEntry> collection)
319 {
320 Iterator<DocIdEntry> iter;
321 DocIdEntry tem;
322
323 iter = collection.iterator ();
324 while (iter.hasNext ())
325 {
326 /* Get the cache entry. */
327 tem = iter.next ();
328
329 /* If it's not valid anymore, remove it. Iterating over a
330 collection whose contents are being removed is undefined
331 unless the removal is performed using the iterator's own
332 `remove' function, so tem.remove cannot be used here. */
333
334 if (tem.isValid ())
335 continue;
336
337 iter.remove ();
338 }
339 }
340
341 /* Remove every entry older than CACHE_INVALID_TIME from each
342 toplevel inside `cachedToplevels'. */
343
344 private void
345 pruneCache ()
346 {
347 Iterator<CacheEntry> iter;
348 Iterator<StatCacheEntry> statIter;
349 CacheEntry tem;
350 StatCacheEntry stat;
351
352 for (CacheToplevel toplevel : cacheToplevels.values ())
353 {
354 /* First, clean up expired cache entries. */
355 iter = toplevel.idCache.values ().iterator ();
356
357 while (iter.hasNext ())
358 {
359 /* Get the cache entry. */
360 tem = iter.next ();
361
362 /* If it's not valid anymore, remove it. Iterating over a
363 collection whose contents are being removed is
364 undefined unless the removal is performed using the
365 iterator's own `remove' function, so tem.remove cannot
366 be used here. */
367
368 if (tem.isValid ())
369 {
370 /* Otherwise, clean up expired items in its document
371 ID cache. */
372 pruneCache1 (tem.children.values ());
373 continue;
374 }
375
376 iter.remove ();
377 }
378
379 statIter = toplevel.statCache.values ().iterator ();
380
381 while (statIter.hasNext ())
382 {
383 /* Get the cache entry. */
384 stat = statIter.next ();
385
386 /* If it's not valid anymore, remove it. Iterating over a
387 collection whose contents are being removed is
388 undefined unless the removal is performed using the
389 iterator's own `remove' function, so tem.remove cannot
390 be used here. */
391
392 if (stat.isValid ())
393 continue;
394
395 statIter.remove ();
396 }
397 }
398
399 postPruneMessage ();
400 }
401
402 /* Cache file information within TOPLEVEL, under the list of
403 children CHILDREN.
404
405 NAME, ID, and TYPE should respectively be the display name of the
406 document within its parent document (the CacheEntry whose
407 `children' field is CHILDREN), its document ID, and its MIME
408 type.
409
410 If ID_ENTRY_EXISTS, don't create a new document ID entry within
411 CHILDREN indexed by NAME.
412
413 Value is the cache entry saved for the document ID. */
414
415 private CacheEntry
416 cacheChild (CacheToplevel toplevel,
417 HashMap<String, DocIdEntry> children,
418 String name, String id, String type,
419 boolean id_entry_exists)
420 {
421 DocIdEntry idEntry;
422 CacheEntry cacheEntry;
423
424 if (!id_entry_exists)
425 {
426 idEntry = new DocIdEntry ();
427 idEntry.documentId = id;
428 children.put (name, idEntry);
429 }
430
431 cacheEntry = new CacheEntry ();
432 cacheEntry.type = type;
433 toplevel.idCache.put (id, cacheEntry);
434 return cacheEntry;
435 }
436
437 /* Cache file status for DOCUMENTID within TOPLEVEL. Value is the
438 new cache entry. CURSOR is the cursor from where to retrieve the
439 file status, in the form of the columns COLUMN_FLAGS,
440 COLUMN_SIZE, COLUMN_MIME_TYPE and COLUMN_LAST_MODIFIED. */
441
442 private StatCacheEntry
443 cacheFileStatus (String documentId, CacheToplevel toplevel,
444 Cursor cursor)
445 {
446 StatCacheEntry entry;
447 int flagsIndex, columnIndex, typeIndex;
448 int sizeIndex, mtimeIndex;
449 String type;
450
451 /* Obtain the indices for columns wanted from this cursor. */
452 flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS);
453 sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE);
454 typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
455 mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED);
456
457 /* COLUMN_LAST_MODIFIED is allowed to be absent in a
458 conforming documents provider. */
459 if (flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0)
460 return null;
461
462 /* Get the file status from CURSOR. */
463 entry = new StatCacheEntry ();
464 entry.flags = cursor.getInt (flagsIndex);
465 type = cursor.getString (typeIndex);
466
467 if (type == null)
468 return null;
469
470 entry.isDirectory = type.equals (Document.MIME_TYPE_DIR);
471
472 if (cursor.isNull (sizeIndex))
473 /* The size is unknown. */
474 entry.size = -1;
475 else
476 entry.size = cursor.getLong (sizeIndex);
477
478 /* mtimeIndex is potentially unset, since document providers
479 aren't obligated to provide modification times. */
480
481 if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex))
482 entry.mtime = cursor.getLong (mtimeIndex);
483
484 /* Finally, add this entry to the cache and return. */
485 toplevel.statCache.put (documentId, entry);
486 return entry;
487 }
488
489 /* Cache the type and as many of the children of the directory
490 designated by DOCUMENTID as possible into TOPLEVEL.
491
492 CURSOR should be a cursor representing an open directory stream,
493 with its projection consisting of at least the display name,
494 document ID and MIME type columns.
495
496 Rewind the position of CURSOR to before its first element after
497 completion. */
498
499 private void
500 cacheDirectoryFromCursor (CacheToplevel toplevel, String documentId,
501 Cursor cursor)
502 {
503 CacheEntry entry, constitutent;
504 int nameColumn, idColumn, typeColumn;
505 String id, name, type;
506 DocIdEntry idEntry;
507
508 /* Find the numbers of the columns wanted. */
509
510 nameColumn
511 = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
512 idColumn
513 = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
514 typeColumn
515 = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
516
517 if (nameColumn < 0 || idColumn < 0 || typeColumn < 0)
518 return;
519
520 entry = new CacheEntry ();
521
522 /* We know this is a directory already. */
523 entry.type = Document.MIME_TYPE_DIR;
524 toplevel.idCache.put (documentId, entry);
525
526 /* Now, try to cache each of its constituents. */
527
528 while (cursor.moveToNext ())
529 {
530 try
531 {
532 name = cursor.getString (nameColumn);
533 id = cursor.getString (idColumn);
534 type = cursor.getString (typeColumn);
535
536 if (name == null || id == null || type == null)
537 continue;
538
539 /* First, add the name and ID to ENTRY's map of
540 children. */
541 idEntry = new DocIdEntry ();
542 idEntry.documentId = id;
543 entry.children.put (id, idEntry);
544
545 /* Cache the file status for ID within TOPELVEL too; if a
546 directory listing is being requested, it's very likely
547 that a series of calls for file status will follow. */
548
549 cacheFileStatus (id, toplevel, cursor);
550
551 /* If this constituent is a directory, don't cache any
552 information about it. It cannot be cached without
553 knowing its children. */
554
555 if (type.equals (Document.MIME_TYPE_DIR))
556 continue;
557
558 /* Otherwise, create a new cache entry comprised of its
559 type. */
560 constitutent = new CacheEntry ();
561 constitutent.type = type;
562 toplevel.idCache.put (documentId, entry);
563 }
564 catch (Exception e)
565 {
566 e.printStackTrace ();
567 continue;
568 }
569 }
570
571 /* Rewind cursor back to the beginning. */
572 cursor.moveToPosition (-1);
573 }
574
575 /* Post a message to run `pruneCache' every CACHE_PRUNE_TIME
576 seconds. */
577
578 private void
579 postPruneMessage ()
580 {
581 handler.postDelayed (new Runnable () {
582 @Override
583 public void
584 run ()
585 {
586 pruneCache ();
587 }
588 }, CACHE_PRUNE_TIME * 1000);
589 }
590
591 /* Invalidate the cache entry denoted by DOCUMENT_ID, within the
592 document tree URI.
593 Call this after deleting a document or directory.
594
595 At the same time, remove the final component within the file name
596 CACHENAME from the cache if it exists. */
597
598 public void
599 postInvalidateCache (final Uri uri, final String documentId,
600 final String cacheName)
601 {
602 handler.post (new Runnable () {
603 @Override
604 public void
605 run ()
606 {
607 CacheToplevel toplevel;
608 HashMap<String, DocIdEntry> children;
609 String[] components;
610 CacheEntry entry;
611 DocIdEntry idEntry;
612
613 toplevel = getCache (uri);
614 toplevel.idCache.remove (documentId);
615 toplevel.statCache.remove (documentId);
616
617 /* If the parent of CACHENAME is cached, remove it. */
618
619 children = toplevel.children;
620 components = cacheName.split ("/");
621
622 for (String component : components)
623 {
624 /* Java `split' removes trailing empty matches but not
625 leading or intermediary ones. */
626 if (component.isEmpty ())
627 continue;
628
629 if (component == components[components.length - 1])
630 {
631 /* This is the last component, so remove it from
632 children. */
633 children.remove (component);
634 return;
635 }
636 else
637 {
638 /* Search for this component within the last level
639 of the cache. */
640
641 idEntry = children.get (component);
642
643 if (idEntry == null)
644 /* Not cached, so return. */
645 return;
646
647 entry = toplevel.idCache.get (idEntry.documentId);
648
649 if (entry == null)
650 /* Not cached, so return. */
651 return;
652
653 /* Locate the next component within this
654 directory. */
655 children = entry.children;
656 }
657 }
658 }
659 });
660 }
661
662 /* Invalidate the cache entry denoted by DOCUMENT_ID, within the
663 document tree URI.
664 Call this after deleting a document or directory.
665
666 At the same time, remove the child referring to DOCUMENTID from
667 within CACHENAME's cache entry if it exists. */
668
669 public void
670 postInvalidateCacheDir (final Uri uri, final String documentId,
671 final String cacheName)
672 {
673 handler.post (new Runnable () {
674 @Override
675 public void
676 run ()
677 {
678 CacheToplevel toplevel;
679 HashMap<String, DocIdEntry> children;
680 String[] components;
681 CacheEntry entry;
682 DocIdEntry idEntry;
683 Iterator<DocIdEntry> iter;
684
685 toplevel = getCache (uri);
686 toplevel.idCache.remove (documentId);
687 toplevel.statCache.remove (documentId);
688
689 /* Now remove DOCUMENTID from CACHENAME's cache entry, if
690 any. */
691
692 children = toplevel.children;
693 components = cacheName.split ("/");
694
695 for (String component : components)
696 {
697 /* Java `split' removes trailing empty matches but not
698 leading or intermediary ones. */
699 if (component.isEmpty ())
700 continue;
701
702 /* Search for this component within the last level
703 of the cache. */
704
705 idEntry = children.get (component);
706
707 if (idEntry == null)
708 /* Not cached, so return. */
709 return;
710
711 entry = toplevel.idCache.get (idEntry.documentId);
712
713 if (entry == null)
714 /* Not cached, so return. */
715 return;
716
717 /* Locate the next component within this
718 directory. */
719 children = entry.children;
720 }
721
722 iter = children.values ().iterator ();
723 while (iter.hasNext ())
724 {
725 idEntry = iter.next ();
726
727 if (idEntry.documentId.equals (documentId))
728 {
729 iter.remove ();
730 break;
731 }
732 }
733 }
734 });
735 }
736
737 /* Invalidate the file status cache entry for DOCUMENTID within URI.
738 Call this when the contents of a file (i.e. the constituents of a
739 directory file) may have changed, but the document's display name
740 has not. */
741
742 public void
743 postInvalidateStat (final Uri uri, final String documentId)
744 {
745 handler.post (new Runnable () {
746 @Override
747 public void
748 run ()
749 {
750 CacheToplevel toplevel;
751
752 toplevel = getCache (uri);
753 toplevel.statCache.remove (documentId);
754 }
755 });
756 }
757
758
759
760 /* ``Prototypes'' for nested functions that are run within the SAF
761 thread and accepts a cancellation signal. They differ in their
762 return types. */
763
764 private abstract class SafIntFunction
765 {
766 /* The ``throws Throwable'' here is a Java idiosyncracy that tells
767 the compiler to allow arbitrary error objects to be signaled
768 from within this function.
769
770 Later, runIntFunction will try to re-throw any error object
771 generated by this function in the Emacs thread, using a trick
772 to avoid the compiler requirement to expressly declare that an
773 error (and which types of errors) will be signaled. */
774
775 public abstract int runInt (CancellationSignal signal)
776 throws Throwable;
777 };
778
779 private abstract class SafObjectFunction
780 {
781 /* The ``throws Throwable'' here is a Java idiosyncracy that tells
782 the compiler to allow arbitrary error objects to be signaled
783 from within this function.
784
785 Later, runObjectFunction will try to re-throw any error object
786 generated by this function in the Emacs thread, using a trick
787 to avoid the compiler requirement to expressly declare that an
788 error (and which types of errors) will be signaled. */
789
790 public abstract Object runObject (CancellationSignal signal)
791 throws Throwable;
792 };
793
794
795
796 /* Functions that run cancel-able queries. These functions are
797 internally run within the SAF thread. */
798
799 /* Throw the specified EXCEPTION. The type template T is erased by
800 the compiler before the object is compiled, so the compiled code
801 simply throws EXCEPTION without the cast being verified.
802
803 T should be RuntimeException to obtain the desired effect of
804 throwing an exception without a compiler check. */
805
806 @SuppressWarnings("unchecked")
807 private static <T extends Throwable> void
808 throwException (Throwable exception)
809 throws T
810 {
811 throw (T) exception;
812 }
813
814 /* Run the given function (or rather, its `runInt' field) within the
815 SAF thread, waiting for it to complete.
816
817 If async input arrives in the meantime and sets Vquit_flag,
818 signal the cancellation signal supplied to that function.
819
820 Rethrow any exception thrown from that function, and return its
821 value otherwise. */
822
823 private int
824 runIntFunction (final SafIntFunction function)
825 {
826 final EmacsHolder<Object> result;
827 final CancellationSignal signal;
828 Throwable throwable;
829
830 result = new EmacsHolder<Object> ();
831 signal = new CancellationSignal ();
832
833 handler.post (new Runnable () {
834 @Override
835 public void
836 run ()
837 {
838 try
839 {
840 result.thing
841 = Integer.valueOf (function.runInt (signal));
842 }
843 catch (Throwable throwable)
844 {
845 result.thing = throwable;
846 }
847
848 EmacsNative.safPostRequest ();
849 }
850 });
851
852 if (EmacsNative.safSyncAndReadInput () != 0)
853 {
854 signal.cancel ();
855
856 /* Now wait for the function to finish. Either the signal has
857 arrived after the query took place, in which case it will
858 finish normally, or an OperationCanceledException will be
859 thrown. */
860
861 EmacsNative.safSync ();
862 }
863
864 if (result.thing instanceof Throwable)
865 {
866 throwable = (Throwable) result.thing;
867 EmacsSafThread.<RuntimeException>throwException (throwable);
868 }
869
870 return (Integer) result.thing;
871 }
872
873 /* Run the given function (or rather, its `runObject' field) within
874 the SAF thread, waiting for it to complete.
875
876 If async input arrives in the meantime and sets Vquit_flag,
877 signal the cancellation signal supplied to that function.
878
879 Rethrow any exception thrown from that function, and return its
880 value otherwise. */
881
882 private Object
883 runObjectFunction (final SafObjectFunction function)
884 {
885 final EmacsHolder<Object> result;
886 final CancellationSignal signal;
887 Throwable throwable;
888
889 result = new EmacsHolder<Object> ();
890 signal = new CancellationSignal ();
891
892 handler.post (new Runnable () {
893 @Override
894 public void
895 run ()
896 {
897 try
898 {
899 result.thing = function.runObject (signal);
900 }
901 catch (Throwable throwable)
902 {
903 result.thing = throwable;
904 }
905
906 EmacsNative.safPostRequest ();
907 }
908 });
909
910 if (EmacsNative.safSyncAndReadInput () != 0)
911 {
912 signal.cancel ();
913
914 /* Now wait for the function to finish. Either the signal has
915 arrived after the query took place, in which case it will
916 finish normally, or an OperationCanceledException will be
917 thrown. */
918
919 EmacsNative.safSync ();
920 }
921
922 if (result.thing instanceof Throwable)
923 {
924 throwable = (Throwable) result.thing;
925 EmacsSafThread.<RuntimeException>throwException (throwable);
926 }
927
928 return result.thing;
929 }
930
931 /* The crux of `documentIdFromName1', run within the SAF thread.
932 SIGNAL should be a cancellation signal run upon quitting. */
933
934 private int
935 documentIdFromName1 (String tree_uri, String name,
936 String[] id_return, CancellationSignal signal)
937 {
938 Uri uri, treeUri;
939 String id, type, newId, newType;
940 String[] components, projection;
941 Cursor cursor;
942 int nameColumn, idColumn, typeColumn;
943 CacheToplevel toplevel;
944 DocIdEntry idEntry;
945 HashMap<String, DocIdEntry> children, next;
946 CacheEntry cache;
947
948 projection = new String[] {
949 Document.COLUMN_DISPLAY_NAME,
950 Document.COLUMN_DOCUMENT_ID,
951 Document.COLUMN_MIME_TYPE,
952 };
953
954 /* Parse the URI identifying the tree first. */
955 uri = Uri.parse (tree_uri);
956
957 /* Now, split NAME into its individual components. */
958 components = name.split ("/");
959
960 /* Set id and type to the value at the root of the tree. */
961 type = id = null;
962 cursor = null;
963
964 /* Obtain the top level of this cache. */
965 toplevel = getCache (uri);
966
967 /* Set the current map of children to this top level. */
968 children = toplevel.children;
969
970 /* For each component... */
971
972 try
973 {
974 for (String component : components)
975 {
976 /* Java split doesn't behave very much like strtok when it
977 comes to trailing and leading delimiters... */
978 if (component.isEmpty ())
979 continue;
980
981 /* Search for component within the currently cached list
982 of children. */
983
984 idEntry = children.get (component);
985
986 if (idEntry != null)
987 {
988 /* The document ID is known. Now find the
989 corresponding document ID cache. */
990
991 cache = toplevel.idCache.get (idEntry.documentId);
992
993 /* Fetch just the information for this document. */
994
995 if (cache == null)
996 cache = idEntry.getCacheEntry (resolver, uri, toplevel,
997 signal);
998
999 if (cache == null)
1000 {
1001 /* File status matching idEntry could not be
1002 obtained. Treat this as if the file does not
1003 exist. */
1004
1005 children.remove (component);
1006
1007 if (id == null)
1008 id = DocumentsContract.getTreeDocumentId (uri);
1009
1010 id_return[0] = id;
1011
1012 if ((type == null
1013 || type.equals (Document.MIME_TYPE_DIR))
1014 /* ... and type and id currently represent the
1015 penultimate component. */
1016 && component == components[components.length - 1])
1017 return -2;
1018
1019 return -1;
1020 }
1021
1022 /* Otherwise, use the cached information. */
1023 id = idEntry.documentId;
1024 type = cache.type;
1025 children = cache.children;
1026 continue;
1027 }
1028
1029 /* Create the tree URI for URI from ID if it exists, or
1030 the root otherwise. */
1031
1032 if (id == null)
1033 id = DocumentsContract.getTreeDocumentId (uri);
1034
1035 treeUri
1036 = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id);
1037
1038 /* Look for a file in this directory by the name of
1039 component. */
1040
1041 cursor = resolver.query (treeUri, projection,
1042 (Document.COLUMN_DISPLAY_NAME
1043 + " = ?"),
1044 new String[] { component, },
1045 null, signal);
1046
1047 if (cursor == null)
1048 return -1;
1049
1050 /* Find the column numbers for each of the columns that
1051 are wanted. */
1052
1053 nameColumn
1054 = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
1055 idColumn
1056 = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
1057 typeColumn
1058 = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1059
1060 if (nameColumn < 0 || idColumn < 0 || typeColumn < 0)
1061 return -1;
1062
1063 next = null;
1064
1065 while (true)
1066 {
1067 /* Even though the query selects for a specific
1068 display name, some content providers nevertheless
1069 return every file within the directory. */
1070
1071 if (!cursor.moveToNext ())
1072 {
1073 /* If a component has been found, break out of the
1074 loop. */
1075
1076 if (next != null)
1077 break;
1078
1079 /* If the last component considered is a
1080 directory... */
1081 if ((type == null
1082 || type.equals (Document.MIME_TYPE_DIR))
1083 /* ... and type and id currently represent the
1084 penultimate component. */
1085 && component == components[components.length - 1])
1086 {
1087 /* The cursor is empty. In this case, return
1088 -2 and the current document ID (belonging
1089 to the previous component) in
1090 ID_RETURN. */
1091
1092 id_return[0] = id;
1093
1094 /* But return -1 on the off chance that id is
1095 null. */
1096
1097 if (id == null)
1098 return -1;
1099
1100 return -2;
1101 }
1102
1103 /* The last component found is not a directory, so
1104 return -1. */
1105 return -1;
1106 }
1107
1108 /* So move CURSOR to a row with the right display
1109 name. */
1110
1111 name = cursor.getString (nameColumn);
1112 newId = cursor.getString (idColumn);
1113 newType = cursor.getString (typeColumn);
1114
1115 /* Any of the three variables above may be NULL if the
1116 column data is of the wrong type depending on how
1117 the Cursor returned is implemented. */
1118
1119 if (name == null || newId == null || newType == null)
1120 return -1;
1121
1122 /* Cache this name, even if it isn't the document
1123 that's being searched for. */
1124
1125 cache = cacheChild (toplevel, children, name,
1126 newId, newType,
1127 idEntry != null);
1128
1129 /* Record the desired component once it is located,
1130 but continue reading and caching items from the
1131 cursor. */
1132
1133 if (name.equals (component))
1134 {
1135 id = newId;
1136 next = cache.children;
1137 type = newType;
1138 }
1139 }
1140
1141 children = next;
1142
1143 /* Now close the cursor. */
1144 cursor.close ();
1145 cursor = null;
1146
1147 /* ID may have become NULL if the data is in an invalid
1148 format. */
1149 if (id == null)
1150 return -1;
1151 }
1152 }
1153 finally
1154 {
1155 /* If an error is thrown within the block above, let
1156 android_saf_exception_check handle it, but make sure the
1157 cursor is closed. */
1158
1159 if (cursor != null)
1160 cursor.close ();
1161 }
1162
1163 /* Here, id is either NULL (meaning the same as TREE_URI), and
1164 type is either NULL (in which case id should also be NULL) or
1165 the MIME type of the file. */
1166
1167 /* First return the ID. */
1168
1169 if (id == null)
1170 id_return[0] = DocumentsContract.getTreeDocumentId (uri);
1171 else
1172 id_return[0] = id;
1173
1174 /* Next, return whether or not this is a directory. */
1175 if (type == null || type.equals (Document.MIME_TYPE_DIR))
1176 return 1;
1177
1178 return 0;
1179 }
1180
1181 /* Find the document ID of the file within TREE_URI designated by
1182 NAME.
1183
1184 NAME is a ``file name'' comprised of the display names of
1185 individual files. Each constituent component prior to the last
1186 must name a directory file within TREE_URI.
1187
1188 Upon success, return 0 or 1 (contingent upon whether or not the
1189 last component within NAME is a directory) and place the document
1190 ID of the named file in ID_RETURN[0].
1191
1192 If the designated file can't be located, but each component of
1193 NAME up to the last component can and is a directory, return -2
1194 and the ID of the last component located in ID_RETURN[0].
1195
1196 If the designated file can't be located, return -1, or signal one
1197 of OperationCanceledException, SecurityException,
1198 FileNotFoundException, or UnsupportedOperationException. */
1199
1200 public int
1201 documentIdFromName (final String tree_uri, final String name,
1202 final String[] id_return)
1203 {
1204 return runIntFunction (new SafIntFunction () {
1205 @Override
1206 public int
1207 runInt (CancellationSignal signal)
1208 {
1209 return documentIdFromName1 (tree_uri, name, id_return,
1210 signal);
1211 }
1212 });
1213 }
1214
1215 /* The bulk of `statDocument'. SIGNAL should be a cancelation
1216 signal. */
1217
1218 private long[]
1219 statDocument1 (String uri, String documentId,
1220 CancellationSignal signal)
1221 {
1222 Uri uriObject, tree;
1223 String[] projection;
1224 long[] stat;
1225 Cursor cursor;
1226 CacheToplevel toplevel;
1227 StatCacheEntry cache;
1228
1229 tree = Uri.parse (uri);
1230
1231 if (documentId == null)
1232 documentId = DocumentsContract.getTreeDocumentId (tree);
1233
1234 /* Create a document URI representing DOCUMENTID within URI's
1235 authority. */
1236
1237 uriObject
1238 = DocumentsContract.buildDocumentUriUsingTree (tree, documentId);
1239
1240 /* See if the file status cache currently contains this
1241 document. */
1242
1243 toplevel = getCache (tree);
1244 cache = toplevel.statCache.get (documentId);
1245
1246 if (cache == null || !cache.isValid ())
1247 {
1248 /* Stat this document and enter its information into the
1249 cache. */
1250
1251 projection = new String[] {
1252 Document.COLUMN_FLAGS,
1253 Document.COLUMN_LAST_MODIFIED,
1254 Document.COLUMN_MIME_TYPE,
1255 Document.COLUMN_SIZE,
1256 };
1257
1258 cursor = resolver.query (uriObject, projection, null,
1259 null, null, signal);
1260
1261 if (cursor == null)
1262 return null;
1263
1264 try
1265 {
1266 if (!cursor.moveToFirst ())
1267 return null;
1268
1269 cache = cacheFileStatus (documentId, toplevel, cursor);
1270 }
1271 finally
1272 {
1273 cursor.close ();
1274 }
1275
1276 /* If cache is still null, return null. */
1277
1278 if (cache == null)
1279 return null;
1280 }
1281
1282 /* Create the array of file status and populate it with the
1283 information within cache. */
1284 stat = new long[3];
1285
1286 stat[0] |= S_IRUSR;
1287 if ((cache.flags & Document.FLAG_SUPPORTS_WRITE) != 0)
1288 stat[0] |= S_IWUSR;
1289
1290 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
1291 && (cache.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
1292 stat[0] |= S_IFCHR;
1293
1294 stat[1] = cache.size;
1295
1296 /* Check if this is a directory file. */
1297 if (cache.isDirectory
1298 /* Files shouldn't be specials and directories at the same
1299 time, but Android doesn't forbid document providers
1300 from returning this information. */
1301 && (stat[0] & S_IFCHR) == 0)
1302 {
1303 /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories,
1304 just assume they're writable. */
1305 stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR;
1306
1307 /* Directory files cannot be modified if
1308 FLAG_DIR_SUPPORTS_CREATE is not set. */
1309
1310 if ((cache.flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
1311 stat[0] &= ~S_IWUSR;
1312 }
1313
1314 /* If this file is neither a character special nor a
1315 directory, indicate that it's a regular file. */
1316
1317 if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0)
1318 stat[0] |= S_IFREG;
1319
1320 stat[2] = cache.mtime;
1321 return stat;
1322 }
1323
1324 /* Return file status for the document designated by the given
1325 DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document
1326 ID in URI itself.
1327
1328 Value is null upon failure, or an array of longs [MODE, SIZE,
1329 MTIM] upon success, where MODE contains the file type and access
1330 modes of the file as in `struct stat', SIZE is the size of the
1331 file in BYTES or -1 if not known, and MTIM is the time of the
1332 last modification to this file in milliseconds since 00:00,
1333 January 1st, 1970.
1334
1335 OperationCanceledException and other typical exceptions may be
1336 signaled upon receiving async input or other errors. */
1337
1338 public long[]
1339 statDocument (final String uri, final String documentId)
1340 {
1341 return (long[]) runObjectFunction (new SafObjectFunction () {
1342 @Override
1343 public Object
1344 runObject (CancellationSignal signal)
1345 {
1346 return statDocument1 (uri, documentId, signal);
1347 }
1348 });
1349 }
1350
1351 /* The bulk of `accessDocument'. SIGNAL should be a cancellation
1352 signal. */
1353
1354 private int
1355 accessDocument1 (String uri, String documentId, boolean writable,
1356 CancellationSignal signal)
1357 {
1358 Uri uriObject;
1359 String[] projection;
1360 int tem, index;
1361 String tem1;
1362 Cursor cursor;
1363 CacheToplevel toplevel;
1364 CacheEntry entry;
1365
1366 uriObject = Uri.parse (uri);
1367
1368 if (documentId == null)
1369 documentId = DocumentsContract.getTreeDocumentId (uriObject);
1370
1371 /* If WRITABLE is false and the document ID is cached, use its
1372 cached value instead. This speeds up
1373 `directory-files-with-attributes' a little. */
1374
1375 if (!writable)
1376 {
1377 toplevel = getCache (uriObject);
1378 entry = toplevel.idCache.get (documentId);
1379
1380 if (entry != null)
1381 return 0;
1382 }
1383
1384 /* Create a document URI representing DOCUMENTID within URI's
1385 authority. */
1386
1387 uriObject
1388 = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
1389
1390 /* Now stat this document. */
1391
1392 projection = new String[] {
1393 Document.COLUMN_FLAGS,
1394 Document.COLUMN_MIME_TYPE,
1395 };
1396
1397 cursor = resolver.query (uriObject, projection, null,
1398 null, null, signal);
1399
1400 if (cursor == null)
1401 return -1;
1402
1403 try
1404 {
1405 if (!cursor.moveToFirst ())
1406 return -1;
1407
1408 if (!writable)
1409 return 0;
1410
1411 index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1412 if (index < 0)
1413 return -3;
1414
1415 /* Get the type of this file to check if it's a directory. */
1416 tem1 = cursor.getString (index);
1417
1418 /* Check if this is a directory file. */
1419 if (tem1.equals (Document.MIME_TYPE_DIR))
1420 {
1421 /* If so, don't check for FLAG_SUPPORTS_WRITE.
1422 Check for FLAG_DIR_SUPPORTS_CREATE instead. */
1423
1424 if (!writable)
1425 return 0;
1426
1427 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
1428 if (index < 0)
1429 return -3;
1430
1431 tem = cursor.getInt (index);
1432 if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
1433 return -3;
1434
1435 return 0;
1436 }
1437
1438 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
1439 if (index < 0)
1440 return -3;
1441
1442 tem = cursor.getInt (index);
1443 if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0)
1444 return -3;
1445 }
1446 finally
1447 {
1448 /* Close the cursor if an exception occurs. */
1449 cursor.close ();
1450 }
1451
1452 return 0;
1453 }
1454
1455 /* Find out whether Emacs has access to the document designated by
1456 the specified DOCUMENTID within the tree URI. If DOCUMENTID is
1457 NULL, use the document ID in URI itself.
1458
1459 If WRITABLE, also check that the file is writable, which is true
1460 if it is either a directory or its flags contains
1461 FLAG_SUPPORTS_WRITE.
1462
1463 Value is 0 if the file is accessible, and one of the following if
1464 not:
1465
1466 -1, if the file does not exist.
1467 -2, if WRITABLE and the file is not writable.
1468 -3, upon any other error.
1469
1470 In addition, arbitrary runtime exceptions (such as
1471 SecurityException or UnsupportedOperationException) may be
1472 thrown. */
1473
1474 public int
1475 accessDocument (final String uri, final String documentId,
1476 final boolean writable)
1477 {
1478 return runIntFunction (new SafIntFunction () {
1479 @Override
1480 public int
1481 runInt (CancellationSignal signal)
1482 {
1483 return accessDocument1 (uri, documentId, writable,
1484 signal);
1485 }
1486 });
1487 }
1488
1489 /* The crux of openDocumentDirectory. SIGNAL must be a cancellation
1490 signal. */
1491
1492 private Cursor
1493 openDocumentDirectory1 (String uri, String documentId,
1494 CancellationSignal signal)
1495 {
1496 Uri uriObject, tree;
1497 Cursor cursor;
1498 String projection[];
1499 CacheToplevel toplevel;
1500
1501 tree = uriObject = Uri.parse (uri);
1502
1503 /* If documentId is not set, use the document ID of the tree URI
1504 itself. */
1505
1506 if (documentId == null)
1507 documentId = DocumentsContract.getTreeDocumentId (uriObject);
1508
1509 /* Build a URI representing each directory entry within
1510 DOCUMENTID. */
1511
1512 uriObject
1513 = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject,
1514 documentId);
1515
1516 projection = new String [] {
1517 Document.COLUMN_DISPLAY_NAME,
1518 Document.COLUMN_DOCUMENT_ID,
1519 Document.COLUMN_MIME_TYPE,
1520 Document.COLUMN_FLAGS,
1521 Document.COLUMN_LAST_MODIFIED,
1522 Document.COLUMN_SIZE,
1523 };
1524
1525 cursor = resolver.query (uriObject, projection, null, null,
1526 null, signal);
1527
1528 /* Create a new cache entry tied to this document ID. */
1529
1530 if (cursor != null)
1531 {
1532 toplevel = getCache (tree);
1533 cacheDirectoryFromCursor (toplevel, documentId,
1534 cursor);
1535 }
1536
1537 /* Return the cursor. */
1538 return cursor;
1539 }
1540
1541 /* Open a cursor representing each entry within the directory
1542 designated by the specified DOCUMENTID within the tree URI.
1543
1544 If DOCUMENTID is NULL, use the document ID within URI itself.
1545 Value is NULL upon failure.
1546
1547 In addition, arbitrary runtime exceptions (such as
1548 SecurityException or UnsupportedOperationException) may be
1549 thrown. */
1550
1551 public Cursor
1552 openDocumentDirectory (final String uri, final String documentId)
1553 {
1554 return (Cursor) runObjectFunction (new SafObjectFunction () {
1555 @Override
1556 public Object
1557 runObject (CancellationSignal signal)
1558 {
1559 return openDocumentDirectory1 (uri, documentId, signal);
1560 }
1561 });
1562 }
1563
1564 /* The crux of `openDocument'. SIGNAL must be a cancellation
1565 signal. */
1566
1567 public ParcelFileDescriptor
1568 openDocument1 (String uri, String documentId, boolean write,
1569 boolean truncate, CancellationSignal signal)
1570 throws Throwable
1571 {
1572 Uri treeUri, documentUri;
1573 String mode;
1574 ParcelFileDescriptor fileDescriptor;
1575 CacheToplevel toplevel;
1576
1577 treeUri = Uri.parse (uri);
1578
1579 /* documentId must be set for this request, since it doesn't make
1580 sense to ``open'' the root of the directory tree. */
1581
1582 documentUri
1583 = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId);
1584
1585 /* Select the mode used to open the file. */
1586
1587 if (write)
1588 {
1589 if (truncate)
1590 mode = "rwt";
1591 else
1592 mode = "rw";
1593 }
1594 else
1595 mode = "r";
1596
1597 fileDescriptor
1598 = resolver.openFileDescriptor (documentUri, mode,
1599 signal);
1600
1601 /* If a writable file descriptor is requested and TRUNCATE is set,
1602 then probe the file descriptor to detect if it is actually
1603 readable. If not, close this file descriptor and reopen it
1604 with MODE set to rw; some document providers granting access to
1605 Samba shares don't implement rwt, but these document providers
1606 invariably truncate the file opened even when the mode is
1607 merely rw.
1608
1609 This may be ascribed to a mix-up in Android's documentation
1610 regardin DocumentsProvider: the `openDocument' function is only
1611 documented to accept r or rw, whereas the default
1612 implementation of the `openFile' function (which documents rwt)
1613 delegates to `openDocument'. */
1614
1615 if (write && truncate && fileDescriptor != null
1616 && !EmacsNative.ftruncate (fileDescriptor.getFd ()))
1617 {
1618 try
1619 {
1620 fileDescriptor.closeWithError ("File descriptor requested"
1621 + " is not writable");
1622 }
1623 catch (IOException e)
1624 {
1625 Log.w (TAG, "Leaking unclosed file descriptor " + e);
1626 }
1627
1628 fileDescriptor
1629 = resolver.openFileDescriptor (documentUri, "rw", signal);
1630
1631 /* Try to truncate fileDescriptor just to stay on the safe
1632 side. */
1633 if (fileDescriptor != null)
1634 EmacsNative.ftruncate (fileDescriptor.getFd ());
1635 }
1636
1637 /* Every time a document is opened, remove it from the file status
1638 cache. */
1639 toplevel = getCache (treeUri);
1640 toplevel.statCache.remove (documentId);
1641
1642 return fileDescriptor;
1643 }
1644
1645 /* Open a file descriptor for a file document designated by
1646 DOCUMENTID within the document tree identified by URI. If
1647 TRUNCATE and the document already exists, truncate its contents
1648 before returning.
1649
1650 On Android 9.0 and earlier, always open the document in
1651 ``read-write'' mode; this instructs the document provider to
1652 return a seekable file that is stored on disk and returns correct
1653 file status.
1654
1655 Under newer versions of Android, open the document in a
1656 non-writable mode if WRITE is false. This is possible because
1657 these versions allow Emacs to explicitly request a seekable
1658 on-disk file.
1659
1660 Value is NULL upon failure or a parcel file descriptor upon
1661 success. Call `ParcelFileDescriptor.close' on this file
1662 descriptor instead of using the `close' system call.
1663
1664 FileNotFoundException and/or SecurityException and/or
1665 UnsupportedOperationException and/or OperationCanceledException
1666 may be thrown upon failure. */
1667
1668 public ParcelFileDescriptor
1669 openDocument (final String uri, final String documentId,
1670 final boolean write, final boolean truncate)
1671 {
1672 Object tem;
1673
1674 tem = runObjectFunction (new SafObjectFunction () {
1675 @Override
1676 public Object
1677 runObject (CancellationSignal signal)
1678 throws Throwable
1679 {
1680 return openDocument1 (uri, documentId, write, truncate,
1681 signal);
1682 }
1683 });
1684
1685 return (ParcelFileDescriptor) tem;
1686 }
1687};
diff --git a/java/org/gnu/emacs/EmacsSdk11Clipboard.java b/java/org/gnu/emacs/EmacsSdk11Clipboard.java
new file mode 100644
index 00000000000..4959ec36eed
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSdk11Clipboard.java
@@ -0,0 +1,290 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.content.ClipboardManager;
23import android.content.Context;
24import android.content.ContentResolver;
25import android.content.ClipData;
26import android.content.ClipDescription;
27
28import android.content.res.AssetFileDescriptor;
29
30import android.net.Uri;
31
32import android.util.Log;
33
34import android.os.Build;
35
36import java.io.FileNotFoundException;
37import java.io.IOException;
38import java.io.UnsupportedEncodingException;
39
40/* This class implements EmacsClipboard for Android 3.0 and later
41 systems. */
42
43public final class EmacsSdk11Clipboard extends EmacsClipboard
44 implements ClipboardManager.OnPrimaryClipChangedListener
45{
46 private static final String TAG = "EmacsSdk11Clipboard";
47 private ClipboardManager manager;
48 private boolean ownsClipboard;
49 private int clipboardChangedCount;
50 private int monitoredClipboardChangedCount;
51 private ContentResolver resolver;
52
53 public
54 EmacsSdk11Clipboard ()
55 {
56 manager = EmacsService.SERVICE.getClipboardManager ();
57
58 /* The system forbids Emacs from reading clipboard data in the
59 background under Android 10 or later. */
60
61 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
62 manager.addPrimaryClipChangedListener (this);
63
64 /* Now obtain the content resolver used to open file
65 descriptors. */
66
67 resolver = EmacsService.SERVICE.getContentResolver ();
68 }
69
70 @Override
71 public synchronized void
72 onPrimaryClipChanged ()
73 {
74 Log.d (TAG, ("onPrimaryClipChanged: "
75 + monitoredClipboardChangedCount
76 + " " + clipboardChangedCount));
77
78 /* Increment monitoredClipboardChangeCount. If it is now greater
79 than clipboardChangedCount, then Emacs no longer owns the
80 clipboard. */
81 monitoredClipboardChangedCount++;
82
83 if (monitoredClipboardChangedCount > clipboardChangedCount)
84 {
85 ownsClipboard = false;
86
87 /* Reset both values back to 0. */
88 monitoredClipboardChangedCount = 0;
89 clipboardChangedCount = 0;
90 }
91 }
92
93 /* Set the clipboard text to CLIPBOARD, a string in UTF-8
94 encoding. */
95
96 @Override
97 public synchronized void
98 setClipboard (byte[] bytes)
99 {
100 ClipData data;
101 String string;
102
103 try
104 {
105 string = new String (bytes, "UTF-8");
106 data = ClipData.newPlainText ("Emacs", string);
107 manager.setPrimaryClip (data);
108 ownsClipboard = true;
109
110 /* onPrimaryClipChanged will be called again. Use this
111 variable to keep track of how many times the clipboard has
112 been changed. */
113 ++clipboardChangedCount;
114 }
115 catch (UnsupportedEncodingException exception)
116 {
117 Log.w (TAG, "setClipboard: " + exception);
118 }
119 }
120
121 /* Return whether or not Emacs owns the clipboard. Value is 1 if
122 Emacs does, 0 if Emacs does not, and -1 if that information is
123 unavailable. */
124
125 @Override
126 public synchronized int
127 ownsClipboard ()
128 {
129 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
130 return -1;
131
132 return ownsClipboard ? 1 : 0;
133 }
134
135 /* Return whether or not clipboard content currently exists. */
136
137 @Override
138 public boolean
139 clipboardExists ()
140 {
141 return manager.hasPrimaryClip ();
142 }
143
144 /* Return the current content of the clipboard, as plain text, or
145 NULL if no content is available. */
146
147 @Override
148 public byte[]
149 getClipboard ()
150 {
151 ClipData clip;
152 CharSequence text;
153 Context context;
154
155 clip = manager.getPrimaryClip ();
156
157 if (clip == null || clip.getItemCount () < 1)
158 return null;
159
160 context = EmacsService.SERVICE;
161
162 try
163 {
164 text = clip.getItemAt (0).coerceToText (context);
165 return text.toString ().getBytes ("UTF-8");
166 }
167 catch (UnsupportedEncodingException exception)
168 {
169 Log.w (TAG, "getClipboard: " + exception);
170 }
171
172 return null;
173 }
174
175 /* Return an array of targets currently provided by the
176 clipboard, or NULL if there are none. */
177
178 @Override
179 public byte[][]
180 getClipboardTargets ()
181 {
182 ClipData clip;
183 ClipDescription description;
184 byte[][] typeArray;
185 int i;
186
187 /* N.B. that Android calls the clipboard the ``primary clip''; it
188 is not related to the X primary selection. */
189 clip = manager.getPrimaryClip ();
190 description = clip.getDescription ();
191 i = description.getMimeTypeCount ();
192 typeArray = new byte[i][i];
193
194 try
195 {
196 for (i = 0; i < description.getMimeTypeCount (); ++i)
197 typeArray[i] = description.getMimeType (i).getBytes ("UTF-8");
198 }
199 catch (UnsupportedEncodingException exception)
200 {
201 return null;
202 }
203
204 return typeArray;
205 }
206
207 /* Return the clipboard data for the given target, or NULL if it
208 does not exist.
209
210 Value is normally an array of three longs: the file descriptor,
211 the start offset of the data, and its length; length may be
212 AssetFileDescriptor.UNKOWN_LENGTH, meaning that the data extends
213 from that offset to the end of the file.
214
215 Do not use this function to open text targets; use `getClipboard'
216 for that instead, as it will handle selection data consisting
217 solely of a URI. */
218
219 @Override
220 public long[]
221 getClipboardData (byte[] target)
222 {
223 ClipData data;
224 String mimeType;
225 int fd;
226 AssetFileDescriptor assetFd;
227 Uri uri;
228 long[] value;
229
230 /* Decode the target given by Emacs. */
231 try
232 {
233 mimeType = new String (target, "UTF-8");
234 }
235 catch (UnsupportedEncodingException exception)
236 {
237 return null;
238 }
239
240 Log.d (TAG, "getClipboardData: "+ mimeType);
241
242 /* Now obtain the clipboard data and the data corresponding to
243 that MIME type. */
244
245 data = manager.getPrimaryClip ();
246
247 if (data.getItemCount () < 1)
248 return null;
249
250 try
251 {
252 uri = data.getItemAt (0).getUri ();
253
254 if (uri == null)
255 return null;
256
257 Log.d (TAG, "getClipboardData: "+ uri);
258
259 /* Now open the file descriptor. */
260 assetFd = resolver.openTypedAssetFileDescriptor (uri, mimeType,
261 null);
262
263 /* Duplicate the file descriptor. */
264 fd = assetFd.getParcelFileDescriptor ().getFd ();
265 fd = EmacsNative.dup (fd);
266
267 /* Return the relevant information. */
268 value = new long[] { fd, assetFd.getStartOffset (),
269 assetFd.getLength (), };
270
271 /* Close the original offset. */
272 assetFd.close ();
273
274 Log.d (TAG, "getClipboardData: "+ value);
275 }
276 catch (FileNotFoundException e)
277 {
278 return null;
279 }
280 catch (IOException e)
281 {
282 return null;
283 }
284
285 /* Don't return value if the file descriptor couldn't be
286 created. */
287
288 return fd != -1 ? value : null;
289 }
290};
diff --git a/java/org/gnu/emacs/EmacsSdk23FontDriver.java b/java/org/gnu/emacs/EmacsSdk23FontDriver.java
new file mode 100644
index 00000000000..aaba8dbd166
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSdk23FontDriver.java
@@ -0,0 +1,114 @@
1/* Font backend for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Paint;
23import android.graphics.Rect;
24
25public final class EmacsSdk23FontDriver extends EmacsSdk7FontDriver
26{
27 private void
28 textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics,
29 Paint paint, Rect bounds)
30 {
31 char[] text;
32
33 text = new char[2];
34 text[0] = (char) code;
35 text[1] = 'c';
36
37 paint.getTextBounds (text, 0, 1, bounds);
38
39 metrics.lbearing = (short) bounds.left;
40 metrics.rbearing = (short) bounds.right;
41 metrics.ascent = (short) -bounds.top;
42 metrics.descent = (short) bounds.bottom;
43 metrics.width
44 = (short) paint.getRunAdvance (text, 0, 1, 0, 1, false, 1);
45 }
46
47 @Override
48 public void
49 textExtents (FontObject font, int code[], FontMetrics fontMetrics)
50 {
51 int i;
52 Paint paintCache;
53 Rect boundsCache;
54 Sdk7FontObject fontObject;
55 char[] text;
56 float width;
57
58 fontObject = (Sdk7FontObject) font;
59 paintCache = fontObject.typeface.typefacePaint;
60 paintCache.setTextSize (fontObject.pixelSize);
61 boundsCache = new Rect ();
62
63 if (code.length == 0)
64 {
65 fontMetrics.lbearing = 0;
66 fontMetrics.rbearing = 0;
67 fontMetrics.ascent = 0;
68 fontMetrics.descent = 0;
69 fontMetrics.width = 0;
70 }
71 else if (code.length == 1)
72 textExtents1 ((Sdk7FontObject) font, code[0], fontMetrics,
73 paintCache, boundsCache);
74 else
75 {
76 text = new char[code.length + 1];
77
78 for (i = 0; i < code.length; ++i)
79 text[i] = (char) code[i];
80
81 text[code.length] = 'c';
82
83 paintCache.getTextBounds (text, 0, code.length,
84 boundsCache);
85 width = paintCache.getRunAdvance (text, 0, code.length, 0,
86 code.length,
87 false, code.length);
88
89 fontMetrics.lbearing = (short) boundsCache.left;
90 fontMetrics.rbearing = (short) boundsCache.right;
91 fontMetrics.ascent = (short) -boundsCache.top;
92 fontMetrics.descent = (short) boundsCache.bottom;
93 fontMetrics.width = (short) width;
94 }
95 }
96
97 @Override
98 public int
99 hasChar (FontSpec font, char charCode)
100 {
101 Sdk7FontObject fontObject;
102 Paint paint;
103
104 if (font instanceof Sdk7FontObject)
105 {
106 fontObject = (Sdk7FontObject) font;
107 paint = fontObject.typeface.typefacePaint;
108 }
109 else
110 paint = ((Sdk7FontEntity) font).typeface.typefacePaint;
111
112 return paint.hasGlyph (String.valueOf (charCode)) ? 1 : 0;
113 }
114};
diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java
new file mode 100644
index 00000000000..97969585d16
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java
@@ -0,0 +1,539 @@
1/* Font backend for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.io.File;
23
24import java.util.LinkedList;
25import java.util.List;
26
27import android.graphics.Paint;
28import android.graphics.Rect;
29import android.graphics.Typeface;
30import android.graphics.Canvas;
31
32import android.util.Log;
33
34public class EmacsSdk7FontDriver extends EmacsFontDriver
35{
36 private static final String TOFU_STRING = "\uDB3F\uDFFD";
37 private static final String EM_STRING = "m";
38 private static final String TAG = "EmacsSdk7FontDriver";
39
40 protected static final class Sdk7Typeface
41 {
42 /* The typeface and paint. */
43 public Typeface typeface;
44 public Paint typefacePaint;
45 public String familyName;
46 public int slant, width, weight, spacing;
47
48 public
49 Sdk7Typeface (String fileName, Typeface typeface)
50 {
51 String style, testString;
52 int index, measured, i;
53 float[] widths;
54
55 slant = NORMAL;
56 weight = REGULAR;
57 width = UNSPECIFIED;
58 spacing = PROPORTIONAL;
59
60 this.typeface = typeface;
61
62 typefacePaint = new Paint ();
63 typefacePaint.setAntiAlias (true);
64 typefacePaint.setTypeface (typeface);
65
66 /* For the calls to measureText below. */
67 typefacePaint.setTextSize (10.0f);
68
69 /* Parse the file name into some useful data. First, strip off
70 the extension. */
71 fileName = fileName.split ("\\.", 2)[0];
72
73 /* Next, split the file name by dashes. Everything before the
74 last dash is part of the family name. */
75 index = fileName.lastIndexOf ("-");
76
77 if (index > 0)
78 {
79 style = fileName.substring (index + 1, fileName.length ());
80 familyName = fileName.substring (0, index);
81
82 /* Look for something describing the weight. */
83 if (style.contains ("Thin"))
84 weight = THIN;
85 else if (style.contains ("UltraLight"))
86 weight = ULTRA_LIGHT;
87 else if (style.contains ("SemiLight"))
88 weight = SEMI_LIGHT;
89 else if (style.contains ("Light"))
90 weight = LIGHT;
91 else if (style.contains ("Medium"))
92 weight = MEDIUM;
93 else if (style.contains ("SemiBold"))
94 weight = SEMI_BOLD;
95 else if (style.contains ("ExtraBold"))
96 weight = EXTRA_BOLD;
97 else if (style.contains ("Bold"))
98 weight = BOLD;
99 else if (style.contains ("Black"))
100 weight = BLACK;
101 else if (style.contains ("UltraHeavy"))
102 weight = ULTRA_HEAVY;
103
104 /* And the slant. */
105 if (style.contains ("ReverseOblique"))
106 slant = OBLIQUE;
107 else if (style.contains ("ReverseItalic"))
108 slant = REVERSE_ITALIC;
109 else if (style.contains ("Italic"))
110 slant = ITALIC;
111 else if (style.contains ("Oblique"))
112 slant = OBLIQUE;
113
114 /* Finally, the width. */
115 if (style.contains ("UltraCondensed"))
116 width = ULTRA_CONDENSED;
117 else if (style.contains ("ExtraCondensed"))
118 width = EXTRA_CONDENSED;
119 else if (style.contains ("SemiCondensed"))
120 width = SEMI_CONDENSED;
121 else if (style.contains ("Condensed"))
122 width = CONDENSED;
123 else if (style.contains ("SemiExpanded"))
124 width = SEMI_EXPANDED;
125 else if (style.contains ("ExtraExpanded"))
126 width = EXTRA_EXPANDED;
127 else if (style.contains ("UltraExpanded"))
128 width = ULTRA_EXPANDED;
129 else if (style.contains ("Expanded"))
130 width = EXPANDED;
131
132 /* Guess the spacing information. */
133 testString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
134 widths = new float[testString.length ()];
135
136 measured = typefacePaint.getTextWidths (testString,
137 0, testString.length (),
138 widths);
139 spacing = MONO;
140 for (i = 0; i < measured; ++i)
141 {
142 if (i != 0 && widths[i - 1] != widths[i])
143 /* This isn't a monospace font. */
144 spacing = PROPORTIONAL;
145 }
146 }
147 else
148 familyName = fileName;
149 }
150
151 @Override
152 public String
153 toString ()
154 {
155 return ("Sdk7Typeface ("
156 + String.valueOf (familyName) + ", "
157 + String.valueOf (slant) + ", "
158 + String.valueOf (width) + ", "
159 + String.valueOf (weight) + ", "
160 + String.valueOf (spacing) + ")");
161 }
162 };
163
164 protected static final class Sdk7FontEntity extends FontEntity
165 {
166 /* The typeface. */
167 public Sdk7Typeface typeface;
168
169 public
170 Sdk7FontEntity (Sdk7Typeface typeface)
171 {
172 foundry = "Google";
173 family = typeface.familyName;
174 adstyle = null;
175 weight = typeface.weight;
176 slant = typeface.slant;
177 spacing = typeface.spacing;
178 width = typeface.width;
179 dpi = Math.round (EmacsService.SERVICE.metrics.scaledDensity * 160f);
180
181 this.typeface = typeface;
182 }
183 };
184
185 protected final class Sdk7FontObject extends FontObject
186 {
187 /* The typeface. */
188 public Sdk7Typeface typeface;
189
190 public
191 Sdk7FontObject (Sdk7Typeface typeface, int pixelSize)
192 {
193 float totalWidth;
194 String testWidth, testString;
195
196 this.typeface = typeface;
197 this.pixelSize = pixelSize;
198
199 family = typeface.familyName;
200 adstyle = null;
201 weight = typeface.weight;
202 slant = typeface.slant;
203 spacing = typeface.spacing;
204 width = typeface.width;
205 dpi = Math.round (EmacsService.SERVICE.metrics.scaledDensity * 160f);
206
207 /* Compute the ascent and descent. */
208 typeface.typefacePaint.setTextSize (pixelSize);
209 ascent
210 = Math.round (-typeface.typefacePaint.ascent ());
211 descent
212 = Math.round (typeface.typefacePaint.descent ());
213
214 /* Compute the average width. */
215 testString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
216 totalWidth = typeface.typefacePaint.measureText (testString);
217
218 if (totalWidth > 0)
219 avgwidth = Math.round (totalWidth
220 / testString.length ());
221
222 /* Android doesn't expose the font average width and height
223 information, so this will have to do. */
224 minWidth = maxWidth = avgwidth;
225
226 /* This is different from avgwidth in the font spec! */
227 averageWidth = avgwidth;
228
229 /* Set the space width. */
230 totalWidth = typeface.typefacePaint.measureText (" ");
231 spaceWidth = Math.round (totalWidth);
232
233 /* Set the height and default ascent. */
234 height = ascent + descent;
235 defaultAscent = ascent;
236 }
237 };
238
239 private String[] fontFamilyList;
240 private Sdk7Typeface[] typefaceList;
241 private Sdk7Typeface fallbackTypeface;
242
243 public
244 EmacsSdk7FontDriver ()
245 {
246 int i;
247 File systemFontsDirectory, fontFile;
248 Typeface typeface;
249
250 systemFontsDirectory = new File ("/system/fonts");
251
252 fontFamilyList = systemFontsDirectory.list ();
253
254 /* If that returned null, replace it with an empty array. */
255 fontFamilyList = new String[0];
256
257 typefaceList = new Sdk7Typeface[fontFamilyList.length + 3];
258
259 /* It would be nice to avoid opening each and every font upon
260 startup. But that doesn't seem to be possible on
261 Android. */
262
263 for (i = 0; i < fontFamilyList.length; ++i)
264 {
265 fontFile = new File (systemFontsDirectory,
266 fontFamilyList[i]);
267 typeface = Typeface.createFromFile (fontFile);
268 typefaceList[i] = new Sdk7Typeface (fontFile.getName (),
269 typeface);
270 }
271
272 /* Initialize the default monospace and serif typefaces. */
273 fallbackTypeface = new Sdk7Typeface ("monospace",
274 Typeface.MONOSPACE);
275 typefaceList[fontFamilyList.length] = fallbackTypeface;
276
277 fallbackTypeface = new Sdk7Typeface ("Monospace",
278 Typeface.MONOSPACE);
279 typefaceList[fontFamilyList.length + 1] = fallbackTypeface;
280
281 fallbackTypeface = new Sdk7Typeface ("Sans Serif",
282 Typeface.DEFAULT);
283 typefaceList[fontFamilyList.length + 2] = fallbackTypeface;
284 }
285
286 private boolean
287 checkMatch (Sdk7Typeface typeface, FontSpec fontSpec)
288 {
289 if (fontSpec.family != null
290 && !fontSpec.family.equals (typeface.familyName))
291 return false;
292
293 if (fontSpec.slant != null
294 && !fontSpec.weight.equals (typeface.weight))
295 return false;
296
297 if (fontSpec.spacing != null
298 && !fontSpec.spacing.equals (typeface.spacing))
299 return false;
300
301 if (fontSpec.weight != null
302 && !fontSpec.weight.equals (typeface.weight))
303 return false;
304
305 if (fontSpec.width != null
306 && !fontSpec.width.equals (typeface.width))
307 return false;
308
309 return true;
310 }
311
312 @Override
313 public FontEntity[]
314 list (FontSpec fontSpec)
315 {
316 LinkedList<FontEntity> list;
317 int i;
318
319 list = new LinkedList<FontEntity> ();
320
321 for (i = 0; i < typefaceList.length; ++i)
322 {
323 if (checkMatch (typefaceList[i], fontSpec))
324 list.add (new Sdk7FontEntity (typefaceList[i]));
325 }
326
327 return list.toArray (new FontEntity[0]);
328 }
329
330 @Override
331 public FontEntity
332 match (FontSpec fontSpec)
333 {
334 FontEntity[] entities;
335 int i;
336
337 entities = this.list (fontSpec);
338
339 if (entities.length == 0)
340 return new Sdk7FontEntity (fallbackTypeface);
341
342 return entities[0];
343 }
344
345 @Override
346 public String[]
347 listFamilies ()
348 {
349 return fontFamilyList;
350 }
351
352 @Override
353 public FontObject
354 openFont (FontEntity fontEntity, int pixelSize)
355 {
356 return new Sdk7FontObject (((Sdk7FontEntity) fontEntity).typeface,
357 pixelSize);
358 }
359
360 @Override
361 public int
362 hasChar (FontSpec font, char charCode)
363 {
364 float missingGlyphWidth, width;
365 Rect rect1, rect2;
366 Paint paint;
367 Sdk7FontObject fontObject;
368
369 if (font instanceof Sdk7FontObject)
370 {
371 fontObject = (Sdk7FontObject) font;
372 paint = fontObject.typeface.typefacePaint;
373 }
374 else
375 paint = ((Sdk7FontEntity) font).typeface.typefacePaint;
376
377 paint.setTextSize (10);
378
379 if (Character.isWhitespace (charCode))
380 return 1;
381
382 missingGlyphWidth = paint.measureText (TOFU_STRING);
383 width = paint.measureText ("" + charCode);
384
385 if (width == 0f)
386 return 0;
387
388 if (width != missingGlyphWidth)
389 return 1;
390
391 rect1 = new Rect ();
392 rect2 = new Rect ();
393
394 paint.getTextBounds (TOFU_STRING, 0, TOFU_STRING.length (),
395 rect1);
396 paint.getTextBounds ("" + charCode, 0, 1, rect2);
397 return rect1.equals (rect2) ? 0 : 1;
398 }
399
400 private void
401 textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics,
402 Paint paint, Rect bounds)
403 {
404 char[] text;
405
406 text = new char[1];
407 text[0] = (char) code;
408
409 paint.getTextBounds (text, 0, 1, bounds);
410
411 /* bounds is the bounding box of the glyph corresponding to CODE.
412 Translate these into XCharStruct values.
413
414 The origin is at 0, 0, and lbearing is the distance counting
415 rightwards from the origin to the left most pixel in the glyph
416 raster. rbearing is the distance between the origin and the
417 rightmost pixel in the glyph raster. ascent is the distance
418 counting upwards between the the topmost pixel in the glyph
419 raster. descent is the distance (once again counting
420 downwards) between the origin and the bottommost pixel in the
421 glyph raster.
422
423 width is the distance between the origin and the origin of any
424 character to the right. */
425
426 metrics.lbearing = (short) bounds.left;
427 metrics.rbearing = (short) bounds.right;
428 metrics.ascent = (short) -bounds.top;
429 metrics.descent = (short) bounds.bottom;
430 metrics.width = (short) paint.measureText ("" + text[0]);
431 }
432
433 @Override
434 public void
435 textExtents (FontObject font, int code[], FontMetrics fontMetrics)
436 {
437 int i;
438 Paint paintCache;
439 Rect boundsCache;
440 Sdk7FontObject fontObject;
441 char[] text;
442 float width;
443
444 fontObject = (Sdk7FontObject) font;
445 paintCache = fontObject.typeface.typefacePaint;
446 paintCache.setTextSize (fontObject.pixelSize);
447 boundsCache = new Rect ();
448
449 if (code.length == 0)
450 {
451 fontMetrics.lbearing = 0;
452 fontMetrics.rbearing = 0;
453 fontMetrics.ascent = 0;
454 fontMetrics.descent = 0;
455 fontMetrics.width = 0;
456 }
457 else if (code.length == 1)
458 textExtents1 ((Sdk7FontObject) font, code[0], fontMetrics,
459 paintCache, boundsCache);
460 else
461 {
462 text = new char[code.length];
463
464 for (i = 0; i < code.length; ++i)
465 text[i] = (char) code[i];
466
467 paintCache.getTextBounds (text, 0, code.length,
468 boundsCache);
469 width = paintCache.measureText (text, 0, code.length);
470
471 fontMetrics.lbearing = (short) boundsCache.left;
472 fontMetrics.rbearing = (short) boundsCache.right;
473 fontMetrics.ascent = (short) -boundsCache.top;
474 fontMetrics.descent = (short) boundsCache.bottom;
475 fontMetrics.width = (short) Math.round (width);
476 }
477 }
478
479 @Override
480 public int
481 encodeChar (FontObject fontObject, char charCode)
482 {
483 return charCode;
484 }
485
486 @Override
487 public int
488 draw (FontObject fontObject, EmacsGC gc, EmacsDrawable drawable,
489 int[] chars, int x, int y, int backgroundWidth,
490 boolean withBackground)
491 {
492 Rect backgroundRect, bounds;
493 Sdk7FontObject sdk7FontObject;
494 char[] charsArray;
495 int i;
496 Canvas canvas;
497 Paint paint;
498
499 sdk7FontObject = (Sdk7FontObject) fontObject;
500 charsArray = new char[chars.length];
501
502 for (i = 0; i < chars.length; ++i)
503 charsArray[i] = (char) chars[i];
504
505 backgroundRect = new Rect ();
506 backgroundRect.top = y - sdk7FontObject.ascent;
507 backgroundRect.left = x;
508 backgroundRect.right = x + backgroundWidth;
509 backgroundRect.bottom = y + sdk7FontObject.descent;
510
511 canvas = drawable.lockCanvas (gc);
512
513 if (canvas == null)
514 return 0;
515
516 paint = gc.gcPaint;
517 paint.setStyle (Paint.Style.FILL);
518
519 if (withBackground)
520 {
521 paint.setColor (gc.background | 0xff000000);
522 canvas.drawRect (backgroundRect, paint);
523 paint.setColor (gc.foreground | 0xff000000);
524 }
525
526 paint.setTextSize (sdk7FontObject.pixelSize);
527 paint.setTypeface (sdk7FontObject.typeface.typeface);
528 paint.setAntiAlias (true);
529 canvas.drawText (charsArray, 0, chars.length, x, y, paint);
530
531 bounds = new Rect ();
532 paint.getTextBounds (charsArray, 0, chars.length, bounds);
533 bounds.offset (x, y);
534 bounds.union (backgroundRect);
535 drawable.damageRect (bounds);
536 paint.setAntiAlias (false);
537 return 1;
538 }
539};
diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java
new file mode 100644
index 00000000000..9622641810f
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java
@@ -0,0 +1,147 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22/* Importing the entire package instead of just the legacy
23 ClipboardManager class avoids the deprecation warning. */
24
25import android.text.*;
26
27import android.content.Context;
28import android.util.Log;
29
30import java.io.UnsupportedEncodingException;
31
32/* This class implements EmacsClipboard for Android 2.2 and other
33 similarly old systems. */
34
35@SuppressWarnings ("deprecation")
36public final class EmacsSdk8Clipboard extends EmacsClipboard
37{
38 private static final String TAG = "EmacsSdk8Clipboard";
39 private ClipboardManager manager;
40
41 public
42 EmacsSdk8Clipboard ()
43 {
44 String what;
45 Context context;
46
47 what = Context.CLIPBOARD_SERVICE;
48 context = EmacsService.SERVICE;
49 manager
50 = (ClipboardManager) context.getSystemService (what);
51 }
52
53 /* Set the clipboard text to CLIPBOARD, a string in UTF-8
54 encoding. */
55
56 @Override
57 public void
58 setClipboard (byte[] bytes)
59 {
60 try
61 {
62 manager.setText (new String (bytes, "UTF-8"));
63 }
64 catch (UnsupportedEncodingException exception)
65 {
66 Log.w (TAG, "setClipboard: " + exception);
67 }
68 }
69
70 /* Return whether or not Emacs owns the clipboard. Value is 1 if
71 Emacs does, 0 if Emacs does not, and -1 if that information is
72 unavailable. */
73
74 @Override
75 public int
76 ownsClipboard ()
77 {
78 return -1;
79 }
80
81 /* Return whether or not clipboard content currently exists. */
82
83 @Override
84 public boolean
85 clipboardExists ()
86 {
87 return manager.hasText ();
88 }
89
90 /* Return the current content of the clipboard, as plain text, or
91 NULL if no content is available. */
92
93 @Override
94 public byte[]
95 getClipboard ()
96 {
97 String string;
98 CharSequence text;
99
100 text = manager.getText ();
101
102 if (text == null)
103 return null;
104
105 string = text.toString ();
106
107 try
108 {
109 return string.getBytes ("UTF-8");
110 }
111 catch (UnsupportedEncodingException exception)
112 {
113 Log.w (TAG, "getClipboard: " + exception);
114 }
115
116 return null;
117 }
118
119 /* Return an array of targets currently provided by the
120 clipboard, or NULL if there are none. */
121
122 @Override
123 public byte[][]
124 getClipboardTargets ()
125 {
126 return null;
127 }
128
129 /* Return the clipboard data for the given target, or NULL if it
130 does not exist.
131
132 Value is normally an array of three longs: the file descriptor,
133 the start offset of the data, and its length; length may be
134 AssetFileDescriptor.UNKOWN_LENGTH, meaning that the data extends
135 from that offset to the end of the file.
136
137 Do not use this function to open text targets; use `getClipboard'
138 for that instead, as it will handle selection data consisting
139 solely of a URI. */
140
141 @Override
142 public long[]
143 getClipboardData (byte[] target)
144 {
145 return null;
146 }
147};
diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java
new file mode 100644
index 00000000000..d91d8f66009
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsService.java
@@ -0,0 +1,1820 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.io.FileNotFoundException;
23import java.io.IOException;
24import java.io.UnsupportedEncodingException;
25
26import java.util.ArrayList;
27import java.util.HashSet;
28import java.util.List;
29
30import java.util.concurrent.atomic.AtomicInteger;
31
32import android.database.Cursor;
33
34import android.graphics.Matrix;
35import android.graphics.Point;
36
37import android.webkit.MimeTypeMap;
38
39import android.view.InputDevice;
40import android.view.KeyEvent;
41import android.view.inputmethod.CursorAnchorInfo;
42import android.view.inputmethod.ExtractedText;
43
44import android.app.Notification;
45import android.app.NotificationManager;
46import android.app.NotificationChannel;
47import android.app.Service;
48
49import android.content.ClipboardManager;
50import android.content.Context;
51import android.content.ContentResolver;
52import android.content.Intent;
53import android.content.IntentFilter;
54import android.content.UriPermission;
55
56import android.content.pm.ApplicationInfo;
57import android.content.pm.PackageManager.ApplicationInfoFlags;
58import android.content.pm.PackageManager;
59
60import android.content.res.AssetManager;
61
62import android.hardware.input.InputManager;
63
64import android.net.Uri;
65
66import android.os.BatteryManager;
67import android.os.Build;
68import android.os.Looper;
69import android.os.IBinder;
70import android.os.Handler;
71import android.os.ParcelFileDescriptor;
72import android.os.Vibrator;
73import android.os.VibratorManager;
74import android.os.VibrationEffect;
75
76import android.provider.DocumentsContract;
77import android.provider.DocumentsContract.Document;
78
79import android.util.Log;
80import android.util.DisplayMetrics;
81
82import android.widget.Toast;
83
84/* EmacsService is the service that starts the thread running Emacs
85 and handles requests by that Emacs instance. */
86
87public final class EmacsService extends Service
88{
89 public static final String TAG = "EmacsService";
90
91 /* The started Emacs service object. */
92 public static EmacsService SERVICE;
93
94 /* If non-NULL, an extra argument to pass to
95 `android_emacs_init'. */
96 public static String extraStartupArgument;
97
98 /* The thread running Emacs C code. */
99 private EmacsThread thread;
100
101 /* Handler used to run tasks on the main thread. */
102 private Handler handler;
103
104 /* Content resolver used to access URIs. */
105 private ContentResolver resolver;
106
107 /* Keep this in synch with androidgui.h. */
108 public static final int IC_MODE_NULL = 0;
109 public static final int IC_MODE_ACTION = 1;
110 public static final int IC_MODE_TEXT = 2;
111
112 /* Display metrics used by font backends. */
113 public DisplayMetrics metrics;
114
115 /* Flag that says whether or not to print verbose debugging
116 information when responding to an input method. */
117 public static final boolean DEBUG_IC = false;
118
119 /* Flag that says whether or not to stringently check that only the
120 Emacs thread is performing drawing calls. */
121 private static final boolean DEBUG_THREADS = false;
122
123 /* Atomic integer used for synchronization between
124 icBeginSynchronous/icEndSynchronous and viewGetSelection.
125
126 Value is 0 if no query is in progress, 1 if viewGetSelection is
127 being called, and 2 if icBeginSynchronous was called. */
128 public static final AtomicInteger servicingQuery;
129
130 /* Thread used to query document providers, or null if it hasn't
131 been created yet. */
132 private EmacsSafThread storageThread;
133
134 static
135 {
136 servicingQuery = new AtomicInteger ();
137 };
138
139 /* Return the directory leading to the directory in which native
140 library files are stored on behalf of CONTEXT. */
141
142 public static String
143 getLibraryDirectory (Context context)
144 {
145 int apiLevel;
146
147 apiLevel = Build.VERSION.SDK_INT;
148
149 if (apiLevel >= Build.VERSION_CODES.GINGERBREAD)
150 return context.getApplicationInfo ().nativeLibraryDir;
151
152 return context.getApplicationInfo ().dataDir + "/lib";
153 }
154
155 @Override
156 public int
157 onStartCommand (Intent intent, int flags, int startId)
158 {
159 Notification notification;
160 NotificationManager manager;
161 NotificationChannel channel;
162 String infoBlurb;
163 Object tem;
164
165 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
166 {
167 tem = getSystemService (Context.NOTIFICATION_SERVICE);
168 manager = (NotificationManager) tem;
169 infoBlurb = ("This notification is displayed to keep Emacs"
170 + " running while it is in the background. You"
171 + " may disable it if you want;"
172 + " see (emacs)Android Environment.");
173 channel
174 = new NotificationChannel ("emacs", "Emacs persistent notification",
175 NotificationManager.IMPORTANCE_DEFAULT);
176 manager.createNotificationChannel (channel);
177 notification = (new Notification.Builder (this, "emacs")
178 .setContentTitle ("Emacs")
179 .setContentText (infoBlurb)
180 .setSmallIcon (android.R.drawable.sym_def_app_icon)
181 .build ());
182 manager.notify (1, notification);
183 startForeground (1, notification);
184 }
185
186 return START_NOT_STICKY;
187 }
188
189 @Override
190 public IBinder
191 onBind (Intent intent)
192 {
193 return null;
194 }
195
196 @SuppressWarnings ("deprecation")
197 private String
198 getApkFile ()
199 {
200 PackageManager manager;
201 ApplicationInfo info;
202
203 manager = getPackageManager ();
204
205 try
206 {
207 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
208 info = manager.getApplicationInfo ("org.gnu.emacs", 0);
209 else
210 info = manager.getApplicationInfo ("org.gnu.emacs",
211 ApplicationInfoFlags.of (0));
212
213 /* Return an empty string upon failure. */
214
215 if (info.sourceDir != null)
216 return info.sourceDir;
217
218 return "";
219 }
220 catch (Exception e)
221 {
222 return "";
223 }
224 }
225
226 @Override
227 public void
228 onCreate ()
229 {
230 final AssetManager manager;
231 Context app_context;
232 final String filesDir, libDir, cacheDir, classPath;
233 final double pixelDensityX;
234 final double pixelDensityY;
235 final double scaledDensity;
236 double tempScaledDensity;
237
238 SERVICE = this;
239 handler = new Handler (Looper.getMainLooper ());
240 manager = getAssets ();
241 app_context = getApplicationContext ();
242 metrics = getResources ().getDisplayMetrics ();
243 pixelDensityX = metrics.xdpi;
244 pixelDensityY = metrics.ydpi;
245 tempScaledDensity = ((metrics.scaledDensity
246 / metrics.density)
247 * pixelDensityX);
248 resolver = getContentResolver ();
249
250 /* If the density used to compute the text size is lesser than
251 160, there's likely a bug with display density computation.
252 Reset it to 160 in that case.
253
254 Note that Android uses 160 ``dpi'' as the density where 1 point
255 corresponds to 1 pixel, not 72 or 96 as used elsewhere. This
256 difference is codified in PT_PER_INCH defined in font.h. */
257
258 if (tempScaledDensity < 160)
259 tempScaledDensity = 160;
260
261 /* scaledDensity is const as required to refer to it from within
262 the nested function below. */
263 scaledDensity = tempScaledDensity;
264
265 try
266 {
267 /* Configure Emacs with the asset manager and other necessary
268 parameters. */
269 filesDir = app_context.getFilesDir ().getCanonicalPath ();
270 libDir = getLibraryDirectory (this);
271 cacheDir = app_context.getCacheDir ().getCanonicalPath ();
272
273 /* Now provide this application's apk file, so a recursive
274 invocation of app_process (through android-emacs) can
275 find EmacsNoninteractive. */
276 classPath = getApkFile ();
277
278 Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir
279 + ", libDir = " + libDir + ", and classPath = " + classPath
280 + "; fileToOpen = " + EmacsOpenActivity.fileToOpen
281 + "; display density: " + pixelDensityX + " by "
282 + pixelDensityY + " scaled to " + scaledDensity);
283
284 /* Start the thread that runs Emacs. */
285 thread = new EmacsThread (this, new Runnable () {
286 @Override
287 public void
288 run ()
289 {
290 EmacsNative.setEmacsParams (manager, filesDir, libDir,
291 cacheDir, (float) pixelDensityX,
292 (float) pixelDensityY,
293 (float) scaledDensity,
294 classPath, EmacsService.this,
295 Build.VERSION.SDK_INT);
296 }
297 }, extraStartupArgument,
298 /* If any file needs to be opened, open it now. */
299 EmacsOpenActivity.fileToOpen);
300 thread.start ();
301 }
302 catch (IOException exception)
303 {
304 EmacsNative.emacsAbort ();
305 return;
306 }
307 }
308
309
310
311 /* Functions from here on must only be called from the Emacs
312 thread. */
313
314 public void
315 runOnUiThread (Runnable runnable)
316 {
317 handler.post (runnable);
318 }
319
320 public EmacsView
321 getEmacsView (final EmacsWindow window, final int visibility,
322 final boolean isFocusedByDefault)
323 {
324 Runnable runnable;
325 final EmacsHolder<EmacsView> view;
326
327 view = new EmacsHolder<EmacsView> ();
328
329 runnable = new Runnable () {
330 @Override
331 public void
332 run ()
333 {
334 synchronized (this)
335 {
336 view.thing = new EmacsView (window);
337 view.thing.setVisibility (visibility);
338
339 /* The following function is only present on Android 26
340 or later. */
341 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
342 view.thing.setFocusedByDefault (isFocusedByDefault);
343
344 notify ();
345 }
346 }
347 };
348
349 syncRunnable (runnable);
350 return view.thing;
351 }
352
353 public void
354 getLocationOnScreen (final EmacsView view, final int[] coordinates)
355 {
356 Runnable runnable;
357
358 runnable = new Runnable () {
359 public void
360 run ()
361 {
362 synchronized (this)
363 {
364 view.getLocationOnScreen (coordinates);
365 notify ();
366 }
367 }
368 };
369
370 syncRunnable (runnable);
371 }
372
373
374
375 public static void
376 checkEmacsThread ()
377 {
378 if (DEBUG_THREADS)
379 {
380 if (Thread.currentThread () instanceof EmacsThread)
381 return;
382
383 throw new RuntimeException ("Emacs thread function"
384 + " called from other thread!");
385 }
386 }
387
388 /* These drawing functions must only be called from the Emacs
389 thread. */
390
391 public void
392 fillRectangle (EmacsDrawable drawable, EmacsGC gc,
393 int x, int y, int width, int height)
394 {
395 checkEmacsThread ();
396 EmacsFillRectangle.perform (drawable, gc, x, y,
397 width, height);
398 }
399
400 public void
401 fillPolygon (EmacsDrawable drawable, EmacsGC gc,
402 Point points[])
403 {
404 checkEmacsThread ();
405 EmacsFillPolygon.perform (drawable, gc, points);
406 }
407
408 public void
409 drawRectangle (EmacsDrawable drawable, EmacsGC gc,
410 int x, int y, int width, int height)
411 {
412 checkEmacsThread ();
413 EmacsDrawRectangle.perform (drawable, gc, x, y,
414 width, height);
415 }
416
417 public void
418 drawLine (EmacsDrawable drawable, EmacsGC gc,
419 int x, int y, int x2, int y2)
420 {
421 checkEmacsThread ();
422 EmacsDrawLine.perform (drawable, gc, x, y,
423 x2, y2);
424 }
425
426 public void
427 drawPoint (EmacsDrawable drawable, EmacsGC gc,
428 int x, int y)
429 {
430 checkEmacsThread ();
431 EmacsDrawPoint.perform (drawable, gc, x, y);
432 }
433
434 public void
435 clearWindow (EmacsWindow window)
436 {
437 checkEmacsThread ();
438 window.clearWindow ();
439 }
440
441 public void
442 clearArea (EmacsWindow window, int x, int y, int width,
443 int height)
444 {
445 checkEmacsThread ();
446 window.clearArea (x, y, width, height);
447 }
448
449 @SuppressWarnings ("deprecation")
450 public void
451 ringBell ()
452 {
453 Vibrator vibrator;
454 VibrationEffect effect;
455 VibratorManager vibratorManager;
456 Object tem;
457
458 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
459 {
460 tem = getSystemService (Context.VIBRATOR_MANAGER_SERVICE);
461 vibratorManager = (VibratorManager) tem;
462 vibrator = vibratorManager.getDefaultVibrator ();
463 }
464 else
465 vibrator
466 = (Vibrator) getSystemService (Context.VIBRATOR_SERVICE);
467
468 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
469 {
470 effect
471 = VibrationEffect.createOneShot (50,
472 VibrationEffect.DEFAULT_AMPLITUDE);
473 vibrator.vibrate (effect);
474 }
475 else
476 vibrator.vibrate (50);
477 }
478
479 public short[]
480 queryTree (EmacsWindow window)
481 {
482 short[] array;
483 List<EmacsWindow> windowList;
484 int i;
485
486 if (window == null)
487 /* Just return all the windows without a parent. */
488 windowList = EmacsWindowAttachmentManager.MANAGER.copyWindows ();
489 else
490 windowList = window.children;
491
492 array = new short[windowList.size () + 1];
493 i = 1;
494
495 array[0] = (window == null
496 ? 0 : (window.parent != null
497 ? window.parent.handle : 0));
498
499 for (EmacsWindow treeWindow : windowList)
500 array[i++] = treeWindow.handle;
501
502 return array;
503 }
504
505 public int
506 getScreenWidth (boolean mmWise)
507 {
508 DisplayMetrics metrics;
509
510 metrics = getResources ().getDisplayMetrics ();
511
512 if (!mmWise)
513 return metrics.widthPixels;
514 else
515 return (int) ((metrics.widthPixels / metrics.xdpi) * 2540.0);
516 }
517
518 public int
519 getScreenHeight (boolean mmWise)
520 {
521 DisplayMetrics metrics;
522
523 metrics = getResources ().getDisplayMetrics ();
524
525 if (!mmWise)
526 return metrics.heightPixels;
527 else
528 return (int) ((metrics.heightPixels / metrics.ydpi) * 2540.0);
529 }
530
531 public boolean
532 detectMouse ()
533 {
534 InputManager manager;
535 InputDevice device;
536 int[] ids;
537 int i;
538
539 if (Build.VERSION.SDK_INT
540 /* Android 4.0 and earlier don't support mouse input events at
541 all. */
542 < Build.VERSION_CODES.JELLY_BEAN)
543 return false;
544
545 manager = (InputManager) getSystemService (Context.INPUT_SERVICE);
546 ids = manager.getInputDeviceIds ();
547
548 for (i = 0; i < ids.length; ++i)
549 {
550 device = manager.getInputDevice (ids[i]);
551
552 if (device == null)
553 continue;
554
555 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
556 {
557 if (device.supportsSource (InputDevice.SOURCE_MOUSE))
558 return true;
559 }
560 else
561 {
562 /* `supportsSource' is only present on API level 21 and
563 later, but earlier versions provide a bit mask
564 containing each supported source. */
565
566 if ((device.getSources () & InputDevice.SOURCE_MOUSE) != 0)
567 return true;
568 }
569 }
570
571 return false;
572 }
573
574 public String
575 nameKeysym (int keysym)
576 {
577 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1)
578 return KeyEvent.keyCodeToString (keysym);
579
580 return String.valueOf (keysym);
581 }
582
583
584
585 /* Start the Emacs service if necessary. On Android 26 and up,
586 start Emacs as a foreground service with a notification, to avoid
587 it being killed by the system.
588
589 On older systems, simply start it as a normal background
590 service. */
591
592 public static void
593 startEmacsService (Context context)
594 {
595 if (EmacsService.SERVICE == null)
596 {
597 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
598 /* Start the Emacs service now. */
599 context.startService (new Intent (context,
600 EmacsService.class));
601 else
602 /* Display the permanant notification and start Emacs as a
603 foreground service. */
604 context.startForegroundService (new Intent (context,
605 EmacsService.class));
606 }
607 }
608
609 /* Ask the system to open the specified URL in an application that
610 understands how to open it.
611
612 If SEND, tell the system to also open applications that can
613 ``send'' the URL (through mail, for example), instead of only
614 those that can view the URL.
615
616 Value is NULL upon success, or a string describing the error
617 upon failure. */
618
619 public String
620 browseUrl (String url, boolean send)
621 {
622 Intent intent;
623 Uri uri;
624
625 try
626 {
627 /* Parse the URI. */
628 if (!send)
629 {
630 uri = Uri.parse (url);
631
632 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
633 {
634 /* On Android 4.4 and later, check if URI is actually
635 a file name. If so, rewrite it into a content
636 provider URI, so that it can be accessed by other
637 programs. */
638
639 if (uri.getScheme ().equals ("file")
640 && uri.getPath () != null)
641 uri
642 = DocumentsContract.buildDocumentUri ("org.gnu.emacs",
643 uri.getPath ());
644 }
645
646 Log.d (TAG, ("browseUri: browsing " + url
647 + " --> " + uri.getPath ()
648 + " --> " + uri));
649
650 intent = new Intent (Intent.ACTION_VIEW, uri);
651 intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK
652 | Intent.FLAG_GRANT_READ_URI_PERMISSION);
653 }
654 else
655 {
656 intent = new Intent (Intent.ACTION_SEND);
657 intent.setType ("text/plain");
658 intent.putExtra (Intent.EXTRA_SUBJECT, "Sharing link");
659 intent.putExtra (Intent.EXTRA_TEXT, url);
660
661 /* Display a list of programs able to send this URL. */
662 intent = Intent.createChooser (intent, "Send");
663
664 /* Apparently flags need to be set after a choser is
665 created. */
666 intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK);
667 }
668
669 startActivity (intent);
670 }
671 catch (Exception e)
672 {
673 return e.toString ();
674 }
675
676 return null;
677 }
678
679 /* Get a SDK 11 ClipboardManager.
680
681 Android 4.0.x requires that this be called from the main
682 thread. */
683
684 public ClipboardManager
685 getClipboardManager ()
686 {
687 final EmacsHolder<ClipboardManager> manager;
688 Runnable runnable;
689
690 manager = new EmacsHolder<ClipboardManager> ();
691
692 runnable = new Runnable () {
693 public void
694 run ()
695 {
696 Object tem;
697
698 synchronized (this)
699 {
700 tem = getSystemService (Context.CLIPBOARD_SERVICE);
701 manager.thing = (ClipboardManager) tem;
702 notify ();
703 }
704 }
705 };
706
707 syncRunnable (runnable);
708 return manager.thing;
709 }
710
711 public void
712 restartEmacs ()
713 {
714 Intent intent;
715
716 intent = new Intent (this, EmacsActivity.class);
717 intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK
718 | Intent.FLAG_ACTIVITY_CLEAR_TASK);
719 startActivity (intent);
720 System.exit (0);
721 }
722
723 /* Wait synchronously for the specified RUNNABLE to complete in the
724 UI thread. Must be called from the Emacs thread. */
725
726 public static void
727 syncRunnable (Runnable runnable)
728 {
729 EmacsNative.beginSynchronous ();
730
731 synchronized (runnable)
732 {
733 SERVICE.runOnUiThread (runnable);
734
735 while (true)
736 {
737 try
738 {
739 runnable.wait ();
740 break;
741 }
742 catch (InterruptedException e)
743 {
744 continue;
745 }
746 }
747 }
748
749 EmacsNative.endSynchronous ();
750 }
751
752
753
754 /* IMM functions such as `updateSelection' holds an internal lock
755 that is also taken before `onCreateInputConnection' (in
756 EmacsView.java) is called; when that then asks the UI thread for
757 the current selection, a dead lock results. To remedy this,
758 reply to any synchronous queries now -- and prohibit more queries
759 for the duration of `updateSelection' -- if EmacsView may have
760 been asking for the value of the region. */
761
762 public static void
763 icBeginSynchronous ()
764 {
765 /* Set servicingQuery to 2, so viewGetSelection knows it shouldn't
766 proceed. */
767
768 if (servicingQuery.getAndSet (2) == 1)
769 /* But if viewGetSelection is already in progress, answer it
770 first. */
771 EmacsNative.answerQuerySpin ();
772 }
773
774 public static void
775 icEndSynchronous ()
776 {
777 if (servicingQuery.getAndSet (0) != 2)
778 throw new RuntimeException ("incorrect value of `servicingQuery': "
779 + "likely 1");
780 }
781
782 public static int[]
783 viewGetSelection (short window)
784 {
785 int[] selection;
786
787 /* See if a query is already in progress from the other
788 direction. */
789 if (!servicingQuery.compareAndSet (0, 1))
790 return null;
791
792 /* Now call the regular getSelection. Note that this can't race
793 with answerQuerySpin, as `android_servicing_query' can never be
794 2 when icBeginSynchronous is called, so a query will always be
795 started. */
796 selection = EmacsNative.getSelection (window);
797
798 /* Finally, clear servicingQuery if its value is still 1. If a
799 query has started from the other side, it ought to be 2. */
800
801 servicingQuery.compareAndSet (1, 0);
802 return selection;
803 }
804
805
806
807 public void
808 updateIC (EmacsWindow window, int newSelectionStart,
809 int newSelectionEnd, int composingRegionStart,
810 int composingRegionEnd)
811 {
812 if (DEBUG_IC)
813 Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart
814 + " " + newSelectionEnd + " "
815 + composingRegionStart + " "
816 + composingRegionEnd));
817
818 icBeginSynchronous ();
819 window.view.imManager.updateSelection (window.view,
820 newSelectionStart,
821 newSelectionEnd,
822 composingRegionStart,
823 composingRegionEnd);
824 icEndSynchronous ();
825 }
826
827 public void
828 resetIC (EmacsWindow window, int icMode)
829 {
830 int oldMode;
831
832 if (DEBUG_IC)
833 Log.d (TAG, "resetIC: " + window + ", " + icMode);
834
835 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
836 && (oldMode = window.view.getICMode ()) == icMode
837 /* Don't do this if there is currently no input
838 connection. */
839 && oldMode != IC_MODE_NULL)
840 {
841 if (DEBUG_IC)
842 Log.d (TAG, "resetIC: calling invalidateInput");
843
844 /* Android 33 and later allow the IM reset to be optimized out
845 and replaced by a call to `invalidateInput', which is much
846 faster, as it does not involve resetting the input
847 connection. */
848
849 icBeginSynchronous ();
850 window.view.imManager.invalidateInput (window.view);
851 icEndSynchronous ();
852
853 return;
854 }
855
856 window.view.setICMode (icMode);
857
858 icBeginSynchronous ();
859 window.view.icGeneration++;
860 window.view.imManager.restartInput (window.view);
861 icEndSynchronous ();
862 }
863
864 public void
865 updateCursorAnchorInfo (EmacsWindow window, float x,
866 float y, float yBaseline,
867 float yBottom)
868 {
869 CursorAnchorInfo info;
870 CursorAnchorInfo.Builder builder;
871 Matrix matrix;
872 int[] offsets;
873
874 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
875 return;
876
877 offsets = new int[2];
878 builder = new CursorAnchorInfo.Builder ();
879 matrix = new Matrix (window.view.getMatrix ());
880 window.view.getLocationOnScreen (offsets);
881 matrix.postTranslate (offsets[0], offsets[1]);
882 builder.setMatrix (matrix);
883 builder.setInsertionMarkerLocation (x, y, yBaseline, yBottom,
884 0);
885 info = builder.build ();
886
887
888
889 if (DEBUG_IC)
890 Log.d (TAG, ("updateCursorAnchorInfo: " + x + " " + y
891 + " " + yBaseline + "-" + yBottom));
892
893 icBeginSynchronous ();
894 window.view.imManager.updateCursorAnchorInfo (window.view, info);
895 icEndSynchronous ();
896 }
897
898
899
900 /* Content provider functions. */
901
902 /* Open a content URI described by the bytes BYTES, a non-terminated
903 string; make it writable if WRITABLE, and readable if READABLE.
904 Truncate the file if TRUNCATE.
905
906 Value is the resulting file descriptor or -1 upon failure. */
907
908 public int
909 openContentUri (byte[] bytes, boolean writable, boolean readable,
910 boolean truncate)
911 {
912 String name, mode;
913 ParcelFileDescriptor fd;
914 int i;
915
916 /* Figure out the file access mode. */
917
918 mode = "";
919
920 if (readable)
921 mode += "r";
922
923 if (writable)
924 mode += "w";
925
926 if (truncate)
927 mode += "t";
928
929 /* Try to open an associated ParcelFileDescriptor. */
930
931 try
932 {
933 /* The usual file name encoding question rears its ugly head
934 again. */
935
936 name = new String (bytes, "UTF-8");
937 fd = resolver.openFileDescriptor (Uri.parse (name), mode);
938
939 /* Use detachFd on newer versions of Android or plain old
940 dup. */
941
942 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1)
943 {
944 i = fd.detachFd ();
945 fd.close ();
946
947 return i;
948 }
949 else
950 {
951 i = EmacsNative.dup (fd.getFd ());
952 fd.close ();
953
954 return i;
955 }
956 }
957 catch (Exception exception)
958 {
959 return -1;
960 }
961 }
962
963 public boolean
964 checkContentUri (byte[] string, boolean readable, boolean writable)
965 {
966 String mode, name;
967 ParcelFileDescriptor fd;
968
969 /* Decode this into a URI. */
970
971 try
972 {
973 /* The usual file name encoding question rears its ugly head
974 again. */
975 name = new String (string, "UTF-8");
976 }
977 catch (UnsupportedEncodingException exception)
978 {
979 name = null;
980 throw new RuntimeException (exception);
981 }
982
983 mode = "r";
984
985 if (writable)
986 mode += "w";
987
988 try
989 {
990 fd = resolver.openFileDescriptor (Uri.parse (name), mode);
991 fd.close ();
992
993 return true;
994 }
995 catch (Exception exception)
996 {
997 /* Fall through. */
998 }
999
1000 return false;
1001 }
1002
1003 /* Build a content file name for URI.
1004
1005 Return a file name within the /contents/by-authority
1006 pseudo-directory that `android_get_content_name' can then
1007 transform back into an encoded URI.
1008
1009 A content name consists of any number of unencoded path segments
1010 separated by `/' characters, possibly followed by a question mark
1011 and an encoded query string. */
1012
1013 public static String
1014 buildContentName (Uri uri)
1015 {
1016 StringBuilder builder;
1017
1018 builder = new StringBuilder ("/content/by-authority/");
1019 builder.append (uri.getAuthority ());
1020
1021 /* First, append each path segment. */
1022
1023 for (String segment : uri.getPathSegments ())
1024 {
1025 /* FIXME: what if segment contains a slash character? */
1026 builder.append ('/');
1027 builder.append (uri.encode (segment));
1028 }
1029
1030 /* Now, append the query string if necessary. */
1031
1032 if (uri.getEncodedQuery () != null)
1033 builder.append ('?').append (uri.getEncodedQuery ());
1034
1035 return builder.toString ();
1036 }
1037
1038
1039
1040 private long[]
1041 queryBattery19 ()
1042 {
1043 IntentFilter filter;
1044 Intent battery;
1045 long capacity, chargeCounter, currentAvg, currentNow;
1046 long status, remaining, plugged, temp;
1047
1048 filter = new IntentFilter (Intent.ACTION_BATTERY_CHANGED);
1049 battery = registerReceiver (null, filter);
1050
1051 if (battery == null)
1052 return null;
1053
1054 capacity = battery.getIntExtra (BatteryManager.EXTRA_LEVEL, 0);
1055 chargeCounter
1056 = (battery.getIntExtra (BatteryManager.EXTRA_SCALE, 0)
1057 / battery.getIntExtra (BatteryManager.EXTRA_LEVEL, 100) * 100);
1058 currentAvg = 0;
1059 currentNow = 0;
1060 status = battery.getIntExtra (BatteryManager.EXTRA_STATUS, 0);
1061 remaining = -1;
1062 plugged = battery.getIntExtra (BatteryManager.EXTRA_PLUGGED, 0);
1063 temp = battery.getIntExtra (BatteryManager.EXTRA_TEMPERATURE, 0);
1064
1065 return new long[] { capacity, chargeCounter, currentAvg,
1066 currentNow, remaining, status, plugged,
1067 temp, };
1068 }
1069
1070 /* Return the status of the battery. See struct
1071 android_battery_status for the order of the elements
1072 returned.
1073
1074 Value may be null upon failure. */
1075
1076 public long[]
1077 queryBattery ()
1078 {
1079 Object tem;
1080 BatteryManager manager;
1081 long capacity, chargeCounter, currentAvg, currentNow;
1082 long status, remaining, plugged, temp;
1083 int prop;
1084 IntentFilter filter;
1085 Intent battery;
1086
1087 /* Android 4.4 or earlier require applications to use a different
1088 API to query the battery status. */
1089
1090 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
1091 return queryBattery19 ();
1092
1093 tem = getSystemService (Context.BATTERY_SERVICE);
1094 manager = (BatteryManager) tem;
1095 remaining = -1;
1096
1097 prop = BatteryManager.BATTERY_PROPERTY_CAPACITY;
1098 capacity = manager.getLongProperty (prop);
1099 prop = BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER;
1100 chargeCounter = manager.getLongProperty (prop);
1101 prop = BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE;
1102 currentAvg = manager.getLongProperty (prop);
1103 prop = BatteryManager.BATTERY_PROPERTY_CURRENT_NOW;
1104 currentNow = manager.getLongProperty (prop);
1105
1106 /* Return the battery status. N.B. that Android 7.1 and earlier
1107 only return ``charging'' or ``discharging''. */
1108
1109 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
1110 status
1111 = manager.getIntProperty (BatteryManager.BATTERY_PROPERTY_STATUS);
1112 else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
1113 status = (manager.isCharging ()
1114 ? BatteryManager.BATTERY_STATUS_CHARGING
1115 : BatteryManager.BATTERY_STATUS_DISCHARGING);
1116 else
1117 status = (currentNow > 0
1118 ? BatteryManager.BATTERY_STATUS_CHARGING
1119 : BatteryManager.BATTERY_STATUS_DISCHARGING);
1120
1121 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
1122 remaining = manager.computeChargeTimeRemaining ();
1123
1124 plugged = -1;
1125 temp = -1;
1126
1127 /* Now obtain additional information from the battery manager. */
1128
1129 filter = new IntentFilter (Intent.ACTION_BATTERY_CHANGED);
1130 battery = registerReceiver (null, filter);
1131
1132 if (battery != null)
1133 {
1134 plugged = battery.getIntExtra (BatteryManager.EXTRA_PLUGGED, 0);
1135 temp = battery.getIntExtra (BatteryManager.EXTRA_TEMPERATURE, 0);
1136
1137 /* Make status more reliable. */
1138 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
1139 status = battery.getIntExtra (BatteryManager.EXTRA_STATUS, 0);
1140 }
1141
1142 return new long[] { capacity, chargeCounter, currentAvg,
1143 currentNow, remaining, status, plugged,
1144 temp, };
1145 }
1146
1147 public void
1148 updateExtractedText (EmacsWindow window, ExtractedText text,
1149 int token)
1150 {
1151 if (DEBUG_IC)
1152 Log.d (TAG, "updateExtractedText: @" + token + ", " + text);
1153
1154 window.view.imManager.updateExtractedText (window.view,
1155 token, text);
1156 }
1157
1158
1159
1160 /* Document tree management functions. These functions shouldn't be
1161 called before Android 5.0. */
1162
1163 /* Return an array of each document authority providing at least one
1164 tree URI that Emacs holds the rights to persistently access. */
1165
1166 public String[]
1167 getDocumentAuthorities ()
1168 {
1169 List<UriPermission> permissions;
1170 HashSet<String> allProviders;
1171 Uri uri;
1172
1173 permissions = resolver.getPersistedUriPermissions ();
1174 allProviders = new HashSet<String> ();
1175
1176 for (UriPermission permission : permissions)
1177 {
1178 uri = permission.getUri ();
1179
1180 if (DocumentsContract.isTreeUri (uri)
1181 && permission.isReadPermission ())
1182 allProviders.add (uri.getAuthority ());
1183 }
1184
1185 return allProviders.toArray (new String[0]);
1186 }
1187
1188 /* Start a file chooser activity to request access to a directory
1189 tree.
1190
1191 Value is 1 if the activity couldn't be started for some reason,
1192 and 0 in any other case. */
1193
1194 public int
1195 requestDirectoryAccess ()
1196 {
1197 Runnable runnable;
1198 final EmacsHolder<Integer> rc;
1199
1200 /* Return 1 if Android is too old to support this feature. */
1201
1202 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
1203 return 1;
1204
1205 rc = new EmacsHolder<Integer> ();
1206 rc.thing = Integer.valueOf (1);
1207
1208 runnable = new Runnable () {
1209 @Override
1210 public void
1211 run ()
1212 {
1213 EmacsActivity activity;
1214 Intent intent;
1215 int id;
1216
1217 synchronized (this)
1218 {
1219 /* Try to obtain an activity that will receive the
1220 response from the file chooser dialog. */
1221
1222 if (EmacsActivity.focusedActivities.isEmpty ())
1223 {
1224 /* If focusedActivities is empty then this dialog
1225 may have been displayed immediately after another
1226 popup dialog was dismissed. Try the
1227 EmacsActivity to be focused. */
1228
1229 activity = EmacsActivity.lastFocusedActivity;
1230
1231 if (activity == null)
1232 {
1233 /* Still no luck. Return failure. */
1234 notify ();
1235 return;
1236 }
1237 }
1238 else
1239 activity = EmacsActivity.focusedActivities.get (0);
1240
1241 /* Now create the intent. */
1242 intent = new Intent (Intent.ACTION_OPEN_DOCUMENT_TREE);
1243
1244 try
1245 {
1246 id = EmacsActivity.ACCEPT_DOCUMENT_TREE;
1247 activity.startActivityForResult (intent, id, null);
1248 rc.thing = Integer.valueOf (0);
1249 }
1250 catch (Exception e)
1251 {
1252 e.printStackTrace ();
1253 }
1254
1255 notify ();
1256 }
1257 }
1258 };
1259
1260 syncRunnable (runnable);
1261 return rc.thing;
1262 }
1263
1264 /* Return an array of each tree provided by the document PROVIDER
1265 that Emacs has permission to access.
1266
1267 Value is an array if the provider really does exist, NULL
1268 otherwise. */
1269
1270 public String[]
1271 getDocumentTrees (byte provider[])
1272 {
1273 String providerName;
1274 List<String> treeList;
1275 List<UriPermission> permissions;
1276 Uri uri;
1277
1278 try
1279 {
1280 providerName = new String (provider, "US-ASCII");
1281 }
1282 catch (UnsupportedEncodingException exception)
1283 {
1284 return null;
1285 }
1286
1287 permissions = resolver.getPersistedUriPermissions ();
1288 treeList = new ArrayList<String> ();
1289
1290 for (UriPermission permission : permissions)
1291 {
1292 uri = permission.getUri ();
1293
1294 if (DocumentsContract.isTreeUri (uri)
1295 && uri.getAuthority ().equals (providerName)
1296 && permission.isReadPermission ())
1297 /* Make sure the tree document ID is encoded. Refrain from
1298 encoding characters such as +:&?#, since they don't
1299 conflict with file name separators or other special
1300 characters. */
1301 treeList.add (Uri.encode (DocumentsContract.getTreeDocumentId (uri),
1302 " +:&?#"));
1303 }
1304
1305 return treeList.toArray (new String[0]);
1306 }
1307
1308 /* Find the document ID of the file within TREE_URI designated by
1309 NAME.
1310
1311 NAME is a ``file name'' comprised of the display names of
1312 individual files. Each constituent component prior to the last
1313 must name a directory file within TREE_URI.
1314
1315 Upon success, return 0 or 1 (contingent upon whether or not the
1316 last component within NAME is a directory) and place the document
1317 ID of the named file in ID_RETURN[0].
1318
1319 If the designated file can't be located, but each component of
1320 NAME up to the last component can and is a directory, return -2
1321 and the ID of the last component located in ID_RETURN[0].
1322
1323 If the designated file can't be located, return -1, or signal one
1324 of OperationCanceledException, SecurityException,
1325 FileNotFoundException, or UnsupportedOperationException. */
1326
1327 private int
1328 documentIdFromName (String tree_uri, String name, String[] id_return)
1329 {
1330 /* Start the thread used to run SAF requests if it isn't already
1331 running. */
1332
1333 if (storageThread == null)
1334 {
1335 storageThread = new EmacsSafThread (resolver);
1336 storageThread.start ();
1337 }
1338
1339 return storageThread.documentIdFromName (tree_uri, name,
1340 id_return);
1341 }
1342
1343 /* Return an encoded document URI representing a tree with the
1344 specified IDENTIFIER supplied by the authority AUTHORITY.
1345
1346 Return null instead if Emacs does not have permanent access
1347 to the specified document tree recorded on disk. */
1348
1349 public String
1350 getTreeUri (String tree, String authority)
1351 {
1352 Uri uri, grantedUri;
1353 List<UriPermission> permissions;
1354
1355 /* First, build the URI. */
1356 tree = Uri.decode (tree);
1357 uri = DocumentsContract.buildTreeDocumentUri (authority, tree);
1358
1359 /* Now, search for it within the list of persisted URI
1360 permissions. */
1361 permissions = resolver.getPersistedUriPermissions ();
1362
1363 for (UriPermission permission : permissions)
1364 {
1365 /* If the permission doesn't entitle Emacs to read access,
1366 skip it. */
1367
1368 if (!permission.isReadPermission ())
1369 continue;
1370
1371 grantedUri = permission.getUri ();
1372
1373 if (grantedUri.equals (uri))
1374 return uri.toString ();
1375 }
1376
1377 /* Emacs doesn't have permission to access this tree URI. */
1378 return null;
1379 }
1380
1381 /* Return file status for the document designated by the given
1382 DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document
1383 ID in URI itself.
1384
1385 Value is null upon failure, or an array of longs [MODE, SIZE,
1386 MTIM] upon success, where MODE contains the file type and access
1387 modes of the file as in `struct stat', SIZE is the size of the
1388 file in BYTES or -1 if not known, and MTIM is the time of the
1389 last modification to this file in milliseconds since 00:00,
1390 January 1st, 1970.
1391
1392 OperationCanceledException and other typical exceptions may be
1393 signaled upon receiving async input or other errors. */
1394
1395 public long[]
1396 statDocument (String uri, String documentId)
1397 {
1398 /* Start the thread used to run SAF requests if it isn't already
1399 running. */
1400
1401 if (storageThread == null)
1402 {
1403 storageThread = new EmacsSafThread (resolver);
1404 storageThread.start ();
1405 }
1406
1407 return storageThread.statDocument (uri, documentId);
1408 }
1409
1410 /* Find out whether Emacs has access to the document designated by
1411 the specified DOCUMENTID within the tree URI. If DOCUMENTID is
1412 NULL, use the document ID in URI itself.
1413
1414 If WRITABLE, also check that the file is writable, which is true
1415 if it is either a directory or its flags contains
1416 FLAG_SUPPORTS_WRITE.
1417
1418 Value is 0 if the file is accessible, and one of the following if
1419 not:
1420
1421 -1, if the file does not exist.
1422 -2, if WRITABLE and the file is not writable.
1423 -3, upon any other error.
1424
1425 In addition, arbitrary runtime exceptions (such as
1426 SecurityException or UnsupportedOperationException) may be
1427 thrown. */
1428
1429 public int
1430 accessDocument (String uri, String documentId, boolean writable)
1431 {
1432 /* Start the thread used to run SAF requests if it isn't already
1433 running. */
1434
1435 if (storageThread == null)
1436 {
1437 storageThread = new EmacsSafThread (resolver);
1438 storageThread.start ();
1439 }
1440
1441 return storageThread.accessDocument (uri, documentId, writable);
1442 }
1443
1444 /* Open a cursor representing each entry within the directory
1445 designated by the specified DOCUMENTID within the tree URI.
1446
1447 If DOCUMENTID is NULL, use the document ID within URI itself.
1448 Value is NULL upon failure.
1449
1450 In addition, arbitrary runtime exceptions (such as
1451 SecurityException or UnsupportedOperationException) may be
1452 thrown. */
1453
1454 public Cursor
1455 openDocumentDirectory (String uri, String documentId)
1456 {
1457 /* Start the thread used to run SAF requests if it isn't already
1458 running. */
1459
1460 if (storageThread == null)
1461 {
1462 storageThread = new EmacsSafThread (resolver);
1463 storageThread.start ();
1464 }
1465
1466 return storageThread.openDocumentDirectory (uri, documentId);
1467 }
1468
1469 /* Read a single directory entry from the specified CURSOR. Return
1470 NULL if at the end of the directory stream, and a directory entry
1471 with `d_name' set to NULL if an error occurs. */
1472
1473 public EmacsDirectoryEntry
1474 readDirectoryEntry (Cursor cursor)
1475 {
1476 EmacsDirectoryEntry entry;
1477 int index;
1478 String name, type;
1479
1480 entry = new EmacsDirectoryEntry ();
1481
1482 while (true)
1483 {
1484 if (!cursor.moveToNext ())
1485 return null;
1486
1487 /* First, retrieve the display name. */
1488 index = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
1489
1490 if (index < 0)
1491 /* Return an invalid directory entry upon failure. */
1492 return entry;
1493
1494 try
1495 {
1496 name = cursor.getString (index);
1497 }
1498 catch (Exception exception)
1499 {
1500 return entry;
1501 }
1502
1503 /* Skip this entry if its name cannot be represented. */
1504
1505 if (name.equals ("..") || name.equals (".") || name.contains ("/"))
1506 continue;
1507
1508 /* Now, look for its type. */
1509
1510 index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1511
1512 if (index < 0)
1513 /* Return an invalid directory entry upon failure. */
1514 return entry;
1515
1516 try
1517 {
1518 type = cursor.getString (index);
1519 }
1520 catch (Exception exception)
1521 {
1522 return entry;
1523 }
1524
1525 if (type != null
1526 && type.equals (Document.MIME_TYPE_DIR))
1527 entry.d_type = 1;
1528 entry.d_name = name;
1529 return entry;
1530 }
1531
1532 /* Not reached. */
1533 }
1534
1535 /* Open a file descriptor for a file document designated by
1536 DOCUMENTID within the document tree identified by URI. If
1537 TRUNCATE and the document already exists, truncate its contents
1538 before returning.
1539
1540 On Android 9.0 and earlier, always open the document in
1541 ``read-write'' mode; this instructs the document provider to
1542 return a seekable file that is stored on disk and returns correct
1543 file status.
1544
1545 Under newer versions of Android, open the document in a
1546 non-writable mode if WRITE is false. This is possible because
1547 these versions allow Emacs to explicitly request a seekable
1548 on-disk file.
1549
1550 Value is NULL upon failure or a parcel file descriptor upon
1551 success. Call `ParcelFileDescriptor.close' on this file
1552 descriptor instead of using the `close' system call.
1553
1554 FileNotFoundException and/or SecurityException and
1555 UnsupportedOperationException may be thrown upon failure. */
1556
1557 public ParcelFileDescriptor
1558 openDocument (String uri, String documentId, boolean write,
1559 boolean truncate)
1560 {
1561 /* Start the thread used to run SAF requests if it isn't already
1562 running. */
1563
1564 if (storageThread == null)
1565 {
1566 storageThread = new EmacsSafThread (resolver);
1567 storageThread.start ();
1568 }
1569
1570 return storageThread.openDocument (uri, documentId, write,
1571 truncate);
1572 }
1573
1574 /* Create a new document with the given display NAME within the
1575 directory identified by DOCUMENTID inside the document tree
1576 designated by URI.
1577
1578 If DOCUMENTID is NULL, create the document inside the root of
1579 that tree.
1580
1581 Either FileNotFoundException, SecurityException or
1582 UnsupportedOperationException may be thrown upon failure.
1583
1584 Return the document ID of the new file upon success, NULL
1585 otherwise. */
1586
1587 public String
1588 createDocument (String uri, String documentId, String name)
1589 throws FileNotFoundException
1590 {
1591 String mimeType, separator, mime, extension;
1592 int index;
1593 MimeTypeMap singleton;
1594 Uri treeUri, directoryUri, docUri;
1595
1596 /* Try to get the MIME type for this document.
1597 Default to ``application/octet-stream''. */
1598
1599 mimeType = "application/octet-stream";
1600
1601 /* Abuse WebView stuff to get the file's MIME type. */
1602
1603 index = name.lastIndexOf ('.');
1604
1605 if (index > 0)
1606 {
1607 singleton = MimeTypeMap.getSingleton ();
1608 extension = name.substring (index + 1);
1609 mime = singleton.getMimeTypeFromExtension (extension);
1610
1611 if (mime != null)
1612 mimeType = mime;
1613 }
1614
1615 /* Now parse URI. */
1616 treeUri = Uri.parse (uri);
1617
1618 if (documentId == null)
1619 documentId = DocumentsContract.getTreeDocumentId (treeUri);
1620
1621 /* And build a file URI referring to the directory. */
1622
1623 directoryUri
1624 = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri,
1625 documentId);
1626
1627 docUri = DocumentsContract.createDocument (resolver,
1628 directoryUri,
1629 mimeType, name);
1630
1631 if (docUri == null)
1632 return null;
1633
1634 /* Invalidate the file status of the containing directory. */
1635
1636 if (storageThread != null)
1637 storageThread.postInvalidateStat (treeUri, documentId);
1638
1639 /* Return the ID of the new document. */
1640 return DocumentsContract.getDocumentId (docUri);
1641 }
1642
1643 /* Like `createDocument', but create a directory instead of an
1644 ordinary document. */
1645
1646 public String
1647 createDirectory (String uri, String documentId, String name)
1648 throws FileNotFoundException
1649 {
1650 int index;
1651 Uri treeUri, directoryUri, docUri;
1652
1653 /* Now parse URI. */
1654 treeUri = Uri.parse (uri);
1655
1656 if (documentId == null)
1657 documentId = DocumentsContract.getTreeDocumentId (treeUri);
1658
1659 /* And build a file URI referring to the directory. */
1660
1661 directoryUri
1662 = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri,
1663 documentId);
1664
1665 /* If name ends with a directory separator character, delete
1666 it. */
1667
1668 if (name.endsWith ("/"))
1669 name = name.substring (0, name.length () - 1);
1670
1671 /* From Android's perspective, directories are just ordinary
1672 documents with the `MIME_TYPE_DIR' type. */
1673
1674 docUri = DocumentsContract.createDocument (resolver,
1675 directoryUri,
1676 Document.MIME_TYPE_DIR,
1677 name);
1678
1679 if (docUri == null)
1680 return null;
1681
1682 /* Return the ID of the new document, but first invalidate the
1683 state of the containing directory. */
1684
1685 if (storageThread != null)
1686 storageThread.postInvalidateStat (treeUri, documentId);
1687
1688 return DocumentsContract.getDocumentId (docUri);
1689 }
1690
1691 /* Delete the document identified by ID from the document tree
1692 identified by URI. Return 0 upon success and -1 upon
1693 failure.
1694
1695 NAME should be the name of the document being deleted, and is
1696 used to invalidate the cache. */
1697
1698 public int
1699 deleteDocument (String uri, String id, String name)
1700 throws FileNotFoundException
1701 {
1702 Uri uriObject, tree;
1703
1704 tree = Uri.parse (uri);
1705 uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, id);
1706
1707 if (DocumentsContract.deleteDocument (resolver, uriObject))
1708 {
1709 if (storageThread != null)
1710 storageThread.postInvalidateCache (tree, id, name);
1711
1712 return 0;
1713 }
1714
1715 return -1;
1716 }
1717
1718 /* Rename the document designated by DOCID inside the directory tree
1719 identified by URI, which should be within the directory
1720 designated by DIR, to NAME. If the file can't be renamed because
1721 it doesn't support renaming, return -1, 0 otherwise. */
1722
1723 public int
1724 renameDocument (String uri, String docId, String dir, String name)
1725 throws FileNotFoundException
1726 {
1727 Uri tree, uriObject;
1728
1729 tree = Uri.parse (uri);
1730 uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, docId);
1731
1732 if (DocumentsContract.renameDocument (resolver, uriObject,
1733 name)
1734 != null)
1735 {
1736 /* Invalidate the cache. */
1737 if (storageThread != null)
1738 storageThread.postInvalidateCacheDir (tree, docId,
1739 name);
1740 return 0;
1741 }
1742
1743 /* Handle errors specially, so `android_saf_rename_document' can
1744 return ENXDEV. */
1745 return -1;
1746 }
1747
1748 /* Move the document designated by DOCID from the directory under
1749 DIR_NAME designated by SRCID to the directory designated by
1750 DSTID. If the ID of the document being moved changes as a
1751 consequence of the movement, return the new ID, else NULL.
1752
1753 URI is the document tree containing all three documents. */
1754
1755 public String
1756 moveDocument (String uri, String docId, String dirName,
1757 String dstId, String srcId)
1758 throws FileNotFoundException
1759 {
1760 Uri uri1, docId1, dstId1, srcId1;
1761 Uri name;
1762
1763 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
1764 throw new UnsupportedOperationException ("Documents aren't capable"
1765 + " of being moved on Android"
1766 + " versions before 7.0.");
1767
1768 uri1 = Uri.parse (uri);
1769 docId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, docId);
1770 dstId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, dstId);
1771 srcId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, srcId);
1772
1773 /* Move the document; this function returns the new ID of the
1774 document should it change. */
1775 name = DocumentsContract.moveDocument (resolver, docId1,
1776 srcId1, dstId1);
1777
1778 /* Now invalidate the caches for both DIRNAME and DOCID. */
1779
1780 if (storageThread != null)
1781 {
1782 storageThread.postInvalidateCacheDir (uri1, docId, dirName);
1783
1784 /* Invalidate the stat cache entries for both the source and
1785 destination directories, since their contents have
1786 changed. */
1787 storageThread.postInvalidateStat (uri1, dstId);
1788 storageThread.postInvalidateStat (uri1, srcId);
1789 }
1790
1791 return (name != null
1792 ? DocumentsContract.getDocumentId (name)
1793 : null);
1794 }
1795
1796 /* Return if there is a content provider by the name of AUTHORITY
1797 supplying at least one tree URI Emacs retains persistent rights
1798 to access. */
1799
1800 public boolean
1801 validAuthority (String authority)
1802 {
1803 List<UriPermission> permissions;
1804 Uri uri;
1805
1806 permissions = resolver.getPersistedUriPermissions ();
1807
1808 for (UriPermission permission : permissions)
1809 {
1810 uri = permission.getUri ();
1811
1812 if (DocumentsContract.isTreeUri (uri)
1813 && permission.isReadPermission ()
1814 && uri.getAuthority ().equals (authority))
1815 return true;
1816 }
1817
1818 return false;
1819 }
1820};
diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java
new file mode 100644
index 00000000000..c47696b35c0
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSurfaceView.java
@@ -0,0 +1,223 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.view.View;
23
24import android.os.Build;
25
26import android.graphics.Bitmap;
27import android.graphics.Canvas;
28import android.graphics.Rect;
29import android.graphics.Paint;
30
31import java.lang.ref.WeakReference;
32
33/* This originally extended SurfaceView. However, doing so proved to
34 be too slow, and Android's surface view keeps up to three of its
35 own back buffers, which use too much memory (up to 96 MB for a
36 single frame.) */
37
38public final class EmacsSurfaceView extends View
39{
40 private static final String TAG = "EmacsSurfaceView";
41
42 /* The complete buffer contents at the time of the last draw. */
43 private Bitmap frontBuffer;
44
45 /* Whether frontBuffer has been updated since the last call to
46 `onDraw'. */
47 private boolean bitmapChanged;
48
49 /* Canvas representing the front buffer. */
50 private Canvas bitmapCanvas;
51
52 /* Reference to the last bitmap copied to the front buffer. */
53 private WeakReference<Bitmap> bitmap;
54
55 /* Paint objects used on the main and UI threads, respectively. */
56 private static final Paint bitmapPaint, uiThreadPaint;
57
58 static
59 {
60 /* Create two different Paint objects; one is used on the main
61 thread for buffer swaps, while the other is used from the UI
62 thread in `onDraw'. This is necessary because Paint objects
63 are not thread-safe, even if their uses are interlocked. */
64
65 bitmapPaint = new Paint ();
66 uiThreadPaint = new Paint ();
67 };
68
69 public
70 EmacsSurfaceView (EmacsView view)
71 {
72 super (view.getContext ());
73
74 this.bitmap = new WeakReference<Bitmap> (null);
75 }
76
77 private void
78 copyToFrontBuffer (Bitmap bitmap, Rect damageRect)
79 {
80 EmacsService.checkEmacsThread ();
81
82 if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O
83 && Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1
84 && Build.VERSION.SDK_INT != Build.VERSION_CODES.N_MR1
85 && Build.VERSION.SDK_INT != Build.VERSION_CODES.N)
86 {
87 /* If `drawBitmap' can safely be used while a bitmap is locked
88 by another thread, continue here... */
89
90 if (damageRect != null)
91 bitmapCanvas.drawBitmap (bitmap, damageRect, damageRect,
92 bitmapPaint);
93 else
94 bitmapCanvas.drawBitmap (bitmap, 0f, 0f, bitmapPaint);
95 }
96 else
97 {
98 /* But if it can not, as on Android 7.0 through 8.1, then use
99 a replacement function. */
100
101 if (damageRect != null)
102 EmacsNative.blitRect (bitmap, frontBuffer,
103 damageRect.left,
104 damageRect.top,
105 damageRect.right,
106 damageRect.bottom);
107 else
108 EmacsNative.blitRect (bitmap, frontBuffer, 0, 0,
109 bitmap.getWidth (),
110 bitmap.getHeight ());
111 }
112
113 /* See the large comment inside `onDraw'. */
114 bitmapChanged = true;
115 }
116
117 private void
118 reconfigureFrontBuffer (Bitmap bitmap)
119 {
120 /* First, remove the old front buffer. */
121
122 if (frontBuffer != null)
123 {
124 frontBuffer.recycle ();
125 frontBuffer = null;
126 bitmapCanvas = null;
127 }
128
129 this.bitmap = new WeakReference<Bitmap> (bitmap);
130
131 /* Next, create the new front buffer if necessary. */
132
133 if (bitmap != null && frontBuffer == null)
134 {
135 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
136 frontBuffer = Bitmap.createBitmap (bitmap.getWidth (),
137 bitmap.getHeight (),
138 Bitmap.Config.ARGB_8888,
139 false);
140 else
141 frontBuffer = Bitmap.createBitmap (bitmap.getWidth (),
142 bitmap.getHeight (),
143 Bitmap.Config.ARGB_8888);
144
145 bitmapCanvas = new Canvas (frontBuffer);
146
147 /* And copy over the bitmap contents. */
148 copyToFrontBuffer (bitmap, null);
149 }
150 else if (bitmap != null)
151 /* Just copy over the bitmap contents. */
152 copyToFrontBuffer (bitmap, null);
153 }
154
155 public synchronized void
156 setBitmap (Bitmap bitmap, Rect damageRect)
157 {
158 if (bitmap != this.bitmap.get ())
159 reconfigureFrontBuffer (bitmap);
160 else if (bitmap != null)
161 copyToFrontBuffer (bitmap, damageRect);
162
163 if (bitmap != null)
164 {
165 /* In newer versions of Android, the invalid rectangle is
166 supposedly internally calculated by the system. How that
167 is done is unknown, but calling `invalidateRect' is now
168 deprecated.
169
170 Fortunately, nobody has deprecated the version of
171 `postInvalidate' that accepts a dirty rectangle. */
172
173 if (damageRect != null)
174 postInvalidate (damageRect.left, damageRect.top,
175 damageRect.right, damageRect.bottom);
176 else
177 postInvalidate ();
178 }
179 }
180
181 @Override
182 public synchronized void
183 onDraw (Canvas canvas)
184 {
185 /* Paint the view's bitmap; the bitmap might be recycled right
186 now. */
187
188 if (frontBuffer != null)
189 {
190 /* The first time the bitmap is drawn after a buffer swap,
191 mark its contents as having changed. This increments the
192 ``generation ID'' used by Android to avoid uploading buffer
193 textures for unchanged bitmaps.
194
195 When a buffer swap takes place, the bitmap is initially
196 updated from the Emacs thread, resulting in the generation
197 ID being increased. If the render thread is texturizing
198 the bitmap while the swap takes place, it might record the
199 generation ID after the update for a texture containing the
200 contents of the bitmap prior to the swap, leaving the
201 texture tied to the bitmap partially updated.
202
203 Android never calls `onDraw' if the render thread is still
204 processing the bitmap. Update the generation ID here to
205 ensure that a new texture will be uploaded if the bitmap
206 has changed.
207
208 Uploading the bitmap contents to the GPU uses an excessive
209 amount of memory, as the entire bitmap is placed into the
210 graphics command queue, but this memory is actually shared
211 among all other applications and reclaimed by the system
212 when necessary. */
213
214 if (bitmapChanged)
215 {
216 EmacsNative.notifyPixelsChanged (frontBuffer);
217 bitmapChanged = false;
218 }
219
220 canvas.drawBitmap (frontBuffer, 0f, 0f, uiThreadPaint);
221 }
222 }
223};
diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java
new file mode 100644
index 00000000000..5307015b46f
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsThread.java
@@ -0,0 +1,82 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.lang.Thread;
23import java.util.Arrays;
24
25import android.util.Log;
26
27public final class EmacsThread extends Thread
28{
29 private static final String TAG = "EmacsThread";
30
31 /* Whether or not Emacs should be started with an additional
32 argument, and that additional argument if non-NULL. */
33 private String extraStartupArgument;
34
35 /* Runnable run to initialize Emacs. */
36 private Runnable paramsClosure;
37
38 /* Whether or not to open a file after starting Emacs. */
39 private String fileToOpen;
40
41 public
42 EmacsThread (EmacsService service, Runnable paramsClosure,
43 String extraStartupArgument, String fileToOpen)
44 {
45 super ("Emacs main thread");
46 this.extraStartupArgument = extraStartupArgument;
47 this.paramsClosure = paramsClosure;
48 this.fileToOpen = fileToOpen;
49 }
50
51 @Override
52 public void
53 run ()
54 {
55 String args[];
56
57 if (fileToOpen == null)
58 {
59 if (extraStartupArgument == null)
60 args = new String[] { "libandroid-emacs.so", };
61 else
62 args = new String[] { "libandroid-emacs.so",
63 extraStartupArgument, };
64 }
65 else
66 {
67 if (extraStartupArgument == null)
68 args = new String[] { "libandroid-emacs.so",
69 fileToOpen, };
70 else
71 args = new String[] { "libandroid-emacs.so",
72 extraStartupArgument,
73 fileToOpen, };
74 }
75
76 paramsClosure.run ();
77
78 /* Run the native code now. */
79 Log.d (TAG, "run: " + Arrays.toString (args));
80 EmacsNative.initEmacs (args, EmacsApplication.dumpFileName);
81 }
82};
diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java
new file mode 100644
index 00000000000..12d8ff4da56
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsView.java
@@ -0,0 +1,777 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.content.Context;
23
24import android.text.InputType;
25
26import android.view.ContextMenu;
27import android.view.View;
28import android.view.KeyEvent;
29import android.view.MotionEvent;
30import android.view.ViewGroup;
31import android.view.ViewTreeObserver;
32
33import android.view.inputmethod.EditorInfo;
34import android.view.inputmethod.InputConnection;
35import android.view.inputmethod.InputMethodManager;
36
37import android.graphics.Bitmap;
38import android.graphics.Canvas;
39import android.graphics.Rect;
40import android.graphics.Region;
41import android.graphics.Paint;
42
43import android.os.Build;
44import android.util.Log;
45
46/* This is an Android view which has a back and front buffer. When
47 swapBuffers is called, the back buffer is swapped to the front
48 buffer, and any damage is invalidated. frontBitmap and backBitmap
49 are modified and used both from the UI and the Emacs thread. As a
50 result, there is a lock held during all drawing operations.
51
52 It is also a ViewGroup, as it also lays out children. */
53
54public final class EmacsView extends ViewGroup
55 implements ViewTreeObserver.OnGlobalLayoutListener
56{
57 public static final String TAG = "EmacsView";
58
59 /* The associated EmacsWindow. */
60 public EmacsWindow window;
61
62 /* The buffer bitmap. */
63 public Bitmap bitmap;
64
65 /* The associated canvases. */
66 public Canvas canvas;
67
68 /* The damage region. */
69 public Region damageRegion;
70
71 /* The associated surface view. */
72 private EmacsSurfaceView surfaceView;
73
74 /* Whether or not a configure event must be sent for the next layout
75 event regardless of what changed. */
76 public boolean mustReportLayout;
77
78 /* Whether or not bitmaps must be recreated upon the next call to
79 getBitmap. */
80 private boolean bitmapDirty;
81
82 /* Whether or not a popup is active. */
83 private boolean popupActive;
84
85 /* The current context menu. */
86 private EmacsContextMenu contextMenu;
87
88 /* The last measured width and height. */
89 private int measuredWidth, measuredHeight;
90
91 /* Object acting as a lock for those values. */
92 private Object dimensionsLock;
93
94 /* The serial of the last clip rectangle change. */
95 private long lastClipSerial;
96
97 /* The InputMethodManager for this view's context. */
98 public InputMethodManager imManager;
99
100 /* Whether or not this view is attached to a window. */
101 public boolean isAttachedToWindow;
102
103 /* Whether or not this view should have the on screen keyboard
104 displayed whenever possible. */
105 public boolean isCurrentlyTextEditor;
106
107 /* The associated input connection. */
108 private EmacsInputConnection inputConnection;
109
110 /* The current IC mode. See `android_reset_ic' for more
111 details. */
112 private int icMode;
113
114 /* The number of calls to `resetIC' to have taken place the last
115 time an InputConnection was created. */
116 public long icSerial;
117
118 /* The number of calls to `recetIC' that have taken place. */
119 public volatile long icGeneration;
120
121 public
122 EmacsView (EmacsWindow window)
123 {
124 super (EmacsService.SERVICE);
125
126 Object tem;
127 Context context;
128
129 this.window = window;
130 this.damageRegion = new Region ();
131
132 setFocusable (true);
133 setFocusableInTouchMode (true);
134
135 /* Create the surface view. */
136 this.surfaceView = new EmacsSurfaceView (this);
137 addView (this.surfaceView);
138
139 /* Get rid of the default focus highlight. */
140 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O)
141 setDefaultFocusHighlightEnabled (false);
142
143 /* Obtain the input method manager. */
144 context = getContext ();
145 tem = context.getSystemService (Context.INPUT_METHOD_SERVICE);
146 imManager = (InputMethodManager) tem;
147
148 /* Add this view as its own global layout listener. */
149 getViewTreeObserver ().addOnGlobalLayoutListener (this);
150
151 /* Create an object used as a lock. */
152 this.dimensionsLock = new Object ();
153 }
154
155 private void
156 handleDirtyBitmap ()
157 {
158 Bitmap oldBitmap;
159 int measuredWidth, measuredHeight;
160
161 synchronized (dimensionsLock)
162 {
163 /* Load measuredWidth and measuredHeight. */
164 measuredWidth = this.measuredWidth;
165 measuredHeight = this.measuredHeight;
166 }
167
168 if (measuredWidth == 0 || measuredHeight == 0)
169 return;
170
171 if (!isAttachedToWindow)
172 return;
173
174 /* If bitmap is the same width and height as the measured width
175 and height, there is no need to do anything. Avoid allocating
176 the extra bitmap. */
177 if (bitmap != null
178 && (bitmap.getWidth () == measuredWidth
179 && bitmap.getHeight () == measuredHeight))
180 {
181 bitmapDirty = false;
182 return;
183 }
184
185 /* Save the old bitmap. */
186 oldBitmap = bitmap;
187
188 /* Recreate the back buffer bitmap. */
189 bitmap
190 = Bitmap.createBitmap (measuredWidth,
191 measuredHeight,
192 Bitmap.Config.ARGB_8888);
193 bitmap.eraseColor (window.background | 0xff000000);
194
195 /* And canvases. */
196 canvas = new Canvas (bitmap);
197 canvas.save ();
198
199 /* Since the clip rectangles have been cleared, clear the clip
200 rectangle ID. */
201 lastClipSerial = 0;
202
203 /* Copy over the contents of the old bitmap. */
204 if (oldBitmap != null)
205 canvas.drawBitmap (oldBitmap, 0f, 0f, new Paint ());
206
207 bitmapDirty = false;
208
209 /* Explicitly free the old bitmap's memory. */
210
211 if (oldBitmap != null)
212 oldBitmap.recycle ();
213
214 /* Some Android versions still don't free the bitmap until the
215 next GC. */
216 Runtime.getRuntime ().gc ();
217 }
218
219 public synchronized void
220 explicitlyDirtyBitmap ()
221 {
222 bitmapDirty = true;
223 }
224
225 public synchronized Bitmap
226 getBitmap ()
227 {
228 if (bitmapDirty || bitmap == null)
229 handleDirtyBitmap ();
230
231 return bitmap;
232 }
233
234 public synchronized Canvas
235 getCanvas (EmacsGC gc)
236 {
237 int i;
238
239 if (bitmapDirty || bitmap == null)
240 handleDirtyBitmap ();
241
242 if (canvas == null)
243 return null;
244
245 /* Update clip rectangles if necessary. */
246 if (gc.clipRectID != lastClipSerial)
247 {
248 canvas.restore ();
249 canvas.save ();
250
251 if (gc.real_clip_rects != null)
252 {
253 for (i = 0; i < gc.real_clip_rects.length; ++i)
254 canvas.clipRect (gc.real_clip_rects[i]);
255 }
256
257 lastClipSerial = gc.clipRectID;
258 }
259
260 return canvas;
261 }
262
263 public void
264 prepareForLayout (int wantedWidth, int wantedHeight)
265 {
266 synchronized (dimensionsLock)
267 {
268 measuredWidth = wantedWidth;
269 measuredHeight = wantedWidth;
270 }
271 }
272
273 @Override
274 protected void
275 onMeasure (int widthMeasureSpec, int heightMeasureSpec)
276 {
277 Rect measurements;
278 int width, height;
279
280 /* Return the width and height of the window regardless of what
281 the parent says. */
282 measurements = window.getGeometry ();
283
284 width = measurements.width ();
285 height = measurements.height ();
286
287 /* Now apply any extra requirements in widthMeasureSpec and
288 heightMeasureSpec. */
289
290 if (MeasureSpec.getMode (widthMeasureSpec) == MeasureSpec.EXACTLY)
291 width = MeasureSpec.getSize (widthMeasureSpec);
292 else if (MeasureSpec.getMode (widthMeasureSpec) == MeasureSpec.AT_MOST
293 && width > MeasureSpec.getSize (widthMeasureSpec))
294 width = MeasureSpec.getSize (widthMeasureSpec);
295
296 if (MeasureSpec.getMode (heightMeasureSpec) == MeasureSpec.EXACTLY)
297 height = MeasureSpec.getSize (heightMeasureSpec);
298 else if (MeasureSpec.getMode (heightMeasureSpec) == MeasureSpec.AT_MOST
299 && height > MeasureSpec.getSize (heightMeasureSpec))
300 height = MeasureSpec.getSize (heightMeasureSpec);
301
302 super.setMeasuredDimension (width, height);
303 }
304
305 /* Note that the monitor lock for the window must never be held from
306 within the lock for the view, because the window also locks the
307 other way around. */
308
309 @Override
310 protected void
311 onLayout (boolean changed, int left, int top, int right,
312 int bottom)
313 {
314 int count, i, oldMeasuredWidth, oldMeasuredHeight;
315 View child;
316 Rect windowRect;
317 boolean needExpose;
318
319 count = getChildCount ();
320 needExpose = false;
321
322 synchronized (dimensionsLock)
323 {
324 /* Load measuredWidth and measuredHeight. */
325 oldMeasuredWidth = measuredWidth;
326 oldMeasuredHeight = measuredHeight;
327
328 /* Set measuredWidth and measuredHeight. */
329 measuredWidth = right - left;
330 measuredHeight = bottom - top;
331 }
332
333 /* Dirty the back buffer if the layout change resulted in the view
334 being resized. */
335
336 if (changed && (right - left != oldMeasuredWidth
337 || bottom - top != oldMeasuredHeight))
338 {
339 explicitlyDirtyBitmap ();
340
341 /* Expose the window upon a change in the view's size. */
342
343 if (right - left > oldMeasuredWidth
344 || bottom - top > oldMeasuredHeight)
345 needExpose = true;
346 }
347
348 for (i = 0; i < count; ++i)
349 {
350 child = getChildAt (i);
351
352 Log.d (TAG, "onLayout: " + child);
353
354 if (child == surfaceView)
355 child.layout (0, 0, right - left, bottom - top);
356 else if (child.getVisibility () != GONE)
357 {
358 if (!(child instanceof EmacsView))
359 continue;
360
361 /* What to do: lay out the view precisely according to its
362 window rect. */
363 windowRect = ((EmacsView) child).window.getGeometry ();
364 child.layout (windowRect.left, windowRect.top,
365 windowRect.right, windowRect.bottom);
366 }
367 }
368
369 /* Now report the layout change to the window. */
370
371 if (changed || mustReportLayout)
372 {
373 mustReportLayout = false;
374 window.viewLayout (left, top, right, bottom);
375 }
376
377 if (needExpose)
378 EmacsNative.sendExpose (this.window.handle, 0, 0,
379 right - left, bottom - top);
380 }
381
382 public void
383 damageRect (Rect damageRect)
384 {
385 EmacsService.checkEmacsThread ();
386 damageRegion.union (damageRect);
387 }
388
389 /* This method is called from both the UI thread and the Emacs
390 thread. */
391
392 public void
393 swapBuffers ()
394 {
395 Canvas canvas;
396 Rect damageRect;
397 Bitmap bitmap;
398
399 /* Make sure this function is called only from the Emacs
400 thread. */
401 EmacsService.checkEmacsThread ();
402
403 damageRect = null;
404
405 /* Now see if there is a damage region. */
406
407 if (damageRegion.isEmpty ())
408 return;
409
410 /* And extract and clear the damage region. */
411
412 damageRect = damageRegion.getBounds ();
413 damageRegion.setEmpty ();
414
415 bitmap = getBitmap ();
416
417 /* Transfer the bitmap to the surface view, then invalidate
418 it. */
419 surfaceView.setBitmap (bitmap, damageRect);
420 }
421
422 @Override
423 public boolean
424 onKeyDown (int keyCode, KeyEvent event)
425 {
426 if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP
427 || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
428 || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE)
429 && !EmacsNative.shouldForwardMultimediaButtons ())
430 return false;
431
432 window.onKeyDown (keyCode, event);
433 return true;
434 }
435
436 @Override
437 public boolean
438 onKeyMultiple (int keyCode, int repeatCount, KeyEvent event)
439 {
440 if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP
441 || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
442 || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE)
443 && !EmacsNative.shouldForwardMultimediaButtons ())
444 return false;
445
446 window.onKeyDown (keyCode, event);
447 return true;
448 }
449
450 @Override
451 public boolean
452 onKeyUp (int keyCode, KeyEvent event)
453 {
454 if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP
455 || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
456 || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE)
457 && !EmacsNative.shouldForwardMultimediaButtons ())
458 return false;
459
460 window.onKeyUp (keyCode, event);
461 return true;
462 }
463
464 @Override
465 public void
466 onFocusChanged (boolean gainFocus, int direction,
467 Rect previouslyFocusedRect)
468 {
469 window.onFocusChanged (gainFocus);
470 super.onFocusChanged (gainFocus, direction,
471 previouslyFocusedRect);
472 }
473
474 @Override
475 public boolean
476 onGenericMotionEvent (MotionEvent motion)
477 {
478 return window.onGenericMotionEvent (motion);
479 }
480
481 @Override
482 public boolean
483 onTouchEvent (MotionEvent motion)
484 {
485 return window.onTouchEvent (motion);
486 }
487
488 private void
489 moveChildToBack (View child)
490 {
491 int index;
492
493 index = indexOfChild (child);
494
495 if (index > 0)
496 {
497 detachViewFromParent (index);
498
499 /* The view at 0 is the surface view. */
500 attachViewToParent (child, 1,
501 child.getLayoutParams());
502 }
503 }
504
505 /* The following two functions must not be called if the view has no
506 parent, or is parented to an activity. */
507
508 public void
509 raise ()
510 {
511 EmacsView parent;
512
513 parent = (EmacsView) getParent ();
514
515 Log.d (TAG, "raise: parent " + parent);
516
517 if (parent.indexOfChild (this)
518 == parent.getChildCount () - 1)
519 return;
520
521 parent.bringChildToFront (this);
522 }
523
524 public void
525 lower ()
526 {
527 EmacsView parent;
528
529 parent = (EmacsView) getParent ();
530
531 Log.d (TAG, "lower: parent " + parent);
532
533 if (parent.indexOfChild (this) == 1)
534 return;
535
536 parent.moveChildToBack (this);
537 }
538
539 @Override
540 protected void
541 onCreateContextMenu (ContextMenu menu)
542 {
543 if (contextMenu == null)
544 return;
545
546 contextMenu.expandTo (menu, this);
547 }
548
549 public boolean
550 popupMenu (EmacsContextMenu menu, int xPosition,
551 int yPosition, boolean force)
552 {
553 if (popupActive && !force)
554 return false;
555
556 contextMenu = menu;
557 popupActive = true;
558
559 Log.d (TAG, "popupMenu: " + menu + " @" + xPosition
560 + ", " + yPosition + " " + force);
561
562 /* Use showContextMenu (float, float) on N to get actual popup
563 behavior. */
564 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
565 return showContextMenu ((float) xPosition, (float) yPosition);
566 else
567 return showContextMenu ();
568 }
569
570 public void
571 cancelPopupMenu ()
572 {
573 if (!popupActive)
574 throw new IllegalStateException ("cancelPopupMenu called without"
575 + " popupActive set");
576
577 contextMenu = null;
578 popupActive = false;
579
580 /* It is not possible to know with 100% certainty which activity
581 is currently displaying the context menu. Loop through each
582 activity and call `closeContextMenu' instead. */
583
584 for (EmacsWindowAttachmentManager.WindowConsumer consumer
585 : EmacsWindowAttachmentManager.MANAGER.consumers)
586 {
587 if (consumer instanceof EmacsActivity)
588 ((EmacsActivity) consumer).closeContextMenu ();
589 }
590 }
591
592 @Override
593 public synchronized void
594 onDetachedFromWindow ()
595 {
596 isAttachedToWindow = false;
597
598 /* Recycle the bitmap and call GC. */
599
600 if (bitmap != null)
601 bitmap.recycle ();
602
603 bitmap = null;
604 canvas = null;
605 surfaceView.setBitmap (null, null);
606
607 /* Collect the bitmap storage; it could be large. */
608 Runtime.getRuntime ().gc ();
609
610 super.onDetachedFromWindow ();
611 }
612
613 @Override
614 public synchronized void
615 onAttachedToWindow ()
616 {
617 isAttachedToWindow = true;
618
619 /* Dirty the bitmap, as it was destroyed when onDetachedFromWindow
620 was called. */
621 bitmapDirty = true;
622
623 synchronized (dimensionsLock)
624 {
625 /* Now expose the view contents again. */
626 EmacsNative.sendExpose (this.window.handle, 0, 0,
627 measuredWidth, measuredHeight);
628 }
629
630 super.onAttachedToWindow ();
631 }
632
633 public void
634 showOnScreenKeyboard ()
635 {
636 /* Specifying no flags at all tells the system the user asked for
637 the input method to be displayed. */
638
639 imManager.showSoftInput (this, 0);
640 isCurrentlyTextEditor = true;
641 }
642
643 public void
644 hideOnScreenKeyboard ()
645 {
646 imManager.hideSoftInputFromWindow (this.getWindowToken (),
647 0);
648 isCurrentlyTextEditor = false;
649 }
650
651 @Override
652 public InputConnection
653 onCreateInputConnection (EditorInfo info)
654 {
655 int mode;
656 int[] selection;
657
658 /* Figure out what kind of IME behavior Emacs wants. */
659 mode = getICMode ();
660
661 /* Make sure the input method never displays a full screen input
662 box that obscures Emacs. */
663 info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
664 info.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI;
665
666 /* Set a reasonable inputType. */
667 info.inputType = InputType.TYPE_CLASS_TEXT;
668
669 /* If this fails or ANDROID_IC_MODE_NULL was requested, then don't
670 initialize the input connection. */
671
672 if (mode == EmacsService.IC_MODE_NULL)
673 {
674 info.inputType = InputType.TYPE_NULL;
675 return null;
676 }
677
678 /* Set icSerial. If icSerial < icGeneration, the input connection
679 has been reset, and future input should be ignored until a new
680 connection is created. */
681
682 icSerial = icGeneration;
683
684 /* Reset flags set by the previous input method. */
685
686 EmacsNative.clearInputFlags (window.handle);
687
688 /* Obtain the current position of point and set it as the
689 selection. Don't do this under one specific situation: if
690 `android_update_ic' is being called in the main thread, trying
691 to synchronize with it can cause a dead lock in the IM manager.
692 See icBeginSynchronous in EmacsService.java for more
693 details. */
694
695 selection = EmacsService.viewGetSelection (window.handle);
696
697 if (selection != null)
698 Log.d (TAG, "onCreateInputConnection: current selection is: "
699 + selection[0] + ", by " + selection[1]);
700 else
701 {
702 Log.d (TAG, "onCreateInputConnection: current selection could"
703 + " not be retrieved.");
704
705 /* If the selection could not be obtained, return 0 by 0.
706 However, ask for the selection position to be updated as
707 soon as possible. */
708
709 selection = new int[] { 0, 0, };
710 EmacsNative.requestSelectionUpdate (window.handle);
711 }
712
713 if (mode == EmacsService.IC_MODE_ACTION)
714 info.imeOptions |= EditorInfo.IME_ACTION_DONE;
715
716 /* Set the initial selection fields. */
717 info.initialSelStart = selection[0];
718 info.initialSelEnd = selection[1];
719
720 /* Create the input connection if necessary. */
721
722 if (inputConnection == null)
723 inputConnection = new EmacsInputConnection (this);
724 else
725 /* Clear several pieces of state in the input connection. */
726 inputConnection.reset ();
727
728 /* Return the input connection. */
729 return inputConnection;
730 }
731
732 @Override
733 public synchronized boolean
734 onCheckIsTextEditor ()
735 {
736 /* If value is true, then the system will display the on screen
737 keyboard. */
738 return isCurrentlyTextEditor;
739 }
740
741 @Override
742 public boolean
743 isOpaque ()
744 {
745 /* Returning true here allows the system to not draw the contents
746 of windows underneath this view, thereby improving
747 performance. */
748 return true;
749 }
750
751 public synchronized void
752 setICMode (int icMode)
753 {
754 this.icMode = icMode;
755 }
756
757 public synchronized int
758 getICMode ()
759 {
760 return icMode;
761 }
762
763 @Override
764 public void
765 onGlobalLayout ()
766 {
767 int[] locations;
768
769 /* Get the absolute offset of this view and specify its left and
770 top position in subsequent ConfigureNotify events. */
771
772 locations = new int[2];
773 getLocationInWindow (locations);
774 window.notifyContentRectPosition (locations[0],
775 locations[1]);
776 }
777};
diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java
new file mode 100644
index 00000000000..a1f70644e16
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsWindow.java
@@ -0,0 +1,1445 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.lang.IllegalStateException;
23import java.util.ArrayList;
24import java.util.List;
25import java.util.HashMap;
26import java.util.LinkedHashMap;
27import java.util.Map;
28
29import android.content.Context;
30
31import android.graphics.Rect;
32import android.graphics.Canvas;
33import android.graphics.Bitmap;
34import android.graphics.PixelFormat;
35
36import android.view.View;
37import android.view.ViewManager;
38import android.view.Gravity;
39import android.view.KeyEvent;
40import android.view.MotionEvent;
41import android.view.InputDevice;
42import android.view.WindowManager;
43
44import android.util.Log;
45
46import android.os.Build;
47
48/* This defines a window, which is a handle. Windows represent a
49 rectangular subset of the screen with their own contents.
50
51 Windows either have a parent window, in which case their views are
52 attached to the parent's view, or are "floating", in which case
53 their views are attached to the parent activity (if any), else
54 nothing.
55
56 Views are also drawables, meaning they can accept drawing
57 requests. */
58
59public final class EmacsWindow extends EmacsHandleObject
60 implements EmacsDrawable
61{
62 private static final String TAG = "EmacsWindow";
63
64 private static class Coordinate
65 {
66 /* Integral coordinate. */
67 int x, y;
68
69 /* Button associated with the coordinate, or 0 if it is a touch
70 event. */
71 int button;
72
73 /* Pointer ID associated with the coordinate. */
74 int id;
75
76 public
77 Coordinate (int x, int y, int button, int id)
78 {
79 this.x = x;
80 this.y = y;
81 this.button = button;
82 this.id = id;
83 }
84 };
85
86 /* The view associated with the window. */
87 public EmacsView view;
88
89 /* The geometry of the window. */
90 private Rect rect;
91
92 /* The parent window, or null if it is the root window. */
93 public EmacsWindow parent;
94
95 /* List of all children in stacking order. This must be kept
96 consistent with their Z order! */
97 public ArrayList<EmacsWindow> children;
98
99 /* Map between pointer identifiers and last known position. Used to
100 compute which pointer changed upon a touch event. */
101 private HashMap<Integer, Coordinate> pointerMap;
102
103 /* The window consumer currently attached, if it exists. */
104 private EmacsWindowAttachmentManager.WindowConsumer attached;
105
106 /* The window background scratch GC. foreground is always the
107 window background. */
108 private EmacsGC scratchGC;
109
110 /* The button state and keyboard modifier mask at the time of the
111 last button press or release event. */
112 public int lastButtonState;
113
114 /* Whether or not the window is mapped. */
115 private volatile boolean isMapped;
116
117 /* Whether or not to ask for focus upon being mapped. */
118 private boolean dontFocusOnMap;
119
120 /* Whether or not the window is override-redirect. An
121 override-redirect window always has its own system window. */
122 private boolean overrideRedirect;
123
124 /* The window manager that is the parent of this window. NULL if
125 there is no such window manager. */
126 private WindowManager windowManager;
127
128 /* The time of the last KEYCODE_VOLUME_DOWN release. This is used
129 to quit Emacs upon two rapid clicks of the volume down
130 button. */
131 private long lastVolumeButtonRelease;
132
133 /* Linked list of character strings which were recently sent as
134 events. */
135 public LinkedHashMap<Integer, String> eventStrings;
136
137 /* Whether or not this window is fullscreen. */
138 public boolean fullscreen;
139
140 /* The window background pixel. This is used by EmacsView when
141 creating new bitmaps. */
142 public volatile int background;
143
144 /* The position of this window relative to the root window. */
145 public int xPosition, yPosition;
146
147 public
148 EmacsWindow (short handle, final EmacsWindow parent, int x, int y,
149 int width, int height, boolean overrideRedirect)
150 {
151 super (handle);
152
153 rect = new Rect (x, y, x + width, y + height);
154 pointerMap = new HashMap<Integer, Coordinate> ();
155
156 /* Create the view from the context's UI thread. The window is
157 unmapped, so the view is GONE. */
158 view = EmacsService.SERVICE.getEmacsView (this, View.GONE,
159 parent == null);
160 this.parent = parent;
161 this.overrideRedirect = overrideRedirect;
162
163 /* Create the list of children. */
164 children = new ArrayList<EmacsWindow> ();
165
166 if (parent != null)
167 {
168 parent.children.add (this);
169 EmacsService.SERVICE.runOnUiThread (new Runnable () {
170 @Override
171 public void
172 run ()
173 {
174 parent.view.addView (view);
175 }
176 });
177 }
178
179 scratchGC = new EmacsGC ((short) 0);
180
181 /* Create the map of input method-committed strings. Keep at most
182 ten strings in the map. */
183
184 eventStrings
185 = new LinkedHashMap<Integer, String> () {
186 @Override
187 protected boolean
188 removeEldestEntry (Map.Entry<Integer, String> entry)
189 {
190 return size () > 10;
191 }
192 };
193 }
194
195 public void
196 changeWindowBackground (int pixel)
197 {
198 /* scratchGC is used as the argument to a FillRectangles req. */
199 scratchGC.foreground = pixel;
200 scratchGC.markDirty (false);
201
202 /* Make the background known to the view as well. */
203 background = pixel;
204 }
205
206 public synchronized Rect
207 getGeometry ()
208 {
209 return new Rect (rect);
210 }
211
212 @Override
213 public synchronized void
214 destroyHandle () throws IllegalStateException
215 {
216 if (parent != null)
217 parent.children.remove (this);
218
219 EmacsActivity.invalidateFocus ();
220
221 if (!children.isEmpty ())
222 throw new IllegalStateException ("Trying to destroy window with "
223 + "children!");
224
225 /* Remove the view from its parent and make it invisible. */
226 EmacsService.SERVICE.runOnUiThread (new Runnable () {
227 public void
228 run ()
229 {
230 ViewManager parent;
231 EmacsWindowAttachmentManager manager;
232
233 if (EmacsActivity.focusedWindow == EmacsWindow.this)
234 EmacsActivity.focusedWindow = null;
235
236 manager = EmacsWindowAttachmentManager.MANAGER;
237 view.setVisibility (View.GONE);
238
239 /* If the window manager is set, use that instead. */
240 if (windowManager != null)
241 parent = windowManager;
242 else
243 parent = (ViewManager) view.getParent ();
244 windowManager = null;
245
246 if (parent != null)
247 parent.removeView (view);
248
249 manager.detachWindow (EmacsWindow.this);
250 }
251 });
252
253 super.destroyHandle ();
254 }
255
256 public void
257 setConsumer (EmacsWindowAttachmentManager.WindowConsumer consumer)
258 {
259 attached = consumer;
260 }
261
262 public EmacsWindowAttachmentManager.WindowConsumer
263 getAttachedConsumer ()
264 {
265 return attached;
266 }
267
268 public synchronized long
269 viewLayout (int left, int top, int right, int bottom)
270 {
271 int rectWidth, rectHeight;
272
273 rect.left = left;
274 rect.top = top;
275 rect.right = right;
276 rect.bottom = bottom;
277
278 rectWidth = right - left;
279 rectHeight = bottom - top;
280
281 /* If parent is null, use xPosition and yPosition instead of the
282 geometry rectangle positions. */
283
284 if (parent == null)
285 {
286 left = xPosition;
287 top = yPosition;
288 }
289
290 return EmacsNative.sendConfigureNotify (this.handle,
291 System.currentTimeMillis (),
292 left, top, rectWidth,
293 rectHeight);
294 }
295
296 public void
297 requestViewLayout ()
298 {
299 view.explicitlyDirtyBitmap ();
300
301 EmacsService.SERVICE.runOnUiThread (new Runnable () {
302 @Override
303 public void
304 run ()
305 {
306 if (overrideRedirect)
307 /* Set the layout parameters again. */
308 view.setLayoutParams (getWindowLayoutParams ());
309
310 view.mustReportLayout = true;
311 view.requestLayout ();
312 }
313 });
314 }
315
316 public synchronized void
317 resizeWindow (int width, int height)
318 {
319 rect.right = rect.left + width;
320 rect.bottom = rect.top + height;
321
322 requestViewLayout ();
323 }
324
325 public synchronized void
326 moveWindow (int x, int y)
327 {
328 int width, height;
329
330 width = rect.width ();
331 height = rect.height ();
332
333 rect.left = x;
334 rect.top = y;
335 rect.right = x + width;
336 rect.bottom = y + height;
337
338 requestViewLayout ();
339 }
340
341 private WindowManager.LayoutParams
342 getWindowLayoutParams ()
343 {
344 WindowManager.LayoutParams params;
345 int flags, type;
346 Rect rect;
347
348 flags = 0;
349 rect = getGeometry ();
350 flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
351 flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
352 type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
353
354 params
355 = new WindowManager.LayoutParams (rect.width (), rect.height (),
356 rect.left, rect.top,
357 type, flags,
358 PixelFormat.RGBA_8888);
359 params.gravity = Gravity.TOP | Gravity.LEFT;
360 return params;
361 }
362
363 private Context
364 findSuitableActivityContext ()
365 {
366 /* Find a recently focused activity. */
367 if (!EmacsActivity.focusedActivities.isEmpty ())
368 return EmacsActivity.focusedActivities.get (0);
369
370 /* Return the service context, which probably won't work. */
371 return EmacsService.SERVICE;
372 }
373
374 public synchronized void
375 mapWindow ()
376 {
377 final int width, height;
378
379 if (isMapped)
380 return;
381
382 isMapped = true;
383 width = rect.width ();
384 height = rect.height ();
385
386 if (parent == null)
387 {
388 EmacsService.SERVICE.runOnUiThread (new Runnable () {
389 @Override
390 public void
391 run ()
392 {
393 EmacsWindowAttachmentManager manager;
394 WindowManager windowManager;
395 Context ctx;
396 Object tem;
397 WindowManager.LayoutParams params;
398
399 /* Make the view visible, first of all. */
400 view.setVisibility (View.VISIBLE);
401
402 if (!overrideRedirect)
403 {
404 manager = EmacsWindowAttachmentManager.MANAGER;
405
406 /* If parent is the root window, notice that there are new
407 children available for interested activites to pick
408 up. */
409 manager.registerWindow (EmacsWindow.this);
410
411 if (!getDontFocusOnMap ())
412 /* Eventually this should check no-focus-on-map. */
413 view.requestFocus ();
414 }
415 else
416 {
417 /* But if the window is an override-redirect window,
418 then:
419
420 - Find an activity that is currently active.
421
422 - Map the window as a panel on top of that
423 activity using the system window manager. */
424
425 ctx = findSuitableActivityContext ();
426 tem = ctx.getSystemService (Context.WINDOW_SERVICE);
427 windowManager = (WindowManager) tem;
428
429 /* Calculate layout parameters. */
430 params = getWindowLayoutParams ();
431 view.setLayoutParams (params);
432
433 /* Attach the view. */
434 try
435 {
436 view.prepareForLayout (width, height);
437 windowManager.addView (view, params);
438
439 /* Record the window manager being used in the
440 EmacsWindow object. */
441 EmacsWindow.this.windowManager = windowManager;
442 }
443 catch (Exception e)
444 {
445 Log.w (TAG,
446 "failed to attach override-redirect window, " + e);
447 }
448 }
449 }
450 });
451 }
452 else
453 {
454 /* Do the same thing as above, but don't register this
455 window. */
456 EmacsService.SERVICE.runOnUiThread (new Runnable () {
457 @Override
458 public void
459 run ()
460 {
461 /* Prior to mapping the view, set its measuredWidth and
462 measuredHeight to some reasonable value, in order to
463 avoid excessive bitmap dirtying. */
464
465 view.prepareForLayout (width, height);
466 view.setVisibility (View.VISIBLE);
467
468 if (!getDontFocusOnMap ())
469 view.requestFocus ();
470 }
471 });
472 }
473 }
474
475 public synchronized void
476 unmapWindow ()
477 {
478 if (!isMapped)
479 return;
480
481 isMapped = false;
482
483 view.post (new Runnable () {
484 @Override
485 public void
486 run ()
487 {
488 EmacsWindowAttachmentManager manager;
489
490 manager = EmacsWindowAttachmentManager.MANAGER;
491
492 view.setVisibility (View.GONE);
493
494 /* Detach the view from the window manager if possible. */
495 if (windowManager != null)
496 windowManager.removeView (view);
497 windowManager = null;
498
499 /* Now that the window is unmapped, unregister it as
500 well. */
501 manager.detachWindow (EmacsWindow.this);
502 }
503 });
504 }
505
506 @Override
507 public Canvas
508 lockCanvas (EmacsGC gc)
509 {
510 return view.getCanvas (gc);
511 }
512
513 @Override
514 public void
515 damageRect (Rect damageRect)
516 {
517 view.damageRect (damageRect);
518 }
519
520 public void
521 swapBuffers ()
522 {
523 view.swapBuffers ();
524 }
525
526 public void
527 clearWindow ()
528 {
529 EmacsService.SERVICE.fillRectangle (this, scratchGC,
530 0, 0, rect.width (),
531 rect.height ());
532 }
533
534 public void
535 clearArea (int x, int y, int width, int height)
536 {
537 EmacsService.SERVICE.fillRectangle (this, scratchGC,
538 x, y, width, height);
539 }
540
541 @Override
542 public Bitmap
543 getBitmap ()
544 {
545 return view.getBitmap ();
546 }
547
548 /* event.getCharacters is used because older input methods still
549 require it. */
550 @SuppressWarnings ("deprecation")
551 public int
552 getEventUnicodeChar (KeyEvent event, int state)
553 {
554 String characters;
555
556 if (event.getUnicodeChar (state) != 0)
557 return event.getUnicodeChar (state);
558
559 characters = event.getCharacters ();
560
561 if (characters != null && characters.length () == 1)
562 return characters.charAt (0);
563
564 return characters == null ? 0 : -1;
565 }
566
567 public void
568 saveUnicodeString (int serial, String string)
569 {
570 eventStrings.put (serial, string);
571 }
572
573
574
575 /* Return the modifier mask associated with the specified keyboard
576 input EVENT. Replace bits corresponding to Left or Right keys
577 with their corresponding general modifier bits. */
578
579 private int
580 eventModifiers (KeyEvent event)
581 {
582 int state;
583
584 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2)
585 state = event.getModifiers ();
586 else
587 {
588 /* Replace this with getMetaState and manual
589 normalization. */
590 state = event.getMetaState ();
591
592 /* Normalize the state by setting the generic modifier bit if
593 either a left or right modifier is pressed. */
594
595 if ((state & KeyEvent.META_ALT_LEFT_ON) != 0
596 || (state & KeyEvent.META_ALT_RIGHT_ON) != 0)
597 state |= KeyEvent.META_ALT_MASK;
598
599 if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0
600 || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0)
601 state |= KeyEvent.META_CTRL_MASK;
602 }
603
604 return state;
605 }
606
607 /* event.getCharacters is used because older input methods still
608 require it. */
609 @SuppressWarnings ("deprecation")
610 public void
611 onKeyDown (int keyCode, KeyEvent event)
612 {
613 int state, state_1;
614 long serial;
615 String characters;
616
617 state = eventModifiers (event);
618
619 /* Ignore meta-state understood by Emacs for now, or key presses
620 such as Ctrl+C and Meta+C will not be recognized as an ASCII
621 key press event. */
622
623 state_1
624 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK
625 | KeyEvent.META_SYM_ON | KeyEvent.META_META_MASK);
626
627 synchronized (eventStrings)
628 {
629 serial
630 = EmacsNative.sendKeyPress (this.handle,
631 event.getEventTime (),
632 state, keyCode,
633 getEventUnicodeChar (event,
634 state_1));
635
636 characters = event.getCharacters ();
637
638 if (characters != null && characters.length () > 1)
639 saveUnicodeString ((int) serial, characters);
640 }
641 }
642
643 public void
644 onKeyUp (int keyCode, KeyEvent event)
645 {
646 int state, state_1;
647 long time;
648
649 /* Compute the event's modifier mask. */
650 state = eventModifiers (event);
651
652 /* Ignore meta-state understood by Emacs for now, or key presses
653 such as Ctrl+C and Meta+C will not be recognized as an ASCII
654 key press event. */
655
656 state_1
657 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK
658 | KeyEvent.META_SYM_ON | KeyEvent.META_META_MASK);
659
660 EmacsNative.sendKeyRelease (this.handle,
661 event.getEventTime (),
662 state, keyCode,
663 getEventUnicodeChar (event,
664 state_1));
665
666 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
667 {
668 /* Check if this volume down press should quit Emacs.
669 Most Android devices have no physical keyboard, so it
670 is unreasonably hard to press C-g. */
671
672 time = event.getEventTime ();
673
674 if (time - lastVolumeButtonRelease < 350)
675 EmacsNative.quit ();
676
677 lastVolumeButtonRelease = time;
678 }
679 }
680
681 public void
682 onFocusChanged (boolean gainFocus)
683 {
684 EmacsActivity.invalidateFocus ();
685 }
686
687 /* Notice that the activity has been detached or destroyed.
688
689 ISFINISHING is set if the activity is not the main activity, or
690 if the activity was not destroyed in response to explicit user
691 action. */
692
693 public void
694 onActivityDetached (boolean isFinishing)
695 {
696 /* Destroy the associated frame when the activity is detached in
697 response to explicit user action. */
698
699 if (isFinishing)
700 EmacsNative.sendWindowAction (this.handle, 0);
701 }
702
703
704
705 /* Mouse and touch event handling.
706
707 Android does not conceptually distinguish between mouse events
708 (those coming from a device whose movement affects the on-screen
709 pointer image) and touch screen events. Each click or touch
710 starts a single pointer gesture sequence, and subsequent motion
711 of the device will result in updates being reported relative to
712 that sequence until the mouse button or touch is released.
713
714 When a touch, click, or pointer motion takes place, several kinds
715 of event can be sent:
716
717 ACTION_DOWN or ACTION_POINTER_DOWN is sent with a new coordinate
718 and an associated ``pointer ID'' identifying the event and its
719 gesture sequence when a click or touch takes place. Emacs is
720 responsible for recording both the position and pointer ID of
721 this click for the purpose of determining future changes to its
722 position.
723
724 ACTION_UP or ACTION_POINTER_UP is sent with a pointer ID when the
725 click associated with a previous ACTION_DOWN event is released.
726
727 ACTION_CANCEL (or ACTION_POINTER_UP with FLAG_CANCELED) is sent
728 if a similar situation transpires: the window system has chosen
729 to grab the click, and future changes to its position will no
730 longer be reported to Emacs.
731
732 ACTION_MOVE is sent if a coordinate tied to a click that has not
733 been released changes. Emacs processes this event by comparing
734 each of the coordinates within the event with its recollection of
735 those contained within prior ACTION_DOWN and ACTION_MOVE events;
736 the pointer ID of the differing coordinate is then reported
737 within a touch or pointer motion event along with its new
738 position.
739
740 The events described above are all sent for both touch and mouse
741 click events. Determining whether an ACTION_DOWN event is
742 associated with a button event is performed by inspecting the
743 mouse button state associated with that event. If it contains
744 any mouse buttons that were not contained in the button state at
745 the time of the last ACTION_DOWN or ACTION_UP event, the
746 coordinate contained within is assumed to be a mouse click,
747 leading to it and associated motion or ACTION_UP events being
748 reported as mouse button or motion events. Otherwise, those
749 events are reported as touch screen events, with the touch ID set
750 to the pointer ID.
751
752 In addition to the events illustrated above, Android also sends
753 several other types of event upon select types of activity from a
754 mouse device:
755
756 ACTION_HOVER_MOVE is sent with the coordinate of the mouse
757 pointer if it moves above a frame prior to any click taking
758 place. Emacs sends a mouse motion event containing the
759 coordinate.
760
761 ACTION_HOVER_ENTER and ACTION_HOVER_LEAVE are respectively sent
762 when the mouse pointer enters and leaves a frame. Moreover,
763 ACTION_HOVER_LEAVE events are sent immediately before an
764 ACTION_DOWN event associated with a mouse click. These
765 extraneous events are distinct in that their button states always
766 contain an additional button compared to the button state
767 recorded at the time of the last ACTION_UP event.
768
769 On Android 6.0 and later, ACTION_BUTTON_PRESS is sent with the
770 coordinate of the mouse pointer if a mouse click occurs,
771 alongside a ACTION_DOWN event. ACTION_BUTTON_RELEASE is sent
772 with the same information upon a mouse click being released, also
773 accompanying an ACTION_UP event.
774
775 However, both types of button events are implemented in a buggy
776 fashion and cannot be used to report button events. */
777
778 /* Look through the button state to determine what button EVENT was
779 generated from. DOWN is true if EVENT is a button press event,
780 false otherwise. Value is the X number of the button. */
781
782 private int
783 whatButtonWasIt (MotionEvent event, boolean down)
784 {
785 int eventState, notIn;
786
787 /* Obtain the new button state. */
788 eventState = event.getButtonState ();
789
790 /* Compute which button is now set or no longer set. */
791
792 notIn = (down ? eventState & ~lastButtonState
793 : lastButtonState & ~eventState);
794
795 if ((notIn & (MotionEvent.BUTTON_PRIMARY
796 | MotionEvent.BUTTON_SECONDARY
797 | MotionEvent.BUTTON_TERTIARY)) == 0)
798 /* No buttons have been pressed, so this is a touch event. */
799 return 0;
800
801 if ((notIn & MotionEvent.BUTTON_PRIMARY) != 0)
802 return 1;
803
804 if ((notIn & MotionEvent.BUTTON_SECONDARY) != 0)
805 return 3;
806
807 if ((notIn & MotionEvent.BUTTON_TERTIARY) != 0)
808 return 2;
809
810 /* Buttons 4, 5, 6 and 7 are actually scroll wheels under X.
811 Thus, report additional buttons starting at 8. */
812
813 if ((notIn & MotionEvent.BUTTON_BACK) != 0)
814 return 8;
815
816 if ((notIn & MotionEvent.BUTTON_FORWARD) != 0)
817 return 9;
818
819 /* Report stylus events as touch screen events. */
820
821 if ((notIn & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0)
822 return 0;
823
824 if ((notIn & MotionEvent.BUTTON_STYLUS_SECONDARY) != 0)
825 return 0;
826
827 /* Not a real value. */
828 return 11;
829 }
830
831 /* Return the mouse button associated with the specified ACTION_DOWN
832 or ACTION_POINTER_DOWN EVENT.
833
834 Value is 0 if no mouse button was pressed, or the X number of
835 that mouse button. */
836
837 private int
838 buttonForEvent (MotionEvent event)
839 {
840 /* ICS and earlier don't support true mouse button events, so
841 treat all down events as touch screen events. */
842
843 if (Build.VERSION.SDK_INT
844 < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
845 return 0;
846
847 return whatButtonWasIt (event, true);
848 }
849
850 /* Return the coordinate object associated with the specified
851 EVENT, or null if it is not known. */
852
853 private Coordinate
854 figureChange (MotionEvent event)
855 {
856 int i, truncatedX, truncatedY, pointerIndex, pointerID, count;
857 Coordinate coordinate;
858
859 /* Initialize this variable now. */
860 coordinate = null;
861
862 switch (event.getActionMasked ())
863 {
864 case MotionEvent.ACTION_DOWN:
865 /* Primary pointer pressed with index 0. */
866
867 pointerID = event.getPointerId (0);
868 coordinate = new Coordinate ((int) event.getX (0),
869 (int) event.getY (0),
870 buttonForEvent (event),
871 pointerID);
872 pointerMap.put (pointerID, coordinate);
873 break;
874
875 case MotionEvent.ACTION_UP:
876 case MotionEvent.ACTION_CANCEL:
877 /* Primary pointer released with index 0. */
878 pointerID = event.getPointerId (0);
879 coordinate = pointerMap.remove (pointerID);
880 break;
881
882 case MotionEvent.ACTION_POINTER_DOWN:
883 /* New pointer. Find the pointer ID from the index and place
884 it in the map. */
885 pointerIndex = event.getActionIndex ();
886 pointerID = event.getPointerId (pointerIndex);
887 coordinate = new Coordinate ((int) event.getX (0),
888 (int) event.getY (0),
889 buttonForEvent (event),
890 pointerID);
891 pointerMap.put (pointerID, coordinate);
892 break;
893
894 case MotionEvent.ACTION_POINTER_UP:
895 /* Pointer removed. Remove it from the map. */
896 pointerIndex = event.getActionIndex ();
897 pointerID = event.getPointerId (pointerIndex);
898 coordinate = pointerMap.remove (pointerID);
899 break;
900
901 default:
902
903 /* Loop through each pointer in the event. */
904
905 count = event.getPointerCount ();
906 for (i = 0; i < count; ++i)
907 {
908 pointerID = event.getPointerId (i);
909
910 /* Look up that pointer in the map. */
911 coordinate = pointerMap.get (pointerID);
912
913 if (coordinate != null)
914 {
915 /* See if coordinates have changed. */
916 truncatedX = (int) event.getX (i);
917 truncatedY = (int) event.getY (i);
918
919 if (truncatedX != coordinate.x
920 || truncatedY != coordinate.y)
921 {
922 /* The pointer changed. Update the coordinate and
923 break out of the loop. */
924 coordinate.x = truncatedX;
925 coordinate.y = truncatedY;
926
927 break;
928 }
929 }
930 }
931
932 /* Set coordinate to NULL if the loop failed to find any
933 matching pointer. */
934
935 if (i == count)
936 coordinate = null;
937 }
938
939 /* Return the pointer ID. */
940 return coordinate;
941 }
942
943 /* Return the modifier mask associated with the specified motion
944 EVENT. Replace bits corresponding to Left or Right keys with
945 their corresponding general modifier bits. */
946
947 private int
948 motionEventModifiers (MotionEvent event)
949 {
950 int state;
951
952 state = event.getMetaState ();
953
954 /* Normalize the state by setting the generic modifier bit if
955 either a left or right modifier is pressed. */
956
957 if ((state & KeyEvent.META_ALT_LEFT_ON) != 0
958 || (state & KeyEvent.META_ALT_RIGHT_ON) != 0)
959 state |= KeyEvent.META_ALT_MASK;
960
961 if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0
962 || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0)
963 state |= KeyEvent.META_CTRL_MASK;
964
965 return state;
966 }
967
968 /* Process a single ACTION_DOWN, ACTION_POINTER_DOWN, ACTION_UP,
969 ACTION_POINTER_UP, ACTION_CANCEL, or ACTION_MOVE event.
970
971 Ascertain which coordinate changed and send an appropriate mouse
972 or touch screen event. */
973
974 private void
975 motionEvent (MotionEvent event)
976 {
977 Coordinate coordinate;
978 int modifiers;
979 long time;
980
981 /* Find data associated with this event's pointer. Namely, its
982 current location, whether or not a change has taken place, and
983 whether or not it is a button event. */
984
985 coordinate = figureChange (event);
986
987 if (coordinate == null)
988 return;
989
990 time = event.getEventTime ();
991
992 if (coordinate.button != 0)
993 {
994 /* This event is tied to a mouse click, so report mouse motion
995 and button events. */
996
997 modifiers = motionEventModifiers (event);
998
999 switch (event.getAction ())
1000 {
1001 case MotionEvent.ACTION_POINTER_DOWN:
1002 case MotionEvent.ACTION_DOWN:
1003 EmacsNative.sendButtonPress (this.handle, coordinate.x,
1004 coordinate.y, time, modifiers,
1005 coordinate.button);
1006 break;
1007
1008 case MotionEvent.ACTION_POINTER_UP:
1009 case MotionEvent.ACTION_UP:
1010 case MotionEvent.ACTION_CANCEL:
1011 EmacsNative.sendButtonRelease (this.handle, coordinate.x,
1012 coordinate.y, time, modifiers,
1013 coordinate.button);
1014 break;
1015
1016 case MotionEvent.ACTION_MOVE:
1017 EmacsNative.sendMotionNotify (this.handle, coordinate.x,
1018 coordinate.y, time);
1019 break;
1020 }
1021 }
1022 else
1023 {
1024 /* This event is a touch event, and the touch ID is the
1025 pointer ID. */
1026
1027 switch (event.getActionMasked ())
1028 {
1029 case MotionEvent.ACTION_DOWN:
1030 case MotionEvent.ACTION_POINTER_DOWN:
1031 /* Touch down event. */
1032 EmacsNative.sendTouchDown (this.handle, coordinate.x,
1033 coordinate.y, time,
1034 coordinate.id, 0);
1035 break;
1036
1037 case MotionEvent.ACTION_UP:
1038 case MotionEvent.ACTION_POINTER_UP:
1039 /* Touch up event. */
1040 EmacsNative.sendTouchUp (this.handle, coordinate.x,
1041 coordinate.y, time,
1042 coordinate.id, 0);
1043 break;
1044
1045 case MotionEvent.ACTION_CANCEL:
1046 /* Touch sequence cancellation event. */
1047 EmacsNative.sendTouchUp (this.handle, coordinate.x,
1048 coordinate.y, time,
1049 coordinate.id,
1050 1 /* ANDROID_TOUCH_SEQUENCE_CANCELED */);
1051 break;
1052
1053 case MotionEvent.ACTION_MOVE:
1054 /* Pointer motion event. */
1055 EmacsNative.sendTouchMove (this.handle, coordinate.x,
1056 coordinate.y, time,
1057 coordinate.id, 0);
1058 break;
1059 }
1060 }
1061
1062 if (Build.VERSION.SDK_INT
1063 < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
1064 return;
1065
1066 /* Now update the button state. */
1067 lastButtonState = event.getButtonState ();
1068 return;
1069 }
1070
1071 public boolean
1072 onTouchEvent (MotionEvent event)
1073 {
1074 switch (event.getActionMasked ())
1075 {
1076 case MotionEvent.ACTION_DOWN:
1077 case MotionEvent.ACTION_POINTER_DOWN:
1078 case MotionEvent.ACTION_UP:
1079 case MotionEvent.ACTION_POINTER_UP:
1080 case MotionEvent.ACTION_CANCEL:
1081 case MotionEvent.ACTION_MOVE:
1082 motionEvent (event);
1083 return true;
1084 }
1085
1086 return false;
1087 }
1088
1089 public boolean
1090 onGenericMotionEvent (MotionEvent event)
1091 {
1092 switch (event.getAction ())
1093 {
1094 case MotionEvent.ACTION_HOVER_ENTER:
1095 EmacsNative.sendEnterNotify (this.handle, (int) event.getX (),
1096 (int) event.getY (),
1097 event.getEventTime ());
1098 return true;
1099
1100 case MotionEvent.ACTION_HOVER_MOVE:
1101 EmacsNative.sendMotionNotify (this.handle, (int) event.getX (),
1102 (int) event.getY (),
1103 event.getEventTime ());
1104 return true;
1105
1106 case MotionEvent.ACTION_HOVER_EXIT:
1107
1108 /* If the exit event comes from a button press, its button
1109 state will have extra bits compared to the last known
1110 button state. Since the exit event will interfere with
1111 tool bar button presses, ignore such splurious events. */
1112
1113 if ((event.getButtonState () & ~lastButtonState) == 0)
1114 EmacsNative.sendLeaveNotify (this.handle, (int) event.getX (),
1115 (int) event.getY (),
1116 event.getEventTime ());
1117
1118 return true;
1119
1120 case MotionEvent.ACTION_DOWN:
1121 case MotionEvent.ACTION_POINTER_DOWN:
1122 case MotionEvent.ACTION_UP:
1123 case MotionEvent.ACTION_POINTER_UP:
1124 case MotionEvent.ACTION_CANCEL:
1125 case MotionEvent.ACTION_MOVE:
1126 /* MotionEvents may either be sent to onGenericMotionEvent or
1127 onTouchEvent depending on if Android thinks it is a mouse
1128 event or not, but we detect them ourselves. */
1129 motionEvent (event);
1130 return true;
1131
1132 case MotionEvent.ACTION_SCROLL:
1133 /* Send a scroll event with the specified deltas. */
1134 EmacsNative.sendWheel (this.handle, (int) event.getX (),
1135 (int) event.getY (),
1136 event.getEventTime (),
1137 motionEventModifiers (event),
1138 event.getAxisValue (MotionEvent.AXIS_HSCROLL),
1139 event.getAxisValue (MotionEvent.AXIS_VSCROLL));
1140 return true;
1141 }
1142
1143 return false;
1144 }
1145
1146
1147
1148 public synchronized void
1149 reparentTo (final EmacsWindow otherWindow, int x, int y)
1150 {
1151 int width, height;
1152
1153 /* Reparent this window to the other window. */
1154
1155 if (parent != null)
1156 parent.children.remove (this);
1157
1158 if (otherWindow != null)
1159 otherWindow.children.add (this);
1160
1161 parent = otherWindow;
1162
1163 /* Move this window to the new location. */
1164 width = rect.width ();
1165 height = rect.height ();
1166 rect.left = x;
1167 rect.top = y;
1168 rect.right = x + width;
1169 rect.bottom = y + height;
1170
1171 /* Now do the work necessary on the UI thread to reparent the
1172 window. */
1173 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1174 @Override
1175 public void
1176 run ()
1177 {
1178 EmacsWindowAttachmentManager manager;
1179 ViewManager parent;
1180
1181 /* First, detach this window if necessary. */
1182 manager = EmacsWindowAttachmentManager.MANAGER;
1183 manager.detachWindow (EmacsWindow.this);
1184
1185 /* Also unparent this view. */
1186
1187 /* If the window manager is set, use that instead. */
1188 if (windowManager != null)
1189 parent = windowManager;
1190 else
1191 parent = (ViewManager) view.getParent ();
1192 windowManager = null;
1193
1194 if (parent != null)
1195 parent.removeView (view);
1196
1197 /* Next, either add this window as a child of the new
1198 parent's view, or make it available again. */
1199 if (otherWindow != null)
1200 otherWindow.view.addView (view);
1201 else if (EmacsWindow.this.isMapped)
1202 manager.registerWindow (EmacsWindow.this);
1203
1204 /* Request relayout. */
1205 view.requestLayout ();
1206 }
1207 });
1208 }
1209
1210 public void
1211 makeInputFocus (long time)
1212 {
1213 /* TIME is currently ignored. Request the input focus now. */
1214
1215 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1216 @Override
1217 public void
1218 run ()
1219 {
1220 view.requestFocus ();
1221 }
1222 });
1223 }
1224
1225 public synchronized void
1226 raise ()
1227 {
1228 /* This does nothing here. */
1229 if (parent == null)
1230 return;
1231
1232 /* Remove and add this view again. */
1233 parent.children.remove (this);
1234 parent.children.add (this);
1235
1236 /* Request a relayout. */
1237 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1238 @Override
1239 public void
1240 run ()
1241 {
1242 view.raise ();
1243 }
1244 });
1245 }
1246
1247 public synchronized void
1248 lower ()
1249 {
1250 /* This does nothing here. */
1251 if (parent == null)
1252 return;
1253
1254 /* Remove and add this view again. */
1255 parent.children.remove (this);
1256 parent.children.add (this);
1257
1258 /* Request a relayout. */
1259 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1260 @Override
1261 public void
1262 run ()
1263 {
1264 view.lower ();
1265 }
1266 });
1267 }
1268
1269 public synchronized int[]
1270 getWindowGeometry ()
1271 {
1272 int[] array;
1273
1274 array = new int[4];
1275
1276 array[0] = parent != null ? rect.left : xPosition;
1277 array[1] = parent != null ? rect.top : yPosition;
1278 array[2] = rect.width ();
1279 array[3] = rect.height ();
1280
1281 return array;
1282 }
1283
1284 public void
1285 noticeIconified ()
1286 {
1287 EmacsNative.sendIconified (this.handle);
1288 }
1289
1290 public void
1291 noticeDeiconified ()
1292 {
1293 EmacsNative.sendDeiconified (this.handle);
1294 }
1295
1296 public synchronized void
1297 setDontAcceptFocus (final boolean dontAcceptFocus)
1298 {
1299 /* Update the view's focus state. */
1300 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1301 @Override
1302 public void
1303 run ()
1304 {
1305 view.setFocusable (!dontAcceptFocus);
1306 view.setFocusableInTouchMode (!dontAcceptFocus);
1307 }
1308 });
1309 }
1310
1311 public synchronized void
1312 setDontFocusOnMap (final boolean dontFocusOnMap)
1313 {
1314 this.dontFocusOnMap = dontFocusOnMap;
1315 }
1316
1317 public synchronized boolean
1318 getDontFocusOnMap ()
1319 {
1320 return dontFocusOnMap;
1321 }
1322
1323 public int[]
1324 translateCoordinates (int x, int y)
1325 {
1326 int[] array;
1327
1328 /* This is supposed to translate coordinates to the root
1329 window. */
1330 array = new int[2];
1331 EmacsService.SERVICE.getLocationOnScreen (view, array);
1332
1333 /* Now, the coordinates of the view should be in array. Offset X
1334 and Y by them. */
1335 array[0] += x;
1336 array[1] += y;
1337
1338 /* Return the resulting coordinates. */
1339 return array;
1340 }
1341
1342 public void
1343 toggleOnScreenKeyboard (final boolean on)
1344 {
1345 /* Even though InputMethodManager functions are thread safe,
1346 `showOnScreenKeyboard' etc must be called from the UI thread in
1347 order to avoid deadlocks if the calls happen in tandem with a
1348 call to a synchronizing function within
1349 `onCreateInputConnection'. */
1350
1351 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1352 @Override
1353 public void
1354 run ()
1355 {
1356 if (on)
1357 view.showOnScreenKeyboard ();
1358 else
1359 view.hideOnScreenKeyboard ();
1360 }
1361 });
1362 }
1363
1364 public String
1365 lookupString (int eventSerial)
1366 {
1367 String any;
1368
1369 synchronized (eventStrings)
1370 {
1371 any = eventStrings.remove (eventSerial);
1372 }
1373
1374 return any;
1375 }
1376
1377 public void
1378 setFullscreen (final boolean isFullscreen)
1379 {
1380 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1381 @Override
1382 public void
1383 run ()
1384 {
1385 EmacsActivity activity;
1386 Object tem;
1387
1388 fullscreen = isFullscreen;
1389 tem = getAttachedConsumer ();
1390
1391 if (tem != null)
1392 {
1393 activity = (EmacsActivity) tem;
1394 activity.syncFullscreenWith (EmacsWindow.this);
1395 }
1396 }
1397 });
1398 }
1399
1400 public void
1401 defineCursor (final EmacsCursor cursor)
1402 {
1403 /* Don't post this message if pointer icons aren't supported. */
1404
1405 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
1406 view.post (new Runnable () {
1407 @Override
1408 public void
1409 run ()
1410 {
1411 if (cursor != null)
1412 view.setPointerIcon (cursor.icon);
1413 else
1414 view.setPointerIcon (null);
1415 }
1416 });
1417 }
1418
1419 public synchronized void
1420 notifyContentRectPosition (int xPosition, int yPosition)
1421 {
1422 Rect geometry;
1423
1424 /* Ignore these notifications if not a child of the root
1425 window. */
1426 if (parent != null)
1427 return;
1428
1429 /* xPosition and yPosition are the position of this window
1430 relative to the screen. Set them and request a ConfigureNotify
1431 event. */
1432
1433 if (this.xPosition != xPosition
1434 || this.yPosition != yPosition)
1435 {
1436 this.xPosition = xPosition;
1437 this.yPosition = yPosition;
1438
1439 EmacsNative.sendConfigureNotify (this.handle,
1440 System.currentTimeMillis (),
1441 xPosition, yPosition,
1442 rect.width (), rect.height ());
1443 }
1444 }
1445};
diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java
new file mode 100644
index 00000000000..bc96de7fe1a
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java
@@ -0,0 +1,230 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.util.ArrayList;
23import java.util.List;
24
25import android.app.ActivityOptions;
26import android.content.Intent;
27import android.os.Build;
28import android.util.Log;
29
30/* Code to paper over the differences in lifecycles between
31 "activities" and windows. There are four interfaces to an instance
32 of this class:
33
34 registerWindowConsumer (WindowConsumer)
35 registerWindow (EmacsWindow)
36 removeWindowConsumer (WindowConsumer)
37 removeWindow (EmacsWindow)
38
39 A WindowConsumer is expected to allow an EmacsWindow to be attached
40 to it, and be created or destroyed.
41
42 Every time a window is created, registerWindow checks the list of
43 window consumers. If a consumer exists and does not currently have
44 a window of its own attached, it gets the new window. Otherwise,
45 the window attachment manager starts a new consumer.
46
47 Every time a consumer is registered, registerWindowConsumer checks
48 the list of available windows. If a window exists and is not
49 currently attached to a consumer, then the consumer gets it.
50
51 Finally, every time a window is removed, the consumer is
52 destroyed. */
53
54public final class EmacsWindowAttachmentManager
55{
56 private final static String TAG = "EmacsWindowAttachmentManager";
57
58 /* The single window attachment manager ``object''. */
59 public static final EmacsWindowAttachmentManager MANAGER;
60
61 static
62 {
63 MANAGER = new EmacsWindowAttachmentManager ();
64 };
65
66 public interface WindowConsumer
67 {
68 public void attachWindow (EmacsWindow window);
69 public EmacsWindow getAttachedWindow ();
70 public void detachWindow ();
71 public void destroy ();
72 };
73
74 /* List of currently attached window consumers. */
75 public List<WindowConsumer> consumers;
76
77 /* List of currently attached windows. */
78 public List<EmacsWindow> windows;
79
80 public
81 EmacsWindowAttachmentManager ()
82 {
83 consumers = new ArrayList<WindowConsumer> ();
84 windows = new ArrayList<EmacsWindow> ();
85 }
86
87 public void
88 registerWindowConsumer (WindowConsumer consumer)
89 {
90 Log.d (TAG, "registerWindowConsumer " + consumer);
91
92 consumers.add (consumer);
93
94 for (EmacsWindow window : windows)
95 {
96 if (window.getAttachedConsumer () == null)
97 {
98 Log.d (TAG, "registerWindowConsumer: attaching " + window);
99 consumer.attachWindow (window);
100 return;
101 }
102 }
103
104 Log.d (TAG, "registerWindowConsumer: sendWindowAction 0, 0");
105 EmacsNative.sendWindowAction ((short) 0, 0);
106 }
107
108 public synchronized void
109 registerWindow (EmacsWindow window)
110 {
111 Intent intent;
112 ActivityOptions options;
113
114 Log.d (TAG, "registerWindow (maybe): " + window);
115
116 if (windows.contains (window))
117 /* The window is already registered. */
118 return;
119
120 Log.d (TAG, "registerWindow: " + window);
121
122 windows.add (window);
123
124 for (WindowConsumer consumer : consumers)
125 {
126 if (consumer.getAttachedWindow () == null)
127 {
128 Log.d (TAG, "registerWindow: attaching " + consumer);
129 consumer.attachWindow (window);
130 return;
131 }
132 }
133
134 intent = new Intent (EmacsService.SERVICE,
135 EmacsMultitaskActivity.class);
136 intent.addFlags (Intent.FLAG_ACTIVITY_NEW_DOCUMENT
137 | Intent.FLAG_ACTIVITY_NEW_TASK
138 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
139
140 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
141 EmacsService.SERVICE.startActivity (intent);
142 else
143 {
144 /* Specify the desired window size. */
145 options = ActivityOptions.makeBasic ();
146 options.setLaunchBounds (window.getGeometry ());
147 EmacsService.SERVICE.startActivity (intent,
148 options.toBundle ());
149 }
150
151 Log.d (TAG, "registerWindow: startActivity");
152 }
153
154 public void
155 removeWindowConsumer (WindowConsumer consumer, boolean isFinishing)
156 {
157 EmacsWindow window;
158
159 Log.d (TAG, "removeWindowConsumer " + consumer);
160
161 window = consumer.getAttachedWindow ();
162
163 if (window != null)
164 {
165 Log.d (TAG, "removeWindowConsumer: detaching " + window);
166
167 consumer.detachWindow ();
168 window.onActivityDetached (isFinishing);
169 }
170
171 Log.d (TAG, "removeWindowConsumer: removing " + consumer);
172 consumers.remove (consumer);
173 }
174
175 public synchronized void
176 detachWindow (EmacsWindow window)
177 {
178 WindowConsumer consumer;
179
180 Log.d (TAG, "detachWindow " + window);
181
182 if (window.getAttachedConsumer () != null)
183 {
184 consumer = window.getAttachedConsumer ();
185
186 Log.d (TAG, "detachWindow: removing" + consumer);
187
188 consumers.remove (consumer);
189 consumer.destroy ();
190 }
191
192 windows.remove (window);
193 }
194
195 public void
196 noticeIconified (WindowConsumer consumer)
197 {
198 EmacsWindow window;
199
200 Log.d (TAG, "noticeIconified " + consumer);
201
202 /* If a window is attached, send the appropriate iconification
203 events. */
204 window = consumer.getAttachedWindow ();
205
206 if (window != null)
207 window.noticeIconified ();
208 }
209
210 public void
211 noticeDeiconified (WindowConsumer consumer)
212 {
213 EmacsWindow window;
214
215 Log.d (TAG, "noticeDeiconified " + consumer);
216
217 /* If a window is attached, send the appropriate iconification
218 events. */
219 window = consumer.getAttachedWindow ();
220
221 if (window != null)
222 window.noticeDeiconified ();
223 }
224
225 public synchronized List<EmacsWindow>
226 copyWindows ()
227 {
228 return new ArrayList<EmacsWindow> (windows);
229 }
230};