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