aboutsummaryrefslogtreecommitdiffstats
path: root/java
diff options
context:
space:
mode:
Diffstat (limited to 'java')
-rw-r--r--java/AndroidManifest.xml.in225
-rw-r--r--java/INSTALL977
-rw-r--r--java/Makefile.in337
-rw-r--r--java/README1049
-rwxr-xr-xjava/debug.sh368
-rw-r--r--java/emacs.keystorebin0 -> 2776 bytes
-rw-r--r--java/org/gnu/emacs/EmacsActivity.java481
-rw-r--r--java/org/gnu/emacs/EmacsApplication.java92
-rw-r--r--java/org/gnu/emacs/EmacsClipboard.java47
-rw-r--r--java/org/gnu/emacs/EmacsContextMenu.java393
-rw-r--r--java/org/gnu/emacs/EmacsCursor.java47
-rw-r--r--java/org/gnu/emacs/EmacsDialog.java427
-rw-r--r--java/org/gnu/emacs/EmacsDialogButtonLayout.java152
-rw-r--r--java/org/gnu/emacs/EmacsDirectoryEntry.java33
-rw-r--r--java/org/gnu/emacs/EmacsDocumentsProvider.java578
-rw-r--r--java/org/gnu/emacs/EmacsDrawLine.java79
-rw-r--r--java/org/gnu/emacs/EmacsDrawPoint.java34
-rw-r--r--java/org/gnu/emacs/EmacsDrawRectangle.java120
-rw-r--r--java/org/gnu/emacs/EmacsDrawable.java32
-rw-r--r--java/org/gnu/emacs/EmacsFillPolygon.java80
-rw-r--r--java/org/gnu/emacs/EmacsFillRectangle.java116
-rw-r--r--java/org/gnu/emacs/EmacsFontDriver.java173
-rw-r--r--java/org/gnu/emacs/EmacsGC.java121
-rw-r--r--java/org/gnu/emacs/EmacsHandleObject.java59
-rw-r--r--java/org/gnu/emacs/EmacsHolder.java30
-rw-r--r--java/org/gnu/emacs/EmacsInputConnection.java698
-rw-r--r--java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java31
-rw-r--r--java/org/gnu/emacs/EmacsMultitaskActivity.java29
-rw-r--r--java/org/gnu/emacs/EmacsNative.java316
-rw-r--r--java/org/gnu/emacs/EmacsNoninteractive.java203
-rw-r--r--java/org/gnu/emacs/EmacsOpenActivity.java552
-rw-r--r--java/org/gnu/emacs/EmacsPixmap.java192
-rw-r--r--java/org/gnu/emacs/EmacsPreferencesActivity.java168
-rw-r--r--java/org/gnu/emacs/EmacsSafThread.java1687
-rw-r--r--java/org/gnu/emacs/EmacsSdk11Clipboard.java290
-rw-r--r--java/org/gnu/emacs/EmacsSdk23FontDriver.java114
-rw-r--r--java/org/gnu/emacs/EmacsSdk7FontDriver.java539
-rw-r--r--java/org/gnu/emacs/EmacsSdk8Clipboard.java147
-rw-r--r--java/org/gnu/emacs/EmacsService.java1820
-rw-r--r--java/org/gnu/emacs/EmacsSurfaceView.java223
-rw-r--r--java/org/gnu/emacs/EmacsThread.java82
-rw-r--r--java/org/gnu/emacs/EmacsView.java777
-rw-r--r--java/org/gnu/emacs/EmacsWindow.java1445
-rw-r--r--java/org/gnu/emacs/EmacsWindowAttachmentManager.java230
-rw-r--r--java/res/drawable/emacs.pngbin0 -> 13462 bytes
-rw-r--r--java/res/drawable/emacs_wrench.pngbin0 -> 24996 bytes
-rw-r--r--java/res/values-v11/style.xml24
-rw-r--r--java/res/values-v14/style.xml25
-rw-r--r--java/res/values-v19/bool.xml22
-rw-r--r--java/res/values-v24/bool.xml22
-rw-r--r--java/res/values-v29/style.xml32
-rw-r--r--java/res/values/bool.xml23
-rw-r--r--java/res/values/strings.xml45
-rw-r--r--java/res/values/style.xml26
-rw-r--r--java/res/xml/preferences.xml30
55 files changed, 15842 insertions, 0 deletions
diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in
new file mode 100644
index 00000000000..895e7f88c57
--- /dev/null
+++ b/java/AndroidManifest.xml.in
@@ -0,0 +1,225 @@
1<!-- @configure_input@
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<!-- targetSandboxVersion must be 1. Otherwise, fascist security
21 restrictions prevent Emacs from making HTTP connections. -->
22
23<manifest xmlns:android="http://schemas.android.com/apk/res/android"
24 package="org.gnu.emacs"
25 android:targetSandboxVersion="1"
26 android:installLocation="auto"
27 android:requestLegacyExternalStorage="true"
28 @ANDROID_SHARED_USER_ID@
29 @ANDROID_SHARED_USER_NAME@
30 android:versionCode="@emacs_major_version@"
31 android:versionName="@version@">
32
33 <!-- Paste in every permission in existence so Emacs can do
34 anything. -->
35
36 <uses-permission android:name="android.permission.READ_CONTACTS" />
37 <uses-permission android:name="android.permission.WRITE_CONTACTS" />
38 <uses-permission android:name="android.permission.VIBRATE" />
39 <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
40 <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
41 <uses-permission android:name="android.permission.INTERNET" />
42 <uses-permission android:name="android.permission.SET_WALLPAPER" />
43 <!-- Despite the claim that WRITE_EXTERNAL_STORAGE also covers
44 reading from external storage, specifying READ_EXTERNAL_STORAGE
45 seems to still be necessary on some versions of Android.
46 (bug#64445) -->
47 <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
48 <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
49 <uses-permission android:name="android.permission.SEND_SMS" />
50 <uses-permission android:name="android.permission.RECEIVE_SMS" />
51 <uses-permission android:name="android.permission.RECEIVE_MMS"/>
52 <uses-permission android:name="android.permission.WRITE_SMS"/>
53 <uses-permission android:name="android.permission.READ_SMS"/>
54 <uses-permission android:name="android.permission.NFC" />
55 <uses-permission android:name="android.permission.TRANSMIT_IR" />
56 <uses-permission android:name="android.permission.READ_PHONE_STATE"/>
57 <uses-permission android:name="android.permission.WAKE_LOCK"/>
58 <uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
59 <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES"/>
60 <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES"/>
61 <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW"/>
62 <uses-permission android:name="android.permission.RECORD_AUDIO" />
63 <uses-permission android:name="android.permission.CAMERA" />
64
65 <!-- This is required on Android 11 or later to access /sdcard. -->
66
67 <uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE"/>
68
69 <uses-sdk android:minSdkVersion="@ANDROID_MIN_SDK@"
70 android:targetSdkVersion="33"/>
71
72 <application android:name="org.gnu.emacs.EmacsApplication"
73 android:label="Emacs"
74 android:icon="@drawable/emacs"
75 android:hardwareAccelerated="true"
76 android:supportsRtl="true"
77 android:theme="@style/EmacsStyle"
78 android:debuggable="@ANDROID_DEBUGGABLE@"
79 android:allowBackup="true"
80 android:extractNativeLibs="true">
81
82 <activity android:name="org.gnu.emacs.EmacsActivity"
83 android:launchMode="singleInstance"
84 android:windowSoftInputMode="adjustResize"
85 android:exported="true"
86 android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
87 <intent-filter>
88 <action android:name="android.intent.action.MAIN" />
89 <category android:name="android.intent.category.DEFAULT" />
90 <category android:name="android.intent.category.LAUNCHER" />
91 </intent-filter>
92 </activity>
93
94 <activity android:name="org.gnu.emacs.EmacsOpenActivity"
95 android:taskAffinity="open.dialog"
96 android:excludeFromRecents="true"
97 android:exported="true">
98
99 <!-- Allow Emacs to open all kinds of files known to Android. -->
100
101 <intent-filter>
102 <action android:name="android.intent.action.VIEW"/>
103 <action android:name="android.intent.action.EDIT"/>
104 <action android:name="android.intent.action.PICK"/>
105
106 <category android:name="android.intent.category.DEFAULT"/>
107
108 <data android:mimeType="image/aces"/>
109 <data android:mimeType="image/avci"/>
110 <data android:mimeType="image/avcs"/>
111 <data android:mimeType="image/avif"/>
112 <data android:mimeType="image/bmp"/>
113 <data android:mimeType="image/cgm"/>
114 <data android:mimeType="image/dicom-rle"/>
115 <data android:mimeType="image/dpx"/>
116 <data android:mimeType="image/emf"/>
117 <data android:mimeType="image/example"/>
118 <data android:mimeType="image/fits"/>
119 <data android:mimeType="image/g3fax"/>
120 <data android:mimeType="image/heic"/>
121 <data android:mimeType="image/heic-sequence"/>
122 <data android:mimeType="image/heif"/>
123 <data android:mimeType="image/heif-sequence"/>
124 <data android:mimeType="image/hej2k"/>
125 <data android:mimeType="image/hsj2"/>
126 <data android:mimeType="image/jls"/>
127 <data android:mimeType="image/jp2"/>
128 <data android:mimeType="image/jph"/>
129 <data android:mimeType="image/jphc"/>
130 <data android:mimeType="image/jpm"/>
131 <data android:mimeType="image/jpx"/>
132 <data android:mimeType="image/jxr"/>
133 <data android:mimeType="image/jxrA"/>
134 <data android:mimeType="image/jxrS"/>
135 <data android:mimeType="image/jxs"/>
136 <data android:mimeType="image/jxsc"/>
137 <data android:mimeType="image/jxsi"/>
138 <data android:mimeType="image/jxss"/>
139 <data android:mimeType="image/ktx"/>
140 <data android:mimeType="image/ktx2"/>
141 <data android:mimeType="image/naplps"/>
142 <data android:mimeType="image/png"/>
143 <data android:mimeType="image/prs.btif"/>
144 <data android:mimeType="image/prs.pti"/>
145 <data android:mimeType="image/pwg-raster"/>
146 <data android:mimeType="image/svg+xml"/>
147 <data android:mimeType="image/t38"/>
148 <data android:mimeType="image/tiff"/>
149 <data android:mimeType="image/tiff-fx"/>
150 <data android:mimeType="image/xpm"/>
151 <data android:mimeType="text/*"/>
152 <data android:mimeType="application/*xml"/>
153 <data android:mimeType="application/atom+xml"/>
154 <data android:mimeType="application/dxf"/>
155 <data android:mimeType="application/ecmascript"/>
156 <data android:mimeType="application/javascript"/>
157 <data android:mimeType="application/json"/>
158 <data android:mimeType="application/*log*"/>
159 <data android:mimeType="application/octet-stream"/>
160 <data android:mimeType="application/soap+xm"/>
161 <data android:mimeType="application/x-caramel"/>
162 <data android:mimeType="application/x-klaunch"/>
163 <data android:mimeType="application/x-latex"/>
164 <data android:mimeType="application/x-sh"/>
165 <data android:mimeType="application/x-tcl"/>
166 <data android:mimeType="application/x-tex*"/>
167 <data android:mimeType="application/x-troff*"/>
168 <data android:mimeType="application/xhtml+xml"/>
169 <data android:mimeType="application/xml*"/>
170 <data android:mimeType="application/zip"/>
171 <data android:mimeType="application/x-zip-compressed"/>
172 </intent-filter>
173 </activity>
174
175 <activity android:name="org.gnu.emacs.EmacsMultitaskActivity"
176 android:windowSoftInputMode="adjustResize"
177 android:exported="true"
178 android:configChanges="orientation|screenSize|screenLayout|keyboardHidden"/>
179
180 <activity android:autoRemoveFromRecents="true"
181 android:label="Emacs options"
182 android:exported="true"
183 android:name=".EmacsPreferencesActivity">
184 <intent-filter>
185 <action android:name="android.intent.action.APPLICATION_PREFERENCES" />
186 <category android:name="android.intent.category.DEFAULT" />
187 </intent-filter>
188 </activity>
189
190 <!-- Android 6 and earlier don't display ``application
191 preferences'' activities in Settings, so display the
192 preferences activity as a launcher icon instead. -->
193
194 <activity android:autoRemoveFromRecents="true"
195 android:label="Emacs options"
196 android:enabled="@bool/isBeforeNougat"
197 android:exported="@bool/isBeforeNougat"
198 android:icon="@drawable/emacs_wrench"
199 android:name=".EmacsLauncherPreferencesActivity">
200 <intent-filter>
201 <action android:name="android.intent.action.MAIN" />
202 <category android:name="android.intent.category.DEFAULT" />
203 <category android:name="android.intent.category.LAUNCHER" />
204 </intent-filter>
205 </activity>
206
207 <provider android:name="org.gnu.emacs.EmacsDocumentsProvider"
208 android:authorities="org.gnu.emacs"
209 android:exported="true"
210 android:grantUriPermissions="true"
211 android:permission="android.permission.MANAGE_DOCUMENTS"
212 android:enabled="@bool/isAtLeastKitKat">
213 <intent-filter>
214 <action
215 android:name="android.content.action.DOCUMENTS_PROVIDER"/>
216 </intent-filter>
217 </provider>
218
219 <service android:name="org.gnu.emacs.EmacsService"
220 android:directBootAware="false"
221 android:enabled="true"
222 android:exported="false"
223 android:label="GNU Emacs service"/>
224 </application>
225</manifest>
diff --git a/java/INSTALL b/java/INSTALL
new file mode 100644
index 00000000000..0646db426e0
--- /dev/null
+++ b/java/INSTALL
@@ -0,0 +1,977 @@
1Installation instructions for Android
2Copyright (C) 2023 Free Software Foundation, Inc.
3See the end of the file for license conditions.
4
5Please read the entirety of this file before attempting to build Emacs
6as an application package which can run on Android devices.
7
8When building from the source repository, make sure to read
9INSTALL.REPO as well.
10
11
12
13Android is an unusual operating system in that program binaries cannot
14be produced on computers running Android themselves. Instead, they
15must be built on some other computer using a set of tools known as the
16``Android SDK'' (Software Development Kit) and the ``Android NDK''
17(Native Development Kit.) Appropriate versions of both must be
18obtained to build GNU Emacs; after being built, the generated binaries
19will work on almost all Android devices. This document does not
20elaborate on how both sets of tools can be obtained. However, for
21your freedom's sake, you should use the Android SDK provided by the
22Debian project.
23
24In addition to the Android SDK and Android NDK, Emacs also requires
25the Java compiler from OpenJDK 1.7.0 to be installed on your system,
26along with a working `m4' macro processor. Building on GNU systems is
27all that is officially supported. We are told that Mac OS works too,
28and other Unix systems will likely work as well, but MS Windows and
29Cygwin will not.
30
31Once all of those tools are obtained, you may invoke the `configure'
32script like so:
33
34 ./configure --with-android=/path/to/android.jar \
35 ANDROID_CC=/path/to/android/ndk/cc \
36 SDK_BUILD_TOOLS=/path/to/sdk/build/tools
37
38Replacing the paths in the command line above with:
39
40 - the path to the `android.jar' headers which come with the Android
41 SDK. They must correspond to Android version 13 (API level 33) or
42 later.
43
44 - the path to the C compiler in the Android NDK, for the kind of CPU
45 you are building Emacs to run on.
46
47 - the path to the directory in the Android SDK containing binaries
48 such as `aapt', `apksigner', and `d8'. These are used to build
49 the application package.
50
51Where the type of CPU can either be `armeabi', `armv7*', `i686',
52`x86_64', `mips', or `mips64'.
53
54After the configuration process completes, you may run:
55
56 make all
57
58Once `make' finishes, there should be a file in the `java' directory
59named along the lines of:
60
61 emacs-<version>-<api-version>-<abi>.apk
62
63where <api-version> is the oldest version of Android that the package
64will run on, and <abi> is the type of Android machine the package was
65built for.
66
67The generated package can be uploaded onto an SD card (or similar
68medium) and installed on-device.
69
70
71BUILDING WITH OLD NDK VERSIONS
72
73Building Emacs with an old version of the Android NDK requires special
74setup. This is because there is no separate C compiler binary for
75each version of Android in those versions of the NDK.
76
77Before running `configure', you must identify three variables:
78
79 - What kind of Android system you are building Emacs for.
80
81 - The minimum API version of Android you want to build Emacs for.
82
83 - The locations of the system root and include files for that
84 version of Android in the NDK.
85
86That information must then be specified as arguments to the NDK C
87compiler. For example:
88
89 ./configure [...] \
90 ANDROID_CC="i686-linux-android-gcc \
91 --sysroot=/path/to/ndk/platforms/android-14/arch-x86/"
92 ANDROID_CFLAGS="-isystem /path/to/ndk/sysroot/usr/include \
93 -isystem /path/to/ndk/sysroot/usr/include/i686-linux-android \
94 -D__ANDROID_API__=14"
95
96Where __ANDROID_API__ and the version identifier in
97"platforms/android-14" defines the version of Android you are building
98for, and the include directories specify the paths to the relevant
99Android headers. In addition, it may be necessary to specify
100"-gdwarf-2", due to a bug in the Android NDK.
101
102Even older versions of the Android SDK do not require the extra
103`-isystem' directives.
104
105Emacs is known to run on Android 2.2 (API version 8) or later, with
106the NDK r10b or later. We wanted to make Emacs work on even older
107versions of Android, but they are missing the required JNI graphics
108library that allows Emacs to display text from C code.
109
110Due to an extremely nasty bug in the Android 2.2 system, the generated
111Emacs package cannot be compressed in builds for Android 2.2. As a
112result, the Emacs package will be approximately 100 megabytes larger
113than a compressed package for a newer version of Android.
114
115
116BUILDING C++ DEPENDENCIES
117
118With a new version of the NDK, dependencies containing C++ code should
119build without any futher configuration. However, older versions
120require that you use the ``make_standalone_toolchain.py'' script in
121the NDK distribution to create a ``standalone toolchain'', and use
122that instead, in order for C++ headers to be found.
123
124See https://developer.android.com/ndk/guides/standalone_toolchain for
125more details; when a ``standalone toolchain'' is specified, the
126configure script will try to determine the location of the C++
127compiler based on the C compiler specified. If that automatic
128detection does not work, you can specify a C++ compiler yourself, like
129so:
130
131 ./configure --with-ndk-cxx=/path/to/toolchain/bin/i686-linux-android-g++
132
133Some versions of the NDK have a bug, where GCC fails to locate
134``stddef.h'' after being copied to a standalone toolchain. To work
135around this problem (which normally exhibits itself when building C++
136code), add:
137
138 -isystem /path/to/toolchain/include/c++/4.9.x
139
140to ANDROID_CFLAGS.
141
142
143DEBUG AND RELEASE BUILDS
144
145Android makes a distinction between ``debug'' and ``release'' builds
146of applications. With ``release'' builds, the system will apply
147stronger optimizations to the application at the cost of being unable
148to debug them with the steps in etc/DEBUG.
149
150Emacs is built as a debuggable package by default, but:
151
152 ./configure --without-android-debug
153
154will create a release build of Emacs instead. This may be useful when
155running Emacs on resource constrained machines.
156
157If you are building an Emacs package for redistribution, we urge you
158to provide both debug and release versions.
159
160
161BUILDING WITH A SHARED USER ID
162
163Sometimes it may be desirable to build Emacs so that it is able to
164access executables and application data from another program. To
165achieve this, that other program must have a ``shared user ID'', and
166be signed with the same signing key used to sign Emacs (normally
167`emacs.keystore'.)
168
169Once you have both that signing key and its ``shared user ID'', you
170can give it to configure:
171
172 ./configure --with-shared-user-id=MY.SHARED.USER.ID
173
174For instance,
175
176 ./configure --with-shared-user-id=com.termux
177
178will result in Termux (https://termux.dev)'s application data being
179accessible to Emacs, within its own application data directory located
180at `/data/data/com.termux/files'.
181
182Don't do this if you already have Emacs installed with a different
183shared user ID, as the system does not allow programs to change their
184user IDs after being installed.
185
186
187BUILDING WITH THIRD PARTY LIBRARIES
188
189The Android NDK does not support the usual ways of locating third
190party libraries, especially not via `pkg-config'. Instead, it uses
191its own system called `ndk-build'. The one exception to this rule is
192zlib, which is considered a part of the Android OS itself and is
193available on all devices running Android.
194
195Android also requires that each application include its own
196dependencies, as the system makes no guarantee about the existence of
197any particular library.
198
199Emacs is not built with the `ndk-build' system. Instead, it is built
200with Autoconf and Make.
201
202However, it supports building and including dependencies which use the
203similarly Make-based `ndk-build' system.
204
205To use dependencies built through `ndk-build', you must specify a list
206of directories within which Emacs will search for ``Android.mk''
207files, like so:
208
209 ./configure "--with-ndk-path=directory1 directory2"
210
211If `configure' complains about not being able to find
212``libc++_shared.so'', then you must locate that file in your copy of
213the NDK, and specify it like so:
214
215 ./configure --with-ndk-cxx-shared=/path/to/sysroot/libc++_shared.so
216
217Emacs will then read the ``Android.mk'' file in each directory, and
218automatically build and use those modules.
219
220When building for Intel systems, some ``ndk-build'' modules require
221the Netwide Assembler, usually installed under ``nasm'', to be present
222on the system that is building Emacs.
223
224Google, Inc. has adapted many common Emacs dependencies to use the
225`ndk-build' system. Here is a non-exhaustive list of what is known to
226work, along with what has to be patched to make them work:
227
228 libpng - https://android.googlesource.com/platform/external/libpng
229 libwebp - https://android.googlesource.com/platform/external/webp
230 (You must apply the patch at the end of this file for the resulting
231 binary to work on armv7 devices.)
232 giflib - https://android.googlesource.com/platform/external/giflib
233 (You must add LOCAL_EXPORT_CFLAGS := -I$(LOCAL_PATH) before
234 its Android.mk includes $(BUILD_STATIC_LIBRARY))
235 libjpeg-turbo - https://android.googlesource.com/platform/external/libjpeg-turbo
236 (You must add LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH) before
237 its Android.mk includes $(BUILD_SHARED_LIBRARY))
238 libxml2 - https://android.googlesource.com/platform/external/libxml2/
239 (You must also place the dependency icu4c in ``--with-ndk-path'',
240 and apply the patch at the end of this file.)
241 icu4c - https://android.googlesource.com/platform/external/icu/
242 (You must apply the patch at the end of this file.)
243 sqlite3 - https://android.googlesource.com/platform/external/sqlite/
244 (You must apply the patch at the end of this file, and add the `dist'
245 directory to ``--with-ndk-path''.)
246 libselinux - https://android.googlesource.com/platform/external/libselinux
247 (You must apply the patches at the end of the file, and obtain
248 the following three dependencies.)
249 libpackagelistparser
250 https://android.googlesource.com/platform/system/core/+/refs/heads/nougat-mr1-dev/libpackagelistparser/
251 libpcre - https://android.googlesource.com/platform/external/pcre
252 libcrypto - https://android.googlesource.com/platform/external/boringssl
253 (You must apply the patch at the end of this file when building for
254 ARM systems.)
255
256Many of these dependencies have been migrated over to the
257``Android.bp'' build system now used to build Android itself.
258However, the old ``Android.mk'' Makefiles are still present in older
259branches, and can be easily adapte to newer versions.
260
261In addition, some Emacs dependencies provide `ndk-build' support
262themselves:
263
264 libjansson - https://github.com/akheron/jansson
265 (You must add LOCAL_EXPORT_INCLUDES := $(LOCAL_C_INCLUDES) before
266 its Android.mk includes $(BUILD_SHARED_LIBRARY), then copy
267 android/jansson_config.h to android/jansson_private_config.h.)
268
269Emacs developers have ported the following dependencies to ARM Android
270systems:
271
272 gnutls, gmp - https://sourceforge.net/projects/android-ports-for-gnu-emacs
273 (Please see the section GNUTLS near the end of this file.)
274 libtiff - https://sourceforge.net/projects/android-ports-for-gnu-emacs
275 (Extract and point ``--with-ndk-path'' to tiff-4.5.0-emacs.tar.gz.)
276 tree-sitter - https://sourceforge.net/projects/android-ports-for-gnu-emacs
277 (Please see the section TREE-SITTER near the end of this file.)
278 harfbuzz - https://sourceforge.net/projects/android-ports-for-gnu-emacs
279 (Please see the section HARFBUZZ near the end of this file.)
280
281And other developers have ported the following dependencies to Android
282systems:
283
284 ImageMagick, lcms2 - https://github.com/MolotovCherry/Android-ImageMagick7
285 (Please see the section IMAGEMAGICK near the end of this file.)
286
287We anticipate that most untested non-trivial ndk-build dependencies
288will need adjustments in Emacs to work, as the Emacs build system
289which emulates ndk-build is in an extremely early state.
290
291
292GNUTLS
293
294Modified copies of GnuTLS and its dependencies (such as libgmp,
295libtasn1, p11-kit) which can be built with the ndk-build system can be
296found at https://sourceforge.net/projects/android-ports-for-gnu-emacs.
297
298They have only been tested on arm64 Android systems running Android
2995.0 or later, and armv7l systems running Android 13 or later, so your
300mileage may vary, especially if you are trying to build Emacs for
301another kind of machine.
302
303To build Emacs with GnuTLS, you must unpack each of the following tar
304archives in that site:
305
306 gmp-6.2.1-emacs.tgz
307 gnutls-3.7.8-emacs.tar.gz
308 libtasn1-4.19.0-emacs.tar.gz
309 p11-kit-0.24.1-emacs.tar.gz
310 nettle-3.8-emacs.tar.gz
311
312and add the resulting folders to ``--with-ndk-path''. Note that you
313should not try to build these packages separately using any
314`configure' script or Makefiles inside.
315
316
317TREE-SITTER
318
319A copy of tree-sitter modified to build with the ndk-build system can
320also be found that URL. To build Emacs with tree-sitter, you must
321unpack the following tar archive in that site:
322
323 tree-sitter-0.20.7-emacs.tar.gz
324
325and add the resulting folder to ``--with-ndk-build''.
326
327
328HARFBUZZ
329
330A copy of HarfBuzz modified to build with the ndk-build system can
331also be found at that URL. To build Emacs with HarfBuzz, you must
332unpack the following tar archive in that site:
333
334 harfbuzz-7.1.0-emacs.tar.gz
335
336and add the resulting folder to ``--with-ndk-build''.
337
338
339IMAGEMAGICK
340
341There is a third party port of ImageMagick to Android. Unfortunately,
342the port also uses its own patched versions of libpng, libjpeg,
343libtiff and libwebp, which conflict with those used by Emacs. Its
344Makefiles were also written for MS Windows, so you must also apply the
345patch at the end of this file.
346
347
348
349PATCH FOR LIBXML2
350
351This patch must be applied to the Android.mk in Google's version of
352libxml2 before it can be built for Emacs. In addition, you must also
353revert the commit `edb5870767fed8712a9b77ef34097209b61ab2db'.
354
355diff --git a/Android.mk b/Android.mk
356index 07c7b372..24f67e49 100644
357--- a/Android.mk
358+++ b/Android.mk
359@@ -80,6 +80,7 @@ LOCAL_SHARED_LIBRARIES := libicuuc
360 LOCAL_MODULE:= libxml2
361 LOCAL_CLANG := true
362 LOCAL_ADDITIONAL_DEPENDENCIES += $(LOCAL_PATH)/Android.mk
363+LOCAL_EXPORT_C_INCLUDES += $(LOCAL_PATH)
364 include $(BUILD_SHARED_LIBRARY)
365
366 # For the host
367@@ -94,3 +95,5 @@ LOCAL_MODULE := libxml2
368 LOCAL_CLANG := true
369 LOCAL_ADDITIONAL_DEPENDENCIES += $(LOCAL_PATH)/Android.mk
370 include $(BUILD_HOST_STATIC_LIBRARY)
371+
372+$(call import-module,libicuuc)
373
374PATCH FOR ICU
375
376This patch must be applied to icu4j/Android.mk in Google's version of
377icu before it can be built for Emacs.
378
379diff --git a/icu4j/Android.mk b/icu4j/Android.mk
380index d1ab3d5..69eff81 100644
381--- a/icu4j/Android.mk
382+++ b/icu4j/Android.mk
383@@ -69,7 +69,7 @@ include $(BUILD_STATIC_JAVA_LIBRARY)
384 # Path to the ICU4C data files in the Android device file system:
385 icu4c_data := /system/usr/icu
386 icu4j_config_root := $(LOCAL_PATH)/main/classes/core/src
387-include external/icu/icu4j/adjust_icudt_path.mk
388+include $(LOCAL_PATH)/adjust_icudt_path.mk
389
390 include $(CLEAR_VARS)
391 LOCAL_SRC_FILES := $(icu4j_src_files)
392
393diff --git a/icu4c/source/common/Android.mk b/icu4c/source/common/Android.mk
394index 8e5f757..44bb130 100644
395--- a/icu4c/source/common/Android.mk
396+++ b/icu4c/source/common/Android.mk
397@@ -231,7 +231,7 @@ include $(CLEAR_VARS)
398 LOCAL_SRC_FILES += $(src_files)
399 LOCAL_C_INCLUDES += $(c_includes) $(optional_android_logging_includes)
400 LOCAL_CFLAGS += $(local_cflags) -DPIC -fPIC
401-LOCAL_SHARED_LIBRARIES += libdl $(optional_android_logging_libraries)
402+LOCAL_SHARED_LIBRARIES += libdl libstdc++ $(optional_android_logging_libraries)
403 LOCAL_MODULE_TAGS := optional
404 LOCAL_MODULE := libicuuc
405 LOCAL_RTTI_FLAG := -frtti
406
407PATCH FOR SQLITE3
408
409diff --git a/dist/Android.mk b/dist/Android.mk
410index bf277d2..36734d9 100644
411--- a/dist/Android.mk
412+++ b/dist/Android.mk
413@@ -141,6 +141,7 @@ include $(BUILD_HOST_EXECUTABLE)
414 include $(CLEAR_VARS)
415 LOCAL_SRC_FILES := $(common_src_files)
416 LOCAL_CFLAGS += $(minimal_sqlite_flags)
417+LOCAL_EXPORT_C_INCLUDES += $(LOCAL_PATH)
418 LOCAL_MODULE:= libsqlite_static_minimal
419 LOCAL_SDK_VERSION := 23
420 include $(BUILD_STATIC_LIBRARY)
421
422diff --git a/dist/sqlite3.c b/dist/sqlite3.c
423index b0536a4..8fa1ee9 100644
424--- a/dist/sqlite3.c
425+++ b/dist/sqlite3.c
426@@ -26474,7 +26474,7 @@ SQLITE_PRIVATE const char *sqlite3OpcodeName(int i){
427 */
428 #if !defined(HAVE_POSIX_FALLOCATE) \
429 && (_XOPEN_SOURCE >= 600 || _POSIX_C_SOURCE >= 200112L)
430-# define HAVE_POSIX_FALLOCATE 1
431+/* # define HAVE_POSIX_FALLOCATE 1 */
432 #endif
433
434 /*
435
436PATCH FOR WEBP
437
438diff --git a/Android.mk b/Android.mk
439index c7bcb0f5..d4da1704 100644
440--- a/Android.mk
441+++ b/Android.mk
442@@ -28,9 +28,10 @@ ifneq ($(findstring armeabi-v7a, $(TARGET_ARCH_ABI)),)
443 # Setting LOCAL_ARM_NEON will enable -mfpu=neon which may cause illegal
444 # instructions to be generated for armv7a code. Instead target the neon code
445 # specifically.
446- NEON := c.neon
447- USE_CPUFEATURES := yes
448- WEBP_CFLAGS += -DHAVE_CPU_FEATURES_H
449+ # NEON := c.neon
450+ # USE_CPUFEATURES := yes
451+ # WEBP_CFLAGS += -DHAVE_CPU_FEATURES_H
452+ NEON := c
453 else
454 NEON := c
455 endif
456
457PATCHES FOR SELINUX
458
459diff --git a/Android.mk b/Android.mk
460index 659232e..1e64fd6 100644
461--- a/Android.mk
462+++ b/Android.mk
463@@ -116,3 +116,7 @@ LOCAL_STATIC_LIBRARIES := libselinux
464 LOCAL_WHOLE_STATIC_LIBRARIES := libpcre
465 LOCAL_C_INCLUDES := external/pcre
466 include $(BUILD_HOST_EXECUTABLE)
467+
468+$(call import-module,libpcre)
469+$(call import-module,libpackagelistparser)
470+$(call import-module,libcrypto)
471
472diff --git a/src/android.c b/src/android.c
473index 5206a9f..b351ffc 100644
474--- a/src/android.c
475+++ b/src/android.c
476@@ -21,8 +21,7 @@
477 #include <selinux/label.h>
478 #include <selinux/avc.h>
479 #include <openssl/sha.h>
480-#include <private/android_filesystem_config.h>
481-#include <log/log.h>
482+#include <android/log.h>
483 #include "policy.h"
484 #include "callbacks.h"
485 #include "selinux_internal.h"
486@@ -686,6 +685,7 @@ static int seapp_context_lookup(enum seapp_kind kind,
487 seinfo = parsedseinfo;
488 }
489
490+#if 0
491 userid = uid / AID_USER;
492 isOwner = (userid == 0);
493 appid = uid % AID_USER;
494@@ -702,9 +702,13 @@ static int seapp_context_lookup(enum seapp_kind kind,
495 username = "_app";
496 appid -= AID_APP;
497 } else {
498+#endif
499 username = "_isolated";
500+ appid = 0;
501+#if 0
502 appid -= AID_ISOLATED_START;
503 }
504+#endif
505
506 if (appid >= CAT_MAPPING_MAX_ID || userid >= CAT_MAPPING_MAX_ID)
507 goto err;
508@@ -1662,8 +1666,10 @@ int selinux_log_callback(int type, const char *fmt, ...)
509
510 va_start(ap, fmt);
511 if (vasprintf(&strp, fmt, ap) != -1) {
512+#if 0
513 LOG_PRI(priority, "SELinux", "%s", strp);
514 LOG_EVENT_STRING(AUDITD_LOG_TAG, strp);
515+#endif
516 free(strp);
517 }
518 va_end(ap);
519
520PATCH FOR BORINGSSL
521
522diff --git a/Android.mk b/Android.mk
523index 3e3ef2a..277d4a9 100644
524--- a/Android.mk
525+++ b/Android.mk
526@@ -27,7 +27,9 @@ LOCAL_MODULE := libcrypto
527 LOCAL_EXPORT_C_INCLUDE_DIRS := $(LOCAL_PATH)/src/include
528 LOCAL_ADDITIONAL_DEPENDENCIES := $(LOCAL_PATH)/Android.mk $(LOCAL_PATH)/crypto-sources.mk
529 LOCAL_CFLAGS += -fvisibility=hidden -DBORINGSSL_SHARED_LIBRARY -DBORINGSSL_IMPLEMENTATION -DOPENSSL_SMALL -Wno-unused-parameter
530+LOCAL_CFLAGS_arm = -DOPENSSL_STATIC_ARMCAP -DOPENSSL_NO_ASM
531 LOCAL_SDK_VERSION := 9
532+LOCAL_LDFLAGS = --no-undefined
533 # sha256-armv4.S does not compile with clang.
534 LOCAL_CLANG_ASFLAGS_arm += -no-integrated-as
535 LOCAL_CLANG_ASFLAGS_arm64 += -march=armv8-a+crypto
536diff --git a/sources.mk b/sources.mk
537index e82f3d5..be3a3c4 100644
538--- a/sources.mk
539+++ b/sources.mk
540@@ -337,20 +337,20 @@ linux_aarch64_sources := \
541 linux-aarch64/crypto/sha/sha256-armv8.S\
542 linux-aarch64/crypto/sha/sha512-armv8.S\
543
544-linux_arm_sources := \
545- linux-arm/crypto/aes/aes-armv4.S\
546- linux-arm/crypto/aes/aesv8-armx32.S\
547- linux-arm/crypto/aes/bsaes-armv7.S\
548- linux-arm/crypto/bn/armv4-mont.S\
549- linux-arm/crypto/modes/ghash-armv4.S\
550- linux-arm/crypto/modes/ghashv8-armx32.S\
551- linux-arm/crypto/sha/sha1-armv4-large.S\
552- linux-arm/crypto/sha/sha256-armv4.S\
553- linux-arm/crypto/sha/sha512-armv4.S\
554- src/crypto/chacha/chacha_vec_arm.S\
555- src/crypto/cpu-arm-asm.S\
556- src/crypto/curve25519/asm/x25519-asm-arm.S\
557- src/crypto/poly1305/poly1305_arm_asm.S\
558+# linux_arm_sources := \
559+# linux-arm/crypto/aes/aes-armv4.S\
560+# linux-arm/crypto/aes/aesv8-armx32.S\
561+# linux-arm/crypto/aes/bsaes-armv7.S\
562+# linux-arm/crypto/bn/armv4-mont.S\
563+# linux-arm/crypto/modes/ghash-armv4.S\
564+# linux-arm/crypto/modes/ghashv8-armx32.S\
565+# linux-arm/crypto/sha/sha1-armv4-large.S\
566+# linux-arm/crypto/sha/sha256-armv4.S\
567+# linux-arm/crypto/sha/sha512-armv4.S\
568+# src/crypto/chacha/chacha_vec_arm.S\
569+# src/crypto/cpu-arm-asm.S\
570+# src/crypto/curve25519/asm/x25519-asm-arm.S\
571+# src/crypto/poly1305/poly1305_arm_asm.S\
572
573 linux_x86_sources := \
574 linux-x86/crypto/aes/aes-586.S\
575
576PATCH FOR IMAGEMAGICK
577
578diff --git a/Android.mk b/Android.mk
579index 5ab6699..4441417 100644
580--- a/Android.mk
581+++ b/Android.mk
582@@ -52,6 +52,20 @@ LZMA_LIB_PATH := $(LOCAL_PATH)/xz-5.2.4
583 BZLIB_LIB_PATH := $(LOCAL_PATH)/bzip-1.0.8
584 LCMS_LIB_PATH := $(LOCAL_PATH)/liblcms2-2.9
585
586+LIBBZ2_ENABLED := true
587+LIBFFTW_ENABLED := true
588+LIBFREETYPE2_ENABLED := true
589+LIBJPEG_TURBO_ENABLED := true
590+LIBLZMA_ENABLED := true
591+LIBOPENJPEG_ENABLED := true
592+LIBPNG_ENABLED := true
593+LIBTIFF_ENABLED := true
594+LIBWEBP_ENABLED := true
595+LIBXML2_ENABLED := true
596+LIBZLIB_ENABLED := true
597+LIBLCMS2_ENABLED := true
598+BUILD_MAGICKWAND := true
599+
600 #-------------------------------------------------------------
601 # Include all modules
602 #-------------------------------------------------------------
603@@ -68,6 +82,9 @@ include $(MAKE_PATH)/libjpeg-turbo.mk
604 # libopenjpeg
605 include $(MAKE_PATH)/libopenjpeg.mk
606
607+# libwebp
608+include $(MAKE_PATH)/libwebp.mk
609+
610 # libtiff
611 include $(MAKE_PATH)/libtiff.mk
612
613@@ -77,9 +94,6 @@ include $(MAKE_PATH)/libpng.mk
614 # libfreetype2
615 include $(MAKE_PATH)/libfreetype2.mk
616
617-# libwebp
618-include $(MAKE_PATH)/libwebp.mk
619-
620 # libfftw
621 include $(MAKE_PATH)/libfftw.mk
622
623diff --git a/libjpeg-turbo-2.0.2/jconfig.h b/libjpeg-turbo-2.0.2/jconfig.h
624index 47d14c9..5c6f8ee 100644
625--- a/libjpeg-turbo-2.0.2/jconfig.h
626+++ b/libjpeg-turbo-2.0.2/jconfig.h
627@@ -1,57 +1,43 @@
628-/* autogenerated jconfig.h based on Android.mk var JCONFIG_FLAGS */
629+/* autogenerated jconfig.h based on Android.mk var JCONFIG_FLAGS */
630 #ifndef JPEG_LIB_VERSION
631 #define JPEG_LIB_VERSION 62
632 #endif
633-
634 #ifndef LIBJPEG_TURBO_VERSION
635 #define LIBJPEG_TURBO_VERSION 2.0.2
636 #endif
637-
638 #ifndef LIBJPEG_TURBO_VERSION_NUMBER
639 #define LIBJPEG_TURBO_VERSION_NUMBER 202
640 #endif
641-
642 #ifndef C_ARITH_CODING_SUPPORTED
643 #define C_ARITH_CODING_SUPPORTED
644 #endif
645-
646 #ifndef D_ARITH_CODING_SUPPORTED
647 #define D_ARITH_CODING_SUPPORTED
648 #endif
649-
650 #ifndef MEM_SRCDST_SUPPORTED
651 #define MEM_SRCDST_SUPPORTED
652 #endif
653-
654 #ifndef WITH_SIMD
655 #define WITH_SIMD
656 #endif
657-
658 #ifndef BITS_IN_JSAMPLE
659 #define BITS_IN_JSAMPLE 8
660 #endif
661-
662 #ifndef HAVE_LOCALE_H
663 #define HAVE_LOCALE_H
664 #endif
665-
666 #ifndef HAVE_STDDEF_H
667 #define HAVE_STDDEF_H
668 #endif
669-
670 #ifndef HAVE_STDLIB_H
671 #define HAVE_STDLIB_H
672 #endif
673-
674 #ifndef NEED_SYS_TYPES_H
675 #define NEED_SYS_TYPES_H
676 #endif
677-
678 #ifndef HAVE_UNSIGNED_CHAR
679 #define HAVE_UNSIGNED_CHAR
680 #endif
681-
682 #ifndef HAVE_UNSIGNED_SHORT
683 #define HAVE_UNSIGNED_SHORT
684 #endif
685-
686diff --git a/libxml2-2.9.9/encoding.c b/libxml2-2.9.9/encoding.c
687index a3aaf10..60f165b 100644
688--- a/libxml2-2.9.9/encoding.c
689+++ b/libxml2-2.9.9/encoding.c
690@@ -2394,7 +2394,6 @@ xmlCharEncOutput(xmlOutputBufferPtr output, int init)
691 {
692 int ret;
693 size_t written;
694- size_t writtentot = 0;
695 size_t toconv;
696 int c_in;
697 int c_out;
698@@ -2451,7 +2450,6 @@ retry:
699 xmlBufContent(in), &c_in);
700 xmlBufShrink(in, c_in);
701 xmlBufAddLen(out, c_out);
702- writtentot += c_out;
703 if (ret == -1) {
704 if (c_out > 0) {
705 /* Can be a limitation of iconv or uconv */
706@@ -2536,7 +2534,6 @@ retry:
707 }
708
709 xmlBufAddLen(out, c_out);
710- writtentot += c_out;
711 goto retry;
712 }
713 }
714@@ -2567,9 +2564,7 @@ xmlCharEncOutFunc(xmlCharEncodingHandler *handler, xmlBufferPtr out,
715 xmlBufferPtr in) {
716 int ret;
717 int written;
718- int writtentot = 0;
719 int toconv;
720- int output = 0;
721
722 if (handler == NULL) return(-1);
723 if (out == NULL) return(-1);
724@@ -2612,7 +2607,6 @@ retry:
725 in->content, &toconv);
726 xmlBufferShrink(in, toconv);
727 out->use += written;
728- writtentot += written;
729 out->content[out->use] = 0;
730 if (ret == -1) {
731 if (written > 0) {
732@@ -2622,8 +2616,6 @@ retry:
733 ret = -3;
734 }
735
736- if (ret >= 0) output += ret;
737-
738 /*
739 * Attempt to handle error cases
740 */
741@@ -2700,7 +2692,6 @@ retry:
742 }
743
744 out->use += written;
745- writtentot += written;
746 out->content[out->use] = 0;
747 goto retry;
748 }
749diff --git a/libxml2-2.9.9/xpath.c b/libxml2-2.9.9/xpath.c
750index 5e3bb9f..505ec82 100644
751--- a/libxml2-2.9.9/xpath.c
752+++ b/libxml2-2.9.9/xpath.c
753@@ -10547,7 +10547,7 @@ xmlXPathCompFilterExpr(xmlXPathParserContextPtr ctxt) {
754
755 static xmlChar *
756 xmlXPathScanName(xmlXPathParserContextPtr ctxt) {
757- int len = 0, l;
758+ int l;
759 int c;
760 const xmlChar *cur;
761 xmlChar *ret;
762@@ -10567,7 +10567,6 @@ xmlXPathScanName(xmlXPathParserContextPtr ctxt) {
763 (c == '_') || (c == ':') ||
764 (IS_COMBINING(c)) ||
765 (IS_EXTENDER(c)))) {
766- len += l;
767 NEXTL(l);
768 c = CUR_CHAR(l);
769 }
770diff --git a/make/libicu4c.mk b/make/libicu4c.mk
771index 21ec121..8b77865 100644
772--- a/make/libicu4c.mk
773+++ b/make/libicu4c.mk
774@@ -250,7 +250,7 @@ LOCAL_MODULE := libicuuc
775 LOCAL_SRC_FILES := $(src_files)
776
777 # when built in android, they require uconfig_local (because of android project), but we don't need this
778-$(shell > $(ICU_COMMON_PATH)/unicode/uconfig_local.h echo /* Autogenerated stub file to make libicuuc build happy */) \
779+$(shell > $(ICU_COMMON_PATH)/unicode/uconfig_local.h echo /\* Autogenerated stub file to make libicuuc build happy \*/) \
780
781 ifeq ($(LIBXML2_ENABLED),true)
782 include $(BUILD_STATIC_LIBRARY)
783diff --git a/make/libjpeg-turbo.mk b/make/libjpeg-turbo.mk
784index d39dd41..fdebcf3 100644
785--- a/make/libjpeg-turbo.mk
786+++ b/make/libjpeg-turbo.mk
787@@ -230,30 +230,30 @@ JCONFIG_FLAGS += \
788 HAVE_UNSIGNED_SHORT
789
790 JCONFIGINT_FLAGS += \
791- BUILD="20190814" \
792- PACKAGE_NAME="libjpeg-turbo" \
793- VERSION="2.0.2"
794+ BUILD=\"20190814\" \
795+ PACKAGE_NAME=\"libjpeg-turbo\" \
796+ VERSION=\"2.0.2\"
797
798 # originally defined in jconfigint.h, but the substitution has problems with spaces
799 LOCAL_CFLAGS := \
800 -DINLINE="inline __attribute__((always_inline))"
801
802 # create definition file jconfig.h, needed in order to build
803-$(shell echo /* autogenerated jconfig.h based on Android.mk var JCONFIG_FLAGS */ > $(JPEG_LIB_PATH)/jconfig.h)
804+$(shell echo \/\* autogenerated jconfig.h based on Android.mk var JCONFIG_FLAGS \*\/ > $(JPEG_LIB_PATH)/jconfig.h)
805 $(foreach name,$(JCONFIG_FLAGS), \
806 $(if $(findstring =,$(name)), \
807- $(shell >>$(JPEG_LIB_PATH)/jconfig.h echo #ifndef $(firstword $(subst =, ,$(name)))) \
808+ $(shell >>$(JPEG_LIB_PATH)/jconfig.h echo \#ifndef $(firstword $(subst =, ,$(name)))) \
809 , \
810- $(shell >>$(JPEG_LIB_PATH)/jconfig.h echo #ifndef $(name)) \
811+ $(shell >>$(JPEG_LIB_PATH)/jconfig.h echo \#ifndef $(name)) \
812 ) \
813- $(shell >>$(JPEG_LIB_PATH)/jconfig.h echo #define $(subst =, ,$(name))) \
814- $(shell >>$(JPEG_LIB_PATH)/jconfig.h echo #endif) \
815+ $(shell >>$(JPEG_LIB_PATH)/jconfig.h echo \#define $(subst =, ,$(name))) \
816+ $(shell >>$(JPEG_LIB_PATH)/jconfig.h echo \#endif) \
817 $(shell >> $(JPEG_LIB_PATH)/jconfig.h echo.) \
818 )
819
820 # create definition file jconfigint.h, needed in order to build
821-$(shell >$(JPEG_LIB_PATH)/jconfigint.h echo /* autogenerated jconfigint.h based on Android.mk vars JCONFIGINT_FLAGS */)
822-$(foreach name,$(JCONFIGINT_FLAGS),$(shell >>$(JPEG_LIB_PATH)/jconfigint.h echo #define $(subst =, ,$(name))))
823+$(shell >$(JPEG_LIB_PATH)/jconfigint.h echo /\* autogenerated jconfigint.h based on Android.mk vars JCONFIGINT_FLAGS \*/)
824+$(foreach name,$(JCONFIGINT_FLAGS),$(shell >>$(JPEG_LIB_PATH)/jconfigint.h echo \#define $(subst =, ,$(name))))
825
826 ifeq ($(LIBJPEG_TURBO_ENABLED),true)
827 include $(BUILD_STATIC_LIBRARY)
828diff --git a/make/liblcms2.mk b/make/liblcms2.mk
829index e1fd3b9..29ca791 100644
830--- a/make/liblcms2.mk
831+++ b/make/liblcms2.mk
832@@ -10,6 +10,10 @@ LOCAL_C_INCLUDES := \
833 $(LCMS_LIB_PATH)/include \
834 $(LCMS_LIB_PATH)/src
835
836+LOCAL_EXPORT_C_INCLUDES := \
837+ $(LCMS_LIB_PATH) \
838+ $(LCMS_LIB_PATH)/include \
839+ $(LCMS_LIB_PATH)/src
840
841 LOCAL_CFLAGS := \
842 -DHAVE_FUNC_ATTRIBUTE_VISIBILITY=1 \
843diff --git a/make/libmagick++-7.mk b/make/libmagick++-7.mk
844index 5352ccb..929396d 100644
845--- a/make/libmagick++-7.mk
846+++ b/make/libmagick++-7.mk
847@@ -12,7 +12,7 @@ LOCAL_C_INCLUDES := \
848
849 ifneq ($(STATIC_BUILD),true)
850 LOCAL_LDFLAGS += -fexceptions
851- LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -lz
852+ LOCAL_LDLIBS := -llog -lz
853 endif
854
855 LOCAL_SRC_FILES := \
856diff --git a/make/libmagickcore-7.mk b/make/libmagickcore-7.mk
857index 81293b2..d51fced 100644
858--- a/make/libmagickcore-7.mk
859+++ b/make/libmagickcore-7.mk
860@@ -25,6 +25,7 @@ else ifeq ($(TARGET_ARCH_ABI),x86_64)
861
862 endif
863
864+LOCAL_EXPORT_C_INCLUDES += $(IMAGE_MAGICK)
865
866 LOCAL_C_INCLUDES += \
867 $(IMAGE_MAGICK) \
868@@ -45,10 +46,9 @@ LOCAL_C_INCLUDES += \
869 $(BZLIB_LIB_PATH) \
870 $(LCMS_LIB_PATH)/include
871
872-
873 ifneq ($(STATIC_BUILD),true)
874 # ignored in static library builds
875- LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -lz
876+ LOCAL_LDLIBS := -llog -lz
877 endif
878
879
880diff --git a/make/libmagickwand-7.mk b/make/libmagickwand-7.mk
881index 7be2fb6..0bbcca5 100644
882--- a/make/libmagickwand-7.mk
883+++ b/make/libmagickwand-7.mk
884@@ -14,7 +14,7 @@ LOCAL_C_INCLUDES := \
885
886 # always ignored in static builds
887 ifneq ($(STATIC_BUILD),true)
888- LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -lz
889+ LOCAL_LDLIBS := -llog -lz
890 endif
891
892 LOCAL_SRC_FILES := \
893@@ -54,6 +54,29 @@ ifeq ($(OPENCL_BUILD),true)
894 LOCAL_SHARED_LIBRARIES += libopencl
895 endif
896
897+LOCAL_SHARED_LIBRARIES += libstdc++
898+
899+ifeq ($(TARGET_ARCH_ABI),arm64-v8a)
900+ LOCAL_EXPORT_C_INCLUDES += $(IMAGE_MAGICK)/configs/arm64
901+ LOCAL_C_INCLUDES += $(IMAGE_MAGICK)/configs/arm64
902+else ifeq ($(TARGET_ARCH_ABI),armeabi-v7a)
903+ LOCAL_EXPORT_C_INCLUDES += $(IMAGE_MAGICK)/configs/arm
904+ LOCAL_C_INCLUDES += $(IMAGE_MAGICK)/configs/arm
905+else ifeq ($(TARGET_ARCH_ABI),x86)
906+ LOCAL_EXPORT_C_INCLUDES += $(IMAGE_MAGICK)/configs/x86
907+ LOCAL_C_INCLUDES += $(IMAGE_MAGICK)/configs/x86
908+else ifeq ($(TARGET_ARCH_ABI),x86_64)
909+ LOCAL_EXPORT_C_INCLUDES += $(IMAGE_MAGICK)/configs/x86-64
910+ LOCAL_C_INCLUDES += $(IMAGE_MAGICK)/configs/x86-64
911+
912+ ifneq ($(STATIC_BUILD),true)
913+ LOCAL_LDFLAGS += -latomic
914+ endif
915+
916+endif
917+
918+LOCAL_EXPORT_C_INCLUDES += $(IMAGE_MAGICK)
919+
920 ifeq ($(BUILD_MAGICKWAND),true)
921 ifeq ($(STATIC_BUILD),true)
922 LOCAL_STATIC_LIBRARIES := \
923diff --git a/make/libpng.mk b/make/libpng.mk
924index 24fb8ac..dda05fd 100644
925--- a/make/libpng.mk
926+++ b/make/libpng.mk
927@@ -30,6 +30,7 @@ ifeq ($(TARGET_ARCH_ABI), arm64-v8a)
928 endif # TARGET_ARCH_ABI == arm64-v8a
929
930
931+LOCAL_EXPORT_C_INCLUDES := $(PNG_LIB_PATH)
932 LOCAL_C_INCLUDES := $(PNG_LIB_PATH)
933
934 LOCAL_SRC_FILES += \
935diff --git a/make/libtiff.mk b/make/libtiff.mk
936index ca43f25..2b17508 100644
937--- a/make/libtiff.mk
938+++ b/make/libtiff.mk
939@@ -12,6 +12,9 @@ LOCAL_C_INCLUDES := \
940 $(LZMA_LIB_PATH)/liblzma/api \
941 $(WEBP_LIB_PATH)/src
942
943+LOCAL_EXPORT_C_INCLUDES := \
944+ $(TIFF_LIB_PATH)
945+
946 ifeq ($(LIBLZMA_ENABLED),true)
947 LOCAL_CFLAGS += -DLZMA_SUPPORT=1
948 endif
949diff --git a/make/magick.mk b/make/magick.mk
950index 3ba4b1d..5471608 100644
951--- a/make/magick.mk
952+++ b/make/magick.mk
953@@ -18,7 +18,7 @@ LOCAL_C_INCLUDES := \
954 $(FREETYPE_LIB_PATH)/include
955
956
957-LOCAL_LDLIBS := -L$(SYSROOT)/usr/lib -llog -lz
958+LOCAL_LDLIBS := -llog -lz
959 LOCAL_SRC_FILES := \
960 $(IMAGE_MAGICK)/utilities/magick.c \
961
962
963
964This file is part of GNU Emacs.
965
966GNU Emacs is free software: you can redistribute it and/or modify
967it under the terms of the GNU General Public License as published by
968the Free Software Foundation, either version 3 of the License, or
969(at your option) any later version.
970
971GNU Emacs is distributed in the hope that it will be useful,
972but WITHOUT ANY WARRANTY; without even the implied warranty of
973MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
974GNU General Public License for more details.
975
976You should have received a copy of the GNU General Public License
977along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
diff --git a/java/Makefile.in b/java/Makefile.in
new file mode 100644
index 00000000000..804d4669c7a
--- /dev/null
+++ b/java/Makefile.in
@@ -0,0 +1,337 @@
1### @configure_input@
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
10# (at 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
20top_builddir = @top_builddir@
21top_srcdir = @top_srcdir@
22srcdir = @srcdir@
23builddir = @builddir@
24version = @version@
25
26# Don't install movemail if mailutils are to be used.
27emacs_use_mailutils = @emacs_use_mailutils@
28
29# This is the host lib-src and lib, not the cross compiler's lib-src.
30libsrc = ../lib-src
31EXEEXT = @EXEEXT@
32
33-include ${top_builddir}/src/verbose.mk
34
35SHELL = @SHELL@
36JAVAC = @JAVAC@
37AAPT = @AAPT@
38D8 = @D8@
39ZIPALIGN = @ZIPALIGN@
40JARSIGNER = @JARSIGNER@
41APKSIGNER = @APKSIGNER@
42JARSIGNER_FLAGS =
43ANDROID_JAR = @ANDROID_JAR@
44ANDROID_ABI = @ANDROID_ABI@
45ANDROID_SDK_18_OR_EARLIER = @ANDROID_SDK_18_OR_EARLIER@
46ANDROID_SDK_8_OR_EARLIER = @ANDROID_SDK_8_OR_EARLIER@
47WARN_JAVAFLAGS = @WARN_JAVAFLAGS@
48JAVAFLAGS = $(WARN_JAVAFLAGS) -classpath "$(ANDROID_JAR):$(srcdir)"
49FIND_DELETE = @FIND_DELETE@
50
51# Android 4.3 and earlier require Emacs to be signed with a different
52# digital signature algorithm.
53
54ifneq (,$(ANDROID_SDK_18_OR_EARLIER))
55JARSIGNER_FLAGS = -sigalg MD5withRSA -digestalg SHA1
56else
57JARSIGNER_FLAGS =
58endif
59
60# When building Emacs for Android 2.2, assets must not be compressed.
61# Otherwise, the asset manager fails to extract files larger than 1
62# MB.
63
64ifneq (,$(ANDROID_SDK_8_OR_EARLIER))
65AAPT_ASSET_ARGS = -0 ""
66else
67AAPT_ASSET_ARGS =
68endif
69
70SIGN_EMACS = -keystore $(srcdir)/emacs.keystore -storepass \
71 emacs1 $(JARSIGNER_FLAGS)
72SIGN_EMACS_V2 = sign --v2-signing-enabled --ks \
73 $(srcdir)/emacs.keystore -debuggable-apk-permitted \
74 --ks-pass pass:emacs1
75
76JAVA_FILES := $(wildcard $(srcdir)/org/gnu/emacs/*.java)
77RESOURCE_FILES := $(foreach file,$(wildcard $(srcdir)/res/*), \
78 $(wildcard $(file)/*))
79
80# R.java is a file generated by the `aapt' utility containing
81# constants that can then be used to locate ``resource identifiers''.
82# It is not a regular file and should not be compiled as Java source
83# code. Instead, it is automatically included by the Java compiler.
84RESOURCE_FILE := $(srcdir)/org/gnu/emacs/R.java
85
86# CLASS_FILES is what should actually be built and included in the
87# resulting Emacs executable. The Java compiler might generate more
88# than one class file for each source file, so this only serves as a
89# list of dependencies for Make.
90CLASS_FILES := $(foreach file,$(JAVA_FILES),$(basename $(file)).class)
91
92# Remove RESOURCE_FILE from JAVA_FILES, if it is already present.
93JAVA_FILES := $(filter-out $(RESOURCE_FILE),$(JAVA_FILES))
94
95# Compute the name for the Emacs application package. This should be:
96# emacs-<version>-<min-sdk>-<abi>.apk
97
98ANDROID_MIN_SDK := @ANDROID_MIN_SDK@
99APK_NAME := emacs-$(version)-$(ANDROID_MIN_SDK)-$(ANDROID_ABI).apk
100
101# How this stuff works.
102
103# emacs.apk depends on emacs.apk-in, which is simply a ZIP archive
104# containing the following files:
105# lib/$(ANDROID_ABI)/libemacs.so
106# lib/$(ANDROID_ABI)/libandroid-emacs.so
107# lib/$(ANDROID_ABI)/libctags.so
108# lib/$(ANDROID_ABI)/libetags.so
109# lib/$(ANDROID_ABI)/libhexl.so
110# lib/$(ANDROID_ABI)/libmovemail.so
111# lib/$(ANDROID_ABI)/librcs2log.so
112# lib/$(ANDROID_ABI)/libebrowse.so
113# assets/info/
114# assets/etc/
115# assets/lisp/
116
117.PHONY: emacs.apk-in all
118all: $(APK_NAME)
119
120# Binaries to cross-compile.
121CROSS_SRC_BINS := $(top_builddir)/cross/src/android-emacs
122CROSS_LIBSRC_BINS := $(top_builddir)/cross/lib-src/ctags \
123 $(top_builddir)/cross/lib-src/hexl \
124 $(top_builddir)/cross/lib-src/ebrowse \
125 $(top_builddir)/cross/lib-src/emacsclient \
126 $(top_builddir)/cross/lib-src/etags
127CROSS_LIBSRC_BINS_MOVEMAIL := $(top_builddir)/cross/lib-src/movemail
128CROSS_EXEC_BINS := $(top_builddir)/exec/exec1 $(top_builddir)/exec/loader
129CROSS_BINS = $(CROSS_SRC_BINS) $(CROSS_LIBSRC_BINS) $(CROSS_EXEC_BINS)
130
131ifneq ($(emacs_use_mailutils),yes)
132CROSS_LIBSRC_BINS := $(CROSS_LIBSRC_BINS) $(CROSS_LIBSRC_BINS_MOVEMAIL)
133endif
134
135# Libraries to cross-compile.
136CROSS_LIBS = $(top_builddir)/cross/src/libemacs.so
137
138# Make sure gnulib is built first!
139# If not, then the recursive invocations of make below will try to
140# build gnulib at the same time.
141CROSS_ARCHIVES = $(top_builddir)/cross/lib/libgnu.a
142
143# Third party libraries to compile.
144-include $(top_builddir)/cross/ndk-build/ndk-build.mk
145
146.PHONY: $(CROSS_BINS) $(CROSS_LIBS) $(CROSS_ARCHIVES)
147
148# There should only be a single invocation of $(MAKE) -C
149# $(top_srcdir)/cross for each directory under $(top_srcdir)/cross.
150$(CROSS_SRC_BINS) $(CROSS_LIBS) &: $(CROSS_ARCHIVES)
151 $(MAKE) -C $(top_builddir)/cross $(foreach file, \
152 $(CROSS_SRC_BINS) \
153 $(CROSS_LIBS), \
154 src/$(notdir $(file)))
155
156$(CROSS_LIBSRC_BINS) &: $(CROSS_ARCHIVES)
157 $(MAKE) -C $(top_builddir)/cross $(foreach file, \
158 $(CROSS_LIBSRC_BINS), \
159 lib-src/$(notdir $(file)))
160
161$(CROSS_ARCHIVES):
162 $(MAKE) -C $(top_builddir)/cross lib/libgnu.a
163
164# These two binaries are helpers used to execute binaries on Android
165# 10 and later.
166
167$(CROSS_EXEC_BINS) &:
168 $(MAKE) -C $(top_builddir)/exec $(notdir $(CROSS_EXEC_BINS))
169
170# This is needed to generate the ``.directory-tree'' file used by the
171# Android emulations of readdir and faccessat.
172
173$(libsrc)/asset-directory-tool:
174 $(MAKE) -C $(libsrc) $(notdir $@)
175
176# install_tmp is a directory used to generate emacs.apk-in.
177# That is then packaged into $(APK_NAME).
178# There is no need to depend on NDK_BUILD_SHARED as libemacs.so
179# does already.
180
181.PHONY: install_temp install_temp/assets/directory-tree
182install_temp: $(CROSS_BINS) $(CROSS_LIBS) $(RESOURCE_FILES)
183 $(AM_V_GEN)
184# Make the working directory for this stuff
185 $(AM_V_SILENT) rm -rf install_temp
186 $(AM_V_SILENT) mkdir -p install_temp/lib/$(ANDROID_ABI)
187 $(AM_V_SILENT) mkdir -p install_temp/assets/etc
188 $(AM_V_SILENT) mkdir -p install_temp/assets/lisp
189 $(AM_V_SILENT) mkdir -p install_temp/assets/info
190# Install architecture independents to assets/etc and assets/lisp
191 $(AM_V_SILENT) cp -r $(top_srcdir)/lisp install_temp/assets
192 $(AM_V_SILENT) cp -r $(top_srcdir)/etc install_temp/assets
193 $(AM_V_SILENT) cp -r $(top_srcdir)/info install_temp/assets
194# Replace etc/DOC generated by compiling Emacs for the build machine
195# with etc/DOC from the cross-compiled Emacs.
196 $(AM_V_SILENT) test -f $(top_builddir)/cross/etc/DOC \
197 && cp -r $(top_builddir)/cross/etc/DOC \
198 install_temp/assets/etc
199# Remove undesirable files from those directories.
200 $(AM_V_SILENT) \
201 for subdir in `find install_temp -type d -print`; do \
202 chmod a+rx $${subdir} ; \
203 rm -rf $${subdir}/.gitignore ; \
204 rm -rf $${subdir}/.DS_Store ; \
205 rm -rf $${subdir}/#* ; \
206 rm -rf $${subdir}/.#* ; \
207 rm -rf $${subdir}/*~ ; \
208 rm -rf $${subdir}/*.orig ; \
209 rm -rf $${subdir}/ChangeLog* ; \
210 rm -rf $${subdir}/[mM]akefile*[.-]in ; \
211 rm -rf $${subdir}/Makefile; \
212 done
213# Generate the directory tree for those directories.
214# Install architecture dependents to lib/$(ANDROID_ABI). This
215# perculiar naming scheme is required to make Android preserve these
216# binaries upon installation.
217 $(AM_V_SILENT) \
218 for file in $(CROSS_BINS); do \
219 if [ -x $$file ]; then \
220 filename=`basename $$file`; \
221 cp -f $$file install_temp/lib/$(ANDROID_ABI)/lib$${filename}.so; \
222 fi \
223 done
224 $(AM_V_SILENT) \
225 for file in $(CROSS_LIBS); do \
226 if [ -x $$file ]; then \
227 cp -f $$file install_temp/lib/$(ANDROID_ABI); \
228 fi \
229 done
230ifneq ($(NDK_BUILD_SHARED),)
231 $(AM_V_SILENT) cp -f $(NDK_BUILD_SHARED) \
232 install_temp/lib/$(ANDROID_ABI)
233endif
234
235install_temp/assets/directory-tree: $(libsrc)/asset-directory-tool \
236 install_temp install_temp/assets/version \
237 install_temp/assets/build_info
238 $(AM_V_GEN) $(libsrc)/asset-directory-tool install_temp/assets \
239 install_temp/assets/directory-tree
240
241install_temp/assets/version: install_temp
242 $(AM_V_GEN) { (cd $(top_srcdir) \
243 && git rev-parse HEAD || echo "Unknown") \
244 && (git rev-parse --abbrev-ref HEAD \
245 || echo "Unknown") } 2> /dev/null > $@
246
247install_temp/assets/build_info: install_temp
248 $(AM_V_GEN) { hostname; date +%s; } > $@
249
250emacs.apk-in: install_temp install_temp/assets/directory-tree \
251 AndroidManifest.xml install_temp/assets/version \
252 install_temp/assets/build_info
253# Package everything. Specifying the assets on this command line is
254# necessary for AAssetManager_getNextFileName to work on old versions
255# of Android. Make sure not to generate R.java, as it's already been
256# generated.
257 $(AM_V_AAPT) $(AAPT) p -I "$(ANDROID_JAR)" -F $@ \
258 -f -M AndroidManifest.xml $(AAPT_ASSET_ARGS) \
259 -A install_temp/assets \
260 -S $(top_srcdir)/java/res -J install_temp
261 $(AM_V_SILENT) pushd install_temp &> /dev/null; \
262 $(AAPT) add ../$@ `find lib -type f`; \
263 popd &> /dev/null
264 $(AM_V_SILENT) rm -rf install_temp
265
266# Makefile itself.
267.PRECIOUS: $(top_srcdir)/config.status Makefile
268$(top_srcdir)/config.status: $(top_srcdir)/configure.ac $(top_srcdir)/m4/*.m4
269 $(MAKE) -C $(dir $@) $(notdir $@)
270Makefile: $(top_srcdir)/config.status $(top_srcdir)/java/Makefile.in
271 $(MAKE) -C .. java/$@
272
273# AndroidManifest.xml:
274AndroidManifest.xml: $(top_srcdir)/configure.ac $(top_srcdir)/m4/*.m4 \
275 $(srcdir)/AndroidManifest.xml.in
276 pushd ..; ./config.status java/AndroidManifest.xml; popd
277
278# R.java:
279$(RESOURCE_FILE): $(RESOURCE_FILES)
280 $(AM_V_GEN) $(AAPT) p -I "$(ANDROID_JAR)" -f \
281 -J $(dir $@) -M AndroidManifest.xml \
282 -S $(top_srcdir)/java/res
283
284# Make all class files depend on R.java being built.
285$(CLASS_FILES): $(RESOURCE_FILE)
286
287.SUFFIXES: .java .class
288$(CLASS_FILES) &: $(JAVA_FILES)
289 $(AM_V_JAVAC) $(JAVAC) $(JAVAFLAGS) $(JAVA_FILES)
290 $(AM_V_SILENT) touch $(CLASS_FILES)
291
292# N.B. that find must be called all over again in case javac generated
293# nested classes.
294
295classes.dex: $(CLASS_FILES)
296 $(AM_V_D8) $(D8) --classpath $(ANDROID_JAR) \
297 $(subst $$,\$$,$(shell find $(srcdir) -type f \
298 -name *.class)) --output $(builddir)
299
300# When emacs.keystore expires, regenerate it with:
301#
302# keytool -genkey -v -keystore emacs.keystore -alias "Emacs keystore" \
303# -keyalg RSA -sigalg SHA1withRSA -keysize 2048 -validity 100000
304
305.PHONY: clean maintainer-clean
306
307$(APK_NAME): classes.dex emacs.apk-in $(srcdir)/emacs.keystore
308 $(AM_V_GEN)
309 $(AM_V_SILENT) cp -f emacs.apk-in $@.unaligned
310 $(AM_V_SILENT) $(AAPT) add $@.unaligned classes.dex
311 $(AM_V_SILENT) $(JARSIGNER) $(SIGN_EMACS) $@.unaligned "Emacs keystore"
312 $(AM_V_SILENT) $(ZIPALIGN) -f 4 $@.unaligned $@
313# Signing must happen after alignment!
314 $(AM_V_SILENT) $(APKSIGNER) $(SIGN_EMACS_V2) $@
315 $(AM_V_SILENT) rm -f $@.unaligned *.idsig
316
317# TAGS generation.
318
319ETAGS = $(top_builddir)/lib-src/etags
320
321$(ETAGS): FORCE
322 $(MAKE) -C ../lib-src $(notdir $@)
323
324tagsfiles = $(JAVA_FILES) $(RESOURCE_FILE)
325
326.PHONY: tags FORCE
327tags: TAGS
328TAGS: $(ETAGS) $(tagsfiles)
329 $(AM_V_GEN) $(ETAGS) $(tagsfiles)
330
331clean:
332 rm -f *.apk emacs.apk-in *.dex *.unaligned *.class *.idsig
333 rm -rf install-temp $(RESOURCE_FILE) TAGS
334 find . -name '*.class' $(FIND_DELETE)
335
336maintainer-clean distclean bootstrap-clean: clean
337 rm -f Makefile ndk-build.mk
diff --git a/java/README b/java/README
new file mode 100644
index 00000000000..a6adb805b9e
--- /dev/null
+++ b/java/README
@@ -0,0 +1,1049 @@
1This directory holds the Java sources of the port of GNU Emacs to
2Android-like systems, along with files needed to create an application
3package out of them. If you need to build this port, please read the
4file INSTALL in this directory.
5
6The ``org/gnu/emacs'' subdirectory contains the Java sources under the
7``org.gnu.emacs'' package identifier.
8
9``AndroidManifest.xml'' contains a manifest describing the Java
10sources to the system.
11
12The ``res'' directory contains resources, mainly the Emacs icon and
13several ``boolean resources'' which are used as a form of conditional
14evaluation for manifest entries.
15
16`emacs.keystore' is the signing key used to build Emacs. It is kept
17here, and we encourage all people redistributing Emacs to use this
18key. It holds no security value, and otherwise it will be impossible
19to install different builds of Emacs on top of each other.
20
21Please keep the Java code indented with tabs and formatted according
22to the rules for C code in the GNU coding standards. Always use
23C-style comments.
24
25======================================================================
26
27OVERVIEW OF JAVA
28
29Emacs developers do not know Java, and there is no reason they should
30have to. Thus, the code in this directory is confined to what is
31strictly necessary to support Emacs, and only uses a subset of Java
32written in a way that is easily understandable to C programmers.
33
34Java is required because the entire Android runtime is based around
35Java, and there is no way to write an Android program which runs
36without Java.
37
38This text exists to prime other Emacs developers, already familar with
39C, on the basic architecture of the Android port, and to teach them
40how to read and write the Java code found in this directory.
41
42Java is an object oriented language with automatic memory management
43compiled down to bytecode, which is then subject to interpretation by
44a Java virtual machine.
45
46What that means, is that:
47
48struct emacs_window
49{
50 int some_fields;
51 int of_emacs_window;
52};
53
54static void
55do_something_with_emacs_window (struct emacs_window *a, int n)
56{
57 a->some_fields = a->of_emacs_window + n;
58}
59
60would be written:
61
62public class EmacsWindow
63{
64 public int someFields;
65 public int ofEmacsWindow;
66
67 public void
68 doSomething (int n)
69 {
70 someFields = ofEmacsWindow + n;
71 }
72}
73
74and instead of doing:
75
76do_something_with_emacs_window (my_window, 1);
77
78you say:
79
80myWindow.doSomething (1);
81
82In addition to functions associated with an object of a given class
83(such as EmacsWindow), Java also has two other kinds of functions.
84
85The first are so-called ``static'' functions (the static means
86something entirely different from what it does in C.)
87
88A static function, while still having to be defined within a class,
89can be called without any object. Instead of the object, you write
90the name of the Java class within which it is defined. For example,
91the following C code:
92
93int
94multiply_a_with_b_and_then_add_c (int a, int b, int c)
95{
96 return a * b + c;
97}
98
99would be:
100
101public class EmacsSomething
102{
103 public static int
104 multiplyAWithBAndThenAddC (int a, int b, int c)
105 {
106 return a * b + c;
107 }
108};
109
110Then, instead of calling:
111
112int foo;
113
114foo = multiply_a_with_b_then_add_c (1, 2, 3);
115
116you say:
117
118int foo;
119
120foo = EmacsSomething.multiplyAWithBAndThenAddC (1, 2, 3);
121
122In Java, ``static'' does not mean that the function is only used
123within its compilation unit! Instead, the ``private'' qualifier is
124used to mean more or less the same thing:
125
126static void
127this_procedure_is_only_used_within_this_file (void)
128{
129 do_something ();
130}
131
132becomes
133
134public class EmacsSomething
135{
136 private static void
137 thisProcedureIsOnlyUsedWithinThisClass ()
138 {
139
140 }
141}
142
143the other kind are called ``constructors''. They are functions that
144must be called to allocate memory to hold a class:
145
146public class EmacsFoo
147{
148 int bar;
149
150 public
151 EmacsFoo (int tokenA, int tokenB)
152 {
153 bar = tokenA + tokenB;
154 }
155}
156
157now, the following statement:
158
159EmacsFoo foo;
160
161foo = new EmacsFoo (1, 2);
162
163becomes more or less equivalent to the following C code:
164
165struct emacs_foo
166{
167 int bar;
168};
169
170struct emacs_foo *
171make_emacs_foo (int token_a, int token_b)
172{
173 struct emacs_foo *foo;
174
175 foo = xmalloc (sizeof *foo);
176 foo->bar = token_a + token_b;
177
178 return foo;
179}
180
181/* ... */
182
183struct emacs_foo *foo;
184
185foo = make_emacs_foo (1, 2);
186
187A class may have any number of constructors, or no constructors at
188all, in which case the compiler inserts an empty constructor.
189
190
191
192Sometimes, you will see Java code that looks like this:
193
194 allFiles = filesDirectory.listFiles (new FileFilter () {
195 @Override
196 public boolean
197 accept (File file)
198 {
199 return (!file.isDirectory ()
200 && file.getName ().endsWith (".pdmp"));
201 }
202 });
203
204This is Java's version of GCC's nested function extension. The major
205difference is that the nested function may still be called even after
206it goes out of scope, and always retains a reference to the class and
207local variables around where it was called.
208
209Being an object-oriented language, Java also allows defining that a
210class ``extends'' another class. The following C code:
211
212struct a
213{
214 long thirty_two;
215};
216
217struct b
218{
219 struct a a;
220 long long sixty_four;
221};
222
223extern void do_something (struct a *);
224
225void
226my_function (struct b *b)
227{
228 do_something (&b->a);
229}
230
231is roughly equivalent to the following Java code, split into two
232files:
233
234 A.java
235
236public class A
237{
238 int thirtyTwo;
239
240 public void
241 doSomething ()
242 {
243 etcEtcEtc ();
244 }
245};
246
247 B.java
248
249public class B extends A
250{
251 long sixty_four;
252
253 public static void
254 myFunction (B b)
255 {
256 b.doSomething ();
257 }
258}
259
260the Java runtime has transformed the call to ``b.doSomething'' to
261``((A) b).doSomething''.
262
263However, Java also allows overriding this behavior, by specifying the
264@Override keyword:
265
266public class B extends A
267{
268 long sixty_four;
269
270 @Override
271 public void
272 doSomething ()
273 {
274 Something.doSomethingTwo ();
275 super.doSomething ();
276 }
277}
278
279now, any call to ``doSomething'' on a ``B'' created using ``new B ()''
280will end up calling ``Something.doSomethingTwo'', before calling back
281to ``A.doSomething''. This override also applies in reverse; that is
282to say, even if you write:
283
284 ((A) b).doSomething ();
285
286B's version of doSomething will still be called, if ``b'' was created
287using ``new B ()''.
288
289This mechanism is used extensively throughout the Java language and
290Android windowing APIs.
291
292Elsewhere, you will encounter Java code that defines arrays:
293
294public class EmacsFrobinicator
295{
296 public static void
297 emacsFrobinicate (int something)
298 {
299 int[] primesFromSomething;
300
301 primesFromSomething = new int[numberOfPrimes];
302 /* ... */
303 }
304}
305
306Java arrays are similar to C arrays in that they can not grow. But
307they are very much unlike C arrays in that they are always references
308(as opposed to decaying into pointers in only some situations), and
309contain information about their length.
310
311If another function named ``frobinicate1'' takes an array as an
312argument, then it need not take the length of the array.
313
314Instead, it may simply iterate over the array like so:
315
316int i, k;
317
318for (i = 0; i < array.length; ++i)
319 {
320 k = array[i];
321
322 Whatever.doSomethingWithK (k);
323 }
324
325The syntax used to define arrays is also slightly different. As
326arrays are always references, there is no way for you to tell the
327runtime to allocate an array of size N in a structure (class.)
328
329Instead, if you need an array of that size, you must declare a field
330with the type of the array, and allocate the array inside the class's
331constructor, like so:
332
333public class EmacsArrayContainer
334{
335 public int[] myArray;
336
337 public
338 EmacsArrayContainer ()
339 {
340 myArray = new array[10];
341 }
342}
343
344while in C, you could just have written:
345
346struct emacs_array_container
347{
348 int my_array[10];
349};
350
351or, possibly even better,
352
353typedef int emacs_array_container[10];
354
355Alas, Java has no equivalent of `typedef'.
356
357Like in C, Java string literals are delimited by double quotes.
358Unlike C, however, strings are not NULL-terminated arrays of
359characters, but a distinct type named ``String''. They store their
360own length, characters in Java's 16-bit ``char'' type, and are capable
361of holding NULL bytes.
362
363Instead of writing:
364
365wchar_t character;
366extern char *s;
367size_t s;
368
369 for (/* determine n, s in a loop. */)
370 s += mbstowc (&character, s, n);
371
372or:
373
374const char *byte;
375
376for (byte = my_string; *byte; ++byte)
377 /* do something with *byte. */;
378
379or perhaps even:
380
381size_t length, i;
382char foo;
383
384length = strlen (my_string);
385
386for (i = 0; i < length; ++i)
387 foo = my_string[i];
388
389you write:
390
391char foo;
392int i;
393
394for (i = 0; i < myString.length (); ++i)
395 foo = myString.charAt (0);
396
397Java also has stricter rules on what can be used as a truth value in a
398conditional. While in C, any non-zero value is true, Java requires
399that every truth value be of the boolean type ``boolean''.
400
401What this means is that instead of simply writing:
402
403 if (foo || bar)
404
405where foo can either be 1 or 0, and bar can either be NULL or a
406pointer to something, you must explicitly write:
407
408 if (foo != 0 || bar != null)
409
410in Java.
411
412JAVA NATIVE INTERFACE
413
414Java also provides an interface for C code to interface with Java.
415
416C functions exported from a shared library become static Java
417functions within a class, like so:
418
419public class EmacsNative
420{
421 /* Obtain the fingerprint of this build of Emacs. The fingerprint
422 can be used to determine the dump file name. */
423 public static native String getFingerprint ();
424
425 /* Set certain parameters before initializing Emacs.
426
427 assetManager must be the asset manager associated with the
428 context that is loading Emacs. It is saved and remains for the
429 remainder the lifetime of the Emacs process.
430
431 filesDir must be the package's data storage location for the
432 current Android user.
433
434 libDir must be the package's data storage location for native
435 libraries. It is used as PATH.
436
437 cacheDir must be the package's cache directory. It is used as
438 the `temporary-file-directory'.
439
440 pixelDensityX and pixelDensityY are the DPI values that will be
441 used by Emacs.
442
443 classPath must be the classpath of this app_process process, or
444 NULL.
445
446 emacsService must be the EmacsService singleton, or NULL. */
447 public static native void setEmacsParams (AssetManager assetManager,
448 String filesDir,
449 String libDir,
450 String cacheDir,
451 float pixelDensityX,
452 float pixelDensityY,
453 String classPath,
454 EmacsService emacsService);
455}
456
457Where the corresponding C functions are located in android.c, and
458loaded by the special invocation:
459
460 static
461 {
462 System.loadLibrary ("emacs");
463 };
464
465where ``static'' defines a section of code which will be run upon the
466object (containing class) being loaded. This is like:
467
468 __attribute__((constructor))
469
470on systems where shared object constructors are supported.
471
472See http://docs.oracle.com/en/java/javase/19/docs/specs/jni/intro.html
473for more details.
474
475
476
477OVERVIEW OF ANDROID
478
479When the Android system starts an application, it does not actually
480call the application's ``main'' function. It may not even start the
481application's process if one is already running.
482
483Instead, Android is organized around components. When the user opens
484the ``Emacs'' icon, the Android system looks up and starts the
485component associated with the ``Emacs'' icon. In this case, the
486component is called an activity, and is declared in
487the AndroidManifest.xml in this directory:
488
489 <activity android:name="org.gnu.emacs.EmacsActivity"
490 android:launchMode="singleTop"
491 android:windowSoftInputMode="adjustResize"
492 android:exported="true"
493 android:configChanges="orientation|screenSize|screenLayout|keyboardHidden">
494 <intent-filter>
495 <action android:name="android.intent.action.MAIN" />
496 <category android:name="android.intent.category.DEFAULT" />
497 <category android:name="android.intent.category.LAUNCHER" />
498 </intent-filter>
499 </activity>
500
501This tells Android to start the activity defined in ``EmacsActivity''
502(defined in org/gnu/emacs/EmacsActivity.java), a class extending the
503Android class ``Activity''.
504
505To do so, the Android system creates an instance of ``EmacsActivity''
506and the window system window associated with it, and eventually calls:
507
508 Activity activity;
509
510 activity.onCreate (...);
511
512But which ``onCreate'' is really called?
513It is actually the ``onCreate'' defined in EmacsActivity.java, as
514it overrides the ``onCreate'' defined in Android's own Activity class:
515
516 @Override
517 public void
518 onCreate (Bundle savedInstanceState)
519 {
520 FrameLayout.LayoutParams params;
521 Intent intent;
522
523Then, this is what happens step-by-step within the ``onCreate''
524function:
525
526 /* See if Emacs should be started with -Q. */
527 intent = getIntent ();
528 EmacsService.needDashQ
529 = intent.getBooleanExtra ("org.gnu.emacs.START_DASH_Q",
530 false);
531
532Here, Emacs obtains the intent (a request to start a component) which
533was used to start Emacs, and sets a special flag if it contains a
534request for Emacs to start with the ``-Q'' command-line argument.
535
536 /* Set the theme to one without a title bar. */
537
538 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
539 setTheme (android.R.style.Theme_DeviceDefault_NoActionBar);
540 else
541 setTheme (android.R.style.Theme_NoTitleBar);
542
543Next, Emacs sets an appropriate theme for the activity's associated
544window decorations.
545
546 params = new FrameLayout.LayoutParams (LayoutParams.MATCH_PARENT,
547 LayoutParams.MATCH_PARENT);
548
549 /* Make the frame layout. */
550 layout = new FrameLayout (this);
551 layout.setLayoutParams (params);
552
553 /* Set it as the content view. */
554 setContentView (layout);
555
556Then, Emacs creates a ``FrameLayout'', a widget that holds a single
557other widget, and makes it the activity's ``content view''.
558
559The activity itself is a ``FrameLayout'', so the ``layout parameters''
560here apply to the FrameLayout itself, and not its children.
561
562 /* Maybe start the Emacs service if necessary. */
563 EmacsService.startEmacsService (this);
564
565And after that, Emacs calls the static function ``startEmacsService'',
566defined in the class ``EmacsService''. This starts the Emacs service
567component if necessary.
568
569 /* Add this activity to the list of available activities. */
570 EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this);
571
572 super.onCreate (savedInstanceState);
573
574Finally, Emacs registers that this activity is now ready to receive
575top-level frames (windows) created from Lisp.
576
577Activities come and go, but Emacs has to stay running in the mean
578time. Thus, Emacs also defines a ``service'', which is a long-running
579component that the Android system allows to run in the background.
580
581Let us go back and review the definition of ``startEmacsService'':
582
583 public static void
584 startEmacsService (Context context)
585 {
586 if (EmacsService.SERVICE == null)
587 {
588 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
589 /* Start the Emacs service now. */
590 context.startService (new Intent (context,
591 EmacsService.class));
592 else
593 /* Display the permanant notification and start Emacs as a
594 foreground service. */
595 context.startForegroundService (new Intent (context,
596 EmacsService.class));
597 }
598 }
599
600If ``EmacsService.SERVICE'' does not yet exist, what this does is to
601tell the ``context'' (the equivalent of an Xlib Display *) to start a
602service defined by the class ``EmacsService''. Eventually, this
603results in ``EmacsService.onCreate'' being called:
604
605 @Override
606 public void
607 onCreate ()
608 {
609 AssetManager manager;
610 Context app_context;
611 String filesDir, libDir, cacheDir, classPath;
612 double pixelDensityX;
613 double pixelDensityY;
614
615Here is what this function does, step-by-step:
616
617 SERVICE = this;
618
619First, it sets the special static variable ``SERVICE'' to ``this'',
620which is a pointer to the ``EmacsService' object that was created.
621
622 handler = new Handler (Looper.getMainLooper ());
623
624Next, it creates a ``Handler'' object for the ``main looper''.
625This is a helper structure which allows executing code on the Android
626user interface thread.
627
628 manager = getAssets ();
629 app_context = getApplicationContext ();
630 metrics = getResources ().getDisplayMetrics ();
631 pixelDensityX = metrics.xdpi;
632 pixelDensityY = metrics.ydpi;
633
634Finally, it obtains:
635
636 - the asset manager, which is used to retrieve assets packaged
637 into the Emacs application package.
638
639 - the application context, used to obtain application specific
640 information.
641
642 - the display metrics, and from them, the X and Y densities in dots
643 per inch.
644
645Then, inside a ``try'' block:
646
647 try
648 {
649 /* Configure Emacs with the asset manager and other necessary
650 parameters. */
651 filesDir = app_context.getFilesDir ().getCanonicalPath ();
652 libDir = getLibraryDirectory ();
653 cacheDir = app_context.getCacheDir ().getCanonicalPath ();
654
655It obtains the names of the Emacs home, shared library, and temporary
656file directories.
657
658 /* Now provide this application's apk file, so a recursive
659 invocation of app_process (through android-emacs) can
660 find EmacsNoninteractive. */
661 classPath = getApkFile ();
662
663The name of the Emacs application package.
664
665 Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir
666 + ", libDir = " + libDir + ", and classPath = " + classPath);
667
668Prints a debug message to the Android system log with this
669information.
670
671 EmacsNative.setEmacsParams (manager, filesDir, libDir,
672 cacheDir, (float) pixelDensityX,
673 (float) pixelDensityY,
674 classPath, this);
675
676And calls the native function ``setEmacsParams'' (defined in
677android.c) to configure Emacs with this information.
678
679 /* Start the thread that runs Emacs. */
680 thread = new EmacsThread (this, needDashQ);
681 thread.start ();
682
683Then, it allocates an ``EmacsThread'' object, and starts that thread.
684Inside that thread is where Emacs's C code runs.
685
686 }
687 catch (IOException exception)
688 {
689 EmacsNative.emacsAbort ();
690 return;
691
692And here is the purpose of the ``try'' block. Functions related to
693file names in Java will signal errors of various types upon failure.
694
695This ``catch'' block means that the Java virtual machine will abort
696execution of the contents of the ``try'' block as soon as an error of
697type ``IOException'' is encountered, and begin executing the contents
698of the ``catch'' block.
699
700Any failure of that type here is a crash, and
701``EmacsNative.emacsAbort'' is called to quickly abort the process to
702get a useful backtrace.
703 }
704 }
705
706Now, let us look at the definition of the class ``EmacsThread'', found
707in org/gnu/emacs/EmacsThread.java:
708
709public class EmacsThread extends Thread
710{
711 /* Whether or not Emacs should be started -Q. */
712 private boolean startDashQ;
713
714 public
715 EmacsThread (EmacsService service, boolean startDashQ)
716 {
717 super ("Emacs main thread");
718 this.startDashQ = startDashQ;
719 }
720
721 @Override
722 public void
723 run ()
724 {
725 String args[];
726
727 if (!startDashQ)
728 args = new String[] { "libandroid-emacs.so", };
729 else
730 args = new String[] { "libandroid-emacs.so", "-Q", };
731
732 /* Run the native code now. */
733 EmacsNative.initEmacs (args, EmacsApplication.dumpFileName);
734 }
735};
736
737The class itself defines a single field, ``startDashQ'', a constructor
738with an unused argument of the type ``EmacsService'' (which is useful
739while debugging) and a flag ``startDashQ'', and a single function
740``run'', overriding the same function in the class ``Thread''.
741
742When ``thread.start'' is called, the Java virtual machine creates a
743new thread, and then calls the function ``run'' within that thread.
744
745This function then computes a suitable argument vector, and calls
746``EmacsNative.initEmacs'' (defined in android.c), which then calls a
747modified version of the regular Emacs ``main'' function.
748
749At that point, Emacs initialization proceeds as usual:
750Vinitial_window_system is set, loadup.el calls `normal-top-level',
751which calls `command-line', and finally
752`window-system-initialization', which initializes the `android'
753terminal interface as usual.
754
755What happens here is the same as on other platforms. Now, here is
756what happens when the initial frame is created: Fx_create_frame calls
757`android_create_frame_window' to create a top level window:
758
759static void
760android_create_frame_window (struct frame *f)
761{
762 struct android_set_window_attributes attributes;
763 enum android_window_value_mask attribute_mask;
764
765 attributes.background_pixel = FRAME_BACKGROUND_PIXEL (f);
766 attribute_mask = ANDROID_CW_BACK_PIXEL;
767
768 block_input ();
769 FRAME_ANDROID_WINDOW (f)
770 = android_create_window (FRAME_DISPLAY_INFO (f)->root_window,
771 f->left_pos,
772 f->top_pos,
773 FRAME_PIXEL_WIDTH (f),
774 FRAME_PIXEL_HEIGHT (f),
775 attribute_mask, &attributes);
776 unblock_input ();
777}
778
779This calls the function `android_create_window' with some arguments
780whose meanings are identical to the arguments to `XCreateWindow'.
781
782Here is the definition of `android_create_window', in android.c:
783
784android_window
785android_create_window (android_window parent, int x, int y,
786 int width, int height,
787 enum android_window_value_mask value_mask,
788 struct android_set_window_attributes *attrs)
789{
790 static jclass class;
791 static jmethodID constructor;
792 jobject object, parent_object, old;
793 android_window window;
794 android_handle prev_max_handle;
795 bool override_redirect;
796
797What does it do? First, some context:
798
799At any time, there can be at most 65535 Java objects referred to by
800the rest of Emacs through the Java native interface. Each such object
801is assigned a ``handle'' (similar to an XID on X) and given a unique
802type. The function `android_resolve_handle' returns the JNI `jobject'
803associated with a given handle.
804
805 parent_object = android_resolve_handle (parent, ANDROID_HANDLE_WINDOW);
806
807Here, it is being used to look up the `jobject' associated with the
808`parent' handle.
809
810 prev_max_handle = max_handle;
811 window = android_alloc_id ();
812
813Next, `max_handle' is saved, and a new handle is allocated for
814`window'.
815
816 if (!window)
817 error ("Out of window handles!");
818
819An error is signalled if Emacs runs out of available handles.
820
821 if (!class)
822 {
823 class = (*android_java_env)->FindClass (android_java_env,
824 "org/gnu/emacs/EmacsWindow");
825 assert (class != NULL);
826
827Then, if this initialization has not yet been completed, Emacs
828proceeds to find the Java class named ``EmacsWindow''.
829
830 constructor
831 = (*android_java_env)->GetMethodID (android_java_env, class, "<init>",
832 "(SLorg/gnu/emacs/EmacsWindow;"
833 "IIIIZ)V");
834 assert (constructor != NULL);
835
836And it tries to look up the constructor, which should take seven
837arguments:
838
839 S - a short. (the handle ID)
840 Lorg/gnu/Emacs/EmacsWindow; - an instance of the EmacsWindow
841 class. (the parent)
842 IIII - four ints. (the window geometry.)
843 Z - a boolean. (whether or not the
844 window is override-redirect; see
845 XChangeWindowAttributes.)
846
847 old = class;
848 class = (*android_java_env)->NewGlobalRef (android_java_env, class);
849 (*android_java_env)->ExceptionClear (android_java_env);
850 ANDROID_DELETE_LOCAL_REF (old);
851
852Next, it saves a global reference to the class and deletes the local
853reference. Global references will never be deallocated by the Java
854virtual machine as long as they still exist.
855
856 if (!class)
857 memory_full (0);
858 }
859
860 /* N.B. that ANDROID_CW_OVERRIDE_REDIRECT can only be set at window
861 creation time. */
862 override_redirect = ((value_mask
863 & ANDROID_CW_OVERRIDE_REDIRECT)
864 && attrs->override_redirect);
865
866 object = (*android_java_env)->NewObject (android_java_env, class,
867 constructor, (jshort) window,
868 parent_object, (jint) x, (jint) y,
869 (jint) width, (jint) height,
870 (jboolean) override_redirect);
871
872Then, it creates an instance of the ``EmacsWindow'' class with the
873appropriate arguments and previously determined constructor.
874
875 if (!object)
876 {
877 (*android_java_env)->ExceptionClear (android_java_env);
878
879 max_handle = prev_max_handle;
880 memory_full (0);
881
882If creating the object fails, Emacs clears the ``pending exception''
883and signals that it is out of memory.
884 }
885
886 android_handles[window].type = ANDROID_HANDLE_WINDOW;
887 android_handles[window].handle
888 = (*android_java_env)->NewGlobalRef (android_java_env,
889 object);
890 (*android_java_env)->ExceptionClear (android_java_env);
891 ANDROID_DELETE_LOCAL_REF (object);
892
893Otherwise, it associates a new global reference to the object with the
894handle, and deletes the local reference returned from the JNI
895NewObject function.
896
897 if (!android_handles[window].handle)
898 memory_full (0);
899
900If allocating the global reference fails, Emacs signals that it is out
901of memory.
902
903 android_change_window_attributes (window, value_mask, attrs);
904 return window;
905
906Otherwise, it applies the specified window attributes and returns the
907handle of the new window.
908}
909
910
911
912DRAWABLES, CURSORS AND HANDLES
913
914Each widget created by Emacs corresponds to a single ``window'', which
915has its own backing store. This arrangement is quite similar to X.
916
917C code does not directly refer to the EmacsView widgets that implement
918the UI logic behind windows. Instead, its handles refer to
919EmacsWindow structures, which contain the state necessary to interact
920with the widgets in an orderly and synchronized manner.
921
922Like X, both pixmaps and windows are drawable resources, and the same
923graphics operations can be applied to both. Thus, a separate
924EmacsPixmap structure is used to wrap around Android Bitmap resources,
925and the Java-level graphics operation functions are capable of
926operating on them both.
927
928Finally, graphics contexts are maintained on both the C and Java
929levels; the C state recorded in `struct android_gc' is kept in sync
930with the Java state in the GContext handle's corresponding EmacsGC
931structure, and cursors are used through handles that refer to
932EmacsCursor structures that hold system PointerIcons.
933
934In all cases, the interfaces provided are identical to X.
935
936
937
938EVENT LOOP
939
940In a typical Android application, the event loop is managed by the
941operating system, and callbacks (implemented through overriding
942separate functions in widgets) are run by the event loop wherever
943necessary. The thread which runs the event loop is also the only
944thread capable of creating and manipulating widgets and activities,
945and is referred to as the ``UI thread''.
946
947These callbacks are used by Emacs to write representations of X-like
948events to a separate event queue, which are then read from Emacs's own
949event loop running in a separate thread. This is accomplished through
950replacing `select' by a function which waits for the event queue to be
951occupied, in addition to any file descriptors that `select' would
952normally wait for.
953
954Conversely, Emacs's event loop sometimes needs to send events to the
955UI thread. These events are implemented as tiny fragments of code,
956which are run as they are received by the main thread.
957
958A typical example is `displayToast', which is implemented in
959EmacsService.java:
960
961 public void
962 displayToast (final String string)
963 {
964 runOnUiThread (new Runnable () {
965 @Override
966 public void
967 run ()
968 {
969 Toast toast;
970
971 toast = Toast.makeText (getApplicationContext (),
972 string, Toast.LENGTH_SHORT);
973 toast.show ();
974 }
975 });
976 }
977
978Here, the variable `string' is used by a nested function. This nested
979function contains a copy of that variable, and is run on the main
980thread using the function `runOnUiThread', in order to display a short
981status message on the display.
982
983When Emacs needs to wait for the nested function to finish, it uses a
984mechanism implemented in `syncRunnable'. This mechanism first calls a
985deadlock avoidance mechanism, then runs a nested function on the UI
986thread, which is expected to signal itself as a condition variable
987upon completion. It is typically used to allocate resources that can
988only be allocated from the UI thread, or to obtain non-thread-safe
989information. The following function is an example; it returns a new
990EmacsView widget corresponding to the provided window:
991
992 public EmacsView
993 getEmacsView (final EmacsWindow window, final int visibility,
994 final boolean isFocusedByDefault)
995 {
996 Runnable runnable;
997 final EmacsHolder<EmacsView> view;
998
999 view = new EmacsHolder<EmacsView> ();
1000
1001 runnable = new Runnable () {
1002 public void
1003 run ()
1004 {
1005 synchronized (this)
1006 {
1007 view.thing = new EmacsView (window);
1008 view.thing.setVisibility (visibility);
1009
1010 /* The following function is only present on Android 26
1011 or later. */
1012 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
1013 view.thing.setFocusedByDefault (isFocusedByDefault);
1014
1015 notify ();
1016 }
1017 }
1018 };
1019
1020 syncRunnable (runnable);
1021 return view.thing;
1022 }
1023
1024As no value can be directly returned from the nested function, a
1025separate container object is used to hold the result after the
1026function finishes execution. Note the type name inside the angle
1027brackets: this type is substituted into the class definition as it is
1028used; a definition such as:
1029
1030public class Foo<T>
1031{
1032 T bar;
1033};
1034
1035can not be used alone:
1036
1037 Foo holder; /* Error! */
1038
1039but must have a type specified:
1040
1041 Foo<Object> holder;
1042
1043in which case the effective definition is:
1044
1045public class Foo
1046{
1047 Object bar;
1048};
1049
diff --git a/java/debug.sh b/java/debug.sh
new file mode 100755
index 00000000000..d6e439bec90
--- /dev/null
+++ b/java/debug.sh
@@ -0,0 +1,368 @@
1#!/bin/bash
2### Run Emacs under GDB or JDB on Android.
3
4## Copyright (C) 2023 Free Software Foundation, Inc.
5
6## This file is part of GNU Emacs.
7
8## GNU Emacs is free software: you can redistribute it and/or modify
9## it under the terms of the GNU General Public License as published by
10## the Free Software Foundation, either version 3 of the License, or
11## (at your option) any later version.
12
13## GNU Emacs is distributed in the hope that it will be useful,
14## but WITHOUT ANY WARRANTY; without even the implied warranty of
15## MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
16## GNU General Public License for more details.
17
18## You should have received a copy of the GNU General Public License
19## along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>.
20
21set -m
22oldpwd=`pwd`
23cd `dirname $0`
24
25devices=`adb devices | grep device | awk -- '/device\y/ { print $1 }' -`
26device=
27progname=$0
28package=org.gnu.emacs
29activity=org.gnu.emacs.EmacsActivity
30gdb_port=5039
31jdb_port=64013
32jdb=no
33attach_existing=no
34gdbserver=
35gdb=gdb
36
37while [ $# -gt 0 ]; do
38 case "$1" in
39 ## This option specifies the serial number of a device to use.
40 "--device" )
41 device="$2"
42 if [ -z device ]; then
43 echo "You must specify an argument to --device"
44 exit 1
45 fi
46 shift
47 ;;
48 "--help" )
49 echo "Usage: $progname [options] -- [gdb options]"
50 echo ""
51 echo " --device DEVICE run Emacs on the specified device"
52 echo " --port PORT run the GDB server on a specific port"
53 echo " --jdb-port PORT run the JDB server on a specific port"
54 echo " --jdb run JDB instead of GDB"
55 echo " --gdb use specified GDB binary"
56 echo " --attach-existing attach to an existing process"
57 echo " --gdbserver BINARY upload and use the specified gdbserver binary"
58 echo " --help print this message"
59 echo ""
60 echo "Available devices:"
61 for device in $devices; do
62 echo " " $device
63 done
64 echo ""
65 exit 0
66 ;;
67 "--jdb" )
68 jdb=yes
69 ;;
70 "--gdb" )
71 shift
72 gdb=$1
73 ;;
74 "--gdbserver" )
75 shift
76 gdbserver=$1
77 ;;
78 "--port" )
79 shift
80 gdb_port=$1
81 ;;
82 "--jdb-port" )
83 shift
84 jdb_port=$1
85 ;;
86 "--attach-existing" )
87 attach_existing=yes
88 ;;
89 "--" )
90 shift
91 gdbargs=$@
92 break;
93 ;;
94 * )
95 echo "$progname: Unrecognized argument $1"
96 exit 1
97 ;;
98 esac
99 shift
100done
101
102if [ -z "$devices" ]; then
103 echo "No devices are available."
104 exit 1
105fi
106
107if [ -z $device ]; then
108 device=$devices
109fi
110
111if [ `wc -w <<< "$devices"` -gt 1 ] && [ -z device ]; then
112 echo "Multiple devices are available. Please pick one using"
113 echo "--device and try again."
114fi
115
116echo "Looking for $package on device $device"
117
118# Find the application data directory
119app_data_dir=`adb -s $device shell run-as $package sh -c 'pwd 2> /dev/null'`
120
121if [ -z $app_data_dir ]; then
122 echo "The data directory for the package $package was not found."
123 echo "Is it installed?"
124fi
125
126echo "Found application data directory at" "$app_data_dir"
127
128# Generate an awk script to extract PIDs from Android ps output. It
129# is enough to run `ps' as the package user on newer versions of
130# Android, but that doesn't work on Android 2.3.
131cat << EOF > tmp.awk
132BEGIN {
133 pid = 0;
134 pid_column = 2;
135}
136
137{
138 # Remove any trailing carriage return from the input line.
139 gsub ("\r", "", \$NF)
140
141 # If this is line 1, figure out which column contains the PID.
142 if (NR == 1)
143 {
144 for (n = 1; n <= NF; ++n)
145 {
146 if (\$n == "PID")
147 pid_column=n;
148 }
149 }
150 else if (\$NF == "$package")
151 print \$pid_column
152}
153EOF
154
155# Make sure that file disappears once this script exits.
156trap "rm -f $(pwd)/tmp.awk" 0
157
158# First, run ps to fetch the list of process IDs.
159package_pids=`adb -s $device shell ps`
160
161# Next, extract the list of PIDs currently running.
162package_pids=`awk -f tmp.awk <<< $package_pids`
163
164if [ "$attach_existing" != "yes" ]; then
165 # Finally, kill each existing process.
166 for pid in $package_pids; do
167 echo "Killing existing process $pid..."
168 adb -s $device shell run-as $package kill -9 $pid &> /dev/null
169 done
170
171 # Now run the main activity. This must be done as the adb user and
172 # not as the package user.
173 echo "Starting activity $activity and attaching debugger"
174
175 # Exit if the activity could not be started.
176 adb -s $device shell am start -D -n "$package/$activity"
177 if [ ! $? ]; then
178 exit 1;
179 fi
180
181 # Sleep for a bit. Otherwise, the process may not have started
182 # yet.
183 sleep 1
184
185 # Now look for processes matching the package again.
186 package_pids=`adb -s $device shell ps`
187
188 # Next, remove lines matching "ps" itself.
189 package_pids=`awk -f tmp.awk <<< $package_pids`
190fi
191
192pid=$package_pids
193num_pids=`wc -w <<< "$package_pids"`
194
195if [ $num_pids -gt 1 ]; then
196 echo "More than one process was started:"
197 echo ""
198 adb -s $device shell run-as $package ps | awk -- "{
199 if (!match (\$0, /ps/) && match (\$0, /$package/))
200 print \$0
201 }"
202 echo ""
203 printf "Which one do you want to attach to? "
204 read pid
205elif [ -z $package_pids ]; then
206 echo "No processes were found to attach to."
207 exit 1
208fi
209
210# If either --jdb was specified or debug.sh is not connecting to an
211# existing process, then store a suitable JDB invocation in
212# jdb_command. GDB will then run JDB to unblock the application from
213# the wait dialog after startup.
214
215if [ "$jdb" = "yes" ] || [ "$attach_existing" != yes ]; then
216 adb -s $device forward --remove-all
217 adb -s $device forward "tcp:$jdb_port" "jdwp:$pid"
218
219 if [ ! $? ]; then
220 echo "Failed to forward jdwp:$pid to $jdb_port!"
221 echo "Perhaps you need to specify a different port with --port?"
222 exit 1;
223 fi
224
225 jdb_command="jdb -connect \
226 com.sun.jdi.SocketAttach:hostname=localhost,port=$jdb_port"
227
228 if [ $jdb = "yes" ]; then
229 # Just start JDB and then exit
230 $jdb_command
231 exit 1
232 fi
233fi
234
235if [ -n "$jdb_command" ]; then
236 echo "Starting JDB to unblock application."
237
238 # Start JDB to unblock the application.
239 coproc JDB { $jdb_command; }
240
241 # Tell JDB to first suspend all threads.
242 echo "suspend" >&${JDB[1]}
243
244 # Tell JDB to print a magic string once the program is
245 # initialized.
246 echo "print \"__verify_jdb_has_started__\"" >&${JDB[1]}
247
248 # Now wait for JDB to give the string back.
249 line=
250 while :; do
251 read -u ${JDB[0]} line
252 if [ ! $? ]; then
253 echo "Failed to read JDB output"
254 exit 1
255 fi
256
257 case "$line" in
258 *__verify_jdb_has_started__*)
259 # Android only polls for a Java debugger every 200ms, so
260 # the debugger must be connected for at least that long.
261 echo "Pausing 1 second for the program to continue."
262 sleep 1
263 break
264 ;;
265 esac
266 done
267
268 # Note that JDB does not exit until GDB is fully attached!
269fi
270
271# See if gdbserver has to be uploaded
272gdbserver_cmd=
273is_root=
274if [ -z "$gdbserver" ]; then
275 gdbserver_bin=/system/bin/gdbserver64
276else
277 gdbserver_bin=/data/local/tmp/gdbserver
278 gdbserver_cat="cat $gdbserver_bin | run-as $package sh -c \
279 \"tee gdbserver > /dev/null\""
280
281 # Upload the specified gdbserver binary to the device.
282 adb -s $device push "$gdbserver" "$gdbserver_bin"
283
284 if (adb -s $device shell ls /system/bin | grep -G tee); then
285 # Copy it to the user directory.
286 adb -s $device shell "$gdbserver_cat"
287 adb -s $device shell "run-as $package chmod 777 gdbserver"
288 gdbserver_cmd="./gdbserver"
289 else
290 # Hopefully this is an old version of Android which allows
291 # execution from /data/local/tmp. Its `chmod' doesn't support
292 # `+x' either.
293 adb -s $device shell "chmod 777 $gdbserver_bin"
294 gdbserver_cmd="$gdbserver_bin"
295
296 # If the user is root, then there is no need to open any kind
297 # of TCP socket.
298 if (adb -s $device shell id | grep -G root); then
299 gdbserver=
300 is_root=yes
301 fi
302 fi
303fi
304
305# Now start gdbserver on the device asynchronously.
306
307echo "Attaching gdbserver to $pid on $device..."
308exec 5<> /tmp/file-descriptor-stamp
309rm -f /tmp/file-descriptor-stamp
310
311if [ -z "$gdbserver" ]; then
312 if [ "$is_root" = "yes" ]; then
313 adb -s $device shell $gdbserver_bin --multi \
314 "0.0.0.0:7564" --attach $pid >&5 &
315 gdb_socket="tcp:7564"
316 else
317 adb -s $device shell $gdbserver_bin --multi \
318 "0.0.0.0:7564" --attach $pid >&5 &
319 gdb_socket="tcp:7564"
320 fi
321else
322 # Normally the program cannot access $gdbserver_bin when it is
323 # placed in /data/local/tmp.
324 adb -s $device shell run-as $package $gdbserver_cmd --multi \
325 "+debug.$package.socket" --attach $pid >&5 &
326 gdb_socket="localfilesystem:$app_data_dir/debug.$package.socket"
327fi
328
329# In order to allow adb to forward to the gdbserver socket, make the
330# app data directory a+x.
331adb -s $device shell run-as $package chmod a+x $app_data_dir
332
333# Wait until gdbserver successfully runs.
334line=
335while read -u 5 line; do
336 case "$line" in
337 *Attached* )
338 break;
339 ;;
340 *error* | *Error* | failed )
341 echo "GDB error:" $line
342 exit 1
343 ;;
344 * )
345 ;;
346 esac
347done
348
349# Now that GDB is attached, tell the Java debugger to resume execution
350# and then exit.
351
352if [ -n "$jdb_command" ]; then
353 echo "resume" >&${JDB[1]}
354 echo "exit" >&${JDB[1]}
355fi
356
357# Forward the gdb server port here.
358adb -s $device forward "tcp:$gdb_port" $gdb_socket
359if [ ! $? ]; then
360 echo "Failed to forward $app_data_dir/debug.$package.socket"
361 echo "to $gdb_port! Perhaps you need to specify a different port"
362 echo "with --port?"
363 exit 1;
364fi
365
366# Finally, start gdb with any extra arguments needed.
367cd "$oldpwd"
368$gdb --eval-command "target remote localhost:$gdb_port" $gdbargs
diff --git a/java/emacs.keystore b/java/emacs.keystore
new file mode 100644
index 00000000000..76d80b6db65
--- /dev/null
+++ b/java/emacs.keystore
Binary files differ
diff --git a/java/org/gnu/emacs/EmacsActivity.java b/java/org/gnu/emacs/EmacsActivity.java
new file mode 100644
index 00000000000..4ddf51fbb20
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsActivity.java
@@ -0,0 +1,481 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.lang.IllegalStateException;
23import java.util.List;
24import java.util.ArrayList;
25
26import android.app.Activity;
27
28import android.content.ContentResolver;
29import android.content.Context;
30import android.content.Intent;
31
32import android.os.Build;
33import android.os.Bundle;
34
35import android.util.Log;
36
37import android.net.Uri;
38
39import android.view.Menu;
40import android.view.View;
41import android.view.ViewTreeObserver;
42import android.view.Window;
43import android.view.WindowInsets;
44import android.view.WindowInsetsController;
45
46import android.widget.FrameLayout;
47
48public class EmacsActivity extends Activity
49 implements EmacsWindowAttachmentManager.WindowConsumer,
50 ViewTreeObserver.OnGlobalLayoutListener
51{
52 public static final String TAG = "EmacsActivity";
53
54 /* ID for URIs from a granted document tree. */
55 public static final int ACCEPT_DOCUMENT_TREE = 1;
56
57 /* The currently attached EmacsWindow, or null if none. */
58 private EmacsWindow window;
59
60 /* The frame layout associated with the activity. */
61 private FrameLayout layout;
62
63 /* List of activities with focus. */
64 public static final List<EmacsActivity> focusedActivities;
65
66 /* The last activity to have been focused. */
67 public static EmacsActivity lastFocusedActivity;
68
69 /* The currently focused window. */
70 public static EmacsWindow focusedWindow;
71
72 /* Whether or not this activity is paused. */
73 private boolean isPaused;
74
75 /* Whether or not this activity is fullscreen. */
76 private boolean isFullscreen;
77
78 /* The last context menu to be closed. */
79 private static Menu lastClosedMenu;
80
81 static
82 {
83 focusedActivities = new ArrayList<EmacsActivity> ();
84 };
85
86 public static void
87 invalidateFocus1 (EmacsWindow window)
88 {
89 if (window.view.isFocused ())
90 focusedWindow = window;
91
92 for (EmacsWindow child : window.children)
93 invalidateFocus1 (child);
94 }
95
96 public static void
97 invalidateFocus ()
98 {
99 EmacsWindow oldFocus;
100
101 /* Walk through each focused activity and assign the window focus
102 to the bottom-most focused window within. Record the old focus
103 as well. */
104 oldFocus = focusedWindow;
105 focusedWindow = null;
106
107 for (EmacsActivity activity : focusedActivities)
108 {
109 if (activity.window != null)
110 invalidateFocus1 (activity.window);
111 }
112
113 /* Send focus in- and out- events to the previous and current
114 focus. */
115
116 if (oldFocus != null)
117 EmacsNative.sendFocusOut (oldFocus.handle,
118 System.currentTimeMillis ());
119
120 if (focusedWindow != null)
121 EmacsNative.sendFocusIn (focusedWindow.handle,
122 System.currentTimeMillis ());
123 }
124
125 @Override
126 public final void
127 detachWindow ()
128 {
129 syncFullscreenWith (null);
130
131 if (window == null)
132 Log.w (TAG, "detachWindow called, but there is no window");
133 else
134 {
135 /* Clear the window's pointer to this activity and remove the
136 window's view. */
137 window.setConsumer (null);
138
139 /* The window can't be iconified any longer. */
140 window.noticeDeiconified ();
141 layout.removeView (window.view);
142 window = null;
143
144 invalidateFocus ();
145 }
146 }
147
148 @Override
149 public final void
150 attachWindow (EmacsWindow child)
151 {
152 Log.d (TAG, "attachWindow: " + child);
153
154 if (window != null)
155 throw new IllegalStateException ("trying to attach window when one"
156 + " already exists");
157
158 syncFullscreenWith (child);
159
160 /* Record and attach the view. */
161
162 window = child;
163 layout.addView (window.view);
164 child.setConsumer (this);
165
166 /* If the window isn't no-focus-on-map, focus its view. */
167 if (!child.getDontFocusOnMap ())
168 window.view.requestFocus ();
169
170 /* If the activity is iconified, send that to the window. */
171 if (isPaused)
172 window.noticeIconified ();
173
174 /* Invalidate the focus. */
175 invalidateFocus ();
176 }
177
178 @Override
179 public final void
180 destroy ()
181 {
182 finish ();
183 }
184
185 @Override
186 public final EmacsWindow
187 getAttachedWindow ()
188 {
189 return window;
190 }
191
192 @Override
193 public final void
194 onCreate (Bundle savedInstanceState)
195 {
196 FrameLayout.LayoutParams params;
197 Intent intent;
198 View decorView;
199 ViewTreeObserver observer;
200 int matchParent;
201
202 /* See if Emacs should be started with any extra arguments, such
203 as `--quick'. */
204 intent = getIntent ();
205 EmacsService.extraStartupArgument
206 = intent.getStringExtra ("org.gnu.emacs.STARTUP_ARGUMENT");
207
208 matchParent = FrameLayout.LayoutParams.MATCH_PARENT;
209 params
210 = new FrameLayout.LayoutParams (matchParent,
211 matchParent);
212
213 /* Make the frame layout. */
214 layout = new FrameLayout (this);
215 layout.setLayoutParams (params);
216
217 /* Set it as the content view. */
218 setContentView (layout);
219
220 /* Maybe start the Emacs service if necessary. */
221 EmacsService.startEmacsService (this);
222
223 /* Add this activity to the list of available activities. */
224 EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this);
225
226 /* Start observing global layout changes between Jelly Bean and Q.
227 This is required to restore the fullscreen state whenever the
228 on screen keyboard is displayed, as there is otherwise no way
229 to determine when the on screen keyboard becomes visible. */
230
231 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
232 && Build.VERSION.SDK_INT < Build.VERSION_CODES.R)
233 {
234 decorView = getWindow ().getDecorView ();
235 observer = decorView.getViewTreeObserver ();
236 observer.addOnGlobalLayoutListener (this);
237 }
238
239 super.onCreate (savedInstanceState);
240 }
241
242 @Override
243 public final void
244 onGlobalLayout ()
245 {
246 syncFullscreenWith (window);
247 }
248
249 @Override
250 public final void
251 onDestroy ()
252 {
253 EmacsWindowAttachmentManager manager;
254 boolean isMultitask;
255
256 manager = EmacsWindowAttachmentManager.MANAGER;
257
258 /* The activity will die shortly hereafter. If there is a window
259 attached, close it now. */
260 Log.d (TAG, "onDestroy " + this);
261 isMultitask = this instanceof EmacsMultitaskActivity;
262 manager.removeWindowConsumer (this, isMultitask || isFinishing ());
263 focusedActivities.remove (this);
264 invalidateFocus ();
265
266 /* Remove this activity from the static field, lest it leak. */
267 if (lastFocusedActivity == this)
268 lastFocusedActivity = null;
269
270 super.onDestroy ();
271 }
272
273 @Override
274 public final void
275 onWindowFocusChanged (boolean isFocused)
276 {
277 Log.d (TAG, ("onWindowFocusChanged: "
278 + (isFocused ? "YES" : "NO")));
279
280 if (isFocused && !focusedActivities.contains (this))
281 {
282 focusedActivities.add (this);
283 lastFocusedActivity = this;
284
285 /* Update the window insets as the focus change may have
286 changed the window insets as well, and the system does not
287 automatically restore visibility flags. */
288
289 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN
290 && Build.VERSION.SDK_INT < Build.VERSION_CODES.R
291 && isFullscreen)
292 syncFullscreenWith (window);
293 }
294 else
295 focusedActivities.remove (this);
296
297 invalidateFocus ();
298 }
299
300 @Override
301 public final void
302 onPause ()
303 {
304 isPaused = true;
305
306 EmacsWindowAttachmentManager.MANAGER.noticeIconified (this);
307 super.onPause ();
308 }
309
310 @Override
311 public final void
312 onResume ()
313 {
314 isPaused = false;
315
316 EmacsWindowAttachmentManager.MANAGER.noticeDeiconified (this);
317 super.onResume ();
318 }
319
320 @Override
321 public final void
322 onContextMenuClosed (Menu menu)
323 {
324 int serial;
325
326 Log.d (TAG, "onContextMenuClosed: " + menu);
327
328 /* See the comment inside onMenuItemClick. */
329
330 if (((EmacsContextMenu.wasSubmenuSelected == -2)
331 || (EmacsContextMenu.wasSubmenuSelected >= 0
332 && ((System.currentTimeMillis ()
333 - EmacsContextMenu.wasSubmenuSelected)
334 <= 300)))
335 || menu == lastClosedMenu)
336 {
337 EmacsContextMenu.wasSubmenuSelected = -1;
338 lastClosedMenu = menu;
339 return;
340 }
341
342 /* lastClosedMenu is set because Android apparently calls this
343 function twice. */
344
345 lastClosedMenu = null;
346
347 /* Send a context menu event given that no menu item has already
348 been selected. */
349 if (!EmacsContextMenu.itemAlreadySelected)
350 {
351 serial = EmacsContextMenu.lastMenuEventSerial;
352 EmacsNative.sendContextMenu ((short) 0, 0,
353 serial);
354 }
355
356 super.onContextMenuClosed (menu);
357 }
358
359 @SuppressWarnings ("deprecation")
360 public final void
361 syncFullscreenWith (EmacsWindow emacsWindow)
362 {
363 WindowInsetsController controller;
364 Window window;
365 int behavior, flags;
366 View view;
367
368 if (emacsWindow != null)
369 isFullscreen = emacsWindow.fullscreen;
370 else
371 isFullscreen = false;
372
373 /* On Android 11 or later, use the window insets controller to
374 control whether or not the view is fullscreen. */
375
376 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R)
377 {
378 window = getWindow ();
379
380 /* If there is no attached window, return immediately. */
381 if (window == null)
382 return;
383
384 behavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE;
385 controller = window.getInsetsController ();
386 controller.setSystemBarsBehavior (behavior);
387
388 if (isFullscreen)
389 controller.hide (WindowInsets.Type.statusBars ()
390 | WindowInsets.Type.navigationBars ());
391 else
392 controller.show (WindowInsets.Type.statusBars ()
393 | WindowInsets.Type.navigationBars ());
394
395 return;
396 }
397
398 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN)
399 {
400 /* On Android 4.1 or later, use `setSystemUiVisibility'. */
401
402 window = getWindow ();
403
404 if (window == null)
405 return;
406
407 view = window.getDecorView ();
408
409 if (isFullscreen)
410 {
411 flags = 0;
412 flags |= View.SYSTEM_UI_FLAG_FULLSCREEN;
413
414 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
415 {
416 /* These flags means that Emacs will be full screen as
417 long as the state flag is set. */
418 flags |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
419 flags |= View.SYSTEM_UI_FLAG_IMMERSIVE;
420 flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
421 }
422
423 /* Apply the given flags. */
424 view.setSystemUiVisibility (flags);
425 }
426 else
427 view.setSystemUiVisibility (View.SYSTEM_UI_FLAG_VISIBLE);
428 }
429 }
430
431 @Override
432 public final void
433 onAttachedToWindow ()
434 {
435 super.onAttachedToWindow ();
436
437 /* Update the window insets. */
438 syncFullscreenWith (window);
439 }
440
441
442
443 @Override
444 public final void
445 onActivityResult (int requestCode, int resultCode, Intent data)
446 {
447 ContentResolver resolver;
448 Uri uri;
449 int flags;
450
451 switch (requestCode)
452 {
453 case ACCEPT_DOCUMENT_TREE:
454
455 /* A document granted through
456 EmacsService.requestDirectoryAccess. */
457
458 if (resultCode == RESULT_OK)
459 {
460 resolver = getContentResolver ();
461 uri = data.getData ();
462 flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION
463 | Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
464
465 try
466 {
467 if (uri != null)
468 resolver.takePersistableUriPermission (uri, flags);
469 }
470 catch (Exception exception)
471 {
472 /* Permission to access URI might've been revoked in
473 between selecting the file and this callback being
474 invoked. Don't crash in such cases. */
475 }
476 }
477
478 break;
479 }
480 }
481};
diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java
new file mode 100644
index 00000000000..8afa5bcedb4
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsApplication.java
@@ -0,0 +1,92 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.io.File;
23import java.io.FileFilter;
24
25import android.content.Context;
26
27import android.app.Application;
28import android.util.Log;
29
30public final class EmacsApplication extends Application
31{
32 private static final String TAG = "EmacsApplication";
33
34 /* The name of the dump file to use. */
35 public static String dumpFileName;
36
37 public static void
38 findDumpFile (Context context)
39 {
40 File filesDirectory;
41 File[] allFiles;
42 String wantedDumpFile;
43 int i;
44
45 wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint ()
46 + ".pdmp");
47
48 /* Obtain a list of all files ending with ``.pdmp''. Then, look
49 for a file named ``emacs-<fingerprint>.pdmp'' and delete the
50 rest. */
51 filesDirectory = context.getFilesDir ();
52
53 allFiles = filesDirectory.listFiles (new FileFilter () {
54 @Override
55 public boolean
56 accept (File file)
57 {
58 return (!file.isDirectory ()
59 && file.getName ().endsWith (".pdmp"));
60 }
61 });
62
63 if (allFiles == null)
64 return;
65
66 /* Now try to find the right dump file. */
67 for (i = 0; i < allFiles.length; ++i)
68 {
69 if (allFiles[i].getName ().equals (wantedDumpFile))
70 dumpFileName = allFiles[i].getAbsolutePath ();
71 else
72 /* Delete this outdated dump file. */
73 allFiles[i].delete ();
74 }
75 }
76
77 @Override
78 public void
79 onCreate ()
80 {
81 /* Block signals which don't interest the current thread and its
82 descendants created by the system. The original signal mask
83 will be restored for the Emacs thread in `initEmacs'. */
84 EmacsNative.setupSystemThread ();
85
86 /* Locate a suitable dump file. */
87 findDumpFile (this);
88
89 /* Start the rest of the application. */
90 super.onCreate ();
91 }
92};
diff --git a/java/org/gnu/emacs/EmacsClipboard.java b/java/org/gnu/emacs/EmacsClipboard.java
new file mode 100644
index 00000000000..5cd48af6e3a
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsClipboard.java
@@ -0,0 +1,47 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.os.Build;
23
24/* This class provides helper code for accessing the clipboard,
25 abstracting between the different interfaces on API 8 and 11. */
26
27public abstract class EmacsClipboard
28{
29 public abstract void setClipboard (byte[] bytes);
30 public abstract int ownsClipboard ();
31 public abstract boolean clipboardExists ();
32 public abstract byte[] getClipboard ();
33
34 public abstract byte[][] getClipboardTargets ();
35 public abstract long[] getClipboardData (byte[] target);
36
37 /* Create the correct kind of clipboard for this system. */
38
39 public static EmacsClipboard
40 makeClipboard ()
41 {
42 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB)
43 return new EmacsSdk11Clipboard ();
44 else
45 return new EmacsSdk8Clipboard ();
46 }
47};
diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java
new file mode 100644
index 00000000000..46eddeeda3d
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsContextMenu.java
@@ -0,0 +1,393 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.util.List;
23import java.util.ArrayList;
24
25import android.content.Context;
26import android.content.Intent;
27
28import android.os.Build;
29
30import android.view.ContextMenu;
31import android.view.Menu;
32import android.view.MenuItem;
33import android.view.View;
34import android.view.SubMenu;
35
36import android.util.Log;
37
38/* Context menu implementation. This object is built from JNI and
39 describes a menu hiearchy. Then, `inflate' can turn it into an
40 Android menu, which can be turned into a popup (or other kind of)
41 menu. */
42
43public final class EmacsContextMenu
44{
45 private static final String TAG = "EmacsContextMenu";
46
47 /* Whether or not an item was selected. */
48 public static boolean itemAlreadySelected;
49
50 /* Whether or not a submenu was selected.
51 Value is -1 if no; value is -2 if yes, and a context menu
52 close event will definitely be sent. Any other value is
53 the timestamp when the submenu was selected. */
54 public static long wasSubmenuSelected;
55
56 /* The serial ID of the last context menu to be displayed. */
57 public static int lastMenuEventSerial;
58
59 /* The last group ID used for a menu item. */
60 public int lastGroupId;
61
62 private static final class Item implements MenuItem.OnMenuItemClickListener
63 {
64 public int itemID;
65 public String itemName, tooltip;
66 public EmacsContextMenu subMenu;
67 public boolean isEnabled, isCheckable, isChecked;
68 public EmacsView inflatedView;
69 public boolean isRadio;
70
71 @Override
72 public boolean
73 onMenuItemClick (MenuItem item)
74 {
75 Log.d (TAG, "onMenuItemClick: " + itemName + " (" + itemID + ")");
76
77 if (subMenu != null)
78 {
79 /* Android 6.0 and earlier don't support nested submenus
80 properly, so display the submenu popup by hand. */
81
82 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
83 {
84 Log.d (TAG, "onMenuItemClick: displaying submenu " + subMenu);
85
86 /* Still set wasSubmenuSelected -- if not set, the
87 dismissal of this context menu will result in a
88 context menu event being sent. */
89 wasSubmenuSelected = -2;
90
91 /* Running a popup menu from inside a click handler
92 doesn't work, so make sure it is displayed
93 outside. */
94
95 inflatedView.post (new Runnable () {
96 @Override
97 public void
98 run ()
99 {
100 inflatedView.popupMenu (subMenu, 0, 0, true);
101 }
102 });
103
104 return true;
105 }
106
107 /* After opening a submenu within a submenu, Android will
108 send onContextMenuClosed for a ContextMenuBuilder. This
109 will normally confuse Emacs into thinking that the
110 context menu has been dismissed. Wrong!
111
112 Setting this flag makes EmacsActivity to only handle
113 SubMenuBuilder being closed, which always means the menu
114 has actually been dismissed.
115
116 However, these extraneous events aren't sent on devices
117 where submenus display without dismissing their parents.
118 Thus, only ignore the close event if it happens within
119 300 milliseconds of the submenu being selected. */
120 wasSubmenuSelected = System.currentTimeMillis ();
121 return false;
122 }
123
124 /* Send a context menu event. */
125 EmacsNative.sendContextMenu ((short) 0, itemID,
126 lastMenuEventSerial);
127
128 /* Say that an item has already been selected. */
129 itemAlreadySelected = true;
130 return true;
131 }
132 };
133
134 /* List of menu items contained in this menu. */
135 public List<Item> menuItems;
136
137 /* The parent context menu, or NULL if none. */
138 private EmacsContextMenu parent;
139
140 /* The title of this context menu, or NULL if none. */
141 private String title;
142
143
144
145 /* Create a context menu with no items inside and the title TITLE,
146 which may be NULL. */
147
148 public static EmacsContextMenu
149 createContextMenu (String title)
150 {
151 EmacsContextMenu menu;
152
153 menu = new EmacsContextMenu ();
154 menu.title = title;
155 menu.menuItems = new ArrayList<Item> ();
156
157 return menu;
158 }
159
160 /* Add a normal menu item to the context menu with the id ITEMID and
161 the name ITEMNAME. Enable it if ISENABLED, else keep it
162 disabled.
163
164 If this is not a submenu and ISCHECKABLE is set, make the item
165 checkable. Likewise, if ISCHECKED is set, make the item
166 checked.
167
168 If TOOLTIP is non-NULL, set the menu item tooltip to TOOLTIP.
169
170 If ISRADIO, then display the check mark as a radio button. */
171
172 public void
173 addItem (int itemID, String itemName, boolean isEnabled,
174 boolean isCheckable, boolean isChecked,
175 String tooltip, boolean isRadio)
176 {
177 Item item;
178
179 item = new Item ();
180 item.itemID = itemID;
181 item.itemName = itemName;
182 item.isEnabled = isEnabled;
183 item.isCheckable = isCheckable;
184 item.isChecked = isChecked;
185 item.tooltip = tooltip;
186 item.isRadio = isRadio;
187
188 menuItems.add (item);
189 }
190
191 /* Create a disabled menu item with the name ITEMNAME. */
192
193 public void
194 addPane (String itemName)
195 {
196 Item item;
197
198 item = new Item ();
199 item.itemName = itemName;
200
201 menuItems.add (item);
202 }
203
204 /* Add a submenu to the context menu with the specified title and
205 item name. */
206
207 public EmacsContextMenu
208 addSubmenu (String itemName, String tooltip)
209 {
210 EmacsContextMenu submenu;
211 Item item;
212
213 item = new Item ();
214 item.itemID = 0;
215 item.itemName = itemName;
216 item.tooltip = tooltip;
217 item.subMenu = createContextMenu (itemName);
218 item.subMenu.parent = this;
219
220 menuItems.add (item);
221 return item.subMenu;
222 }
223
224 /* Add the contents of this menu to MENU. Assume MENU will be
225 displayed in INFLATEDVIEW. */
226
227 private void
228 inflateMenuItems (Menu menu, EmacsView inflatedView)
229 {
230 Intent intent;
231 MenuItem menuItem;
232 SubMenu submenu;
233
234 for (Item item : menuItems)
235 {
236 if (item.subMenu != null)
237 {
238 /* This is a submenu. On versions of Android which
239 support doing so, create the submenu and add the
240 contents of the menu to it.
241
242 Note that Android 4.0 and later technically supports
243 having multiple layers of nested submenus, but if they
244 are used, onContextMenuClosed becomes unreliable. */
245
246 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
247 {
248 submenu = menu.addSubMenu (item.itemName);
249 item.subMenu.inflateMenuItems (submenu, inflatedView);
250
251 /* This is still needed to set wasSubmenuSelected. */
252 menuItem = submenu.getItem ();
253 }
254 else
255 menuItem = menu.add (item.itemName);
256
257 item.inflatedView = inflatedView;
258 menuItem.setOnMenuItemClickListener (item);
259 }
260 else
261 {
262 if (item.isRadio)
263 menuItem = menu.add (++lastGroupId, Menu.NONE, Menu.NONE,
264 item.itemName);
265 else
266 menuItem = menu.add (item.itemName);
267 menuItem.setOnMenuItemClickListener (item);
268
269 /* If the item ID is zero, then disable the item. */
270 if (item.itemID == 0 || !item.isEnabled)
271 menuItem.setEnabled (false);
272
273 /* Now make the menu item display a checkmark as
274 appropriate. */
275
276 if (item.isCheckable)
277 menuItem.setCheckable (true);
278
279 if (item.isChecked)
280 menuItem.setChecked (true);
281
282 /* Define an exclusively checkable group if the item is a
283 radio button. */
284
285 if (item.isRadio)
286 menu.setGroupCheckable (lastGroupId, true, true);
287
288 /* If the tooltip text is set and the system is new enough
289 to support menu item tooltips, set it on the item. */
290
291 if (item.tooltip != null
292 && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
293 menuItem.setTooltipText (item.tooltip);
294 }
295 }
296 }
297
298 /* Enter the items in this context menu to MENU.
299 Assume that MENU will be displayed in VIEW; this may lead to
300 popupMenu being called on VIEW if a submenu is selected.
301
302 If MENU is a ContextMenu, set its header title to the one
303 contained in this object. */
304
305 public void
306 expandTo (Menu menu, EmacsView view)
307 {
308 inflateMenuItems (menu, view);
309
310 /* See if menu is a ContextMenu and a title is set. */
311 if (title == null || !(menu instanceof ContextMenu))
312 return;
313
314 /* Set its title to this.title. */
315 ((ContextMenu) menu).setHeaderTitle (title);
316 }
317
318 /* Return the parent or NULL. */
319
320 public EmacsContextMenu
321 parent ()
322 {
323 return this.parent;
324 }
325
326 /* Like display, but does the actual work and runs in the main
327 thread. */
328
329 private boolean
330 display1 (EmacsWindow window, int xPosition, int yPosition)
331 {
332 /* Set this flag to false. It is used to decide whether or not to
333 send 0 in response to the context menu being closed. */
334 itemAlreadySelected = false;
335
336 /* No submenu has been selected yet. */
337 wasSubmenuSelected = -1;
338
339 return window.view.popupMenu (this, xPosition, yPosition,
340 false);
341 }
342
343 /* Display this context menu on WINDOW, at xPosition and yPosition.
344 SERIAL is a number that will be returned in any menu event
345 generated to identify this context menu. */
346
347 public boolean
348 display (final EmacsWindow window, final int xPosition,
349 final int yPosition, final int serial)
350 {
351 Runnable runnable;
352 final EmacsHolder<Boolean> rc;
353
354 rc = new EmacsHolder<Boolean> ();
355 rc.thing = false;
356
357 runnable = new Runnable () {
358 @Override
359 public void
360 run ()
361 {
362 synchronized (this)
363 {
364 lastMenuEventSerial = serial;
365 rc.thing = display1 (window, xPosition, yPosition);
366 notify ();
367 }
368 }
369 };
370
371 EmacsService.syncRunnable (runnable);
372 return rc.thing;
373 }
374
375 /* Dismiss this context menu. WINDOW is the window where the
376 context menu is being displayed. */
377
378 public void
379 dismiss (final EmacsWindow window)
380 {
381 Runnable runnable;
382
383 EmacsService.SERVICE.runOnUiThread (new Runnable () {
384 @Override
385 public void
386 run ()
387 {
388 window.view.cancelPopupMenu ();
389 itemAlreadySelected = false;
390 }
391 });
392 }
393};
diff --git a/java/org/gnu/emacs/EmacsCursor.java b/java/org/gnu/emacs/EmacsCursor.java
new file mode 100644
index 00000000000..c14c6f2a11b
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsCursor.java
@@ -0,0 +1,47 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.view.PointerIcon;
23import android.os.Build;
24
25/* Cursor wrapper. Note that pointer icons are not supported prior to
26 Android 24. */
27
28public final class EmacsCursor extends EmacsHandleObject
29{
30 /* The pointer icon associated with this cursor. */
31 public final PointerIcon icon;
32
33 public
34 EmacsCursor (short handle, int glyph)
35 {
36 super (handle);
37
38 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
39 {
40 icon = null;
41 return;
42 }
43
44 icon = PointerIcon.getSystemIcon (EmacsService.SERVICE,
45 glyph);
46 }
47};
diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java
new file mode 100644
index 00000000000..e4ed2271741
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDialog.java
@@ -0,0 +1,427 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.util.List;
23import java.util.ArrayList;
24
25import android.app.AlertDialog;
26
27import android.content.Context;
28import android.content.DialogInterface;
29
30import android.content.res.Resources.NotFoundException;
31import android.content.res.Resources.Theme;
32import android.content.res.TypedArray;
33
34import android.os.Build;
35
36import android.provider.Settings;
37
38import android.util.Log;
39
40import android.widget.Button;
41import android.widget.LinearLayout;
42import android.widget.FrameLayout;
43
44import android.view.View;
45import android.view.ViewGroup;
46import android.view.Window;
47import android.view.WindowManager;
48
49/* Toolkit dialog implementation. This object is built from JNI and
50 describes a single alert dialog. Then, `inflate' turns it into
51 AlertDialog. */
52
53public final class EmacsDialog implements DialogInterface.OnDismissListener
54{
55 private static final String TAG = "EmacsDialog";
56
57 /* List of buttons in this dialog. */
58 private List<EmacsButton> buttons;
59
60 /* Dialog title. */
61 private String title;
62
63 /* Dialog text. */
64 private String text;
65
66 /* Whether or not a selection has already been made. */
67 private boolean wasButtonClicked;
68
69 /* Dialog to dismiss after click. */
70 private AlertDialog dismissDialog;
71
72 /* The menu serial associated with this dialog box. */
73 private int menuEventSerial;
74
75 private final class EmacsButton implements View.OnClickListener,
76 DialogInterface.OnClickListener
77 {
78 /* Name of this button. */
79 public String name;
80
81 /* ID of this button. */
82 public int id;
83
84 /* Whether or not the button is enabled. */
85 public boolean enabled;
86
87 @Override
88 public void
89 onClick (View view)
90 {
91 Log.d (TAG, "onClicked " + this);
92
93 wasButtonClicked = true;
94 EmacsNative.sendContextMenu ((short) 0, id, menuEventSerial);
95 dismissDialog.dismiss ();
96 }
97
98 @Override
99 public void
100 onClick (DialogInterface dialog, int which)
101 {
102 Log.d (TAG, "onClicked " + this);
103
104 wasButtonClicked = true;
105 EmacsNative.sendContextMenu ((short) 0, id, menuEventSerial);
106 }
107 };
108
109 /* Create a popup dialog with the title TITLE and the text TEXT.
110 TITLE may be NULL. MENUEVENTSERIAL is a number which will
111 identify this popup dialog inside events it sends. */
112
113 public static EmacsDialog
114 createDialog (String title, String text, int menuEventSerial)
115 {
116 EmacsDialog dialog;
117
118 dialog = new EmacsDialog ();
119 dialog.buttons = new ArrayList<EmacsButton> ();
120 dialog.title = title;
121 dialog.text = text;
122 dialog.menuEventSerial = menuEventSerial;
123
124 return dialog;
125 }
126
127 /* Add a button named NAME, with the identifier ID. If DISABLE,
128 disable the button. */
129
130 public void
131 addButton (String name, int id, boolean disable)
132 {
133 EmacsButton button;
134
135 button = new EmacsButton ();
136 button.name = name;
137 button.id = id;
138 button.enabled = !disable;
139 buttons.add (button);
140 }
141
142 /* Turn this dialog into an AlertDialog for the specified
143 CONTEXT.
144
145 Upon a button being selected, the dialog will send an
146 ANDROID_CONTEXT_MENU event with the id of that button.
147
148 Upon the dialog being dismissed, an ANDROID_CONTEXT_MENU event
149 will be sent with an id of 0. */
150
151 public AlertDialog
152 toAlertDialog (Context context)
153 {
154 AlertDialog dialog;
155 int size, styleId, flag;
156 int[] attrs;
157 EmacsButton button;
158 EmacsDialogButtonLayout layout;
159 Button buttonView;
160 ViewGroup.LayoutParams layoutParams;
161 Theme theme;
162 TypedArray attributes;
163 Window window;
164
165 size = buttons.size ();
166 styleId = -1;
167
168 if (size <= 3)
169 {
170 dialog = new AlertDialog.Builder (context).create ();
171 dialog.setMessage (text);
172 dialog.setCancelable (true);
173 dialog.setOnDismissListener (this);
174
175 if (title != null)
176 dialog.setTitle (title);
177
178 /* There are less than 4 buttons. Add the buttons the way
179 Android intends them to be added. */
180
181 if (size >= 1)
182 {
183 button = buttons.get (0);
184 dialog.setButton (DialogInterface.BUTTON_POSITIVE,
185 button.name, button);
186 }
187
188 if (size >= 2)
189 {
190 button = buttons.get (1);
191 dialog.setButton (DialogInterface.BUTTON_NEGATIVE,
192 button.name, button);
193 }
194
195 if (size >= 3)
196 {
197 button = buttons.get (2);
198 dialog.setButton (DialogInterface.BUTTON_NEUTRAL,
199 button.name, button);
200 }
201 }
202 else
203 {
204 /* There are more than 3 buttons. Add them all to a special
205 container widget that handles wrapping. First, create the
206 layout. */
207
208 layout = new EmacsDialogButtonLayout (context);
209 layoutParams
210 = new FrameLayout.LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT,
211 ViewGroup.LayoutParams.WRAP_CONTENT);
212 layout.setLayoutParams (layoutParams);
213
214 /* Add that layout to the dialog's custom view.
215
216 android.R.id.custom is documented to work. But looking it
217 up returns NULL, so setView must be used instead. */
218
219 dialog = new AlertDialog.Builder (context).setView (layout).create ();
220 dialog.setMessage (text);
221 dialog.setCancelable (true);
222 dialog.setOnDismissListener (this);
223
224 if (title != null)
225 dialog.setTitle (title);
226
227 /* Now that the dialog has been created, set the style of each
228 custom button to match the usual dialog buttons found on
229 Android 5 and later. */
230
231 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
232 {
233 /* Obtain the Theme associated with the dialog. */
234 theme = dialog.getContext ().getTheme ();
235
236 /* Resolve the dialog button style. */
237 attrs
238 = new int [] { android.R.attr.buttonBarNeutralButtonStyle, };
239
240 try
241 {
242 attributes = theme.obtainStyledAttributes (attrs);
243
244 /* Look for the style ID. Default to -1 if it could
245 not be found. */
246 styleId = attributes.getResourceId (0, -1);
247
248 /* Now clean up the TypedAttributes object. */
249 attributes.recycle ();
250 }
251 catch (NotFoundException e)
252 {
253 /* Nothing to do here. */
254 }
255 }
256
257 /* Create each button and add it to the layout. Set the style
258 if necessary. */
259
260 for (EmacsButton emacsButton : buttons)
261 {
262 if (styleId == -1)
263 /* No specific style... */
264 buttonView = new Button (context);
265 else
266 /* Use the given styleId. */
267 buttonView = new Button (context, null, 0, styleId);
268
269 /* Set the text and on click handler. */
270 buttonView.setText (emacsButton.name);
271 buttonView.setOnClickListener (emacsButton);
272 buttonView.setEnabled (emacsButton.enabled);
273 layout.addView (buttonView);
274 }
275 }
276
277 return dialog;
278 }
279
280 /* Internal helper for display run on the main thread. */
281
282 @SuppressWarnings("deprecation")
283 private boolean
284 display1 ()
285 {
286 Context context;
287 int size, type;
288 Button buttonView;
289 EmacsButton button;
290 AlertDialog dialog;
291 Window window;
292
293 if (EmacsActivity.focusedActivities.isEmpty ())
294 {
295 /* If focusedActivities is empty then this dialog may have
296 been displayed immediately after another popup dialog was
297 dismissed. Or Emacs might legitimately be in the
298 background, possibly displaying this popup in response to
299 an Emacsclient request. Try the service context if it will
300 work, then any focused EmacsOpenActivity, and finally the
301 last EmacsActivity to be focused. */
302
303 Log.d (TAG, "display1: no focused activities...");
304 Log.d (TAG, ("display1: EmacsOpenActivity.currentActivity: "
305 + EmacsOpenActivity.currentActivity));
306
307 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M
308 || Settings.canDrawOverlays (EmacsService.SERVICE))
309 context = EmacsService.SERVICE;
310 else if (EmacsOpenActivity.currentActivity != null)
311 context = EmacsOpenActivity.currentActivity;
312 else
313 context = EmacsActivity.lastFocusedActivity;
314
315 if (context == null)
316 return false;
317 }
318 else
319 /* Display using the activity context when Emacs is in the
320 foreground, as this allows the dialog to be dismissed more
321 consistently. */
322 context = EmacsActivity.focusedActivities.get (0);
323
324 Log.d (TAG, "display1: using context " + context);
325
326 dialog = dismissDialog = toAlertDialog (context);
327
328 try
329 {
330 if (context == EmacsService.SERVICE)
331 {
332 /* Apply the system alert window type to make sure this
333 dialog can be displayed. */
334
335 window = dialog.getWindow ();
336 type = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O
337 ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY
338 : WindowManager.LayoutParams.TYPE_PHONE);
339 window.setType (type);
340 }
341
342 dismissDialog.show ();
343 }
344 catch (Exception exception)
345 {
346 /* This can happen when the system decides Emacs is not in the
347 foreground any longer. */
348 return false;
349 }
350
351 /* If there are less than four buttons, then they must be
352 individually enabled or disabled after the dialog is
353 displayed. */
354 size = buttons.size ();
355
356 if (size <= 3)
357 {
358 if (size >= 1)
359 {
360 button = buttons.get (0);
361 buttonView
362 = dialog.getButton (DialogInterface.BUTTON_POSITIVE);
363 buttonView.setEnabled (button.enabled);
364 }
365
366 if (size >= 2)
367 {
368 button = buttons.get (1);
369 buttonView
370 = dialog.getButton (DialogInterface.BUTTON_NEGATIVE);
371 buttonView.setEnabled (button.enabled);
372 }
373
374 if (size >= 3)
375 {
376 button = buttons.get (2);
377 buttonView
378 = dialog.getButton (DialogInterface.BUTTON_NEUTRAL);
379 buttonView.setEnabled (button.enabled);
380 }
381 }
382
383 return true;
384 }
385
386 /* Display this dialog for a suitable activity.
387 Value is false if the dialog could not be displayed,
388 and true otherwise. */
389
390 public boolean
391 display ()
392 {
393 Runnable runnable;
394 final EmacsHolder<Boolean> rc;
395
396 rc = new EmacsHolder<Boolean> ();
397 runnable = new Runnable () {
398 @Override
399 public void
400 run ()
401 {
402 synchronized (this)
403 {
404 rc.thing = display1 ();
405 notify ();
406 }
407 }
408 };
409
410 EmacsService.syncRunnable (runnable);
411 return rc.thing;
412 }
413
414
415
416 @Override
417 public void
418 onDismiss (DialogInterface dialog)
419 {
420 Log.d (TAG, "onDismiss: " + this);
421
422 if (wasButtonClicked)
423 return;
424
425 EmacsNative.sendContextMenu ((short) 0, 0, menuEventSerial);
426 }
427};
diff --git a/java/org/gnu/emacs/EmacsDialogButtonLayout.java b/java/org/gnu/emacs/EmacsDialogButtonLayout.java
new file mode 100644
index 00000000000..fd8d63d81d3
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDialogButtonLayout.java
@@ -0,0 +1,152 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22
23
24import android.content.Context;
25
26import android.view.View;
27import android.view.View.MeasureSpec;
28import android.view.ViewGroup;
29
30
31
32/* This ``view group'' implements a container widget for multiple
33 buttons of the type found in pop-up dialogs. It is used when
34 displaying a dialog box that contains more than three buttons, as
35 the default dialog box widget is not capable of holding more than
36 that many. */
37
38
39
40public final class EmacsDialogButtonLayout extends ViewGroup
41{
42 public
43 EmacsDialogButtonLayout (Context context)
44 {
45 super (context);
46 }
47
48 @Override
49 protected void
50 onMeasure (int widthMeasureSpec, int heightMeasureSpec)
51 {
52 int width, count, i, x, y, height, spec, tempSpec;
53 View view;
54
55 /* Obtain the width of this widget and create the measure
56 specification used to measure children. */
57
58 width = MeasureSpec.getSize (widthMeasureSpec);
59 spec = MeasureSpec.makeMeasureSpec (0, MeasureSpec.UNSPECIFIED);
60 tempSpec
61 = MeasureSpec.makeMeasureSpec (width, MeasureSpec.AT_MOST);
62 x = y = height = 0;
63
64 /* Run through each widget. */
65
66 count = getChildCount ();
67
68 for (i = 0; i < count; ++i)
69 {
70 view = getChildAt (i);
71
72 /* Measure this view. */
73 view.measure (spec, spec);
74
75 if (width - x < view.getMeasuredWidth ())
76 {
77 /* Move onto the next line, unless this line is empty. */
78
79 if (x != 0)
80 {
81 y += height;
82 height = x = 0;
83 }
84
85 if (view.getMeasuredWidth () > width)
86 /* Measure the view again, this time forcing it to be at
87 most width wide, if it is not already. */
88 view.measure (tempSpec, spec);
89 }
90
91 height = Math.max (height, view.getMeasuredHeight ());
92 x += view.getMeasuredWidth ();
93 }
94
95 /* Now set the measured size of this widget. */
96 setMeasuredDimension (width, y + height);
97 }
98
99 @Override
100 protected void
101 onLayout (boolean changed, int left, int top, int right,
102 int bottom)
103 {
104 int width, count, i, x, y, height, spec, tempSpec;
105 View view;
106
107 /* Obtain the width of this widget and create the measure
108 specification used to measure children. */
109
110 width = getMeasuredWidth ();
111 spec = MeasureSpec.makeMeasureSpec (0, MeasureSpec.UNSPECIFIED);
112 tempSpec
113 = MeasureSpec.makeMeasureSpec (width, MeasureSpec.AT_MOST);
114 x = y = height = 0;
115
116 /* Run through each widget. */
117
118 count = getChildCount ();
119
120 for (i = 0; i < count; ++i)
121 {
122 view = getChildAt (i);
123
124 /* Measure this view. */
125 view.measure (spec, spec);
126
127 if (width - x < view.getMeasuredWidth ())
128 {
129 /* Move onto the next line, unless this line is empty. */
130
131 if (x != 0)
132 {
133 y += height;
134 height = x = 0;
135 }
136
137 if (view.getMeasuredWidth () > width)
138 /* Measure the view again, this time forcing it to be at
139 most width wide, if it is not already. */
140 view.measure (tempSpec, spec);
141 }
142
143 /* Now assign this view its position. */
144 view.layout (x, y, x + view.getMeasuredWidth (),
145 y + view.getMeasuredHeight ());
146
147 /* And move on to the next widget. */
148 height = Math.max (height, view.getMeasuredHeight ());
149 x += view.getMeasuredWidth ();
150 }
151 }
152};
diff --git a/java/org/gnu/emacs/EmacsDirectoryEntry.java b/java/org/gnu/emacs/EmacsDirectoryEntry.java
new file mode 100644
index 00000000000..75c52e48002
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDirectoryEntry.java
@@ -0,0 +1,33 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22/* Structure holding a single ``directory entry'' from a document
23 provider. */
24
25public final class EmacsDirectoryEntry
26{
27 /* The type of this directory entry. 0 means a regular file and 1
28 means a directory. */
29 public int d_type;
30
31 /* The display name of the file represented. */
32 public String d_name;
33};
diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java
new file mode 100644
index 00000000000..96dc2bc6e14
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java
@@ -0,0 +1,578 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.content.Context;
23
24import android.database.Cursor;
25import android.database.MatrixCursor;
26
27import android.os.Build;
28import android.os.CancellationSignal;
29import android.os.ParcelFileDescriptor;
30
31import android.provider.DocumentsContract.Document;
32import android.provider.DocumentsContract.Root;
33import static android.provider.DocumentsContract.buildChildDocumentsUri;
34import android.provider.DocumentsProvider;
35
36import android.webkit.MimeTypeMap;
37
38import android.net.Uri;
39
40import java.io.File;
41import java.io.FileInputStream;
42import java.io.FileNotFoundException;
43import java.io.FileOutputStream;
44import java.io.IOException;
45
46/* ``Documents provider''. This allows Emacs's home directory to be
47 modified by other programs holding permissions to manage system
48 storage, which is useful to (for example) correct misconfigurations
49 which prevent Emacs from starting up.
50
51 This functionality is only available on Android 19 and later. */
52
53public final class EmacsDocumentsProvider extends DocumentsProvider
54{
55 /* Home directory. This is the directory whose contents are
56 initially returned to requesting applications. */
57 private File baseDir;
58
59 /* The default projection for requests for the root directory. */
60 private static final String[] DEFAULT_ROOT_PROJECTION;
61
62 /* The default projection for requests for a file. */
63 private static final String[] DEFAULT_DOCUMENT_PROJECTION;
64
65 static
66 {
67 DEFAULT_ROOT_PROJECTION = new String[] {
68 Root.COLUMN_ROOT_ID,
69 Root.COLUMN_MIME_TYPES,
70 Root.COLUMN_FLAGS,
71 Root.COLUMN_ICON,
72 Root.COLUMN_TITLE,
73 Root.COLUMN_SUMMARY,
74 Root.COLUMN_DOCUMENT_ID,
75 Root.COLUMN_AVAILABLE_BYTES,
76 };
77
78 DEFAULT_DOCUMENT_PROJECTION = new String[] {
79 Document.COLUMN_DOCUMENT_ID,
80 Document.COLUMN_MIME_TYPE,
81 Document.COLUMN_DISPLAY_NAME,
82 Document.COLUMN_LAST_MODIFIED,
83 Document.COLUMN_FLAGS,
84 Document.COLUMN_SIZE,
85 };
86 }
87
88 @Override
89 public boolean
90 onCreate ()
91 {
92 /* Set the base directory to Emacs's files directory. */
93 baseDir = getContext ().getFilesDir ();
94 return true;
95 }
96
97 @Override
98 public Cursor
99 queryRoots (String[] projection)
100 {
101 MatrixCursor result;
102 MatrixCursor.RowBuilder row;
103
104 /* If the requestor asked for nothing at all, then it wants some
105 data by default. */
106
107 if (projection == null)
108 projection = DEFAULT_ROOT_PROJECTION;
109
110 result = new MatrixCursor (projection);
111 row = result.newRow ();
112
113 /* Now create and add a row for each file in the base
114 directory. */
115 row.add (Root.COLUMN_ROOT_ID, baseDir.getAbsolutePath ());
116 row.add (Root.COLUMN_SUMMARY, "Emacs home directory");
117
118 /* Add the appropriate flags. */
119
120 row.add (Root.COLUMN_FLAGS, (Root.FLAG_SUPPORTS_CREATE
121 | Root.FLAG_SUPPORTS_IS_CHILD));
122 row.add (Root.COLUMN_ICON, R.drawable.emacs);
123 row.add (Root.FLAG_LOCAL_ONLY);
124 row.add (Root.COLUMN_TITLE, "Emacs");
125 row.add (Root.COLUMN_DOCUMENT_ID, baseDir.getAbsolutePath ());
126
127 return result;
128 }
129
130 private Uri
131 getNotificationUri (File file)
132 {
133 Uri updatedUri;
134
135 updatedUri
136 = buildChildDocumentsUri ("org.gnu.emacs",
137 file.getAbsolutePath ());
138
139 return updatedUri;
140 }
141
142 /* Inform the system that FILE's contents (or FILE itself) has
143 changed. */
144
145 private void
146 notifyChange (File file)
147 {
148 Uri updatedUri;
149 Context context;
150
151 context = getContext ();
152 updatedUri
153 = buildChildDocumentsUri ("org.gnu.emacs",
154 file.getAbsolutePath ());
155 context.getContentResolver ().notifyChange (updatedUri, null);
156 }
157
158 /* Inform the system that FILE's contents (or FILE itself) has
159 changed. FILE is a string describing containing the file name of
160 a directory as opposed to a File. */
161
162 private void
163 notifyChangeByName (String file)
164 {
165 Uri updatedUri;
166 Context context;
167
168 context = getContext ();
169 updatedUri
170 = buildChildDocumentsUri ("org.gnu.emacs", file);
171 context.getContentResolver ().notifyChange (updatedUri, null);
172 }
173
174 /* Return the MIME type of a file FILE. */
175
176 private String
177 getMimeType (File file)
178 {
179 String name, extension, mime;
180 int extensionSeparator;
181 MimeTypeMap singleton;
182
183 if (file.isDirectory ())
184 return Document.MIME_TYPE_DIR;
185
186 /* Abuse WebView stuff to get the file's MIME type. */
187 name = file.getName ();
188 extensionSeparator = name.lastIndexOf ('.');
189
190 if (extensionSeparator > 0)
191 {
192 singleton = MimeTypeMap.getSingleton ();
193 extension = name.substring (extensionSeparator + 1);
194 mime = singleton.getMimeTypeFromExtension (extension);
195
196 if (mime != null)
197 return mime;
198 }
199
200 return "application/octet-stream";
201 }
202
203 /* Append the specified FILE to the query result RESULT.
204 Handle both directories and ordinary files. */
205
206 private void
207 queryDocument1 (MatrixCursor result, File file)
208 {
209 MatrixCursor.RowBuilder row;
210 String fileName, displayName, mimeType;
211 int flags;
212
213 row = result.newRow ();
214 flags = 0;
215
216 /* fileName is a string that the system will ask for some time in
217 the future. Here, it is just the absolute name of the file. */
218 fileName = file.getAbsolutePath ();
219
220 /* If file is a directory, add the right flags for that. */
221
222 if (file.isDirectory ())
223 {
224 if (file.canWrite ())
225 {
226 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
227 flags |= Document.FLAG_SUPPORTS_DELETE;
228
229 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
230 flags |= Document.FLAG_SUPPORTS_RENAME;
231
232 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
233 flags |= Document.FLAG_SUPPORTS_MOVE;
234 }
235 }
236 else if (file.canWrite ())
237 {
238 /* Apply the correct flags for a writable file. */
239 flags |= Document.FLAG_SUPPORTS_WRITE;
240 flags |= Document.FLAG_SUPPORTS_DELETE;
241
242 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
243 flags |= Document.FLAG_SUPPORTS_RENAME;
244
245 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
246 {
247 flags |= Document.FLAG_SUPPORTS_REMOVE;
248 flags |= Document.FLAG_SUPPORTS_MOVE;
249 }
250 }
251
252 displayName = file.getName ();
253 mimeType = getMimeType (file);
254
255 row.add (Document.COLUMN_DOCUMENT_ID, fileName);
256 row.add (Document.COLUMN_DISPLAY_NAME, displayName);
257 row.add (Document.COLUMN_SIZE, file.length ());
258 row.add (Document.COLUMN_MIME_TYPE, mimeType);
259 row.add (Document.COLUMN_LAST_MODIFIED, file.lastModified ());
260 row.add (Document.COLUMN_FLAGS, flags);
261 }
262
263 @Override
264 public Cursor
265 queryDocument (String documentId, String[] projection)
266 throws FileNotFoundException
267 {
268 MatrixCursor result;
269 File file;
270 Context context;
271
272 file = new File (documentId);
273 context = getContext ();
274
275 if (projection == null)
276 projection = DEFAULT_DOCUMENT_PROJECTION;
277
278 result = new MatrixCursor (projection);
279 queryDocument1 (result, file);
280
281 /* Now allow interested applications to detect changes. */
282 result.setNotificationUri (context.getContentResolver (),
283 getNotificationUri (file));
284
285 return result;
286 }
287
288 @Override
289 public Cursor
290 queryChildDocuments (String parentDocumentId, String[] projection,
291 String sortOrder) throws FileNotFoundException
292 {
293 MatrixCursor result;
294 File directory;
295 File[] files;
296 Context context;
297
298 if (projection == null)
299 projection = DEFAULT_DOCUMENT_PROJECTION;
300
301 result = new MatrixCursor (projection);
302
303 /* Try to open the file corresponding to the location being
304 requested. */
305 directory = new File (parentDocumentId);
306
307 /* Look up each child. */
308 files = directory.listFiles ();
309
310 if (files != null)
311 {
312 /* Now add each child. */
313 for (File child : files)
314 queryDocument1 (result, child);
315 }
316
317 context = getContext ();
318
319 /* Now allow interested applications to detect changes. */
320 result.setNotificationUri (context.getContentResolver (),
321 getNotificationUri (directory));
322
323 return result;
324 }
325
326 @Override
327 public ParcelFileDescriptor
328 openDocument (String documentId, String mode,
329 CancellationSignal signal) throws FileNotFoundException
330 {
331 return ParcelFileDescriptor.open (new File (documentId),
332 ParcelFileDescriptor.parseMode (mode));
333 }
334
335 @Override
336 public String
337 createDocument (String documentId, String mimeType,
338 String displayName) throws FileNotFoundException
339 {
340 File file, parentFile;
341 boolean rc;
342
343 file = new File (documentId, displayName);
344
345 try
346 {
347 rc = false;
348
349 if (Document.MIME_TYPE_DIR.equals (mimeType))
350 {
351 file.mkdirs ();
352
353 if (file.isDirectory ())
354 rc = true;
355 }
356 else
357 {
358 file.createNewFile ();
359
360 if (file.isFile ()
361 && file.setWritable (true)
362 && file.setReadable (true))
363 rc = true;
364 }
365
366 if (!rc)
367 throw new FileNotFoundException ("rc != 1");
368 }
369 catch (IOException e)
370 {
371 throw new FileNotFoundException (e.toString ());
372 }
373
374 parentFile = file.getParentFile ();
375
376 if (parentFile != null)
377 notifyChange (parentFile);
378
379 return file.getAbsolutePath ();
380 }
381
382 private void
383 deleteDocument1 (File child)
384 {
385 File[] children;
386
387 /* Don't delete symlinks recursively.
388
389 Calling readlink or stat is problematic due to file name
390 encoding problems, so try to delete the file first, and only
391 try to delete files recursively afterword. */
392
393 if (child.delete ())
394 return;
395
396 children = child.listFiles ();
397
398 if (children != null)
399 {
400 for (File file : children)
401 deleteDocument1 (file);
402 }
403
404 child.delete ();
405 }
406
407 @Override
408 public void
409 deleteDocument (String documentId)
410 throws FileNotFoundException
411 {
412 File file, parent;
413 File[] children;
414
415 /* Java makes recursively deleting a file hard. File name
416 encoding issues also prevent easily calling into C... */
417
418 file = new File (documentId);
419 parent = file.getParentFile ();
420
421 if (parent == null)
422 throw new RuntimeException ("trying to delete file without"
423 + " parent!");
424
425 if (file.delete ())
426 {
427 /* Tell the system about the change. */
428 notifyChange (parent);
429 return;
430 }
431
432 children = file.listFiles ();
433
434 if (children != null)
435 {
436 for (File child : children)
437 deleteDocument1 (child);
438 }
439
440 if (file.delete ())
441 /* Tell the system about the change. */
442 notifyChange (parent);
443 }
444
445 @Override
446 public void
447 removeDocument (String documentId, String parentDocumentId)
448 throws FileNotFoundException
449 {
450 deleteDocument (documentId);
451 }
452
453 @Override
454 public String
455 getDocumentType (String documentId)
456 {
457 return getMimeType (new File (documentId));
458 }
459
460 @Override
461 public String
462 renameDocument (String documentId, String displayName)
463 throws FileNotFoundException
464 {
465 File file, newName;
466 File parent;
467
468 file = new File (documentId);
469 parent = file.getParentFile ();
470 newName = new File (parent, displayName);
471
472 if (parent == null)
473 throw new FileNotFoundException ("parent is null");
474
475 file = new File (documentId);
476
477 if (!file.renameTo (newName))
478 return null;
479
480 notifyChange (parent);
481 return newName.getAbsolutePath ();
482 }
483
484 @Override
485 public boolean
486 isChildDocument (String parentDocumentId, String documentId)
487 {
488 return documentId.startsWith (parentDocumentId);
489 }
490
491 @Override
492 public String
493 moveDocument (String sourceDocumentId,
494 String sourceParentDocumentId,
495 String targetParentDocumentId)
496 throws FileNotFoundException
497 {
498 File file, newName;
499 FileInputStream inputStream;
500 FileOutputStream outputStream;
501 byte buffer[];
502 int length;
503
504 file = new File (sourceDocumentId);
505
506 /* Now, create the file name of the parent document. */
507 newName = new File (targetParentDocumentId,
508 file.getName ());
509
510 /* Try to perform a simple rename, before falling back to
511 copying. */
512
513 if (file.renameTo (newName))
514 {
515 notifyChangeByName (file.getParent ());
516 notifyChangeByName (targetParentDocumentId);
517 return newName.getAbsolutePath ();
518 }
519
520 /* If that doesn't work, create the new file and copy over the old
521 file's contents. */
522
523 inputStream = null;
524 outputStream = null;
525
526 try
527 {
528 if (!newName.createNewFile ()
529 || !newName.setWritable (true)
530 || !newName.setReadable (true))
531 throw new FileNotFoundException ("failed to create new file");
532
533 /* Open the file in preparation for a copy. */
534
535 inputStream = new FileInputStream (file);
536 outputStream = new FileOutputStream (newName);
537
538 /* Allocate the buffer used to hold data. */
539
540 buffer = new byte[4096];
541
542 while ((length = inputStream.read (buffer)) > 0)
543 outputStream.write (buffer, 0, length);
544 }
545 catch (IOException e)
546 {
547 throw new FileNotFoundException ("IOException: " + e);
548 }
549 finally
550 {
551 try
552 {
553 if (inputStream != null)
554 inputStream.close ();
555 }
556 catch (IOException e)
557 {
558
559 }
560
561 try
562 {
563 if (outputStream != null)
564 outputStream.close ();
565 }
566 catch (IOException e)
567 {
568
569 }
570 }
571
572 file.delete ();
573 notifyChangeByName (file.getParent ());
574 notifyChangeByName (targetParentDocumentId);
575
576 return newName.getAbsolutePath ();
577 }
578}
diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java
new file mode 100644
index 00000000000..d367ccff9c4
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDrawLine.java
@@ -0,0 +1,79 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Canvas;
23import android.graphics.Paint;
24import android.graphics.Rect;
25
26public final class EmacsDrawLine
27{
28 public static void
29 perform (EmacsDrawable drawable, EmacsGC gc,
30 int x, int y, int x2, int y2)
31 {
32 Rect rect;
33 Canvas canvas;
34 Paint paint;
35 int x0, x1, y0, y1;
36
37 /* TODO implement stippling. */
38 if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED)
39 return;
40
41 /* Calculate the leftmost and rightmost points. */
42
43 x0 = Math.min (x, x2 + 1);
44 x1 = Math.max (x, x2 + 1);
45 y0 = Math.min (y, y2 + 1);
46 y1 = Math.max (y, y2 + 1);
47
48 /* And the clip rectangle. */
49
50 paint = gc.gcPaint;
51 rect = new Rect (x0, y0, x1, y1);
52 canvas = drawable.lockCanvas (gc);
53
54 if (canvas == null)
55 return;
56
57 paint.setStyle (Paint.Style.FILL);
58
59 /* Since drawLine has PostScript style behavior, adjust the
60 coordinates appropriately.
61
62 The left most pixel of a straight line is always partially
63 filled. Patch it in manually. */
64
65 if (gc.clip_mask == null)
66 {
67 canvas.drawLine ((float) x + 0.5f, (float) y + 0.5f,
68 (float) x2 + 0.5f, (float) y2 + 0.5f,
69 paint);
70
71 if (x2 > x)
72 canvas.drawRect (new Rect (x, y, x + 1, y + 1), paint);
73 }
74
75 /* DrawLine with clip mask not implemented; it is not used by
76 Emacs. */
77 drawable.damageRect (rect);
78 }
79}
diff --git a/java/org/gnu/emacs/EmacsDrawPoint.java b/java/org/gnu/emacs/EmacsDrawPoint.java
new file mode 100644
index 00000000000..6a1cb744d60
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDrawPoint.java
@@ -0,0 +1,34 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22public final class EmacsDrawPoint
23{
24 public static void
25 perform (EmacsDrawable drawable,
26 EmacsGC immutableGC, int x, int y)
27 {
28 /* Use EmacsFillRectangle instead of EmacsDrawRectangle, as the
29 latter actually draws a rectangle one pixel wider than
30 specified. */
31 EmacsFillRectangle.perform (drawable, immutableGC,
32 x, y, 1, 1);
33 }
34}
diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java
new file mode 100644
index 00000000000..e1261b4a2d2
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDrawRectangle.java
@@ -0,0 +1,120 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Bitmap;
23import android.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.Rect;
26import android.graphics.RectF;
27
28import android.util.Log;
29
30public final class EmacsDrawRectangle
31{
32 public static void
33 perform (EmacsDrawable drawable, EmacsGC gc,
34 int x, int y, int width, int height)
35 {
36 Paint maskPaint, paint;
37 Canvas maskCanvas;
38 Bitmap maskBitmap;
39 Rect maskRect, dstRect;
40 Canvas canvas;
41 Bitmap clipBitmap;
42
43 /* TODO implement stippling. */
44 if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED)
45 return;
46
47 canvas = drawable.lockCanvas (gc);
48
49 if (canvas == null)
50 return;
51
52 paint = gc.gcPaint;
53 paint.setStyle (Paint.Style.STROKE);
54
55 if (gc.clip_mask == null)
56 /* Use canvas.drawRect with a RectF. That seems to reliably
57 get PostScript behavior. */
58 canvas.drawRect (new RectF (x + 0.5f, y + 0.5f,
59 x + width + 0.5f,
60 y + height + 0.5f),
61 paint);
62 else
63 {
64 /* Drawing with a clip mask involves calculating the
65 intersection of the clip mask with the dst rect, and
66 extrapolating the corresponding part of the src rect. */
67 clipBitmap = gc.clip_mask.bitmap;
68 dstRect = new Rect (x, y, x + width, y + height);
69 maskRect = new Rect (gc.clip_x_origin,
70 gc.clip_y_origin,
71 (gc.clip_x_origin
72 + clipBitmap.getWidth ()),
73 (gc.clip_y_origin
74 + clipBitmap.getHeight ()));
75
76 if (!maskRect.setIntersect (dstRect, maskRect))
77 /* There is no intersection between the clip mask and the
78 dest rect. */
79 return;
80
81 /* Finally, create a temporary bitmap that is the size of
82 maskRect. */
83
84 maskBitmap
85 = Bitmap.createBitmap (maskRect.width (), maskRect.height (),
86 Bitmap.Config.ARGB_8888);
87
88 /* Draw the mask onto the maskBitmap. */
89 maskCanvas = new Canvas (maskBitmap);
90 maskRect.offset (-gc.clip_x_origin,
91 -gc.clip_y_origin);
92 maskCanvas.drawBitmap (gc.clip_mask.bitmap,
93 maskRect, new Rect (0, 0,
94 maskRect.width (),
95 maskRect.height ()),
96 paint);
97 maskRect.offset (gc.clip_x_origin,
98 gc.clip_y_origin);
99
100 /* Set the transfer mode to SRC_IN to preserve only the parts
101 of the source that overlap with the mask. */
102 maskPaint = new Paint ();
103 maskPaint.setXfermode (EmacsGC.srcInAlu);
104 maskPaint.setStyle (Paint.Style.STROKE);
105
106 /* Draw the source. */
107 maskCanvas.drawRect (maskRect, maskPaint);
108
109 /* Finally, draw the mask bitmap to the destination. */
110 paint.setXfermode (null);
111 canvas.drawBitmap (maskBitmap, null, maskRect, paint);
112
113 /* Recycle this unused bitmap. */
114 maskBitmap.recycle ();
115 }
116
117 drawable.damageRect (new Rect (x, y, x + width + 1,
118 y + height + 1));
119 }
120}
diff --git a/java/org/gnu/emacs/EmacsDrawable.java b/java/org/gnu/emacs/EmacsDrawable.java
new file mode 100644
index 00000000000..f2f8885e976
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDrawable.java
@@ -0,0 +1,32 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Rect;
23import android.graphics.Bitmap;
24import android.graphics.Canvas;
25
26public interface EmacsDrawable
27{
28 public Canvas lockCanvas (EmacsGC gc);
29 public void damageRect (Rect damageRect);
30 public Bitmap getBitmap ();
31 public boolean isDestroyed ();
32};
diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java
new file mode 100644
index 00000000000..4ae3882cab4
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsFillPolygon.java
@@ -0,0 +1,80 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Canvas;
23import android.graphics.Paint;
24import android.graphics.Path;
25import android.graphics.Point;
26import android.graphics.Rect;
27import android.graphics.RectF;
28
29public final class EmacsFillPolygon
30{
31 public static void
32 perform (EmacsDrawable drawable, EmacsGC gc, Point points[])
33 {
34 Canvas canvas;
35 Path path;
36 Paint paint;
37 Rect rect;
38 RectF rectF;
39 int i;
40
41 canvas = drawable.lockCanvas (gc);
42
43 if (canvas == null)
44 return;
45
46 paint = gc.gcPaint;
47
48 /* Build the path from the given array of points. */
49 path = new Path ();
50
51 if (points.length >= 1)
52 {
53 path.moveTo (points[0].x, points[0].y);
54
55 for (i = 1; i < points.length; ++i)
56 path.lineTo (points[i].x, points[i].y);
57
58 path.close ();
59 }
60
61 /* Compute the damage rectangle. */
62 rectF = new RectF (0, 0, 0, 0);
63 path.computeBounds (rectF, true);
64
65 rect = new Rect ((int) Math.floor (rectF.left),
66 (int) Math.floor (rectF.top),
67 (int) Math.ceil (rectF.right),
68 (int) Math.ceil (rectF.bottom));
69
70 paint.setStyle (Paint.Style.FILL);
71
72 if (gc.clip_mask == null)
73 canvas.drawPath (path, paint);
74
75 drawable.damageRect (rect);
76
77 /* FillPolygon with clip mask not implemented; it is not used by
78 Emacs. */
79 }
80}
diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java
new file mode 100644
index 00000000000..461fd3c639c
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsFillRectangle.java
@@ -0,0 +1,116 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Bitmap;
23import android.graphics.Canvas;
24import android.graphics.Paint;
25import android.graphics.Rect;
26
27import android.util.Log;
28
29public final class EmacsFillRectangle
30{
31 public static void
32 perform (EmacsDrawable drawable, EmacsGC gc,
33 int x, int y, int width, int height)
34 {
35 Paint maskPaint, paint;
36 Canvas maskCanvas;
37 Bitmap maskBitmap;
38 Rect rect;
39 Rect maskRect, dstRect;
40 Canvas canvas;
41 Bitmap clipBitmap;
42
43 /* TODO implement stippling. */
44 if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED)
45 return;
46
47 canvas = drawable.lockCanvas (gc);
48
49 if (canvas == null)
50 return;
51
52 paint = gc.gcPaint;
53 rect = new Rect (x, y, x + width, y + height);
54
55 paint.setStyle (Paint.Style.FILL);
56
57 if (gc.clip_mask == null)
58 canvas.drawRect (rect, paint);
59 else
60 {
61 /* Drawing with a clip mask involves calculating the
62 intersection of the clip mask with the dst rect, and
63 extrapolating the corresponding part of the src rect. */
64
65 clipBitmap = gc.clip_mask.bitmap;
66 dstRect = new Rect (x, y, x + width, y + height);
67 maskRect = new Rect (gc.clip_x_origin,
68 gc.clip_y_origin,
69 (gc.clip_x_origin
70 + clipBitmap.getWidth ()),
71 (gc.clip_y_origin
72 + clipBitmap.getHeight ()));
73
74 if (!maskRect.setIntersect (dstRect, maskRect))
75 /* There is no intersection between the clip mask and the
76 dest rect. */
77 return;
78
79 /* Finally, create a temporary bitmap that is the size of
80 maskRect. */
81
82 maskBitmap
83 = Bitmap.createBitmap (maskRect.width (), maskRect.height (),
84 Bitmap.Config.ARGB_8888);
85
86 /* Draw the mask onto the maskBitmap. */
87 maskCanvas = new Canvas (maskBitmap);
88 maskRect.offset (-gc.clip_x_origin,
89 -gc.clip_y_origin);
90 maskCanvas.drawBitmap (gc.clip_mask.bitmap,
91 maskRect, new Rect (0, 0,
92 maskRect.width (),
93 maskRect.height ()),
94 paint);
95 maskRect.offset (gc.clip_x_origin,
96 gc.clip_y_origin);
97
98 /* Set the transfer mode to SRC_IN to preserve only the parts
99 of the source that overlap with the mask. */
100 maskPaint = new Paint ();
101 maskPaint.setXfermode (EmacsGC.srcInAlu);
102
103 /* Draw the source. */
104 maskCanvas.drawRect (maskRect, maskPaint);
105
106 /* Finally, draw the mask bitmap to the destination. */
107 paint.setXfermode (null);
108 canvas.drawBitmap (maskBitmap, null, maskRect, paint);
109
110 /* Recycle this unused bitmap. */
111 maskBitmap.recycle ();
112 }
113
114 drawable.damageRect (rect);
115 }
116}
diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java
new file mode 100644
index 00000000000..ff52899a897
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsFontDriver.java
@@ -0,0 +1,173 @@
1/* Font backend for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.os.Build;
23
24/* This code is mostly unused. See sfntfont-android.c for the code
25 that is actually used. */
26
27public abstract class EmacsFontDriver
28{
29 /* Font weights. */
30 public static final int THIN = 0;
31 public static final int ULTRA_LIGHT = 40;
32 public static final int LIGHT = 50;
33 public static final int SEMI_LIGHT = 55;
34 public static final int REGULAR = 80;
35 public static final int MEDIUM = 100;
36 public static final int SEMI_BOLD = 180;
37 public static final int BOLD = 200;
38 public static final int EXTRA_BOLD = 205;
39 public static final int BLACK = 210;
40 public static final int ULTRA_HEAVY = 250;
41
42 /* Font slants. */
43 public static final int REVERSE_OBLIQUE = 0;
44 public static final int REVERSE_ITALIC = 10;
45 public static final int NORMAL = 100;
46 public static final int ITALIC = 200;
47 public static final int OBLIQUE = 210;
48
49 /* Font widths. */
50 public static final int ULTRA_CONDENSED = 50;
51 public static final int EXTRA_CONDENSED = 63;
52 public static final int CONDENSED = 75;
53 public static final int SEMI_CONDENSED = 87;
54 public static final int UNSPECIFIED = 100;
55 public static final int SEMI_EXPANDED = 113;
56 public static final int EXPANDED = 125;
57 public static final int EXTRA_EXPANDED = 150;
58 public static final int ULTRA_EXPANDED = 200;
59
60 /* Font spacings. */
61 public static final int PROPORTIONAL = 0;
62 public static final int DUAL = 90;
63 public static final int MONO = 100;
64 public static final int CHARCELL = 110;
65
66 public static class FontSpec
67 {
68 /* The fields below mean the same as they do in enum
69 font_property_index in font.h. */
70
71 public String foundry;
72 public String family;
73 public String adstyle;
74 public String registry;
75 public Integer width;
76 public Integer weight;
77 public Integer slant;
78 public Integer size;
79 public Integer spacing;
80 public Integer avgwidth;
81 public Integer dpi;
82
83 @Override
84 public String
85 toString ()
86 {
87 return ("foundry: " + foundry
88 + " family: " + family
89 + " adstyle: " + adstyle
90 + " registry: " + registry
91 + " width: " + width
92 + " weight: " + weight
93 + " slant: " + slant
94 + " spacing: " + spacing
95 + " avgwidth: " + avgwidth
96 + " dpi: " + dpi);
97 }
98 };
99
100 public static class FontMetrics
101 {
102 public short lbearing;
103 public short rbearing;
104 public short width;
105 public short ascent;
106 public short descent;
107
108 @Override
109 public String
110 toString ()
111 {
112 return ("lbearing " + lbearing
113 + " rbearing " + rbearing
114 + " width " + width
115 + " ascent " + ascent
116 + " descent " + descent);
117 }
118 }
119
120 public static class FontEntity extends FontSpec
121 {
122 /* No extra fields here. */
123 };
124
125 public abstract class FontObject extends FontSpec
126 {
127 public int minWidth;
128 public int maxWidth;
129 public int pixelSize;
130 public int height;
131 public int spaceWidth;
132 public int averageWidth;
133 public int ascent;
134 public int descent;
135 public int underlineThickness;
136 public int underlinePosition;
137 public int baselineOffset;
138 public int relativeCompose;
139 public int defaultAscent;
140 public int encodingCharset;
141 public int repertoryCharset;
142
143 public
144 FontObject ()
145 {
146 encodingCharset = -1;
147 repertoryCharset = -1;
148 }
149 };
150
151 /* These mean the same as they do in struct font_driver. */
152 public abstract FontEntity[] list (FontSpec fontSpec);
153 public abstract FontEntity match (FontSpec fontSpec);
154 public abstract String[] listFamilies ();
155 public abstract FontObject openFont (FontEntity fontEntity, int pixelSize);
156 public abstract int hasChar (FontSpec font, char charCode);
157 public abstract void textExtents (FontObject font, int code[],
158 FontMetrics fontMetrics);
159 public abstract int encodeChar (FontObject fontObject, char charCode);
160 public abstract int draw (FontObject fontObject, EmacsGC gc,
161 EmacsDrawable drawable, int[] chars,
162 int x, int y, int backgroundWidth,
163 boolean withBackground);
164
165 public static EmacsFontDriver
166 createFontDriver ()
167 {
168 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
169 return new EmacsSdk23FontDriver ();
170
171 return new EmacsSdk7FontDriver ();
172 }
173};
diff --git a/java/org/gnu/emacs/EmacsGC.java b/java/org/gnu/emacs/EmacsGC.java
new file mode 100644
index 00000000000..a7467cb9bd0
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsGC.java
@@ -0,0 +1,121 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Rect;
23import android.graphics.Paint;
24
25import android.graphics.PorterDuff.Mode;
26import android.graphics.PorterDuffXfermode;
27import android.graphics.Xfermode;
28
29/* X like graphics context structures. Keep the enums in synch with
30 androidgui.h! */
31
32public final class EmacsGC extends EmacsHandleObject
33{
34 public static final int GC_COPY = 0;
35 public static final int GC_XOR = 1;
36
37 public static final int GC_FILL_SOLID = 0;
38 public static final int GC_FILL_OPAQUE_STIPPLED = 1;
39
40 public static final Xfermode xorAlu, srcInAlu;
41
42 public int function, fill_style;
43 public int foreground, background;
44 public int clip_x_origin, clip_y_origin;
45 public int ts_origin_x, ts_origin_y;
46 public Rect clip_rects[], real_clip_rects[];
47 public EmacsPixmap clip_mask, stipple;
48 public Paint gcPaint;
49
50 /* ID incremented every time the clipping rectangles of any GC
51 changes. */
52 private static long clip_serial;
53
54 /* The value of clipRectID after the last time this GCs clip
55 rectangles changed. 0 if there are no clip rectangles. */
56 public long clipRectID;
57
58 static
59 {
60 xorAlu = new PorterDuffXfermode (Mode.XOR);
61 srcInAlu = new PorterDuffXfermode (Mode.SRC_IN);
62 }
63
64 /* The following fields are only set on immutable GCs. */
65
66 public
67 EmacsGC (short handle)
68 {
69 /* For historical reasons the C code has an extra layer of
70 indirection above this GC handle. struct android_gc is the GC
71 used by Emacs code, while android_gcontext is the type of the
72 handle. */
73 super (handle);
74
75 fill_style = GC_FILL_SOLID;
76 function = GC_COPY;
77 foreground = 0;
78 background = 0xffffff;
79 gcPaint = new Paint ();
80 }
81
82 /* Mark this GC as dirty. Apply parameters to the paint and
83 recompute real_clip_rects. */
84
85 public void
86 markDirty (boolean clipRectsChanged)
87 {
88 int i;
89
90 if (clipRectsChanged)
91 {
92 if ((ts_origin_x != 0 || ts_origin_y != 0)
93 && clip_rects != null)
94 {
95 real_clip_rects = new Rect[clip_rects.length];
96
97 for (i = 0; i < clip_rects.length; ++i)
98 {
99 real_clip_rects[i] = new Rect (clip_rects[i]);
100 real_clip_rects[i].offset (ts_origin_x, ts_origin_y);
101 }
102 }
103 else
104 real_clip_rects = clip_rects;
105
106 clipRectID = ++clip_serial;
107 }
108
109 gcPaint.setStrokeWidth (1f);
110 gcPaint.setColor (foreground | 0xff000000);
111 gcPaint.setXfermode (function == GC_XOR
112 ? xorAlu : srcInAlu);
113 }
114
115 public void
116 resetXfermode ()
117 {
118 gcPaint.setXfermode (function == GC_XOR
119 ? xorAlu : srcInAlu);
120 }
121};
diff --git a/java/org/gnu/emacs/EmacsHandleObject.java b/java/org/gnu/emacs/EmacsHandleObject.java
new file mode 100644
index 00000000000..5b889895337
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsHandleObject.java
@@ -0,0 +1,59 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.lang.IllegalStateException;
23
24/* This defines something that is a so-called ``handle''. Handles
25 must be created by C code, and will remain existing until
26 destroyHandle is called. C code then refers to the handle by a
27 number which maps into the Java object representing the handle.
28
29 All handle operations must be done from the Emacs thread. */
30
31public abstract class EmacsHandleObject
32{
33 /* Whether or not this handle has been destroyed. */
34 volatile boolean destroyed;
35
36 /* The handle associated with this object. */
37 public short handle;
38
39 public
40 EmacsHandleObject (short handle)
41 {
42 this.handle = handle;
43 }
44
45 public void
46 destroyHandle () throws IllegalStateException
47 {
48 synchronized (this)
49 {
50 destroyed = true;
51 }
52 }
53
54 public boolean
55 isDestroyed ()
56 {
57 return destroyed;
58 }
59};
diff --git a/java/org/gnu/emacs/EmacsHolder.java b/java/org/gnu/emacs/EmacsHolder.java
new file mode 100644
index 00000000000..6cd48ba57ce
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsHolder.java
@@ -0,0 +1,30 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22
23
24/* This class serves as a simple reference to an object of type T.
25 Nothing could be found inside the standard library. */
26
27public final class EmacsHolder<T>
28{
29 T thing;
30};
diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java
new file mode 100644
index 00000000000..c3764a7b29f
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsInputConnection.java
@@ -0,0 +1,698 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.os.Build;
23import android.os.Bundle;
24import android.os.Handler;
25
26import android.view.KeyEvent;
27
28import android.view.inputmethod.CompletionInfo;
29import android.view.inputmethod.CorrectionInfo;
30import android.view.inputmethod.ExtractedText;
31import android.view.inputmethod.ExtractedTextRequest;
32import android.view.inputmethod.InputConnection;
33import android.view.inputmethod.InputContentInfo;
34import android.view.inputmethod.SurroundingText;
35import android.view.inputmethod.TextAttribute;
36import android.view.inputmethod.TextSnapshot;
37
38import android.util.Log;
39
40/* Android input methods, take number six. See textconv.c for more
41 details; this is more-or-less a thin wrapper around that file. */
42
43public final class EmacsInputConnection implements InputConnection
44{
45 private static final String TAG = "EmacsInputConnection";
46
47 /* View associated with this input connection. */
48 private EmacsView view;
49
50 /* The handle ID associated with that view's window. */
51 private short windowHandle;
52
53 /* Number of batch edits currently underway. Used to avoid
54 synchronizing with the Emacs thread after each
55 `endBatchEdit'. */
56 private int batchEditCount;
57
58 /* Whether or not to synchronize and call `updateIC' with the
59 selection position after committing text.
60
61 This helps with on screen keyboard programs found in some vendor
62 versions of Android, which rely on immediate updates to the point
63 position after text is commited in order to place the cursor
64 within that text. */
65
66 private static boolean syncAfterCommit;
67
68 /* Whether or not to return empty text with the offset set to zero
69 if a request arrives that has no flags set and has requested no
70 characters at all.
71
72 This is necessary with on screen keyboard programs found in some
73 vendor versions of Android which don't rely on the documented
74 meaning of `ExtractedText.startOffset', and instead take the
75 selection offset inside at face value. */
76
77 private static boolean extractAbsoluteOffsets;
78
79 static
80 {
81 if (Build.MANUFACTURER.equalsIgnoreCase ("Huawei")
82 || Build.MANUFACTURER.equalsIgnoreCase ("Honor"))
83 extractAbsoluteOffsets = syncAfterCommit = true;
84
85 /* The Samsung and Vivo keyboards take `selectionStart' at face
86 value if some text is returned, and also searches for words
87 solely within that text. However, when no text is returned, it
88 falls back to getTextAfterCursor and getTextBeforeCursor. */
89 if (Build.MANUFACTURER.equalsIgnoreCase ("Samsung")
90 || Build.MANUFACTURER.equalsIgnoreCase ("Vivo"))
91 extractAbsoluteOffsets = true;
92 };
93
94
95 public
96 EmacsInputConnection (EmacsView view)
97 {
98 this.view = view;
99 this.windowHandle = view.window.handle;
100 }
101
102
103 /* The functions below are called by input methods whenever they
104 need to perform an edit. */
105
106 @Override
107 public boolean
108 beginBatchEdit ()
109 {
110 /* Return if the input connection is out of date. */
111 if (view.icSerial < view.icGeneration)
112 return false;
113
114 if (EmacsService.DEBUG_IC)
115 Log.d (TAG, "beginBatchEdit");
116
117 EmacsNative.beginBatchEdit (windowHandle);
118
119 /* Keep a record of the number of outstanding batch edits here as
120 well. */
121 batchEditCount++;
122 return true;
123 }
124
125 @Override
126 public boolean
127 endBatchEdit ()
128 {
129 /* Return if the input connection is out of date. */
130 if (view.icSerial < view.icGeneration)
131 return false;
132
133 if (EmacsService.DEBUG_IC)
134 Log.d (TAG, "endBatchEdit");
135
136 EmacsNative.endBatchEdit (windowHandle);
137
138 /* Subtract one from the UI thread record of the number of batch
139 edits currently under way. */
140
141 if (batchEditCount > 0)
142 batchEditCount -= 1;
143
144 return batchEditCount > 0;
145 }
146
147 public boolean
148 commitCompletion (CompletionInfo info)
149 {
150 /* Return if the input connection is out of date. */
151 if (view.icSerial < view.icGeneration)
152 return false;
153
154 if (EmacsService.DEBUG_IC)
155 Log.d (TAG, "commitCompletion: " + info);
156
157 EmacsNative.commitCompletion (windowHandle,
158 info.getText ().toString (),
159 info.getPosition ());
160 return true;
161 }
162
163 @Override
164 public boolean
165 commitCorrection (CorrectionInfo info)
166 {
167 /* The input method calls this function not to commit text, but to
168 indicate that a subsequent edit will consist of a correction.
169 Emacs has no use for this information.
170
171 Of course this completely contradicts the provided
172 documentation, but this is how Android actually behaves. */
173 return false;
174 }
175
176 @Override
177 public boolean
178 commitText (CharSequence text, int newCursorPosition)
179 {
180 int[] selection;
181
182 /* Return if the input connection is out of date. */
183 if (view.icSerial < view.icGeneration)
184 return false;
185
186 if (EmacsService.DEBUG_IC)
187 Log.d (TAG, "commitText: " + text + " " + newCursorPosition);
188
189 EmacsNative.commitText (windowHandle, text.toString (),
190 newCursorPosition);
191
192 if (syncAfterCommit)
193 {
194 /* Synchronize with the Emacs thread, obtain the new
195 selection, and report it immediately. */
196
197 selection = EmacsNative.getSelection (windowHandle);
198
199 if (EmacsService.DEBUG_IC && selection != null)
200 Log.d (TAG, "commitText: new selection is " + selection[0]
201 + ", by " + selection[1]);
202
203 if (selection != null)
204 /* N.B. that the composing region is removed after text is
205 committed. */
206 view.imManager.updateSelection (view, selection[0],
207 selection[1], -1, -1);
208 }
209
210 return true;
211 }
212
213 @Override
214 public boolean
215 commitText (CharSequence text, int newCursorPosition,
216 TextAttribute textAttribute)
217 {
218 return commitText (text, newCursorPosition);
219 }
220
221 @Override
222 public boolean
223 deleteSurroundingText (int leftLength, int rightLength)
224 {
225 /* Return if the input connection is out of date. */
226 if (view.icSerial < view.icGeneration)
227 return false;
228
229 if (EmacsService.DEBUG_IC)
230 Log.d (TAG, ("deleteSurroundingText: "
231 + leftLength + " " + rightLength));
232
233 EmacsNative.deleteSurroundingText (windowHandle, leftLength,
234 rightLength);
235 return true;
236 }
237
238 @Override
239 public boolean
240 deleteSurroundingTextInCodePoints (int leftLength, int rightLength)
241 {
242 /* Emacs returns characters which cannot be represented in a Java
243 `char' as NULL characters, so code points always reflect
244 characters themselves. */
245 return deleteSurroundingText (leftLength, rightLength);
246 }
247
248 @Override
249 public boolean
250 finishComposingText ()
251 {
252 /* Return if the input connection is out of date. */
253 if (view.icSerial < view.icGeneration)
254 return false;
255
256 if (EmacsService.DEBUG_IC)
257 Log.d (TAG, "finishComposingText");
258
259 EmacsNative.finishComposingText (windowHandle);
260 return true;
261 }
262
263 @Override
264 public String
265 getSelectedText (int flags)
266 {
267 /* Return if the input connection is out of date. */
268 if (view.icSerial < view.icGeneration)
269 return null;
270
271 if (EmacsService.DEBUG_IC)
272 Log.d (TAG, "getSelectedText: " + flags);
273
274 return EmacsNative.getSelectedText (windowHandle, flags);
275 }
276
277 @Override
278 public String
279 getTextAfterCursor (int length, int flags)
280 {
281 String string;
282
283 /* Return if the input connection is out of date. */
284 if (view.icSerial < view.icGeneration)
285 return null;
286
287 if (EmacsService.DEBUG_IC)
288 Log.d (TAG, "getTextAfterCursor: " + length + " " + flags);
289
290 string = EmacsNative.getTextAfterCursor (windowHandle, length,
291 flags);
292
293 if (EmacsService.DEBUG_IC)
294 Log.d (TAG, " --> " + string);
295
296 return string;
297 }
298
299 @Override
300 public String
301 getTextBeforeCursor (int length, int flags)
302 {
303 String string;
304
305 /* Return if the input connection is out of date. */
306 if (view.icSerial < view.icGeneration)
307 return null;
308
309 if (EmacsService.DEBUG_IC)
310 Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags);
311
312 string = EmacsNative.getTextBeforeCursor (windowHandle, length,
313 flags);
314
315 if (EmacsService.DEBUG_IC)
316 Log.d (TAG, " --> " + string);
317
318 return string;
319 }
320
321 @Override
322 public boolean
323 setComposingText (CharSequence text, int newCursorPosition)
324 {
325 /* Return if the input connection is out of date. */
326 if (view.icSerial < view.icGeneration)
327 return false;
328
329 if (EmacsService.DEBUG_IC)
330 Log.d (TAG, ("setComposingText: "
331 + text + " ## " + newCursorPosition));
332
333 EmacsNative.setComposingText (windowHandle, text.toString (),
334 newCursorPosition);
335 return true;
336 }
337
338 @Override
339 public boolean
340 setComposingText (CharSequence text, int newCursorPosition,
341 TextAttribute textAttribute)
342 {
343 return setComposingText (text, newCursorPosition);
344 }
345
346 @Override
347 public boolean
348 setComposingRegion (int start, int end)
349 {
350 /* Return if the input connection is out of date. */
351 if (view.icSerial < view.icGeneration)
352 return false;
353
354 if (EmacsService.DEBUG_IC)
355 Log.d (TAG, "setComposingRegion: " + start + " " + end);
356
357 EmacsNative.setComposingRegion (windowHandle, start, end);
358 return true;
359 }
360
361 @Override
362 public boolean
363 setComposingRegion (int start, int end, TextAttribute textAttribute)
364 {
365 return setComposingRegion (start, end);
366 }
367
368 @Override
369 public boolean
370 performEditorAction (int editorAction)
371 {
372 /* Return if the input connection is out of date. */
373 if (view.icSerial < view.icGeneration)
374 return false;
375
376 if (EmacsService.DEBUG_IC)
377 Log.d (TAG, "performEditorAction: " + editorAction);
378
379 EmacsNative.performEditorAction (windowHandle, editorAction);
380 return true;
381 }
382
383 @Override
384 public boolean
385 performContextMenuAction (int contextMenuAction)
386 {
387 int action;
388
389 /* Return if the input connection is out of date. */
390 if (view.icSerial < view.icGeneration)
391 return false;
392
393 if (EmacsService.DEBUG_IC)
394 Log.d (TAG, "performContextMenuAction: " + contextMenuAction);
395
396 /* Translate the action in Java code. That way, a great deal of
397 JNI boilerplate can be avoided. */
398
399 switch (contextMenuAction)
400 {
401 case android.R.id.selectAll:
402 action = 0;
403 break;
404
405 case android.R.id.startSelectingText:
406 action = 1;
407 break;
408
409 case android.R.id.stopSelectingText:
410 action = 2;
411 break;
412
413 case android.R.id.cut:
414 action = 3;
415 break;
416
417 case android.R.id.copy:
418 action = 4;
419 break;
420
421 case android.R.id.paste:
422 action = 5;
423 break;
424
425 default:
426 return true;
427 }
428
429 EmacsNative.performContextMenuAction (windowHandle, action);
430 return true;
431 }
432
433 @Override
434 public ExtractedText
435 getExtractedText (ExtractedTextRequest request, int flags)
436 {
437 ExtractedText text;
438 int[] selection;
439
440 /* Return if the input connection is out of date. */
441 if (view.icSerial < view.icGeneration)
442 return null;
443
444 if (EmacsService.DEBUG_IC)
445 Log.d (TAG, "getExtractedText: " + request.hintMaxChars + ", "
446 + request.hintMaxLines + " " + flags);
447
448 /* If a request arrives with hintMaxChars, hintMaxLines and flags
449 set to 0, and the system is known to be buggy, return an empty
450 extracted text object with the absolute selection positions. */
451
452 if (extractAbsoluteOffsets
453 && request.hintMaxChars == 0
454 && request.hintMaxLines == 0
455 && flags == 0)
456 {
457 /* Obtain the selection. */
458 selection = EmacsNative.getSelection (windowHandle);
459 if (selection == null)
460 return null;
461
462 /* Create the workaround extracted text. */
463 text = new ExtractedText ();
464 text.partialStartOffset = -1;
465 text.partialEndOffset = -1;
466 text.text = "";
467 text.selectionStart = selection[0];
468 text.selectionEnd = selection[1];
469 }
470 else
471 text = EmacsNative.getExtractedText (windowHandle, request,
472 flags);
473
474 if (text == null)
475 {
476 if (EmacsService.DEBUG_IC)
477 Log.d (TAG, "getExtractedText: text is NULL");
478
479 return null;
480 }
481
482 if (EmacsService.DEBUG_IC)
483 Log.d (TAG, "getExtractedText: " + text.text + " @"
484 + text.startOffset + ":" + text.selectionStart
485 + ", " + text.selectionEnd);
486
487 return text;
488 }
489
490 @Override
491 public boolean
492 setSelection (int start, int end)
493 {
494 /* Return if the input connection is out of date. */
495 if (view.icSerial < view.icGeneration)
496 return false;
497
498 if (EmacsService.DEBUG_IC)
499 Log.d (TAG, "setSelection: " + start + " " + end);
500
501 EmacsNative.setSelection (windowHandle, start, end);
502 return true;
503 }
504
505 @Override
506 /* ACTION_MULTIPLE is apparently obsolete. */
507 @SuppressWarnings ("deprecation")
508 public boolean
509 sendKeyEvent (KeyEvent key)
510 {
511 /* Return if the input connection is out of date. */
512 if (view.icSerial < view.icGeneration)
513 return false;
514
515 if (EmacsService.DEBUG_IC)
516 Log.d (TAG, "sendKeyEvent: " + key);
517
518 /* Use the standard API if possible. */
519
520 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
521 view.imManager.dispatchKeyEventFromInputMethod (view, key);
522 else
523 {
524 /* Fall back to dispatching the event manually if not. */
525
526 switch (key.getAction ())
527 {
528 case KeyEvent.ACTION_DOWN:
529 view.onKeyDown (key.getKeyCode (), key);
530 break;
531
532 case KeyEvent.ACTION_UP:
533 view.onKeyUp (key.getKeyCode (), key);
534 break;
535
536 case KeyEvent.ACTION_MULTIPLE:
537 view.onKeyMultiple (key.getKeyCode (),
538 key.getRepeatCount (),
539 key);
540 break;
541 }
542 }
543
544 return true;
545 }
546
547 @Override
548 public boolean
549 requestCursorUpdates (int cursorUpdateMode)
550 {
551 /* Return if the input connection is out of date. */
552 if (view.icSerial < view.icGeneration)
553 return false;
554
555 if (EmacsService.DEBUG_IC)
556 Log.d (TAG, "requestCursorUpdates: " + cursorUpdateMode);
557
558 EmacsNative.requestCursorUpdates (windowHandle, cursorUpdateMode);
559 return true;
560 }
561
562 @Override
563 public boolean
564 requestCursorUpdates (int cursorUpdateMode, int filter)
565 {
566 if (filter != 0)
567 return false;
568
569 return requestCursorUpdates (cursorUpdateMode);
570 }
571
572 @Override
573 public SurroundingText
574 getSurroundingText (int beforeLength, int afterLength,
575 int flags)
576 {
577 SurroundingText text;
578
579 /* Return if the input connection is out of date. */
580 if (view.icSerial < view.icGeneration)
581 return null;
582
583 if (EmacsService.DEBUG_IC)
584 Log.d (TAG, ("getSurroundingText: " + beforeLength + ", "
585 + afterLength));
586
587 text = EmacsNative.getSurroundingText (windowHandle, beforeLength,
588 afterLength, flags);
589
590 if (EmacsService.DEBUG_IC && text != null)
591 Log.d (TAG, ("getSurroundingText: "
592 + text.getSelectionStart ()
593 + ","
594 + text.getSelectionEnd ()
595 + "+"
596 + text.getOffset ()
597 + ": "
598 + text.getText ()));
599
600 return text;
601 }
602
603 @Override
604 public TextSnapshot
605 takeSnapshot ()
606 {
607 TextSnapshot snapshot;
608
609 /* Return if the input connection is out of date. */
610 if (view.icSerial < view.icGeneration)
611 return null;
612
613 snapshot = EmacsNative.takeSnapshot (windowHandle);
614
615 if (EmacsService.DEBUG_IC)
616 Log.d (TAG, ("takeSnapshot: "
617 + snapshot.getSurroundingText ().getText ()
618 + " @ " + snapshot.getCompositionEnd ()
619 + ", " + snapshot.getCompositionStart ()));
620
621 return snapshot;
622 }
623
624 @Override
625 public void
626 closeConnection ()
627 {
628 batchEditCount = 0;
629 }
630
631
632
633 public void
634 reset ()
635 {
636 batchEditCount = 0;
637 }
638
639
640 /* Override functions which are not implemented. */
641
642 @Override
643 public Handler
644 getHandler ()
645 {
646 return null;
647 }
648
649 @Override
650 public boolean
651 commitContent (InputContentInfo inputContentInfo, int flags,
652 Bundle opts)
653 {
654 return false;
655 }
656
657 @Override
658 public boolean
659 setImeConsumesInput (boolean imeConsumesInput)
660 {
661 return false;
662 }
663
664 @Override
665 public boolean
666 clearMetaKeyStates (int states)
667 {
668 return false;
669 }
670
671 @Override
672 public boolean
673 reportFullscreenMode (boolean enabled)
674 {
675 return false;
676 }
677
678 @Override
679 public boolean
680 performSpellCheck ()
681 {
682 return false;
683 }
684
685 @Override
686 public boolean
687 performPrivateCommand (String action, Bundle data)
688 {
689 return false;
690 }
691
692 @Override
693 public int
694 getCursorCapsMode (int reqModes)
695 {
696 return 0;
697 }
698}
diff --git a/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java b/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java
new file mode 100644
index 00000000000..1e1e5d97631
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java
@@ -0,0 +1,31 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22/* This class only exists because EmacsPreferencesActivity is already
23 defined as an activity, the system wants a new class in order to
24 define a new activity, and only activities can be enabled or
25 disabled per the API level of the host. */
26
27public final class EmacsLauncherPreferencesActivity
28 extends EmacsPreferencesActivity
29{
30
31}
diff --git a/java/org/gnu/emacs/EmacsMultitaskActivity.java b/java/org/gnu/emacs/EmacsMultitaskActivity.java
new file mode 100644
index 00000000000..b1c48f03fba
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsMultitaskActivity.java
@@ -0,0 +1,29 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22/* This class only exists because EmacsActivity is already defined as
23 an activity, and the system wants a new class in order to define a
24 new activity. */
25
26public final class EmacsMultitaskActivity extends EmacsActivity
27{
28
29}
diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java
new file mode 100644
index 00000000000..fae0ba98f86
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsNative.java
@@ -0,0 +1,316 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.content.res.AssetManager;
23
24import android.graphics.Bitmap;
25
26import android.view.inputmethod.ExtractedText;
27import android.view.inputmethod.ExtractedTextRequest;
28import android.view.inputmethod.SurroundingText;
29import android.view.inputmethod.TextSnapshot;
30
31public final class EmacsNative
32{
33 /* List of native libraries that must be loaded during class
34 initialization. */
35 private static final String[] libraryDeps;
36
37
38 /* Like `dup' in C. */
39 public static native int dup (int fd);
40
41 /* Obtain the fingerprint of this build of Emacs. The fingerprint
42 can be used to determine the dump file name. */
43 public static native String getFingerprint ();
44
45 /* Set certain parameters before initializing Emacs.
46
47 assetManager must be the asset manager associated with the
48 context that is loading Emacs. It is saved and remains for the
49 remainder the lifetime of the Emacs process.
50
51 filesDir must be the package's data storage location for the
52 current Android user.
53
54 libDir must be the package's data storage location for native
55 libraries. It is used as PATH.
56
57 cacheDir must be the package's cache directory. It is used as
58 the `temporary-file-directory'.
59
60 pixelDensityX and pixelDensityY are the DPI values that will be
61 used by Emacs.
62
63 scaledDensity is the DPI value used to translate point sizes to
64 pixel sizes when loading fonts.
65
66 classPath must be the classpath of this app_process process, or
67 NULL.
68
69 emacsService must be the EmacsService singleton, or NULL.
70
71 apiLevel is the version of Android being run. */
72 public static native void setEmacsParams (AssetManager assetManager,
73 String filesDir,
74 String libDir,
75 String cacheDir,
76 float pixelDensityX,
77 float pixelDensityY,
78 float scaledDensity,
79 String classPath,
80 EmacsService emacsService,
81 int apiLevel);
82
83 /* Initialize Emacs with the argument array ARGV. Each argument
84 must contain a NULL terminated string, or else the behavior is
85 undefined.
86
87 DUMPFILE is the dump file to use, or NULL if Emacs is to load
88 loadup.el itself. */
89 public static native void initEmacs (String argv[], String dumpFile);
90
91 /* Abort and generate a native core dump. */
92 public static native void emacsAbort ();
93
94 /* Set Vquit_flag to t, resulting in Emacs quitting as soon as
95 possible. */
96 public static native void quit ();
97
98 /* Send an ANDROID_CONFIGURE_NOTIFY event. The values of all the
99 functions below are the serials of the events sent. */
100 public static native long sendConfigureNotify (short window, long time,
101 int x, int y, int width,
102 int height);
103
104 /* Send an ANDROID_KEY_PRESS event. */
105 public static native long sendKeyPress (short window, long time, int state,
106 int keyCode, int unicodeChar);
107
108 /* Send an ANDROID_KEY_RELEASE event. */
109 public static native long sendKeyRelease (short window, long time, int state,
110 int keyCode, int unicodeChar);
111
112 /* Send an ANDROID_FOCUS_IN event. */
113 public static native long sendFocusIn (short window, long time);
114
115 /* Send an ANDROID_FOCUS_OUT event. */
116 public static native long sendFocusOut (short window, long time);
117
118 /* Send an ANDROID_WINDOW_ACTION event. */
119 public static native long sendWindowAction (short window, int action);
120
121 /* Send an ANDROID_ENTER_NOTIFY event. */
122 public static native long sendEnterNotify (short window, int x, int y,
123 long time);
124
125 /* Send an ANDROID_LEAVE_NOTIFY event. */
126 public static native long sendLeaveNotify (short window, int x, int y,
127 long time);
128
129 /* Send an ANDROID_MOTION_NOTIFY event. */
130 public static native long sendMotionNotify (short window, int x, int y,
131 long time);
132
133 /* Send an ANDROID_BUTTON_PRESS event. */
134 public static native long sendButtonPress (short window, int x, int y,
135 long time, int state,
136 int button);
137
138 /* Send an ANDROID_BUTTON_RELEASE event. */
139 public static native long sendButtonRelease (short window, int x, int y,
140 long time, int state,
141 int button);
142
143 /* Send an ANDROID_TOUCH_DOWN event. */
144 public static native long sendTouchDown (short window, int x, int y,
145 long time, int pointerID,
146 int flags);
147
148 /* Send an ANDROID_TOUCH_UP event. */
149 public static native long sendTouchUp (short window, int x, int y,
150 long time, int pointerID,
151 int flags);
152
153 /* Send an ANDROID_TOUCH_MOVE event. */
154 public static native long sendTouchMove (short window, int x, int y,
155 long time, int pointerID,
156 int flags);
157
158 /* Send an ANDROID_WHEEL event. */
159 public static native long sendWheel (short window, int x, int y,
160 long time, int state,
161 float xDelta, float yDelta);
162
163 /* Send an ANDROID_ICONIFIED event. */
164 public static native long sendIconified (short window);
165
166 /* Send an ANDROID_DEICONIFIED event. */
167 public static native long sendDeiconified (short window);
168
169 /* Send an ANDROID_CONTEXT_MENU event. */
170 public static native long sendContextMenu (short window, int menuEventID,
171 int menuEventSerial);
172
173 /* Send an ANDROID_EXPOSE event. */
174 public static native long sendExpose (short window, int x, int y,
175 int width, int height);
176
177 /* Return the file name associated with the specified file
178 descriptor, or NULL if there is none. */
179 public static native byte[] getProcName (int fd);
180
181 /* Notice that the Emacs thread will now start waiting for the main
182 thread's looper to respond. */
183 public static native void beginSynchronous ();
184
185 /* Notice that the Emacs thread will has finished waiting for the
186 main thread's looper to respond. */
187 public static native void endSynchronous ();
188
189 /* Prevent deadlocks while reliably allowing queries from the Emacs
190 thread to the main thread to complete by waiting for a query to
191 start from the main thread, then answer it; assume that a query
192 is certain to start shortly. */
193 public static native void answerQuerySpin ();
194
195 /* Return whether or not KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP and
196 KEYCODE_VOLUME_MUTE should be forwarded to Emacs. */
197 public static native boolean shouldForwardMultimediaButtons ();
198
199 /* Initialize the current thread, by blocking signals that do not
200 interest it. */
201 public static native void setupSystemThread ();
202
203
204
205 /* Input connection functions. These mostly correspond to their
206 counterparts in Android's InputConnection. */
207
208 public static native void beginBatchEdit (short window);
209 public static native void endBatchEdit (short window);
210 public static native void commitCompletion (short window, String text,
211 int position);
212 public static native void commitText (short window, String text,
213 int position);
214 public static native void deleteSurroundingText (short window,
215 int leftLength,
216 int rightLength);
217 public static native void finishComposingText (short window);
218 public static native String getSelectedText (short window, int flags);
219 public static native String getTextAfterCursor (short window, int length,
220 int flags);
221 public static native String getTextBeforeCursor (short window, int length,
222 int flags);
223 public static native void setComposingText (short window, String text,
224 int newCursorPosition);
225 public static native void setComposingRegion (short window, int start,
226 int end);
227 public static native void setSelection (short window, int start, int end);
228 public static native void performEditorAction (short window,
229 int editorAction);
230 public static native void performContextMenuAction (short window,
231 int contextMenuAction);
232 public static native ExtractedText getExtractedText (short window,
233 ExtractedTextRequest req,
234 int flags);
235 public static native void requestSelectionUpdate (short window);
236 public static native void requestCursorUpdates (short window, int mode);
237 public static native void clearInputFlags (short window);
238 public static native SurroundingText getSurroundingText (short window,
239 int left, int right,
240 int flags);
241 public static native TextSnapshot takeSnapshot (short window);
242
243
244 /* Return the current value of the selection, or -1 upon
245 failure. */
246 public static native int[] getSelection (short window);
247
248
249 /* Graphics functions used as a replacement for potentially buggy
250 Android APIs. */
251
252 public static native void blitRect (Bitmap src, Bitmap dest, int x1,
253 int y1, int x2, int y2);
254
255 /* Increment the generation ID of the specified BITMAP, forcing its
256 texture to be re-uploaded to the GPU. */
257
258 public static native void notifyPixelsChanged (Bitmap bitmap);
259
260
261 /* Functions used to synchronize document provider access with the
262 main thread. */
263
264 /* Wait for a call to `safPostRequest' while also reading async
265 input.
266
267 If asynchronous input arrives and sets Vquit_flag, return 1. */
268 public static native int safSyncAndReadInput ();
269
270 /* Wait for a call to `safPostRequest'. */
271 public static native void safSync ();
272
273 /* Post the semaphore used to await the completion of SAF
274 operations. */
275 public static native void safPostRequest ();
276
277 /* Detect and return FD is writable. FD may be truncated to 0 bytes
278 in the process. */
279 public static native boolean ftruncate (int fd);
280
281 static
282 {
283 /* Older versions of Android cannot link correctly with shared
284 libraries that link with other shared libraries built along
285 Emacs unless all requisite shared libraries are explicitly
286 loaded from Java.
287
288 Every time you add a new shared library dependency to Emacs,
289 please add it here as well. */
290
291 libraryDeps = new String[] { "png_emacs", "selinux_emacs",
292 "crypto_emacs", "pcre_emacs",
293 "packagelistparser_emacs",
294 "gnutls_emacs", "gmp_emacs",
295 "nettle_emacs", "p11-kit_emacs",
296 "tasn1_emacs", "hogweed_emacs",
297 "jansson_emacs", "jpeg_emacs",
298 "tiff_emacs", "xml2_emacs",
299 "icuuc_emacs",
300 "tree-sitter_emacs", };
301
302 for (String dependency : libraryDeps)
303 {
304 try
305 {
306 System.loadLibrary (dependency);
307 }
308 catch (UnsatisfiedLinkError exception)
309 {
310 /* Ignore this exception. */
311 }
312 }
313
314 System.loadLibrary ("emacs");
315 };
316};
diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java
new file mode 100644
index 00000000000..1c7513e1cc9
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsNoninteractive.java
@@ -0,0 +1,203 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.os.Looper;
23import android.os.Build;
24
25import android.content.Context;
26import android.content.res.AssetManager;
27
28import java.lang.reflect.Constructor;
29import java.lang.reflect.Method;
30
31/* Noninteractive Emacs.
32
33 This is the class that libandroid-emacs.so starts.
34 libandroid-emacs.so figures out the system classpath, then starts
35 dalvikvm with the framework jars.
36
37 At that point, dalvikvm calls main, which sets up the main looper,
38 creates an ActivityThread and attaches it to the main thread.
39
40 Then, it obtains an application context for the LoadedApk in the
41 application thread.
42
43 Finally, it obtains the necessary context specific objects and
44 initializes Emacs. */
45
46@SuppressWarnings ("unchecked")
47public final class EmacsNoninteractive
48{
49 public static void
50 main (String[] args)
51 {
52 Object activityThread, loadedApk;
53 Class activityThreadClass, loadedApkClass, contextImplClass;
54 Class compatibilityInfoClass;
55 Method method;
56 Context context;
57 AssetManager assets;
58 String filesDir, libDir, cacheDir;
59
60 Looper.prepare ();
61 context = null;
62 assets = null;
63 filesDir = libDir = cacheDir = null;
64
65 try
66 {
67 /* Get the activity thread. */
68 activityThreadClass = Class.forName ("android.app.ActivityThread");
69
70 /* Get the systemMain method. */
71 method = activityThreadClass.getMethod ("systemMain");
72
73 /* Create and attach the activity thread. */
74 activityThread = method.invoke (null);
75 context = null;
76
77 /* Now get an LoadedApk. */
78
79 try
80 {
81 loadedApkClass = Class.forName ("android.app.LoadedApk");
82 }
83 catch (ClassNotFoundException exception)
84 {
85 /* Android 2.2 has no LoadedApk class, but fortunately it
86 does not need to be used, since contexts can be
87 directly created. */
88
89 loadedApkClass = null;
90 contextImplClass = Class.forName ("android.app.ContextImpl");
91
92 method = activityThreadClass.getDeclaredMethod ("getSystemContext");
93 context = (Context) method.invoke (activityThread);
94 method = contextImplClass.getDeclaredMethod ("createPackageContext",
95 String.class,
96 int.class);
97 method.setAccessible (true);
98 context = (Context) method.invoke (context, "org.gnu.emacs",
99 0);
100 }
101
102 /* If the context has not already been created, then do what
103 is appropriate for newer versions of Android. */
104
105 if (context == null)
106 {
107 /* Get a LoadedApk. How to do this varies by Android version.
108 On Android 2.3.3 and earlier, there is no
109 ``compatibilityInfo'' argument to getPackageInfo. */
110
111 if (Build.VERSION.SDK_INT
112 <= Build.VERSION_CODES.GINGERBREAD_MR1)
113 {
114 method
115 = activityThreadClass.getMethod ("getPackageInfo",
116 String.class,
117 int.class);
118 loadedApk = method.invoke (activityThread, "org.gnu.emacs",
119 0);
120 }
121 else
122 {
123 compatibilityInfoClass
124 = Class.forName ("android.content.res.CompatibilityInfo");
125
126 method
127 = activityThreadClass.getMethod ("getPackageInfo",
128 String.class,
129 compatibilityInfoClass,
130 int.class);
131 loadedApk = method.invoke (activityThread, "org.gnu.emacs",
132 null, 0);
133 }
134
135 if (loadedApk == null)
136 throw new RuntimeException ("getPackageInfo returned NULL");
137
138 /* Now, get a context. */
139 contextImplClass = Class.forName ("android.app.ContextImpl");
140
141 try
142 {
143 method
144 = contextImplClass.getDeclaredMethod ("createAppContext",
145 activityThreadClass,
146 loadedApkClass);
147 method.setAccessible (true);
148 context = (Context) method.invoke (null, activityThread,
149 loadedApk);
150 }
151 catch (NoSuchMethodException exception)
152 {
153 /* Older Android versions don't have createAppContext, but
154 instead require creating a ContextImpl, and then
155 calling createPackageContext. */
156 method
157 = activityThreadClass.getDeclaredMethod ("getSystemContext");
158 context = (Context) method.invoke (activityThread);
159 method
160 = contextImplClass.getDeclaredMethod ("createPackageContext",
161 String.class,
162 int.class);
163 method.setAccessible (true);
164 context = (Context) method.invoke (context, "org.gnu.emacs",
165 0);
166 }
167 }
168
169 /* Don't actually start the looper or anything. Instead, obtain
170 an AssetManager. */
171 assets = context.getAssets ();
172
173 /* Now configure Emacs. The class path should already be set. */
174
175 filesDir = context.getFilesDir ().getCanonicalPath ();
176 libDir = EmacsService.getLibraryDirectory (context);
177 cacheDir = context.getCacheDir ().getCanonicalPath ();
178 }
179 catch (Exception e)
180 {
181 System.err.println ("Internal error: " + e);
182 System.err.println ("This means that the Android platform changed,");
183 System.err.println ("and that Emacs needs adjustments in order to");
184 System.err.println ("obtain required system internal resources.");
185 System.err.println ("Please report this bug to bug-gnu-emacs@gnu.org.");
186 e.printStackTrace ();
187
188 System.exit (1);
189 }
190
191 EmacsNative.setEmacsParams (assets, filesDir,
192 libDir, cacheDir, 0.0f,
193 0.0f, 0.0f, null, null,
194 Build.VERSION.SDK_INT);
195
196 /* Now find the dump file that Emacs should use, if it has already
197 been dumped. */
198 EmacsApplication.findDumpFile (context);
199
200 /* Start Emacs. */
201 EmacsNative.initEmacs (args, EmacsApplication.dumpFileName);
202 }
203};
diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java
new file mode 100644
index 00000000000..3832cd2faab
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsOpenActivity.java
@@ -0,0 +1,552 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22/* This class makes the Emacs server work reasonably on Android.
23
24 There is no way to make the Unix socket publicly available on
25 Android.
26
27 Instead, this activity tries to connect to the Emacs server, to
28 make it open files the system asks Emacs to open, and to emulate
29 some reasonable behavior when Emacs has not yet started.
30
31 First, Emacs registers itself as an application that can open text
32 and image files.
33
34 Then, when the user is asked to open a file and selects ``Emacs''
35 as the application that will open the file, the system pops up a
36 window, this activity, and calls the `onCreate' function.
37
38 `onCreate' then tries very to find the file name of the file that
39 was selected, and give it to emacsclient.
40
41 If emacsclient successfully opens the file, then this activity
42 starts EmacsActivity (to bring it on to the screen); otherwise, it
43 displays the output of emacsclient or any error message that occurs
44 and exits. */
45
46import android.app.AlertDialog;
47import android.app.Activity;
48
49import android.content.ContentResolver;
50import android.content.DialogInterface;
51import android.content.Intent;
52
53import android.net.Uri;
54
55import android.os.Build;
56import android.os.Bundle;
57import android.os.ParcelFileDescriptor;
58
59import android.util.Log;
60
61import java.io.File;
62import java.io.FileInputStream;
63import java.io.FileNotFoundException;
64import java.io.FileOutputStream;
65import java.io.FileReader;
66import java.io.IOException;
67import java.io.InputStream;
68import java.io.UnsupportedEncodingException;
69
70public final class EmacsOpenActivity extends Activity
71 implements DialogInterface.OnClickListener,
72 DialogInterface.OnCancelListener
73{
74 private static final String TAG = "EmacsOpenActivity";
75
76 /* The name of any file that should be opened as EmacsThread starts
77 Emacs. This is never cleared, even if EmacsOpenActivity is
78 started a second time, as EmacsThread only starts once. */
79 public static String fileToOpen;
80
81 /* Any currently focused EmacsOpenActivity. Used to show pop ups
82 while the activity is active and Emacs doesn't have permission to
83 display over other programs. */
84 public static EmacsOpenActivity currentActivity;
85
86 private class EmacsClientThread extends Thread
87 {
88 private ProcessBuilder builder;
89
90 public
91 EmacsClientThread (ProcessBuilder processBuilder)
92 {
93 builder = processBuilder;
94 }
95
96 @Override
97 public void
98 run ()
99 {
100 Process process;
101 InputStream error;
102 String errorText;
103
104 try
105 {
106 /* Start emacsclient. */
107 process = builder.start ();
108 process.waitFor ();
109
110 /* Now figure out whether or not starting the process was
111 successful. */
112 if (process.exitValue () == 0)
113 finishSuccess ();
114 else
115 finishFailure ("Error opening file", null);
116 }
117 catch (IOException exception)
118 {
119 finishFailure ("Internal error", exception.toString ());
120 }
121 catch (InterruptedException exception)
122 {
123 finishFailure ("Internal error", exception.toString ());
124 }
125 }
126 }
127
128 @Override
129 public void
130 onClick (DialogInterface dialog, int which)
131 {
132 finish ();
133 }
134
135 @Override
136 public void
137 onCancel (DialogInterface dialog)
138 {
139 finish ();
140 }
141
142 public String
143 readEmacsClientLog ()
144 {
145 File file, cache;
146 FileReader reader;
147 char[] buffer;
148 int rc;
149 StringBuilder builder;
150
151 /* Because the ProcessBuilder functions necessary to redirect
152 process output are not implemented on Android 7 and earlier,
153 print a generic error message. */
154
155 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
156 return ("This is likely because the Emacs server"
157 + " is not running, or because you did"
158 + " not grant Emacs permission to access"
159 + " external storage.");
160
161 cache = getCacheDir ();
162 file = new File (cache, "emacsclient.log");
163 builder = new StringBuilder ();
164 reader = null;
165
166 try
167 {
168 reader = new FileReader (file);
169 buffer = new char[2048];
170
171 while ((rc = reader.read (buffer, 0, 2048)) != -1)
172 builder.append (buffer, 0, rc);
173
174 reader.close ();
175 return builder.toString ();
176 }
177 catch (IOException exception)
178 {
179 /* Close the reader if it's already been opened. */
180
181 try
182 {
183 if (reader != null)
184 reader.close ();
185 }
186 catch (IOException e)
187 {
188 /* Not sure what to do here. */
189 }
190
191 return ("Couldn't read emacsclient.log: "
192 + exception.toString ());
193 }
194 }
195
196 private void
197 displayFailureDialog (String title, String text)
198 {
199 AlertDialog.Builder builder;
200 AlertDialog dialog;
201
202 builder = new AlertDialog.Builder (this);
203 dialog = builder.create ();
204 dialog.setTitle (title);
205
206 if (text == null)
207 /* Read in emacsclient.log instead. */
208 text = readEmacsClientLog ();
209
210 dialog.setMessage (text);
211 dialog.setButton (DialogInterface.BUTTON_POSITIVE, "OK", this);
212 dialog.setOnCancelListener (this);
213 dialog.show ();
214 }
215
216 /* Check that the specified FILE is readable. If Android 4.4 or
217 later is being used, return URI formatted into a `/content/' file
218 name.
219
220 If it is not, then copy the file in FD to a location in the
221 system cache directory and return the name of that file. */
222
223 private String
224 checkReadableOrCopy (String file, ParcelFileDescriptor fd,
225 Uri uri)
226 throws IOException, FileNotFoundException
227 {
228 File inFile;
229 FileOutputStream outStream;
230 InputStream stream;
231 byte buffer[];
232 int read;
233 String content;
234
235 Log.d (TAG, "checkReadableOrCopy: " + file);
236
237 inFile = new File (file);
238
239 if (inFile.canRead ())
240 return file;
241
242 Log.d (TAG, "checkReadableOrCopy: NO");
243
244 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
245 {
246 content = EmacsService.buildContentName (uri);
247 Log.d (TAG, "checkReadableOrCopy: " + content);
248 return content;
249 }
250
251 /* inFile is now the file being written to. */
252 inFile = new File (getCacheDir (), inFile.getName ());
253 buffer = new byte[4098];
254
255 /* Initialize both streams to NULL. */
256 outStream = null;
257 stream = null;
258
259 try
260 {
261 outStream = new FileOutputStream (inFile);
262 stream = new FileInputStream (fd.getFileDescriptor ());
263
264 while ((read = stream.read (buffer)) >= 0)
265 outStream.write (buffer, 0, read);
266 }
267 finally
268 {
269 /* Note that this does not close FD.
270
271 Keep in mind that execution is transferred to ``finally''
272 even if an exception happens inside the while loop
273 above. */
274
275 if (stream != null)
276 stream.close ();
277
278 if (outStream != null)
279 outStream.close ();
280 }
281
282 return inFile.getCanonicalPath ();
283 }
284
285 /* Finish this activity in response to emacsclient having
286 successfully opened a file.
287
288 In the main thread, close this window, and open a window
289 belonging to an Emacs frame. */
290
291 public void
292 finishSuccess ()
293 {
294 runOnUiThread (new Runnable () {
295 @Override
296 public void
297 run ()
298 {
299 Intent intent;
300
301 intent = new Intent (EmacsOpenActivity.this,
302 EmacsActivity.class);
303
304 /* This means only an existing frame will be displayed. */
305 intent.addFlags (Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
306 startActivity (intent);
307
308 EmacsOpenActivity.this.finish ();
309 }
310 });
311 }
312
313 /* Finish this activity after displaying a dialog associated with
314 failure to open a file.
315
316 Use TITLE as the title of the dialog. If TEXT is non-NULL,
317 display that text in the dialog. Otherwise, use the contents of
318 emacsclient.log in the cache directory instead, or describe why
319 that file cannot be read. */
320
321 public void
322 finishFailure (final String title, final String text)
323 {
324 runOnUiThread (new Runnable () {
325 @Override
326 public void
327 run ()
328 {
329 displayFailureDialog (title, text);
330 }
331 });
332 }
333
334 public void
335 startEmacsClient (String fileName)
336 {
337 String libDir;
338 ProcessBuilder builder;
339 Process process;
340 EmacsClientThread thread;
341 File file;
342 Intent intent;
343
344 /* If the Emacs service is not running, then start Emacs and make
345 it open this file. */
346
347 if (EmacsService.SERVICE == null)
348 {
349 fileToOpen = fileName;
350 intent = new Intent (EmacsOpenActivity.this,
351 EmacsActivity.class);
352 finish ();
353 startActivity (intent);
354 return;
355 }
356
357 libDir = EmacsService.getLibraryDirectory (this);
358 builder = new ProcessBuilder (libDir + "/libemacsclient.so",
359 fileName, "--reuse-frame",
360 "--timeout=10", "--no-wait");
361
362 /* Redirection is unfortunately not possible in Android 7 and
363 earlier. */
364
365 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
366 {
367 file = new File (getCacheDir (), "emacsclient.log");
368
369 /* Redirect standard error to a file so that errors can be
370 meaningfully reported. */
371
372 if (file.exists ())
373 file.delete ();
374
375 builder.redirectError (file);
376 }
377
378 /* Track process output in a new thread, since this is the UI
379 thread and doing so here can cause deadlocks when EmacsService
380 decides to wait for something. */
381
382 thread = new EmacsClientThread (builder);
383 thread.start ();
384 }
385
386 /* Run emacsclient to open the file specified in the Intent that
387 caused this activity to start.
388
389 Determine the name of the file corresponding to the URI specified
390 in that intent; then, run emacsclient and wait for it to finish.
391
392 Finally, display any error message, transfer the focus to an
393 Emacs frame, and finish the activity. */
394
395 @Override
396 public void
397 onCreate (Bundle savedInstanceState)
398 {
399 String action, fileName;
400 Intent intent;
401 Uri uri;
402 ContentResolver resolver;
403 ParcelFileDescriptor fd;
404 byte[] names;
405 String errorBlurb;
406
407 super.onCreate (savedInstanceState);
408
409 /* Obtain the intent that started Emacs. */
410 intent = getIntent ();
411 action = intent.getAction ();
412
413 if (action == null)
414 {
415 finish ();
416 return;
417 }
418
419 /* Now see if the action specified is supported by Emacs. */
420
421 if (action.equals ("android.intent.action.VIEW")
422 || action.equals ("android.intent.action.EDIT")
423 || action.equals ("android.intent.action.PICK"))
424 {
425 /* Obtain the URI of the action. */
426 uri = intent.getData ();
427
428 if (uri == null)
429 {
430 finish ();
431 return;
432 }
433
434 /* Now, try to get the file name. */
435
436 if (uri.getScheme ().equals ("file"))
437 fileName = uri.getPath ();
438 else
439 {
440 fileName = null;
441
442 if (uri.getScheme ().equals ("content"))
443 {
444 /* This is one of the annoying Android ``content''
445 URIs. Most of the time, there is actually an
446 underlying file, but it cannot be found without
447 opening the file and doing readlink on its file
448 descriptor in /proc/self/fd. */
449 resolver = getContentResolver ();
450 fd = null;
451
452 try
453 {
454 fd = resolver.openFileDescriptor (uri, "r");
455 names = EmacsNative.getProcName (fd.getFd ());
456
457 /* What is the right encoding here? */
458
459 if (names != null)
460 fileName = new String (names, "UTF-8");
461
462 fileName = checkReadableOrCopy (fileName, fd, uri);
463 }
464 catch (FileNotFoundException exception)
465 {
466 /* Do nothing. */
467 }
468 catch (IOException exception)
469 {
470 /* Do nothing. */
471 }
472
473 if (fd != null)
474 {
475 try
476 {
477 fd.close ();
478 }
479 catch (IOException exception)
480 {
481 /* Do nothing. */
482 }
483 }
484 }
485
486 if (fileName == null)
487 {
488 errorBlurb = ("The URI: " + uri + " could not be opened"
489 + ", as it does not encode file name inform"
490 + "ation.");
491 displayFailureDialog ("Error opening file", errorBlurb);
492 return;
493 }
494 }
495
496 /* And start emacsclient. Set `currentActivity' to this now.
497 Presumably, it will shortly become capable of displaying
498 dialogs. */
499 currentActivity = this;
500 startEmacsClient (fileName);
501 }
502 else
503 finish ();
504 }
505
506
507
508 @Override
509 public void
510 onDestroy ()
511 {
512 Log.d (TAG, "onDestroy: " + this);
513
514 /* Clear `currentActivity' if it refers to the activity being
515 destroyed. */
516
517 if (currentActivity == this)
518 this.currentActivity = null;
519
520 super.onDestroy ();
521 }
522
523 @Override
524 public void
525 onWindowFocusChanged (boolean isFocused)
526 {
527 Log.d (TAG, "onWindowFocusChanged: " + this + ", is now focused: "
528 + isFocused);
529
530 if (isFocused)
531 currentActivity = this;
532 else if (currentActivity == this)
533 currentActivity = null;
534
535 super.onWindowFocusChanged (isFocused);
536 }
537
538 @Override
539 public void
540 onPause ()
541 {
542 Log.d (TAG, "onPause: " + this);
543
544 /* XXX: clear currentActivity here as well; I don't know whether
545 or not onWindowFocusChanged is always called prior to this. */
546
547 if (currentActivity == this)
548 currentActivity = null;
549
550 super.onPause ();
551 }
552}
diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java
new file mode 100644
index 00000000000..eb011bc5e65
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsPixmap.java
@@ -0,0 +1,192 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.lang.IllegalArgumentException;
23
24import android.graphics.Bitmap;
25import android.graphics.Canvas;
26import android.graphics.Rect;
27
28import android.os.Build;
29
30/* Drawable backed by bitmap. */
31
32public final class EmacsPixmap extends EmacsHandleObject
33 implements EmacsDrawable
34{
35 /* The depth of the bitmap. This is not actually used, just defined
36 in order to be consistent with X. */
37 public int depth, width, height;
38
39 /* The bitmap itself. */
40 public Bitmap bitmap;
41
42 /* The canvas used to draw to BITMAP. */
43 public Canvas canvas;
44
45 /* Whether or not GC should be explicitly triggered upon
46 release. */
47 private boolean needCollect;
48
49 /* ID used to determine whether or not the GC clip rects
50 changed. */
51 private long gcClipRectID;
52
53 public
54 EmacsPixmap (short handle, int colors[], int width,
55 int height, int depth)
56 {
57 super (handle);
58
59 if (depth != 1 && depth != 24)
60 throw new IllegalArgumentException ("Invalid depth specified"
61 + " for pixmap: " + depth);
62
63 switch (depth)
64 {
65 case 1:
66 bitmap = Bitmap.createBitmap (colors, width, height,
67 Bitmap.Config.ALPHA_8);
68 break;
69
70 case 24:
71 bitmap = Bitmap.createBitmap (colors, width, height,
72 Bitmap.Config.ARGB_8888);
73 bitmap.setHasAlpha (false);
74 break;
75 }
76
77 this.width = width;
78 this.height = height;
79 this.depth = depth;
80 }
81
82 public
83 EmacsPixmap (short handle, int width, int height, int depth)
84 {
85 super (handle);
86
87 if (depth != 1 && depth != 24)
88 throw new IllegalArgumentException ("Invalid depth specified"
89 + " for pixmap: " + depth);
90
91 switch (depth)
92 {
93 case 1:
94 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
95 bitmap = Bitmap.createBitmap (width, height,
96 Bitmap.Config.ALPHA_8,
97 false);
98 else
99 bitmap = Bitmap.createBitmap (width, height,
100 Bitmap.Config.ALPHA_8);
101 break;
102
103 case 24:
104
105 /* Emacs doesn't just use the first kind of `createBitmap'
106 because the latter allows specifying that the pixmap is
107 always opaque, which really increases efficiency. */
108 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
109 bitmap = Bitmap.createBitmap (width, height,
110 Bitmap.Config.ARGB_8888);
111 else
112 bitmap = Bitmap.createBitmap (width, height,
113 Bitmap.Config.ARGB_8888,
114 false);
115 break;
116 }
117
118 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1)
119 /* On these old versions of Android, Bitmap.recycle frees bitmap
120 contents immediately. */
121 needCollect = false;
122 else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT)
123 needCollect = (bitmap.getByteCount ()
124 >= 1024 * 512);
125 else
126 needCollect = (bitmap.getAllocationByteCount ()
127 >= 1024 * 512);
128
129 bitmap.eraseColor (0xff000000);
130
131 this.width = width;
132 this.height = height;
133 this.depth = depth;
134 }
135
136 @Override
137 public Canvas
138 lockCanvas (EmacsGC gc)
139 {
140 int i;
141
142 if (canvas == null)
143 {
144 canvas = new Canvas (bitmap);
145 canvas.save ();
146 }
147
148 /* Now see if clipping has to be redone. */
149 if (gc.clipRectID == gcClipRectID)
150 return canvas;
151
152 /* It does have to be redone. Reapply gc.real_clip_rects. */
153 canvas.restore ();
154 canvas.save ();
155
156 if (gc.real_clip_rects != null)
157 {
158 for (i = 0; i < gc.real_clip_rects.length; ++i)
159 canvas.clipRect (gc.real_clip_rects[i]);
160 }
161
162 /* Save the clip rect ID again. */
163 gcClipRectID = gc.clipRectID;
164 return canvas;
165 }
166
167 @Override
168 public void
169 damageRect (Rect damageRect)
170 {
171
172 }
173
174 @Override
175 public Bitmap
176 getBitmap ()
177 {
178 return bitmap;
179 }
180
181 @Override
182 public void
183 destroyHandle ()
184 {
185 bitmap.recycle ();
186 bitmap = null;
187
188 /* Collect the bitmap storage if the bitmap is big. */
189 if (needCollect)
190 Runtime.getRuntime ().gc ();
191 }
192};
diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java
new file mode 100644
index 00000000000..7e67cc3679b
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java
@@ -0,0 +1,168 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.io.File;
23
24import android.app.Activity;
25
26import android.content.Intent;
27
28import android.os.Bundle;
29import android.os.Build;
30
31import android.widget.Toast;
32
33import android.preference.*;
34
35/* This module provides a ``preferences'' display for Emacs. It is
36 supposed to be launched from inside the Settings application to
37 perform various actions, such as starting Emacs with the ``-Q''
38 option, which would not be possible otherwise, as there is no
39 command line on Android.
40
41 Android provides a preferences activity, but it is deprecated.
42 Unfortunately, there is no alternative that looks the same way. */
43
44@SuppressWarnings ("deprecation")
45public class EmacsPreferencesActivity extends PreferenceActivity
46{
47 /* Restart Emacs with -Q. Call EmacsThread.exit to kill Emacs now,
48 and tell the system to start EmacsActivity with some parameters
49 later. */
50
51 private void
52 startEmacsQ ()
53 {
54 Intent intent;
55
56 intent = new Intent (this, EmacsActivity.class);
57 intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK
58 | Intent.FLAG_ACTIVITY_CLEAR_TASK);
59 intent.putExtra ("org.gnu.emacs.STARTUP_ARGUMENT", "--quick");
60 startActivity (intent);
61 System.exit (0);
62 }
63
64 /* Restart Emacs with `--debug-init'. Call EmacsThread.exit to kill
65 Emacs now, and tell the system to EmacsActivity with some
66 parameters later. */
67
68 private void
69 startEmacsDebugInit ()
70 {
71 Intent intent;
72
73 intent = new Intent (this, EmacsActivity.class);
74 intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK
75 | Intent.FLAG_ACTIVITY_CLEAR_TASK);
76 intent.putExtra ("org.gnu.emacs.STARTUP_ARGUMENT", "--debug-init");
77 startActivity (intent);
78 System.exit (0);
79 }
80
81 /* Erase Emacs's dump file. */
82
83 private void
84 eraseDumpFile ()
85 {
86 String wantedDumpFile;
87 File file;
88 Toast toast;
89
90 wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint ()
91 + ".pdmp");
92 file = new File (getFilesDir (), wantedDumpFile);
93
94 if (file.exists ())
95 file.delete ();
96
97 /* Make sure to clear EmacsApplication.dumpFileName, or
98 starting Emacs without restarting this program will
99 make Emacs try to load a nonexistent dump file. */
100 EmacsApplication.dumpFileName = null;
101
102 /* Display a message stating that the dump file has been
103 erased. */
104 toast = Toast.makeText (this, "Dump file removed",
105 Toast.LENGTH_SHORT);
106 toast.show ();
107 }
108
109 @Override
110 public final void
111 onCreate (Bundle savedInstanceState)
112 {
113 Preference tem;
114 Preference.OnPreferenceClickListener listener;
115
116 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
117 setTheme (android.R.style.Theme_DeviceDefault_Settings);
118 else if (Build.VERSION.SDK_INT
119 >= Build.VERSION_CODES.ICE_CREAM_SANDWICH)
120 setTheme (android.R.style.Theme_DeviceDefault);
121
122 /* This must come before using any preference APIs. */
123 super.onCreate (savedInstanceState);
124
125 /* Add preferences from the XML file where they are defined. */
126 addPreferencesFromResource (R.xml.preferences);
127
128 /* Now, set up on click handlers for each of the preferences
129 items. */
130
131 tem = findPreference ("start_quick");
132 listener = new Preference.OnPreferenceClickListener () {
133 @Override
134 public boolean
135 onPreferenceClick (Preference preference)
136 {
137 startEmacsQ ();
138 return true;
139 }
140 };
141
142 tem.setOnPreferenceClickListener (listener);
143 tem = findPreference ("start_debug_init");
144 listener = new Preference.OnPreferenceClickListener () {
145 @Override
146 public boolean
147 onPreferenceClick (Preference preference)
148 {
149 startEmacsDebugInit ();
150 return true;
151 }
152 };
153
154 tem.setOnPreferenceClickListener (listener);
155 tem = findPreference ("erase_dump");
156 listener = new Preference.OnPreferenceClickListener () {
157 @Override
158 public boolean
159 onPreferenceClick (Preference preference)
160 {
161 eraseDumpFile ();
162 return true;
163 }
164 };
165
166 tem.setOnPreferenceClickListener (listener);
167 }
168};
diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java
new file mode 100644
index 00000000000..3ae3c0839ce
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSafThread.java
@@ -0,0 +1,1687 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.util.Collection;
23import java.util.HashMap;
24import java.util.Iterator;
25
26import java.io.FileNotFoundException;
27import java.io.IOException;
28
29import android.content.ContentResolver;
30import android.database.Cursor;
31import android.net.Uri;
32
33import android.os.Build;
34import android.os.CancellationSignal;
35import android.os.Handler;
36import android.os.HandlerThread;
37import android.os.OperationCanceledException;
38import android.os.ParcelFileDescriptor;
39import android.os.SystemClock;
40
41import android.util.Log;
42
43import android.provider.DocumentsContract;
44import android.provider.DocumentsContract.Document;
45
46
47
48/* Emacs runs long-running SAF operations on a second thread running
49 its own handler. These operations include opening files and
50 maintaining the path to document ID cache.
51
52 Because Emacs paths are based on file display names, while Android
53 document identifiers have no discernible hierarchy of their own,
54 each file name lookup must carry out a repeated search for
55 directory documents with the names of all of the file name's
56 constituent components, where each iteration searches within the
57 directory document identified by the previous iteration.
58
59 A time limited cache tying components to document IDs is maintained
60 in order to speed up consecutive searches for file names sharing
61 the same components. Since listening for changes to each document
62 in the cache is prohibitively expensive, Emacs instead elects to
63 periodically remove entries that are older than a predetermined
64 amount of a time.
65
66 The cache is split into two levels: the first caches the
67 relationships between display names and document IDs, while the
68 second caches individual document IDs and their contents (children,
69 type, etc.)
70
71 Long-running operations are also run on this thread for another
72 reason: Android uses special cancellation objects to terminate
73 ongoing IPC operations. However, the functions that perform these
74 operations block instead of providing mechanisms for the caller to
75 wait for their completion while also reading async input, as a
76 consequence of which the calling thread is unable to signal the
77 cancellation objects that it provides. Performing the blocking
78 operations in this auxiliary thread enables the main thread to wait
79 for completion itself, signaling the cancellation objects when it
80 deems necessary. */
81
82
83
84public final class EmacsSafThread extends HandlerThread
85{
86 private static final String TAG = "EmacsSafThread";
87
88 /* The content resolver used by this thread. */
89 private final ContentResolver resolver;
90
91 /* Map between tree URIs and the cache entry representing its
92 toplevel directory. */
93 private final HashMap<Uri, CacheToplevel> cacheToplevels;
94
95 /* Handler for this thread's main loop. */
96 private Handler handler;
97
98 /* File access mode constants. See `man 7 inode'. */
99 public static final int S_IRUSR = 0000400;
100 public static final int S_IWUSR = 0000200;
101 public static final int S_IXUSR = 0000100;
102 public static final int S_IFCHR = 0020000;
103 public static final int S_IFDIR = 0040000;
104 public static final int S_IFREG = 0100000;
105
106 /* Number of seconds in between each attempt to prune the storage
107 cache. */
108 public static final int CACHE_PRUNE_TIME = 10;
109
110 /* Number of seconds after which an entry in the cache is to be
111 considered invalid. */
112 public static final int CACHE_INVALID_TIME = 10;
113
114 public
115 EmacsSafThread (ContentResolver resolver)
116 {
117 super ("Document provider access thread");
118 this.resolver = resolver;
119 this.cacheToplevels = new HashMap<Uri, CacheToplevel> ();
120 }
121
122
123
124 @Override
125 public void
126 start ()
127 {
128 super.start ();
129
130 /* Set up the handler after the thread starts. */
131 handler = new Handler (getLooper ());
132
133 /* And start periodically pruning the cache. */
134 postPruneMessage ();
135 }
136
137
138 private static final class CacheToplevel
139 {
140 /* Map between document names and children. */
141 HashMap<String, DocIdEntry> children;
142
143 /* Map between document names and file status. */
144 HashMap<String, StatCacheEntry> statCache;
145
146 /* Map between document IDs and cache items. */
147 HashMap<String, CacheEntry> idCache;
148 };
149
150 private static final class StatCacheEntry
151 {
152 /* The time at which this cache entry was created. */
153 long time;
154
155 /* Flags, size, and modification time of this file. */
156 long flags, size, mtime;
157
158 /* Whether or not this file is a directory. */
159 boolean isDirectory;
160
161 public
162 StatCacheEntry ()
163 {
164 time = SystemClock.uptimeMillis ();
165 }
166
167 public boolean
168 isValid ()
169 {
170 return ((SystemClock.uptimeMillis () - time)
171 < CACHE_INVALID_TIME * 1000);
172 }
173 };
174
175 private static final class DocIdEntry
176 {
177 /* The document ID. */
178 String documentId;
179
180 /* The time this entry was created. */
181 long time;
182
183 public
184 DocIdEntry ()
185 {
186 time = SystemClock.uptimeMillis ();
187 }
188
189 /* Return a cache entry comprised of the state of the file
190 identified by `documentId'. TREE is the URI of the tree
191 containing this entry, and TOPLEVEL is the toplevel
192 representing it. SIGNAL is a cancellation signal.
193
194 RESOLVER is the content provider used to retrieve file
195 information.
196
197 Value is NULL if the file cannot be found. */
198
199 public CacheEntry
200 getCacheEntry (ContentResolver resolver, Uri tree,
201 CacheToplevel toplevel,
202 CancellationSignal signal)
203 {
204 Uri uri;
205 String[] projection;
206 String type;
207 Cursor cursor;
208 int column;
209 CacheEntry entry;
210
211 /* Create a document URI representing DOCUMENTID within URI's
212 authority. */
213
214 uri = DocumentsContract.buildDocumentUriUsingTree (tree,
215 documentId);
216 projection = new String[] {
217 Document.COLUMN_MIME_TYPE,
218 };
219
220 cursor = null;
221
222 try
223 {
224 cursor = resolver.query (uri, projection, null,
225 null, null, signal);
226
227 if (!cursor.moveToFirst ())
228 return null;
229
230 column = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
231
232 if (column < 0)
233 return null;
234
235 type = cursor.getString (column);
236
237 if (type == null)
238 return null;
239
240 entry = new CacheEntry ();
241 entry.type = type;
242 toplevel.idCache.put (documentId, entry);
243 return entry;
244 }
245 catch (OperationCanceledException e)
246 {
247 throw e;
248 }
249 catch (Throwable e)
250 {
251 return null;
252 }
253 finally
254 {
255 if (cursor != null)
256 cursor.close ();
257 }
258 }
259
260 public boolean
261 isValid ()
262 {
263 return ((SystemClock.uptimeMillis () - time)
264 < CACHE_INVALID_TIME * 1000);
265 }
266 };
267
268 private static final class CacheEntry
269 {
270 /* The type of this document. */
271 String type;
272
273 /* Map between document names and children. */
274 HashMap<String, DocIdEntry> children;
275
276 /* The time this entry was created. */
277 long time;
278
279 public
280 CacheEntry ()
281 {
282 children = new HashMap<String, DocIdEntry> ();
283 time = SystemClock.uptimeMillis ();
284 }
285
286 public boolean
287 isValid ()
288 {
289 return ((SystemClock.uptimeMillis () - time)
290 < CACHE_INVALID_TIME * 1000);
291 }
292 };
293
294 /* Create or return a toplevel for the given tree URI. */
295
296 private CacheToplevel
297 getCache (Uri uri)
298 {
299 CacheToplevel toplevel;
300
301 toplevel = cacheToplevels.get (uri);
302
303 if (toplevel != null)
304 return toplevel;
305
306 toplevel = new CacheToplevel ();
307 toplevel.children = new HashMap<String, DocIdEntry> ();
308 toplevel.statCache = new HashMap<String, StatCacheEntry> ();
309 toplevel.idCache = new HashMap<String, CacheEntry> ();
310 cacheToplevels.put (uri, toplevel);
311 return toplevel;
312 }
313
314 /* Remove each cache entry within COLLECTION older than
315 CACHE_INVALID_TIME. */
316
317 private void
318 pruneCache1 (Collection<DocIdEntry> collection)
319 {
320 Iterator<DocIdEntry> iter;
321 DocIdEntry tem;
322
323 iter = collection.iterator ();
324 while (iter.hasNext ())
325 {
326 /* Get the cache entry. */
327 tem = iter.next ();
328
329 /* If it's not valid anymore, remove it. Iterating over a
330 collection whose contents are being removed is undefined
331 unless the removal is performed using the iterator's own
332 `remove' function, so tem.remove cannot be used here. */
333
334 if (tem.isValid ())
335 continue;
336
337 iter.remove ();
338 }
339 }
340
341 /* Remove every entry older than CACHE_INVALID_TIME from each
342 toplevel inside `cachedToplevels'. */
343
344 private void
345 pruneCache ()
346 {
347 Iterator<CacheEntry> iter;
348 Iterator<StatCacheEntry> statIter;
349 CacheEntry tem;
350 StatCacheEntry stat;
351
352 for (CacheToplevel toplevel : cacheToplevels.values ())
353 {
354 /* First, clean up expired cache entries. */
355 iter = toplevel.idCache.values ().iterator ();
356
357 while (iter.hasNext ())
358 {
359 /* Get the cache entry. */
360 tem = iter.next ();
361
362 /* If it's not valid anymore, remove it. Iterating over a
363 collection whose contents are being removed is
364 undefined unless the removal is performed using the
365 iterator's own `remove' function, so tem.remove cannot
366 be used here. */
367
368 if (tem.isValid ())
369 {
370 /* Otherwise, clean up expired items in its document
371 ID cache. */
372 pruneCache1 (tem.children.values ());
373 continue;
374 }
375
376 iter.remove ();
377 }
378
379 statIter = toplevel.statCache.values ().iterator ();
380
381 while (statIter.hasNext ())
382 {
383 /* Get the cache entry. */
384 stat = statIter.next ();
385
386 /* If it's not valid anymore, remove it. Iterating over a
387 collection whose contents are being removed is
388 undefined unless the removal is performed using the
389 iterator's own `remove' function, so tem.remove cannot
390 be used here. */
391
392 if (stat.isValid ())
393 continue;
394
395 statIter.remove ();
396 }
397 }
398
399 postPruneMessage ();
400 }
401
402 /* Cache file information within TOPLEVEL, under the list of
403 children CHILDREN.
404
405 NAME, ID, and TYPE should respectively be the display name of the
406 document within its parent document (the CacheEntry whose
407 `children' field is CHILDREN), its document ID, and its MIME
408 type.
409
410 If ID_ENTRY_EXISTS, don't create a new document ID entry within
411 CHILDREN indexed by NAME.
412
413 Value is the cache entry saved for the document ID. */
414
415 private CacheEntry
416 cacheChild (CacheToplevel toplevel,
417 HashMap<String, DocIdEntry> children,
418 String name, String id, String type,
419 boolean id_entry_exists)
420 {
421 DocIdEntry idEntry;
422 CacheEntry cacheEntry;
423
424 if (!id_entry_exists)
425 {
426 idEntry = new DocIdEntry ();
427 idEntry.documentId = id;
428 children.put (name, idEntry);
429 }
430
431 cacheEntry = new CacheEntry ();
432 cacheEntry.type = type;
433 toplevel.idCache.put (id, cacheEntry);
434 return cacheEntry;
435 }
436
437 /* Cache file status for DOCUMENTID within TOPLEVEL. Value is the
438 new cache entry. CURSOR is the cursor from where to retrieve the
439 file status, in the form of the columns COLUMN_FLAGS,
440 COLUMN_SIZE, COLUMN_MIME_TYPE and COLUMN_LAST_MODIFIED. */
441
442 private StatCacheEntry
443 cacheFileStatus (String documentId, CacheToplevel toplevel,
444 Cursor cursor)
445 {
446 StatCacheEntry entry;
447 int flagsIndex, columnIndex, typeIndex;
448 int sizeIndex, mtimeIndex;
449 String type;
450
451 /* Obtain the indices for columns wanted from this cursor. */
452 flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS);
453 sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE);
454 typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
455 mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED);
456
457 /* COLUMN_LAST_MODIFIED is allowed to be absent in a
458 conforming documents provider. */
459 if (flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0)
460 return null;
461
462 /* Get the file status from CURSOR. */
463 entry = new StatCacheEntry ();
464 entry.flags = cursor.getInt (flagsIndex);
465 type = cursor.getString (typeIndex);
466
467 if (type == null)
468 return null;
469
470 entry.isDirectory = type.equals (Document.MIME_TYPE_DIR);
471
472 if (cursor.isNull (sizeIndex))
473 /* The size is unknown. */
474 entry.size = -1;
475 else
476 entry.size = cursor.getLong (sizeIndex);
477
478 /* mtimeIndex is potentially unset, since document providers
479 aren't obligated to provide modification times. */
480
481 if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex))
482 entry.mtime = cursor.getLong (mtimeIndex);
483
484 /* Finally, add this entry to the cache and return. */
485 toplevel.statCache.put (documentId, entry);
486 return entry;
487 }
488
489 /* Cache the type and as many of the children of the directory
490 designated by DOCUMENTID as possible into TOPLEVEL.
491
492 CURSOR should be a cursor representing an open directory stream,
493 with its projection consisting of at least the display name,
494 document ID and MIME type columns.
495
496 Rewind the position of CURSOR to before its first element after
497 completion. */
498
499 private void
500 cacheDirectoryFromCursor (CacheToplevel toplevel, String documentId,
501 Cursor cursor)
502 {
503 CacheEntry entry, constitutent;
504 int nameColumn, idColumn, typeColumn;
505 String id, name, type;
506 DocIdEntry idEntry;
507
508 /* Find the numbers of the columns wanted. */
509
510 nameColumn
511 = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
512 idColumn
513 = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
514 typeColumn
515 = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
516
517 if (nameColumn < 0 || idColumn < 0 || typeColumn < 0)
518 return;
519
520 entry = new CacheEntry ();
521
522 /* We know this is a directory already. */
523 entry.type = Document.MIME_TYPE_DIR;
524 toplevel.idCache.put (documentId, entry);
525
526 /* Now, try to cache each of its constituents. */
527
528 while (cursor.moveToNext ())
529 {
530 try
531 {
532 name = cursor.getString (nameColumn);
533 id = cursor.getString (idColumn);
534 type = cursor.getString (typeColumn);
535
536 if (name == null || id == null || type == null)
537 continue;
538
539 /* First, add the name and ID to ENTRY's map of
540 children. */
541 idEntry = new DocIdEntry ();
542 idEntry.documentId = id;
543 entry.children.put (id, idEntry);
544
545 /* Cache the file status for ID within TOPELVEL too; if a
546 directory listing is being requested, it's very likely
547 that a series of calls for file status will follow. */
548
549 cacheFileStatus (id, toplevel, cursor);
550
551 /* If this constituent is a directory, don't cache any
552 information about it. It cannot be cached without
553 knowing its children. */
554
555 if (type.equals (Document.MIME_TYPE_DIR))
556 continue;
557
558 /* Otherwise, create a new cache entry comprised of its
559 type. */
560 constitutent = new CacheEntry ();
561 constitutent.type = type;
562 toplevel.idCache.put (documentId, entry);
563 }
564 catch (Exception e)
565 {
566 e.printStackTrace ();
567 continue;
568 }
569 }
570
571 /* Rewind cursor back to the beginning. */
572 cursor.moveToPosition (-1);
573 }
574
575 /* Post a message to run `pruneCache' every CACHE_PRUNE_TIME
576 seconds. */
577
578 private void
579 postPruneMessage ()
580 {
581 handler.postDelayed (new Runnable () {
582 @Override
583 public void
584 run ()
585 {
586 pruneCache ();
587 }
588 }, CACHE_PRUNE_TIME * 1000);
589 }
590
591 /* Invalidate the cache entry denoted by DOCUMENT_ID, within the
592 document tree URI.
593 Call this after deleting a document or directory.
594
595 At the same time, remove the final component within the file name
596 CACHENAME from the cache if it exists. */
597
598 public void
599 postInvalidateCache (final Uri uri, final String documentId,
600 final String cacheName)
601 {
602 handler.post (new Runnable () {
603 @Override
604 public void
605 run ()
606 {
607 CacheToplevel toplevel;
608 HashMap<String, DocIdEntry> children;
609 String[] components;
610 CacheEntry entry;
611 DocIdEntry idEntry;
612
613 toplevel = getCache (uri);
614 toplevel.idCache.remove (documentId);
615 toplevel.statCache.remove (documentId);
616
617 /* If the parent of CACHENAME is cached, remove it. */
618
619 children = toplevel.children;
620 components = cacheName.split ("/");
621
622 for (String component : components)
623 {
624 /* Java `split' removes trailing empty matches but not
625 leading or intermediary ones. */
626 if (component.isEmpty ())
627 continue;
628
629 if (component == components[components.length - 1])
630 {
631 /* This is the last component, so remove it from
632 children. */
633 children.remove (component);
634 return;
635 }
636 else
637 {
638 /* Search for this component within the last level
639 of the cache. */
640
641 idEntry = children.get (component);
642
643 if (idEntry == null)
644 /* Not cached, so return. */
645 return;
646
647 entry = toplevel.idCache.get (idEntry.documentId);
648
649 if (entry == null)
650 /* Not cached, so return. */
651 return;
652
653 /* Locate the next component within this
654 directory. */
655 children = entry.children;
656 }
657 }
658 }
659 });
660 }
661
662 /* Invalidate the cache entry denoted by DOCUMENT_ID, within the
663 document tree URI.
664 Call this after deleting a document or directory.
665
666 At the same time, remove the child referring to DOCUMENTID from
667 within CACHENAME's cache entry if it exists. */
668
669 public void
670 postInvalidateCacheDir (final Uri uri, final String documentId,
671 final String cacheName)
672 {
673 handler.post (new Runnable () {
674 @Override
675 public void
676 run ()
677 {
678 CacheToplevel toplevel;
679 HashMap<String, DocIdEntry> children;
680 String[] components;
681 CacheEntry entry;
682 DocIdEntry idEntry;
683 Iterator<DocIdEntry> iter;
684
685 toplevel = getCache (uri);
686 toplevel.idCache.remove (documentId);
687 toplevel.statCache.remove (documentId);
688
689 /* Now remove DOCUMENTID from CACHENAME's cache entry, if
690 any. */
691
692 children = toplevel.children;
693 components = cacheName.split ("/");
694
695 for (String component : components)
696 {
697 /* Java `split' removes trailing empty matches but not
698 leading or intermediary ones. */
699 if (component.isEmpty ())
700 continue;
701
702 /* Search for this component within the last level
703 of the cache. */
704
705 idEntry = children.get (component);
706
707 if (idEntry == null)
708 /* Not cached, so return. */
709 return;
710
711 entry = toplevel.idCache.get (idEntry.documentId);
712
713 if (entry == null)
714 /* Not cached, so return. */
715 return;
716
717 /* Locate the next component within this
718 directory. */
719 children = entry.children;
720 }
721
722 iter = children.values ().iterator ();
723 while (iter.hasNext ())
724 {
725 idEntry = iter.next ();
726
727 if (idEntry.documentId.equals (documentId))
728 {
729 iter.remove ();
730 break;
731 }
732 }
733 }
734 });
735 }
736
737 /* Invalidate the file status cache entry for DOCUMENTID within URI.
738 Call this when the contents of a file (i.e. the constituents of a
739 directory file) may have changed, but the document's display name
740 has not. */
741
742 public void
743 postInvalidateStat (final Uri uri, final String documentId)
744 {
745 handler.post (new Runnable () {
746 @Override
747 public void
748 run ()
749 {
750 CacheToplevel toplevel;
751
752 toplevel = getCache (uri);
753 toplevel.statCache.remove (documentId);
754 }
755 });
756 }
757
758
759
760 /* ``Prototypes'' for nested functions that are run within the SAF
761 thread and accepts a cancellation signal. They differ in their
762 return types. */
763
764 private abstract class SafIntFunction
765 {
766 /* The ``throws Throwable'' here is a Java idiosyncracy that tells
767 the compiler to allow arbitrary error objects to be signaled
768 from within this function.
769
770 Later, runIntFunction will try to re-throw any error object
771 generated by this function in the Emacs thread, using a trick
772 to avoid the compiler requirement to expressly declare that an
773 error (and which types of errors) will be signaled. */
774
775 public abstract int runInt (CancellationSignal signal)
776 throws Throwable;
777 };
778
779 private abstract class SafObjectFunction
780 {
781 /* The ``throws Throwable'' here is a Java idiosyncracy that tells
782 the compiler to allow arbitrary error objects to be signaled
783 from within this function.
784
785 Later, runObjectFunction will try to re-throw any error object
786 generated by this function in the Emacs thread, using a trick
787 to avoid the compiler requirement to expressly declare that an
788 error (and which types of errors) will be signaled. */
789
790 public abstract Object runObject (CancellationSignal signal)
791 throws Throwable;
792 };
793
794
795
796 /* Functions that run cancel-able queries. These functions are
797 internally run within the SAF thread. */
798
799 /* Throw the specified EXCEPTION. The type template T is erased by
800 the compiler before the object is compiled, so the compiled code
801 simply throws EXCEPTION without the cast being verified.
802
803 T should be RuntimeException to obtain the desired effect of
804 throwing an exception without a compiler check. */
805
806 @SuppressWarnings("unchecked")
807 private static <T extends Throwable> void
808 throwException (Throwable exception)
809 throws T
810 {
811 throw (T) exception;
812 }
813
814 /* Run the given function (or rather, its `runInt' field) within the
815 SAF thread, waiting for it to complete.
816
817 If async input arrives in the meantime and sets Vquit_flag,
818 signal the cancellation signal supplied to that function.
819
820 Rethrow any exception thrown from that function, and return its
821 value otherwise. */
822
823 private int
824 runIntFunction (final SafIntFunction function)
825 {
826 final EmacsHolder<Object> result;
827 final CancellationSignal signal;
828 Throwable throwable;
829
830 result = new EmacsHolder<Object> ();
831 signal = new CancellationSignal ();
832
833 handler.post (new Runnable () {
834 @Override
835 public void
836 run ()
837 {
838 try
839 {
840 result.thing
841 = Integer.valueOf (function.runInt (signal));
842 }
843 catch (Throwable throwable)
844 {
845 result.thing = throwable;
846 }
847
848 EmacsNative.safPostRequest ();
849 }
850 });
851
852 if (EmacsNative.safSyncAndReadInput () != 0)
853 {
854 signal.cancel ();
855
856 /* Now wait for the function to finish. Either the signal has
857 arrived after the query took place, in which case it will
858 finish normally, or an OperationCanceledException will be
859 thrown. */
860
861 EmacsNative.safSync ();
862 }
863
864 if (result.thing instanceof Throwable)
865 {
866 throwable = (Throwable) result.thing;
867 EmacsSafThread.<RuntimeException>throwException (throwable);
868 }
869
870 return (Integer) result.thing;
871 }
872
873 /* Run the given function (or rather, its `runObject' field) within
874 the SAF thread, waiting for it to complete.
875
876 If async input arrives in the meantime and sets Vquit_flag,
877 signal the cancellation signal supplied to that function.
878
879 Rethrow any exception thrown from that function, and return its
880 value otherwise. */
881
882 private Object
883 runObjectFunction (final SafObjectFunction function)
884 {
885 final EmacsHolder<Object> result;
886 final CancellationSignal signal;
887 Throwable throwable;
888
889 result = new EmacsHolder<Object> ();
890 signal = new CancellationSignal ();
891
892 handler.post (new Runnable () {
893 @Override
894 public void
895 run ()
896 {
897 try
898 {
899 result.thing = function.runObject (signal);
900 }
901 catch (Throwable throwable)
902 {
903 result.thing = throwable;
904 }
905
906 EmacsNative.safPostRequest ();
907 }
908 });
909
910 if (EmacsNative.safSyncAndReadInput () != 0)
911 {
912 signal.cancel ();
913
914 /* Now wait for the function to finish. Either the signal has
915 arrived after the query took place, in which case it will
916 finish normally, or an OperationCanceledException will be
917 thrown. */
918
919 EmacsNative.safSync ();
920 }
921
922 if (result.thing instanceof Throwable)
923 {
924 throwable = (Throwable) result.thing;
925 EmacsSafThread.<RuntimeException>throwException (throwable);
926 }
927
928 return result.thing;
929 }
930
931 /* The crux of `documentIdFromName1', run within the SAF thread.
932 SIGNAL should be a cancellation signal run upon quitting. */
933
934 private int
935 documentIdFromName1 (String tree_uri, String name,
936 String[] id_return, CancellationSignal signal)
937 {
938 Uri uri, treeUri;
939 String id, type, newId, newType;
940 String[] components, projection;
941 Cursor cursor;
942 int nameColumn, idColumn, typeColumn;
943 CacheToplevel toplevel;
944 DocIdEntry idEntry;
945 HashMap<String, DocIdEntry> children, next;
946 CacheEntry cache;
947
948 projection = new String[] {
949 Document.COLUMN_DISPLAY_NAME,
950 Document.COLUMN_DOCUMENT_ID,
951 Document.COLUMN_MIME_TYPE,
952 };
953
954 /* Parse the URI identifying the tree first. */
955 uri = Uri.parse (tree_uri);
956
957 /* Now, split NAME into its individual components. */
958 components = name.split ("/");
959
960 /* Set id and type to the value at the root of the tree. */
961 type = id = null;
962 cursor = null;
963
964 /* Obtain the top level of this cache. */
965 toplevel = getCache (uri);
966
967 /* Set the current map of children to this top level. */
968 children = toplevel.children;
969
970 /* For each component... */
971
972 try
973 {
974 for (String component : components)
975 {
976 /* Java split doesn't behave very much like strtok when it
977 comes to trailing and leading delimiters... */
978 if (component.isEmpty ())
979 continue;
980
981 /* Search for component within the currently cached list
982 of children. */
983
984 idEntry = children.get (component);
985
986 if (idEntry != null)
987 {
988 /* The document ID is known. Now find the
989 corresponding document ID cache. */
990
991 cache = toplevel.idCache.get (idEntry.documentId);
992
993 /* Fetch just the information for this document. */
994
995 if (cache == null)
996 cache = idEntry.getCacheEntry (resolver, uri, toplevel,
997 signal);
998
999 if (cache == null)
1000 {
1001 /* File status matching idEntry could not be
1002 obtained. Treat this as if the file does not
1003 exist. */
1004
1005 children.remove (component);
1006
1007 if (id == null)
1008 id = DocumentsContract.getTreeDocumentId (uri);
1009
1010 id_return[0] = id;
1011
1012 if ((type == null
1013 || type.equals (Document.MIME_TYPE_DIR))
1014 /* ... and type and id currently represent the
1015 penultimate component. */
1016 && component == components[components.length - 1])
1017 return -2;
1018
1019 return -1;
1020 }
1021
1022 /* Otherwise, use the cached information. */
1023 id = idEntry.documentId;
1024 type = cache.type;
1025 children = cache.children;
1026 continue;
1027 }
1028
1029 /* Create the tree URI for URI from ID if it exists, or
1030 the root otherwise. */
1031
1032 if (id == null)
1033 id = DocumentsContract.getTreeDocumentId (uri);
1034
1035 treeUri
1036 = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id);
1037
1038 /* Look for a file in this directory by the name of
1039 component. */
1040
1041 cursor = resolver.query (treeUri, projection,
1042 (Document.COLUMN_DISPLAY_NAME
1043 + " = ?"),
1044 new String[] { component, },
1045 null, signal);
1046
1047 if (cursor == null)
1048 return -1;
1049
1050 /* Find the column numbers for each of the columns that
1051 are wanted. */
1052
1053 nameColumn
1054 = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
1055 idColumn
1056 = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
1057 typeColumn
1058 = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1059
1060 if (nameColumn < 0 || idColumn < 0 || typeColumn < 0)
1061 return -1;
1062
1063 next = null;
1064
1065 while (true)
1066 {
1067 /* Even though the query selects for a specific
1068 display name, some content providers nevertheless
1069 return every file within the directory. */
1070
1071 if (!cursor.moveToNext ())
1072 {
1073 /* If a component has been found, break out of the
1074 loop. */
1075
1076 if (next != null)
1077 break;
1078
1079 /* If the last component considered is a
1080 directory... */
1081 if ((type == null
1082 || type.equals (Document.MIME_TYPE_DIR))
1083 /* ... and type and id currently represent the
1084 penultimate component. */
1085 && component == components[components.length - 1])
1086 {
1087 /* The cursor is empty. In this case, return
1088 -2 and the current document ID (belonging
1089 to the previous component) in
1090 ID_RETURN. */
1091
1092 id_return[0] = id;
1093
1094 /* But return -1 on the off chance that id is
1095 null. */
1096
1097 if (id == null)
1098 return -1;
1099
1100 return -2;
1101 }
1102
1103 /* The last component found is not a directory, so
1104 return -1. */
1105 return -1;
1106 }
1107
1108 /* So move CURSOR to a row with the right display
1109 name. */
1110
1111 name = cursor.getString (nameColumn);
1112 newId = cursor.getString (idColumn);
1113 newType = cursor.getString (typeColumn);
1114
1115 /* Any of the three variables above may be NULL if the
1116 column data is of the wrong type depending on how
1117 the Cursor returned is implemented. */
1118
1119 if (name == null || newId == null || newType == null)
1120 return -1;
1121
1122 /* Cache this name, even if it isn't the document
1123 that's being searched for. */
1124
1125 cache = cacheChild (toplevel, children, name,
1126 newId, newType,
1127 idEntry != null);
1128
1129 /* Record the desired component once it is located,
1130 but continue reading and caching items from the
1131 cursor. */
1132
1133 if (name.equals (component))
1134 {
1135 id = newId;
1136 next = cache.children;
1137 type = newType;
1138 }
1139 }
1140
1141 children = next;
1142
1143 /* Now close the cursor. */
1144 cursor.close ();
1145 cursor = null;
1146
1147 /* ID may have become NULL if the data is in an invalid
1148 format. */
1149 if (id == null)
1150 return -1;
1151 }
1152 }
1153 finally
1154 {
1155 /* If an error is thrown within the block above, let
1156 android_saf_exception_check handle it, but make sure the
1157 cursor is closed. */
1158
1159 if (cursor != null)
1160 cursor.close ();
1161 }
1162
1163 /* Here, id is either NULL (meaning the same as TREE_URI), and
1164 type is either NULL (in which case id should also be NULL) or
1165 the MIME type of the file. */
1166
1167 /* First return the ID. */
1168
1169 if (id == null)
1170 id_return[0] = DocumentsContract.getTreeDocumentId (uri);
1171 else
1172 id_return[0] = id;
1173
1174 /* Next, return whether or not this is a directory. */
1175 if (type == null || type.equals (Document.MIME_TYPE_DIR))
1176 return 1;
1177
1178 return 0;
1179 }
1180
1181 /* Find the document ID of the file within TREE_URI designated by
1182 NAME.
1183
1184 NAME is a ``file name'' comprised of the display names of
1185 individual files. Each constituent component prior to the last
1186 must name a directory file within TREE_URI.
1187
1188 Upon success, return 0 or 1 (contingent upon whether or not the
1189 last component within NAME is a directory) and place the document
1190 ID of the named file in ID_RETURN[0].
1191
1192 If the designated file can't be located, but each component of
1193 NAME up to the last component can and is a directory, return -2
1194 and the ID of the last component located in ID_RETURN[0].
1195
1196 If the designated file can't be located, return -1, or signal one
1197 of OperationCanceledException, SecurityException,
1198 FileNotFoundException, or UnsupportedOperationException. */
1199
1200 public int
1201 documentIdFromName (final String tree_uri, final String name,
1202 final String[] id_return)
1203 {
1204 return runIntFunction (new SafIntFunction () {
1205 @Override
1206 public int
1207 runInt (CancellationSignal signal)
1208 {
1209 return documentIdFromName1 (tree_uri, name, id_return,
1210 signal);
1211 }
1212 });
1213 }
1214
1215 /* The bulk of `statDocument'. SIGNAL should be a cancelation
1216 signal. */
1217
1218 private long[]
1219 statDocument1 (String uri, String documentId,
1220 CancellationSignal signal)
1221 {
1222 Uri uriObject, tree;
1223 String[] projection;
1224 long[] stat;
1225 Cursor cursor;
1226 CacheToplevel toplevel;
1227 StatCacheEntry cache;
1228
1229 tree = Uri.parse (uri);
1230
1231 if (documentId == null)
1232 documentId = DocumentsContract.getTreeDocumentId (tree);
1233
1234 /* Create a document URI representing DOCUMENTID within URI's
1235 authority. */
1236
1237 uriObject
1238 = DocumentsContract.buildDocumentUriUsingTree (tree, documentId);
1239
1240 /* See if the file status cache currently contains this
1241 document. */
1242
1243 toplevel = getCache (tree);
1244 cache = toplevel.statCache.get (documentId);
1245
1246 if (cache == null || !cache.isValid ())
1247 {
1248 /* Stat this document and enter its information into the
1249 cache. */
1250
1251 projection = new String[] {
1252 Document.COLUMN_FLAGS,
1253 Document.COLUMN_LAST_MODIFIED,
1254 Document.COLUMN_MIME_TYPE,
1255 Document.COLUMN_SIZE,
1256 };
1257
1258 cursor = resolver.query (uriObject, projection, null,
1259 null, null, signal);
1260
1261 if (cursor == null)
1262 return null;
1263
1264 try
1265 {
1266 if (!cursor.moveToFirst ())
1267 return null;
1268
1269 cache = cacheFileStatus (documentId, toplevel, cursor);
1270 }
1271 finally
1272 {
1273 cursor.close ();
1274 }
1275
1276 /* If cache is still null, return null. */
1277
1278 if (cache == null)
1279 return null;
1280 }
1281
1282 /* Create the array of file status and populate it with the
1283 information within cache. */
1284 stat = new long[3];
1285
1286 stat[0] |= S_IRUSR;
1287 if ((cache.flags & Document.FLAG_SUPPORTS_WRITE) != 0)
1288 stat[0] |= S_IWUSR;
1289
1290 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
1291 && (cache.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
1292 stat[0] |= S_IFCHR;
1293
1294 stat[1] = cache.size;
1295
1296 /* Check if this is a directory file. */
1297 if (cache.isDirectory
1298 /* Files shouldn't be specials and directories at the same
1299 time, but Android doesn't forbid document providers
1300 from returning this information. */
1301 && (stat[0] & S_IFCHR) == 0)
1302 {
1303 /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories,
1304 just assume they're writable. */
1305 stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR;
1306
1307 /* Directory files cannot be modified if
1308 FLAG_DIR_SUPPORTS_CREATE is not set. */
1309
1310 if ((cache.flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
1311 stat[0] &= ~S_IWUSR;
1312 }
1313
1314 /* If this file is neither a character special nor a
1315 directory, indicate that it's a regular file. */
1316
1317 if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0)
1318 stat[0] |= S_IFREG;
1319
1320 stat[2] = cache.mtime;
1321 return stat;
1322 }
1323
1324 /* Return file status for the document designated by the given
1325 DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document
1326 ID in URI itself.
1327
1328 Value is null upon failure, or an array of longs [MODE, SIZE,
1329 MTIM] upon success, where MODE contains the file type and access
1330 modes of the file as in `struct stat', SIZE is the size of the
1331 file in BYTES or -1 if not known, and MTIM is the time of the
1332 last modification to this file in milliseconds since 00:00,
1333 January 1st, 1970.
1334
1335 OperationCanceledException and other typical exceptions may be
1336 signaled upon receiving async input or other errors. */
1337
1338 public long[]
1339 statDocument (final String uri, final String documentId)
1340 {
1341 return (long[]) runObjectFunction (new SafObjectFunction () {
1342 @Override
1343 public Object
1344 runObject (CancellationSignal signal)
1345 {
1346 return statDocument1 (uri, documentId, signal);
1347 }
1348 });
1349 }
1350
1351 /* The bulk of `accessDocument'. SIGNAL should be a cancellation
1352 signal. */
1353
1354 private int
1355 accessDocument1 (String uri, String documentId, boolean writable,
1356 CancellationSignal signal)
1357 {
1358 Uri uriObject;
1359 String[] projection;
1360 int tem, index;
1361 String tem1;
1362 Cursor cursor;
1363 CacheToplevel toplevel;
1364 CacheEntry entry;
1365
1366 uriObject = Uri.parse (uri);
1367
1368 if (documentId == null)
1369 documentId = DocumentsContract.getTreeDocumentId (uriObject);
1370
1371 /* If WRITABLE is false and the document ID is cached, use its
1372 cached value instead. This speeds up
1373 `directory-files-with-attributes' a little. */
1374
1375 if (!writable)
1376 {
1377 toplevel = getCache (uriObject);
1378 entry = toplevel.idCache.get (documentId);
1379
1380 if (entry != null)
1381 return 0;
1382 }
1383
1384 /* Create a document URI representing DOCUMENTID within URI's
1385 authority. */
1386
1387 uriObject
1388 = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
1389
1390 /* Now stat this document. */
1391
1392 projection = new String[] {
1393 Document.COLUMN_FLAGS,
1394 Document.COLUMN_MIME_TYPE,
1395 };
1396
1397 cursor = resolver.query (uriObject, projection, null,
1398 null, null, signal);
1399
1400 if (cursor == null)
1401 return -1;
1402
1403 try
1404 {
1405 if (!cursor.moveToFirst ())
1406 return -1;
1407
1408 if (!writable)
1409 return 0;
1410
1411 index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1412 if (index < 0)
1413 return -3;
1414
1415 /* Get the type of this file to check if it's a directory. */
1416 tem1 = cursor.getString (index);
1417
1418 /* Check if this is a directory file. */
1419 if (tem1.equals (Document.MIME_TYPE_DIR))
1420 {
1421 /* If so, don't check for FLAG_SUPPORTS_WRITE.
1422 Check for FLAG_DIR_SUPPORTS_CREATE instead. */
1423
1424 if (!writable)
1425 return 0;
1426
1427 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
1428 if (index < 0)
1429 return -3;
1430
1431 tem = cursor.getInt (index);
1432 if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
1433 return -3;
1434
1435 return 0;
1436 }
1437
1438 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
1439 if (index < 0)
1440 return -3;
1441
1442 tem = cursor.getInt (index);
1443 if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0)
1444 return -3;
1445 }
1446 finally
1447 {
1448 /* Close the cursor if an exception occurs. */
1449 cursor.close ();
1450 }
1451
1452 return 0;
1453 }
1454
1455 /* Find out whether Emacs has access to the document designated by
1456 the specified DOCUMENTID within the tree URI. If DOCUMENTID is
1457 NULL, use the document ID in URI itself.
1458
1459 If WRITABLE, also check that the file is writable, which is true
1460 if it is either a directory or its flags contains
1461 FLAG_SUPPORTS_WRITE.
1462
1463 Value is 0 if the file is accessible, and one of the following if
1464 not:
1465
1466 -1, if the file does not exist.
1467 -2, if WRITABLE and the file is not writable.
1468 -3, upon any other error.
1469
1470 In addition, arbitrary runtime exceptions (such as
1471 SecurityException or UnsupportedOperationException) may be
1472 thrown. */
1473
1474 public int
1475 accessDocument (final String uri, final String documentId,
1476 final boolean writable)
1477 {
1478 return runIntFunction (new SafIntFunction () {
1479 @Override
1480 public int
1481 runInt (CancellationSignal signal)
1482 {
1483 return accessDocument1 (uri, documentId, writable,
1484 signal);
1485 }
1486 });
1487 }
1488
1489 /* The crux of openDocumentDirectory. SIGNAL must be a cancellation
1490 signal. */
1491
1492 private Cursor
1493 openDocumentDirectory1 (String uri, String documentId,
1494 CancellationSignal signal)
1495 {
1496 Uri uriObject, tree;
1497 Cursor cursor;
1498 String projection[];
1499 CacheToplevel toplevel;
1500
1501 tree = uriObject = Uri.parse (uri);
1502
1503 /* If documentId is not set, use the document ID of the tree URI
1504 itself. */
1505
1506 if (documentId == null)
1507 documentId = DocumentsContract.getTreeDocumentId (uriObject);
1508
1509 /* Build a URI representing each directory entry within
1510 DOCUMENTID. */
1511
1512 uriObject
1513 = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject,
1514 documentId);
1515
1516 projection = new String [] {
1517 Document.COLUMN_DISPLAY_NAME,
1518 Document.COLUMN_DOCUMENT_ID,
1519 Document.COLUMN_MIME_TYPE,
1520 Document.COLUMN_FLAGS,
1521 Document.COLUMN_LAST_MODIFIED,
1522 Document.COLUMN_SIZE,
1523 };
1524
1525 cursor = resolver.query (uriObject, projection, null, null,
1526 null, signal);
1527
1528 /* Create a new cache entry tied to this document ID. */
1529
1530 if (cursor != null)
1531 {
1532 toplevel = getCache (tree);
1533 cacheDirectoryFromCursor (toplevel, documentId,
1534 cursor);
1535 }
1536
1537 /* Return the cursor. */
1538 return cursor;
1539 }
1540
1541 /* Open a cursor representing each entry within the directory
1542 designated by the specified DOCUMENTID within the tree URI.
1543
1544 If DOCUMENTID is NULL, use the document ID within URI itself.
1545 Value is NULL upon failure.
1546
1547 In addition, arbitrary runtime exceptions (such as
1548 SecurityException or UnsupportedOperationException) may be
1549 thrown. */
1550
1551 public Cursor
1552 openDocumentDirectory (final String uri, final String documentId)
1553 {
1554 return (Cursor) runObjectFunction (new SafObjectFunction () {
1555 @Override
1556 public Object
1557 runObject (CancellationSignal signal)
1558 {
1559 return openDocumentDirectory1 (uri, documentId, signal);
1560 }
1561 });
1562 }
1563
1564 /* The crux of `openDocument'. SIGNAL must be a cancellation
1565 signal. */
1566
1567 public ParcelFileDescriptor
1568 openDocument1 (String uri, String documentId, boolean write,
1569 boolean truncate, CancellationSignal signal)
1570 throws Throwable
1571 {
1572 Uri treeUri, documentUri;
1573 String mode;
1574 ParcelFileDescriptor fileDescriptor;
1575 CacheToplevel toplevel;
1576
1577 treeUri = Uri.parse (uri);
1578
1579 /* documentId must be set for this request, since it doesn't make
1580 sense to ``open'' the root of the directory tree. */
1581
1582 documentUri
1583 = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId);
1584
1585 /* Select the mode used to open the file. */
1586
1587 if (write)
1588 {
1589 if (truncate)
1590 mode = "rwt";
1591 else
1592 mode = "rw";
1593 }
1594 else
1595 mode = "r";
1596
1597 fileDescriptor
1598 = resolver.openFileDescriptor (documentUri, mode,
1599 signal);
1600
1601 /* If a writable file descriptor is requested and TRUNCATE is set,
1602 then probe the file descriptor to detect if it is actually
1603 readable. If not, close this file descriptor and reopen it
1604 with MODE set to rw; some document providers granting access to
1605 Samba shares don't implement rwt, but these document providers
1606 invariably truncate the file opened even when the mode is
1607 merely rw.
1608
1609 This may be ascribed to a mix-up in Android's documentation
1610 regardin DocumentsProvider: the `openDocument' function is only
1611 documented to accept r or rw, whereas the default
1612 implementation of the `openFile' function (which documents rwt)
1613 delegates to `openDocument'. */
1614
1615 if (write && truncate && fileDescriptor != null
1616 && !EmacsNative.ftruncate (fileDescriptor.getFd ()))
1617 {
1618 try
1619 {
1620 fileDescriptor.closeWithError ("File descriptor requested"
1621 + " is not writable");
1622 }
1623 catch (IOException e)
1624 {
1625 Log.w (TAG, "Leaking unclosed file descriptor " + e);
1626 }
1627
1628 fileDescriptor
1629 = resolver.openFileDescriptor (documentUri, "rw", signal);
1630
1631 /* Try to truncate fileDescriptor just to stay on the safe
1632 side. */
1633 if (fileDescriptor != null)
1634 EmacsNative.ftruncate (fileDescriptor.getFd ());
1635 }
1636
1637 /* Every time a document is opened, remove it from the file status
1638 cache. */
1639 toplevel = getCache (treeUri);
1640 toplevel.statCache.remove (documentId);
1641
1642 return fileDescriptor;
1643 }
1644
1645 /* Open a file descriptor for a file document designated by
1646 DOCUMENTID within the document tree identified by URI. If
1647 TRUNCATE and the document already exists, truncate its contents
1648 before returning.
1649
1650 On Android 9.0 and earlier, always open the document in
1651 ``read-write'' mode; this instructs the document provider to
1652 return a seekable file that is stored on disk and returns correct
1653 file status.
1654
1655 Under newer versions of Android, open the document in a
1656 non-writable mode if WRITE is false. This is possible because
1657 these versions allow Emacs to explicitly request a seekable
1658 on-disk file.
1659
1660 Value is NULL upon failure or a parcel file descriptor upon
1661 success. Call `ParcelFileDescriptor.close' on this file
1662 descriptor instead of using the `close' system call.
1663
1664 FileNotFoundException and/or SecurityException and/or
1665 UnsupportedOperationException and/or OperationCanceledException
1666 may be thrown upon failure. */
1667
1668 public ParcelFileDescriptor
1669 openDocument (final String uri, final String documentId,
1670 final boolean write, final boolean truncate)
1671 {
1672 Object tem;
1673
1674 tem = runObjectFunction (new SafObjectFunction () {
1675 @Override
1676 public Object
1677 runObject (CancellationSignal signal)
1678 throws Throwable
1679 {
1680 return openDocument1 (uri, documentId, write, truncate,
1681 signal);
1682 }
1683 });
1684
1685 return (ParcelFileDescriptor) tem;
1686 }
1687};
diff --git a/java/org/gnu/emacs/EmacsSdk11Clipboard.java b/java/org/gnu/emacs/EmacsSdk11Clipboard.java
new file mode 100644
index 00000000000..4959ec36eed
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSdk11Clipboard.java
@@ -0,0 +1,290 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.content.ClipboardManager;
23import android.content.Context;
24import android.content.ContentResolver;
25import android.content.ClipData;
26import android.content.ClipDescription;
27
28import android.content.res.AssetFileDescriptor;
29
30import android.net.Uri;
31
32import android.util.Log;
33
34import android.os.Build;
35
36import java.io.FileNotFoundException;
37import java.io.IOException;
38import java.io.UnsupportedEncodingException;
39
40/* This class implements EmacsClipboard for Android 3.0 and later
41 systems. */
42
43public final class EmacsSdk11Clipboard extends EmacsClipboard
44 implements ClipboardManager.OnPrimaryClipChangedListener
45{
46 private static final String TAG = "EmacsSdk11Clipboard";
47 private ClipboardManager manager;
48 private boolean ownsClipboard;
49 private int clipboardChangedCount;
50 private int monitoredClipboardChangedCount;
51 private ContentResolver resolver;
52
53 public
54 EmacsSdk11Clipboard ()
55 {
56 manager = EmacsService.SERVICE.getClipboardManager ();
57
58 /* The system forbids Emacs from reading clipboard data in the
59 background under Android 10 or later. */
60
61 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
62 manager.addPrimaryClipChangedListener (this);
63
64 /* Now obtain the content resolver used to open file
65 descriptors. */
66
67 resolver = EmacsService.SERVICE.getContentResolver ();
68 }
69
70 @Override
71 public synchronized void
72 onPrimaryClipChanged ()
73 {
74 Log.d (TAG, ("onPrimaryClipChanged: "
75 + monitoredClipboardChangedCount
76 + " " + clipboardChangedCount));
77
78 /* Increment monitoredClipboardChangeCount. If it is now greater
79 than clipboardChangedCount, then Emacs no longer owns the
80 clipboard. */
81 monitoredClipboardChangedCount++;
82
83 if (monitoredClipboardChangedCount > clipboardChangedCount)
84 {
85 ownsClipboard = false;
86
87 /* Reset both values back to 0. */
88 monitoredClipboardChangedCount = 0;
89 clipboardChangedCount = 0;
90 }
91 }
92
93 /* Set the clipboard text to CLIPBOARD, a string in UTF-8
94 encoding. */
95
96 @Override
97 public synchronized void
98 setClipboard (byte[] bytes)
99 {
100 ClipData data;
101 String string;
102
103 try
104 {
105 string = new String (bytes, "UTF-8");
106 data = ClipData.newPlainText ("Emacs", string);
107 manager.setPrimaryClip (data);
108 ownsClipboard = true;
109
110 /* onPrimaryClipChanged will be called again. Use this
111 variable to keep track of how many times the clipboard has
112 been changed. */
113 ++clipboardChangedCount;
114 }
115 catch (UnsupportedEncodingException exception)
116 {
117 Log.w (TAG, "setClipboard: " + exception);
118 }
119 }
120
121 /* Return whether or not Emacs owns the clipboard. Value is 1 if
122 Emacs does, 0 if Emacs does not, and -1 if that information is
123 unavailable. */
124
125 @Override
126 public synchronized int
127 ownsClipboard ()
128 {
129 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q)
130 return -1;
131
132 return ownsClipboard ? 1 : 0;
133 }
134
135 /* Return whether or not clipboard content currently exists. */
136
137 @Override
138 public boolean
139 clipboardExists ()
140 {
141 return manager.hasPrimaryClip ();
142 }
143
144 /* Return the current content of the clipboard, as plain text, or
145 NULL if no content is available. */
146
147 @Override
148 public byte[]
149 getClipboard ()
150 {
151 ClipData clip;
152 CharSequence text;
153 Context context;
154
155 clip = manager.getPrimaryClip ();
156
157 if (clip == null || clip.getItemCount () < 1)
158 return null;
159
160 context = EmacsService.SERVICE;
161
162 try
163 {
164 text = clip.getItemAt (0).coerceToText (context);
165 return text.toString ().getBytes ("UTF-8");
166 }
167 catch (UnsupportedEncodingException exception)
168 {
169 Log.w (TAG, "getClipboard: " + exception);
170 }
171
172 return null;
173 }
174
175 /* Return an array of targets currently provided by the
176 clipboard, or NULL if there are none. */
177
178 @Override
179 public byte[][]
180 getClipboardTargets ()
181 {
182 ClipData clip;
183 ClipDescription description;
184 byte[][] typeArray;
185 int i;
186
187 /* N.B. that Android calls the clipboard the ``primary clip''; it
188 is not related to the X primary selection. */
189 clip = manager.getPrimaryClip ();
190 description = clip.getDescription ();
191 i = description.getMimeTypeCount ();
192 typeArray = new byte[i][i];
193
194 try
195 {
196 for (i = 0; i < description.getMimeTypeCount (); ++i)
197 typeArray[i] = description.getMimeType (i).getBytes ("UTF-8");
198 }
199 catch (UnsupportedEncodingException exception)
200 {
201 return null;
202 }
203
204 return typeArray;
205 }
206
207 /* Return the clipboard data for the given target, or NULL if it
208 does not exist.
209
210 Value is normally an array of three longs: the file descriptor,
211 the start offset of the data, and its length; length may be
212 AssetFileDescriptor.UNKOWN_LENGTH, meaning that the data extends
213 from that offset to the end of the file.
214
215 Do not use this function to open text targets; use `getClipboard'
216 for that instead, as it will handle selection data consisting
217 solely of a URI. */
218
219 @Override
220 public long[]
221 getClipboardData (byte[] target)
222 {
223 ClipData data;
224 String mimeType;
225 int fd;
226 AssetFileDescriptor assetFd;
227 Uri uri;
228 long[] value;
229
230 /* Decode the target given by Emacs. */
231 try
232 {
233 mimeType = new String (target, "UTF-8");
234 }
235 catch (UnsupportedEncodingException exception)
236 {
237 return null;
238 }
239
240 Log.d (TAG, "getClipboardData: "+ mimeType);
241
242 /* Now obtain the clipboard data and the data corresponding to
243 that MIME type. */
244
245 data = manager.getPrimaryClip ();
246
247 if (data.getItemCount () < 1)
248 return null;
249
250 try
251 {
252 uri = data.getItemAt (0).getUri ();
253
254 if (uri == null)
255 return null;
256
257 Log.d (TAG, "getClipboardData: "+ uri);
258
259 /* Now open the file descriptor. */
260 assetFd = resolver.openTypedAssetFileDescriptor (uri, mimeType,
261 null);
262
263 /* Duplicate the file descriptor. */
264 fd = assetFd.getParcelFileDescriptor ().getFd ();
265 fd = EmacsNative.dup (fd);
266
267 /* Return the relevant information. */
268 value = new long[] { fd, assetFd.getStartOffset (),
269 assetFd.getLength (), };
270
271 /* Close the original offset. */
272 assetFd.close ();
273
274 Log.d (TAG, "getClipboardData: "+ value);
275 }
276 catch (FileNotFoundException e)
277 {
278 return null;
279 }
280 catch (IOException e)
281 {
282 return null;
283 }
284
285 /* Don't return value if the file descriptor couldn't be
286 created. */
287
288 return fd != -1 ? value : null;
289 }
290};
diff --git a/java/org/gnu/emacs/EmacsSdk23FontDriver.java b/java/org/gnu/emacs/EmacsSdk23FontDriver.java
new file mode 100644
index 00000000000..aaba8dbd166
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSdk23FontDriver.java
@@ -0,0 +1,114 @@
1/* Font backend for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.graphics.Paint;
23import android.graphics.Rect;
24
25public final class EmacsSdk23FontDriver extends EmacsSdk7FontDriver
26{
27 private void
28 textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics,
29 Paint paint, Rect bounds)
30 {
31 char[] text;
32
33 text = new char[2];
34 text[0] = (char) code;
35 text[1] = 'c';
36
37 paint.getTextBounds (text, 0, 1, bounds);
38
39 metrics.lbearing = (short) bounds.left;
40 metrics.rbearing = (short) bounds.right;
41 metrics.ascent = (short) -bounds.top;
42 metrics.descent = (short) bounds.bottom;
43 metrics.width
44 = (short) paint.getRunAdvance (text, 0, 1, 0, 1, false, 1);
45 }
46
47 @Override
48 public void
49 textExtents (FontObject font, int code[], FontMetrics fontMetrics)
50 {
51 int i;
52 Paint paintCache;
53 Rect boundsCache;
54 Sdk7FontObject fontObject;
55 char[] text;
56 float width;
57
58 fontObject = (Sdk7FontObject) font;
59 paintCache = fontObject.typeface.typefacePaint;
60 paintCache.setTextSize (fontObject.pixelSize);
61 boundsCache = new Rect ();
62
63 if (code.length == 0)
64 {
65 fontMetrics.lbearing = 0;
66 fontMetrics.rbearing = 0;
67 fontMetrics.ascent = 0;
68 fontMetrics.descent = 0;
69 fontMetrics.width = 0;
70 }
71 else if (code.length == 1)
72 textExtents1 ((Sdk7FontObject) font, code[0], fontMetrics,
73 paintCache, boundsCache);
74 else
75 {
76 text = new char[code.length + 1];
77
78 for (i = 0; i < code.length; ++i)
79 text[i] = (char) code[i];
80
81 text[code.length] = 'c';
82
83 paintCache.getTextBounds (text, 0, code.length,
84 boundsCache);
85 width = paintCache.getRunAdvance (text, 0, code.length, 0,
86 code.length,
87 false, code.length);
88
89 fontMetrics.lbearing = (short) boundsCache.left;
90 fontMetrics.rbearing = (short) boundsCache.right;
91 fontMetrics.ascent = (short) -boundsCache.top;
92 fontMetrics.descent = (short) boundsCache.bottom;
93 fontMetrics.width = (short) width;
94 }
95 }
96
97 @Override
98 public int
99 hasChar (FontSpec font, char charCode)
100 {
101 Sdk7FontObject fontObject;
102 Paint paint;
103
104 if (font instanceof Sdk7FontObject)
105 {
106 fontObject = (Sdk7FontObject) font;
107 paint = fontObject.typeface.typefacePaint;
108 }
109 else
110 paint = ((Sdk7FontEntity) font).typeface.typefacePaint;
111
112 return paint.hasGlyph (String.valueOf (charCode)) ? 1 : 0;
113 }
114};
diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java
new file mode 100644
index 00000000000..97969585d16
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java
@@ -0,0 +1,539 @@
1/* Font backend for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.io.File;
23
24import java.util.LinkedList;
25import java.util.List;
26
27import android.graphics.Paint;
28import android.graphics.Rect;
29import android.graphics.Typeface;
30import android.graphics.Canvas;
31
32import android.util.Log;
33
34public class EmacsSdk7FontDriver extends EmacsFontDriver
35{
36 private static final String TOFU_STRING = "\uDB3F\uDFFD";
37 private static final String EM_STRING = "m";
38 private static final String TAG = "EmacsSdk7FontDriver";
39
40 protected static final class Sdk7Typeface
41 {
42 /* The typeface and paint. */
43 public Typeface typeface;
44 public Paint typefacePaint;
45 public String familyName;
46 public int slant, width, weight, spacing;
47
48 public
49 Sdk7Typeface (String fileName, Typeface typeface)
50 {
51 String style, testString;
52 int index, measured, i;
53 float[] widths;
54
55 slant = NORMAL;
56 weight = REGULAR;
57 width = UNSPECIFIED;
58 spacing = PROPORTIONAL;
59
60 this.typeface = typeface;
61
62 typefacePaint = new Paint ();
63 typefacePaint.setAntiAlias (true);
64 typefacePaint.setTypeface (typeface);
65
66 /* For the calls to measureText below. */
67 typefacePaint.setTextSize (10.0f);
68
69 /* Parse the file name into some useful data. First, strip off
70 the extension. */
71 fileName = fileName.split ("\\.", 2)[0];
72
73 /* Next, split the file name by dashes. Everything before the
74 last dash is part of the family name. */
75 index = fileName.lastIndexOf ("-");
76
77 if (index > 0)
78 {
79 style = fileName.substring (index + 1, fileName.length ());
80 familyName = fileName.substring (0, index);
81
82 /* Look for something describing the weight. */
83 if (style.contains ("Thin"))
84 weight = THIN;
85 else if (style.contains ("UltraLight"))
86 weight = ULTRA_LIGHT;
87 else if (style.contains ("SemiLight"))
88 weight = SEMI_LIGHT;
89 else if (style.contains ("Light"))
90 weight = LIGHT;
91 else if (style.contains ("Medium"))
92 weight = MEDIUM;
93 else if (style.contains ("SemiBold"))
94 weight = SEMI_BOLD;
95 else if (style.contains ("ExtraBold"))
96 weight = EXTRA_BOLD;
97 else if (style.contains ("Bold"))
98 weight = BOLD;
99 else if (style.contains ("Black"))
100 weight = BLACK;
101 else if (style.contains ("UltraHeavy"))
102 weight = ULTRA_HEAVY;
103
104 /* And the slant. */
105 if (style.contains ("ReverseOblique"))
106 slant = OBLIQUE;
107 else if (style.contains ("ReverseItalic"))
108 slant = REVERSE_ITALIC;
109 else if (style.contains ("Italic"))
110 slant = ITALIC;
111 else if (style.contains ("Oblique"))
112 slant = OBLIQUE;
113
114 /* Finally, the width. */
115 if (style.contains ("UltraCondensed"))
116 width = ULTRA_CONDENSED;
117 else if (style.contains ("ExtraCondensed"))
118 width = EXTRA_CONDENSED;
119 else if (style.contains ("SemiCondensed"))
120 width = SEMI_CONDENSED;
121 else if (style.contains ("Condensed"))
122 width = CONDENSED;
123 else if (style.contains ("SemiExpanded"))
124 width = SEMI_EXPANDED;
125 else if (style.contains ("ExtraExpanded"))
126 width = EXTRA_EXPANDED;
127 else if (style.contains ("UltraExpanded"))
128 width = ULTRA_EXPANDED;
129 else if (style.contains ("Expanded"))
130 width = EXPANDED;
131
132 /* Guess the spacing information. */
133 testString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
134 widths = new float[testString.length ()];
135
136 measured = typefacePaint.getTextWidths (testString,
137 0, testString.length (),
138 widths);
139 spacing = MONO;
140 for (i = 0; i < measured; ++i)
141 {
142 if (i != 0 && widths[i - 1] != widths[i])
143 /* This isn't a monospace font. */
144 spacing = PROPORTIONAL;
145 }
146 }
147 else
148 familyName = fileName;
149 }
150
151 @Override
152 public String
153 toString ()
154 {
155 return ("Sdk7Typeface ("
156 + String.valueOf (familyName) + ", "
157 + String.valueOf (slant) + ", "
158 + String.valueOf (width) + ", "
159 + String.valueOf (weight) + ", "
160 + String.valueOf (spacing) + ")");
161 }
162 };
163
164 protected static final class Sdk7FontEntity extends FontEntity
165 {
166 /* The typeface. */
167 public Sdk7Typeface typeface;
168
169 public
170 Sdk7FontEntity (Sdk7Typeface typeface)
171 {
172 foundry = "Google";
173 family = typeface.familyName;
174 adstyle = null;
175 weight = typeface.weight;
176 slant = typeface.slant;
177 spacing = typeface.spacing;
178 width = typeface.width;
179 dpi = Math.round (EmacsService.SERVICE.metrics.scaledDensity * 160f);
180
181 this.typeface = typeface;
182 }
183 };
184
185 protected final class Sdk7FontObject extends FontObject
186 {
187 /* The typeface. */
188 public Sdk7Typeface typeface;
189
190 public
191 Sdk7FontObject (Sdk7Typeface typeface, int pixelSize)
192 {
193 float totalWidth;
194 String testWidth, testString;
195
196 this.typeface = typeface;
197 this.pixelSize = pixelSize;
198
199 family = typeface.familyName;
200 adstyle = null;
201 weight = typeface.weight;
202 slant = typeface.slant;
203 spacing = typeface.spacing;
204 width = typeface.width;
205 dpi = Math.round (EmacsService.SERVICE.metrics.scaledDensity * 160f);
206
207 /* Compute the ascent and descent. */
208 typeface.typefacePaint.setTextSize (pixelSize);
209 ascent
210 = Math.round (-typeface.typefacePaint.ascent ());
211 descent
212 = Math.round (typeface.typefacePaint.descent ());
213
214 /* Compute the average width. */
215 testString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
216 totalWidth = typeface.typefacePaint.measureText (testString);
217
218 if (totalWidth > 0)
219 avgwidth = Math.round (totalWidth
220 / testString.length ());
221
222 /* Android doesn't expose the font average width and height
223 information, so this will have to do. */
224 minWidth = maxWidth = avgwidth;
225
226 /* This is different from avgwidth in the font spec! */
227 averageWidth = avgwidth;
228
229 /* Set the space width. */
230 totalWidth = typeface.typefacePaint.measureText (" ");
231 spaceWidth = Math.round (totalWidth);
232
233 /* Set the height and default ascent. */
234 height = ascent + descent;
235 defaultAscent = ascent;
236 }
237 };
238
239 private String[] fontFamilyList;
240 private Sdk7Typeface[] typefaceList;
241 private Sdk7Typeface fallbackTypeface;
242
243 public
244 EmacsSdk7FontDriver ()
245 {
246 int i;
247 File systemFontsDirectory, fontFile;
248 Typeface typeface;
249
250 systemFontsDirectory = new File ("/system/fonts");
251
252 fontFamilyList = systemFontsDirectory.list ();
253
254 /* If that returned null, replace it with an empty array. */
255 fontFamilyList = new String[0];
256
257 typefaceList = new Sdk7Typeface[fontFamilyList.length + 3];
258
259 /* It would be nice to avoid opening each and every font upon
260 startup. But that doesn't seem to be possible on
261 Android. */
262
263 for (i = 0; i < fontFamilyList.length; ++i)
264 {
265 fontFile = new File (systemFontsDirectory,
266 fontFamilyList[i]);
267 typeface = Typeface.createFromFile (fontFile);
268 typefaceList[i] = new Sdk7Typeface (fontFile.getName (),
269 typeface);
270 }
271
272 /* Initialize the default monospace and serif typefaces. */
273 fallbackTypeface = new Sdk7Typeface ("monospace",
274 Typeface.MONOSPACE);
275 typefaceList[fontFamilyList.length] = fallbackTypeface;
276
277 fallbackTypeface = new Sdk7Typeface ("Monospace",
278 Typeface.MONOSPACE);
279 typefaceList[fontFamilyList.length + 1] = fallbackTypeface;
280
281 fallbackTypeface = new Sdk7Typeface ("Sans Serif",
282 Typeface.DEFAULT);
283 typefaceList[fontFamilyList.length + 2] = fallbackTypeface;
284 }
285
286 private boolean
287 checkMatch (Sdk7Typeface typeface, FontSpec fontSpec)
288 {
289 if (fontSpec.family != null
290 && !fontSpec.family.equals (typeface.familyName))
291 return false;
292
293 if (fontSpec.slant != null
294 && !fontSpec.weight.equals (typeface.weight))
295 return false;
296
297 if (fontSpec.spacing != null
298 && !fontSpec.spacing.equals (typeface.spacing))
299 return false;
300
301 if (fontSpec.weight != null
302 && !fontSpec.weight.equals (typeface.weight))
303 return false;
304
305 if (fontSpec.width != null
306 && !fontSpec.width.equals (typeface.width))
307 return false;
308
309 return true;
310 }
311
312 @Override
313 public FontEntity[]
314 list (FontSpec fontSpec)
315 {
316 LinkedList<FontEntity> list;
317 int i;
318
319 list = new LinkedList<FontEntity> ();
320
321 for (i = 0; i < typefaceList.length; ++i)
322 {
323 if (checkMatch (typefaceList[i], fontSpec))
324 list.add (new Sdk7FontEntity (typefaceList[i]));
325 }
326
327 return list.toArray (new FontEntity[0]);
328 }
329
330 @Override
331 public FontEntity
332 match (FontSpec fontSpec)
333 {
334 FontEntity[] entities;
335 int i;
336
337 entities = this.list (fontSpec);
338
339 if (entities.length == 0)
340 return new Sdk7FontEntity (fallbackTypeface);
341
342 return entities[0];
343 }
344
345 @Override
346 public String[]
347 listFamilies ()
348 {
349 return fontFamilyList;
350 }
351
352 @Override
353 public FontObject
354 openFont (FontEntity fontEntity, int pixelSize)
355 {
356 return new Sdk7FontObject (((Sdk7FontEntity) fontEntity).typeface,
357 pixelSize);
358 }
359
360 @Override
361 public int
362 hasChar (FontSpec font, char charCode)
363 {
364 float missingGlyphWidth, width;
365 Rect rect1, rect2;
366 Paint paint;
367 Sdk7FontObject fontObject;
368
369 if (font instanceof Sdk7FontObject)
370 {
371 fontObject = (Sdk7FontObject) font;
372 paint = fontObject.typeface.typefacePaint;
373 }
374 else
375 paint = ((Sdk7FontEntity) font).typeface.typefacePaint;
376
377 paint.setTextSize (10);
378
379 if (Character.isWhitespace (charCode))
380 return 1;
381
382 missingGlyphWidth = paint.measureText (TOFU_STRING);
383 width = paint.measureText ("" + charCode);
384
385 if (width == 0f)
386 return 0;
387
388 if (width != missingGlyphWidth)
389 return 1;
390
391 rect1 = new Rect ();
392 rect2 = new Rect ();
393
394 paint.getTextBounds (TOFU_STRING, 0, TOFU_STRING.length (),
395 rect1);
396 paint.getTextBounds ("" + charCode, 0, 1, rect2);
397 return rect1.equals (rect2) ? 0 : 1;
398 }
399
400 private void
401 textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics,
402 Paint paint, Rect bounds)
403 {
404 char[] text;
405
406 text = new char[1];
407 text[0] = (char) code;
408
409 paint.getTextBounds (text, 0, 1, bounds);
410
411 /* bounds is the bounding box of the glyph corresponding to CODE.
412 Translate these into XCharStruct values.
413
414 The origin is at 0, 0, and lbearing is the distance counting
415 rightwards from the origin to the left most pixel in the glyph
416 raster. rbearing is the distance between the origin and the
417 rightmost pixel in the glyph raster. ascent is the distance
418 counting upwards between the the topmost pixel in the glyph
419 raster. descent is the distance (once again counting
420 downwards) between the origin and the bottommost pixel in the
421 glyph raster.
422
423 width is the distance between the origin and the origin of any
424 character to the right. */
425
426 metrics.lbearing = (short) bounds.left;
427 metrics.rbearing = (short) bounds.right;
428 metrics.ascent = (short) -bounds.top;
429 metrics.descent = (short) bounds.bottom;
430 metrics.width = (short) paint.measureText ("" + text[0]);
431 }
432
433 @Override
434 public void
435 textExtents (FontObject font, int code[], FontMetrics fontMetrics)
436 {
437 int i;
438 Paint paintCache;
439 Rect boundsCache;
440 Sdk7FontObject fontObject;
441 char[] text;
442 float width;
443
444 fontObject = (Sdk7FontObject) font;
445 paintCache = fontObject.typeface.typefacePaint;
446 paintCache.setTextSize (fontObject.pixelSize);
447 boundsCache = new Rect ();
448
449 if (code.length == 0)
450 {
451 fontMetrics.lbearing = 0;
452 fontMetrics.rbearing = 0;
453 fontMetrics.ascent = 0;
454 fontMetrics.descent = 0;
455 fontMetrics.width = 0;
456 }
457 else if (code.length == 1)
458 textExtents1 ((Sdk7FontObject) font, code[0], fontMetrics,
459 paintCache, boundsCache);
460 else
461 {
462 text = new char[code.length];
463
464 for (i = 0; i < code.length; ++i)
465 text[i] = (char) code[i];
466
467 paintCache.getTextBounds (text, 0, code.length,
468 boundsCache);
469 width = paintCache.measureText (text, 0, code.length);
470
471 fontMetrics.lbearing = (short) boundsCache.left;
472 fontMetrics.rbearing = (short) boundsCache.right;
473 fontMetrics.ascent = (short) -boundsCache.top;
474 fontMetrics.descent = (short) boundsCache.bottom;
475 fontMetrics.width = (short) Math.round (width);
476 }
477 }
478
479 @Override
480 public int
481 encodeChar (FontObject fontObject, char charCode)
482 {
483 return charCode;
484 }
485
486 @Override
487 public int
488 draw (FontObject fontObject, EmacsGC gc, EmacsDrawable drawable,
489 int[] chars, int x, int y, int backgroundWidth,
490 boolean withBackground)
491 {
492 Rect backgroundRect, bounds;
493 Sdk7FontObject sdk7FontObject;
494 char[] charsArray;
495 int i;
496 Canvas canvas;
497 Paint paint;
498
499 sdk7FontObject = (Sdk7FontObject) fontObject;
500 charsArray = new char[chars.length];
501
502 for (i = 0; i < chars.length; ++i)
503 charsArray[i] = (char) chars[i];
504
505 backgroundRect = new Rect ();
506 backgroundRect.top = y - sdk7FontObject.ascent;
507 backgroundRect.left = x;
508 backgroundRect.right = x + backgroundWidth;
509 backgroundRect.bottom = y + sdk7FontObject.descent;
510
511 canvas = drawable.lockCanvas (gc);
512
513 if (canvas == null)
514 return 0;
515
516 paint = gc.gcPaint;
517 paint.setStyle (Paint.Style.FILL);
518
519 if (withBackground)
520 {
521 paint.setColor (gc.background | 0xff000000);
522 canvas.drawRect (backgroundRect, paint);
523 paint.setColor (gc.foreground | 0xff000000);
524 }
525
526 paint.setTextSize (sdk7FontObject.pixelSize);
527 paint.setTypeface (sdk7FontObject.typeface.typeface);
528 paint.setAntiAlias (true);
529 canvas.drawText (charsArray, 0, chars.length, x, y, paint);
530
531 bounds = new Rect ();
532 paint.getTextBounds (charsArray, 0, chars.length, bounds);
533 bounds.offset (x, y);
534 bounds.union (backgroundRect);
535 drawable.damageRect (bounds);
536 paint.setAntiAlias (false);
537 return 1;
538 }
539};
diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java
new file mode 100644
index 00000000000..9622641810f
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java
@@ -0,0 +1,147 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22/* Importing the entire package instead of just the legacy
23 ClipboardManager class avoids the deprecation warning. */
24
25import android.text.*;
26
27import android.content.Context;
28import android.util.Log;
29
30import java.io.UnsupportedEncodingException;
31
32/* This class implements EmacsClipboard for Android 2.2 and other
33 similarly old systems. */
34
35@SuppressWarnings ("deprecation")
36public final class EmacsSdk8Clipboard extends EmacsClipboard
37{
38 private static final String TAG = "EmacsSdk8Clipboard";
39 private ClipboardManager manager;
40
41 public
42 EmacsSdk8Clipboard ()
43 {
44 String what;
45 Context context;
46
47 what = Context.CLIPBOARD_SERVICE;
48 context = EmacsService.SERVICE;
49 manager
50 = (ClipboardManager) context.getSystemService (what);
51 }
52
53 /* Set the clipboard text to CLIPBOARD, a string in UTF-8
54 encoding. */
55
56 @Override
57 public void
58 setClipboard (byte[] bytes)
59 {
60 try
61 {
62 manager.setText (new String (bytes, "UTF-8"));
63 }
64 catch (UnsupportedEncodingException exception)
65 {
66 Log.w (TAG, "setClipboard: " + exception);
67 }
68 }
69
70 /* Return whether or not Emacs owns the clipboard. Value is 1 if
71 Emacs does, 0 if Emacs does not, and -1 if that information is
72 unavailable. */
73
74 @Override
75 public int
76 ownsClipboard ()
77 {
78 return -1;
79 }
80
81 /* Return whether or not clipboard content currently exists. */
82
83 @Override
84 public boolean
85 clipboardExists ()
86 {
87 return manager.hasText ();
88 }
89
90 /* Return the current content of the clipboard, as plain text, or
91 NULL if no content is available. */
92
93 @Override
94 public byte[]
95 getClipboard ()
96 {
97 String string;
98 CharSequence text;
99
100 text = manager.getText ();
101
102 if (text == null)
103 return null;
104
105 string = text.toString ();
106
107 try
108 {
109 return string.getBytes ("UTF-8");
110 }
111 catch (UnsupportedEncodingException exception)
112 {
113 Log.w (TAG, "getClipboard: " + exception);
114 }
115
116 return null;
117 }
118
119 /* Return an array of targets currently provided by the
120 clipboard, or NULL if there are none. */
121
122 @Override
123 public byte[][]
124 getClipboardTargets ()
125 {
126 return null;
127 }
128
129 /* Return the clipboard data for the given target, or NULL if it
130 does not exist.
131
132 Value is normally an array of three longs: the file descriptor,
133 the start offset of the data, and its length; length may be
134 AssetFileDescriptor.UNKOWN_LENGTH, meaning that the data extends
135 from that offset to the end of the file.
136
137 Do not use this function to open text targets; use `getClipboard'
138 for that instead, as it will handle selection data consisting
139 solely of a URI. */
140
141 @Override
142 public long[]
143 getClipboardData (byte[] target)
144 {
145 return null;
146 }
147};
diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java
new file mode 100644
index 00000000000..d91d8f66009
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsService.java
@@ -0,0 +1,1820 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.io.FileNotFoundException;
23import java.io.IOException;
24import java.io.UnsupportedEncodingException;
25
26import java.util.ArrayList;
27import java.util.HashSet;
28import java.util.List;
29
30import java.util.concurrent.atomic.AtomicInteger;
31
32import android.database.Cursor;
33
34import android.graphics.Matrix;
35import android.graphics.Point;
36
37import android.webkit.MimeTypeMap;
38
39import android.view.InputDevice;
40import android.view.KeyEvent;
41import android.view.inputmethod.CursorAnchorInfo;
42import android.view.inputmethod.ExtractedText;
43
44import android.app.Notification;
45import android.app.NotificationManager;
46import android.app.NotificationChannel;
47import android.app.Service;
48
49import android.content.ClipboardManager;
50import android.content.Context;
51import android.content.ContentResolver;
52import android.content.Intent;
53import android.content.IntentFilter;
54import android.content.UriPermission;
55
56import android.content.pm.ApplicationInfo;
57import android.content.pm.PackageManager.ApplicationInfoFlags;
58import android.content.pm.PackageManager;
59
60import android.content.res.AssetManager;
61
62import android.hardware.input.InputManager;
63
64import android.net.Uri;
65
66import android.os.BatteryManager;
67import android.os.Build;
68import android.os.Looper;
69import android.os.IBinder;
70import android.os.Handler;
71import android.os.ParcelFileDescriptor;
72import android.os.Vibrator;
73import android.os.VibratorManager;
74import android.os.VibrationEffect;
75
76import android.provider.DocumentsContract;
77import android.provider.DocumentsContract.Document;
78
79import android.util.Log;
80import android.util.DisplayMetrics;
81
82import android.widget.Toast;
83
84/* EmacsService is the service that starts the thread running Emacs
85 and handles requests by that Emacs instance. */
86
87public final class EmacsService extends Service
88{
89 public static final String TAG = "EmacsService";
90
91 /* The started Emacs service object. */
92 public static EmacsService SERVICE;
93
94 /* If non-NULL, an extra argument to pass to
95 `android_emacs_init'. */
96 public static String extraStartupArgument;
97
98 /* The thread running Emacs C code. */
99 private EmacsThread thread;
100
101 /* Handler used to run tasks on the main thread. */
102 private Handler handler;
103
104 /* Content resolver used to access URIs. */
105 private ContentResolver resolver;
106
107 /* Keep this in synch with androidgui.h. */
108 public static final int IC_MODE_NULL = 0;
109 public static final int IC_MODE_ACTION = 1;
110 public static final int IC_MODE_TEXT = 2;
111
112 /* Display metrics used by font backends. */
113 public DisplayMetrics metrics;
114
115 /* Flag that says whether or not to print verbose debugging
116 information when responding to an input method. */
117 public static final boolean DEBUG_IC = false;
118
119 /* Flag that says whether or not to stringently check that only the
120 Emacs thread is performing drawing calls. */
121 private static final boolean DEBUG_THREADS = false;
122
123 /* Atomic integer used for synchronization between
124 icBeginSynchronous/icEndSynchronous and viewGetSelection.
125
126 Value is 0 if no query is in progress, 1 if viewGetSelection is
127 being called, and 2 if icBeginSynchronous was called. */
128 public static final AtomicInteger servicingQuery;
129
130 /* Thread used to query document providers, or null if it hasn't
131 been created yet. */
132 private EmacsSafThread storageThread;
133
134 static
135 {
136 servicingQuery = new AtomicInteger ();
137 };
138
139 /* Return the directory leading to the directory in which native
140 library files are stored on behalf of CONTEXT. */
141
142 public static String
143 getLibraryDirectory (Context context)
144 {
145 int apiLevel;
146
147 apiLevel = Build.VERSION.SDK_INT;
148
149 if (apiLevel >= Build.VERSION_CODES.GINGERBREAD)
150 return context.getApplicationInfo ().nativeLibraryDir;
151
152 return context.getApplicationInfo ().dataDir + "/lib";
153 }
154
155 @Override
156 public int
157 onStartCommand (Intent intent, int flags, int startId)
158 {
159 Notification notification;
160 NotificationManager manager;
161 NotificationChannel channel;
162 String infoBlurb;
163 Object tem;
164
165 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
166 {
167 tem = getSystemService (Context.NOTIFICATION_SERVICE);
168 manager = (NotificationManager) tem;
169 infoBlurb = ("This notification is displayed to keep Emacs"
170 + " running while it is in the background. You"
171 + " may disable it if you want;"
172 + " see (emacs)Android Environment.");
173 channel
174 = new NotificationChannel ("emacs", "Emacs persistent notification",
175 NotificationManager.IMPORTANCE_DEFAULT);
176 manager.createNotificationChannel (channel);
177 notification = (new Notification.Builder (this, "emacs")
178 .setContentTitle ("Emacs")
179 .setContentText (infoBlurb)
180 .setSmallIcon (android.R.drawable.sym_def_app_icon)
181 .build ());
182 manager.notify (1, notification);
183 startForeground (1, notification);
184 }
185
186 return START_NOT_STICKY;
187 }
188
189 @Override
190 public IBinder
191 onBind (Intent intent)
192 {
193 return null;
194 }
195
196 @SuppressWarnings ("deprecation")
197 private String
198 getApkFile ()
199 {
200 PackageManager manager;
201 ApplicationInfo info;
202
203 manager = getPackageManager ();
204
205 try
206 {
207 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU)
208 info = manager.getApplicationInfo ("org.gnu.emacs", 0);
209 else
210 info = manager.getApplicationInfo ("org.gnu.emacs",
211 ApplicationInfoFlags.of (0));
212
213 /* Return an empty string upon failure. */
214
215 if (info.sourceDir != null)
216 return info.sourceDir;
217
218 return "";
219 }
220 catch (Exception e)
221 {
222 return "";
223 }
224 }
225
226 @Override
227 public void
228 onCreate ()
229 {
230 final AssetManager manager;
231 Context app_context;
232 final String filesDir, libDir, cacheDir, classPath;
233 final double pixelDensityX;
234 final double pixelDensityY;
235 final double scaledDensity;
236 double tempScaledDensity;
237
238 SERVICE = this;
239 handler = new Handler (Looper.getMainLooper ());
240 manager = getAssets ();
241 app_context = getApplicationContext ();
242 metrics = getResources ().getDisplayMetrics ();
243 pixelDensityX = metrics.xdpi;
244 pixelDensityY = metrics.ydpi;
245 tempScaledDensity = ((metrics.scaledDensity
246 / metrics.density)
247 * pixelDensityX);
248 resolver = getContentResolver ();
249
250 /* If the density used to compute the text size is lesser than
251 160, there's likely a bug with display density computation.
252 Reset it to 160 in that case.
253
254 Note that Android uses 160 ``dpi'' as the density where 1 point
255 corresponds to 1 pixel, not 72 or 96 as used elsewhere. This
256 difference is codified in PT_PER_INCH defined in font.h. */
257
258 if (tempScaledDensity < 160)
259 tempScaledDensity = 160;
260
261 /* scaledDensity is const as required to refer to it from within
262 the nested function below. */
263 scaledDensity = tempScaledDensity;
264
265 try
266 {
267 /* Configure Emacs with the asset manager and other necessary
268 parameters. */
269 filesDir = app_context.getFilesDir ().getCanonicalPath ();
270 libDir = getLibraryDirectory (this);
271 cacheDir = app_context.getCacheDir ().getCanonicalPath ();
272
273 /* Now provide this application's apk file, so a recursive
274 invocation of app_process (through android-emacs) can
275 find EmacsNoninteractive. */
276 classPath = getApkFile ();
277
278 Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir
279 + ", libDir = " + libDir + ", and classPath = " + classPath
280 + "; fileToOpen = " + EmacsOpenActivity.fileToOpen
281 + "; display density: " + pixelDensityX + " by "
282 + pixelDensityY + " scaled to " + scaledDensity);
283
284 /* Start the thread that runs Emacs. */
285 thread = new EmacsThread (this, new Runnable () {
286 @Override
287 public void
288 run ()
289 {
290 EmacsNative.setEmacsParams (manager, filesDir, libDir,
291 cacheDir, (float) pixelDensityX,
292 (float) pixelDensityY,
293 (float) scaledDensity,
294 classPath, EmacsService.this,
295 Build.VERSION.SDK_INT);
296 }
297 }, extraStartupArgument,
298 /* If any file needs to be opened, open it now. */
299 EmacsOpenActivity.fileToOpen);
300 thread.start ();
301 }
302 catch (IOException exception)
303 {
304 EmacsNative.emacsAbort ();
305 return;
306 }
307 }
308
309
310
311 /* Functions from here on must only be called from the Emacs
312 thread. */
313
314 public void
315 runOnUiThread (Runnable runnable)
316 {
317 handler.post (runnable);
318 }
319
320 public EmacsView
321 getEmacsView (final EmacsWindow window, final int visibility,
322 final boolean isFocusedByDefault)
323 {
324 Runnable runnable;
325 final EmacsHolder<EmacsView> view;
326
327 view = new EmacsHolder<EmacsView> ();
328
329 runnable = new Runnable () {
330 @Override
331 public void
332 run ()
333 {
334 synchronized (this)
335 {
336 view.thing = new EmacsView (window);
337 view.thing.setVisibility (visibility);
338
339 /* The following function is only present on Android 26
340 or later. */
341 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
342 view.thing.setFocusedByDefault (isFocusedByDefault);
343
344 notify ();
345 }
346 }
347 };
348
349 syncRunnable (runnable);
350 return view.thing;
351 }
352
353 public void
354 getLocationOnScreen (final EmacsView view, final int[] coordinates)
355 {
356 Runnable runnable;
357
358 runnable = new Runnable () {
359 public void
360 run ()
361 {
362 synchronized (this)
363 {
364 view.getLocationOnScreen (coordinates);
365 notify ();
366 }
367 }
368 };
369
370 syncRunnable (runnable);
371 }
372
373
374
375 public static void
376 checkEmacsThread ()
377 {
378 if (DEBUG_THREADS)
379 {
380 if (Thread.currentThread () instanceof EmacsThread)
381 return;
382
383 throw new RuntimeException ("Emacs thread function"
384 + " called from other thread!");
385 }
386 }
387
388 /* These drawing functions must only be called from the Emacs
389 thread. */
390
391 public void
392 fillRectangle (EmacsDrawable drawable, EmacsGC gc,
393 int x, int y, int width, int height)
394 {
395 checkEmacsThread ();
396 EmacsFillRectangle.perform (drawable, gc, x, y,
397 width, height);
398 }
399
400 public void
401 fillPolygon (EmacsDrawable drawable, EmacsGC gc,
402 Point points[])
403 {
404 checkEmacsThread ();
405 EmacsFillPolygon.perform (drawable, gc, points);
406 }
407
408 public void
409 drawRectangle (EmacsDrawable drawable, EmacsGC gc,
410 int x, int y, int width, int height)
411 {
412 checkEmacsThread ();
413 EmacsDrawRectangle.perform (drawable, gc, x, y,
414 width, height);
415 }
416
417 public void
418 drawLine (EmacsDrawable drawable, EmacsGC gc,
419 int x, int y, int x2, int y2)
420 {
421 checkEmacsThread ();
422 EmacsDrawLine.perform (drawable, gc, x, y,
423 x2, y2);
424 }
425
426 public void
427 drawPoint (EmacsDrawable drawable, EmacsGC gc,
428 int x, int y)
429 {
430 checkEmacsThread ();
431 EmacsDrawPoint.perform (drawable, gc, x, y);
432 }
433
434 public void
435 clearWindow (EmacsWindow window)
436 {
437 checkEmacsThread ();
438 window.clearWindow ();
439 }
440
441 public void
442 clearArea (EmacsWindow window, int x, int y, int width,
443 int height)
444 {
445 checkEmacsThread ();
446 window.clearArea (x, y, width, height);
447 }
448
449 @SuppressWarnings ("deprecation")
450 public void
451 ringBell ()
452 {
453 Vibrator vibrator;
454 VibrationEffect effect;
455 VibratorManager vibratorManager;
456 Object tem;
457
458 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
459 {
460 tem = getSystemService (Context.VIBRATOR_MANAGER_SERVICE);
461 vibratorManager = (VibratorManager) tem;
462 vibrator = vibratorManager.getDefaultVibrator ();
463 }
464 else
465 vibrator
466 = (Vibrator) getSystemService (Context.VIBRATOR_SERVICE);
467
468 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
469 {
470 effect
471 = VibrationEffect.createOneShot (50,
472 VibrationEffect.DEFAULT_AMPLITUDE);
473 vibrator.vibrate (effect);
474 }
475 else
476 vibrator.vibrate (50);
477 }
478
479 public short[]
480 queryTree (EmacsWindow window)
481 {
482 short[] array;
483 List<EmacsWindow> windowList;
484 int i;
485
486 if (window == null)
487 /* Just return all the windows without a parent. */
488 windowList = EmacsWindowAttachmentManager.MANAGER.copyWindows ();
489 else
490 windowList = window.children;
491
492 array = new short[windowList.size () + 1];
493 i = 1;
494
495 array[0] = (window == null
496 ? 0 : (window.parent != null
497 ? window.parent.handle : 0));
498
499 for (EmacsWindow treeWindow : windowList)
500 array[i++] = treeWindow.handle;
501
502 return array;
503 }
504
505 public int
506 getScreenWidth (boolean mmWise)
507 {
508 DisplayMetrics metrics;
509
510 metrics = getResources ().getDisplayMetrics ();
511
512 if (!mmWise)
513 return metrics.widthPixels;
514 else
515 return (int) ((metrics.widthPixels / metrics.xdpi) * 2540.0);
516 }
517
518 public int
519 getScreenHeight (boolean mmWise)
520 {
521 DisplayMetrics metrics;
522
523 metrics = getResources ().getDisplayMetrics ();
524
525 if (!mmWise)
526 return metrics.heightPixels;
527 else
528 return (int) ((metrics.heightPixels / metrics.ydpi) * 2540.0);
529 }
530
531 public boolean
532 detectMouse ()
533 {
534 InputManager manager;
535 InputDevice device;
536 int[] ids;
537 int i;
538
539 if (Build.VERSION.SDK_INT
540 /* Android 4.0 and earlier don't support mouse input events at
541 all. */
542 < Build.VERSION_CODES.JELLY_BEAN)
543 return false;
544
545 manager = (InputManager) getSystemService (Context.INPUT_SERVICE);
546 ids = manager.getInputDeviceIds ();
547
548 for (i = 0; i < ids.length; ++i)
549 {
550 device = manager.getInputDevice (ids[i]);
551
552 if (device == null)
553 continue;
554
555 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
556 {
557 if (device.supportsSource (InputDevice.SOURCE_MOUSE))
558 return true;
559 }
560 else
561 {
562 /* `supportsSource' is only present on API level 21 and
563 later, but earlier versions provide a bit mask
564 containing each supported source. */
565
566 if ((device.getSources () & InputDevice.SOURCE_MOUSE) != 0)
567 return true;
568 }
569 }
570
571 return false;
572 }
573
574 public String
575 nameKeysym (int keysym)
576 {
577 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1)
578 return KeyEvent.keyCodeToString (keysym);
579
580 return String.valueOf (keysym);
581 }
582
583
584
585 /* Start the Emacs service if necessary. On Android 26 and up,
586 start Emacs as a foreground service with a notification, to avoid
587 it being killed by the system.
588
589 On older systems, simply start it as a normal background
590 service. */
591
592 public static void
593 startEmacsService (Context context)
594 {
595 if (EmacsService.SERVICE == null)
596 {
597 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
598 /* Start the Emacs service now. */
599 context.startService (new Intent (context,
600 EmacsService.class));
601 else
602 /* Display the permanant notification and start Emacs as a
603 foreground service. */
604 context.startForegroundService (new Intent (context,
605 EmacsService.class));
606 }
607 }
608
609 /* Ask the system to open the specified URL in an application that
610 understands how to open it.
611
612 If SEND, tell the system to also open applications that can
613 ``send'' the URL (through mail, for example), instead of only
614 those that can view the URL.
615
616 Value is NULL upon success, or a string describing the error
617 upon failure. */
618
619 public String
620 browseUrl (String url, boolean send)
621 {
622 Intent intent;
623 Uri uri;
624
625 try
626 {
627 /* Parse the URI. */
628 if (!send)
629 {
630 uri = Uri.parse (url);
631
632 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
633 {
634 /* On Android 4.4 and later, check if URI is actually
635 a file name. If so, rewrite it into a content
636 provider URI, so that it can be accessed by other
637 programs. */
638
639 if (uri.getScheme ().equals ("file")
640 && uri.getPath () != null)
641 uri
642 = DocumentsContract.buildDocumentUri ("org.gnu.emacs",
643 uri.getPath ());
644 }
645
646 Log.d (TAG, ("browseUri: browsing " + url
647 + " --> " + uri.getPath ()
648 + " --> " + uri));
649
650 intent = new Intent (Intent.ACTION_VIEW, uri);
651 intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK
652 | Intent.FLAG_GRANT_READ_URI_PERMISSION);
653 }
654 else
655 {
656 intent = new Intent (Intent.ACTION_SEND);
657 intent.setType ("text/plain");
658 intent.putExtra (Intent.EXTRA_SUBJECT, "Sharing link");
659 intent.putExtra (Intent.EXTRA_TEXT, url);
660
661 /* Display a list of programs able to send this URL. */
662 intent = Intent.createChooser (intent, "Send");
663
664 /* Apparently flags need to be set after a choser is
665 created. */
666 intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK);
667 }
668
669 startActivity (intent);
670 }
671 catch (Exception e)
672 {
673 return e.toString ();
674 }
675
676 return null;
677 }
678
679 /* Get a SDK 11 ClipboardManager.
680
681 Android 4.0.x requires that this be called from the main
682 thread. */
683
684 public ClipboardManager
685 getClipboardManager ()
686 {
687 final EmacsHolder<ClipboardManager> manager;
688 Runnable runnable;
689
690 manager = new EmacsHolder<ClipboardManager> ();
691
692 runnable = new Runnable () {
693 public void
694 run ()
695 {
696 Object tem;
697
698 synchronized (this)
699 {
700 tem = getSystemService (Context.CLIPBOARD_SERVICE);
701 manager.thing = (ClipboardManager) tem;
702 notify ();
703 }
704 }
705 };
706
707 syncRunnable (runnable);
708 return manager.thing;
709 }
710
711 public void
712 restartEmacs ()
713 {
714 Intent intent;
715
716 intent = new Intent (this, EmacsActivity.class);
717 intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK
718 | Intent.FLAG_ACTIVITY_CLEAR_TASK);
719 startActivity (intent);
720 System.exit (0);
721 }
722
723 /* Wait synchronously for the specified RUNNABLE to complete in the
724 UI thread. Must be called from the Emacs thread. */
725
726 public static void
727 syncRunnable (Runnable runnable)
728 {
729 EmacsNative.beginSynchronous ();
730
731 synchronized (runnable)
732 {
733 SERVICE.runOnUiThread (runnable);
734
735 while (true)
736 {
737 try
738 {
739 runnable.wait ();
740 break;
741 }
742 catch (InterruptedException e)
743 {
744 continue;
745 }
746 }
747 }
748
749 EmacsNative.endSynchronous ();
750 }
751
752
753
754 /* IMM functions such as `updateSelection' holds an internal lock
755 that is also taken before `onCreateInputConnection' (in
756 EmacsView.java) is called; when that then asks the UI thread for
757 the current selection, a dead lock results. To remedy this,
758 reply to any synchronous queries now -- and prohibit more queries
759 for the duration of `updateSelection' -- if EmacsView may have
760 been asking for the value of the region. */
761
762 public static void
763 icBeginSynchronous ()
764 {
765 /* Set servicingQuery to 2, so viewGetSelection knows it shouldn't
766 proceed. */
767
768 if (servicingQuery.getAndSet (2) == 1)
769 /* But if viewGetSelection is already in progress, answer it
770 first. */
771 EmacsNative.answerQuerySpin ();
772 }
773
774 public static void
775 icEndSynchronous ()
776 {
777 if (servicingQuery.getAndSet (0) != 2)
778 throw new RuntimeException ("incorrect value of `servicingQuery': "
779 + "likely 1");
780 }
781
782 public static int[]
783 viewGetSelection (short window)
784 {
785 int[] selection;
786
787 /* See if a query is already in progress from the other
788 direction. */
789 if (!servicingQuery.compareAndSet (0, 1))
790 return null;
791
792 /* Now call the regular getSelection. Note that this can't race
793 with answerQuerySpin, as `android_servicing_query' can never be
794 2 when icBeginSynchronous is called, so a query will always be
795 started. */
796 selection = EmacsNative.getSelection (window);
797
798 /* Finally, clear servicingQuery if its value is still 1. If a
799 query has started from the other side, it ought to be 2. */
800
801 servicingQuery.compareAndSet (1, 0);
802 return selection;
803 }
804
805
806
807 public void
808 updateIC (EmacsWindow window, int newSelectionStart,
809 int newSelectionEnd, int composingRegionStart,
810 int composingRegionEnd)
811 {
812 if (DEBUG_IC)
813 Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart
814 + " " + newSelectionEnd + " "
815 + composingRegionStart + " "
816 + composingRegionEnd));
817
818 icBeginSynchronous ();
819 window.view.imManager.updateSelection (window.view,
820 newSelectionStart,
821 newSelectionEnd,
822 composingRegionStart,
823 composingRegionEnd);
824 icEndSynchronous ();
825 }
826
827 public void
828 resetIC (EmacsWindow window, int icMode)
829 {
830 int oldMode;
831
832 if (DEBUG_IC)
833 Log.d (TAG, "resetIC: " + window + ", " + icMode);
834
835 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU
836 && (oldMode = window.view.getICMode ()) == icMode
837 /* Don't do this if there is currently no input
838 connection. */
839 && oldMode != IC_MODE_NULL)
840 {
841 if (DEBUG_IC)
842 Log.d (TAG, "resetIC: calling invalidateInput");
843
844 /* Android 33 and later allow the IM reset to be optimized out
845 and replaced by a call to `invalidateInput', which is much
846 faster, as it does not involve resetting the input
847 connection. */
848
849 icBeginSynchronous ();
850 window.view.imManager.invalidateInput (window.view);
851 icEndSynchronous ();
852
853 return;
854 }
855
856 window.view.setICMode (icMode);
857
858 icBeginSynchronous ();
859 window.view.icGeneration++;
860 window.view.imManager.restartInput (window.view);
861 icEndSynchronous ();
862 }
863
864 public void
865 updateCursorAnchorInfo (EmacsWindow window, float x,
866 float y, float yBaseline,
867 float yBottom)
868 {
869 CursorAnchorInfo info;
870 CursorAnchorInfo.Builder builder;
871 Matrix matrix;
872 int[] offsets;
873
874 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
875 return;
876
877 offsets = new int[2];
878 builder = new CursorAnchorInfo.Builder ();
879 matrix = new Matrix (window.view.getMatrix ());
880 window.view.getLocationOnScreen (offsets);
881 matrix.postTranslate (offsets[0], offsets[1]);
882 builder.setMatrix (matrix);
883 builder.setInsertionMarkerLocation (x, y, yBaseline, yBottom,
884 0);
885 info = builder.build ();
886
887
888
889 if (DEBUG_IC)
890 Log.d (TAG, ("updateCursorAnchorInfo: " + x + " " + y
891 + " " + yBaseline + "-" + yBottom));
892
893 icBeginSynchronous ();
894 window.view.imManager.updateCursorAnchorInfo (window.view, info);
895 icEndSynchronous ();
896 }
897
898
899
900 /* Content provider functions. */
901
902 /* Open a content URI described by the bytes BYTES, a non-terminated
903 string; make it writable if WRITABLE, and readable if READABLE.
904 Truncate the file if TRUNCATE.
905
906 Value is the resulting file descriptor or -1 upon failure. */
907
908 public int
909 openContentUri (byte[] bytes, boolean writable, boolean readable,
910 boolean truncate)
911 {
912 String name, mode;
913 ParcelFileDescriptor fd;
914 int i;
915
916 /* Figure out the file access mode. */
917
918 mode = "";
919
920 if (readable)
921 mode += "r";
922
923 if (writable)
924 mode += "w";
925
926 if (truncate)
927 mode += "t";
928
929 /* Try to open an associated ParcelFileDescriptor. */
930
931 try
932 {
933 /* The usual file name encoding question rears its ugly head
934 again. */
935
936 name = new String (bytes, "UTF-8");
937 fd = resolver.openFileDescriptor (Uri.parse (name), mode);
938
939 /* Use detachFd on newer versions of Android or plain old
940 dup. */
941
942 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1)
943 {
944 i = fd.detachFd ();
945 fd.close ();
946
947 return i;
948 }
949 else
950 {
951 i = EmacsNative.dup (fd.getFd ());
952 fd.close ();
953
954 return i;
955 }
956 }
957 catch (Exception exception)
958 {
959 return -1;
960 }
961 }
962
963 public boolean
964 checkContentUri (byte[] string, boolean readable, boolean writable)
965 {
966 String mode, name;
967 ParcelFileDescriptor fd;
968
969 /* Decode this into a URI. */
970
971 try
972 {
973 /* The usual file name encoding question rears its ugly head
974 again. */
975 name = new String (string, "UTF-8");
976 }
977 catch (UnsupportedEncodingException exception)
978 {
979 name = null;
980 throw new RuntimeException (exception);
981 }
982
983 mode = "r";
984
985 if (writable)
986 mode += "w";
987
988 try
989 {
990 fd = resolver.openFileDescriptor (Uri.parse (name), mode);
991 fd.close ();
992
993 return true;
994 }
995 catch (Exception exception)
996 {
997 /* Fall through. */
998 }
999
1000 return false;
1001 }
1002
1003 /* Build a content file name for URI.
1004
1005 Return a file name within the /contents/by-authority
1006 pseudo-directory that `android_get_content_name' can then
1007 transform back into an encoded URI.
1008
1009 A content name consists of any number of unencoded path segments
1010 separated by `/' characters, possibly followed by a question mark
1011 and an encoded query string. */
1012
1013 public static String
1014 buildContentName (Uri uri)
1015 {
1016 StringBuilder builder;
1017
1018 builder = new StringBuilder ("/content/by-authority/");
1019 builder.append (uri.getAuthority ());
1020
1021 /* First, append each path segment. */
1022
1023 for (String segment : uri.getPathSegments ())
1024 {
1025 /* FIXME: what if segment contains a slash character? */
1026 builder.append ('/');
1027 builder.append (uri.encode (segment));
1028 }
1029
1030 /* Now, append the query string if necessary. */
1031
1032 if (uri.getEncodedQuery () != null)
1033 builder.append ('?').append (uri.getEncodedQuery ());
1034
1035 return builder.toString ();
1036 }
1037
1038
1039
1040 private long[]
1041 queryBattery19 ()
1042 {
1043 IntentFilter filter;
1044 Intent battery;
1045 long capacity, chargeCounter, currentAvg, currentNow;
1046 long status, remaining, plugged, temp;
1047
1048 filter = new IntentFilter (Intent.ACTION_BATTERY_CHANGED);
1049 battery = registerReceiver (null, filter);
1050
1051 if (battery == null)
1052 return null;
1053
1054 capacity = battery.getIntExtra (BatteryManager.EXTRA_LEVEL, 0);
1055 chargeCounter
1056 = (battery.getIntExtra (BatteryManager.EXTRA_SCALE, 0)
1057 / battery.getIntExtra (BatteryManager.EXTRA_LEVEL, 100) * 100);
1058 currentAvg = 0;
1059 currentNow = 0;
1060 status = battery.getIntExtra (BatteryManager.EXTRA_STATUS, 0);
1061 remaining = -1;
1062 plugged = battery.getIntExtra (BatteryManager.EXTRA_PLUGGED, 0);
1063 temp = battery.getIntExtra (BatteryManager.EXTRA_TEMPERATURE, 0);
1064
1065 return new long[] { capacity, chargeCounter, currentAvg,
1066 currentNow, remaining, status, plugged,
1067 temp, };
1068 }
1069
1070 /* Return the status of the battery. See struct
1071 android_battery_status for the order of the elements
1072 returned.
1073
1074 Value may be null upon failure. */
1075
1076 public long[]
1077 queryBattery ()
1078 {
1079 Object tem;
1080 BatteryManager manager;
1081 long capacity, chargeCounter, currentAvg, currentNow;
1082 long status, remaining, plugged, temp;
1083 int prop;
1084 IntentFilter filter;
1085 Intent battery;
1086
1087 /* Android 4.4 or earlier require applications to use a different
1088 API to query the battery status. */
1089
1090 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
1091 return queryBattery19 ();
1092
1093 tem = getSystemService (Context.BATTERY_SERVICE);
1094 manager = (BatteryManager) tem;
1095 remaining = -1;
1096
1097 prop = BatteryManager.BATTERY_PROPERTY_CAPACITY;
1098 capacity = manager.getLongProperty (prop);
1099 prop = BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER;
1100 chargeCounter = manager.getLongProperty (prop);
1101 prop = BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE;
1102 currentAvg = manager.getLongProperty (prop);
1103 prop = BatteryManager.BATTERY_PROPERTY_CURRENT_NOW;
1104 currentNow = manager.getLongProperty (prop);
1105
1106 /* Return the battery status. N.B. that Android 7.1 and earlier
1107 only return ``charging'' or ``discharging''. */
1108
1109 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
1110 status
1111 = manager.getIntProperty (BatteryManager.BATTERY_PROPERTY_STATUS);
1112 else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M)
1113 status = (manager.isCharging ()
1114 ? BatteryManager.BATTERY_STATUS_CHARGING
1115 : BatteryManager.BATTERY_STATUS_DISCHARGING);
1116 else
1117 status = (currentNow > 0
1118 ? BatteryManager.BATTERY_STATUS_CHARGING
1119 : BatteryManager.BATTERY_STATUS_DISCHARGING);
1120
1121 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
1122 remaining = manager.computeChargeTimeRemaining ();
1123
1124 plugged = -1;
1125 temp = -1;
1126
1127 /* Now obtain additional information from the battery manager. */
1128
1129 filter = new IntentFilter (Intent.ACTION_BATTERY_CHANGED);
1130 battery = registerReceiver (null, filter);
1131
1132 if (battery != null)
1133 {
1134 plugged = battery.getIntExtra (BatteryManager.EXTRA_PLUGGED, 0);
1135 temp = battery.getIntExtra (BatteryManager.EXTRA_TEMPERATURE, 0);
1136
1137 /* Make status more reliable. */
1138 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M)
1139 status = battery.getIntExtra (BatteryManager.EXTRA_STATUS, 0);
1140 }
1141
1142 return new long[] { capacity, chargeCounter, currentAvg,
1143 currentNow, remaining, status, plugged,
1144 temp, };
1145 }
1146
1147 public void
1148 updateExtractedText (EmacsWindow window, ExtractedText text,
1149 int token)
1150 {
1151 if (DEBUG_IC)
1152 Log.d (TAG, "updateExtractedText: @" + token + ", " + text);
1153
1154 window.view.imManager.updateExtractedText (window.view,
1155 token, text);
1156 }
1157
1158
1159
1160 /* Document tree management functions. These functions shouldn't be
1161 called before Android 5.0. */
1162
1163 /* Return an array of each document authority providing at least one
1164 tree URI that Emacs holds the rights to persistently access. */
1165
1166 public String[]
1167 getDocumentAuthorities ()
1168 {
1169 List<UriPermission> permissions;
1170 HashSet<String> allProviders;
1171 Uri uri;
1172
1173 permissions = resolver.getPersistedUriPermissions ();
1174 allProviders = new HashSet<String> ();
1175
1176 for (UriPermission permission : permissions)
1177 {
1178 uri = permission.getUri ();
1179
1180 if (DocumentsContract.isTreeUri (uri)
1181 && permission.isReadPermission ())
1182 allProviders.add (uri.getAuthority ());
1183 }
1184
1185 return allProviders.toArray (new String[0]);
1186 }
1187
1188 /* Start a file chooser activity to request access to a directory
1189 tree.
1190
1191 Value is 1 if the activity couldn't be started for some reason,
1192 and 0 in any other case. */
1193
1194 public int
1195 requestDirectoryAccess ()
1196 {
1197 Runnable runnable;
1198 final EmacsHolder<Integer> rc;
1199
1200 /* Return 1 if Android is too old to support this feature. */
1201
1202 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP)
1203 return 1;
1204
1205 rc = new EmacsHolder<Integer> ();
1206 rc.thing = Integer.valueOf (1);
1207
1208 runnable = new Runnable () {
1209 @Override
1210 public void
1211 run ()
1212 {
1213 EmacsActivity activity;
1214 Intent intent;
1215 int id;
1216
1217 synchronized (this)
1218 {
1219 /* Try to obtain an activity that will receive the
1220 response from the file chooser dialog. */
1221
1222 if (EmacsActivity.focusedActivities.isEmpty ())
1223 {
1224 /* If focusedActivities is empty then this dialog
1225 may have been displayed immediately after another
1226 popup dialog was dismissed. Try the
1227 EmacsActivity to be focused. */
1228
1229 activity = EmacsActivity.lastFocusedActivity;
1230
1231 if (activity == null)
1232 {
1233 /* Still no luck. Return failure. */
1234 notify ();
1235 return;
1236 }
1237 }
1238 else
1239 activity = EmacsActivity.focusedActivities.get (0);
1240
1241 /* Now create the intent. */
1242 intent = new Intent (Intent.ACTION_OPEN_DOCUMENT_TREE);
1243
1244 try
1245 {
1246 id = EmacsActivity.ACCEPT_DOCUMENT_TREE;
1247 activity.startActivityForResult (intent, id, null);
1248 rc.thing = Integer.valueOf (0);
1249 }
1250 catch (Exception e)
1251 {
1252 e.printStackTrace ();
1253 }
1254
1255 notify ();
1256 }
1257 }
1258 };
1259
1260 syncRunnable (runnable);
1261 return rc.thing;
1262 }
1263
1264 /* Return an array of each tree provided by the document PROVIDER
1265 that Emacs has permission to access.
1266
1267 Value is an array if the provider really does exist, NULL
1268 otherwise. */
1269
1270 public String[]
1271 getDocumentTrees (byte provider[])
1272 {
1273 String providerName;
1274 List<String> treeList;
1275 List<UriPermission> permissions;
1276 Uri uri;
1277
1278 try
1279 {
1280 providerName = new String (provider, "US-ASCII");
1281 }
1282 catch (UnsupportedEncodingException exception)
1283 {
1284 return null;
1285 }
1286
1287 permissions = resolver.getPersistedUriPermissions ();
1288 treeList = new ArrayList<String> ();
1289
1290 for (UriPermission permission : permissions)
1291 {
1292 uri = permission.getUri ();
1293
1294 if (DocumentsContract.isTreeUri (uri)
1295 && uri.getAuthority ().equals (providerName)
1296 && permission.isReadPermission ())
1297 /* Make sure the tree document ID is encoded. Refrain from
1298 encoding characters such as +:&?#, since they don't
1299 conflict with file name separators or other special
1300 characters. */
1301 treeList.add (Uri.encode (DocumentsContract.getTreeDocumentId (uri),
1302 " +:&?#"));
1303 }
1304
1305 return treeList.toArray (new String[0]);
1306 }
1307
1308 /* Find the document ID of the file within TREE_URI designated by
1309 NAME.
1310
1311 NAME is a ``file name'' comprised of the display names of
1312 individual files. Each constituent component prior to the last
1313 must name a directory file within TREE_URI.
1314
1315 Upon success, return 0 or 1 (contingent upon whether or not the
1316 last component within NAME is a directory) and place the document
1317 ID of the named file in ID_RETURN[0].
1318
1319 If the designated file can't be located, but each component of
1320 NAME up to the last component can and is a directory, return -2
1321 and the ID of the last component located in ID_RETURN[0].
1322
1323 If the designated file can't be located, return -1, or signal one
1324 of OperationCanceledException, SecurityException,
1325 FileNotFoundException, or UnsupportedOperationException. */
1326
1327 private int
1328 documentIdFromName (String tree_uri, String name, String[] id_return)
1329 {
1330 /* Start the thread used to run SAF requests if it isn't already
1331 running. */
1332
1333 if (storageThread == null)
1334 {
1335 storageThread = new EmacsSafThread (resolver);
1336 storageThread.start ();
1337 }
1338
1339 return storageThread.documentIdFromName (tree_uri, name,
1340 id_return);
1341 }
1342
1343 /* Return an encoded document URI representing a tree with the
1344 specified IDENTIFIER supplied by the authority AUTHORITY.
1345
1346 Return null instead if Emacs does not have permanent access
1347 to the specified document tree recorded on disk. */
1348
1349 public String
1350 getTreeUri (String tree, String authority)
1351 {
1352 Uri uri, grantedUri;
1353 List<UriPermission> permissions;
1354
1355 /* First, build the URI. */
1356 tree = Uri.decode (tree);
1357 uri = DocumentsContract.buildTreeDocumentUri (authority, tree);
1358
1359 /* Now, search for it within the list of persisted URI
1360 permissions. */
1361 permissions = resolver.getPersistedUriPermissions ();
1362
1363 for (UriPermission permission : permissions)
1364 {
1365 /* If the permission doesn't entitle Emacs to read access,
1366 skip it. */
1367
1368 if (!permission.isReadPermission ())
1369 continue;
1370
1371 grantedUri = permission.getUri ();
1372
1373 if (grantedUri.equals (uri))
1374 return uri.toString ();
1375 }
1376
1377 /* Emacs doesn't have permission to access this tree URI. */
1378 return null;
1379 }
1380
1381 /* Return file status for the document designated by the given
1382 DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document
1383 ID in URI itself.
1384
1385 Value is null upon failure, or an array of longs [MODE, SIZE,
1386 MTIM] upon success, where MODE contains the file type and access
1387 modes of the file as in `struct stat', SIZE is the size of the
1388 file in BYTES or -1 if not known, and MTIM is the time of the
1389 last modification to this file in milliseconds since 00:00,
1390 January 1st, 1970.
1391
1392 OperationCanceledException and other typical exceptions may be
1393 signaled upon receiving async input or other errors. */
1394
1395 public long[]
1396 statDocument (String uri, String documentId)
1397 {
1398 /* Start the thread used to run SAF requests if it isn't already
1399 running. */
1400
1401 if (storageThread == null)
1402 {
1403 storageThread = new EmacsSafThread (resolver);
1404 storageThread.start ();
1405 }
1406
1407 return storageThread.statDocument (uri, documentId);
1408 }
1409
1410 /* Find out whether Emacs has access to the document designated by
1411 the specified DOCUMENTID within the tree URI. If DOCUMENTID is
1412 NULL, use the document ID in URI itself.
1413
1414 If WRITABLE, also check that the file is writable, which is true
1415 if it is either a directory or its flags contains
1416 FLAG_SUPPORTS_WRITE.
1417
1418 Value is 0 if the file is accessible, and one of the following if
1419 not:
1420
1421 -1, if the file does not exist.
1422 -2, if WRITABLE and the file is not writable.
1423 -3, upon any other error.
1424
1425 In addition, arbitrary runtime exceptions (such as
1426 SecurityException or UnsupportedOperationException) may be
1427 thrown. */
1428
1429 public int
1430 accessDocument (String uri, String documentId, boolean writable)
1431 {
1432 /* Start the thread used to run SAF requests if it isn't already
1433 running. */
1434
1435 if (storageThread == null)
1436 {
1437 storageThread = new EmacsSafThread (resolver);
1438 storageThread.start ();
1439 }
1440
1441 return storageThread.accessDocument (uri, documentId, writable);
1442 }
1443
1444 /* Open a cursor representing each entry within the directory
1445 designated by the specified DOCUMENTID within the tree URI.
1446
1447 If DOCUMENTID is NULL, use the document ID within URI itself.
1448 Value is NULL upon failure.
1449
1450 In addition, arbitrary runtime exceptions (such as
1451 SecurityException or UnsupportedOperationException) may be
1452 thrown. */
1453
1454 public Cursor
1455 openDocumentDirectory (String uri, String documentId)
1456 {
1457 /* Start the thread used to run SAF requests if it isn't already
1458 running. */
1459
1460 if (storageThread == null)
1461 {
1462 storageThread = new EmacsSafThread (resolver);
1463 storageThread.start ();
1464 }
1465
1466 return storageThread.openDocumentDirectory (uri, documentId);
1467 }
1468
1469 /* Read a single directory entry from the specified CURSOR. Return
1470 NULL if at the end of the directory stream, and a directory entry
1471 with `d_name' set to NULL if an error occurs. */
1472
1473 public EmacsDirectoryEntry
1474 readDirectoryEntry (Cursor cursor)
1475 {
1476 EmacsDirectoryEntry entry;
1477 int index;
1478 String name, type;
1479
1480 entry = new EmacsDirectoryEntry ();
1481
1482 while (true)
1483 {
1484 if (!cursor.moveToNext ())
1485 return null;
1486
1487 /* First, retrieve the display name. */
1488 index = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
1489
1490 if (index < 0)
1491 /* Return an invalid directory entry upon failure. */
1492 return entry;
1493
1494 try
1495 {
1496 name = cursor.getString (index);
1497 }
1498 catch (Exception exception)
1499 {
1500 return entry;
1501 }
1502
1503 /* Skip this entry if its name cannot be represented. */
1504
1505 if (name.equals ("..") || name.equals (".") || name.contains ("/"))
1506 continue;
1507
1508 /* Now, look for its type. */
1509
1510 index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1511
1512 if (index < 0)
1513 /* Return an invalid directory entry upon failure. */
1514 return entry;
1515
1516 try
1517 {
1518 type = cursor.getString (index);
1519 }
1520 catch (Exception exception)
1521 {
1522 return entry;
1523 }
1524
1525 if (type != null
1526 && type.equals (Document.MIME_TYPE_DIR))
1527 entry.d_type = 1;
1528 entry.d_name = name;
1529 return entry;
1530 }
1531
1532 /* Not reached. */
1533 }
1534
1535 /* Open a file descriptor for a file document designated by
1536 DOCUMENTID within the document tree identified by URI. If
1537 TRUNCATE and the document already exists, truncate its contents
1538 before returning.
1539
1540 On Android 9.0 and earlier, always open the document in
1541 ``read-write'' mode; this instructs the document provider to
1542 return a seekable file that is stored on disk and returns correct
1543 file status.
1544
1545 Under newer versions of Android, open the document in a
1546 non-writable mode if WRITE is false. This is possible because
1547 these versions allow Emacs to explicitly request a seekable
1548 on-disk file.
1549
1550 Value is NULL upon failure or a parcel file descriptor upon
1551 success. Call `ParcelFileDescriptor.close' on this file
1552 descriptor instead of using the `close' system call.
1553
1554 FileNotFoundException and/or SecurityException and
1555 UnsupportedOperationException may be thrown upon failure. */
1556
1557 public ParcelFileDescriptor
1558 openDocument (String uri, String documentId, boolean write,
1559 boolean truncate)
1560 {
1561 /* Start the thread used to run SAF requests if it isn't already
1562 running. */
1563
1564 if (storageThread == null)
1565 {
1566 storageThread = new EmacsSafThread (resolver);
1567 storageThread.start ();
1568 }
1569
1570 return storageThread.openDocument (uri, documentId, write,
1571 truncate);
1572 }
1573
1574 /* Create a new document with the given display NAME within the
1575 directory identified by DOCUMENTID inside the document tree
1576 designated by URI.
1577
1578 If DOCUMENTID is NULL, create the document inside the root of
1579 that tree.
1580
1581 Either FileNotFoundException, SecurityException or
1582 UnsupportedOperationException may be thrown upon failure.
1583
1584 Return the document ID of the new file upon success, NULL
1585 otherwise. */
1586
1587 public String
1588 createDocument (String uri, String documentId, String name)
1589 throws FileNotFoundException
1590 {
1591 String mimeType, separator, mime, extension;
1592 int index;
1593 MimeTypeMap singleton;
1594 Uri treeUri, directoryUri, docUri;
1595
1596 /* Try to get the MIME type for this document.
1597 Default to ``application/octet-stream''. */
1598
1599 mimeType = "application/octet-stream";
1600
1601 /* Abuse WebView stuff to get the file's MIME type. */
1602
1603 index = name.lastIndexOf ('.');
1604
1605 if (index > 0)
1606 {
1607 singleton = MimeTypeMap.getSingleton ();
1608 extension = name.substring (index + 1);
1609 mime = singleton.getMimeTypeFromExtension (extension);
1610
1611 if (mime != null)
1612 mimeType = mime;
1613 }
1614
1615 /* Now parse URI. */
1616 treeUri = Uri.parse (uri);
1617
1618 if (documentId == null)
1619 documentId = DocumentsContract.getTreeDocumentId (treeUri);
1620
1621 /* And build a file URI referring to the directory. */
1622
1623 directoryUri
1624 = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri,
1625 documentId);
1626
1627 docUri = DocumentsContract.createDocument (resolver,
1628 directoryUri,
1629 mimeType, name);
1630
1631 if (docUri == null)
1632 return null;
1633
1634 /* Invalidate the file status of the containing directory. */
1635
1636 if (storageThread != null)
1637 storageThread.postInvalidateStat (treeUri, documentId);
1638
1639 /* Return the ID of the new document. */
1640 return DocumentsContract.getDocumentId (docUri);
1641 }
1642
1643 /* Like `createDocument', but create a directory instead of an
1644 ordinary document. */
1645
1646 public String
1647 createDirectory (String uri, String documentId, String name)
1648 throws FileNotFoundException
1649 {
1650 int index;
1651 Uri treeUri, directoryUri, docUri;
1652
1653 /* Now parse URI. */
1654 treeUri = Uri.parse (uri);
1655
1656 if (documentId == null)
1657 documentId = DocumentsContract.getTreeDocumentId (treeUri);
1658
1659 /* And build a file URI referring to the directory. */
1660
1661 directoryUri
1662 = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri,
1663 documentId);
1664
1665 /* If name ends with a directory separator character, delete
1666 it. */
1667
1668 if (name.endsWith ("/"))
1669 name = name.substring (0, name.length () - 1);
1670
1671 /* From Android's perspective, directories are just ordinary
1672 documents with the `MIME_TYPE_DIR' type. */
1673
1674 docUri = DocumentsContract.createDocument (resolver,
1675 directoryUri,
1676 Document.MIME_TYPE_DIR,
1677 name);
1678
1679 if (docUri == null)
1680 return null;
1681
1682 /* Return the ID of the new document, but first invalidate the
1683 state of the containing directory. */
1684
1685 if (storageThread != null)
1686 storageThread.postInvalidateStat (treeUri, documentId);
1687
1688 return DocumentsContract.getDocumentId (docUri);
1689 }
1690
1691 /* Delete the document identified by ID from the document tree
1692 identified by URI. Return 0 upon success and -1 upon
1693 failure.
1694
1695 NAME should be the name of the document being deleted, and is
1696 used to invalidate the cache. */
1697
1698 public int
1699 deleteDocument (String uri, String id, String name)
1700 throws FileNotFoundException
1701 {
1702 Uri uriObject, tree;
1703
1704 tree = Uri.parse (uri);
1705 uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, id);
1706
1707 if (DocumentsContract.deleteDocument (resolver, uriObject))
1708 {
1709 if (storageThread != null)
1710 storageThread.postInvalidateCache (tree, id, name);
1711
1712 return 0;
1713 }
1714
1715 return -1;
1716 }
1717
1718 /* Rename the document designated by DOCID inside the directory tree
1719 identified by URI, which should be within the directory
1720 designated by DIR, to NAME. If the file can't be renamed because
1721 it doesn't support renaming, return -1, 0 otherwise. */
1722
1723 public int
1724 renameDocument (String uri, String docId, String dir, String name)
1725 throws FileNotFoundException
1726 {
1727 Uri tree, uriObject;
1728
1729 tree = Uri.parse (uri);
1730 uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, docId);
1731
1732 if (DocumentsContract.renameDocument (resolver, uriObject,
1733 name)
1734 != null)
1735 {
1736 /* Invalidate the cache. */
1737 if (storageThread != null)
1738 storageThread.postInvalidateCacheDir (tree, docId,
1739 name);
1740 return 0;
1741 }
1742
1743 /* Handle errors specially, so `android_saf_rename_document' can
1744 return ENXDEV. */
1745 return -1;
1746 }
1747
1748 /* Move the document designated by DOCID from the directory under
1749 DIR_NAME designated by SRCID to the directory designated by
1750 DSTID. If the ID of the document being moved changes as a
1751 consequence of the movement, return the new ID, else NULL.
1752
1753 URI is the document tree containing all three documents. */
1754
1755 public String
1756 moveDocument (String uri, String docId, String dirName,
1757 String dstId, String srcId)
1758 throws FileNotFoundException
1759 {
1760 Uri uri1, docId1, dstId1, srcId1;
1761 Uri name;
1762
1763 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
1764 throw new UnsupportedOperationException ("Documents aren't capable"
1765 + " of being moved on Android"
1766 + " versions before 7.0.");
1767
1768 uri1 = Uri.parse (uri);
1769 docId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, docId);
1770 dstId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, dstId);
1771 srcId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, srcId);
1772
1773 /* Move the document; this function returns the new ID of the
1774 document should it change. */
1775 name = DocumentsContract.moveDocument (resolver, docId1,
1776 srcId1, dstId1);
1777
1778 /* Now invalidate the caches for both DIRNAME and DOCID. */
1779
1780 if (storageThread != null)
1781 {
1782 storageThread.postInvalidateCacheDir (uri1, docId, dirName);
1783
1784 /* Invalidate the stat cache entries for both the source and
1785 destination directories, since their contents have
1786 changed. */
1787 storageThread.postInvalidateStat (uri1, dstId);
1788 storageThread.postInvalidateStat (uri1, srcId);
1789 }
1790
1791 return (name != null
1792 ? DocumentsContract.getDocumentId (name)
1793 : null);
1794 }
1795
1796 /* Return if there is a content provider by the name of AUTHORITY
1797 supplying at least one tree URI Emacs retains persistent rights
1798 to access. */
1799
1800 public boolean
1801 validAuthority (String authority)
1802 {
1803 List<UriPermission> permissions;
1804 Uri uri;
1805
1806 permissions = resolver.getPersistedUriPermissions ();
1807
1808 for (UriPermission permission : permissions)
1809 {
1810 uri = permission.getUri ();
1811
1812 if (DocumentsContract.isTreeUri (uri)
1813 && permission.isReadPermission ()
1814 && uri.getAuthority ().equals (authority))
1815 return true;
1816 }
1817
1818 return false;
1819 }
1820};
diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java
new file mode 100644
index 00000000000..c47696b35c0
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSurfaceView.java
@@ -0,0 +1,223 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.view.View;
23
24import android.os.Build;
25
26import android.graphics.Bitmap;
27import android.graphics.Canvas;
28import android.graphics.Rect;
29import android.graphics.Paint;
30
31import java.lang.ref.WeakReference;
32
33/* This originally extended SurfaceView. However, doing so proved to
34 be too slow, and Android's surface view keeps up to three of its
35 own back buffers, which use too much memory (up to 96 MB for a
36 single frame.) */
37
38public final class EmacsSurfaceView extends View
39{
40 private static final String TAG = "EmacsSurfaceView";
41
42 /* The complete buffer contents at the time of the last draw. */
43 private Bitmap frontBuffer;
44
45 /* Whether frontBuffer has been updated since the last call to
46 `onDraw'. */
47 private boolean bitmapChanged;
48
49 /* Canvas representing the front buffer. */
50 private Canvas bitmapCanvas;
51
52 /* Reference to the last bitmap copied to the front buffer. */
53 private WeakReference<Bitmap> bitmap;
54
55 /* Paint objects used on the main and UI threads, respectively. */
56 private static final Paint bitmapPaint, uiThreadPaint;
57
58 static
59 {
60 /* Create two different Paint objects; one is used on the main
61 thread for buffer swaps, while the other is used from the UI
62 thread in `onDraw'. This is necessary because Paint objects
63 are not thread-safe, even if their uses are interlocked. */
64
65 bitmapPaint = new Paint ();
66 uiThreadPaint = new Paint ();
67 };
68
69 public
70 EmacsSurfaceView (EmacsView view)
71 {
72 super (view.getContext ());
73
74 this.bitmap = new WeakReference<Bitmap> (null);
75 }
76
77 private void
78 copyToFrontBuffer (Bitmap bitmap, Rect damageRect)
79 {
80 EmacsService.checkEmacsThread ();
81
82 if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O
83 && Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1
84 && Build.VERSION.SDK_INT != Build.VERSION_CODES.N_MR1
85 && Build.VERSION.SDK_INT != Build.VERSION_CODES.N)
86 {
87 /* If `drawBitmap' can safely be used while a bitmap is locked
88 by another thread, continue here... */
89
90 if (damageRect != null)
91 bitmapCanvas.drawBitmap (bitmap, damageRect, damageRect,
92 bitmapPaint);
93 else
94 bitmapCanvas.drawBitmap (bitmap, 0f, 0f, bitmapPaint);
95 }
96 else
97 {
98 /* But if it can not, as on Android 7.0 through 8.1, then use
99 a replacement function. */
100
101 if (damageRect != null)
102 EmacsNative.blitRect (bitmap, frontBuffer,
103 damageRect.left,
104 damageRect.top,
105 damageRect.right,
106 damageRect.bottom);
107 else
108 EmacsNative.blitRect (bitmap, frontBuffer, 0, 0,
109 bitmap.getWidth (),
110 bitmap.getHeight ());
111 }
112
113 /* See the large comment inside `onDraw'. */
114 bitmapChanged = true;
115 }
116
117 private void
118 reconfigureFrontBuffer (Bitmap bitmap)
119 {
120 /* First, remove the old front buffer. */
121
122 if (frontBuffer != null)
123 {
124 frontBuffer.recycle ();
125 frontBuffer = null;
126 bitmapCanvas = null;
127 }
128
129 this.bitmap = new WeakReference<Bitmap> (bitmap);
130
131 /* Next, create the new front buffer if necessary. */
132
133 if (bitmap != null && frontBuffer == null)
134 {
135 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
136 frontBuffer = Bitmap.createBitmap (bitmap.getWidth (),
137 bitmap.getHeight (),
138 Bitmap.Config.ARGB_8888,
139 false);
140 else
141 frontBuffer = Bitmap.createBitmap (bitmap.getWidth (),
142 bitmap.getHeight (),
143 Bitmap.Config.ARGB_8888);
144
145 bitmapCanvas = new Canvas (frontBuffer);
146
147 /* And copy over the bitmap contents. */
148 copyToFrontBuffer (bitmap, null);
149 }
150 else if (bitmap != null)
151 /* Just copy over the bitmap contents. */
152 copyToFrontBuffer (bitmap, null);
153 }
154
155 public synchronized void
156 setBitmap (Bitmap bitmap, Rect damageRect)
157 {
158 if (bitmap != this.bitmap.get ())
159 reconfigureFrontBuffer (bitmap);
160 else if (bitmap != null)
161 copyToFrontBuffer (bitmap, damageRect);
162
163 if (bitmap != null)
164 {
165 /* In newer versions of Android, the invalid rectangle is
166 supposedly internally calculated by the system. How that
167 is done is unknown, but calling `invalidateRect' is now
168 deprecated.
169
170 Fortunately, nobody has deprecated the version of
171 `postInvalidate' that accepts a dirty rectangle. */
172
173 if (damageRect != null)
174 postInvalidate (damageRect.left, damageRect.top,
175 damageRect.right, damageRect.bottom);
176 else
177 postInvalidate ();
178 }
179 }
180
181 @Override
182 public synchronized void
183 onDraw (Canvas canvas)
184 {
185 /* Paint the view's bitmap; the bitmap might be recycled right
186 now. */
187
188 if (frontBuffer != null)
189 {
190 /* The first time the bitmap is drawn after a buffer swap,
191 mark its contents as having changed. This increments the
192 ``generation ID'' used by Android to avoid uploading buffer
193 textures for unchanged bitmaps.
194
195 When a buffer swap takes place, the bitmap is initially
196 updated from the Emacs thread, resulting in the generation
197 ID being increased. If the render thread is texturizing
198 the bitmap while the swap takes place, it might record the
199 generation ID after the update for a texture containing the
200 contents of the bitmap prior to the swap, leaving the
201 texture tied to the bitmap partially updated.
202
203 Android never calls `onDraw' if the render thread is still
204 processing the bitmap. Update the generation ID here to
205 ensure that a new texture will be uploaded if the bitmap
206 has changed.
207
208 Uploading the bitmap contents to the GPU uses an excessive
209 amount of memory, as the entire bitmap is placed into the
210 graphics command queue, but this memory is actually shared
211 among all other applications and reclaimed by the system
212 when necessary. */
213
214 if (bitmapChanged)
215 {
216 EmacsNative.notifyPixelsChanged (frontBuffer);
217 bitmapChanged = false;
218 }
219
220 canvas.drawBitmap (frontBuffer, 0f, 0f, uiThreadPaint);
221 }
222 }
223};
diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java
new file mode 100644
index 00000000000..5307015b46f
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsThread.java
@@ -0,0 +1,82 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.lang.Thread;
23import java.util.Arrays;
24
25import android.util.Log;
26
27public final class EmacsThread extends Thread
28{
29 private static final String TAG = "EmacsThread";
30
31 /* Whether or not Emacs should be started with an additional
32 argument, and that additional argument if non-NULL. */
33 private String extraStartupArgument;
34
35 /* Runnable run to initialize Emacs. */
36 private Runnable paramsClosure;
37
38 /* Whether or not to open a file after starting Emacs. */
39 private String fileToOpen;
40
41 public
42 EmacsThread (EmacsService service, Runnable paramsClosure,
43 String extraStartupArgument, String fileToOpen)
44 {
45 super ("Emacs main thread");
46 this.extraStartupArgument = extraStartupArgument;
47 this.paramsClosure = paramsClosure;
48 this.fileToOpen = fileToOpen;
49 }
50
51 @Override
52 public void
53 run ()
54 {
55 String args[];
56
57 if (fileToOpen == null)
58 {
59 if (extraStartupArgument == null)
60 args = new String[] { "libandroid-emacs.so", };
61 else
62 args = new String[] { "libandroid-emacs.so",
63 extraStartupArgument, };
64 }
65 else
66 {
67 if (extraStartupArgument == null)
68 args = new String[] { "libandroid-emacs.so",
69 fileToOpen, };
70 else
71 args = new String[] { "libandroid-emacs.so",
72 extraStartupArgument,
73 fileToOpen, };
74 }
75
76 paramsClosure.run ();
77
78 /* Run the native code now. */
79 Log.d (TAG, "run: " + Arrays.toString (args));
80 EmacsNative.initEmacs (args, EmacsApplication.dumpFileName);
81 }
82};
diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java
new file mode 100644
index 00000000000..12d8ff4da56
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsView.java
@@ -0,0 +1,777 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.content.Context;
23
24import android.text.InputType;
25
26import android.view.ContextMenu;
27import android.view.View;
28import android.view.KeyEvent;
29import android.view.MotionEvent;
30import android.view.ViewGroup;
31import android.view.ViewTreeObserver;
32
33import android.view.inputmethod.EditorInfo;
34import android.view.inputmethod.InputConnection;
35import android.view.inputmethod.InputMethodManager;
36
37import android.graphics.Bitmap;
38import android.graphics.Canvas;
39import android.graphics.Rect;
40import android.graphics.Region;
41import android.graphics.Paint;
42
43import android.os.Build;
44import android.util.Log;
45
46/* This is an Android view which has a back and front buffer. When
47 swapBuffers is called, the back buffer is swapped to the front
48 buffer, and any damage is invalidated. frontBitmap and backBitmap
49 are modified and used both from the UI and the Emacs thread. As a
50 result, there is a lock held during all drawing operations.
51
52 It is also a ViewGroup, as it also lays out children. */
53
54public final class EmacsView extends ViewGroup
55 implements ViewTreeObserver.OnGlobalLayoutListener
56{
57 public static final String TAG = "EmacsView";
58
59 /* The associated EmacsWindow. */
60 public EmacsWindow window;
61
62 /* The buffer bitmap. */
63 public Bitmap bitmap;
64
65 /* The associated canvases. */
66 public Canvas canvas;
67
68 /* The damage region. */
69 public Region damageRegion;
70
71 /* The associated surface view. */
72 private EmacsSurfaceView surfaceView;
73
74 /* Whether or not a configure event must be sent for the next layout
75 event regardless of what changed. */
76 public boolean mustReportLayout;
77
78 /* Whether or not bitmaps must be recreated upon the next call to
79 getBitmap. */
80 private boolean bitmapDirty;
81
82 /* Whether or not a popup is active. */
83 private boolean popupActive;
84
85 /* The current context menu. */
86 private EmacsContextMenu contextMenu;
87
88 /* The last measured width and height. */
89 private int measuredWidth, measuredHeight;
90
91 /* Object acting as a lock for those values. */
92 private Object dimensionsLock;
93
94 /* The serial of the last clip rectangle change. */
95 private long lastClipSerial;
96
97 /* The InputMethodManager for this view's context. */
98 public InputMethodManager imManager;
99
100 /* Whether or not this view is attached to a window. */
101 public boolean isAttachedToWindow;
102
103 /* Whether or not this view should have the on screen keyboard
104 displayed whenever possible. */
105 public boolean isCurrentlyTextEditor;
106
107 /* The associated input connection. */
108 private EmacsInputConnection inputConnection;
109
110 /* The current IC mode. See `android_reset_ic' for more
111 details. */
112 private int icMode;
113
114 /* The number of calls to `resetIC' to have taken place the last
115 time an InputConnection was created. */
116 public long icSerial;
117
118 /* The number of calls to `recetIC' that have taken place. */
119 public volatile long icGeneration;
120
121 public
122 EmacsView (EmacsWindow window)
123 {
124 super (EmacsService.SERVICE);
125
126 Object tem;
127 Context context;
128
129 this.window = window;
130 this.damageRegion = new Region ();
131
132 setFocusable (true);
133 setFocusableInTouchMode (true);
134
135 /* Create the surface view. */
136 this.surfaceView = new EmacsSurfaceView (this);
137 addView (this.surfaceView);
138
139 /* Get rid of the default focus highlight. */
140 if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O)
141 setDefaultFocusHighlightEnabled (false);
142
143 /* Obtain the input method manager. */
144 context = getContext ();
145 tem = context.getSystemService (Context.INPUT_METHOD_SERVICE);
146 imManager = (InputMethodManager) tem;
147
148 /* Add this view as its own global layout listener. */
149 getViewTreeObserver ().addOnGlobalLayoutListener (this);
150
151 /* Create an object used as a lock. */
152 this.dimensionsLock = new Object ();
153 }
154
155 private void
156 handleDirtyBitmap ()
157 {
158 Bitmap oldBitmap;
159 int measuredWidth, measuredHeight;
160
161 synchronized (dimensionsLock)
162 {
163 /* Load measuredWidth and measuredHeight. */
164 measuredWidth = this.measuredWidth;
165 measuredHeight = this.measuredHeight;
166 }
167
168 if (measuredWidth == 0 || measuredHeight == 0)
169 return;
170
171 if (!isAttachedToWindow)
172 return;
173
174 /* If bitmap is the same width and height as the measured width
175 and height, there is no need to do anything. Avoid allocating
176 the extra bitmap. */
177 if (bitmap != null
178 && (bitmap.getWidth () == measuredWidth
179 && bitmap.getHeight () == measuredHeight))
180 {
181 bitmapDirty = false;
182 return;
183 }
184
185 /* Save the old bitmap. */
186 oldBitmap = bitmap;
187
188 /* Recreate the back buffer bitmap. */
189 bitmap
190 = Bitmap.createBitmap (measuredWidth,
191 measuredHeight,
192 Bitmap.Config.ARGB_8888);
193 bitmap.eraseColor (window.background | 0xff000000);
194
195 /* And canvases. */
196 canvas = new Canvas (bitmap);
197 canvas.save ();
198
199 /* Since the clip rectangles have been cleared, clear the clip
200 rectangle ID. */
201 lastClipSerial = 0;
202
203 /* Copy over the contents of the old bitmap. */
204 if (oldBitmap != null)
205 canvas.drawBitmap (oldBitmap, 0f, 0f, new Paint ());
206
207 bitmapDirty = false;
208
209 /* Explicitly free the old bitmap's memory. */
210
211 if (oldBitmap != null)
212 oldBitmap.recycle ();
213
214 /* Some Android versions still don't free the bitmap until the
215 next GC. */
216 Runtime.getRuntime ().gc ();
217 }
218
219 public synchronized void
220 explicitlyDirtyBitmap ()
221 {
222 bitmapDirty = true;
223 }
224
225 public synchronized Bitmap
226 getBitmap ()
227 {
228 if (bitmapDirty || bitmap == null)
229 handleDirtyBitmap ();
230
231 return bitmap;
232 }
233
234 public synchronized Canvas
235 getCanvas (EmacsGC gc)
236 {
237 int i;
238
239 if (bitmapDirty || bitmap == null)
240 handleDirtyBitmap ();
241
242 if (canvas == null)
243 return null;
244
245 /* Update clip rectangles if necessary. */
246 if (gc.clipRectID != lastClipSerial)
247 {
248 canvas.restore ();
249 canvas.save ();
250
251 if (gc.real_clip_rects != null)
252 {
253 for (i = 0; i < gc.real_clip_rects.length; ++i)
254 canvas.clipRect (gc.real_clip_rects[i]);
255 }
256
257 lastClipSerial = gc.clipRectID;
258 }
259
260 return canvas;
261 }
262
263 public void
264 prepareForLayout (int wantedWidth, int wantedHeight)
265 {
266 synchronized (dimensionsLock)
267 {
268 measuredWidth = wantedWidth;
269 measuredHeight = wantedWidth;
270 }
271 }
272
273 @Override
274 protected void
275 onMeasure (int widthMeasureSpec, int heightMeasureSpec)
276 {
277 Rect measurements;
278 int width, height;
279
280 /* Return the width and height of the window regardless of what
281 the parent says. */
282 measurements = window.getGeometry ();
283
284 width = measurements.width ();
285 height = measurements.height ();
286
287 /* Now apply any extra requirements in widthMeasureSpec and
288 heightMeasureSpec. */
289
290 if (MeasureSpec.getMode (widthMeasureSpec) == MeasureSpec.EXACTLY)
291 width = MeasureSpec.getSize (widthMeasureSpec);
292 else if (MeasureSpec.getMode (widthMeasureSpec) == MeasureSpec.AT_MOST
293 && width > MeasureSpec.getSize (widthMeasureSpec))
294 width = MeasureSpec.getSize (widthMeasureSpec);
295
296 if (MeasureSpec.getMode (heightMeasureSpec) == MeasureSpec.EXACTLY)
297 height = MeasureSpec.getSize (heightMeasureSpec);
298 else if (MeasureSpec.getMode (heightMeasureSpec) == MeasureSpec.AT_MOST
299 && height > MeasureSpec.getSize (heightMeasureSpec))
300 height = MeasureSpec.getSize (heightMeasureSpec);
301
302 super.setMeasuredDimension (width, height);
303 }
304
305 /* Note that the monitor lock for the window must never be held from
306 within the lock for the view, because the window also locks the
307 other way around. */
308
309 @Override
310 protected void
311 onLayout (boolean changed, int left, int top, int right,
312 int bottom)
313 {
314 int count, i, oldMeasuredWidth, oldMeasuredHeight;
315 View child;
316 Rect windowRect;
317 boolean needExpose;
318
319 count = getChildCount ();
320 needExpose = false;
321
322 synchronized (dimensionsLock)
323 {
324 /* Load measuredWidth and measuredHeight. */
325 oldMeasuredWidth = measuredWidth;
326 oldMeasuredHeight = measuredHeight;
327
328 /* Set measuredWidth and measuredHeight. */
329 measuredWidth = right - left;
330 measuredHeight = bottom - top;
331 }
332
333 /* Dirty the back buffer if the layout change resulted in the view
334 being resized. */
335
336 if (changed && (right - left != oldMeasuredWidth
337 || bottom - top != oldMeasuredHeight))
338 {
339 explicitlyDirtyBitmap ();
340
341 /* Expose the window upon a change in the view's size. */
342
343 if (right - left > oldMeasuredWidth
344 || bottom - top > oldMeasuredHeight)
345 needExpose = true;
346 }
347
348 for (i = 0; i < count; ++i)
349 {
350 child = getChildAt (i);
351
352 Log.d (TAG, "onLayout: " + child);
353
354 if (child == surfaceView)
355 child.layout (0, 0, right - left, bottom - top);
356 else if (child.getVisibility () != GONE)
357 {
358 if (!(child instanceof EmacsView))
359 continue;
360
361 /* What to do: lay out the view precisely according to its
362 window rect. */
363 windowRect = ((EmacsView) child).window.getGeometry ();
364 child.layout (windowRect.left, windowRect.top,
365 windowRect.right, windowRect.bottom);
366 }
367 }
368
369 /* Now report the layout change to the window. */
370
371 if (changed || mustReportLayout)
372 {
373 mustReportLayout = false;
374 window.viewLayout (left, top, right, bottom);
375 }
376
377 if (needExpose)
378 EmacsNative.sendExpose (this.window.handle, 0, 0,
379 right - left, bottom - top);
380 }
381
382 public void
383 damageRect (Rect damageRect)
384 {
385 EmacsService.checkEmacsThread ();
386 damageRegion.union (damageRect);
387 }
388
389 /* This method is called from both the UI thread and the Emacs
390 thread. */
391
392 public void
393 swapBuffers ()
394 {
395 Canvas canvas;
396 Rect damageRect;
397 Bitmap bitmap;
398
399 /* Make sure this function is called only from the Emacs
400 thread. */
401 EmacsService.checkEmacsThread ();
402
403 damageRect = null;
404
405 /* Now see if there is a damage region. */
406
407 if (damageRegion.isEmpty ())
408 return;
409
410 /* And extract and clear the damage region. */
411
412 damageRect = damageRegion.getBounds ();
413 damageRegion.setEmpty ();
414
415 bitmap = getBitmap ();
416
417 /* Transfer the bitmap to the surface view, then invalidate
418 it. */
419 surfaceView.setBitmap (bitmap, damageRect);
420 }
421
422 @Override
423 public boolean
424 onKeyDown (int keyCode, KeyEvent event)
425 {
426 if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP
427 || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
428 || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE)
429 && !EmacsNative.shouldForwardMultimediaButtons ())
430 return false;
431
432 window.onKeyDown (keyCode, event);
433 return true;
434 }
435
436 @Override
437 public boolean
438 onKeyMultiple (int keyCode, int repeatCount, KeyEvent event)
439 {
440 if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP
441 || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
442 || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE)
443 && !EmacsNative.shouldForwardMultimediaButtons ())
444 return false;
445
446 window.onKeyDown (keyCode, event);
447 return true;
448 }
449
450 @Override
451 public boolean
452 onKeyUp (int keyCode, KeyEvent event)
453 {
454 if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP
455 || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN
456 || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE)
457 && !EmacsNative.shouldForwardMultimediaButtons ())
458 return false;
459
460 window.onKeyUp (keyCode, event);
461 return true;
462 }
463
464 @Override
465 public void
466 onFocusChanged (boolean gainFocus, int direction,
467 Rect previouslyFocusedRect)
468 {
469 window.onFocusChanged (gainFocus);
470 super.onFocusChanged (gainFocus, direction,
471 previouslyFocusedRect);
472 }
473
474 @Override
475 public boolean
476 onGenericMotionEvent (MotionEvent motion)
477 {
478 return window.onGenericMotionEvent (motion);
479 }
480
481 @Override
482 public boolean
483 onTouchEvent (MotionEvent motion)
484 {
485 return window.onTouchEvent (motion);
486 }
487
488 private void
489 moveChildToBack (View child)
490 {
491 int index;
492
493 index = indexOfChild (child);
494
495 if (index > 0)
496 {
497 detachViewFromParent (index);
498
499 /* The view at 0 is the surface view. */
500 attachViewToParent (child, 1,
501 child.getLayoutParams());
502 }
503 }
504
505 /* The following two functions must not be called if the view has no
506 parent, or is parented to an activity. */
507
508 public void
509 raise ()
510 {
511 EmacsView parent;
512
513 parent = (EmacsView) getParent ();
514
515 Log.d (TAG, "raise: parent " + parent);
516
517 if (parent.indexOfChild (this)
518 == parent.getChildCount () - 1)
519 return;
520
521 parent.bringChildToFront (this);
522 }
523
524 public void
525 lower ()
526 {
527 EmacsView parent;
528
529 parent = (EmacsView) getParent ();
530
531 Log.d (TAG, "lower: parent " + parent);
532
533 if (parent.indexOfChild (this) == 1)
534 return;
535
536 parent.moveChildToBack (this);
537 }
538
539 @Override
540 protected void
541 onCreateContextMenu (ContextMenu menu)
542 {
543 if (contextMenu == null)
544 return;
545
546 contextMenu.expandTo (menu, this);
547 }
548
549 public boolean
550 popupMenu (EmacsContextMenu menu, int xPosition,
551 int yPosition, boolean force)
552 {
553 if (popupActive && !force)
554 return false;
555
556 contextMenu = menu;
557 popupActive = true;
558
559 Log.d (TAG, "popupMenu: " + menu + " @" + xPosition
560 + ", " + yPosition + " " + force);
561
562 /* Use showContextMenu (float, float) on N to get actual popup
563 behavior. */
564 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
565 return showContextMenu ((float) xPosition, (float) yPosition);
566 else
567 return showContextMenu ();
568 }
569
570 public void
571 cancelPopupMenu ()
572 {
573 if (!popupActive)
574 throw new IllegalStateException ("cancelPopupMenu called without"
575 + " popupActive set");
576
577 contextMenu = null;
578 popupActive = false;
579
580 /* It is not possible to know with 100% certainty which activity
581 is currently displaying the context menu. Loop through each
582 activity and call `closeContextMenu' instead. */
583
584 for (EmacsWindowAttachmentManager.WindowConsumer consumer
585 : EmacsWindowAttachmentManager.MANAGER.consumers)
586 {
587 if (consumer instanceof EmacsActivity)
588 ((EmacsActivity) consumer).closeContextMenu ();
589 }
590 }
591
592 @Override
593 public synchronized void
594 onDetachedFromWindow ()
595 {
596 isAttachedToWindow = false;
597
598 /* Recycle the bitmap and call GC. */
599
600 if (bitmap != null)
601 bitmap.recycle ();
602
603 bitmap = null;
604 canvas = null;
605 surfaceView.setBitmap (null, null);
606
607 /* Collect the bitmap storage; it could be large. */
608 Runtime.getRuntime ().gc ();
609
610 super.onDetachedFromWindow ();
611 }
612
613 @Override
614 public synchronized void
615 onAttachedToWindow ()
616 {
617 isAttachedToWindow = true;
618
619 /* Dirty the bitmap, as it was destroyed when onDetachedFromWindow
620 was called. */
621 bitmapDirty = true;
622
623 synchronized (dimensionsLock)
624 {
625 /* Now expose the view contents again. */
626 EmacsNative.sendExpose (this.window.handle, 0, 0,
627 measuredWidth, measuredHeight);
628 }
629
630 super.onAttachedToWindow ();
631 }
632
633 public void
634 showOnScreenKeyboard ()
635 {
636 /* Specifying no flags at all tells the system the user asked for
637 the input method to be displayed. */
638
639 imManager.showSoftInput (this, 0);
640 isCurrentlyTextEditor = true;
641 }
642
643 public void
644 hideOnScreenKeyboard ()
645 {
646 imManager.hideSoftInputFromWindow (this.getWindowToken (),
647 0);
648 isCurrentlyTextEditor = false;
649 }
650
651 @Override
652 public InputConnection
653 onCreateInputConnection (EditorInfo info)
654 {
655 int mode;
656 int[] selection;
657
658 /* Figure out what kind of IME behavior Emacs wants. */
659 mode = getICMode ();
660
661 /* Make sure the input method never displays a full screen input
662 box that obscures Emacs. */
663 info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN;
664 info.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI;
665
666 /* Set a reasonable inputType. */
667 info.inputType = InputType.TYPE_CLASS_TEXT;
668
669 /* If this fails or ANDROID_IC_MODE_NULL was requested, then don't
670 initialize the input connection. */
671
672 if (mode == EmacsService.IC_MODE_NULL)
673 {
674 info.inputType = InputType.TYPE_NULL;
675 return null;
676 }
677
678 /* Set icSerial. If icSerial < icGeneration, the input connection
679 has been reset, and future input should be ignored until a new
680 connection is created. */
681
682 icSerial = icGeneration;
683
684 /* Reset flags set by the previous input method. */
685
686 EmacsNative.clearInputFlags (window.handle);
687
688 /* Obtain the current position of point and set it as the
689 selection. Don't do this under one specific situation: if
690 `android_update_ic' is being called in the main thread, trying
691 to synchronize with it can cause a dead lock in the IM manager.
692 See icBeginSynchronous in EmacsService.java for more
693 details. */
694
695 selection = EmacsService.viewGetSelection (window.handle);
696
697 if (selection != null)
698 Log.d (TAG, "onCreateInputConnection: current selection is: "
699 + selection[0] + ", by " + selection[1]);
700 else
701 {
702 Log.d (TAG, "onCreateInputConnection: current selection could"
703 + " not be retrieved.");
704
705 /* If the selection could not be obtained, return 0 by 0.
706 However, ask for the selection position to be updated as
707 soon as possible. */
708
709 selection = new int[] { 0, 0, };
710 EmacsNative.requestSelectionUpdate (window.handle);
711 }
712
713 if (mode == EmacsService.IC_MODE_ACTION)
714 info.imeOptions |= EditorInfo.IME_ACTION_DONE;
715
716 /* Set the initial selection fields. */
717 info.initialSelStart = selection[0];
718 info.initialSelEnd = selection[1];
719
720 /* Create the input connection if necessary. */
721
722 if (inputConnection == null)
723 inputConnection = new EmacsInputConnection (this);
724 else
725 /* Clear several pieces of state in the input connection. */
726 inputConnection.reset ();
727
728 /* Return the input connection. */
729 return inputConnection;
730 }
731
732 @Override
733 public synchronized boolean
734 onCheckIsTextEditor ()
735 {
736 /* If value is true, then the system will display the on screen
737 keyboard. */
738 return isCurrentlyTextEditor;
739 }
740
741 @Override
742 public boolean
743 isOpaque ()
744 {
745 /* Returning true here allows the system to not draw the contents
746 of windows underneath this view, thereby improving
747 performance. */
748 return true;
749 }
750
751 public synchronized void
752 setICMode (int icMode)
753 {
754 this.icMode = icMode;
755 }
756
757 public synchronized int
758 getICMode ()
759 {
760 return icMode;
761 }
762
763 @Override
764 public void
765 onGlobalLayout ()
766 {
767 int[] locations;
768
769 /* Get the absolute offset of this view and specify its left and
770 top position in subsequent ConfigureNotify events. */
771
772 locations = new int[2];
773 getLocationInWindow (locations);
774 window.notifyContentRectPosition (locations[0],
775 locations[1]);
776 }
777};
diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java
new file mode 100644
index 00000000000..a1f70644e16
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsWindow.java
@@ -0,0 +1,1445 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.lang.IllegalStateException;
23import java.util.ArrayList;
24import java.util.List;
25import java.util.HashMap;
26import java.util.LinkedHashMap;
27import java.util.Map;
28
29import android.content.Context;
30
31import android.graphics.Rect;
32import android.graphics.Canvas;
33import android.graphics.Bitmap;
34import android.graphics.PixelFormat;
35
36import android.view.View;
37import android.view.ViewManager;
38import android.view.Gravity;
39import android.view.KeyEvent;
40import android.view.MotionEvent;
41import android.view.InputDevice;
42import android.view.WindowManager;
43
44import android.util.Log;
45
46import android.os.Build;
47
48/* This defines a window, which is a handle. Windows represent a
49 rectangular subset of the screen with their own contents.
50
51 Windows either have a parent window, in which case their views are
52 attached to the parent's view, or are "floating", in which case
53 their views are attached to the parent activity (if any), else
54 nothing.
55
56 Views are also drawables, meaning they can accept drawing
57 requests. */
58
59public final class EmacsWindow extends EmacsHandleObject
60 implements EmacsDrawable
61{
62 private static final String TAG = "EmacsWindow";
63
64 private static class Coordinate
65 {
66 /* Integral coordinate. */
67 int x, y;
68
69 /* Button associated with the coordinate, or 0 if it is a touch
70 event. */
71 int button;
72
73 /* Pointer ID associated with the coordinate. */
74 int id;
75
76 public
77 Coordinate (int x, int y, int button, int id)
78 {
79 this.x = x;
80 this.y = y;
81 this.button = button;
82 this.id = id;
83 }
84 };
85
86 /* The view associated with the window. */
87 public EmacsView view;
88
89 /* The geometry of the window. */
90 private Rect rect;
91
92 /* The parent window, or null if it is the root window. */
93 public EmacsWindow parent;
94
95 /* List of all children in stacking order. This must be kept
96 consistent with their Z order! */
97 public ArrayList<EmacsWindow> children;
98
99 /* Map between pointer identifiers and last known position. Used to
100 compute which pointer changed upon a touch event. */
101 private HashMap<Integer, Coordinate> pointerMap;
102
103 /* The window consumer currently attached, if it exists. */
104 private EmacsWindowAttachmentManager.WindowConsumer attached;
105
106 /* The window background scratch GC. foreground is always the
107 window background. */
108 private EmacsGC scratchGC;
109
110 /* The button state and keyboard modifier mask at the time of the
111 last button press or release event. */
112 public int lastButtonState;
113
114 /* Whether or not the window is mapped. */
115 private volatile boolean isMapped;
116
117 /* Whether or not to ask for focus upon being mapped. */
118 private boolean dontFocusOnMap;
119
120 /* Whether or not the window is override-redirect. An
121 override-redirect window always has its own system window. */
122 private boolean overrideRedirect;
123
124 /* The window manager that is the parent of this window. NULL if
125 there is no such window manager. */
126 private WindowManager windowManager;
127
128 /* The time of the last KEYCODE_VOLUME_DOWN release. This is used
129 to quit Emacs upon two rapid clicks of the volume down
130 button. */
131 private long lastVolumeButtonRelease;
132
133 /* Linked list of character strings which were recently sent as
134 events. */
135 public LinkedHashMap<Integer, String> eventStrings;
136
137 /* Whether or not this window is fullscreen. */
138 public boolean fullscreen;
139
140 /* The window background pixel. This is used by EmacsView when
141 creating new bitmaps. */
142 public volatile int background;
143
144 /* The position of this window relative to the root window. */
145 public int xPosition, yPosition;
146
147 public
148 EmacsWindow (short handle, final EmacsWindow parent, int x, int y,
149 int width, int height, boolean overrideRedirect)
150 {
151 super (handle);
152
153 rect = new Rect (x, y, x + width, y + height);
154 pointerMap = new HashMap<Integer, Coordinate> ();
155
156 /* Create the view from the context's UI thread. The window is
157 unmapped, so the view is GONE. */
158 view = EmacsService.SERVICE.getEmacsView (this, View.GONE,
159 parent == null);
160 this.parent = parent;
161 this.overrideRedirect = overrideRedirect;
162
163 /* Create the list of children. */
164 children = new ArrayList<EmacsWindow> ();
165
166 if (parent != null)
167 {
168 parent.children.add (this);
169 EmacsService.SERVICE.runOnUiThread (new Runnable () {
170 @Override
171 public void
172 run ()
173 {
174 parent.view.addView (view);
175 }
176 });
177 }
178
179 scratchGC = new EmacsGC ((short) 0);
180
181 /* Create the map of input method-committed strings. Keep at most
182 ten strings in the map. */
183
184 eventStrings
185 = new LinkedHashMap<Integer, String> () {
186 @Override
187 protected boolean
188 removeEldestEntry (Map.Entry<Integer, String> entry)
189 {
190 return size () > 10;
191 }
192 };
193 }
194
195 public void
196 changeWindowBackground (int pixel)
197 {
198 /* scratchGC is used as the argument to a FillRectangles req. */
199 scratchGC.foreground = pixel;
200 scratchGC.markDirty (false);
201
202 /* Make the background known to the view as well. */
203 background = pixel;
204 }
205
206 public synchronized Rect
207 getGeometry ()
208 {
209 return new Rect (rect);
210 }
211
212 @Override
213 public synchronized void
214 destroyHandle () throws IllegalStateException
215 {
216 if (parent != null)
217 parent.children.remove (this);
218
219 EmacsActivity.invalidateFocus ();
220
221 if (!children.isEmpty ())
222 throw new IllegalStateException ("Trying to destroy window with "
223 + "children!");
224
225 /* Remove the view from its parent and make it invisible. */
226 EmacsService.SERVICE.runOnUiThread (new Runnable () {
227 public void
228 run ()
229 {
230 ViewManager parent;
231 EmacsWindowAttachmentManager manager;
232
233 if (EmacsActivity.focusedWindow == EmacsWindow.this)
234 EmacsActivity.focusedWindow = null;
235
236 manager = EmacsWindowAttachmentManager.MANAGER;
237 view.setVisibility (View.GONE);
238
239 /* If the window manager is set, use that instead. */
240 if (windowManager != null)
241 parent = windowManager;
242 else
243 parent = (ViewManager) view.getParent ();
244 windowManager = null;
245
246 if (parent != null)
247 parent.removeView (view);
248
249 manager.detachWindow (EmacsWindow.this);
250 }
251 });
252
253 super.destroyHandle ();
254 }
255
256 public void
257 setConsumer (EmacsWindowAttachmentManager.WindowConsumer consumer)
258 {
259 attached = consumer;
260 }
261
262 public EmacsWindowAttachmentManager.WindowConsumer
263 getAttachedConsumer ()
264 {
265 return attached;
266 }
267
268 public synchronized long
269 viewLayout (int left, int top, int right, int bottom)
270 {
271 int rectWidth, rectHeight;
272
273 rect.left = left;
274 rect.top = top;
275 rect.right = right;
276 rect.bottom = bottom;
277
278 rectWidth = right - left;
279 rectHeight = bottom - top;
280
281 /* If parent is null, use xPosition and yPosition instead of the
282 geometry rectangle positions. */
283
284 if (parent == null)
285 {
286 left = xPosition;
287 top = yPosition;
288 }
289
290 return EmacsNative.sendConfigureNotify (this.handle,
291 System.currentTimeMillis (),
292 left, top, rectWidth,
293 rectHeight);
294 }
295
296 public void
297 requestViewLayout ()
298 {
299 view.explicitlyDirtyBitmap ();
300
301 EmacsService.SERVICE.runOnUiThread (new Runnable () {
302 @Override
303 public void
304 run ()
305 {
306 if (overrideRedirect)
307 /* Set the layout parameters again. */
308 view.setLayoutParams (getWindowLayoutParams ());
309
310 view.mustReportLayout = true;
311 view.requestLayout ();
312 }
313 });
314 }
315
316 public synchronized void
317 resizeWindow (int width, int height)
318 {
319 rect.right = rect.left + width;
320 rect.bottom = rect.top + height;
321
322 requestViewLayout ();
323 }
324
325 public synchronized void
326 moveWindow (int x, int y)
327 {
328 int width, height;
329
330 width = rect.width ();
331 height = rect.height ();
332
333 rect.left = x;
334 rect.top = y;
335 rect.right = x + width;
336 rect.bottom = y + height;
337
338 requestViewLayout ();
339 }
340
341 private WindowManager.LayoutParams
342 getWindowLayoutParams ()
343 {
344 WindowManager.LayoutParams params;
345 int flags, type;
346 Rect rect;
347
348 flags = 0;
349 rect = getGeometry ();
350 flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;
351 flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE;
352 type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG;
353
354 params
355 = new WindowManager.LayoutParams (rect.width (), rect.height (),
356 rect.left, rect.top,
357 type, flags,
358 PixelFormat.RGBA_8888);
359 params.gravity = Gravity.TOP | Gravity.LEFT;
360 return params;
361 }
362
363 private Context
364 findSuitableActivityContext ()
365 {
366 /* Find a recently focused activity. */
367 if (!EmacsActivity.focusedActivities.isEmpty ())
368 return EmacsActivity.focusedActivities.get (0);
369
370 /* Return the service context, which probably won't work. */
371 return EmacsService.SERVICE;
372 }
373
374 public synchronized void
375 mapWindow ()
376 {
377 final int width, height;
378
379 if (isMapped)
380 return;
381
382 isMapped = true;
383 width = rect.width ();
384 height = rect.height ();
385
386 if (parent == null)
387 {
388 EmacsService.SERVICE.runOnUiThread (new Runnable () {
389 @Override
390 public void
391 run ()
392 {
393 EmacsWindowAttachmentManager manager;
394 WindowManager windowManager;
395 Context ctx;
396 Object tem;
397 WindowManager.LayoutParams params;
398
399 /* Make the view visible, first of all. */
400 view.setVisibility (View.VISIBLE);
401
402 if (!overrideRedirect)
403 {
404 manager = EmacsWindowAttachmentManager.MANAGER;
405
406 /* If parent is the root window, notice that there are new
407 children available for interested activites to pick
408 up. */
409 manager.registerWindow (EmacsWindow.this);
410
411 if (!getDontFocusOnMap ())
412 /* Eventually this should check no-focus-on-map. */
413 view.requestFocus ();
414 }
415 else
416 {
417 /* But if the window is an override-redirect window,
418 then:
419
420 - Find an activity that is currently active.
421
422 - Map the window as a panel on top of that
423 activity using the system window manager. */
424
425 ctx = findSuitableActivityContext ();
426 tem = ctx.getSystemService (Context.WINDOW_SERVICE);
427 windowManager = (WindowManager) tem;
428
429 /* Calculate layout parameters. */
430 params = getWindowLayoutParams ();
431 view.setLayoutParams (params);
432
433 /* Attach the view. */
434 try
435 {
436 view.prepareForLayout (width, height);
437 windowManager.addView (view, params);
438
439 /* Record the window manager being used in the
440 EmacsWindow object. */
441 EmacsWindow.this.windowManager = windowManager;
442 }
443 catch (Exception e)
444 {
445 Log.w (TAG,
446 "failed to attach override-redirect window, " + e);
447 }
448 }
449 }
450 });
451 }
452 else
453 {
454 /* Do the same thing as above, but don't register this
455 window. */
456 EmacsService.SERVICE.runOnUiThread (new Runnable () {
457 @Override
458 public void
459 run ()
460 {
461 /* Prior to mapping the view, set its measuredWidth and
462 measuredHeight to some reasonable value, in order to
463 avoid excessive bitmap dirtying. */
464
465 view.prepareForLayout (width, height);
466 view.setVisibility (View.VISIBLE);
467
468 if (!getDontFocusOnMap ())
469 view.requestFocus ();
470 }
471 });
472 }
473 }
474
475 public synchronized void
476 unmapWindow ()
477 {
478 if (!isMapped)
479 return;
480
481 isMapped = false;
482
483 view.post (new Runnable () {
484 @Override
485 public void
486 run ()
487 {
488 EmacsWindowAttachmentManager manager;
489
490 manager = EmacsWindowAttachmentManager.MANAGER;
491
492 view.setVisibility (View.GONE);
493
494 /* Detach the view from the window manager if possible. */
495 if (windowManager != null)
496 windowManager.removeView (view);
497 windowManager = null;
498
499 /* Now that the window is unmapped, unregister it as
500 well. */
501 manager.detachWindow (EmacsWindow.this);
502 }
503 });
504 }
505
506 @Override
507 public Canvas
508 lockCanvas (EmacsGC gc)
509 {
510 return view.getCanvas (gc);
511 }
512
513 @Override
514 public void
515 damageRect (Rect damageRect)
516 {
517 view.damageRect (damageRect);
518 }
519
520 public void
521 swapBuffers ()
522 {
523 view.swapBuffers ();
524 }
525
526 public void
527 clearWindow ()
528 {
529 EmacsService.SERVICE.fillRectangle (this, scratchGC,
530 0, 0, rect.width (),
531 rect.height ());
532 }
533
534 public void
535 clearArea (int x, int y, int width, int height)
536 {
537 EmacsService.SERVICE.fillRectangle (this, scratchGC,
538 x, y, width, height);
539 }
540
541 @Override
542 public Bitmap
543 getBitmap ()
544 {
545 return view.getBitmap ();
546 }
547
548 /* event.getCharacters is used because older input methods still
549 require it. */
550 @SuppressWarnings ("deprecation")
551 public int
552 getEventUnicodeChar (KeyEvent event, int state)
553 {
554 String characters;
555
556 if (event.getUnicodeChar (state) != 0)
557 return event.getUnicodeChar (state);
558
559 characters = event.getCharacters ();
560
561 if (characters != null && characters.length () == 1)
562 return characters.charAt (0);
563
564 return characters == null ? 0 : -1;
565 }
566
567 public void
568 saveUnicodeString (int serial, String string)
569 {
570 eventStrings.put (serial, string);
571 }
572
573
574
575 /* Return the modifier mask associated with the specified keyboard
576 input EVENT. Replace bits corresponding to Left or Right keys
577 with their corresponding general modifier bits. */
578
579 private int
580 eventModifiers (KeyEvent event)
581 {
582 int state;
583
584 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2)
585 state = event.getModifiers ();
586 else
587 {
588 /* Replace this with getMetaState and manual
589 normalization. */
590 state = event.getMetaState ();
591
592 /* Normalize the state by setting the generic modifier bit if
593 either a left or right modifier is pressed. */
594
595 if ((state & KeyEvent.META_ALT_LEFT_ON) != 0
596 || (state & KeyEvent.META_ALT_RIGHT_ON) != 0)
597 state |= KeyEvent.META_ALT_MASK;
598
599 if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0
600 || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0)
601 state |= KeyEvent.META_CTRL_MASK;
602 }
603
604 return state;
605 }
606
607 /* event.getCharacters is used because older input methods still
608 require it. */
609 @SuppressWarnings ("deprecation")
610 public void
611 onKeyDown (int keyCode, KeyEvent event)
612 {
613 int state, state_1;
614 long serial;
615 String characters;
616
617 state = eventModifiers (event);
618
619 /* Ignore meta-state understood by Emacs for now, or key presses
620 such as Ctrl+C and Meta+C will not be recognized as an ASCII
621 key press event. */
622
623 state_1
624 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK
625 | KeyEvent.META_SYM_ON | KeyEvent.META_META_MASK);
626
627 synchronized (eventStrings)
628 {
629 serial
630 = EmacsNative.sendKeyPress (this.handle,
631 event.getEventTime (),
632 state, keyCode,
633 getEventUnicodeChar (event,
634 state_1));
635
636 characters = event.getCharacters ();
637
638 if (characters != null && characters.length () > 1)
639 saveUnicodeString ((int) serial, characters);
640 }
641 }
642
643 public void
644 onKeyUp (int keyCode, KeyEvent event)
645 {
646 int state, state_1;
647 long time;
648
649 /* Compute the event's modifier mask. */
650 state = eventModifiers (event);
651
652 /* Ignore meta-state understood by Emacs for now, or key presses
653 such as Ctrl+C and Meta+C will not be recognized as an ASCII
654 key press event. */
655
656 state_1
657 = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK
658 | KeyEvent.META_SYM_ON | KeyEvent.META_META_MASK);
659
660 EmacsNative.sendKeyRelease (this.handle,
661 event.getEventTime (),
662 state, keyCode,
663 getEventUnicodeChar (event,
664 state_1));
665
666 if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN)
667 {
668 /* Check if this volume down press should quit Emacs.
669 Most Android devices have no physical keyboard, so it
670 is unreasonably hard to press C-g. */
671
672 time = event.getEventTime ();
673
674 if (time - lastVolumeButtonRelease < 350)
675 EmacsNative.quit ();
676
677 lastVolumeButtonRelease = time;
678 }
679 }
680
681 public void
682 onFocusChanged (boolean gainFocus)
683 {
684 EmacsActivity.invalidateFocus ();
685 }
686
687 /* Notice that the activity has been detached or destroyed.
688
689 ISFINISHING is set if the activity is not the main activity, or
690 if the activity was not destroyed in response to explicit user
691 action. */
692
693 public void
694 onActivityDetached (boolean isFinishing)
695 {
696 /* Destroy the associated frame when the activity is detached in
697 response to explicit user action. */
698
699 if (isFinishing)
700 EmacsNative.sendWindowAction (this.handle, 0);
701 }
702
703
704
705 /* Mouse and touch event handling.
706
707 Android does not conceptually distinguish between mouse events
708 (those coming from a device whose movement affects the on-screen
709 pointer image) and touch screen events. Each click or touch
710 starts a single pointer gesture sequence, and subsequent motion
711 of the device will result in updates being reported relative to
712 that sequence until the mouse button or touch is released.
713
714 When a touch, click, or pointer motion takes place, several kinds
715 of event can be sent:
716
717 ACTION_DOWN or ACTION_POINTER_DOWN is sent with a new coordinate
718 and an associated ``pointer ID'' identifying the event and its
719 gesture sequence when a click or touch takes place. Emacs is
720 responsible for recording both the position and pointer ID of
721 this click for the purpose of determining future changes to its
722 position.
723
724 ACTION_UP or ACTION_POINTER_UP is sent with a pointer ID when the
725 click associated with a previous ACTION_DOWN event is released.
726
727 ACTION_CANCEL (or ACTION_POINTER_UP with FLAG_CANCELED) is sent
728 if a similar situation transpires: the window system has chosen
729 to grab the click, and future changes to its position will no
730 longer be reported to Emacs.
731
732 ACTION_MOVE is sent if a coordinate tied to a click that has not
733 been released changes. Emacs processes this event by comparing
734 each of the coordinates within the event with its recollection of
735 those contained within prior ACTION_DOWN and ACTION_MOVE events;
736 the pointer ID of the differing coordinate is then reported
737 within a touch or pointer motion event along with its new
738 position.
739
740 The events described above are all sent for both touch and mouse
741 click events. Determining whether an ACTION_DOWN event is
742 associated with a button event is performed by inspecting the
743 mouse button state associated with that event. If it contains
744 any mouse buttons that were not contained in the button state at
745 the time of the last ACTION_DOWN or ACTION_UP event, the
746 coordinate contained within is assumed to be a mouse click,
747 leading to it and associated motion or ACTION_UP events being
748 reported as mouse button or motion events. Otherwise, those
749 events are reported as touch screen events, with the touch ID set
750 to the pointer ID.
751
752 In addition to the events illustrated above, Android also sends
753 several other types of event upon select types of activity from a
754 mouse device:
755
756 ACTION_HOVER_MOVE is sent with the coordinate of the mouse
757 pointer if it moves above a frame prior to any click taking
758 place. Emacs sends a mouse motion event containing the
759 coordinate.
760
761 ACTION_HOVER_ENTER and ACTION_HOVER_LEAVE are respectively sent
762 when the mouse pointer enters and leaves a frame. Moreover,
763 ACTION_HOVER_LEAVE events are sent immediately before an
764 ACTION_DOWN event associated with a mouse click. These
765 extraneous events are distinct in that their button states always
766 contain an additional button compared to the button state
767 recorded at the time of the last ACTION_UP event.
768
769 On Android 6.0 and later, ACTION_BUTTON_PRESS is sent with the
770 coordinate of the mouse pointer if a mouse click occurs,
771 alongside a ACTION_DOWN event. ACTION_BUTTON_RELEASE is sent
772 with the same information upon a mouse click being released, also
773 accompanying an ACTION_UP event.
774
775 However, both types of button events are implemented in a buggy
776 fashion and cannot be used to report button events. */
777
778 /* Look through the button state to determine what button EVENT was
779 generated from. DOWN is true if EVENT is a button press event,
780 false otherwise. Value is the X number of the button. */
781
782 private int
783 whatButtonWasIt (MotionEvent event, boolean down)
784 {
785 int eventState, notIn;
786
787 /* Obtain the new button state. */
788 eventState = event.getButtonState ();
789
790 /* Compute which button is now set or no longer set. */
791
792 notIn = (down ? eventState & ~lastButtonState
793 : lastButtonState & ~eventState);
794
795 if ((notIn & (MotionEvent.BUTTON_PRIMARY
796 | MotionEvent.BUTTON_SECONDARY
797 | MotionEvent.BUTTON_TERTIARY)) == 0)
798 /* No buttons have been pressed, so this is a touch event. */
799 return 0;
800
801 if ((notIn & MotionEvent.BUTTON_PRIMARY) != 0)
802 return 1;
803
804 if ((notIn & MotionEvent.BUTTON_SECONDARY) != 0)
805 return 3;
806
807 if ((notIn & MotionEvent.BUTTON_TERTIARY) != 0)
808 return 2;
809
810 /* Buttons 4, 5, 6 and 7 are actually scroll wheels under X.
811 Thus, report additional buttons starting at 8. */
812
813 if ((notIn & MotionEvent.BUTTON_BACK) != 0)
814 return 8;
815
816 if ((notIn & MotionEvent.BUTTON_FORWARD) != 0)
817 return 9;
818
819 /* Report stylus events as touch screen events. */
820
821 if ((notIn & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0)
822 return 0;
823
824 if ((notIn & MotionEvent.BUTTON_STYLUS_SECONDARY) != 0)
825 return 0;
826
827 /* Not a real value. */
828 return 11;
829 }
830
831 /* Return the mouse button associated with the specified ACTION_DOWN
832 or ACTION_POINTER_DOWN EVENT.
833
834 Value is 0 if no mouse button was pressed, or the X number of
835 that mouse button. */
836
837 private int
838 buttonForEvent (MotionEvent event)
839 {
840 /* ICS and earlier don't support true mouse button events, so
841 treat all down events as touch screen events. */
842
843 if (Build.VERSION.SDK_INT
844 < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
845 return 0;
846
847 return whatButtonWasIt (event, true);
848 }
849
850 /* Return the coordinate object associated with the specified
851 EVENT, or null if it is not known. */
852
853 private Coordinate
854 figureChange (MotionEvent event)
855 {
856 int i, truncatedX, truncatedY, pointerIndex, pointerID, count;
857 Coordinate coordinate;
858
859 /* Initialize this variable now. */
860 coordinate = null;
861
862 switch (event.getActionMasked ())
863 {
864 case MotionEvent.ACTION_DOWN:
865 /* Primary pointer pressed with index 0. */
866
867 pointerID = event.getPointerId (0);
868 coordinate = new Coordinate ((int) event.getX (0),
869 (int) event.getY (0),
870 buttonForEvent (event),
871 pointerID);
872 pointerMap.put (pointerID, coordinate);
873 break;
874
875 case MotionEvent.ACTION_UP:
876 case MotionEvent.ACTION_CANCEL:
877 /* Primary pointer released with index 0. */
878 pointerID = event.getPointerId (0);
879 coordinate = pointerMap.remove (pointerID);
880 break;
881
882 case MotionEvent.ACTION_POINTER_DOWN:
883 /* New pointer. Find the pointer ID from the index and place
884 it in the map. */
885 pointerIndex = event.getActionIndex ();
886 pointerID = event.getPointerId (pointerIndex);
887 coordinate = new Coordinate ((int) event.getX (0),
888 (int) event.getY (0),
889 buttonForEvent (event),
890 pointerID);
891 pointerMap.put (pointerID, coordinate);
892 break;
893
894 case MotionEvent.ACTION_POINTER_UP:
895 /* Pointer removed. Remove it from the map. */
896 pointerIndex = event.getActionIndex ();
897 pointerID = event.getPointerId (pointerIndex);
898 coordinate = pointerMap.remove (pointerID);
899 break;
900
901 default:
902
903 /* Loop through each pointer in the event. */
904
905 count = event.getPointerCount ();
906 for (i = 0; i < count; ++i)
907 {
908 pointerID = event.getPointerId (i);
909
910 /* Look up that pointer in the map. */
911 coordinate = pointerMap.get (pointerID);
912
913 if (coordinate != null)
914 {
915 /* See if coordinates have changed. */
916 truncatedX = (int) event.getX (i);
917 truncatedY = (int) event.getY (i);
918
919 if (truncatedX != coordinate.x
920 || truncatedY != coordinate.y)
921 {
922 /* The pointer changed. Update the coordinate and
923 break out of the loop. */
924 coordinate.x = truncatedX;
925 coordinate.y = truncatedY;
926
927 break;
928 }
929 }
930 }
931
932 /* Set coordinate to NULL if the loop failed to find any
933 matching pointer. */
934
935 if (i == count)
936 coordinate = null;
937 }
938
939 /* Return the pointer ID. */
940 return coordinate;
941 }
942
943 /* Return the modifier mask associated with the specified motion
944 EVENT. Replace bits corresponding to Left or Right keys with
945 their corresponding general modifier bits. */
946
947 private int
948 motionEventModifiers (MotionEvent event)
949 {
950 int state;
951
952 state = event.getMetaState ();
953
954 /* Normalize the state by setting the generic modifier bit if
955 either a left or right modifier is pressed. */
956
957 if ((state & KeyEvent.META_ALT_LEFT_ON) != 0
958 || (state & KeyEvent.META_ALT_RIGHT_ON) != 0)
959 state |= KeyEvent.META_ALT_MASK;
960
961 if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0
962 || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0)
963 state |= KeyEvent.META_CTRL_MASK;
964
965 return state;
966 }
967
968 /* Process a single ACTION_DOWN, ACTION_POINTER_DOWN, ACTION_UP,
969 ACTION_POINTER_UP, ACTION_CANCEL, or ACTION_MOVE event.
970
971 Ascertain which coordinate changed and send an appropriate mouse
972 or touch screen event. */
973
974 private void
975 motionEvent (MotionEvent event)
976 {
977 Coordinate coordinate;
978 int modifiers;
979 long time;
980
981 /* Find data associated with this event's pointer. Namely, its
982 current location, whether or not a change has taken place, and
983 whether or not it is a button event. */
984
985 coordinate = figureChange (event);
986
987 if (coordinate == null)
988 return;
989
990 time = event.getEventTime ();
991
992 if (coordinate.button != 0)
993 {
994 /* This event is tied to a mouse click, so report mouse motion
995 and button events. */
996
997 modifiers = motionEventModifiers (event);
998
999 switch (event.getAction ())
1000 {
1001 case MotionEvent.ACTION_POINTER_DOWN:
1002 case MotionEvent.ACTION_DOWN:
1003 EmacsNative.sendButtonPress (this.handle, coordinate.x,
1004 coordinate.y, time, modifiers,
1005 coordinate.button);
1006 break;
1007
1008 case MotionEvent.ACTION_POINTER_UP:
1009 case MotionEvent.ACTION_UP:
1010 case MotionEvent.ACTION_CANCEL:
1011 EmacsNative.sendButtonRelease (this.handle, coordinate.x,
1012 coordinate.y, time, modifiers,
1013 coordinate.button);
1014 break;
1015
1016 case MotionEvent.ACTION_MOVE:
1017 EmacsNative.sendMotionNotify (this.handle, coordinate.x,
1018 coordinate.y, time);
1019 break;
1020 }
1021 }
1022 else
1023 {
1024 /* This event is a touch event, and the touch ID is the
1025 pointer ID. */
1026
1027 switch (event.getActionMasked ())
1028 {
1029 case MotionEvent.ACTION_DOWN:
1030 case MotionEvent.ACTION_POINTER_DOWN:
1031 /* Touch down event. */
1032 EmacsNative.sendTouchDown (this.handle, coordinate.x,
1033 coordinate.y, time,
1034 coordinate.id, 0);
1035 break;
1036
1037 case MotionEvent.ACTION_UP:
1038 case MotionEvent.ACTION_POINTER_UP:
1039 /* Touch up event. */
1040 EmacsNative.sendTouchUp (this.handle, coordinate.x,
1041 coordinate.y, time,
1042 coordinate.id, 0);
1043 break;
1044
1045 case MotionEvent.ACTION_CANCEL:
1046 /* Touch sequence cancellation event. */
1047 EmacsNative.sendTouchUp (this.handle, coordinate.x,
1048 coordinate.y, time,
1049 coordinate.id,
1050 1 /* ANDROID_TOUCH_SEQUENCE_CANCELED */);
1051 break;
1052
1053 case MotionEvent.ACTION_MOVE:
1054 /* Pointer motion event. */
1055 EmacsNative.sendTouchMove (this.handle, coordinate.x,
1056 coordinate.y, time,
1057 coordinate.id, 0);
1058 break;
1059 }
1060 }
1061
1062 if (Build.VERSION.SDK_INT
1063 < Build.VERSION_CODES.ICE_CREAM_SANDWICH)
1064 return;
1065
1066 /* Now update the button state. */
1067 lastButtonState = event.getButtonState ();
1068 return;
1069 }
1070
1071 public boolean
1072 onTouchEvent (MotionEvent event)
1073 {
1074 switch (event.getActionMasked ())
1075 {
1076 case MotionEvent.ACTION_DOWN:
1077 case MotionEvent.ACTION_POINTER_DOWN:
1078 case MotionEvent.ACTION_UP:
1079 case MotionEvent.ACTION_POINTER_UP:
1080 case MotionEvent.ACTION_CANCEL:
1081 case MotionEvent.ACTION_MOVE:
1082 motionEvent (event);
1083 return true;
1084 }
1085
1086 return false;
1087 }
1088
1089 public boolean
1090 onGenericMotionEvent (MotionEvent event)
1091 {
1092 switch (event.getAction ())
1093 {
1094 case MotionEvent.ACTION_HOVER_ENTER:
1095 EmacsNative.sendEnterNotify (this.handle, (int) event.getX (),
1096 (int) event.getY (),
1097 event.getEventTime ());
1098 return true;
1099
1100 case MotionEvent.ACTION_HOVER_MOVE:
1101 EmacsNative.sendMotionNotify (this.handle, (int) event.getX (),
1102 (int) event.getY (),
1103 event.getEventTime ());
1104 return true;
1105
1106 case MotionEvent.ACTION_HOVER_EXIT:
1107
1108 /* If the exit event comes from a button press, its button
1109 state will have extra bits compared to the last known
1110 button state. Since the exit event will interfere with
1111 tool bar button presses, ignore such splurious events. */
1112
1113 if ((event.getButtonState () & ~lastButtonState) == 0)
1114 EmacsNative.sendLeaveNotify (this.handle, (int) event.getX (),
1115 (int) event.getY (),
1116 event.getEventTime ());
1117
1118 return true;
1119
1120 case MotionEvent.ACTION_DOWN:
1121 case MotionEvent.ACTION_POINTER_DOWN:
1122 case MotionEvent.ACTION_UP:
1123 case MotionEvent.ACTION_POINTER_UP:
1124 case MotionEvent.ACTION_CANCEL:
1125 case MotionEvent.ACTION_MOVE:
1126 /* MotionEvents may either be sent to onGenericMotionEvent or
1127 onTouchEvent depending on if Android thinks it is a mouse
1128 event or not, but we detect them ourselves. */
1129 motionEvent (event);
1130 return true;
1131
1132 case MotionEvent.ACTION_SCROLL:
1133 /* Send a scroll event with the specified deltas. */
1134 EmacsNative.sendWheel (this.handle, (int) event.getX (),
1135 (int) event.getY (),
1136 event.getEventTime (),
1137 motionEventModifiers (event),
1138 event.getAxisValue (MotionEvent.AXIS_HSCROLL),
1139 event.getAxisValue (MotionEvent.AXIS_VSCROLL));
1140 return true;
1141 }
1142
1143 return false;
1144 }
1145
1146
1147
1148 public synchronized void
1149 reparentTo (final EmacsWindow otherWindow, int x, int y)
1150 {
1151 int width, height;
1152
1153 /* Reparent this window to the other window. */
1154
1155 if (parent != null)
1156 parent.children.remove (this);
1157
1158 if (otherWindow != null)
1159 otherWindow.children.add (this);
1160
1161 parent = otherWindow;
1162
1163 /* Move this window to the new location. */
1164 width = rect.width ();
1165 height = rect.height ();
1166 rect.left = x;
1167 rect.top = y;
1168 rect.right = x + width;
1169 rect.bottom = y + height;
1170
1171 /* Now do the work necessary on the UI thread to reparent the
1172 window. */
1173 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1174 @Override
1175 public void
1176 run ()
1177 {
1178 EmacsWindowAttachmentManager manager;
1179 ViewManager parent;
1180
1181 /* First, detach this window if necessary. */
1182 manager = EmacsWindowAttachmentManager.MANAGER;
1183 manager.detachWindow (EmacsWindow.this);
1184
1185 /* Also unparent this view. */
1186
1187 /* If the window manager is set, use that instead. */
1188 if (windowManager != null)
1189 parent = windowManager;
1190 else
1191 parent = (ViewManager) view.getParent ();
1192 windowManager = null;
1193
1194 if (parent != null)
1195 parent.removeView (view);
1196
1197 /* Next, either add this window as a child of the new
1198 parent's view, or make it available again. */
1199 if (otherWindow != null)
1200 otherWindow.view.addView (view);
1201 else if (EmacsWindow.this.isMapped)
1202 manager.registerWindow (EmacsWindow.this);
1203
1204 /* Request relayout. */
1205 view.requestLayout ();
1206 }
1207 });
1208 }
1209
1210 public void
1211 makeInputFocus (long time)
1212 {
1213 /* TIME is currently ignored. Request the input focus now. */
1214
1215 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1216 @Override
1217 public void
1218 run ()
1219 {
1220 view.requestFocus ();
1221 }
1222 });
1223 }
1224
1225 public synchronized void
1226 raise ()
1227 {
1228 /* This does nothing here. */
1229 if (parent == null)
1230 return;
1231
1232 /* Remove and add this view again. */
1233 parent.children.remove (this);
1234 parent.children.add (this);
1235
1236 /* Request a relayout. */
1237 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1238 @Override
1239 public void
1240 run ()
1241 {
1242 view.raise ();
1243 }
1244 });
1245 }
1246
1247 public synchronized void
1248 lower ()
1249 {
1250 /* This does nothing here. */
1251 if (parent == null)
1252 return;
1253
1254 /* Remove and add this view again. */
1255 parent.children.remove (this);
1256 parent.children.add (this);
1257
1258 /* Request a relayout. */
1259 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1260 @Override
1261 public void
1262 run ()
1263 {
1264 view.lower ();
1265 }
1266 });
1267 }
1268
1269 public synchronized int[]
1270 getWindowGeometry ()
1271 {
1272 int[] array;
1273
1274 array = new int[4];
1275
1276 array[0] = parent != null ? rect.left : xPosition;
1277 array[1] = parent != null ? rect.top : yPosition;
1278 array[2] = rect.width ();
1279 array[3] = rect.height ();
1280
1281 return array;
1282 }
1283
1284 public void
1285 noticeIconified ()
1286 {
1287 EmacsNative.sendIconified (this.handle);
1288 }
1289
1290 public void
1291 noticeDeiconified ()
1292 {
1293 EmacsNative.sendDeiconified (this.handle);
1294 }
1295
1296 public synchronized void
1297 setDontAcceptFocus (final boolean dontAcceptFocus)
1298 {
1299 /* Update the view's focus state. */
1300 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1301 @Override
1302 public void
1303 run ()
1304 {
1305 view.setFocusable (!dontAcceptFocus);
1306 view.setFocusableInTouchMode (!dontAcceptFocus);
1307 }
1308 });
1309 }
1310
1311 public synchronized void
1312 setDontFocusOnMap (final boolean dontFocusOnMap)
1313 {
1314 this.dontFocusOnMap = dontFocusOnMap;
1315 }
1316
1317 public synchronized boolean
1318 getDontFocusOnMap ()
1319 {
1320 return dontFocusOnMap;
1321 }
1322
1323 public int[]
1324 translateCoordinates (int x, int y)
1325 {
1326 int[] array;
1327
1328 /* This is supposed to translate coordinates to the root
1329 window. */
1330 array = new int[2];
1331 EmacsService.SERVICE.getLocationOnScreen (view, array);
1332
1333 /* Now, the coordinates of the view should be in array. Offset X
1334 and Y by them. */
1335 array[0] += x;
1336 array[1] += y;
1337
1338 /* Return the resulting coordinates. */
1339 return array;
1340 }
1341
1342 public void
1343 toggleOnScreenKeyboard (final boolean on)
1344 {
1345 /* Even though InputMethodManager functions are thread safe,
1346 `showOnScreenKeyboard' etc must be called from the UI thread in
1347 order to avoid deadlocks if the calls happen in tandem with a
1348 call to a synchronizing function within
1349 `onCreateInputConnection'. */
1350
1351 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1352 @Override
1353 public void
1354 run ()
1355 {
1356 if (on)
1357 view.showOnScreenKeyboard ();
1358 else
1359 view.hideOnScreenKeyboard ();
1360 }
1361 });
1362 }
1363
1364 public String
1365 lookupString (int eventSerial)
1366 {
1367 String any;
1368
1369 synchronized (eventStrings)
1370 {
1371 any = eventStrings.remove (eventSerial);
1372 }
1373
1374 return any;
1375 }
1376
1377 public void
1378 setFullscreen (final boolean isFullscreen)
1379 {
1380 EmacsService.SERVICE.runOnUiThread (new Runnable () {
1381 @Override
1382 public void
1383 run ()
1384 {
1385 EmacsActivity activity;
1386 Object tem;
1387
1388 fullscreen = isFullscreen;
1389 tem = getAttachedConsumer ();
1390
1391 if (tem != null)
1392 {
1393 activity = (EmacsActivity) tem;
1394 activity.syncFullscreenWith (EmacsWindow.this);
1395 }
1396 }
1397 });
1398 }
1399
1400 public void
1401 defineCursor (final EmacsCursor cursor)
1402 {
1403 /* Don't post this message if pointer icons aren't supported. */
1404
1405 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
1406 view.post (new Runnable () {
1407 @Override
1408 public void
1409 run ()
1410 {
1411 if (cursor != null)
1412 view.setPointerIcon (cursor.icon);
1413 else
1414 view.setPointerIcon (null);
1415 }
1416 });
1417 }
1418
1419 public synchronized void
1420 notifyContentRectPosition (int xPosition, int yPosition)
1421 {
1422 Rect geometry;
1423
1424 /* Ignore these notifications if not a child of the root
1425 window. */
1426 if (parent != null)
1427 return;
1428
1429 /* xPosition and yPosition are the position of this window
1430 relative to the screen. Set them and request a ConfigureNotify
1431 event. */
1432
1433 if (this.xPosition != xPosition
1434 || this.yPosition != yPosition)
1435 {
1436 this.xPosition = xPosition;
1437 this.yPosition = yPosition;
1438
1439 EmacsNative.sendConfigureNotify (this.handle,
1440 System.currentTimeMillis (),
1441 xPosition, yPosition,
1442 rect.width (), rect.height ());
1443 }
1444 }
1445};
diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java
new file mode 100644
index 00000000000..bc96de7fe1a
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java
@@ -0,0 +1,230 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import java.util.ArrayList;
23import java.util.List;
24
25import android.app.ActivityOptions;
26import android.content.Intent;
27import android.os.Build;
28import android.util.Log;
29
30/* Code to paper over the differences in lifecycles between
31 "activities" and windows. There are four interfaces to an instance
32 of this class:
33
34 registerWindowConsumer (WindowConsumer)
35 registerWindow (EmacsWindow)
36 removeWindowConsumer (WindowConsumer)
37 removeWindow (EmacsWindow)
38
39 A WindowConsumer is expected to allow an EmacsWindow to be attached
40 to it, and be created or destroyed.
41
42 Every time a window is created, registerWindow checks the list of
43 window consumers. If a consumer exists and does not currently have
44 a window of its own attached, it gets the new window. Otherwise,
45 the window attachment manager starts a new consumer.
46
47 Every time a consumer is registered, registerWindowConsumer checks
48 the list of available windows. If a window exists and is not
49 currently attached to a consumer, then the consumer gets it.
50
51 Finally, every time a window is removed, the consumer is
52 destroyed. */
53
54public final class EmacsWindowAttachmentManager
55{
56 private final static String TAG = "EmacsWindowAttachmentManager";
57
58 /* The single window attachment manager ``object''. */
59 public static final EmacsWindowAttachmentManager MANAGER;
60
61 static
62 {
63 MANAGER = new EmacsWindowAttachmentManager ();
64 };
65
66 public interface WindowConsumer
67 {
68 public void attachWindow (EmacsWindow window);
69 public EmacsWindow getAttachedWindow ();
70 public void detachWindow ();
71 public void destroy ();
72 };
73
74 /* List of currently attached window consumers. */
75 public List<WindowConsumer> consumers;
76
77 /* List of currently attached windows. */
78 public List<EmacsWindow> windows;
79
80 public
81 EmacsWindowAttachmentManager ()
82 {
83 consumers = new ArrayList<WindowConsumer> ();
84 windows = new ArrayList<EmacsWindow> ();
85 }
86
87 public void
88 registerWindowConsumer (WindowConsumer consumer)
89 {
90 Log.d (TAG, "registerWindowConsumer " + consumer);
91
92 consumers.add (consumer);
93
94 for (EmacsWindow window : windows)
95 {
96 if (window.getAttachedConsumer () == null)
97 {
98 Log.d (TAG, "registerWindowConsumer: attaching " + window);
99 consumer.attachWindow (window);
100 return;
101 }
102 }
103
104 Log.d (TAG, "registerWindowConsumer: sendWindowAction 0, 0");
105 EmacsNative.sendWindowAction ((short) 0, 0);
106 }
107
108 public synchronized void
109 registerWindow (EmacsWindow window)
110 {
111 Intent intent;
112 ActivityOptions options;
113
114 Log.d (TAG, "registerWindow (maybe): " + window);
115
116 if (windows.contains (window))
117 /* The window is already registered. */
118 return;
119
120 Log.d (TAG, "registerWindow: " + window);
121
122 windows.add (window);
123
124 for (WindowConsumer consumer : consumers)
125 {
126 if (consumer.getAttachedWindow () == null)
127 {
128 Log.d (TAG, "registerWindow: attaching " + consumer);
129 consumer.attachWindow (window);
130 return;
131 }
132 }
133
134 intent = new Intent (EmacsService.SERVICE,
135 EmacsMultitaskActivity.class);
136 intent.addFlags (Intent.FLAG_ACTIVITY_NEW_DOCUMENT
137 | Intent.FLAG_ACTIVITY_NEW_TASK
138 | Intent.FLAG_ACTIVITY_MULTIPLE_TASK);
139
140 if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N)
141 EmacsService.SERVICE.startActivity (intent);
142 else
143 {
144 /* Specify the desired window size. */
145 options = ActivityOptions.makeBasic ();
146 options.setLaunchBounds (window.getGeometry ());
147 EmacsService.SERVICE.startActivity (intent,
148 options.toBundle ());
149 }
150
151 Log.d (TAG, "registerWindow: startActivity");
152 }
153
154 public void
155 removeWindowConsumer (WindowConsumer consumer, boolean isFinishing)
156 {
157 EmacsWindow window;
158
159 Log.d (TAG, "removeWindowConsumer " + consumer);
160
161 window = consumer.getAttachedWindow ();
162
163 if (window != null)
164 {
165 Log.d (TAG, "removeWindowConsumer: detaching " + window);
166
167 consumer.detachWindow ();
168 window.onActivityDetached (isFinishing);
169 }
170
171 Log.d (TAG, "removeWindowConsumer: removing " + consumer);
172 consumers.remove (consumer);
173 }
174
175 public synchronized void
176 detachWindow (EmacsWindow window)
177 {
178 WindowConsumer consumer;
179
180 Log.d (TAG, "detachWindow " + window);
181
182 if (window.getAttachedConsumer () != null)
183 {
184 consumer = window.getAttachedConsumer ();
185
186 Log.d (TAG, "detachWindow: removing" + consumer);
187
188 consumers.remove (consumer);
189 consumer.destroy ();
190 }
191
192 windows.remove (window);
193 }
194
195 public void
196 noticeIconified (WindowConsumer consumer)
197 {
198 EmacsWindow window;
199
200 Log.d (TAG, "noticeIconified " + consumer);
201
202 /* If a window is attached, send the appropriate iconification
203 events. */
204 window = consumer.getAttachedWindow ();
205
206 if (window != null)
207 window.noticeIconified ();
208 }
209
210 public void
211 noticeDeiconified (WindowConsumer consumer)
212 {
213 EmacsWindow window;
214
215 Log.d (TAG, "noticeDeiconified " + consumer);
216
217 /* If a window is attached, send the appropriate iconification
218 events. */
219 window = consumer.getAttachedWindow ();
220
221 if (window != null)
222 window.noticeDeiconified ();
223 }
224
225 public synchronized List<EmacsWindow>
226 copyWindows ()
227 {
228 return new ArrayList<EmacsWindow> (windows);
229 }
230};
diff --git a/java/res/drawable/emacs.png b/java/res/drawable/emacs.png
new file mode 100644
index 00000000000..9ab43d704be
--- /dev/null
+++ b/java/res/drawable/emacs.png
Binary files differ
diff --git a/java/res/drawable/emacs_wrench.png b/java/res/drawable/emacs_wrench.png
new file mode 100644
index 00000000000..50572d3bed1
--- /dev/null
+++ b/java/res/drawable/emacs_wrench.png
Binary files differ
diff --git a/java/res/values-v11/style.xml b/java/res/values-v11/style.xml
new file mode 100644
index 00000000000..b114758bf0d
--- /dev/null
+++ b/java/res/values-v11/style.xml
@@ -0,0 +1,24 @@
1<!-- Style resources for GNU Emacs on Android.
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<resources>
21 <!-- Style used for popup menus and relatives on Android 3.x. -->
22 <style name="EmacsStyle" parent="@android:style/Theme.Holo.NoActionBar"/>
23 <style name="EmacsStyleOpen" parent="@android:style/Theme.Holo"/>
24</resources>
diff --git a/java/res/values-v14/style.xml b/java/res/values-v14/style.xml
new file mode 100644
index 00000000000..2cb54dc301b
--- /dev/null
+++ b/java/res/values-v14/style.xml
@@ -0,0 +1,25 @@
1<!-- Style resources for GNU Emacs on Android.
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<resources>
21 <!-- Style used for popup menus and relatives between Android 4.0
22 and Android 10. -->
23 <style name="EmacsStyle" parent="@android:style/Theme.DeviceDefault.NoActionBar"/>
24 <style name="EmacsStyleOpen" parent="@android:style/Theme.DeviceDefault"/>
25</resources>
diff --git a/java/res/values-v19/bool.xml b/java/res/values-v19/bool.xml
new file mode 100644
index 00000000000..a4e3a87ae71
--- /dev/null
+++ b/java/res/values-v19/bool.xml
@@ -0,0 +1,22 @@
1<!-- Boolean resources for GNU Emacs on Android 4.4 or later.
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<resources>
21 <bool name="isAtLeastKitKat">true</bool>
22</resources>
diff --git a/java/res/values-v24/bool.xml b/java/res/values-v24/bool.xml
new file mode 100644
index 00000000000..37f07992995
--- /dev/null
+++ b/java/res/values-v24/bool.xml
@@ -0,0 +1,22 @@
1<!-- Boolean resources for GNU Emacs on Android.
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<resources>
21 <bool name="isBeforeNougat">false</bool>
22</resources>
diff --git a/java/res/values-v29/style.xml b/java/res/values-v29/style.xml
new file mode 100644
index 00000000000..ec7b8d14554
--- /dev/null
+++ b/java/res/values-v29/style.xml
@@ -0,0 +1,32 @@
1<!-- Style resources for GNU Emacs on Android.
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<resources>
21 <!-- Style used for popup menus and relatives from Android 10.0
22 onwards-->
23 <style name="EmacsStyle" parent="@android:style/Theme.DeviceDefault.DayNight">
24 <item name="android:windowActionBar">false</item>
25 <item name="android:windowNoTitle">true</item>
26
27 <!-- Required to make sure the status bar text remains legible. -->
28 <item name="android:statusBarColor">@android:color/black</item>
29 </style>
30 <style name="EmacsStyleOpen"
31 parent="@android:style/Theme.DeviceDefault.DayNight"/>
32</resources>
diff --git a/java/res/values/bool.xml b/java/res/values/bool.xml
new file mode 100644
index 00000000000..2b253824e29
--- /dev/null
+++ b/java/res/values/bool.xml
@@ -0,0 +1,23 @@
1<!-- Boolean resources for GNU Emacs on Android.
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<resources>
21 <bool name="isAtLeastKitKat">false</bool>
22 <bool name="isBeforeNougat">true</bool>
23</resources>
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
new file mode 100644
index 00000000000..8a11cb007ee
--- /dev/null
+++ b/java/res/values/strings.xml
@@ -0,0 +1,45 @@
1<!-- String resources used by GNU Emacs on Android.
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
21 <string name="start_quick_title">
22 Restart Emacs with -Q
23 </string>
24 <string name="start_quick_caption">
25 Restart Emacs, but do not load site lisp or init files.
26 </string>
27 <string name="start_debug_init_title">
28 Restart Emacs with --debug-init
29 </string>
30 <string name="start_debug_init_caption">
31 Restart Emacs, and display the debugger should an error occur while loading initialization files.
32 </string>
33 <string name="erase_dump_title">
34 Delete dump file
35 </string>
36 <string name="erase_dump_caption">
37 Remove the dumped state created when Emacs was installed.
38 </string>
39
40 <!-- This resource describes the purpose of any `sharedUserId'
41 specified at configure-time. -->
42 <string name="shared_user_name">
43 Emacs shared user
44 </string>
45</resources>
diff --git a/java/res/values/style.xml b/java/res/values/style.xml
new file mode 100644
index 00000000000..498e844fda0
--- /dev/null
+++ b/java/res/values/style.xml
@@ -0,0 +1,26 @@
1<!-- Style resources for GNU Emacs on Android.
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<resources>
21 <!-- Style used for popup menus and relatives on Android 2.2 and
22 2.3. Styles used for newer Android versions are defined in
23 the res/values- directories for their respective API levels. -->
24 <style name="EmacsStyle" parent="@android:style/Theme.NoTitleBar"/>
25 <style name="EmacsStyleOpen" parent="@android:style/Theme"/>
26</resources>
diff --git a/java/res/xml/preferences.xml b/java/res/xml/preferences.xml
new file mode 100644
index 00000000000..d52d28816e5
--- /dev/null
+++ b/java/res/xml/preferences.xml
@@ -0,0 +1,30 @@
1<!-- Descriptions for the preferences screen for GNU Emacs on Android.
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android">
21 <Preference android:key="start_quick"
22 android:title="@string/start_quick_title"
23 android:summary="@string/start_quick_caption"/>
24 <Preference android:key="start_debug_init"
25 android:title="@string/start_debug_init_title"
26 android:summary="@string/start_debug_init_caption"/>
27 <Preference android:key="erase_dump"
28 android:title="@string/erase_dump_title"
29 android:summary="@string/erase_dump_caption"/>
30</PreferenceScreen>