diff options
| author | Po Lu | 2023-08-07 08:51:11 +0800 |
|---|---|---|
| committer | Po Lu | 2023-08-07 08:51:11 +0800 |
| commit | c71a520d1da636a722cf87b46534ca3b5aafbc7b (patch) | |
| tree | 95a3099c065800f60602e0a403b551a6d6dba139 /java/org/gnu | |
| parent | 18e7bc87521e3c48b819cfe4a113f532ba905561 (diff) | |
| parent | 9a9f73041d09d2da7ed562c7ffae0d9519562fba (diff) | |
| download | emacs-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/gnu')
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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.lang.IllegalStateException; | ||
| 23 | import java.util.List; | ||
| 24 | import java.util.ArrayList; | ||
| 25 | |||
| 26 | import android.app.Activity; | ||
| 27 | |||
| 28 | import android.content.ContentResolver; | ||
| 29 | import android.content.Context; | ||
| 30 | import android.content.Intent; | ||
| 31 | |||
| 32 | import android.os.Build; | ||
| 33 | import android.os.Bundle; | ||
| 34 | |||
| 35 | import android.util.Log; | ||
| 36 | |||
| 37 | import android.net.Uri; | ||
| 38 | |||
| 39 | import android.view.Menu; | ||
| 40 | import android.view.View; | ||
| 41 | import android.view.ViewTreeObserver; | ||
| 42 | import android.view.Window; | ||
| 43 | import android.view.WindowInsets; | ||
| 44 | import android.view.WindowInsetsController; | ||
| 45 | |||
| 46 | import android.widget.FrameLayout; | ||
| 47 | |||
| 48 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.io.File; | ||
| 23 | import java.io.FileFilter; | ||
| 24 | |||
| 25 | import android.content.Context; | ||
| 26 | |||
| 27 | import android.app.Application; | ||
| 28 | import android.util.Log; | ||
| 29 | |||
| 30 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import 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 | |||
| 27 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.util.List; | ||
| 23 | import java.util.ArrayList; | ||
| 24 | |||
| 25 | import android.content.Context; | ||
| 26 | import android.content.Intent; | ||
| 27 | |||
| 28 | import android.os.Build; | ||
| 29 | |||
| 30 | import android.view.ContextMenu; | ||
| 31 | import android.view.Menu; | ||
| 32 | import android.view.MenuItem; | ||
| 33 | import android.view.View; | ||
| 34 | import android.view.SubMenu; | ||
| 35 | |||
| 36 | import 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 | |||
| 43 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.view.PointerIcon; | ||
| 23 | import android.os.Build; | ||
| 24 | |||
| 25 | /* Cursor wrapper. Note that pointer icons are not supported prior to | ||
| 26 | Android 24. */ | ||
| 27 | |||
| 28 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.util.List; | ||
| 23 | import java.util.ArrayList; | ||
| 24 | |||
| 25 | import android.app.AlertDialog; | ||
| 26 | |||
| 27 | import android.content.Context; | ||
| 28 | import android.content.DialogInterface; | ||
| 29 | |||
| 30 | import android.content.res.Resources.NotFoundException; | ||
| 31 | import android.content.res.Resources.Theme; | ||
| 32 | import android.content.res.TypedArray; | ||
| 33 | |||
| 34 | import android.os.Build; | ||
| 35 | |||
| 36 | import android.provider.Settings; | ||
| 37 | |||
| 38 | import android.util.Log; | ||
| 39 | |||
| 40 | import android.widget.Button; | ||
| 41 | import android.widget.LinearLayout; | ||
| 42 | import android.widget.FrameLayout; | ||
| 43 | |||
| 44 | import android.view.View; | ||
| 45 | import android.view.ViewGroup; | ||
| 46 | import android.view.Window; | ||
| 47 | import 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 | |||
| 53 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | |||
| 23 | |||
| 24 | import android.content.Context; | ||
| 25 | |||
| 26 | import android.view.View; | ||
| 27 | import android.view.View.MeasureSpec; | ||
| 28 | import 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 | |||
| 40 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | /* Structure holding a single ``directory entry'' from a document | ||
| 23 | provider. */ | ||
| 24 | |||
| 25 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.content.Context; | ||
| 23 | |||
| 24 | import android.database.Cursor; | ||
| 25 | import android.database.MatrixCursor; | ||
| 26 | |||
| 27 | import android.os.Build; | ||
| 28 | import android.os.CancellationSignal; | ||
| 29 | import android.os.ParcelFileDescriptor; | ||
| 30 | |||
| 31 | import android.provider.DocumentsContract.Document; | ||
| 32 | import android.provider.DocumentsContract.Root; | ||
| 33 | import static android.provider.DocumentsContract.buildChildDocumentsUri; | ||
| 34 | import android.provider.DocumentsProvider; | ||
| 35 | |||
| 36 | import android.webkit.MimeTypeMap; | ||
| 37 | |||
| 38 | import android.net.Uri; | ||
| 39 | |||
| 40 | import java.io.File; | ||
| 41 | import java.io.FileInputStream; | ||
| 42 | import java.io.FileNotFoundException; | ||
| 43 | import java.io.FileOutputStream; | ||
| 44 | import 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 | |||
| 53 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Canvas; | ||
| 23 | import android.graphics.Paint; | ||
| 24 | import android.graphics.Rect; | ||
| 25 | |||
| 26 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Bitmap; | ||
| 23 | import android.graphics.Canvas; | ||
| 24 | import android.graphics.Paint; | ||
| 25 | import android.graphics.Rect; | ||
| 26 | import android.graphics.RectF; | ||
| 27 | |||
| 28 | import android.util.Log; | ||
| 29 | |||
| 30 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Rect; | ||
| 23 | import android.graphics.Bitmap; | ||
| 24 | import android.graphics.Canvas; | ||
| 25 | |||
| 26 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Canvas; | ||
| 23 | import android.graphics.Paint; | ||
| 24 | import android.graphics.Path; | ||
| 25 | import android.graphics.Point; | ||
| 26 | import android.graphics.Rect; | ||
| 27 | import android.graphics.RectF; | ||
| 28 | |||
| 29 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Bitmap; | ||
| 23 | import android.graphics.Canvas; | ||
| 24 | import android.graphics.Paint; | ||
| 25 | import android.graphics.Rect; | ||
| 26 | |||
| 27 | import android.util.Log; | ||
| 28 | |||
| 29 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.os.Build; | ||
| 23 | |||
| 24 | /* This code is mostly unused. See sfntfont-android.c for the code | ||
| 25 | that is actually used. */ | ||
| 26 | |||
| 27 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Rect; | ||
| 23 | import android.graphics.Paint; | ||
| 24 | |||
| 25 | import android.graphics.PorterDuff.Mode; | ||
| 26 | import android.graphics.PorterDuffXfermode; | ||
| 27 | import android.graphics.Xfermode; | ||
| 28 | |||
| 29 | /* X like graphics context structures. Keep the enums in synch with | ||
| 30 | androidgui.h! */ | ||
| 31 | |||
| 32 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import 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 | |||
| 31 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package 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 | |||
| 27 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.os.Build; | ||
| 23 | import android.os.Bundle; | ||
| 24 | import android.os.Handler; | ||
| 25 | |||
| 26 | import android.view.KeyEvent; | ||
| 27 | |||
| 28 | import android.view.inputmethod.CompletionInfo; | ||
| 29 | import android.view.inputmethod.CorrectionInfo; | ||
| 30 | import android.view.inputmethod.ExtractedText; | ||
| 31 | import android.view.inputmethod.ExtractedTextRequest; | ||
| 32 | import android.view.inputmethod.InputConnection; | ||
| 33 | import android.view.inputmethod.InputContentInfo; | ||
| 34 | import android.view.inputmethod.SurroundingText; | ||
| 35 | import android.view.inputmethod.TextAttribute; | ||
| 36 | import android.view.inputmethod.TextSnapshot; | ||
| 37 | |||
| 38 | import 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 | |||
| 43 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package 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 | |||
| 27 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package 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 | |||
| 26 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.content.res.AssetManager; | ||
| 23 | |||
| 24 | import android.graphics.Bitmap; | ||
| 25 | |||
| 26 | import android.view.inputmethod.ExtractedText; | ||
| 27 | import android.view.inputmethod.ExtractedTextRequest; | ||
| 28 | import android.view.inputmethod.SurroundingText; | ||
| 29 | import android.view.inputmethod.TextSnapshot; | ||
| 30 | |||
| 31 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.os.Looper; | ||
| 23 | import android.os.Build; | ||
| 24 | |||
| 25 | import android.content.Context; | ||
| 26 | import android.content.res.AssetManager; | ||
| 27 | |||
| 28 | import java.lang.reflect.Constructor; | ||
| 29 | import 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") | ||
| 47 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package 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 | |||
| 46 | import android.app.AlertDialog; | ||
| 47 | import android.app.Activity; | ||
| 48 | |||
| 49 | import android.content.ContentResolver; | ||
| 50 | import android.content.DialogInterface; | ||
| 51 | import android.content.Intent; | ||
| 52 | |||
| 53 | import android.net.Uri; | ||
| 54 | |||
| 55 | import android.os.Build; | ||
| 56 | import android.os.Bundle; | ||
| 57 | import android.os.ParcelFileDescriptor; | ||
| 58 | |||
| 59 | import android.util.Log; | ||
| 60 | |||
| 61 | import java.io.File; | ||
| 62 | import java.io.FileInputStream; | ||
| 63 | import java.io.FileNotFoundException; | ||
| 64 | import java.io.FileOutputStream; | ||
| 65 | import java.io.FileReader; | ||
| 66 | import java.io.IOException; | ||
| 67 | import java.io.InputStream; | ||
| 68 | import java.io.UnsupportedEncodingException; | ||
| 69 | |||
| 70 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.lang.IllegalArgumentException; | ||
| 23 | |||
| 24 | import android.graphics.Bitmap; | ||
| 25 | import android.graphics.Canvas; | ||
| 26 | import android.graphics.Rect; | ||
| 27 | |||
| 28 | import android.os.Build; | ||
| 29 | |||
| 30 | /* Drawable backed by bitmap. */ | ||
| 31 | |||
| 32 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.io.File; | ||
| 23 | |||
| 24 | import android.app.Activity; | ||
| 25 | |||
| 26 | import android.content.Intent; | ||
| 27 | |||
| 28 | import android.os.Bundle; | ||
| 29 | import android.os.Build; | ||
| 30 | |||
| 31 | import android.widget.Toast; | ||
| 32 | |||
| 33 | import 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") | ||
| 45 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.util.Collection; | ||
| 23 | import java.util.HashMap; | ||
| 24 | import java.util.Iterator; | ||
| 25 | |||
| 26 | import java.io.FileNotFoundException; | ||
| 27 | import java.io.IOException; | ||
| 28 | |||
| 29 | import android.content.ContentResolver; | ||
| 30 | import android.database.Cursor; | ||
| 31 | import android.net.Uri; | ||
| 32 | |||
| 33 | import android.os.Build; | ||
| 34 | import android.os.CancellationSignal; | ||
| 35 | import android.os.Handler; | ||
| 36 | import android.os.HandlerThread; | ||
| 37 | import android.os.OperationCanceledException; | ||
| 38 | import android.os.ParcelFileDescriptor; | ||
| 39 | import android.os.SystemClock; | ||
| 40 | |||
| 41 | import android.util.Log; | ||
| 42 | |||
| 43 | import android.provider.DocumentsContract; | ||
| 44 | import 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 | |||
| 84 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.content.ClipboardManager; | ||
| 23 | import android.content.Context; | ||
| 24 | import android.content.ContentResolver; | ||
| 25 | import android.content.ClipData; | ||
| 26 | import android.content.ClipDescription; | ||
| 27 | |||
| 28 | import android.content.res.AssetFileDescriptor; | ||
| 29 | |||
| 30 | import android.net.Uri; | ||
| 31 | |||
| 32 | import android.util.Log; | ||
| 33 | |||
| 34 | import android.os.Build; | ||
| 35 | |||
| 36 | import java.io.FileNotFoundException; | ||
| 37 | import java.io.IOException; | ||
| 38 | import java.io.UnsupportedEncodingException; | ||
| 39 | |||
| 40 | /* This class implements EmacsClipboard for Android 3.0 and later | ||
| 41 | systems. */ | ||
| 42 | |||
| 43 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Paint; | ||
| 23 | import android.graphics.Rect; | ||
| 24 | |||
| 25 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.io.File; | ||
| 23 | |||
| 24 | import java.util.LinkedList; | ||
| 25 | import java.util.List; | ||
| 26 | |||
| 27 | import android.graphics.Paint; | ||
| 28 | import android.graphics.Rect; | ||
| 29 | import android.graphics.Typeface; | ||
| 30 | import android.graphics.Canvas; | ||
| 31 | |||
| 32 | import android.util.Log; | ||
| 33 | |||
| 34 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | /* Importing the entire package instead of just the legacy | ||
| 23 | ClipboardManager class avoids the deprecation warning. */ | ||
| 24 | |||
| 25 | import android.text.*; | ||
| 26 | |||
| 27 | import android.content.Context; | ||
| 28 | import android.util.Log; | ||
| 29 | |||
| 30 | import java.io.UnsupportedEncodingException; | ||
| 31 | |||
| 32 | /* This class implements EmacsClipboard for Android 2.2 and other | ||
| 33 | similarly old systems. */ | ||
| 34 | |||
| 35 | @SuppressWarnings ("deprecation") | ||
| 36 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.io.FileNotFoundException; | ||
| 23 | import java.io.IOException; | ||
| 24 | import java.io.UnsupportedEncodingException; | ||
| 25 | |||
| 26 | import java.util.ArrayList; | ||
| 27 | import java.util.HashSet; | ||
| 28 | import java.util.List; | ||
| 29 | |||
| 30 | import java.util.concurrent.atomic.AtomicInteger; | ||
| 31 | |||
| 32 | import android.database.Cursor; | ||
| 33 | |||
| 34 | import android.graphics.Matrix; | ||
| 35 | import android.graphics.Point; | ||
| 36 | |||
| 37 | import android.webkit.MimeTypeMap; | ||
| 38 | |||
| 39 | import android.view.InputDevice; | ||
| 40 | import android.view.KeyEvent; | ||
| 41 | import android.view.inputmethod.CursorAnchorInfo; | ||
| 42 | import android.view.inputmethod.ExtractedText; | ||
| 43 | |||
| 44 | import android.app.Notification; | ||
| 45 | import android.app.NotificationManager; | ||
| 46 | import android.app.NotificationChannel; | ||
| 47 | import android.app.Service; | ||
| 48 | |||
| 49 | import android.content.ClipboardManager; | ||
| 50 | import android.content.Context; | ||
| 51 | import android.content.ContentResolver; | ||
| 52 | import android.content.Intent; | ||
| 53 | import android.content.IntentFilter; | ||
| 54 | import android.content.UriPermission; | ||
| 55 | |||
| 56 | import android.content.pm.ApplicationInfo; | ||
| 57 | import android.content.pm.PackageManager.ApplicationInfoFlags; | ||
| 58 | import android.content.pm.PackageManager; | ||
| 59 | |||
| 60 | import android.content.res.AssetManager; | ||
| 61 | |||
| 62 | import android.hardware.input.InputManager; | ||
| 63 | |||
| 64 | import android.net.Uri; | ||
| 65 | |||
| 66 | import android.os.BatteryManager; | ||
| 67 | import android.os.Build; | ||
| 68 | import android.os.Looper; | ||
| 69 | import android.os.IBinder; | ||
| 70 | import android.os.Handler; | ||
| 71 | import android.os.ParcelFileDescriptor; | ||
| 72 | import android.os.Vibrator; | ||
| 73 | import android.os.VibratorManager; | ||
| 74 | import android.os.VibrationEffect; | ||
| 75 | |||
| 76 | import android.provider.DocumentsContract; | ||
| 77 | import android.provider.DocumentsContract.Document; | ||
| 78 | |||
| 79 | import android.util.Log; | ||
| 80 | import android.util.DisplayMetrics; | ||
| 81 | |||
| 82 | import android.widget.Toast; | ||
| 83 | |||
| 84 | /* EmacsService is the service that starts the thread running Emacs | ||
| 85 | and handles requests by that Emacs instance. */ | ||
| 86 | |||
| 87 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.view.View; | ||
| 23 | |||
| 24 | import android.os.Build; | ||
| 25 | |||
| 26 | import android.graphics.Bitmap; | ||
| 27 | import android.graphics.Canvas; | ||
| 28 | import android.graphics.Rect; | ||
| 29 | import android.graphics.Paint; | ||
| 30 | |||
| 31 | import 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 | |||
| 38 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.lang.Thread; | ||
| 23 | import java.util.Arrays; | ||
| 24 | |||
| 25 | import android.util.Log; | ||
| 26 | |||
| 27 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.content.Context; | ||
| 23 | |||
| 24 | import android.text.InputType; | ||
| 25 | |||
| 26 | import android.view.ContextMenu; | ||
| 27 | import android.view.View; | ||
| 28 | import android.view.KeyEvent; | ||
| 29 | import android.view.MotionEvent; | ||
| 30 | import android.view.ViewGroup; | ||
| 31 | import android.view.ViewTreeObserver; | ||
| 32 | |||
| 33 | import android.view.inputmethod.EditorInfo; | ||
| 34 | import android.view.inputmethod.InputConnection; | ||
| 35 | import android.view.inputmethod.InputMethodManager; | ||
| 36 | |||
| 37 | import android.graphics.Bitmap; | ||
| 38 | import android.graphics.Canvas; | ||
| 39 | import android.graphics.Rect; | ||
| 40 | import android.graphics.Region; | ||
| 41 | import android.graphics.Paint; | ||
| 42 | |||
| 43 | import android.os.Build; | ||
| 44 | import 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 | |||
| 54 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.lang.IllegalStateException; | ||
| 23 | import java.util.ArrayList; | ||
| 24 | import java.util.List; | ||
| 25 | import java.util.HashMap; | ||
| 26 | import java.util.LinkedHashMap; | ||
| 27 | import java.util.Map; | ||
| 28 | |||
| 29 | import android.content.Context; | ||
| 30 | |||
| 31 | import android.graphics.Rect; | ||
| 32 | import android.graphics.Canvas; | ||
| 33 | import android.graphics.Bitmap; | ||
| 34 | import android.graphics.PixelFormat; | ||
| 35 | |||
| 36 | import android.view.View; | ||
| 37 | import android.view.ViewManager; | ||
| 38 | import android.view.Gravity; | ||
| 39 | import android.view.KeyEvent; | ||
| 40 | import android.view.MotionEvent; | ||
| 41 | import android.view.InputDevice; | ||
| 42 | import android.view.WindowManager; | ||
| 43 | |||
| 44 | import android.util.Log; | ||
| 45 | |||
| 46 | import 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 | |||
| 59 | public 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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.util.ArrayList; | ||
| 23 | import java.util.List; | ||
| 24 | |||
| 25 | import android.app.ActivityOptions; | ||
| 26 | import android.content.Intent; | ||
| 27 | import android.os.Build; | ||
| 28 | import 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 | |||
| 54 | public 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 | }; | ||