From 1b8258a1f2b6a080a4f0e819aa4a86c1ec2da89f Mon Sep 17 00:00:00 2001 From: Po Lu Date: Tue, 17 Jan 2023 22:10:43 +0800 Subject: Update Android port * doc/emacs/android.texi (Android Fonts): Document that TTC format fonts are now supported. * doc/emacs/emacs.texi (Top): Fix menus. * doc/lispref/commands.texi (Touchscreen Events) (Key Sequence Input): Document changes to touchscreen events. * etc/DEBUG: Describe how to debug 64 bit binaries on Android. * java/org/gnu/emacs/EmacsCopyArea.java (perform): Explicitly recycle copy bitmap. * java/org/gnu/emacs/EmacsDialog.java (EmacsDialog): New class. * java/org/gnu/emacs/EmacsDrawRectangle.java (perform): Use 5 point PolyLine like X, because Android behaves like Postscript on some devices and X elsewhere. * java/org/gnu/emacs/EmacsFillRectangle.java (perform): Explicitly recycle copy bitmap. * java/org/gnu/emacs/EmacsPixmap.java (destroyHandle): Explicitly recycle bitmap and GC if it is big. * java/org/gnu/emacs/EmacsView.java (EmacsView): Make `bitmapDirty' a boolean. (handleDirtyBitmap): Reimplement in terms of that boolean. Explicitly recycle old bitmap and GC. (onLayout): Fix lock up. (onDetachedFromWindow): Recycle bitmap and GC. * java/org/gnu/emacs/EmacsWindow.java (requestViewLayout): Update call to explicitlyDirtyBitmap. * src/android.c (android_run_select_thread, android_select): Really fix android_select. (android_build_jstring): New function. * src/android.h: Update prototypes. * src/androidmenu.c (android_process_events_for_menu): Totally unblock input before process_pending_signals. (android_menu_show): Remove redundant unblock_input and debugging code. (struct android_emacs_dialog, android_init_emacs_dialog) (android_dialog_show, android_popup_dialog, init_androidmenu): Implement popup dialogs on Android. * src/androidterm.c (android_update_tools) (handle_one_android_event, android_frame_up_to_date): Allow tapping tool bar items. (android_create_terminal): Add dialog hook. (android_wait_for_event): Adjust call to android_select. * src/androidterm.h (struct android_touch_point): New field `tool_bar_p'. * src/keyboard.c (read_key_sequence, head_table) (syms_of_keyboard): Prefix touchscreen events with posn. * src/keyboard.h (EVENT_HEAD): Handle touchscreen events. * src/process.c (wait_reading_process_output): Adjust call to android_select. * src/sfnt.c (sfnt_read_table_directory): If the first long turns out to be ttcf, return -1. (sfnt_read_ttc_header): New function. (main): Test TTC support. * src/sfnt.h (struct sfnt_ttc_header): New structure. (enum sfnt_ttc_tag): New enum. * src/sfntfont-android.c (struct sfntfont_android_scanline_buffer): New structure. (GET_SCANLINE_BUFFER): New macro. Try to avoid so much malloc upon accessing the scanline buffer. (sfntfont_android_put_glyphs): Do not use SAFE_ALLOCA to allocate the scaline buffer. (Fandroid_enumerate_fonts): Enumerate ttc fonts too. * src/sfntfont.c (struct sfnt_font_desc): New field `offset'. (sfnt_enum_font_1): Split out enumeration code from sfnt_enum_font. (sfnt_enum_font): Read TTC tables and enumerate each font therein. (sfntfont_open): Seek to the offset specified. * xcompile/Makefile.in (maintainer-clean): Fix depends here. --- java/org/gnu/emacs/EmacsCopyArea.java | 4 + java/org/gnu/emacs/EmacsDialog.java | 333 +++++++++++++++++++++++++++++ java/org/gnu/emacs/EmacsDrawRectangle.java | 32 ++- java/org/gnu/emacs/EmacsFillRectangle.java | 3 + java/org/gnu/emacs/EmacsPixmap.java | 23 ++ java/org/gnu/emacs/EmacsView.java | 84 ++++++-- java/org/gnu/emacs/EmacsWindow.java | 2 +- 7 files changed, 452 insertions(+), 29 deletions(-) create mode 100644 java/org/gnu/emacs/EmacsDialog.java (limited to 'java') diff --git a/java/org/gnu/emacs/EmacsCopyArea.java b/java/org/gnu/emacs/EmacsCopyArea.java index 00e817bb97d..5d72a7860c8 100644 --- a/java/org/gnu/emacs/EmacsCopyArea.java +++ b/java/org/gnu/emacs/EmacsCopyArea.java @@ -116,6 +116,7 @@ public class EmacsCopyArea src_x, src_y, width, height); canvas.drawBitmap (bitmap, null, rect, paint); + bitmap.recycle (); } else { @@ -183,6 +184,9 @@ public class EmacsCopyArea paint.setXfermode (overAlu); canvas.drawBitmap (maskBitmap, null, maskRect, paint); gc.resetXfermode (); + + /* Recycle this unused bitmap. */ + maskBitmap.recycle (); } canvas.restore (); diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java new file mode 100644 index 00000000000..5bc8efa5978 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDialog.java @@ -0,0 +1,333 @@ +/* Communication module for Android terminals. -*- c-file-style: "GNU" -*- + +Copyright (C) 2023 Free Software Foundation, Inc. + +This file is part of GNU Emacs. + +GNU Emacs is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or (at +your option) any later version. + +GNU Emacs is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with GNU Emacs. If not, see . */ + +package org.gnu.emacs; + +import java.util.List; +import java.util.ArrayList; + +import android.app.AlertDialog; +import android.content.DialogInterface; +import android.content.Context; +import android.util.Log; + +import android.widget.Button; +import android.widget.LinearLayout; +import android.widget.FrameLayout; + +import android.view.View; +import android.view.ViewGroup; + +/* Toolkit dialog implementation. This object is built from JNI and + describes a single alert dialog. Then, `inflate' turns it into + AlertDialog. */ + +public class EmacsDialog implements DialogInterface.OnDismissListener +{ + private static final String TAG = "EmacsDialog"; + + /* List of buttons in this dialog. */ + private List buttons; + + /* Dialog title. */ + private String title; + + /* Dialog text. */ + private String text; + + /* Whether or not a selection has already been made. */ + private boolean wasButtonClicked; + + /* Dialog to dismiss after click. */ + private AlertDialog dismissDialog; + + private class EmacsButton implements View.OnClickListener, + DialogInterface.OnClickListener + { + /* Name of this button. */ + public String name; + + /* ID of this button. */ + public int id; + + /* Whether or not the button is enabled. */ + public boolean enabled; + + @Override + public void + onClick (View view) + { + Log.d (TAG, "onClicked " + this); + + wasButtonClicked = true; + EmacsNative.sendContextMenu ((short) 0, id); + dismissDialog.dismiss (); + } + + @Override + public void + onClick (DialogInterface dialog, int which) + { + Log.d (TAG, "onClicked " + this); + + wasButtonClicked = true; + EmacsNative.sendContextMenu ((short) 0, id); + } + }; + + /* Create a popup dialog with the title TITLE and the text TEXT. + TITLE may be NULL. */ + + public static EmacsDialog + createDialog (String title, String text) + { + EmacsDialog dialog; + + dialog = new EmacsDialog (); + dialog.buttons = new ArrayList (); + dialog.title = title; + dialog.text = text; + + return dialog; + } + + /* Add a button named NAME, with the identifier ID. If DISABLE, + disable the button. */ + + public void + addButton (String name, int id, boolean disable) + { + EmacsButton button; + + button = new EmacsButton (); + button.name = name; + button.id = id; + button.enabled = !disable; + buttons.add (button); + } + + /* Turn this dialog into an AlertDialog for the specified + CONTEXT. + + Upon a button being selected, the dialog will send an + ANDROID_CONTEXT_MENU event with the id of that button. + + Upon the dialog being dismissed, an ANDROID_CONTEXT_MENU event + will be sent with an id of 0. */ + + public AlertDialog + toAlertDialog (Context context) + { + AlertDialog dialog; + int size; + EmacsButton button; + LinearLayout layout; + Button buttonView; + ViewGroup.LayoutParams layoutParams; + + size = buttons.size (); + + if (size <= 3) + { + dialog = new AlertDialog.Builder (context).create (); + dialog.setMessage (text); + dialog.setCancelable (true); + dialog.setOnDismissListener (this); + + if (title != null) + dialog.setTitle (title); + + /* There are less than 4 buttons. Add the buttons the way + Android intends them to be added. */ + + if (size >= 1) + { + button = buttons.get (0); + dialog.setButton (DialogInterface.BUTTON_POSITIVE, + button.name, button); + } + + if (size >= 2) + { + button = buttons.get (1); + dialog.setButton (DialogInterface.BUTTON_NEUTRAL, + button.name, button); + buttonView + = dialog.getButton (DialogInterface.BUTTON_NEUTRAL); + buttonView.setEnabled (button.enabled); + } + + if (size >= 3) + { + button = buttons.get (2); + dialog.setButton (DialogInterface.BUTTON_NEGATIVE, + button.name, button); + } + } + else + { + /* There are more than 4 buttons. Add them all to a + LinearLayout. */ + layout = new LinearLayout (context); + layoutParams + = new LinearLayout.LayoutParams (ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + + for (EmacsButton emacsButton : buttons) + { + buttonView = new Button (context); + buttonView.setText (emacsButton.name); + buttonView.setOnClickListener (emacsButton); + buttonView.setLayoutParams (layoutParams); + buttonView.setEnabled (emacsButton.enabled); + layout.addView (buttonView); + } + + layoutParams + = new FrameLayout.LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, + ViewGroup.LayoutParams.WRAP_CONTENT); + layout.setLayoutParams (layoutParams); + + /* Add that layout to the dialog's custom view. + + android.R.id.custom is documented to work. But looking it + up returns NULL, so setView must be used instead. */ + + dialog = new AlertDialog.Builder (context).setView (layout).create (); + dialog.setMessage (text); + dialog.setCancelable (true); + dialog.setOnDismissListener (this); + + if (title != null) + dialog.setTitle (title); + } + + return dialog; + } + + /* Internal helper for display run on the main thread. */ + + private boolean + display1 () + { + EmacsActivity activity; + int size; + Button buttonView; + EmacsButton button; + AlertDialog dialog; + + if (EmacsActivity.focusedActivities.isEmpty ()) + return false; + + activity = EmacsActivity.focusedActivities.get (0); + dialog = dismissDialog = toAlertDialog (activity); + dismissDialog.show (); + + /* If there are less than four buttons, then they must be + individually enabled or disabled after the dialog is + displayed. */ + size = buttons.size (); + + if (size <= 3) + { + if (size >= 1) + { + button = buttons.get (0); + buttonView + = dialog.getButton (DialogInterface.BUTTON_POSITIVE); + buttonView.setEnabled (button.enabled); + } + + if (size >= 2) + { + button = buttons.get (1); + dialog.setButton (DialogInterface.BUTTON_NEUTRAL, + button.name, button); + buttonView + = dialog.getButton (DialogInterface.BUTTON_NEUTRAL); + buttonView.setEnabled (button.enabled); + } + + if (size >= 3) + { + button = buttons.get (2); + buttonView + = dialog.getButton (DialogInterface.BUTTON_NEGATIVE); + buttonView.setEnabled (button.enabled); + } + } + + return true; + } + + /* Display this dialog for a suitable activity. + Value is false if the dialog could not be displayed, + and true otherwise. */ + + public boolean + display () + { + Runnable runnable; + final Holder rc; + + rc = new Holder (); + runnable = new Runnable () { + @Override + public void + run () + { + synchronized (this) + { + rc.thing = display1 (); + notify (); + } + } + }; + + synchronized (runnable) + { + EmacsService.SERVICE.runOnUiThread (runnable); + + try + { + runnable.wait (); + } + catch (InterruptedException e) + { + EmacsNative.emacsAbort (); + } + } + + return rc.thing; + } + + + + @Override + public void + onDismiss (DialogInterface dialog) + { + Log.d (TAG, "onDismiss: " + this); + + if (wasButtonClicked) + return; + + EmacsNative.sendContextMenu ((short) 0, 0); + } +}; diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java index b42e9556e8c..c29d413f66e 100644 --- a/java/org/gnu/emacs/EmacsDrawRectangle.java +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java @@ -36,10 +36,10 @@ public class EmacsDrawRectangle Paint maskPaint, paint; Canvas maskCanvas; Bitmap maskBitmap; - Rect rect; Rect maskRect, dstRect; Canvas canvas; Bitmap clipBitmap; + Rect clipRect; /* TODO implement stippling. */ if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) @@ -58,13 +58,29 @@ public class EmacsDrawRectangle canvas.clipRect (gc.real_clip_rects[i]); } - paint = gc.gcPaint; - rect = new Rect (x, y, x + width, y + height); + /* Clip to the clipRect because some versions of Android draw an + overly wide line. */ + clipRect = new Rect (x, y, x + width + 1, + y + height + 1); + canvas.clipRect (clipRect); - paint.setStyle (Paint.Style.STROKE); + paint = gc.gcPaint; if (gc.clip_mask == null) - canvas.drawRect (rect, paint); + { + /* canvas.drawRect just doesn't work on Android, producing + different results on various devices. Do a 5 point + PolyLine instead. */ + canvas.drawLine ((float) x, (float) y, (float) x + width, + (float) y, paint); + canvas.drawLine ((float) x + width, (float) y, + (float) x + width, (float) y + height, + paint); + canvas.drawLine ((float) x + width, (float) y + height, + (float) x, (float) y + height, paint); + canvas.drawLine ((float) x, (float) y + height, + (float) x, (float) y, paint); + } else { /* Drawing with a clip mask involves calculating the @@ -116,10 +132,12 @@ public class EmacsDrawRectangle /* Finally, draw the mask bitmap to the destination. */ paint.setXfermode (null); canvas.drawBitmap (maskBitmap, null, maskRect, paint); + + /* Recycle this unused bitmap. */ + maskBitmap.recycle (); } canvas.restore (); - drawable.damageRect (new Rect (x, y, x + width + 1, - y + height + 1)); + drawable.damageRect (clipRect); } } diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java index b733b417d6b..7cc55d3db96 100644 --- a/java/org/gnu/emacs/EmacsFillRectangle.java +++ b/java/org/gnu/emacs/EmacsFillRectangle.java @@ -115,6 +115,9 @@ public class EmacsFillRectangle /* Finally, draw the mask bitmap to the destination. */ paint.setXfermode (null); canvas.drawBitmap (maskBitmap, null, maskRect, paint); + + /* Recycle this unused bitmap. */ + maskBitmap.recycle (); } canvas.restore (); diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java index 15452f007c4..85931c2abd4 100644 --- a/java/org/gnu/emacs/EmacsPixmap.java +++ b/java/org/gnu/emacs/EmacsPixmap.java @@ -25,6 +25,8 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Rect; +import android.os.Build; + /* Drawable backed by bitmap. */ public class EmacsPixmap extends EmacsHandleObject @@ -123,4 +125,25 @@ public class EmacsPixmap extends EmacsHandleObject { return bitmap; } + + @Override + public void + destroyHandle () + { + boolean needCollect; + + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) + needCollect = (bitmap.getByteCount () + >= 1024 * 512); + else + needCollect = (bitmap.getAllocationByteCount () + >= 1024 * 512); + + bitmap.recycle (); + bitmap = null; + + /* Collect the bitmap storage if the bitmap is big. */ + if (needCollect) + Runtime.getRuntime ().gc (); + } }; diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java index 445d8ffa023..6137fd74a7f 100644 --- a/java/org/gnu/emacs/EmacsView.java +++ b/java/org/gnu/emacs/EmacsView.java @@ -70,9 +70,9 @@ public class EmacsView extends ViewGroup event regardless of what changed. */ public boolean mustReportLayout; - /* If non-null, whether or not bitmaps must be recreated upon the - next call to getBitmap. */ - private Rect bitmapDirty; + /* Whether or not bitmaps must be recreated upon the next call to + getBitmap. */ + private boolean bitmapDirty; /* Whether or not a popup is active. */ private boolean popupActive; @@ -80,6 +80,9 @@ public class EmacsView extends ViewGroup /* The current context menu. */ private EmacsContextMenu contextMenu; + /* The last measured width and height. */ + private int measuredWidth, measuredHeight; + public EmacsView (EmacsWindow window) { @@ -116,13 +119,27 @@ public class EmacsView extends ViewGroup { Bitmap oldBitmap; + if (measuredWidth == 0 || measuredHeight == 0) + return; + + /* If bitmap is the same width and height as the measured width + and height, there is no need to do anything. Avoid allocating + the extra bitmap. */ + if (bitmap != null + && (bitmap.getWidth () == measuredWidth + && bitmap.getHeight () == measuredHeight)) + { + bitmapDirty = false; + return; + } + /* Save the old bitmap. */ oldBitmap = bitmap; /* Recreate the front and back buffer bitmaps. */ bitmap - = Bitmap.createBitmap (bitmapDirty.width (), - bitmapDirty.height (), + = Bitmap.createBitmap (measuredWidth, + measuredHeight, Bitmap.Config.ARGB_8888); bitmap.eraseColor (0xffffffff); @@ -133,23 +150,27 @@ public class EmacsView extends ViewGroup if (oldBitmap != null) canvas.drawBitmap (oldBitmap, 0f, 0f, new Paint ()); - bitmapDirty = null; + bitmapDirty = false; + + /* Explicitly free the old bitmap's memory. */ + if (oldBitmap != null) + oldBitmap.recycle (); + + /* Some Android versions still don't free the bitmap until the + next GC. */ + Runtime.getRuntime ().gc (); } public synchronized void - explicitlyDirtyBitmap (Rect rect) + explicitlyDirtyBitmap () { - if (bitmapDirty == null - && (bitmap == null - || rect.width () != bitmap.getWidth () - || rect.height () != bitmap.getHeight ())) - bitmapDirty = rect; + bitmapDirty = true; } public synchronized Bitmap getBitmap () { - if (bitmapDirty != null) + if (bitmapDirty || bitmap == null) handleDirtyBitmap (); return bitmap; @@ -158,7 +179,7 @@ public class EmacsView extends ViewGroup public synchronized Canvas getCanvas () { - if (bitmapDirty != null) + if (bitmapDirty || bitmap == null) handleDirtyBitmap (); return canvas; @@ -196,8 +217,12 @@ public class EmacsView extends ViewGroup super.setMeasuredDimension (width, height); } + /* Note that the monitor lock for the window must never be held from + within the lock for the view, because the window also locks the + other way around. */ + @Override - protected synchronized void + protected void onLayout (boolean changed, int left, int top, int right, int bottom) { @@ -213,12 +238,13 @@ public class EmacsView extends ViewGroup window.viewLayout (left, top, right, bottom); } - if (changed - /* Check that a change has really happened. */ - && (bitmapDirty == null - || bitmapDirty.width () != right - left - || bitmapDirty.height () != bottom - top)) - bitmapDirty = new Rect (left, top, right, bottom); + measuredWidth = right - left; + measuredHeight = bottom - top; + + /* Dirty the back buffer. */ + + if (changed) + explicitlyDirtyBitmap (); for (i = 0; i < count; ++i) { @@ -472,4 +498,20 @@ public class EmacsView extends ViewGroup contextMenu = null; popupActive = false; } + + @Override + public synchronized void + onDetachedFromWindow () + { + synchronized (this) + { + /* Recycle the bitmap and call GC. */ + bitmap.recycle (); + bitmap = null; + canvas = null; + + /* Collect the bitmap storage; it could be large. */ + Runtime.getRuntime ().gc (); + } + } }; diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java index 7181bc89fea..f5b50f11f14 100644 --- a/java/org/gnu/emacs/EmacsWindow.java +++ b/java/org/gnu/emacs/EmacsWindow.java @@ -260,7 +260,7 @@ public class EmacsWindow extends EmacsHandleObject { /* This is necessary because otherwise subsequent drawing on the Emacs thread may be lost. */ - view.explicitlyDirtyBitmap (rect); + view.explicitlyDirtyBitmap (); EmacsService.SERVICE.runOnUiThread (new Runnable () { @Override -- cgit v1.2.1