diff options
Diffstat (limited to 'java')
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 | |||
| 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 | |||
| 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 @@ | |||
| 1 | Installation instructions for Android | ||
| 2 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 3 | See the end of the file for license conditions. | ||
| 4 | |||
| 5 | Please read the entirety of this file before attempting to build Emacs | ||
| 6 | as an application package which can run on Android devices. | ||
| 7 | |||
| 8 | When building from the source repository, make sure to read | ||
| 9 | INSTALL.REPO as well. | ||
| 10 | |||
| 11 | |||
| 12 | |||
| 13 | Android is an unusual operating system in that program binaries cannot | ||
| 14 | be produced on computers running Android themselves. Instead, they | ||
| 15 | must 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 | ||
| 18 | obtained to build GNU Emacs; after being built, the generated binaries | ||
| 19 | will work on almost all Android devices. This document does not | ||
| 20 | elaborate on how both sets of tools can be obtained. However, for | ||
| 21 | your freedom's sake, you should use the Android SDK provided by the | ||
| 22 | Debian project. | ||
| 23 | |||
| 24 | In addition to the Android SDK and Android NDK, Emacs also requires | ||
| 25 | the Java compiler from OpenJDK 1.7.0 to be installed on your system, | ||
| 26 | along with a working `m4' macro processor. Building on GNU systems is | ||
| 27 | all that is officially supported. We are told that Mac OS works too, | ||
| 28 | and other Unix systems will likely work as well, but MS Windows and | ||
| 29 | Cygwin will not. | ||
| 30 | |||
| 31 | Once all of those tools are obtained, you may invoke the `configure' | ||
| 32 | script 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 | |||
| 38 | Replacing 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 | |||
| 51 | Where the type of CPU can either be `armeabi', `armv7*', `i686', | ||
| 52 | `x86_64', `mips', or `mips64'. | ||
| 53 | |||
| 54 | After the configuration process completes, you may run: | ||
| 55 | |||
| 56 | make all | ||
| 57 | |||
| 58 | Once `make' finishes, there should be a file in the `java' directory | ||
| 59 | named along the lines of: | ||
| 60 | |||
| 61 | emacs-<version>-<api-version>-<abi>.apk | ||
| 62 | |||
| 63 | where <api-version> is the oldest version of Android that the package | ||
| 64 | will run on, and <abi> is the type of Android machine the package was | ||
| 65 | built for. | ||
| 66 | |||
| 67 | The generated package can be uploaded onto an SD card (or similar | ||
| 68 | medium) and installed on-device. | ||
| 69 | |||
| 70 | |||
| 71 | BUILDING WITH OLD NDK VERSIONS | ||
| 72 | |||
| 73 | Building Emacs with an old version of the Android NDK requires special | ||
| 74 | setup. This is because there is no separate C compiler binary for | ||
| 75 | each version of Android in those versions of the NDK. | ||
| 76 | |||
| 77 | Before 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 | |||
| 86 | That information must then be specified as arguments to the NDK C | ||
| 87 | compiler. 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 | |||
| 96 | Where __ANDROID_API__ and the version identifier in | ||
| 97 | "platforms/android-14" defines the version of Android you are building | ||
| 98 | for, and the include directories specify the paths to the relevant | ||
| 99 | Android headers. In addition, it may be necessary to specify | ||
| 100 | "-gdwarf-2", due to a bug in the Android NDK. | ||
| 101 | |||
| 102 | Even older versions of the Android SDK do not require the extra | ||
| 103 | `-isystem' directives. | ||
| 104 | |||
| 105 | Emacs is known to run on Android 2.2 (API version 8) or later, with | ||
| 106 | the NDK r10b or later. We wanted to make Emacs work on even older | ||
| 107 | versions of Android, but they are missing the required JNI graphics | ||
| 108 | library that allows Emacs to display text from C code. | ||
| 109 | |||
| 110 | Due to an extremely nasty bug in the Android 2.2 system, the generated | ||
| 111 | Emacs package cannot be compressed in builds for Android 2.2. As a | ||
| 112 | result, the Emacs package will be approximately 100 megabytes larger | ||
| 113 | than a compressed package for a newer version of Android. | ||
| 114 | |||
| 115 | |||
| 116 | BUILDING C++ DEPENDENCIES | ||
| 117 | |||
| 118 | With a new version of the NDK, dependencies containing C++ code should | ||
| 119 | build without any futher configuration. However, older versions | ||
| 120 | require that you use the ``make_standalone_toolchain.py'' script in | ||
| 121 | the NDK distribution to create a ``standalone toolchain'', and use | ||
| 122 | that instead, in order for C++ headers to be found. | ||
| 123 | |||
| 124 | See https://developer.android.com/ndk/guides/standalone_toolchain for | ||
| 125 | more details; when a ``standalone toolchain'' is specified, the | ||
| 126 | configure script will try to determine the location of the C++ | ||
| 127 | compiler based on the C compiler specified. If that automatic | ||
| 128 | detection does not work, you can specify a C++ compiler yourself, like | ||
| 129 | so: | ||
| 130 | |||
| 131 | ./configure --with-ndk-cxx=/path/to/toolchain/bin/i686-linux-android-g++ | ||
| 132 | |||
| 133 | Some versions of the NDK have a bug, where GCC fails to locate | ||
| 134 | ``stddef.h'' after being copied to a standalone toolchain. To work | ||
| 135 | around this problem (which normally exhibits itself when building C++ | ||
| 136 | code), add: | ||
| 137 | |||
| 138 | -isystem /path/to/toolchain/include/c++/4.9.x | ||
| 139 | |||
| 140 | to ANDROID_CFLAGS. | ||
| 141 | |||
| 142 | |||
| 143 | DEBUG AND RELEASE BUILDS | ||
| 144 | |||
| 145 | Android makes a distinction between ``debug'' and ``release'' builds | ||
| 146 | of applications. With ``release'' builds, the system will apply | ||
| 147 | stronger optimizations to the application at the cost of being unable | ||
| 148 | to debug them with the steps in etc/DEBUG. | ||
| 149 | |||
| 150 | Emacs is built as a debuggable package by default, but: | ||
| 151 | |||
| 152 | ./configure --without-android-debug | ||
| 153 | |||
| 154 | will create a release build of Emacs instead. This may be useful when | ||
| 155 | running Emacs on resource constrained machines. | ||
| 156 | |||
| 157 | If you are building an Emacs package for redistribution, we urge you | ||
| 158 | to provide both debug and release versions. | ||
| 159 | |||
| 160 | |||
| 161 | BUILDING WITH A SHARED USER ID | ||
| 162 | |||
| 163 | Sometimes it may be desirable to build Emacs so that it is able to | ||
| 164 | access executables and application data from another program. To | ||
| 165 | achieve this, that other program must have a ``shared user ID'', and | ||
| 166 | be signed with the same signing key used to sign Emacs (normally | ||
| 167 | `emacs.keystore'.) | ||
| 168 | |||
| 169 | Once you have both that signing key and its ``shared user ID'', you | ||
| 170 | can give it to configure: | ||
| 171 | |||
| 172 | ./configure --with-shared-user-id=MY.SHARED.USER.ID | ||
| 173 | |||
| 174 | For instance, | ||
| 175 | |||
| 176 | ./configure --with-shared-user-id=com.termux | ||
| 177 | |||
| 178 | will result in Termux (https://termux.dev)'s application data being | ||
| 179 | accessible to Emacs, within its own application data directory located | ||
| 180 | at `/data/data/com.termux/files'. | ||
| 181 | |||
| 182 | Don't do this if you already have Emacs installed with a different | ||
| 183 | shared user ID, as the system does not allow programs to change their | ||
| 184 | user IDs after being installed. | ||
| 185 | |||
| 186 | |||
| 187 | BUILDING WITH THIRD PARTY LIBRARIES | ||
| 188 | |||
| 189 | The Android NDK does not support the usual ways of locating third | ||
| 190 | party libraries, especially not via `pkg-config'. Instead, it uses | ||
| 191 | its own system called `ndk-build'. The one exception to this rule is | ||
| 192 | zlib, which is considered a part of the Android OS itself and is | ||
| 193 | available on all devices running Android. | ||
| 194 | |||
| 195 | Android also requires that each application include its own | ||
| 196 | dependencies, as the system makes no guarantee about the existence of | ||
| 197 | any particular library. | ||
| 198 | |||
| 199 | Emacs is not built with the `ndk-build' system. Instead, it is built | ||
| 200 | with Autoconf and Make. | ||
| 201 | |||
| 202 | However, it supports building and including dependencies which use the | ||
| 203 | similarly Make-based `ndk-build' system. | ||
| 204 | |||
| 205 | To use dependencies built through `ndk-build', you must specify a list | ||
| 206 | of directories within which Emacs will search for ``Android.mk'' | ||
| 207 | files, like so: | ||
| 208 | |||
| 209 | ./configure "--with-ndk-path=directory1 directory2" | ||
| 210 | |||
| 211 | If `configure' complains about not being able to find | ||
| 212 | ``libc++_shared.so'', then you must locate that file in your copy of | ||
| 213 | the NDK, and specify it like so: | ||
| 214 | |||
| 215 | ./configure --with-ndk-cxx-shared=/path/to/sysroot/libc++_shared.so | ||
| 216 | |||
| 217 | Emacs will then read the ``Android.mk'' file in each directory, and | ||
| 218 | automatically build and use those modules. | ||
| 219 | |||
| 220 | When building for Intel systems, some ``ndk-build'' modules require | ||
| 221 | the Netwide Assembler, usually installed under ``nasm'', to be present | ||
| 222 | on the system that is building Emacs. | ||
| 223 | |||
| 224 | Google, 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 | ||
| 226 | work, 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 | |||
| 256 | Many of these dependencies have been migrated over to the | ||
| 257 | ``Android.bp'' build system now used to build Android itself. | ||
| 258 | However, the old ``Android.mk'' Makefiles are still present in older | ||
| 259 | branches, and can be easily adapte to newer versions. | ||
| 260 | |||
| 261 | In addition, some Emacs dependencies provide `ndk-build' support | ||
| 262 | themselves: | ||
| 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 | |||
| 269 | Emacs developers have ported the following dependencies to ARM Android | ||
| 270 | systems: | ||
| 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 | |||
| 281 | And other developers have ported the following dependencies to Android | ||
| 282 | systems: | ||
| 283 | |||
| 284 | ImageMagick, lcms2 - https://github.com/MolotovCherry/Android-ImageMagick7 | ||
| 285 | (Please see the section IMAGEMAGICK near the end of this file.) | ||
| 286 | |||
| 287 | We anticipate that most untested non-trivial ndk-build dependencies | ||
| 288 | will need adjustments in Emacs to work, as the Emacs build system | ||
| 289 | which emulates ndk-build is in an extremely early state. | ||
| 290 | |||
| 291 | |||
| 292 | GNUTLS | ||
| 293 | |||
| 294 | Modified copies of GnuTLS and its dependencies (such as libgmp, | ||
| 295 | libtasn1, p11-kit) which can be built with the ndk-build system can be | ||
| 296 | found at https://sourceforge.net/projects/android-ports-for-gnu-emacs. | ||
| 297 | |||
| 298 | They have only been tested on arm64 Android systems running Android | ||
| 299 | 5.0 or later, and armv7l systems running Android 13 or later, so your | ||
| 300 | mileage may vary, especially if you are trying to build Emacs for | ||
| 301 | another kind of machine. | ||
| 302 | |||
| 303 | To build Emacs with GnuTLS, you must unpack each of the following tar | ||
| 304 | archives 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 | |||
| 312 | and add the resulting folders to ``--with-ndk-path''. Note that you | ||
| 313 | should not try to build these packages separately using any | ||
| 314 | `configure' script or Makefiles inside. | ||
| 315 | |||
| 316 | |||
| 317 | TREE-SITTER | ||
| 318 | |||
| 319 | A copy of tree-sitter modified to build with the ndk-build system can | ||
| 320 | also be found that URL. To build Emacs with tree-sitter, you must | ||
| 321 | unpack the following tar archive in that site: | ||
| 322 | |||
| 323 | tree-sitter-0.20.7-emacs.tar.gz | ||
| 324 | |||
| 325 | and add the resulting folder to ``--with-ndk-build''. | ||
| 326 | |||
| 327 | |||
| 328 | HARFBUZZ | ||
| 329 | |||
| 330 | A copy of HarfBuzz modified to build with the ndk-build system can | ||
| 331 | also be found at that URL. To build Emacs with HarfBuzz, you must | ||
| 332 | unpack the following tar archive in that site: | ||
| 333 | |||
| 334 | harfbuzz-7.1.0-emacs.tar.gz | ||
| 335 | |||
| 336 | and add the resulting folder to ``--with-ndk-build''. | ||
| 337 | |||
| 338 | |||
| 339 | IMAGEMAGICK | ||
| 340 | |||
| 341 | There is a third party port of ImageMagick to Android. Unfortunately, | ||
| 342 | the port also uses its own patched versions of libpng, libjpeg, | ||
| 343 | libtiff and libwebp, which conflict with those used by Emacs. Its | ||
| 344 | Makefiles were also written for MS Windows, so you must also apply the | ||
| 345 | patch at the end of this file. | ||
| 346 | |||
| 347 | |||
| 348 | |||
| 349 | PATCH FOR LIBXML2 | ||
| 350 | |||
| 351 | This patch must be applied to the Android.mk in Google's version of | ||
| 352 | libxml2 before it can be built for Emacs. In addition, you must also | ||
| 353 | revert the commit `edb5870767fed8712a9b77ef34097209b61ab2db'. | ||
| 354 | |||
| 355 | diff --git a/Android.mk b/Android.mk | ||
| 356 | index 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 | |||
| 374 | PATCH FOR ICU | ||
| 375 | |||
| 376 | This patch must be applied to icu4j/Android.mk in Google's version of | ||
| 377 | icu before it can be built for Emacs. | ||
| 378 | |||
| 379 | diff --git a/icu4j/Android.mk b/icu4j/Android.mk | ||
| 380 | index 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 | |||
| 393 | diff --git a/icu4c/source/common/Android.mk b/icu4c/source/common/Android.mk | ||
| 394 | index 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 | |||
| 407 | PATCH FOR SQLITE3 | ||
| 408 | |||
| 409 | diff --git a/dist/Android.mk b/dist/Android.mk | ||
| 410 | index 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 | |||
| 422 | diff --git a/dist/sqlite3.c b/dist/sqlite3.c | ||
| 423 | index 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 | |||
| 436 | PATCH FOR WEBP | ||
| 437 | |||
| 438 | diff --git a/Android.mk b/Android.mk | ||
| 439 | index 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 | |||
| 457 | PATCHES FOR SELINUX | ||
| 458 | |||
| 459 | diff --git a/Android.mk b/Android.mk | ||
| 460 | index 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 | |||
| 472 | diff --git a/src/android.c b/src/android.c | ||
| 473 | index 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 | |||
| 520 | PATCH FOR BORINGSSL | ||
| 521 | |||
| 522 | diff --git a/Android.mk b/Android.mk | ||
| 523 | index 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 | ||
| 536 | diff --git a/sources.mk b/sources.mk | ||
| 537 | index 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 | |||
| 576 | PATCH FOR IMAGEMAGICK | ||
| 577 | |||
| 578 | diff --git a/Android.mk b/Android.mk | ||
| 579 | index 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 | |||
| 623 | diff --git a/libjpeg-turbo-2.0.2/jconfig.h b/libjpeg-turbo-2.0.2/jconfig.h | ||
| 624 | index 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 | - | ||
| 686 | diff --git a/libxml2-2.9.9/encoding.c b/libxml2-2.9.9/encoding.c | ||
| 687 | index 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 | } | ||
| 749 | diff --git a/libxml2-2.9.9/xpath.c b/libxml2-2.9.9/xpath.c | ||
| 750 | index 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 | } | ||
| 770 | diff --git a/make/libicu4c.mk b/make/libicu4c.mk | ||
| 771 | index 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) | ||
| 783 | diff --git a/make/libjpeg-turbo.mk b/make/libjpeg-turbo.mk | ||
| 784 | index 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) | ||
| 828 | diff --git a/make/liblcms2.mk b/make/liblcms2.mk | ||
| 829 | index 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 \ | ||
| 843 | diff --git a/make/libmagick++-7.mk b/make/libmagick++-7.mk | ||
| 844 | index 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 := \ | ||
| 856 | diff --git a/make/libmagickcore-7.mk b/make/libmagickcore-7.mk | ||
| 857 | index 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 | |||
| 880 | diff --git a/make/libmagickwand-7.mk b/make/libmagickwand-7.mk | ||
| 881 | index 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 := \ | ||
| 923 | diff --git a/make/libpng.mk b/make/libpng.mk | ||
| 924 | index 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 += \ | ||
| 935 | diff --git a/make/libtiff.mk b/make/libtiff.mk | ||
| 936 | index 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 | ||
| 949 | diff --git a/make/magick.mk b/make/magick.mk | ||
| 950 | index 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 | |||
| 964 | This file is part of GNU Emacs. | ||
| 965 | |||
| 966 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 967 | it under the terms of the GNU General Public License as published by | ||
| 968 | the Free Software Foundation, either version 3 of the License, or | ||
| 969 | (at your option) any later version. | ||
| 970 | |||
| 971 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 972 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 973 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 974 | GNU General Public License for more details. | ||
| 975 | |||
| 976 | You should have received a copy of the GNU General Public License | ||
| 977 | along 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 | |||
| 20 | top_builddir = @top_builddir@ | ||
| 21 | top_srcdir = @top_srcdir@ | ||
| 22 | srcdir = @srcdir@ | ||
| 23 | builddir = @builddir@ | ||
| 24 | version = @version@ | ||
| 25 | |||
| 26 | # Don't install movemail if mailutils are to be used. | ||
| 27 | emacs_use_mailutils = @emacs_use_mailutils@ | ||
| 28 | |||
| 29 | # This is the host lib-src and lib, not the cross compiler's lib-src. | ||
| 30 | libsrc = ../lib-src | ||
| 31 | EXEEXT = @EXEEXT@ | ||
| 32 | |||
| 33 | -include ${top_builddir}/src/verbose.mk | ||
| 34 | |||
| 35 | SHELL = @SHELL@ | ||
| 36 | JAVAC = @JAVAC@ | ||
| 37 | AAPT = @AAPT@ | ||
| 38 | D8 = @D8@ | ||
| 39 | ZIPALIGN = @ZIPALIGN@ | ||
| 40 | JARSIGNER = @JARSIGNER@ | ||
| 41 | APKSIGNER = @APKSIGNER@ | ||
| 42 | JARSIGNER_FLAGS = | ||
| 43 | ANDROID_JAR = @ANDROID_JAR@ | ||
| 44 | ANDROID_ABI = @ANDROID_ABI@ | ||
| 45 | ANDROID_SDK_18_OR_EARLIER = @ANDROID_SDK_18_OR_EARLIER@ | ||
| 46 | ANDROID_SDK_8_OR_EARLIER = @ANDROID_SDK_8_OR_EARLIER@ | ||
| 47 | WARN_JAVAFLAGS = @WARN_JAVAFLAGS@ | ||
| 48 | JAVAFLAGS = $(WARN_JAVAFLAGS) -classpath "$(ANDROID_JAR):$(srcdir)" | ||
| 49 | FIND_DELETE = @FIND_DELETE@ | ||
| 50 | |||
| 51 | # Android 4.3 and earlier require Emacs to be signed with a different | ||
| 52 | # digital signature algorithm. | ||
| 53 | |||
| 54 | ifneq (,$(ANDROID_SDK_18_OR_EARLIER)) | ||
| 55 | JARSIGNER_FLAGS = -sigalg MD5withRSA -digestalg SHA1 | ||
| 56 | else | ||
| 57 | JARSIGNER_FLAGS = | ||
| 58 | endif | ||
| 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 | |||
| 64 | ifneq (,$(ANDROID_SDK_8_OR_EARLIER)) | ||
| 65 | AAPT_ASSET_ARGS = -0 "" | ||
| 66 | else | ||
| 67 | AAPT_ASSET_ARGS = | ||
| 68 | endif | ||
| 69 | |||
| 70 | SIGN_EMACS = -keystore $(srcdir)/emacs.keystore -storepass \ | ||
| 71 | emacs1 $(JARSIGNER_FLAGS) | ||
| 72 | SIGN_EMACS_V2 = sign --v2-signing-enabled --ks \ | ||
| 73 | $(srcdir)/emacs.keystore -debuggable-apk-permitted \ | ||
| 74 | --ks-pass pass:emacs1 | ||
| 75 | |||
| 76 | JAVA_FILES := $(wildcard $(srcdir)/org/gnu/emacs/*.java) | ||
| 77 | RESOURCE_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. | ||
| 84 | RESOURCE_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. | ||
| 90 | CLASS_FILES := $(foreach file,$(JAVA_FILES),$(basename $(file)).class) | ||
| 91 | |||
| 92 | # Remove RESOURCE_FILE from JAVA_FILES, if it is already present. | ||
| 93 | JAVA_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 | |||
| 98 | ANDROID_MIN_SDK := @ANDROID_MIN_SDK@ | ||
| 99 | APK_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 | ||
| 118 | all: $(APK_NAME) | ||
| 119 | |||
| 120 | # Binaries to cross-compile. | ||
| 121 | CROSS_SRC_BINS := $(top_builddir)/cross/src/android-emacs | ||
| 122 | CROSS_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 | ||
| 127 | CROSS_LIBSRC_BINS_MOVEMAIL := $(top_builddir)/cross/lib-src/movemail | ||
| 128 | CROSS_EXEC_BINS := $(top_builddir)/exec/exec1 $(top_builddir)/exec/loader | ||
| 129 | CROSS_BINS = $(CROSS_SRC_BINS) $(CROSS_LIBSRC_BINS) $(CROSS_EXEC_BINS) | ||
| 130 | |||
| 131 | ifneq ($(emacs_use_mailutils),yes) | ||
| 132 | CROSS_LIBSRC_BINS := $(CROSS_LIBSRC_BINS) $(CROSS_LIBSRC_BINS_MOVEMAIL) | ||
| 133 | endif | ||
| 134 | |||
| 135 | # Libraries to cross-compile. | ||
| 136 | CROSS_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. | ||
| 141 | CROSS_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 | ||
| 182 | install_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 | ||
| 230 | ifneq ($(NDK_BUILD_SHARED),) | ||
| 231 | $(AM_V_SILENT) cp -f $(NDK_BUILD_SHARED) \ | ||
| 232 | install_temp/lib/$(ANDROID_ABI) | ||
| 233 | endif | ||
| 234 | |||
| 235 | install_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 | |||
| 241 | install_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 | |||
| 247 | install_temp/assets/build_info: install_temp | ||
| 248 | $(AM_V_GEN) { hostname; date +%s; } > $@ | ||
| 249 | |||
| 250 | emacs.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 $@) | ||
| 270 | Makefile: $(top_srcdir)/config.status $(top_srcdir)/java/Makefile.in | ||
| 271 | $(MAKE) -C .. java/$@ | ||
| 272 | |||
| 273 | # AndroidManifest.xml: | ||
| 274 | AndroidManifest.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 | |||
| 295 | classes.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 | |||
| 319 | ETAGS = $(top_builddir)/lib-src/etags | ||
| 320 | |||
| 321 | $(ETAGS): FORCE | ||
| 322 | $(MAKE) -C ../lib-src $(notdir $@) | ||
| 323 | |||
| 324 | tagsfiles = $(JAVA_FILES) $(RESOURCE_FILE) | ||
| 325 | |||
| 326 | .PHONY: tags FORCE | ||
| 327 | tags: TAGS | ||
| 328 | TAGS: $(ETAGS) $(tagsfiles) | ||
| 329 | $(AM_V_GEN) $(ETAGS) $(tagsfiles) | ||
| 330 | |||
| 331 | clean: | ||
| 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 | |||
| 336 | maintainer-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 @@ | |||
| 1 | This directory holds the Java sources of the port of GNU Emacs to | ||
| 2 | Android-like systems, along with files needed to create an application | ||
| 3 | package out of them. If you need to build this port, please read the | ||
| 4 | file INSTALL in this directory. | ||
| 5 | |||
| 6 | The ``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 | ||
| 10 | sources to the system. | ||
| 11 | |||
| 12 | The ``res'' directory contains resources, mainly the Emacs icon and | ||
| 13 | several ``boolean resources'' which are used as a form of conditional | ||
| 14 | evaluation for manifest entries. | ||
| 15 | |||
| 16 | `emacs.keystore' is the signing key used to build Emacs. It is kept | ||
| 17 | here, and we encourage all people redistributing Emacs to use this | ||
| 18 | key. It holds no security value, and otherwise it will be impossible | ||
| 19 | to install different builds of Emacs on top of each other. | ||
| 20 | |||
| 21 | Please keep the Java code indented with tabs and formatted according | ||
| 22 | to the rules for C code in the GNU coding standards. Always use | ||
| 23 | C-style comments. | ||
| 24 | |||
| 25 | ====================================================================== | ||
| 26 | |||
| 27 | OVERVIEW OF JAVA | ||
| 28 | |||
| 29 | Emacs developers do not know Java, and there is no reason they should | ||
| 30 | have to. Thus, the code in this directory is confined to what is | ||
| 31 | strictly necessary to support Emacs, and only uses a subset of Java | ||
| 32 | written in a way that is easily understandable to C programmers. | ||
| 33 | |||
| 34 | Java is required because the entire Android runtime is based around | ||
| 35 | Java, and there is no way to write an Android program which runs | ||
| 36 | without Java. | ||
| 37 | |||
| 38 | This text exists to prime other Emacs developers, already familar with | ||
| 39 | C, on the basic architecture of the Android port, and to teach them | ||
| 40 | how to read and write the Java code found in this directory. | ||
| 41 | |||
| 42 | Java is an object oriented language with automatic memory management | ||
| 43 | compiled down to bytecode, which is then subject to interpretation by | ||
| 44 | a Java virtual machine. | ||
| 45 | |||
| 46 | What that means, is that: | ||
| 47 | |||
| 48 | struct emacs_window | ||
| 49 | { | ||
| 50 | int some_fields; | ||
| 51 | int of_emacs_window; | ||
| 52 | }; | ||
| 53 | |||
| 54 | static void | ||
| 55 | do_something_with_emacs_window (struct emacs_window *a, int n) | ||
| 56 | { | ||
| 57 | a->some_fields = a->of_emacs_window + n; | ||
| 58 | } | ||
| 59 | |||
| 60 | would be written: | ||
| 61 | |||
| 62 | public 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 | |||
| 74 | and instead of doing: | ||
| 75 | |||
| 76 | do_something_with_emacs_window (my_window, 1); | ||
| 77 | |||
| 78 | you say: | ||
| 79 | |||
| 80 | myWindow.doSomething (1); | ||
| 81 | |||
| 82 | In addition to functions associated with an object of a given class | ||
| 83 | (such as EmacsWindow), Java also has two other kinds of functions. | ||
| 84 | |||
| 85 | The first are so-called ``static'' functions (the static means | ||
| 86 | something entirely different from what it does in C.) | ||
| 87 | |||
| 88 | A static function, while still having to be defined within a class, | ||
| 89 | can be called without any object. Instead of the object, you write | ||
| 90 | the name of the Java class within which it is defined. For example, | ||
| 91 | the following C code: | ||
| 92 | |||
| 93 | int | ||
| 94 | multiply_a_with_b_and_then_add_c (int a, int b, int c) | ||
| 95 | { | ||
| 96 | return a * b + c; | ||
| 97 | } | ||
| 98 | |||
| 99 | would be: | ||
| 100 | |||
| 101 | public class EmacsSomething | ||
| 102 | { | ||
| 103 | public static int | ||
| 104 | multiplyAWithBAndThenAddC (int a, int b, int c) | ||
| 105 | { | ||
| 106 | return a * b + c; | ||
| 107 | } | ||
| 108 | }; | ||
| 109 | |||
| 110 | Then, instead of calling: | ||
| 111 | |||
| 112 | int foo; | ||
| 113 | |||
| 114 | foo = multiply_a_with_b_then_add_c (1, 2, 3); | ||
| 115 | |||
| 116 | you say: | ||
| 117 | |||
| 118 | int foo; | ||
| 119 | |||
| 120 | foo = EmacsSomething.multiplyAWithBAndThenAddC (1, 2, 3); | ||
| 121 | |||
| 122 | In Java, ``static'' does not mean that the function is only used | ||
| 123 | within its compilation unit! Instead, the ``private'' qualifier is | ||
| 124 | used to mean more or less the same thing: | ||
| 125 | |||
| 126 | static void | ||
| 127 | this_procedure_is_only_used_within_this_file (void) | ||
| 128 | { | ||
| 129 | do_something (); | ||
| 130 | } | ||
| 131 | |||
| 132 | becomes | ||
| 133 | |||
| 134 | public class EmacsSomething | ||
| 135 | { | ||
| 136 | private static void | ||
| 137 | thisProcedureIsOnlyUsedWithinThisClass () | ||
| 138 | { | ||
| 139 | |||
| 140 | } | ||
| 141 | } | ||
| 142 | |||
| 143 | the other kind are called ``constructors''. They are functions that | ||
| 144 | must be called to allocate memory to hold a class: | ||
| 145 | |||
| 146 | public class EmacsFoo | ||
| 147 | { | ||
| 148 | int bar; | ||
| 149 | |||
| 150 | public | ||
| 151 | EmacsFoo (int tokenA, int tokenB) | ||
| 152 | { | ||
| 153 | bar = tokenA + tokenB; | ||
| 154 | } | ||
| 155 | } | ||
| 156 | |||
| 157 | now, the following statement: | ||
| 158 | |||
| 159 | EmacsFoo foo; | ||
| 160 | |||
| 161 | foo = new EmacsFoo (1, 2); | ||
| 162 | |||
| 163 | becomes more or less equivalent to the following C code: | ||
| 164 | |||
| 165 | struct emacs_foo | ||
| 166 | { | ||
| 167 | int bar; | ||
| 168 | }; | ||
| 169 | |||
| 170 | struct emacs_foo * | ||
| 171 | make_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 | |||
| 183 | struct emacs_foo *foo; | ||
| 184 | |||
| 185 | foo = make_emacs_foo (1, 2); | ||
| 186 | |||
| 187 | A class may have any number of constructors, or no constructors at | ||
| 188 | all, in which case the compiler inserts an empty constructor. | ||
| 189 | |||
| 190 | |||
| 191 | |||
| 192 | Sometimes, 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 | |||
| 204 | This is Java's version of GCC's nested function extension. The major | ||
| 205 | difference is that the nested function may still be called even after | ||
| 206 | it goes out of scope, and always retains a reference to the class and | ||
| 207 | local variables around where it was called. | ||
| 208 | |||
| 209 | Being an object-oriented language, Java also allows defining that a | ||
| 210 | class ``extends'' another class. The following C code: | ||
| 211 | |||
| 212 | struct a | ||
| 213 | { | ||
| 214 | long thirty_two; | ||
| 215 | }; | ||
| 216 | |||
| 217 | struct b | ||
| 218 | { | ||
| 219 | struct a a; | ||
| 220 | long long sixty_four; | ||
| 221 | }; | ||
| 222 | |||
| 223 | extern void do_something (struct a *); | ||
| 224 | |||
| 225 | void | ||
| 226 | my_function (struct b *b) | ||
| 227 | { | ||
| 228 | do_something (&b->a); | ||
| 229 | } | ||
| 230 | |||
| 231 | is roughly equivalent to the following Java code, split into two | ||
| 232 | files: | ||
| 233 | |||
| 234 | A.java | ||
| 235 | |||
| 236 | public class A | ||
| 237 | { | ||
| 238 | int thirtyTwo; | ||
| 239 | |||
| 240 | public void | ||
| 241 | doSomething () | ||
| 242 | { | ||
| 243 | etcEtcEtc (); | ||
| 244 | } | ||
| 245 | }; | ||
| 246 | |||
| 247 | B.java | ||
| 248 | |||
| 249 | public 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 | |||
| 260 | the Java runtime has transformed the call to ``b.doSomething'' to | ||
| 261 | ``((A) b).doSomething''. | ||
| 262 | |||
| 263 | However, Java also allows overriding this behavior, by specifying the | ||
| 264 | @Override keyword: | ||
| 265 | |||
| 266 | public 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 | |||
| 279 | now, any call to ``doSomething'' on a ``B'' created using ``new B ()'' | ||
| 280 | will end up calling ``Something.doSomethingTwo'', before calling back | ||
| 281 | to ``A.doSomething''. This override also applies in reverse; that is | ||
| 282 | to say, even if you write: | ||
| 283 | |||
| 284 | ((A) b).doSomething (); | ||
| 285 | |||
| 286 | B's version of doSomething will still be called, if ``b'' was created | ||
| 287 | using ``new B ()''. | ||
| 288 | |||
| 289 | This mechanism is used extensively throughout the Java language and | ||
| 290 | Android windowing APIs. | ||
| 291 | |||
| 292 | Elsewhere, you will encounter Java code that defines arrays: | ||
| 293 | |||
| 294 | public 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 | |||
| 306 | Java arrays are similar to C arrays in that they can not grow. But | ||
| 307 | they are very much unlike C arrays in that they are always references | ||
| 308 | (as opposed to decaying into pointers in only some situations), and | ||
| 309 | contain information about their length. | ||
| 310 | |||
| 311 | If another function named ``frobinicate1'' takes an array as an | ||
| 312 | argument, then it need not take the length of the array. | ||
| 313 | |||
| 314 | Instead, it may simply iterate over the array like so: | ||
| 315 | |||
| 316 | int i, k; | ||
| 317 | |||
| 318 | for (i = 0; i < array.length; ++i) | ||
| 319 | { | ||
| 320 | k = array[i]; | ||
| 321 | |||
| 322 | Whatever.doSomethingWithK (k); | ||
| 323 | } | ||
| 324 | |||
| 325 | The syntax used to define arrays is also slightly different. As | ||
| 326 | arrays are always references, there is no way for you to tell the | ||
| 327 | runtime to allocate an array of size N in a structure (class.) | ||
| 328 | |||
| 329 | Instead, if you need an array of that size, you must declare a field | ||
| 330 | with the type of the array, and allocate the array inside the class's | ||
| 331 | constructor, like so: | ||
| 332 | |||
| 333 | public class EmacsArrayContainer | ||
| 334 | { | ||
| 335 | public int[] myArray; | ||
| 336 | |||
| 337 | public | ||
| 338 | EmacsArrayContainer () | ||
| 339 | { | ||
| 340 | myArray = new array[10]; | ||
| 341 | } | ||
| 342 | } | ||
| 343 | |||
| 344 | while in C, you could just have written: | ||
| 345 | |||
| 346 | struct emacs_array_container | ||
| 347 | { | ||
| 348 | int my_array[10]; | ||
| 349 | }; | ||
| 350 | |||
| 351 | or, possibly even better, | ||
| 352 | |||
| 353 | typedef int emacs_array_container[10]; | ||
| 354 | |||
| 355 | Alas, Java has no equivalent of `typedef'. | ||
| 356 | |||
| 357 | Like in C, Java string literals are delimited by double quotes. | ||
| 358 | Unlike C, however, strings are not NULL-terminated arrays of | ||
| 359 | characters, but a distinct type named ``String''. They store their | ||
| 360 | own length, characters in Java's 16-bit ``char'' type, and are capable | ||
| 361 | of holding NULL bytes. | ||
| 362 | |||
| 363 | Instead of writing: | ||
| 364 | |||
| 365 | wchar_t character; | ||
| 366 | extern char *s; | ||
| 367 | size_t s; | ||
| 368 | |||
| 369 | for (/* determine n, s in a loop. */) | ||
| 370 | s += mbstowc (&character, s, n); | ||
| 371 | |||
| 372 | or: | ||
| 373 | |||
| 374 | const char *byte; | ||
| 375 | |||
| 376 | for (byte = my_string; *byte; ++byte) | ||
| 377 | /* do something with *byte. */; | ||
| 378 | |||
| 379 | or perhaps even: | ||
| 380 | |||
| 381 | size_t length, i; | ||
| 382 | char foo; | ||
| 383 | |||
| 384 | length = strlen (my_string); | ||
| 385 | |||
| 386 | for (i = 0; i < length; ++i) | ||
| 387 | foo = my_string[i]; | ||
| 388 | |||
| 389 | you write: | ||
| 390 | |||
| 391 | char foo; | ||
| 392 | int i; | ||
| 393 | |||
| 394 | for (i = 0; i < myString.length (); ++i) | ||
| 395 | foo = myString.charAt (0); | ||
| 396 | |||
| 397 | Java also has stricter rules on what can be used as a truth value in a | ||
| 398 | conditional. While in C, any non-zero value is true, Java requires | ||
| 399 | that every truth value be of the boolean type ``boolean''. | ||
| 400 | |||
| 401 | What this means is that instead of simply writing: | ||
| 402 | |||
| 403 | if (foo || bar) | ||
| 404 | |||
| 405 | where foo can either be 1 or 0, and bar can either be NULL or a | ||
| 406 | pointer to something, you must explicitly write: | ||
| 407 | |||
| 408 | if (foo != 0 || bar != null) | ||
| 409 | |||
| 410 | in Java. | ||
| 411 | |||
| 412 | JAVA NATIVE INTERFACE | ||
| 413 | |||
| 414 | Java also provides an interface for C code to interface with Java. | ||
| 415 | |||
| 416 | C functions exported from a shared library become static Java | ||
| 417 | functions within a class, like so: | ||
| 418 | |||
| 419 | public 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 | |||
| 457 | Where the corresponding C functions are located in android.c, and | ||
| 458 | loaded by the special invocation: | ||
| 459 | |||
| 460 | static | ||
| 461 | { | ||
| 462 | System.loadLibrary ("emacs"); | ||
| 463 | }; | ||
| 464 | |||
| 465 | where ``static'' defines a section of code which will be run upon the | ||
| 466 | object (containing class) being loaded. This is like: | ||
| 467 | |||
| 468 | __attribute__((constructor)) | ||
| 469 | |||
| 470 | on systems where shared object constructors are supported. | ||
| 471 | |||
| 472 | See http://docs.oracle.com/en/java/javase/19/docs/specs/jni/intro.html | ||
| 473 | for more details. | ||
| 474 | |||
| 475 | |||
| 476 | |||
| 477 | OVERVIEW OF ANDROID | ||
| 478 | |||
| 479 | When the Android system starts an application, it does not actually | ||
| 480 | call the application's ``main'' function. It may not even start the | ||
| 481 | application's process if one is already running. | ||
| 482 | |||
| 483 | Instead, Android is organized around components. When the user opens | ||
| 484 | the ``Emacs'' icon, the Android system looks up and starts the | ||
| 485 | component associated with the ``Emacs'' icon. In this case, the | ||
| 486 | component is called an activity, and is declared in | ||
| 487 | the 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 | |||
| 501 | This tells Android to start the activity defined in ``EmacsActivity'' | ||
| 502 | (defined in org/gnu/emacs/EmacsActivity.java), a class extending the | ||
| 503 | Android class ``Activity''. | ||
| 504 | |||
| 505 | To do so, the Android system creates an instance of ``EmacsActivity'' | ||
| 506 | and the window system window associated with it, and eventually calls: | ||
| 507 | |||
| 508 | Activity activity; | ||
| 509 | |||
| 510 | activity.onCreate (...); | ||
| 511 | |||
| 512 | But which ``onCreate'' is really called? | ||
| 513 | It is actually the ``onCreate'' defined in EmacsActivity.java, as | ||
| 514 | it 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 | |||
| 523 | Then, this is what happens step-by-step within the ``onCreate'' | ||
| 524 | function: | ||
| 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 | |||
| 532 | Here, Emacs obtains the intent (a request to start a component) which | ||
| 533 | was used to start Emacs, and sets a special flag if it contains a | ||
| 534 | request 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 | |||
| 543 | Next, Emacs sets an appropriate theme for the activity's associated | ||
| 544 | window 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 | |||
| 556 | Then, Emacs creates a ``FrameLayout'', a widget that holds a single | ||
| 557 | other widget, and makes it the activity's ``content view''. | ||
| 558 | |||
| 559 | The activity itself is a ``FrameLayout'', so the ``layout parameters'' | ||
| 560 | here apply to the FrameLayout itself, and not its children. | ||
| 561 | |||
| 562 | /* Maybe start the Emacs service if necessary. */ | ||
| 563 | EmacsService.startEmacsService (this); | ||
| 564 | |||
| 565 | And after that, Emacs calls the static function ``startEmacsService'', | ||
| 566 | defined in the class ``EmacsService''. This starts the Emacs service | ||
| 567 | component if necessary. | ||
| 568 | |||
| 569 | /* Add this activity to the list of available activities. */ | ||
| 570 | EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this); | ||
| 571 | |||
| 572 | super.onCreate (savedInstanceState); | ||
| 573 | |||
| 574 | Finally, Emacs registers that this activity is now ready to receive | ||
| 575 | top-level frames (windows) created from Lisp. | ||
| 576 | |||
| 577 | Activities come and go, but Emacs has to stay running in the mean | ||
| 578 | time. Thus, Emacs also defines a ``service'', which is a long-running | ||
| 579 | component that the Android system allows to run in the background. | ||
| 580 | |||
| 581 | Let 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 | |||
| 600 | If ``EmacsService.SERVICE'' does not yet exist, what this does is to | ||
| 601 | tell the ``context'' (the equivalent of an Xlib Display *) to start a | ||
| 602 | service defined by the class ``EmacsService''. Eventually, this | ||
| 603 | results 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 | |||
| 615 | Here is what this function does, step-by-step: | ||
| 616 | |||
| 617 | SERVICE = this; | ||
| 618 | |||
| 619 | First, it sets the special static variable ``SERVICE'' to ``this'', | ||
| 620 | which is a pointer to the ``EmacsService' object that was created. | ||
| 621 | |||
| 622 | handler = new Handler (Looper.getMainLooper ()); | ||
| 623 | |||
| 624 | Next, it creates a ``Handler'' object for the ``main looper''. | ||
| 625 | This is a helper structure which allows executing code on the Android | ||
| 626 | user interface thread. | ||
| 627 | |||
| 628 | manager = getAssets (); | ||
| 629 | app_context = getApplicationContext (); | ||
| 630 | metrics = getResources ().getDisplayMetrics (); | ||
| 631 | pixelDensityX = metrics.xdpi; | ||
| 632 | pixelDensityY = metrics.ydpi; | ||
| 633 | |||
| 634 | Finally, 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 | |||
| 645 | Then, 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 | |||
| 655 | It obtains the names of the Emacs home, shared library, and temporary | ||
| 656 | file 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 | |||
| 663 | The name of the Emacs application package. | ||
| 664 | |||
| 665 | Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir | ||
| 666 | + ", libDir = " + libDir + ", and classPath = " + classPath); | ||
| 667 | |||
| 668 | Prints a debug message to the Android system log with this | ||
| 669 | information. | ||
| 670 | |||
| 671 | EmacsNative.setEmacsParams (manager, filesDir, libDir, | ||
| 672 | cacheDir, (float) pixelDensityX, | ||
| 673 | (float) pixelDensityY, | ||
| 674 | classPath, this); | ||
| 675 | |||
| 676 | And calls the native function ``setEmacsParams'' (defined in | ||
| 677 | android.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 | |||
| 683 | Then, it allocates an ``EmacsThread'' object, and starts that thread. | ||
| 684 | Inside that thread is where Emacs's C code runs. | ||
| 685 | |||
| 686 | } | ||
| 687 | catch (IOException exception) | ||
| 688 | { | ||
| 689 | EmacsNative.emacsAbort (); | ||
| 690 | return; | ||
| 691 | |||
| 692 | And here is the purpose of the ``try'' block. Functions related to | ||
| 693 | file names in Java will signal errors of various types upon failure. | ||
| 694 | |||
| 695 | This ``catch'' block means that the Java virtual machine will abort | ||
| 696 | execution of the contents of the ``try'' block as soon as an error of | ||
| 697 | type ``IOException'' is encountered, and begin executing the contents | ||
| 698 | of the ``catch'' block. | ||
| 699 | |||
| 700 | Any failure of that type here is a crash, and | ||
| 701 | ``EmacsNative.emacsAbort'' is called to quickly abort the process to | ||
| 702 | get a useful backtrace. | ||
| 703 | } | ||
| 704 | } | ||
| 705 | |||
| 706 | Now, let us look at the definition of the class ``EmacsThread'', found | ||
| 707 | in org/gnu/emacs/EmacsThread.java: | ||
| 708 | |||
| 709 | public 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 | |||
| 737 | The class itself defines a single field, ``startDashQ'', a constructor | ||
| 738 | with an unused argument of the type ``EmacsService'' (which is useful | ||
| 739 | while debugging) and a flag ``startDashQ'', and a single function | ||
| 740 | ``run'', overriding the same function in the class ``Thread''. | ||
| 741 | |||
| 742 | When ``thread.start'' is called, the Java virtual machine creates a | ||
| 743 | new thread, and then calls the function ``run'' within that thread. | ||
| 744 | |||
| 745 | This function then computes a suitable argument vector, and calls | ||
| 746 | ``EmacsNative.initEmacs'' (defined in android.c), which then calls a | ||
| 747 | modified version of the regular Emacs ``main'' function. | ||
| 748 | |||
| 749 | At that point, Emacs initialization proceeds as usual: | ||
| 750 | Vinitial_window_system is set, loadup.el calls `normal-top-level', | ||
| 751 | which calls `command-line', and finally | ||
| 752 | `window-system-initialization', which initializes the `android' | ||
| 753 | terminal interface as usual. | ||
| 754 | |||
| 755 | What happens here is the same as on other platforms. Now, here is | ||
| 756 | what happens when the initial frame is created: Fx_create_frame calls | ||
| 757 | `android_create_frame_window' to create a top level window: | ||
| 758 | |||
| 759 | static void | ||
| 760 | android_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 | |||
| 779 | This calls the function `android_create_window' with some arguments | ||
| 780 | whose meanings are identical to the arguments to `XCreateWindow'. | ||
| 781 | |||
| 782 | Here is the definition of `android_create_window', in android.c: | ||
| 783 | |||
| 784 | android_window | ||
| 785 | android_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 | |||
| 797 | What does it do? First, some context: | ||
| 798 | |||
| 799 | At any time, there can be at most 65535 Java objects referred to by | ||
| 800 | the rest of Emacs through the Java native interface. Each such object | ||
| 801 | is assigned a ``handle'' (similar to an XID on X) and given a unique | ||
| 802 | type. The function `android_resolve_handle' returns the JNI `jobject' | ||
| 803 | associated with a given handle. | ||
| 804 | |||
| 805 | parent_object = android_resolve_handle (parent, ANDROID_HANDLE_WINDOW); | ||
| 806 | |||
| 807 | Here, 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 | |||
| 813 | Next, `max_handle' is saved, and a new handle is allocated for | ||
| 814 | `window'. | ||
| 815 | |||
| 816 | if (!window) | ||
| 817 | error ("Out of window handles!"); | ||
| 818 | |||
| 819 | An 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 | |||
| 827 | Then, if this initialization has not yet been completed, Emacs | ||
| 828 | proceeds 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 | |||
| 836 | And it tries to look up the constructor, which should take seven | ||
| 837 | arguments: | ||
| 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 | |||
| 852 | Next, it saves a global reference to the class and deletes the local | ||
| 853 | reference. Global references will never be deallocated by the Java | ||
| 854 | virtual 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 | |||
| 872 | Then, it creates an instance of the ``EmacsWindow'' class with the | ||
| 873 | appropriate 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 | |||
| 882 | If creating the object fails, Emacs clears the ``pending exception'' | ||
| 883 | and 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 | |||
| 893 | Otherwise, it associates a new global reference to the object with the | ||
| 894 | handle, and deletes the local reference returned from the JNI | ||
| 895 | NewObject function. | ||
| 896 | |||
| 897 | if (!android_handles[window].handle) | ||
| 898 | memory_full (0); | ||
| 899 | |||
| 900 | If allocating the global reference fails, Emacs signals that it is out | ||
| 901 | of memory. | ||
| 902 | |||
| 903 | android_change_window_attributes (window, value_mask, attrs); | ||
| 904 | return window; | ||
| 905 | |||
| 906 | Otherwise, it applies the specified window attributes and returns the | ||
| 907 | handle of the new window. | ||
| 908 | } | ||
| 909 | |||
| 910 | |||
| 911 | |||
| 912 | DRAWABLES, CURSORS AND HANDLES | ||
| 913 | |||
| 914 | Each widget created by Emacs corresponds to a single ``window'', which | ||
| 915 | has its own backing store. This arrangement is quite similar to X. | ||
| 916 | |||
| 917 | C code does not directly refer to the EmacsView widgets that implement | ||
| 918 | the UI logic behind windows. Instead, its handles refer to | ||
| 919 | EmacsWindow structures, which contain the state necessary to interact | ||
| 920 | with the widgets in an orderly and synchronized manner. | ||
| 921 | |||
| 922 | Like X, both pixmaps and windows are drawable resources, and the same | ||
| 923 | graphics operations can be applied to both. Thus, a separate | ||
| 924 | EmacsPixmap structure is used to wrap around Android Bitmap resources, | ||
| 925 | and the Java-level graphics operation functions are capable of | ||
| 926 | operating on them both. | ||
| 927 | |||
| 928 | Finally, graphics contexts are maintained on both the C and Java | ||
| 929 | levels; the C state recorded in `struct android_gc' is kept in sync | ||
| 930 | with the Java state in the GContext handle's corresponding EmacsGC | ||
| 931 | structure, and cursors are used through handles that refer to | ||
| 932 | EmacsCursor structures that hold system PointerIcons. | ||
| 933 | |||
| 934 | In all cases, the interfaces provided are identical to X. | ||
| 935 | |||
| 936 | |||
| 937 | |||
| 938 | EVENT LOOP | ||
| 939 | |||
| 940 | In a typical Android application, the event loop is managed by the | ||
| 941 | operating system, and callbacks (implemented through overriding | ||
| 942 | separate functions in widgets) are run by the event loop wherever | ||
| 943 | necessary. The thread which runs the event loop is also the only | ||
| 944 | thread capable of creating and manipulating widgets and activities, | ||
| 945 | and is referred to as the ``UI thread''. | ||
| 946 | |||
| 947 | These callbacks are used by Emacs to write representations of X-like | ||
| 948 | events to a separate event queue, which are then read from Emacs's own | ||
| 949 | event loop running in a separate thread. This is accomplished through | ||
| 950 | replacing `select' by a function which waits for the event queue to be | ||
| 951 | occupied, in addition to any file descriptors that `select' would | ||
| 952 | normally wait for. | ||
| 953 | |||
| 954 | Conversely, Emacs's event loop sometimes needs to send events to the | ||
| 955 | UI thread. These events are implemented as tiny fragments of code, | ||
| 956 | which are run as they are received by the main thread. | ||
| 957 | |||
| 958 | A typical example is `displayToast', which is implemented in | ||
| 959 | EmacsService.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 | |||
| 978 | Here, the variable `string' is used by a nested function. This nested | ||
| 979 | function contains a copy of that variable, and is run on the main | ||
| 980 | thread using the function `runOnUiThread', in order to display a short | ||
| 981 | status message on the display. | ||
| 982 | |||
| 983 | When Emacs needs to wait for the nested function to finish, it uses a | ||
| 984 | mechanism implemented in `syncRunnable'. This mechanism first calls a | ||
| 985 | deadlock avoidance mechanism, then runs a nested function on the UI | ||
| 986 | thread, which is expected to signal itself as a condition variable | ||
| 987 | upon completion. It is typically used to allocate resources that can | ||
| 988 | only be allocated from the UI thread, or to obtain non-thread-safe | ||
| 989 | information. The following function is an example; it returns a new | ||
| 990 | EmacsView 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 | |||
| 1024 | As no value can be directly returned from the nested function, a | ||
| 1025 | separate container object is used to hold the result after the | ||
| 1026 | function finishes execution. Note the type name inside the angle | ||
| 1027 | brackets: this type is substituted into the class definition as it is | ||
| 1028 | used; a definition such as: | ||
| 1029 | |||
| 1030 | public class Foo<T> | ||
| 1031 | { | ||
| 1032 | T bar; | ||
| 1033 | }; | ||
| 1034 | |||
| 1035 | can not be used alone: | ||
| 1036 | |||
| 1037 | Foo holder; /* Error! */ | ||
| 1038 | |||
| 1039 | but must have a type specified: | ||
| 1040 | |||
| 1041 | Foo<Object> holder; | ||
| 1042 | |||
| 1043 | in which case the effective definition is: | ||
| 1044 | |||
| 1045 | public 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 | |||
| 21 | set -m | ||
| 22 | oldpwd=`pwd` | ||
| 23 | cd `dirname $0` | ||
| 24 | |||
| 25 | devices=`adb devices | grep device | awk -- '/device\y/ { print $1 }' -` | ||
| 26 | device= | ||
| 27 | progname=$0 | ||
| 28 | package=org.gnu.emacs | ||
| 29 | activity=org.gnu.emacs.EmacsActivity | ||
| 30 | gdb_port=5039 | ||
| 31 | jdb_port=64013 | ||
| 32 | jdb=no | ||
| 33 | attach_existing=no | ||
| 34 | gdbserver= | ||
| 35 | gdb=gdb | ||
| 36 | |||
| 37 | while [ $# -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 | ||
| 100 | done | ||
| 101 | |||
| 102 | if [ -z "$devices" ]; then | ||
| 103 | echo "No devices are available." | ||
| 104 | exit 1 | ||
| 105 | fi | ||
| 106 | |||
| 107 | if [ -z $device ]; then | ||
| 108 | device=$devices | ||
| 109 | fi | ||
| 110 | |||
| 111 | if [ `wc -w <<< "$devices"` -gt 1 ] && [ -z device ]; then | ||
| 112 | echo "Multiple devices are available. Please pick one using" | ||
| 113 | echo "--device and try again." | ||
| 114 | fi | ||
| 115 | |||
| 116 | echo "Looking for $package on device $device" | ||
| 117 | |||
| 118 | # Find the application data directory | ||
| 119 | app_data_dir=`adb -s $device shell run-as $package sh -c 'pwd 2> /dev/null'` | ||
| 120 | |||
| 121 | if [ -z $app_data_dir ]; then | ||
| 122 | echo "The data directory for the package $package was not found." | ||
| 123 | echo "Is it installed?" | ||
| 124 | fi | ||
| 125 | |||
| 126 | echo "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. | ||
| 131 | cat << EOF > tmp.awk | ||
| 132 | BEGIN { | ||
| 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 | } | ||
| 153 | EOF | ||
| 154 | |||
| 155 | # Make sure that file disappears once this script exits. | ||
| 156 | trap "rm -f $(pwd)/tmp.awk" 0 | ||
| 157 | |||
| 158 | # First, run ps to fetch the list of process IDs. | ||
| 159 | package_pids=`adb -s $device shell ps` | ||
| 160 | |||
| 161 | # Next, extract the list of PIDs currently running. | ||
| 162 | package_pids=`awk -f tmp.awk <<< $package_pids` | ||
| 163 | |||
| 164 | if [ "$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` | ||
| 190 | fi | ||
| 191 | |||
| 192 | pid=$package_pids | ||
| 193 | num_pids=`wc -w <<< "$package_pids"` | ||
| 194 | |||
| 195 | if [ $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 | ||
| 205 | elif [ -z $package_pids ]; then | ||
| 206 | echo "No processes were found to attach to." | ||
| 207 | exit 1 | ||
| 208 | fi | ||
| 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 | |||
| 215 | if [ "$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 | ||
| 233 | fi | ||
| 234 | |||
| 235 | if [ -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! | ||
| 269 | fi | ||
| 270 | |||
| 271 | # See if gdbserver has to be uploaded | ||
| 272 | gdbserver_cmd= | ||
| 273 | is_root= | ||
| 274 | if [ -z "$gdbserver" ]; then | ||
| 275 | gdbserver_bin=/system/bin/gdbserver64 | ||
| 276 | else | ||
| 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 | ||
| 303 | fi | ||
| 304 | |||
| 305 | # Now start gdbserver on the device asynchronously. | ||
| 306 | |||
| 307 | echo "Attaching gdbserver to $pid on $device..." | ||
| 308 | exec 5<> /tmp/file-descriptor-stamp | ||
| 309 | rm -f /tmp/file-descriptor-stamp | ||
| 310 | |||
| 311 | if [ -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 | ||
| 321 | else | ||
| 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" | ||
| 327 | fi | ||
| 328 | |||
| 329 | # In order to allow adb to forward to the gdbserver socket, make the | ||
| 330 | # app data directory a+x. | ||
| 331 | adb -s $device shell run-as $package chmod a+x $app_data_dir | ||
| 332 | |||
| 333 | # Wait until gdbserver successfully runs. | ||
| 334 | line= | ||
| 335 | while 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 | ||
| 347 | done | ||
| 348 | |||
| 349 | # Now that GDB is attached, tell the Java debugger to resume execution | ||
| 350 | # and then exit. | ||
| 351 | |||
| 352 | if [ -n "$jdb_command" ]; then | ||
| 353 | echo "resume" >&${JDB[1]} | ||
| 354 | echo "exit" >&${JDB[1]} | ||
| 355 | fi | ||
| 356 | |||
| 357 | # Forward the gdb server port here. | ||
| 358 | adb -s $device forward "tcp:$gdb_port" $gdb_socket | ||
| 359 | if [ ! $? ]; 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; | ||
| 364 | fi | ||
| 365 | |||
| 366 | # Finally, start gdb with any extra arguments needed. | ||
| 367 | cd "$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 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.lang.IllegalStateException; | ||
| 23 | import java.util.List; | ||
| 24 | import java.util.ArrayList; | ||
| 25 | |||
| 26 | import android.app.Activity; | ||
| 27 | |||
| 28 | import android.content.ContentResolver; | ||
| 29 | import android.content.Context; | ||
| 30 | import android.content.Intent; | ||
| 31 | |||
| 32 | import android.os.Build; | ||
| 33 | import android.os.Bundle; | ||
| 34 | |||
| 35 | import android.util.Log; | ||
| 36 | |||
| 37 | import android.net.Uri; | ||
| 38 | |||
| 39 | import android.view.Menu; | ||
| 40 | import android.view.View; | ||
| 41 | import android.view.ViewTreeObserver; | ||
| 42 | import android.view.Window; | ||
| 43 | import android.view.WindowInsets; | ||
| 44 | import android.view.WindowInsetsController; | ||
| 45 | |||
| 46 | import android.widget.FrameLayout; | ||
| 47 | |||
| 48 | public class EmacsActivity extends Activity | ||
| 49 | implements EmacsWindowAttachmentManager.WindowConsumer, | ||
| 50 | ViewTreeObserver.OnGlobalLayoutListener | ||
| 51 | { | ||
| 52 | public static final String TAG = "EmacsActivity"; | ||
| 53 | |||
| 54 | /* ID for URIs from a granted document tree. */ | ||
| 55 | public static final int ACCEPT_DOCUMENT_TREE = 1; | ||
| 56 | |||
| 57 | /* The currently attached EmacsWindow, or null if none. */ | ||
| 58 | private EmacsWindow window; | ||
| 59 | |||
| 60 | /* The frame layout associated with the activity. */ | ||
| 61 | private FrameLayout layout; | ||
| 62 | |||
| 63 | /* List of activities with focus. */ | ||
| 64 | public static final List<EmacsActivity> focusedActivities; | ||
| 65 | |||
| 66 | /* The last activity to have been focused. */ | ||
| 67 | public static EmacsActivity lastFocusedActivity; | ||
| 68 | |||
| 69 | /* The currently focused window. */ | ||
| 70 | public static EmacsWindow focusedWindow; | ||
| 71 | |||
| 72 | /* Whether or not this activity is paused. */ | ||
| 73 | private boolean isPaused; | ||
| 74 | |||
| 75 | /* Whether or not this activity is fullscreen. */ | ||
| 76 | private boolean isFullscreen; | ||
| 77 | |||
| 78 | /* The last context menu to be closed. */ | ||
| 79 | private static Menu lastClosedMenu; | ||
| 80 | |||
| 81 | static | ||
| 82 | { | ||
| 83 | focusedActivities = new ArrayList<EmacsActivity> (); | ||
| 84 | }; | ||
| 85 | |||
| 86 | public static void | ||
| 87 | invalidateFocus1 (EmacsWindow window) | ||
| 88 | { | ||
| 89 | if (window.view.isFocused ()) | ||
| 90 | focusedWindow = window; | ||
| 91 | |||
| 92 | for (EmacsWindow child : window.children) | ||
| 93 | invalidateFocus1 (child); | ||
| 94 | } | ||
| 95 | |||
| 96 | public static void | ||
| 97 | invalidateFocus () | ||
| 98 | { | ||
| 99 | EmacsWindow oldFocus; | ||
| 100 | |||
| 101 | /* Walk through each focused activity and assign the window focus | ||
| 102 | to the bottom-most focused window within. Record the old focus | ||
| 103 | as well. */ | ||
| 104 | oldFocus = focusedWindow; | ||
| 105 | focusedWindow = null; | ||
| 106 | |||
| 107 | for (EmacsActivity activity : focusedActivities) | ||
| 108 | { | ||
| 109 | if (activity.window != null) | ||
| 110 | invalidateFocus1 (activity.window); | ||
| 111 | } | ||
| 112 | |||
| 113 | /* Send focus in- and out- events to the previous and current | ||
| 114 | focus. */ | ||
| 115 | |||
| 116 | if (oldFocus != null) | ||
| 117 | EmacsNative.sendFocusOut (oldFocus.handle, | ||
| 118 | System.currentTimeMillis ()); | ||
| 119 | |||
| 120 | if (focusedWindow != null) | ||
| 121 | EmacsNative.sendFocusIn (focusedWindow.handle, | ||
| 122 | System.currentTimeMillis ()); | ||
| 123 | } | ||
| 124 | |||
| 125 | @Override | ||
| 126 | public final void | ||
| 127 | detachWindow () | ||
| 128 | { | ||
| 129 | syncFullscreenWith (null); | ||
| 130 | |||
| 131 | if (window == null) | ||
| 132 | Log.w (TAG, "detachWindow called, but there is no window"); | ||
| 133 | else | ||
| 134 | { | ||
| 135 | /* Clear the window's pointer to this activity and remove the | ||
| 136 | window's view. */ | ||
| 137 | window.setConsumer (null); | ||
| 138 | |||
| 139 | /* The window can't be iconified any longer. */ | ||
| 140 | window.noticeDeiconified (); | ||
| 141 | layout.removeView (window.view); | ||
| 142 | window = null; | ||
| 143 | |||
| 144 | invalidateFocus (); | ||
| 145 | } | ||
| 146 | } | ||
| 147 | |||
| 148 | @Override | ||
| 149 | public final void | ||
| 150 | attachWindow (EmacsWindow child) | ||
| 151 | { | ||
| 152 | Log.d (TAG, "attachWindow: " + child); | ||
| 153 | |||
| 154 | if (window != null) | ||
| 155 | throw new IllegalStateException ("trying to attach window when one" | ||
| 156 | + " already exists"); | ||
| 157 | |||
| 158 | syncFullscreenWith (child); | ||
| 159 | |||
| 160 | /* Record and attach the view. */ | ||
| 161 | |||
| 162 | window = child; | ||
| 163 | layout.addView (window.view); | ||
| 164 | child.setConsumer (this); | ||
| 165 | |||
| 166 | /* If the window isn't no-focus-on-map, focus its view. */ | ||
| 167 | if (!child.getDontFocusOnMap ()) | ||
| 168 | window.view.requestFocus (); | ||
| 169 | |||
| 170 | /* If the activity is iconified, send that to the window. */ | ||
| 171 | if (isPaused) | ||
| 172 | window.noticeIconified (); | ||
| 173 | |||
| 174 | /* Invalidate the focus. */ | ||
| 175 | invalidateFocus (); | ||
| 176 | } | ||
| 177 | |||
| 178 | @Override | ||
| 179 | public final void | ||
| 180 | destroy () | ||
| 181 | { | ||
| 182 | finish (); | ||
| 183 | } | ||
| 184 | |||
| 185 | @Override | ||
| 186 | public final EmacsWindow | ||
| 187 | getAttachedWindow () | ||
| 188 | { | ||
| 189 | return window; | ||
| 190 | } | ||
| 191 | |||
| 192 | @Override | ||
| 193 | public final void | ||
| 194 | onCreate (Bundle savedInstanceState) | ||
| 195 | { | ||
| 196 | FrameLayout.LayoutParams params; | ||
| 197 | Intent intent; | ||
| 198 | View decorView; | ||
| 199 | ViewTreeObserver observer; | ||
| 200 | int matchParent; | ||
| 201 | |||
| 202 | /* See if Emacs should be started with any extra arguments, such | ||
| 203 | as `--quick'. */ | ||
| 204 | intent = getIntent (); | ||
| 205 | EmacsService.extraStartupArgument | ||
| 206 | = intent.getStringExtra ("org.gnu.emacs.STARTUP_ARGUMENT"); | ||
| 207 | |||
| 208 | matchParent = FrameLayout.LayoutParams.MATCH_PARENT; | ||
| 209 | params | ||
| 210 | = new FrameLayout.LayoutParams (matchParent, | ||
| 211 | matchParent); | ||
| 212 | |||
| 213 | /* Make the frame layout. */ | ||
| 214 | layout = new FrameLayout (this); | ||
| 215 | layout.setLayoutParams (params); | ||
| 216 | |||
| 217 | /* Set it as the content view. */ | ||
| 218 | setContentView (layout); | ||
| 219 | |||
| 220 | /* Maybe start the Emacs service if necessary. */ | ||
| 221 | EmacsService.startEmacsService (this); | ||
| 222 | |||
| 223 | /* Add this activity to the list of available activities. */ | ||
| 224 | EmacsWindowAttachmentManager.MANAGER.registerWindowConsumer (this); | ||
| 225 | |||
| 226 | /* Start observing global layout changes between Jelly Bean and Q. | ||
| 227 | This is required to restore the fullscreen state whenever the | ||
| 228 | on screen keyboard is displayed, as there is otherwise no way | ||
| 229 | to determine when the on screen keyboard becomes visible. */ | ||
| 230 | |||
| 231 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN | ||
| 232 | && Build.VERSION.SDK_INT < Build.VERSION_CODES.R) | ||
| 233 | { | ||
| 234 | decorView = getWindow ().getDecorView (); | ||
| 235 | observer = decorView.getViewTreeObserver (); | ||
| 236 | observer.addOnGlobalLayoutListener (this); | ||
| 237 | } | ||
| 238 | |||
| 239 | super.onCreate (savedInstanceState); | ||
| 240 | } | ||
| 241 | |||
| 242 | @Override | ||
| 243 | public final void | ||
| 244 | onGlobalLayout () | ||
| 245 | { | ||
| 246 | syncFullscreenWith (window); | ||
| 247 | } | ||
| 248 | |||
| 249 | @Override | ||
| 250 | public final void | ||
| 251 | onDestroy () | ||
| 252 | { | ||
| 253 | EmacsWindowAttachmentManager manager; | ||
| 254 | boolean isMultitask; | ||
| 255 | |||
| 256 | manager = EmacsWindowAttachmentManager.MANAGER; | ||
| 257 | |||
| 258 | /* The activity will die shortly hereafter. If there is a window | ||
| 259 | attached, close it now. */ | ||
| 260 | Log.d (TAG, "onDestroy " + this); | ||
| 261 | isMultitask = this instanceof EmacsMultitaskActivity; | ||
| 262 | manager.removeWindowConsumer (this, isMultitask || isFinishing ()); | ||
| 263 | focusedActivities.remove (this); | ||
| 264 | invalidateFocus (); | ||
| 265 | |||
| 266 | /* Remove this activity from the static field, lest it leak. */ | ||
| 267 | if (lastFocusedActivity == this) | ||
| 268 | lastFocusedActivity = null; | ||
| 269 | |||
| 270 | super.onDestroy (); | ||
| 271 | } | ||
| 272 | |||
| 273 | @Override | ||
| 274 | public final void | ||
| 275 | onWindowFocusChanged (boolean isFocused) | ||
| 276 | { | ||
| 277 | Log.d (TAG, ("onWindowFocusChanged: " | ||
| 278 | + (isFocused ? "YES" : "NO"))); | ||
| 279 | |||
| 280 | if (isFocused && !focusedActivities.contains (this)) | ||
| 281 | { | ||
| 282 | focusedActivities.add (this); | ||
| 283 | lastFocusedActivity = this; | ||
| 284 | |||
| 285 | /* Update the window insets as the focus change may have | ||
| 286 | changed the window insets as well, and the system does not | ||
| 287 | automatically restore visibility flags. */ | ||
| 288 | |||
| 289 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN | ||
| 290 | && Build.VERSION.SDK_INT < Build.VERSION_CODES.R | ||
| 291 | && isFullscreen) | ||
| 292 | syncFullscreenWith (window); | ||
| 293 | } | ||
| 294 | else | ||
| 295 | focusedActivities.remove (this); | ||
| 296 | |||
| 297 | invalidateFocus (); | ||
| 298 | } | ||
| 299 | |||
| 300 | @Override | ||
| 301 | public final void | ||
| 302 | onPause () | ||
| 303 | { | ||
| 304 | isPaused = true; | ||
| 305 | |||
| 306 | EmacsWindowAttachmentManager.MANAGER.noticeIconified (this); | ||
| 307 | super.onPause (); | ||
| 308 | } | ||
| 309 | |||
| 310 | @Override | ||
| 311 | public final void | ||
| 312 | onResume () | ||
| 313 | { | ||
| 314 | isPaused = false; | ||
| 315 | |||
| 316 | EmacsWindowAttachmentManager.MANAGER.noticeDeiconified (this); | ||
| 317 | super.onResume (); | ||
| 318 | } | ||
| 319 | |||
| 320 | @Override | ||
| 321 | public final void | ||
| 322 | onContextMenuClosed (Menu menu) | ||
| 323 | { | ||
| 324 | int serial; | ||
| 325 | |||
| 326 | Log.d (TAG, "onContextMenuClosed: " + menu); | ||
| 327 | |||
| 328 | /* See the comment inside onMenuItemClick. */ | ||
| 329 | |||
| 330 | if (((EmacsContextMenu.wasSubmenuSelected == -2) | ||
| 331 | || (EmacsContextMenu.wasSubmenuSelected >= 0 | ||
| 332 | && ((System.currentTimeMillis () | ||
| 333 | - EmacsContextMenu.wasSubmenuSelected) | ||
| 334 | <= 300))) | ||
| 335 | || menu == lastClosedMenu) | ||
| 336 | { | ||
| 337 | EmacsContextMenu.wasSubmenuSelected = -1; | ||
| 338 | lastClosedMenu = menu; | ||
| 339 | return; | ||
| 340 | } | ||
| 341 | |||
| 342 | /* lastClosedMenu is set because Android apparently calls this | ||
| 343 | function twice. */ | ||
| 344 | |||
| 345 | lastClosedMenu = null; | ||
| 346 | |||
| 347 | /* Send a context menu event given that no menu item has already | ||
| 348 | been selected. */ | ||
| 349 | if (!EmacsContextMenu.itemAlreadySelected) | ||
| 350 | { | ||
| 351 | serial = EmacsContextMenu.lastMenuEventSerial; | ||
| 352 | EmacsNative.sendContextMenu ((short) 0, 0, | ||
| 353 | serial); | ||
| 354 | } | ||
| 355 | |||
| 356 | super.onContextMenuClosed (menu); | ||
| 357 | } | ||
| 358 | |||
| 359 | @SuppressWarnings ("deprecation") | ||
| 360 | public final void | ||
| 361 | syncFullscreenWith (EmacsWindow emacsWindow) | ||
| 362 | { | ||
| 363 | WindowInsetsController controller; | ||
| 364 | Window window; | ||
| 365 | int behavior, flags; | ||
| 366 | View view; | ||
| 367 | |||
| 368 | if (emacsWindow != null) | ||
| 369 | isFullscreen = emacsWindow.fullscreen; | ||
| 370 | else | ||
| 371 | isFullscreen = false; | ||
| 372 | |||
| 373 | /* On Android 11 or later, use the window insets controller to | ||
| 374 | control whether or not the view is fullscreen. */ | ||
| 375 | |||
| 376 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) | ||
| 377 | { | ||
| 378 | window = getWindow (); | ||
| 379 | |||
| 380 | /* If there is no attached window, return immediately. */ | ||
| 381 | if (window == null) | ||
| 382 | return; | ||
| 383 | |||
| 384 | behavior = WindowInsetsController.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE; | ||
| 385 | controller = window.getInsetsController (); | ||
| 386 | controller.setSystemBarsBehavior (behavior); | ||
| 387 | |||
| 388 | if (isFullscreen) | ||
| 389 | controller.hide (WindowInsets.Type.statusBars () | ||
| 390 | | WindowInsets.Type.navigationBars ()); | ||
| 391 | else | ||
| 392 | controller.show (WindowInsets.Type.statusBars () | ||
| 393 | | WindowInsets.Type.navigationBars ()); | ||
| 394 | |||
| 395 | return; | ||
| 396 | } | ||
| 397 | |||
| 398 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) | ||
| 399 | { | ||
| 400 | /* On Android 4.1 or later, use `setSystemUiVisibility'. */ | ||
| 401 | |||
| 402 | window = getWindow (); | ||
| 403 | |||
| 404 | if (window == null) | ||
| 405 | return; | ||
| 406 | |||
| 407 | view = window.getDecorView (); | ||
| 408 | |||
| 409 | if (isFullscreen) | ||
| 410 | { | ||
| 411 | flags = 0; | ||
| 412 | flags |= View.SYSTEM_UI_FLAG_FULLSCREEN; | ||
| 413 | |||
| 414 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) | ||
| 415 | { | ||
| 416 | /* These flags means that Emacs will be full screen as | ||
| 417 | long as the state flag is set. */ | ||
| 418 | flags |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION; | ||
| 419 | flags |= View.SYSTEM_UI_FLAG_IMMERSIVE; | ||
| 420 | flags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY; | ||
| 421 | } | ||
| 422 | |||
| 423 | /* Apply the given flags. */ | ||
| 424 | view.setSystemUiVisibility (flags); | ||
| 425 | } | ||
| 426 | else | ||
| 427 | view.setSystemUiVisibility (View.SYSTEM_UI_FLAG_VISIBLE); | ||
| 428 | } | ||
| 429 | } | ||
| 430 | |||
| 431 | @Override | ||
| 432 | public final void | ||
| 433 | onAttachedToWindow () | ||
| 434 | { | ||
| 435 | super.onAttachedToWindow (); | ||
| 436 | |||
| 437 | /* Update the window insets. */ | ||
| 438 | syncFullscreenWith (window); | ||
| 439 | } | ||
| 440 | |||
| 441 | |||
| 442 | |||
| 443 | @Override | ||
| 444 | public final void | ||
| 445 | onActivityResult (int requestCode, int resultCode, Intent data) | ||
| 446 | { | ||
| 447 | ContentResolver resolver; | ||
| 448 | Uri uri; | ||
| 449 | int flags; | ||
| 450 | |||
| 451 | switch (requestCode) | ||
| 452 | { | ||
| 453 | case ACCEPT_DOCUMENT_TREE: | ||
| 454 | |||
| 455 | /* A document granted through | ||
| 456 | EmacsService.requestDirectoryAccess. */ | ||
| 457 | |||
| 458 | if (resultCode == RESULT_OK) | ||
| 459 | { | ||
| 460 | resolver = getContentResolver (); | ||
| 461 | uri = data.getData (); | ||
| 462 | flags = (Intent.FLAG_GRANT_READ_URI_PERMISSION | ||
| 463 | | Intent.FLAG_GRANT_WRITE_URI_PERMISSION); | ||
| 464 | |||
| 465 | try | ||
| 466 | { | ||
| 467 | if (uri != null) | ||
| 468 | resolver.takePersistableUriPermission (uri, flags); | ||
| 469 | } | ||
| 470 | catch (Exception exception) | ||
| 471 | { | ||
| 472 | /* Permission to access URI might've been revoked in | ||
| 473 | between selecting the file and this callback being | ||
| 474 | invoked. Don't crash in such cases. */ | ||
| 475 | } | ||
| 476 | } | ||
| 477 | |||
| 478 | break; | ||
| 479 | } | ||
| 480 | } | ||
| 481 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsApplication.java b/java/org/gnu/emacs/EmacsApplication.java new file mode 100644 index 00000000000..8afa5bcedb4 --- /dev/null +++ b/java/org/gnu/emacs/EmacsApplication.java | |||
| @@ -0,0 +1,92 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.io.File; | ||
| 23 | import java.io.FileFilter; | ||
| 24 | |||
| 25 | import android.content.Context; | ||
| 26 | |||
| 27 | import android.app.Application; | ||
| 28 | import android.util.Log; | ||
| 29 | |||
| 30 | public final class EmacsApplication extends Application | ||
| 31 | { | ||
| 32 | private static final String TAG = "EmacsApplication"; | ||
| 33 | |||
| 34 | /* The name of the dump file to use. */ | ||
| 35 | public static String dumpFileName; | ||
| 36 | |||
| 37 | public static void | ||
| 38 | findDumpFile (Context context) | ||
| 39 | { | ||
| 40 | File filesDirectory; | ||
| 41 | File[] allFiles; | ||
| 42 | String wantedDumpFile; | ||
| 43 | int i; | ||
| 44 | |||
| 45 | wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint () | ||
| 46 | + ".pdmp"); | ||
| 47 | |||
| 48 | /* Obtain a list of all files ending with ``.pdmp''. Then, look | ||
| 49 | for a file named ``emacs-<fingerprint>.pdmp'' and delete the | ||
| 50 | rest. */ | ||
| 51 | filesDirectory = context.getFilesDir (); | ||
| 52 | |||
| 53 | allFiles = filesDirectory.listFiles (new FileFilter () { | ||
| 54 | @Override | ||
| 55 | public boolean | ||
| 56 | accept (File file) | ||
| 57 | { | ||
| 58 | return (!file.isDirectory () | ||
| 59 | && file.getName ().endsWith (".pdmp")); | ||
| 60 | } | ||
| 61 | }); | ||
| 62 | |||
| 63 | if (allFiles == null) | ||
| 64 | return; | ||
| 65 | |||
| 66 | /* Now try to find the right dump file. */ | ||
| 67 | for (i = 0; i < allFiles.length; ++i) | ||
| 68 | { | ||
| 69 | if (allFiles[i].getName ().equals (wantedDumpFile)) | ||
| 70 | dumpFileName = allFiles[i].getAbsolutePath (); | ||
| 71 | else | ||
| 72 | /* Delete this outdated dump file. */ | ||
| 73 | allFiles[i].delete (); | ||
| 74 | } | ||
| 75 | } | ||
| 76 | |||
| 77 | @Override | ||
| 78 | public void | ||
| 79 | onCreate () | ||
| 80 | { | ||
| 81 | /* Block signals which don't interest the current thread and its | ||
| 82 | descendants created by the system. The original signal mask | ||
| 83 | will be restored for the Emacs thread in `initEmacs'. */ | ||
| 84 | EmacsNative.setupSystemThread (); | ||
| 85 | |||
| 86 | /* Locate a suitable dump file. */ | ||
| 87 | findDumpFile (this); | ||
| 88 | |||
| 89 | /* Start the rest of the application. */ | ||
| 90 | super.onCreate (); | ||
| 91 | } | ||
| 92 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsClipboard.java b/java/org/gnu/emacs/EmacsClipboard.java new file mode 100644 index 00000000000..5cd48af6e3a --- /dev/null +++ b/java/org/gnu/emacs/EmacsClipboard.java | |||
| @@ -0,0 +1,47 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.os.Build; | ||
| 23 | |||
| 24 | /* This class provides helper code for accessing the clipboard, | ||
| 25 | abstracting between the different interfaces on API 8 and 11. */ | ||
| 26 | |||
| 27 | public abstract class EmacsClipboard | ||
| 28 | { | ||
| 29 | public abstract void setClipboard (byte[] bytes); | ||
| 30 | public abstract int ownsClipboard (); | ||
| 31 | public abstract boolean clipboardExists (); | ||
| 32 | public abstract byte[] getClipboard (); | ||
| 33 | |||
| 34 | public abstract byte[][] getClipboardTargets (); | ||
| 35 | public abstract long[] getClipboardData (byte[] target); | ||
| 36 | |||
| 37 | /* Create the correct kind of clipboard for this system. */ | ||
| 38 | |||
| 39 | public static EmacsClipboard | ||
| 40 | makeClipboard () | ||
| 41 | { | ||
| 42 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) | ||
| 43 | return new EmacsSdk11Clipboard (); | ||
| 44 | else | ||
| 45 | return new EmacsSdk8Clipboard (); | ||
| 46 | } | ||
| 47 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsContextMenu.java b/java/org/gnu/emacs/EmacsContextMenu.java new file mode 100644 index 00000000000..46eddeeda3d --- /dev/null +++ b/java/org/gnu/emacs/EmacsContextMenu.java | |||
| @@ -0,0 +1,393 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.util.List; | ||
| 23 | import java.util.ArrayList; | ||
| 24 | |||
| 25 | import android.content.Context; | ||
| 26 | import android.content.Intent; | ||
| 27 | |||
| 28 | import android.os.Build; | ||
| 29 | |||
| 30 | import android.view.ContextMenu; | ||
| 31 | import android.view.Menu; | ||
| 32 | import android.view.MenuItem; | ||
| 33 | import android.view.View; | ||
| 34 | import android.view.SubMenu; | ||
| 35 | |||
| 36 | import android.util.Log; | ||
| 37 | |||
| 38 | /* Context menu implementation. This object is built from JNI and | ||
| 39 | describes a menu hiearchy. Then, `inflate' can turn it into an | ||
| 40 | Android menu, which can be turned into a popup (or other kind of) | ||
| 41 | menu. */ | ||
| 42 | |||
| 43 | public final class EmacsContextMenu | ||
| 44 | { | ||
| 45 | private static final String TAG = "EmacsContextMenu"; | ||
| 46 | |||
| 47 | /* Whether or not an item was selected. */ | ||
| 48 | public static boolean itemAlreadySelected; | ||
| 49 | |||
| 50 | /* Whether or not a submenu was selected. | ||
| 51 | Value is -1 if no; value is -2 if yes, and a context menu | ||
| 52 | close event will definitely be sent. Any other value is | ||
| 53 | the timestamp when the submenu was selected. */ | ||
| 54 | public static long wasSubmenuSelected; | ||
| 55 | |||
| 56 | /* The serial ID of the last context menu to be displayed. */ | ||
| 57 | public static int lastMenuEventSerial; | ||
| 58 | |||
| 59 | /* The last group ID used for a menu item. */ | ||
| 60 | public int lastGroupId; | ||
| 61 | |||
| 62 | private static final class Item implements MenuItem.OnMenuItemClickListener | ||
| 63 | { | ||
| 64 | public int itemID; | ||
| 65 | public String itemName, tooltip; | ||
| 66 | public EmacsContextMenu subMenu; | ||
| 67 | public boolean isEnabled, isCheckable, isChecked; | ||
| 68 | public EmacsView inflatedView; | ||
| 69 | public boolean isRadio; | ||
| 70 | |||
| 71 | @Override | ||
| 72 | public boolean | ||
| 73 | onMenuItemClick (MenuItem item) | ||
| 74 | { | ||
| 75 | Log.d (TAG, "onMenuItemClick: " + itemName + " (" + itemID + ")"); | ||
| 76 | |||
| 77 | if (subMenu != null) | ||
| 78 | { | ||
| 79 | /* Android 6.0 and earlier don't support nested submenus | ||
| 80 | properly, so display the submenu popup by hand. */ | ||
| 81 | |||
| 82 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) | ||
| 83 | { | ||
| 84 | Log.d (TAG, "onMenuItemClick: displaying submenu " + subMenu); | ||
| 85 | |||
| 86 | /* Still set wasSubmenuSelected -- if not set, the | ||
| 87 | dismissal of this context menu will result in a | ||
| 88 | context menu event being sent. */ | ||
| 89 | wasSubmenuSelected = -2; | ||
| 90 | |||
| 91 | /* Running a popup menu from inside a click handler | ||
| 92 | doesn't work, so make sure it is displayed | ||
| 93 | outside. */ | ||
| 94 | |||
| 95 | inflatedView.post (new Runnable () { | ||
| 96 | @Override | ||
| 97 | public void | ||
| 98 | run () | ||
| 99 | { | ||
| 100 | inflatedView.popupMenu (subMenu, 0, 0, true); | ||
| 101 | } | ||
| 102 | }); | ||
| 103 | |||
| 104 | return true; | ||
| 105 | } | ||
| 106 | |||
| 107 | /* After opening a submenu within a submenu, Android will | ||
| 108 | send onContextMenuClosed for a ContextMenuBuilder. This | ||
| 109 | will normally confuse Emacs into thinking that the | ||
| 110 | context menu has been dismissed. Wrong! | ||
| 111 | |||
| 112 | Setting this flag makes EmacsActivity to only handle | ||
| 113 | SubMenuBuilder being closed, which always means the menu | ||
| 114 | has actually been dismissed. | ||
| 115 | |||
| 116 | However, these extraneous events aren't sent on devices | ||
| 117 | where submenus display without dismissing their parents. | ||
| 118 | Thus, only ignore the close event if it happens within | ||
| 119 | 300 milliseconds of the submenu being selected. */ | ||
| 120 | wasSubmenuSelected = System.currentTimeMillis (); | ||
| 121 | return false; | ||
| 122 | } | ||
| 123 | |||
| 124 | /* Send a context menu event. */ | ||
| 125 | EmacsNative.sendContextMenu ((short) 0, itemID, | ||
| 126 | lastMenuEventSerial); | ||
| 127 | |||
| 128 | /* Say that an item has already been selected. */ | ||
| 129 | itemAlreadySelected = true; | ||
| 130 | return true; | ||
| 131 | } | ||
| 132 | }; | ||
| 133 | |||
| 134 | /* List of menu items contained in this menu. */ | ||
| 135 | public List<Item> menuItems; | ||
| 136 | |||
| 137 | /* The parent context menu, or NULL if none. */ | ||
| 138 | private EmacsContextMenu parent; | ||
| 139 | |||
| 140 | /* The title of this context menu, or NULL if none. */ | ||
| 141 | private String title; | ||
| 142 | |||
| 143 | |||
| 144 | |||
| 145 | /* Create a context menu with no items inside and the title TITLE, | ||
| 146 | which may be NULL. */ | ||
| 147 | |||
| 148 | public static EmacsContextMenu | ||
| 149 | createContextMenu (String title) | ||
| 150 | { | ||
| 151 | EmacsContextMenu menu; | ||
| 152 | |||
| 153 | menu = new EmacsContextMenu (); | ||
| 154 | menu.title = title; | ||
| 155 | menu.menuItems = new ArrayList<Item> (); | ||
| 156 | |||
| 157 | return menu; | ||
| 158 | } | ||
| 159 | |||
| 160 | /* Add a normal menu item to the context menu with the id ITEMID and | ||
| 161 | the name ITEMNAME. Enable it if ISENABLED, else keep it | ||
| 162 | disabled. | ||
| 163 | |||
| 164 | If this is not a submenu and ISCHECKABLE is set, make the item | ||
| 165 | checkable. Likewise, if ISCHECKED is set, make the item | ||
| 166 | checked. | ||
| 167 | |||
| 168 | If TOOLTIP is non-NULL, set the menu item tooltip to TOOLTIP. | ||
| 169 | |||
| 170 | If ISRADIO, then display the check mark as a radio button. */ | ||
| 171 | |||
| 172 | public void | ||
| 173 | addItem (int itemID, String itemName, boolean isEnabled, | ||
| 174 | boolean isCheckable, boolean isChecked, | ||
| 175 | String tooltip, boolean isRadio) | ||
| 176 | { | ||
| 177 | Item item; | ||
| 178 | |||
| 179 | item = new Item (); | ||
| 180 | item.itemID = itemID; | ||
| 181 | item.itemName = itemName; | ||
| 182 | item.isEnabled = isEnabled; | ||
| 183 | item.isCheckable = isCheckable; | ||
| 184 | item.isChecked = isChecked; | ||
| 185 | item.tooltip = tooltip; | ||
| 186 | item.isRadio = isRadio; | ||
| 187 | |||
| 188 | menuItems.add (item); | ||
| 189 | } | ||
| 190 | |||
| 191 | /* Create a disabled menu item with the name ITEMNAME. */ | ||
| 192 | |||
| 193 | public void | ||
| 194 | addPane (String itemName) | ||
| 195 | { | ||
| 196 | Item item; | ||
| 197 | |||
| 198 | item = new Item (); | ||
| 199 | item.itemName = itemName; | ||
| 200 | |||
| 201 | menuItems.add (item); | ||
| 202 | } | ||
| 203 | |||
| 204 | /* Add a submenu to the context menu with the specified title and | ||
| 205 | item name. */ | ||
| 206 | |||
| 207 | public EmacsContextMenu | ||
| 208 | addSubmenu (String itemName, String tooltip) | ||
| 209 | { | ||
| 210 | EmacsContextMenu submenu; | ||
| 211 | Item item; | ||
| 212 | |||
| 213 | item = new Item (); | ||
| 214 | item.itemID = 0; | ||
| 215 | item.itemName = itemName; | ||
| 216 | item.tooltip = tooltip; | ||
| 217 | item.subMenu = createContextMenu (itemName); | ||
| 218 | item.subMenu.parent = this; | ||
| 219 | |||
| 220 | menuItems.add (item); | ||
| 221 | return item.subMenu; | ||
| 222 | } | ||
| 223 | |||
| 224 | /* Add the contents of this menu to MENU. Assume MENU will be | ||
| 225 | displayed in INFLATEDVIEW. */ | ||
| 226 | |||
| 227 | private void | ||
| 228 | inflateMenuItems (Menu menu, EmacsView inflatedView) | ||
| 229 | { | ||
| 230 | Intent intent; | ||
| 231 | MenuItem menuItem; | ||
| 232 | SubMenu submenu; | ||
| 233 | |||
| 234 | for (Item item : menuItems) | ||
| 235 | { | ||
| 236 | if (item.subMenu != null) | ||
| 237 | { | ||
| 238 | /* This is a submenu. On versions of Android which | ||
| 239 | support doing so, create the submenu and add the | ||
| 240 | contents of the menu to it. | ||
| 241 | |||
| 242 | Note that Android 4.0 and later technically supports | ||
| 243 | having multiple layers of nested submenus, but if they | ||
| 244 | are used, onContextMenuClosed becomes unreliable. */ | ||
| 245 | |||
| 246 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) | ||
| 247 | { | ||
| 248 | submenu = menu.addSubMenu (item.itemName); | ||
| 249 | item.subMenu.inflateMenuItems (submenu, inflatedView); | ||
| 250 | |||
| 251 | /* This is still needed to set wasSubmenuSelected. */ | ||
| 252 | menuItem = submenu.getItem (); | ||
| 253 | } | ||
| 254 | else | ||
| 255 | menuItem = menu.add (item.itemName); | ||
| 256 | |||
| 257 | item.inflatedView = inflatedView; | ||
| 258 | menuItem.setOnMenuItemClickListener (item); | ||
| 259 | } | ||
| 260 | else | ||
| 261 | { | ||
| 262 | if (item.isRadio) | ||
| 263 | menuItem = menu.add (++lastGroupId, Menu.NONE, Menu.NONE, | ||
| 264 | item.itemName); | ||
| 265 | else | ||
| 266 | menuItem = menu.add (item.itemName); | ||
| 267 | menuItem.setOnMenuItemClickListener (item); | ||
| 268 | |||
| 269 | /* If the item ID is zero, then disable the item. */ | ||
| 270 | if (item.itemID == 0 || !item.isEnabled) | ||
| 271 | menuItem.setEnabled (false); | ||
| 272 | |||
| 273 | /* Now make the menu item display a checkmark as | ||
| 274 | appropriate. */ | ||
| 275 | |||
| 276 | if (item.isCheckable) | ||
| 277 | menuItem.setCheckable (true); | ||
| 278 | |||
| 279 | if (item.isChecked) | ||
| 280 | menuItem.setChecked (true); | ||
| 281 | |||
| 282 | /* Define an exclusively checkable group if the item is a | ||
| 283 | radio button. */ | ||
| 284 | |||
| 285 | if (item.isRadio) | ||
| 286 | menu.setGroupCheckable (lastGroupId, true, true); | ||
| 287 | |||
| 288 | /* If the tooltip text is set and the system is new enough | ||
| 289 | to support menu item tooltips, set it on the item. */ | ||
| 290 | |||
| 291 | if (item.tooltip != null | ||
| 292 | && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) | ||
| 293 | menuItem.setTooltipText (item.tooltip); | ||
| 294 | } | ||
| 295 | } | ||
| 296 | } | ||
| 297 | |||
| 298 | /* Enter the items in this context menu to MENU. | ||
| 299 | Assume that MENU will be displayed in VIEW; this may lead to | ||
| 300 | popupMenu being called on VIEW if a submenu is selected. | ||
| 301 | |||
| 302 | If MENU is a ContextMenu, set its header title to the one | ||
| 303 | contained in this object. */ | ||
| 304 | |||
| 305 | public void | ||
| 306 | expandTo (Menu menu, EmacsView view) | ||
| 307 | { | ||
| 308 | inflateMenuItems (menu, view); | ||
| 309 | |||
| 310 | /* See if menu is a ContextMenu and a title is set. */ | ||
| 311 | if (title == null || !(menu instanceof ContextMenu)) | ||
| 312 | return; | ||
| 313 | |||
| 314 | /* Set its title to this.title. */ | ||
| 315 | ((ContextMenu) menu).setHeaderTitle (title); | ||
| 316 | } | ||
| 317 | |||
| 318 | /* Return the parent or NULL. */ | ||
| 319 | |||
| 320 | public EmacsContextMenu | ||
| 321 | parent () | ||
| 322 | { | ||
| 323 | return this.parent; | ||
| 324 | } | ||
| 325 | |||
| 326 | /* Like display, but does the actual work and runs in the main | ||
| 327 | thread. */ | ||
| 328 | |||
| 329 | private boolean | ||
| 330 | display1 (EmacsWindow window, int xPosition, int yPosition) | ||
| 331 | { | ||
| 332 | /* Set this flag to false. It is used to decide whether or not to | ||
| 333 | send 0 in response to the context menu being closed. */ | ||
| 334 | itemAlreadySelected = false; | ||
| 335 | |||
| 336 | /* No submenu has been selected yet. */ | ||
| 337 | wasSubmenuSelected = -1; | ||
| 338 | |||
| 339 | return window.view.popupMenu (this, xPosition, yPosition, | ||
| 340 | false); | ||
| 341 | } | ||
| 342 | |||
| 343 | /* Display this context menu on WINDOW, at xPosition and yPosition. | ||
| 344 | SERIAL is a number that will be returned in any menu event | ||
| 345 | generated to identify this context menu. */ | ||
| 346 | |||
| 347 | public boolean | ||
| 348 | display (final EmacsWindow window, final int xPosition, | ||
| 349 | final int yPosition, final int serial) | ||
| 350 | { | ||
| 351 | Runnable runnable; | ||
| 352 | final EmacsHolder<Boolean> rc; | ||
| 353 | |||
| 354 | rc = new EmacsHolder<Boolean> (); | ||
| 355 | rc.thing = false; | ||
| 356 | |||
| 357 | runnable = new Runnable () { | ||
| 358 | @Override | ||
| 359 | public void | ||
| 360 | run () | ||
| 361 | { | ||
| 362 | synchronized (this) | ||
| 363 | { | ||
| 364 | lastMenuEventSerial = serial; | ||
| 365 | rc.thing = display1 (window, xPosition, yPosition); | ||
| 366 | notify (); | ||
| 367 | } | ||
| 368 | } | ||
| 369 | }; | ||
| 370 | |||
| 371 | EmacsService.syncRunnable (runnable); | ||
| 372 | return rc.thing; | ||
| 373 | } | ||
| 374 | |||
| 375 | /* Dismiss this context menu. WINDOW is the window where the | ||
| 376 | context menu is being displayed. */ | ||
| 377 | |||
| 378 | public void | ||
| 379 | dismiss (final EmacsWindow window) | ||
| 380 | { | ||
| 381 | Runnable runnable; | ||
| 382 | |||
| 383 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 384 | @Override | ||
| 385 | public void | ||
| 386 | run () | ||
| 387 | { | ||
| 388 | window.view.cancelPopupMenu (); | ||
| 389 | itemAlreadySelected = false; | ||
| 390 | } | ||
| 391 | }); | ||
| 392 | } | ||
| 393 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsCursor.java b/java/org/gnu/emacs/EmacsCursor.java new file mode 100644 index 00000000000..c14c6f2a11b --- /dev/null +++ b/java/org/gnu/emacs/EmacsCursor.java | |||
| @@ -0,0 +1,47 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.view.PointerIcon; | ||
| 23 | import android.os.Build; | ||
| 24 | |||
| 25 | /* Cursor wrapper. Note that pointer icons are not supported prior to | ||
| 26 | Android 24. */ | ||
| 27 | |||
| 28 | public final class EmacsCursor extends EmacsHandleObject | ||
| 29 | { | ||
| 30 | /* The pointer icon associated with this cursor. */ | ||
| 31 | public final PointerIcon icon; | ||
| 32 | |||
| 33 | public | ||
| 34 | EmacsCursor (short handle, int glyph) | ||
| 35 | { | ||
| 36 | super (handle); | ||
| 37 | |||
| 38 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) | ||
| 39 | { | ||
| 40 | icon = null; | ||
| 41 | return; | ||
| 42 | } | ||
| 43 | |||
| 44 | icon = PointerIcon.getSystemIcon (EmacsService.SERVICE, | ||
| 45 | glyph); | ||
| 46 | } | ||
| 47 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsDialog.java b/java/org/gnu/emacs/EmacsDialog.java new file mode 100644 index 00000000000..e4ed2271741 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDialog.java | |||
| @@ -0,0 +1,427 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.util.List; | ||
| 23 | import java.util.ArrayList; | ||
| 24 | |||
| 25 | import android.app.AlertDialog; | ||
| 26 | |||
| 27 | import android.content.Context; | ||
| 28 | import android.content.DialogInterface; | ||
| 29 | |||
| 30 | import android.content.res.Resources.NotFoundException; | ||
| 31 | import android.content.res.Resources.Theme; | ||
| 32 | import android.content.res.TypedArray; | ||
| 33 | |||
| 34 | import android.os.Build; | ||
| 35 | |||
| 36 | import android.provider.Settings; | ||
| 37 | |||
| 38 | import android.util.Log; | ||
| 39 | |||
| 40 | import android.widget.Button; | ||
| 41 | import android.widget.LinearLayout; | ||
| 42 | import android.widget.FrameLayout; | ||
| 43 | |||
| 44 | import android.view.View; | ||
| 45 | import android.view.ViewGroup; | ||
| 46 | import android.view.Window; | ||
| 47 | import android.view.WindowManager; | ||
| 48 | |||
| 49 | /* Toolkit dialog implementation. This object is built from JNI and | ||
| 50 | describes a single alert dialog. Then, `inflate' turns it into | ||
| 51 | AlertDialog. */ | ||
| 52 | |||
| 53 | public final class EmacsDialog implements DialogInterface.OnDismissListener | ||
| 54 | { | ||
| 55 | private static final String TAG = "EmacsDialog"; | ||
| 56 | |||
| 57 | /* List of buttons in this dialog. */ | ||
| 58 | private List<EmacsButton> buttons; | ||
| 59 | |||
| 60 | /* Dialog title. */ | ||
| 61 | private String title; | ||
| 62 | |||
| 63 | /* Dialog text. */ | ||
| 64 | private String text; | ||
| 65 | |||
| 66 | /* Whether or not a selection has already been made. */ | ||
| 67 | private boolean wasButtonClicked; | ||
| 68 | |||
| 69 | /* Dialog to dismiss after click. */ | ||
| 70 | private AlertDialog dismissDialog; | ||
| 71 | |||
| 72 | /* The menu serial associated with this dialog box. */ | ||
| 73 | private int menuEventSerial; | ||
| 74 | |||
| 75 | private final class EmacsButton implements View.OnClickListener, | ||
| 76 | DialogInterface.OnClickListener | ||
| 77 | { | ||
| 78 | /* Name of this button. */ | ||
| 79 | public String name; | ||
| 80 | |||
| 81 | /* ID of this button. */ | ||
| 82 | public int id; | ||
| 83 | |||
| 84 | /* Whether or not the button is enabled. */ | ||
| 85 | public boolean enabled; | ||
| 86 | |||
| 87 | @Override | ||
| 88 | public void | ||
| 89 | onClick (View view) | ||
| 90 | { | ||
| 91 | Log.d (TAG, "onClicked " + this); | ||
| 92 | |||
| 93 | wasButtonClicked = true; | ||
| 94 | EmacsNative.sendContextMenu ((short) 0, id, menuEventSerial); | ||
| 95 | dismissDialog.dismiss (); | ||
| 96 | } | ||
| 97 | |||
| 98 | @Override | ||
| 99 | public void | ||
| 100 | onClick (DialogInterface dialog, int which) | ||
| 101 | { | ||
| 102 | Log.d (TAG, "onClicked " + this); | ||
| 103 | |||
| 104 | wasButtonClicked = true; | ||
| 105 | EmacsNative.sendContextMenu ((short) 0, id, menuEventSerial); | ||
| 106 | } | ||
| 107 | }; | ||
| 108 | |||
| 109 | /* Create a popup dialog with the title TITLE and the text TEXT. | ||
| 110 | TITLE may be NULL. MENUEVENTSERIAL is a number which will | ||
| 111 | identify this popup dialog inside events it sends. */ | ||
| 112 | |||
| 113 | public static EmacsDialog | ||
| 114 | createDialog (String title, String text, int menuEventSerial) | ||
| 115 | { | ||
| 116 | EmacsDialog dialog; | ||
| 117 | |||
| 118 | dialog = new EmacsDialog (); | ||
| 119 | dialog.buttons = new ArrayList<EmacsButton> (); | ||
| 120 | dialog.title = title; | ||
| 121 | dialog.text = text; | ||
| 122 | dialog.menuEventSerial = menuEventSerial; | ||
| 123 | |||
| 124 | return dialog; | ||
| 125 | } | ||
| 126 | |||
| 127 | /* Add a button named NAME, with the identifier ID. If DISABLE, | ||
| 128 | disable the button. */ | ||
| 129 | |||
| 130 | public void | ||
| 131 | addButton (String name, int id, boolean disable) | ||
| 132 | { | ||
| 133 | EmacsButton button; | ||
| 134 | |||
| 135 | button = new EmacsButton (); | ||
| 136 | button.name = name; | ||
| 137 | button.id = id; | ||
| 138 | button.enabled = !disable; | ||
| 139 | buttons.add (button); | ||
| 140 | } | ||
| 141 | |||
| 142 | /* Turn this dialog into an AlertDialog for the specified | ||
| 143 | CONTEXT. | ||
| 144 | |||
| 145 | Upon a button being selected, the dialog will send an | ||
| 146 | ANDROID_CONTEXT_MENU event with the id of that button. | ||
| 147 | |||
| 148 | Upon the dialog being dismissed, an ANDROID_CONTEXT_MENU event | ||
| 149 | will be sent with an id of 0. */ | ||
| 150 | |||
| 151 | public AlertDialog | ||
| 152 | toAlertDialog (Context context) | ||
| 153 | { | ||
| 154 | AlertDialog dialog; | ||
| 155 | int size, styleId, flag; | ||
| 156 | int[] attrs; | ||
| 157 | EmacsButton button; | ||
| 158 | EmacsDialogButtonLayout layout; | ||
| 159 | Button buttonView; | ||
| 160 | ViewGroup.LayoutParams layoutParams; | ||
| 161 | Theme theme; | ||
| 162 | TypedArray attributes; | ||
| 163 | Window window; | ||
| 164 | |||
| 165 | size = buttons.size (); | ||
| 166 | styleId = -1; | ||
| 167 | |||
| 168 | if (size <= 3) | ||
| 169 | { | ||
| 170 | dialog = new AlertDialog.Builder (context).create (); | ||
| 171 | dialog.setMessage (text); | ||
| 172 | dialog.setCancelable (true); | ||
| 173 | dialog.setOnDismissListener (this); | ||
| 174 | |||
| 175 | if (title != null) | ||
| 176 | dialog.setTitle (title); | ||
| 177 | |||
| 178 | /* There are less than 4 buttons. Add the buttons the way | ||
| 179 | Android intends them to be added. */ | ||
| 180 | |||
| 181 | if (size >= 1) | ||
| 182 | { | ||
| 183 | button = buttons.get (0); | ||
| 184 | dialog.setButton (DialogInterface.BUTTON_POSITIVE, | ||
| 185 | button.name, button); | ||
| 186 | } | ||
| 187 | |||
| 188 | if (size >= 2) | ||
| 189 | { | ||
| 190 | button = buttons.get (1); | ||
| 191 | dialog.setButton (DialogInterface.BUTTON_NEGATIVE, | ||
| 192 | button.name, button); | ||
| 193 | } | ||
| 194 | |||
| 195 | if (size >= 3) | ||
| 196 | { | ||
| 197 | button = buttons.get (2); | ||
| 198 | dialog.setButton (DialogInterface.BUTTON_NEUTRAL, | ||
| 199 | button.name, button); | ||
| 200 | } | ||
| 201 | } | ||
| 202 | else | ||
| 203 | { | ||
| 204 | /* There are more than 3 buttons. Add them all to a special | ||
| 205 | container widget that handles wrapping. First, create the | ||
| 206 | layout. */ | ||
| 207 | |||
| 208 | layout = new EmacsDialogButtonLayout (context); | ||
| 209 | layoutParams | ||
| 210 | = new FrameLayout.LayoutParams (ViewGroup.LayoutParams.MATCH_PARENT, | ||
| 211 | ViewGroup.LayoutParams.WRAP_CONTENT); | ||
| 212 | layout.setLayoutParams (layoutParams); | ||
| 213 | |||
| 214 | /* Add that layout to the dialog's custom view. | ||
| 215 | |||
| 216 | android.R.id.custom is documented to work. But looking it | ||
| 217 | up returns NULL, so setView must be used instead. */ | ||
| 218 | |||
| 219 | dialog = new AlertDialog.Builder (context).setView (layout).create (); | ||
| 220 | dialog.setMessage (text); | ||
| 221 | dialog.setCancelable (true); | ||
| 222 | dialog.setOnDismissListener (this); | ||
| 223 | |||
| 224 | if (title != null) | ||
| 225 | dialog.setTitle (title); | ||
| 226 | |||
| 227 | /* Now that the dialog has been created, set the style of each | ||
| 228 | custom button to match the usual dialog buttons found on | ||
| 229 | Android 5 and later. */ | ||
| 230 | |||
| 231 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) | ||
| 232 | { | ||
| 233 | /* Obtain the Theme associated with the dialog. */ | ||
| 234 | theme = dialog.getContext ().getTheme (); | ||
| 235 | |||
| 236 | /* Resolve the dialog button style. */ | ||
| 237 | attrs | ||
| 238 | = new int [] { android.R.attr.buttonBarNeutralButtonStyle, }; | ||
| 239 | |||
| 240 | try | ||
| 241 | { | ||
| 242 | attributes = theme.obtainStyledAttributes (attrs); | ||
| 243 | |||
| 244 | /* Look for the style ID. Default to -1 if it could | ||
| 245 | not be found. */ | ||
| 246 | styleId = attributes.getResourceId (0, -1); | ||
| 247 | |||
| 248 | /* Now clean up the TypedAttributes object. */ | ||
| 249 | attributes.recycle (); | ||
| 250 | } | ||
| 251 | catch (NotFoundException e) | ||
| 252 | { | ||
| 253 | /* Nothing to do here. */ | ||
| 254 | } | ||
| 255 | } | ||
| 256 | |||
| 257 | /* Create each button and add it to the layout. Set the style | ||
| 258 | if necessary. */ | ||
| 259 | |||
| 260 | for (EmacsButton emacsButton : buttons) | ||
| 261 | { | ||
| 262 | if (styleId == -1) | ||
| 263 | /* No specific style... */ | ||
| 264 | buttonView = new Button (context); | ||
| 265 | else | ||
| 266 | /* Use the given styleId. */ | ||
| 267 | buttonView = new Button (context, null, 0, styleId); | ||
| 268 | |||
| 269 | /* Set the text and on click handler. */ | ||
| 270 | buttonView.setText (emacsButton.name); | ||
| 271 | buttonView.setOnClickListener (emacsButton); | ||
| 272 | buttonView.setEnabled (emacsButton.enabled); | ||
| 273 | layout.addView (buttonView); | ||
| 274 | } | ||
| 275 | } | ||
| 276 | |||
| 277 | return dialog; | ||
| 278 | } | ||
| 279 | |||
| 280 | /* Internal helper for display run on the main thread. */ | ||
| 281 | |||
| 282 | @SuppressWarnings("deprecation") | ||
| 283 | private boolean | ||
| 284 | display1 () | ||
| 285 | { | ||
| 286 | Context context; | ||
| 287 | int size, type; | ||
| 288 | Button buttonView; | ||
| 289 | EmacsButton button; | ||
| 290 | AlertDialog dialog; | ||
| 291 | Window window; | ||
| 292 | |||
| 293 | if (EmacsActivity.focusedActivities.isEmpty ()) | ||
| 294 | { | ||
| 295 | /* If focusedActivities is empty then this dialog may have | ||
| 296 | been displayed immediately after another popup dialog was | ||
| 297 | dismissed. Or Emacs might legitimately be in the | ||
| 298 | background, possibly displaying this popup in response to | ||
| 299 | an Emacsclient request. Try the service context if it will | ||
| 300 | work, then any focused EmacsOpenActivity, and finally the | ||
| 301 | last EmacsActivity to be focused. */ | ||
| 302 | |||
| 303 | Log.d (TAG, "display1: no focused activities..."); | ||
| 304 | Log.d (TAG, ("display1: EmacsOpenActivity.currentActivity: " | ||
| 305 | + EmacsOpenActivity.currentActivity)); | ||
| 306 | |||
| 307 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M | ||
| 308 | || Settings.canDrawOverlays (EmacsService.SERVICE)) | ||
| 309 | context = EmacsService.SERVICE; | ||
| 310 | else if (EmacsOpenActivity.currentActivity != null) | ||
| 311 | context = EmacsOpenActivity.currentActivity; | ||
| 312 | else | ||
| 313 | context = EmacsActivity.lastFocusedActivity; | ||
| 314 | |||
| 315 | if (context == null) | ||
| 316 | return false; | ||
| 317 | } | ||
| 318 | else | ||
| 319 | /* Display using the activity context when Emacs is in the | ||
| 320 | foreground, as this allows the dialog to be dismissed more | ||
| 321 | consistently. */ | ||
| 322 | context = EmacsActivity.focusedActivities.get (0); | ||
| 323 | |||
| 324 | Log.d (TAG, "display1: using context " + context); | ||
| 325 | |||
| 326 | dialog = dismissDialog = toAlertDialog (context); | ||
| 327 | |||
| 328 | try | ||
| 329 | { | ||
| 330 | if (context == EmacsService.SERVICE) | ||
| 331 | { | ||
| 332 | /* Apply the system alert window type to make sure this | ||
| 333 | dialog can be displayed. */ | ||
| 334 | |||
| 335 | window = dialog.getWindow (); | ||
| 336 | type = (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O | ||
| 337 | ? WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY | ||
| 338 | : WindowManager.LayoutParams.TYPE_PHONE); | ||
| 339 | window.setType (type); | ||
| 340 | } | ||
| 341 | |||
| 342 | dismissDialog.show (); | ||
| 343 | } | ||
| 344 | catch (Exception exception) | ||
| 345 | { | ||
| 346 | /* This can happen when the system decides Emacs is not in the | ||
| 347 | foreground any longer. */ | ||
| 348 | return false; | ||
| 349 | } | ||
| 350 | |||
| 351 | /* If there are less than four buttons, then they must be | ||
| 352 | individually enabled or disabled after the dialog is | ||
| 353 | displayed. */ | ||
| 354 | size = buttons.size (); | ||
| 355 | |||
| 356 | if (size <= 3) | ||
| 357 | { | ||
| 358 | if (size >= 1) | ||
| 359 | { | ||
| 360 | button = buttons.get (0); | ||
| 361 | buttonView | ||
| 362 | = dialog.getButton (DialogInterface.BUTTON_POSITIVE); | ||
| 363 | buttonView.setEnabled (button.enabled); | ||
| 364 | } | ||
| 365 | |||
| 366 | if (size >= 2) | ||
| 367 | { | ||
| 368 | button = buttons.get (1); | ||
| 369 | buttonView | ||
| 370 | = dialog.getButton (DialogInterface.BUTTON_NEGATIVE); | ||
| 371 | buttonView.setEnabled (button.enabled); | ||
| 372 | } | ||
| 373 | |||
| 374 | if (size >= 3) | ||
| 375 | { | ||
| 376 | button = buttons.get (2); | ||
| 377 | buttonView | ||
| 378 | = dialog.getButton (DialogInterface.BUTTON_NEUTRAL); | ||
| 379 | buttonView.setEnabled (button.enabled); | ||
| 380 | } | ||
| 381 | } | ||
| 382 | |||
| 383 | return true; | ||
| 384 | } | ||
| 385 | |||
| 386 | /* Display this dialog for a suitable activity. | ||
| 387 | Value is false if the dialog could not be displayed, | ||
| 388 | and true otherwise. */ | ||
| 389 | |||
| 390 | public boolean | ||
| 391 | display () | ||
| 392 | { | ||
| 393 | Runnable runnable; | ||
| 394 | final EmacsHolder<Boolean> rc; | ||
| 395 | |||
| 396 | rc = new EmacsHolder<Boolean> (); | ||
| 397 | runnable = new Runnable () { | ||
| 398 | @Override | ||
| 399 | public void | ||
| 400 | run () | ||
| 401 | { | ||
| 402 | synchronized (this) | ||
| 403 | { | ||
| 404 | rc.thing = display1 (); | ||
| 405 | notify (); | ||
| 406 | } | ||
| 407 | } | ||
| 408 | }; | ||
| 409 | |||
| 410 | EmacsService.syncRunnable (runnable); | ||
| 411 | return rc.thing; | ||
| 412 | } | ||
| 413 | |||
| 414 | |||
| 415 | |||
| 416 | @Override | ||
| 417 | public void | ||
| 418 | onDismiss (DialogInterface dialog) | ||
| 419 | { | ||
| 420 | Log.d (TAG, "onDismiss: " + this); | ||
| 421 | |||
| 422 | if (wasButtonClicked) | ||
| 423 | return; | ||
| 424 | |||
| 425 | EmacsNative.sendContextMenu ((short) 0, 0, menuEventSerial); | ||
| 426 | } | ||
| 427 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsDialogButtonLayout.java b/java/org/gnu/emacs/EmacsDialogButtonLayout.java new file mode 100644 index 00000000000..fd8d63d81d3 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDialogButtonLayout.java | |||
| @@ -0,0 +1,152 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | |||
| 23 | |||
| 24 | import android.content.Context; | ||
| 25 | |||
| 26 | import android.view.View; | ||
| 27 | import android.view.View.MeasureSpec; | ||
| 28 | import android.view.ViewGroup; | ||
| 29 | |||
| 30 | |||
| 31 | |||
| 32 | /* This ``view group'' implements a container widget for multiple | ||
| 33 | buttons of the type found in pop-up dialogs. It is used when | ||
| 34 | displaying a dialog box that contains more than three buttons, as | ||
| 35 | the default dialog box widget is not capable of holding more than | ||
| 36 | that many. */ | ||
| 37 | |||
| 38 | |||
| 39 | |||
| 40 | public final class EmacsDialogButtonLayout extends ViewGroup | ||
| 41 | { | ||
| 42 | public | ||
| 43 | EmacsDialogButtonLayout (Context context) | ||
| 44 | { | ||
| 45 | super (context); | ||
| 46 | } | ||
| 47 | |||
| 48 | @Override | ||
| 49 | protected void | ||
| 50 | onMeasure (int widthMeasureSpec, int heightMeasureSpec) | ||
| 51 | { | ||
| 52 | int width, count, i, x, y, height, spec, tempSpec; | ||
| 53 | View view; | ||
| 54 | |||
| 55 | /* Obtain the width of this widget and create the measure | ||
| 56 | specification used to measure children. */ | ||
| 57 | |||
| 58 | width = MeasureSpec.getSize (widthMeasureSpec); | ||
| 59 | spec = MeasureSpec.makeMeasureSpec (0, MeasureSpec.UNSPECIFIED); | ||
| 60 | tempSpec | ||
| 61 | = MeasureSpec.makeMeasureSpec (width, MeasureSpec.AT_MOST); | ||
| 62 | x = y = height = 0; | ||
| 63 | |||
| 64 | /* Run through each widget. */ | ||
| 65 | |||
| 66 | count = getChildCount (); | ||
| 67 | |||
| 68 | for (i = 0; i < count; ++i) | ||
| 69 | { | ||
| 70 | view = getChildAt (i); | ||
| 71 | |||
| 72 | /* Measure this view. */ | ||
| 73 | view.measure (spec, spec); | ||
| 74 | |||
| 75 | if (width - x < view.getMeasuredWidth ()) | ||
| 76 | { | ||
| 77 | /* Move onto the next line, unless this line is empty. */ | ||
| 78 | |||
| 79 | if (x != 0) | ||
| 80 | { | ||
| 81 | y += height; | ||
| 82 | height = x = 0; | ||
| 83 | } | ||
| 84 | |||
| 85 | if (view.getMeasuredWidth () > width) | ||
| 86 | /* Measure the view again, this time forcing it to be at | ||
| 87 | most width wide, if it is not already. */ | ||
| 88 | view.measure (tempSpec, spec); | ||
| 89 | } | ||
| 90 | |||
| 91 | height = Math.max (height, view.getMeasuredHeight ()); | ||
| 92 | x += view.getMeasuredWidth (); | ||
| 93 | } | ||
| 94 | |||
| 95 | /* Now set the measured size of this widget. */ | ||
| 96 | setMeasuredDimension (width, y + height); | ||
| 97 | } | ||
| 98 | |||
| 99 | @Override | ||
| 100 | protected void | ||
| 101 | onLayout (boolean changed, int left, int top, int right, | ||
| 102 | int bottom) | ||
| 103 | { | ||
| 104 | int width, count, i, x, y, height, spec, tempSpec; | ||
| 105 | View view; | ||
| 106 | |||
| 107 | /* Obtain the width of this widget and create the measure | ||
| 108 | specification used to measure children. */ | ||
| 109 | |||
| 110 | width = getMeasuredWidth (); | ||
| 111 | spec = MeasureSpec.makeMeasureSpec (0, MeasureSpec.UNSPECIFIED); | ||
| 112 | tempSpec | ||
| 113 | = MeasureSpec.makeMeasureSpec (width, MeasureSpec.AT_MOST); | ||
| 114 | x = y = height = 0; | ||
| 115 | |||
| 116 | /* Run through each widget. */ | ||
| 117 | |||
| 118 | count = getChildCount (); | ||
| 119 | |||
| 120 | for (i = 0; i < count; ++i) | ||
| 121 | { | ||
| 122 | view = getChildAt (i); | ||
| 123 | |||
| 124 | /* Measure this view. */ | ||
| 125 | view.measure (spec, spec); | ||
| 126 | |||
| 127 | if (width - x < view.getMeasuredWidth ()) | ||
| 128 | { | ||
| 129 | /* Move onto the next line, unless this line is empty. */ | ||
| 130 | |||
| 131 | if (x != 0) | ||
| 132 | { | ||
| 133 | y += height; | ||
| 134 | height = x = 0; | ||
| 135 | } | ||
| 136 | |||
| 137 | if (view.getMeasuredWidth () > width) | ||
| 138 | /* Measure the view again, this time forcing it to be at | ||
| 139 | most width wide, if it is not already. */ | ||
| 140 | view.measure (tempSpec, spec); | ||
| 141 | } | ||
| 142 | |||
| 143 | /* Now assign this view its position. */ | ||
| 144 | view.layout (x, y, x + view.getMeasuredWidth (), | ||
| 145 | y + view.getMeasuredHeight ()); | ||
| 146 | |||
| 147 | /* And move on to the next widget. */ | ||
| 148 | height = Math.max (height, view.getMeasuredHeight ()); | ||
| 149 | x += view.getMeasuredWidth (); | ||
| 150 | } | ||
| 151 | } | ||
| 152 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsDirectoryEntry.java b/java/org/gnu/emacs/EmacsDirectoryEntry.java new file mode 100644 index 00000000000..75c52e48002 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDirectoryEntry.java | |||
| @@ -0,0 +1,33 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | /* Structure holding a single ``directory entry'' from a document | ||
| 23 | provider. */ | ||
| 24 | |||
| 25 | public final class EmacsDirectoryEntry | ||
| 26 | { | ||
| 27 | /* The type of this directory entry. 0 means a regular file and 1 | ||
| 28 | means a directory. */ | ||
| 29 | public int d_type; | ||
| 30 | |||
| 31 | /* The display name of the file represented. */ | ||
| 32 | public String d_name; | ||
| 33 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java new file mode 100644 index 00000000000..96dc2bc6e14 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java | |||
| @@ -0,0 +1,578 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.content.Context; | ||
| 23 | |||
| 24 | import android.database.Cursor; | ||
| 25 | import android.database.MatrixCursor; | ||
| 26 | |||
| 27 | import android.os.Build; | ||
| 28 | import android.os.CancellationSignal; | ||
| 29 | import android.os.ParcelFileDescriptor; | ||
| 30 | |||
| 31 | import android.provider.DocumentsContract.Document; | ||
| 32 | import android.provider.DocumentsContract.Root; | ||
| 33 | import static android.provider.DocumentsContract.buildChildDocumentsUri; | ||
| 34 | import android.provider.DocumentsProvider; | ||
| 35 | |||
| 36 | import android.webkit.MimeTypeMap; | ||
| 37 | |||
| 38 | import android.net.Uri; | ||
| 39 | |||
| 40 | import java.io.File; | ||
| 41 | import java.io.FileInputStream; | ||
| 42 | import java.io.FileNotFoundException; | ||
| 43 | import java.io.FileOutputStream; | ||
| 44 | import java.io.IOException; | ||
| 45 | |||
| 46 | /* ``Documents provider''. This allows Emacs's home directory to be | ||
| 47 | modified by other programs holding permissions to manage system | ||
| 48 | storage, which is useful to (for example) correct misconfigurations | ||
| 49 | which prevent Emacs from starting up. | ||
| 50 | |||
| 51 | This functionality is only available on Android 19 and later. */ | ||
| 52 | |||
| 53 | public final class EmacsDocumentsProvider extends DocumentsProvider | ||
| 54 | { | ||
| 55 | /* Home directory. This is the directory whose contents are | ||
| 56 | initially returned to requesting applications. */ | ||
| 57 | private File baseDir; | ||
| 58 | |||
| 59 | /* The default projection for requests for the root directory. */ | ||
| 60 | private static final String[] DEFAULT_ROOT_PROJECTION; | ||
| 61 | |||
| 62 | /* The default projection for requests for a file. */ | ||
| 63 | private static final String[] DEFAULT_DOCUMENT_PROJECTION; | ||
| 64 | |||
| 65 | static | ||
| 66 | { | ||
| 67 | DEFAULT_ROOT_PROJECTION = new String[] { | ||
| 68 | Root.COLUMN_ROOT_ID, | ||
| 69 | Root.COLUMN_MIME_TYPES, | ||
| 70 | Root.COLUMN_FLAGS, | ||
| 71 | Root.COLUMN_ICON, | ||
| 72 | Root.COLUMN_TITLE, | ||
| 73 | Root.COLUMN_SUMMARY, | ||
| 74 | Root.COLUMN_DOCUMENT_ID, | ||
| 75 | Root.COLUMN_AVAILABLE_BYTES, | ||
| 76 | }; | ||
| 77 | |||
| 78 | DEFAULT_DOCUMENT_PROJECTION = new String[] { | ||
| 79 | Document.COLUMN_DOCUMENT_ID, | ||
| 80 | Document.COLUMN_MIME_TYPE, | ||
| 81 | Document.COLUMN_DISPLAY_NAME, | ||
| 82 | Document.COLUMN_LAST_MODIFIED, | ||
| 83 | Document.COLUMN_FLAGS, | ||
| 84 | Document.COLUMN_SIZE, | ||
| 85 | }; | ||
| 86 | } | ||
| 87 | |||
| 88 | @Override | ||
| 89 | public boolean | ||
| 90 | onCreate () | ||
| 91 | { | ||
| 92 | /* Set the base directory to Emacs's files directory. */ | ||
| 93 | baseDir = getContext ().getFilesDir (); | ||
| 94 | return true; | ||
| 95 | } | ||
| 96 | |||
| 97 | @Override | ||
| 98 | public Cursor | ||
| 99 | queryRoots (String[] projection) | ||
| 100 | { | ||
| 101 | MatrixCursor result; | ||
| 102 | MatrixCursor.RowBuilder row; | ||
| 103 | |||
| 104 | /* If the requestor asked for nothing at all, then it wants some | ||
| 105 | data by default. */ | ||
| 106 | |||
| 107 | if (projection == null) | ||
| 108 | projection = DEFAULT_ROOT_PROJECTION; | ||
| 109 | |||
| 110 | result = new MatrixCursor (projection); | ||
| 111 | row = result.newRow (); | ||
| 112 | |||
| 113 | /* Now create and add a row for each file in the base | ||
| 114 | directory. */ | ||
| 115 | row.add (Root.COLUMN_ROOT_ID, baseDir.getAbsolutePath ()); | ||
| 116 | row.add (Root.COLUMN_SUMMARY, "Emacs home directory"); | ||
| 117 | |||
| 118 | /* Add the appropriate flags. */ | ||
| 119 | |||
| 120 | row.add (Root.COLUMN_FLAGS, (Root.FLAG_SUPPORTS_CREATE | ||
| 121 | | Root.FLAG_SUPPORTS_IS_CHILD)); | ||
| 122 | row.add (Root.COLUMN_ICON, R.drawable.emacs); | ||
| 123 | row.add (Root.FLAG_LOCAL_ONLY); | ||
| 124 | row.add (Root.COLUMN_TITLE, "Emacs"); | ||
| 125 | row.add (Root.COLUMN_DOCUMENT_ID, baseDir.getAbsolutePath ()); | ||
| 126 | |||
| 127 | return result; | ||
| 128 | } | ||
| 129 | |||
| 130 | private Uri | ||
| 131 | getNotificationUri (File file) | ||
| 132 | { | ||
| 133 | Uri updatedUri; | ||
| 134 | |||
| 135 | updatedUri | ||
| 136 | = buildChildDocumentsUri ("org.gnu.emacs", | ||
| 137 | file.getAbsolutePath ()); | ||
| 138 | |||
| 139 | return updatedUri; | ||
| 140 | } | ||
| 141 | |||
| 142 | /* Inform the system that FILE's contents (or FILE itself) has | ||
| 143 | changed. */ | ||
| 144 | |||
| 145 | private void | ||
| 146 | notifyChange (File file) | ||
| 147 | { | ||
| 148 | Uri updatedUri; | ||
| 149 | Context context; | ||
| 150 | |||
| 151 | context = getContext (); | ||
| 152 | updatedUri | ||
| 153 | = buildChildDocumentsUri ("org.gnu.emacs", | ||
| 154 | file.getAbsolutePath ()); | ||
| 155 | context.getContentResolver ().notifyChange (updatedUri, null); | ||
| 156 | } | ||
| 157 | |||
| 158 | /* Inform the system that FILE's contents (or FILE itself) has | ||
| 159 | changed. FILE is a string describing containing the file name of | ||
| 160 | a directory as opposed to a File. */ | ||
| 161 | |||
| 162 | private void | ||
| 163 | notifyChangeByName (String file) | ||
| 164 | { | ||
| 165 | Uri updatedUri; | ||
| 166 | Context context; | ||
| 167 | |||
| 168 | context = getContext (); | ||
| 169 | updatedUri | ||
| 170 | = buildChildDocumentsUri ("org.gnu.emacs", file); | ||
| 171 | context.getContentResolver ().notifyChange (updatedUri, null); | ||
| 172 | } | ||
| 173 | |||
| 174 | /* Return the MIME type of a file FILE. */ | ||
| 175 | |||
| 176 | private String | ||
| 177 | getMimeType (File file) | ||
| 178 | { | ||
| 179 | String name, extension, mime; | ||
| 180 | int extensionSeparator; | ||
| 181 | MimeTypeMap singleton; | ||
| 182 | |||
| 183 | if (file.isDirectory ()) | ||
| 184 | return Document.MIME_TYPE_DIR; | ||
| 185 | |||
| 186 | /* Abuse WebView stuff to get the file's MIME type. */ | ||
| 187 | name = file.getName (); | ||
| 188 | extensionSeparator = name.lastIndexOf ('.'); | ||
| 189 | |||
| 190 | if (extensionSeparator > 0) | ||
| 191 | { | ||
| 192 | singleton = MimeTypeMap.getSingleton (); | ||
| 193 | extension = name.substring (extensionSeparator + 1); | ||
| 194 | mime = singleton.getMimeTypeFromExtension (extension); | ||
| 195 | |||
| 196 | if (mime != null) | ||
| 197 | return mime; | ||
| 198 | } | ||
| 199 | |||
| 200 | return "application/octet-stream"; | ||
| 201 | } | ||
| 202 | |||
| 203 | /* Append the specified FILE to the query result RESULT. | ||
| 204 | Handle both directories and ordinary files. */ | ||
| 205 | |||
| 206 | private void | ||
| 207 | queryDocument1 (MatrixCursor result, File file) | ||
| 208 | { | ||
| 209 | MatrixCursor.RowBuilder row; | ||
| 210 | String fileName, displayName, mimeType; | ||
| 211 | int flags; | ||
| 212 | |||
| 213 | row = result.newRow (); | ||
| 214 | flags = 0; | ||
| 215 | |||
| 216 | /* fileName is a string that the system will ask for some time in | ||
| 217 | the future. Here, it is just the absolute name of the file. */ | ||
| 218 | fileName = file.getAbsolutePath (); | ||
| 219 | |||
| 220 | /* If file is a directory, add the right flags for that. */ | ||
| 221 | |||
| 222 | if (file.isDirectory ()) | ||
| 223 | { | ||
| 224 | if (file.canWrite ()) | ||
| 225 | { | ||
| 226 | flags |= Document.FLAG_DIR_SUPPORTS_CREATE; | ||
| 227 | flags |= Document.FLAG_SUPPORTS_DELETE; | ||
| 228 | |||
| 229 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) | ||
| 230 | flags |= Document.FLAG_SUPPORTS_RENAME; | ||
| 231 | |||
| 232 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) | ||
| 233 | flags |= Document.FLAG_SUPPORTS_MOVE; | ||
| 234 | } | ||
| 235 | } | ||
| 236 | else if (file.canWrite ()) | ||
| 237 | { | ||
| 238 | /* Apply the correct flags for a writable file. */ | ||
| 239 | flags |= Document.FLAG_SUPPORTS_WRITE; | ||
| 240 | flags |= Document.FLAG_SUPPORTS_DELETE; | ||
| 241 | |||
| 242 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) | ||
| 243 | flags |= Document.FLAG_SUPPORTS_RENAME; | ||
| 244 | |||
| 245 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) | ||
| 246 | { | ||
| 247 | flags |= Document.FLAG_SUPPORTS_REMOVE; | ||
| 248 | flags |= Document.FLAG_SUPPORTS_MOVE; | ||
| 249 | } | ||
| 250 | } | ||
| 251 | |||
| 252 | displayName = file.getName (); | ||
| 253 | mimeType = getMimeType (file); | ||
| 254 | |||
| 255 | row.add (Document.COLUMN_DOCUMENT_ID, fileName); | ||
| 256 | row.add (Document.COLUMN_DISPLAY_NAME, displayName); | ||
| 257 | row.add (Document.COLUMN_SIZE, file.length ()); | ||
| 258 | row.add (Document.COLUMN_MIME_TYPE, mimeType); | ||
| 259 | row.add (Document.COLUMN_LAST_MODIFIED, file.lastModified ()); | ||
| 260 | row.add (Document.COLUMN_FLAGS, flags); | ||
| 261 | } | ||
| 262 | |||
| 263 | @Override | ||
| 264 | public Cursor | ||
| 265 | queryDocument (String documentId, String[] projection) | ||
| 266 | throws FileNotFoundException | ||
| 267 | { | ||
| 268 | MatrixCursor result; | ||
| 269 | File file; | ||
| 270 | Context context; | ||
| 271 | |||
| 272 | file = new File (documentId); | ||
| 273 | context = getContext (); | ||
| 274 | |||
| 275 | if (projection == null) | ||
| 276 | projection = DEFAULT_DOCUMENT_PROJECTION; | ||
| 277 | |||
| 278 | result = new MatrixCursor (projection); | ||
| 279 | queryDocument1 (result, file); | ||
| 280 | |||
| 281 | /* Now allow interested applications to detect changes. */ | ||
| 282 | result.setNotificationUri (context.getContentResolver (), | ||
| 283 | getNotificationUri (file)); | ||
| 284 | |||
| 285 | return result; | ||
| 286 | } | ||
| 287 | |||
| 288 | @Override | ||
| 289 | public Cursor | ||
| 290 | queryChildDocuments (String parentDocumentId, String[] projection, | ||
| 291 | String sortOrder) throws FileNotFoundException | ||
| 292 | { | ||
| 293 | MatrixCursor result; | ||
| 294 | File directory; | ||
| 295 | File[] files; | ||
| 296 | Context context; | ||
| 297 | |||
| 298 | if (projection == null) | ||
| 299 | projection = DEFAULT_DOCUMENT_PROJECTION; | ||
| 300 | |||
| 301 | result = new MatrixCursor (projection); | ||
| 302 | |||
| 303 | /* Try to open the file corresponding to the location being | ||
| 304 | requested. */ | ||
| 305 | directory = new File (parentDocumentId); | ||
| 306 | |||
| 307 | /* Look up each child. */ | ||
| 308 | files = directory.listFiles (); | ||
| 309 | |||
| 310 | if (files != null) | ||
| 311 | { | ||
| 312 | /* Now add each child. */ | ||
| 313 | for (File child : files) | ||
| 314 | queryDocument1 (result, child); | ||
| 315 | } | ||
| 316 | |||
| 317 | context = getContext (); | ||
| 318 | |||
| 319 | /* Now allow interested applications to detect changes. */ | ||
| 320 | result.setNotificationUri (context.getContentResolver (), | ||
| 321 | getNotificationUri (directory)); | ||
| 322 | |||
| 323 | return result; | ||
| 324 | } | ||
| 325 | |||
| 326 | @Override | ||
| 327 | public ParcelFileDescriptor | ||
| 328 | openDocument (String documentId, String mode, | ||
| 329 | CancellationSignal signal) throws FileNotFoundException | ||
| 330 | { | ||
| 331 | return ParcelFileDescriptor.open (new File (documentId), | ||
| 332 | ParcelFileDescriptor.parseMode (mode)); | ||
| 333 | } | ||
| 334 | |||
| 335 | @Override | ||
| 336 | public String | ||
| 337 | createDocument (String documentId, String mimeType, | ||
| 338 | String displayName) throws FileNotFoundException | ||
| 339 | { | ||
| 340 | File file, parentFile; | ||
| 341 | boolean rc; | ||
| 342 | |||
| 343 | file = new File (documentId, displayName); | ||
| 344 | |||
| 345 | try | ||
| 346 | { | ||
| 347 | rc = false; | ||
| 348 | |||
| 349 | if (Document.MIME_TYPE_DIR.equals (mimeType)) | ||
| 350 | { | ||
| 351 | file.mkdirs (); | ||
| 352 | |||
| 353 | if (file.isDirectory ()) | ||
| 354 | rc = true; | ||
| 355 | } | ||
| 356 | else | ||
| 357 | { | ||
| 358 | file.createNewFile (); | ||
| 359 | |||
| 360 | if (file.isFile () | ||
| 361 | && file.setWritable (true) | ||
| 362 | && file.setReadable (true)) | ||
| 363 | rc = true; | ||
| 364 | } | ||
| 365 | |||
| 366 | if (!rc) | ||
| 367 | throw new FileNotFoundException ("rc != 1"); | ||
| 368 | } | ||
| 369 | catch (IOException e) | ||
| 370 | { | ||
| 371 | throw new FileNotFoundException (e.toString ()); | ||
| 372 | } | ||
| 373 | |||
| 374 | parentFile = file.getParentFile (); | ||
| 375 | |||
| 376 | if (parentFile != null) | ||
| 377 | notifyChange (parentFile); | ||
| 378 | |||
| 379 | return file.getAbsolutePath (); | ||
| 380 | } | ||
| 381 | |||
| 382 | private void | ||
| 383 | deleteDocument1 (File child) | ||
| 384 | { | ||
| 385 | File[] children; | ||
| 386 | |||
| 387 | /* Don't delete symlinks recursively. | ||
| 388 | |||
| 389 | Calling readlink or stat is problematic due to file name | ||
| 390 | encoding problems, so try to delete the file first, and only | ||
| 391 | try to delete files recursively afterword. */ | ||
| 392 | |||
| 393 | if (child.delete ()) | ||
| 394 | return; | ||
| 395 | |||
| 396 | children = child.listFiles (); | ||
| 397 | |||
| 398 | if (children != null) | ||
| 399 | { | ||
| 400 | for (File file : children) | ||
| 401 | deleteDocument1 (file); | ||
| 402 | } | ||
| 403 | |||
| 404 | child.delete (); | ||
| 405 | } | ||
| 406 | |||
| 407 | @Override | ||
| 408 | public void | ||
| 409 | deleteDocument (String documentId) | ||
| 410 | throws FileNotFoundException | ||
| 411 | { | ||
| 412 | File file, parent; | ||
| 413 | File[] children; | ||
| 414 | |||
| 415 | /* Java makes recursively deleting a file hard. File name | ||
| 416 | encoding issues also prevent easily calling into C... */ | ||
| 417 | |||
| 418 | file = new File (documentId); | ||
| 419 | parent = file.getParentFile (); | ||
| 420 | |||
| 421 | if (parent == null) | ||
| 422 | throw new RuntimeException ("trying to delete file without" | ||
| 423 | + " parent!"); | ||
| 424 | |||
| 425 | if (file.delete ()) | ||
| 426 | { | ||
| 427 | /* Tell the system about the change. */ | ||
| 428 | notifyChange (parent); | ||
| 429 | return; | ||
| 430 | } | ||
| 431 | |||
| 432 | children = file.listFiles (); | ||
| 433 | |||
| 434 | if (children != null) | ||
| 435 | { | ||
| 436 | for (File child : children) | ||
| 437 | deleteDocument1 (child); | ||
| 438 | } | ||
| 439 | |||
| 440 | if (file.delete ()) | ||
| 441 | /* Tell the system about the change. */ | ||
| 442 | notifyChange (parent); | ||
| 443 | } | ||
| 444 | |||
| 445 | @Override | ||
| 446 | public void | ||
| 447 | removeDocument (String documentId, String parentDocumentId) | ||
| 448 | throws FileNotFoundException | ||
| 449 | { | ||
| 450 | deleteDocument (documentId); | ||
| 451 | } | ||
| 452 | |||
| 453 | @Override | ||
| 454 | public String | ||
| 455 | getDocumentType (String documentId) | ||
| 456 | { | ||
| 457 | return getMimeType (new File (documentId)); | ||
| 458 | } | ||
| 459 | |||
| 460 | @Override | ||
| 461 | public String | ||
| 462 | renameDocument (String documentId, String displayName) | ||
| 463 | throws FileNotFoundException | ||
| 464 | { | ||
| 465 | File file, newName; | ||
| 466 | File parent; | ||
| 467 | |||
| 468 | file = new File (documentId); | ||
| 469 | parent = file.getParentFile (); | ||
| 470 | newName = new File (parent, displayName); | ||
| 471 | |||
| 472 | if (parent == null) | ||
| 473 | throw new FileNotFoundException ("parent is null"); | ||
| 474 | |||
| 475 | file = new File (documentId); | ||
| 476 | |||
| 477 | if (!file.renameTo (newName)) | ||
| 478 | return null; | ||
| 479 | |||
| 480 | notifyChange (parent); | ||
| 481 | return newName.getAbsolutePath (); | ||
| 482 | } | ||
| 483 | |||
| 484 | @Override | ||
| 485 | public boolean | ||
| 486 | isChildDocument (String parentDocumentId, String documentId) | ||
| 487 | { | ||
| 488 | return documentId.startsWith (parentDocumentId); | ||
| 489 | } | ||
| 490 | |||
| 491 | @Override | ||
| 492 | public String | ||
| 493 | moveDocument (String sourceDocumentId, | ||
| 494 | String sourceParentDocumentId, | ||
| 495 | String targetParentDocumentId) | ||
| 496 | throws FileNotFoundException | ||
| 497 | { | ||
| 498 | File file, newName; | ||
| 499 | FileInputStream inputStream; | ||
| 500 | FileOutputStream outputStream; | ||
| 501 | byte buffer[]; | ||
| 502 | int length; | ||
| 503 | |||
| 504 | file = new File (sourceDocumentId); | ||
| 505 | |||
| 506 | /* Now, create the file name of the parent document. */ | ||
| 507 | newName = new File (targetParentDocumentId, | ||
| 508 | file.getName ()); | ||
| 509 | |||
| 510 | /* Try to perform a simple rename, before falling back to | ||
| 511 | copying. */ | ||
| 512 | |||
| 513 | if (file.renameTo (newName)) | ||
| 514 | { | ||
| 515 | notifyChangeByName (file.getParent ()); | ||
| 516 | notifyChangeByName (targetParentDocumentId); | ||
| 517 | return newName.getAbsolutePath (); | ||
| 518 | } | ||
| 519 | |||
| 520 | /* If that doesn't work, create the new file and copy over the old | ||
| 521 | file's contents. */ | ||
| 522 | |||
| 523 | inputStream = null; | ||
| 524 | outputStream = null; | ||
| 525 | |||
| 526 | try | ||
| 527 | { | ||
| 528 | if (!newName.createNewFile () | ||
| 529 | || !newName.setWritable (true) | ||
| 530 | || !newName.setReadable (true)) | ||
| 531 | throw new FileNotFoundException ("failed to create new file"); | ||
| 532 | |||
| 533 | /* Open the file in preparation for a copy. */ | ||
| 534 | |||
| 535 | inputStream = new FileInputStream (file); | ||
| 536 | outputStream = new FileOutputStream (newName); | ||
| 537 | |||
| 538 | /* Allocate the buffer used to hold data. */ | ||
| 539 | |||
| 540 | buffer = new byte[4096]; | ||
| 541 | |||
| 542 | while ((length = inputStream.read (buffer)) > 0) | ||
| 543 | outputStream.write (buffer, 0, length); | ||
| 544 | } | ||
| 545 | catch (IOException e) | ||
| 546 | { | ||
| 547 | throw new FileNotFoundException ("IOException: " + e); | ||
| 548 | } | ||
| 549 | finally | ||
| 550 | { | ||
| 551 | try | ||
| 552 | { | ||
| 553 | if (inputStream != null) | ||
| 554 | inputStream.close (); | ||
| 555 | } | ||
| 556 | catch (IOException e) | ||
| 557 | { | ||
| 558 | |||
| 559 | } | ||
| 560 | |||
| 561 | try | ||
| 562 | { | ||
| 563 | if (outputStream != null) | ||
| 564 | outputStream.close (); | ||
| 565 | } | ||
| 566 | catch (IOException e) | ||
| 567 | { | ||
| 568 | |||
| 569 | } | ||
| 570 | } | ||
| 571 | |||
| 572 | file.delete (); | ||
| 573 | notifyChangeByName (file.getParent ()); | ||
| 574 | notifyChangeByName (targetParentDocumentId); | ||
| 575 | |||
| 576 | return newName.getAbsolutePath (); | ||
| 577 | } | ||
| 578 | } | ||
diff --git a/java/org/gnu/emacs/EmacsDrawLine.java b/java/org/gnu/emacs/EmacsDrawLine.java new file mode 100644 index 00000000000..d367ccff9c4 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDrawLine.java | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Canvas; | ||
| 23 | import android.graphics.Paint; | ||
| 24 | import android.graphics.Rect; | ||
| 25 | |||
| 26 | public final class EmacsDrawLine | ||
| 27 | { | ||
| 28 | public static void | ||
| 29 | perform (EmacsDrawable drawable, EmacsGC gc, | ||
| 30 | int x, int y, int x2, int y2) | ||
| 31 | { | ||
| 32 | Rect rect; | ||
| 33 | Canvas canvas; | ||
| 34 | Paint paint; | ||
| 35 | int x0, x1, y0, y1; | ||
| 36 | |||
| 37 | /* TODO implement stippling. */ | ||
| 38 | if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) | ||
| 39 | return; | ||
| 40 | |||
| 41 | /* Calculate the leftmost and rightmost points. */ | ||
| 42 | |||
| 43 | x0 = Math.min (x, x2 + 1); | ||
| 44 | x1 = Math.max (x, x2 + 1); | ||
| 45 | y0 = Math.min (y, y2 + 1); | ||
| 46 | y1 = Math.max (y, y2 + 1); | ||
| 47 | |||
| 48 | /* And the clip rectangle. */ | ||
| 49 | |||
| 50 | paint = gc.gcPaint; | ||
| 51 | rect = new Rect (x0, y0, x1, y1); | ||
| 52 | canvas = drawable.lockCanvas (gc); | ||
| 53 | |||
| 54 | if (canvas == null) | ||
| 55 | return; | ||
| 56 | |||
| 57 | paint.setStyle (Paint.Style.FILL); | ||
| 58 | |||
| 59 | /* Since drawLine has PostScript style behavior, adjust the | ||
| 60 | coordinates appropriately. | ||
| 61 | |||
| 62 | The left most pixel of a straight line is always partially | ||
| 63 | filled. Patch it in manually. */ | ||
| 64 | |||
| 65 | if (gc.clip_mask == null) | ||
| 66 | { | ||
| 67 | canvas.drawLine ((float) x + 0.5f, (float) y + 0.5f, | ||
| 68 | (float) x2 + 0.5f, (float) y2 + 0.5f, | ||
| 69 | paint); | ||
| 70 | |||
| 71 | if (x2 > x) | ||
| 72 | canvas.drawRect (new Rect (x, y, x + 1, y + 1), paint); | ||
| 73 | } | ||
| 74 | |||
| 75 | /* DrawLine with clip mask not implemented; it is not used by | ||
| 76 | Emacs. */ | ||
| 77 | drawable.damageRect (rect); | ||
| 78 | } | ||
| 79 | } | ||
diff --git a/java/org/gnu/emacs/EmacsDrawPoint.java b/java/org/gnu/emacs/EmacsDrawPoint.java new file mode 100644 index 00000000000..6a1cb744d60 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDrawPoint.java | |||
| @@ -0,0 +1,34 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | public final class EmacsDrawPoint | ||
| 23 | { | ||
| 24 | public static void | ||
| 25 | perform (EmacsDrawable drawable, | ||
| 26 | EmacsGC immutableGC, int x, int y) | ||
| 27 | { | ||
| 28 | /* Use EmacsFillRectangle instead of EmacsDrawRectangle, as the | ||
| 29 | latter actually draws a rectangle one pixel wider than | ||
| 30 | specified. */ | ||
| 31 | EmacsFillRectangle.perform (drawable, immutableGC, | ||
| 32 | x, y, 1, 1); | ||
| 33 | } | ||
| 34 | } | ||
diff --git a/java/org/gnu/emacs/EmacsDrawRectangle.java b/java/org/gnu/emacs/EmacsDrawRectangle.java new file mode 100644 index 00000000000..e1261b4a2d2 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDrawRectangle.java | |||
| @@ -0,0 +1,120 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Bitmap; | ||
| 23 | import android.graphics.Canvas; | ||
| 24 | import android.graphics.Paint; | ||
| 25 | import android.graphics.Rect; | ||
| 26 | import android.graphics.RectF; | ||
| 27 | |||
| 28 | import android.util.Log; | ||
| 29 | |||
| 30 | public final class EmacsDrawRectangle | ||
| 31 | { | ||
| 32 | public static void | ||
| 33 | perform (EmacsDrawable drawable, EmacsGC gc, | ||
| 34 | int x, int y, int width, int height) | ||
| 35 | { | ||
| 36 | Paint maskPaint, paint; | ||
| 37 | Canvas maskCanvas; | ||
| 38 | Bitmap maskBitmap; | ||
| 39 | Rect maskRect, dstRect; | ||
| 40 | Canvas canvas; | ||
| 41 | Bitmap clipBitmap; | ||
| 42 | |||
| 43 | /* TODO implement stippling. */ | ||
| 44 | if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) | ||
| 45 | return; | ||
| 46 | |||
| 47 | canvas = drawable.lockCanvas (gc); | ||
| 48 | |||
| 49 | if (canvas == null) | ||
| 50 | return; | ||
| 51 | |||
| 52 | paint = gc.gcPaint; | ||
| 53 | paint.setStyle (Paint.Style.STROKE); | ||
| 54 | |||
| 55 | if (gc.clip_mask == null) | ||
| 56 | /* Use canvas.drawRect with a RectF. That seems to reliably | ||
| 57 | get PostScript behavior. */ | ||
| 58 | canvas.drawRect (new RectF (x + 0.5f, y + 0.5f, | ||
| 59 | x + width + 0.5f, | ||
| 60 | y + height + 0.5f), | ||
| 61 | paint); | ||
| 62 | else | ||
| 63 | { | ||
| 64 | /* Drawing with a clip mask involves calculating the | ||
| 65 | intersection of the clip mask with the dst rect, and | ||
| 66 | extrapolating the corresponding part of the src rect. */ | ||
| 67 | clipBitmap = gc.clip_mask.bitmap; | ||
| 68 | dstRect = new Rect (x, y, x + width, y + height); | ||
| 69 | maskRect = new Rect (gc.clip_x_origin, | ||
| 70 | gc.clip_y_origin, | ||
| 71 | (gc.clip_x_origin | ||
| 72 | + clipBitmap.getWidth ()), | ||
| 73 | (gc.clip_y_origin | ||
| 74 | + clipBitmap.getHeight ())); | ||
| 75 | |||
| 76 | if (!maskRect.setIntersect (dstRect, maskRect)) | ||
| 77 | /* There is no intersection between the clip mask and the | ||
| 78 | dest rect. */ | ||
| 79 | return; | ||
| 80 | |||
| 81 | /* Finally, create a temporary bitmap that is the size of | ||
| 82 | maskRect. */ | ||
| 83 | |||
| 84 | maskBitmap | ||
| 85 | = Bitmap.createBitmap (maskRect.width (), maskRect.height (), | ||
| 86 | Bitmap.Config.ARGB_8888); | ||
| 87 | |||
| 88 | /* Draw the mask onto the maskBitmap. */ | ||
| 89 | maskCanvas = new Canvas (maskBitmap); | ||
| 90 | maskRect.offset (-gc.clip_x_origin, | ||
| 91 | -gc.clip_y_origin); | ||
| 92 | maskCanvas.drawBitmap (gc.clip_mask.bitmap, | ||
| 93 | maskRect, new Rect (0, 0, | ||
| 94 | maskRect.width (), | ||
| 95 | maskRect.height ()), | ||
| 96 | paint); | ||
| 97 | maskRect.offset (gc.clip_x_origin, | ||
| 98 | gc.clip_y_origin); | ||
| 99 | |||
| 100 | /* Set the transfer mode to SRC_IN to preserve only the parts | ||
| 101 | of the source that overlap with the mask. */ | ||
| 102 | maskPaint = new Paint (); | ||
| 103 | maskPaint.setXfermode (EmacsGC.srcInAlu); | ||
| 104 | maskPaint.setStyle (Paint.Style.STROKE); | ||
| 105 | |||
| 106 | /* Draw the source. */ | ||
| 107 | maskCanvas.drawRect (maskRect, maskPaint); | ||
| 108 | |||
| 109 | /* Finally, draw the mask bitmap to the destination. */ | ||
| 110 | paint.setXfermode (null); | ||
| 111 | canvas.drawBitmap (maskBitmap, null, maskRect, paint); | ||
| 112 | |||
| 113 | /* Recycle this unused bitmap. */ | ||
| 114 | maskBitmap.recycle (); | ||
| 115 | } | ||
| 116 | |||
| 117 | drawable.damageRect (new Rect (x, y, x + width + 1, | ||
| 118 | y + height + 1)); | ||
| 119 | } | ||
| 120 | } | ||
diff --git a/java/org/gnu/emacs/EmacsDrawable.java b/java/org/gnu/emacs/EmacsDrawable.java new file mode 100644 index 00000000000..f2f8885e976 --- /dev/null +++ b/java/org/gnu/emacs/EmacsDrawable.java | |||
| @@ -0,0 +1,32 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Rect; | ||
| 23 | import android.graphics.Bitmap; | ||
| 24 | import android.graphics.Canvas; | ||
| 25 | |||
| 26 | public interface EmacsDrawable | ||
| 27 | { | ||
| 28 | public Canvas lockCanvas (EmacsGC gc); | ||
| 29 | public void damageRect (Rect damageRect); | ||
| 30 | public Bitmap getBitmap (); | ||
| 31 | public boolean isDestroyed (); | ||
| 32 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsFillPolygon.java b/java/org/gnu/emacs/EmacsFillPolygon.java new file mode 100644 index 00000000000..4ae3882cab4 --- /dev/null +++ b/java/org/gnu/emacs/EmacsFillPolygon.java | |||
| @@ -0,0 +1,80 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Canvas; | ||
| 23 | import android.graphics.Paint; | ||
| 24 | import android.graphics.Path; | ||
| 25 | import android.graphics.Point; | ||
| 26 | import android.graphics.Rect; | ||
| 27 | import android.graphics.RectF; | ||
| 28 | |||
| 29 | public final class EmacsFillPolygon | ||
| 30 | { | ||
| 31 | public static void | ||
| 32 | perform (EmacsDrawable drawable, EmacsGC gc, Point points[]) | ||
| 33 | { | ||
| 34 | Canvas canvas; | ||
| 35 | Path path; | ||
| 36 | Paint paint; | ||
| 37 | Rect rect; | ||
| 38 | RectF rectF; | ||
| 39 | int i; | ||
| 40 | |||
| 41 | canvas = drawable.lockCanvas (gc); | ||
| 42 | |||
| 43 | if (canvas == null) | ||
| 44 | return; | ||
| 45 | |||
| 46 | paint = gc.gcPaint; | ||
| 47 | |||
| 48 | /* Build the path from the given array of points. */ | ||
| 49 | path = new Path (); | ||
| 50 | |||
| 51 | if (points.length >= 1) | ||
| 52 | { | ||
| 53 | path.moveTo (points[0].x, points[0].y); | ||
| 54 | |||
| 55 | for (i = 1; i < points.length; ++i) | ||
| 56 | path.lineTo (points[i].x, points[i].y); | ||
| 57 | |||
| 58 | path.close (); | ||
| 59 | } | ||
| 60 | |||
| 61 | /* Compute the damage rectangle. */ | ||
| 62 | rectF = new RectF (0, 0, 0, 0); | ||
| 63 | path.computeBounds (rectF, true); | ||
| 64 | |||
| 65 | rect = new Rect ((int) Math.floor (rectF.left), | ||
| 66 | (int) Math.floor (rectF.top), | ||
| 67 | (int) Math.ceil (rectF.right), | ||
| 68 | (int) Math.ceil (rectF.bottom)); | ||
| 69 | |||
| 70 | paint.setStyle (Paint.Style.FILL); | ||
| 71 | |||
| 72 | if (gc.clip_mask == null) | ||
| 73 | canvas.drawPath (path, paint); | ||
| 74 | |||
| 75 | drawable.damageRect (rect); | ||
| 76 | |||
| 77 | /* FillPolygon with clip mask not implemented; it is not used by | ||
| 78 | Emacs. */ | ||
| 79 | } | ||
| 80 | } | ||
diff --git a/java/org/gnu/emacs/EmacsFillRectangle.java b/java/org/gnu/emacs/EmacsFillRectangle.java new file mode 100644 index 00000000000..461fd3c639c --- /dev/null +++ b/java/org/gnu/emacs/EmacsFillRectangle.java | |||
| @@ -0,0 +1,116 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Bitmap; | ||
| 23 | import android.graphics.Canvas; | ||
| 24 | import android.graphics.Paint; | ||
| 25 | import android.graphics.Rect; | ||
| 26 | |||
| 27 | import android.util.Log; | ||
| 28 | |||
| 29 | public final class EmacsFillRectangle | ||
| 30 | { | ||
| 31 | public static void | ||
| 32 | perform (EmacsDrawable drawable, EmacsGC gc, | ||
| 33 | int x, int y, int width, int height) | ||
| 34 | { | ||
| 35 | Paint maskPaint, paint; | ||
| 36 | Canvas maskCanvas; | ||
| 37 | Bitmap maskBitmap; | ||
| 38 | Rect rect; | ||
| 39 | Rect maskRect, dstRect; | ||
| 40 | Canvas canvas; | ||
| 41 | Bitmap clipBitmap; | ||
| 42 | |||
| 43 | /* TODO implement stippling. */ | ||
| 44 | if (gc.fill_style == EmacsGC.GC_FILL_OPAQUE_STIPPLED) | ||
| 45 | return; | ||
| 46 | |||
| 47 | canvas = drawable.lockCanvas (gc); | ||
| 48 | |||
| 49 | if (canvas == null) | ||
| 50 | return; | ||
| 51 | |||
| 52 | paint = gc.gcPaint; | ||
| 53 | rect = new Rect (x, y, x + width, y + height); | ||
| 54 | |||
| 55 | paint.setStyle (Paint.Style.FILL); | ||
| 56 | |||
| 57 | if (gc.clip_mask == null) | ||
| 58 | canvas.drawRect (rect, paint); | ||
| 59 | else | ||
| 60 | { | ||
| 61 | /* Drawing with a clip mask involves calculating the | ||
| 62 | intersection of the clip mask with the dst rect, and | ||
| 63 | extrapolating the corresponding part of the src rect. */ | ||
| 64 | |||
| 65 | clipBitmap = gc.clip_mask.bitmap; | ||
| 66 | dstRect = new Rect (x, y, x + width, y + height); | ||
| 67 | maskRect = new Rect (gc.clip_x_origin, | ||
| 68 | gc.clip_y_origin, | ||
| 69 | (gc.clip_x_origin | ||
| 70 | + clipBitmap.getWidth ()), | ||
| 71 | (gc.clip_y_origin | ||
| 72 | + clipBitmap.getHeight ())); | ||
| 73 | |||
| 74 | if (!maskRect.setIntersect (dstRect, maskRect)) | ||
| 75 | /* There is no intersection between the clip mask and the | ||
| 76 | dest rect. */ | ||
| 77 | return; | ||
| 78 | |||
| 79 | /* Finally, create a temporary bitmap that is the size of | ||
| 80 | maskRect. */ | ||
| 81 | |||
| 82 | maskBitmap | ||
| 83 | = Bitmap.createBitmap (maskRect.width (), maskRect.height (), | ||
| 84 | Bitmap.Config.ARGB_8888); | ||
| 85 | |||
| 86 | /* Draw the mask onto the maskBitmap. */ | ||
| 87 | maskCanvas = new Canvas (maskBitmap); | ||
| 88 | maskRect.offset (-gc.clip_x_origin, | ||
| 89 | -gc.clip_y_origin); | ||
| 90 | maskCanvas.drawBitmap (gc.clip_mask.bitmap, | ||
| 91 | maskRect, new Rect (0, 0, | ||
| 92 | maskRect.width (), | ||
| 93 | maskRect.height ()), | ||
| 94 | paint); | ||
| 95 | maskRect.offset (gc.clip_x_origin, | ||
| 96 | gc.clip_y_origin); | ||
| 97 | |||
| 98 | /* Set the transfer mode to SRC_IN to preserve only the parts | ||
| 99 | of the source that overlap with the mask. */ | ||
| 100 | maskPaint = new Paint (); | ||
| 101 | maskPaint.setXfermode (EmacsGC.srcInAlu); | ||
| 102 | |||
| 103 | /* Draw the source. */ | ||
| 104 | maskCanvas.drawRect (maskRect, maskPaint); | ||
| 105 | |||
| 106 | /* Finally, draw the mask bitmap to the destination. */ | ||
| 107 | paint.setXfermode (null); | ||
| 108 | canvas.drawBitmap (maskBitmap, null, maskRect, paint); | ||
| 109 | |||
| 110 | /* Recycle this unused bitmap. */ | ||
| 111 | maskBitmap.recycle (); | ||
| 112 | } | ||
| 113 | |||
| 114 | drawable.damageRect (rect); | ||
| 115 | } | ||
| 116 | } | ||
diff --git a/java/org/gnu/emacs/EmacsFontDriver.java b/java/org/gnu/emacs/EmacsFontDriver.java new file mode 100644 index 00000000000..ff52899a897 --- /dev/null +++ b/java/org/gnu/emacs/EmacsFontDriver.java | |||
| @@ -0,0 +1,173 @@ | |||
| 1 | /* Font backend for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.os.Build; | ||
| 23 | |||
| 24 | /* This code is mostly unused. See sfntfont-android.c for the code | ||
| 25 | that is actually used. */ | ||
| 26 | |||
| 27 | public abstract class EmacsFontDriver | ||
| 28 | { | ||
| 29 | /* Font weights. */ | ||
| 30 | public static final int THIN = 0; | ||
| 31 | public static final int ULTRA_LIGHT = 40; | ||
| 32 | public static final int LIGHT = 50; | ||
| 33 | public static final int SEMI_LIGHT = 55; | ||
| 34 | public static final int REGULAR = 80; | ||
| 35 | public static final int MEDIUM = 100; | ||
| 36 | public static final int SEMI_BOLD = 180; | ||
| 37 | public static final int BOLD = 200; | ||
| 38 | public static final int EXTRA_BOLD = 205; | ||
| 39 | public static final int BLACK = 210; | ||
| 40 | public static final int ULTRA_HEAVY = 250; | ||
| 41 | |||
| 42 | /* Font slants. */ | ||
| 43 | public static final int REVERSE_OBLIQUE = 0; | ||
| 44 | public static final int REVERSE_ITALIC = 10; | ||
| 45 | public static final int NORMAL = 100; | ||
| 46 | public static final int ITALIC = 200; | ||
| 47 | public static final int OBLIQUE = 210; | ||
| 48 | |||
| 49 | /* Font widths. */ | ||
| 50 | public static final int ULTRA_CONDENSED = 50; | ||
| 51 | public static final int EXTRA_CONDENSED = 63; | ||
| 52 | public static final int CONDENSED = 75; | ||
| 53 | public static final int SEMI_CONDENSED = 87; | ||
| 54 | public static final int UNSPECIFIED = 100; | ||
| 55 | public static final int SEMI_EXPANDED = 113; | ||
| 56 | public static final int EXPANDED = 125; | ||
| 57 | public static final int EXTRA_EXPANDED = 150; | ||
| 58 | public static final int ULTRA_EXPANDED = 200; | ||
| 59 | |||
| 60 | /* Font spacings. */ | ||
| 61 | public static final int PROPORTIONAL = 0; | ||
| 62 | public static final int DUAL = 90; | ||
| 63 | public static final int MONO = 100; | ||
| 64 | public static final int CHARCELL = 110; | ||
| 65 | |||
| 66 | public static class FontSpec | ||
| 67 | { | ||
| 68 | /* The fields below mean the same as they do in enum | ||
| 69 | font_property_index in font.h. */ | ||
| 70 | |||
| 71 | public String foundry; | ||
| 72 | public String family; | ||
| 73 | public String adstyle; | ||
| 74 | public String registry; | ||
| 75 | public Integer width; | ||
| 76 | public Integer weight; | ||
| 77 | public Integer slant; | ||
| 78 | public Integer size; | ||
| 79 | public Integer spacing; | ||
| 80 | public Integer avgwidth; | ||
| 81 | public Integer dpi; | ||
| 82 | |||
| 83 | @Override | ||
| 84 | public String | ||
| 85 | toString () | ||
| 86 | { | ||
| 87 | return ("foundry: " + foundry | ||
| 88 | + " family: " + family | ||
| 89 | + " adstyle: " + adstyle | ||
| 90 | + " registry: " + registry | ||
| 91 | + " width: " + width | ||
| 92 | + " weight: " + weight | ||
| 93 | + " slant: " + slant | ||
| 94 | + " spacing: " + spacing | ||
| 95 | + " avgwidth: " + avgwidth | ||
| 96 | + " dpi: " + dpi); | ||
| 97 | } | ||
| 98 | }; | ||
| 99 | |||
| 100 | public static class FontMetrics | ||
| 101 | { | ||
| 102 | public short lbearing; | ||
| 103 | public short rbearing; | ||
| 104 | public short width; | ||
| 105 | public short ascent; | ||
| 106 | public short descent; | ||
| 107 | |||
| 108 | @Override | ||
| 109 | public String | ||
| 110 | toString () | ||
| 111 | { | ||
| 112 | return ("lbearing " + lbearing | ||
| 113 | + " rbearing " + rbearing | ||
| 114 | + " width " + width | ||
| 115 | + " ascent " + ascent | ||
| 116 | + " descent " + descent); | ||
| 117 | } | ||
| 118 | } | ||
| 119 | |||
| 120 | public static class FontEntity extends FontSpec | ||
| 121 | { | ||
| 122 | /* No extra fields here. */ | ||
| 123 | }; | ||
| 124 | |||
| 125 | public abstract class FontObject extends FontSpec | ||
| 126 | { | ||
| 127 | public int minWidth; | ||
| 128 | public int maxWidth; | ||
| 129 | public int pixelSize; | ||
| 130 | public int height; | ||
| 131 | public int spaceWidth; | ||
| 132 | public int averageWidth; | ||
| 133 | public int ascent; | ||
| 134 | public int descent; | ||
| 135 | public int underlineThickness; | ||
| 136 | public int underlinePosition; | ||
| 137 | public int baselineOffset; | ||
| 138 | public int relativeCompose; | ||
| 139 | public int defaultAscent; | ||
| 140 | public int encodingCharset; | ||
| 141 | public int repertoryCharset; | ||
| 142 | |||
| 143 | public | ||
| 144 | FontObject () | ||
| 145 | { | ||
| 146 | encodingCharset = -1; | ||
| 147 | repertoryCharset = -1; | ||
| 148 | } | ||
| 149 | }; | ||
| 150 | |||
| 151 | /* These mean the same as they do in struct font_driver. */ | ||
| 152 | public abstract FontEntity[] list (FontSpec fontSpec); | ||
| 153 | public abstract FontEntity match (FontSpec fontSpec); | ||
| 154 | public abstract String[] listFamilies (); | ||
| 155 | public abstract FontObject openFont (FontEntity fontEntity, int pixelSize); | ||
| 156 | public abstract int hasChar (FontSpec font, char charCode); | ||
| 157 | public abstract void textExtents (FontObject font, int code[], | ||
| 158 | FontMetrics fontMetrics); | ||
| 159 | public abstract int encodeChar (FontObject fontObject, char charCode); | ||
| 160 | public abstract int draw (FontObject fontObject, EmacsGC gc, | ||
| 161 | EmacsDrawable drawable, int[] chars, | ||
| 162 | int x, int y, int backgroundWidth, | ||
| 163 | boolean withBackground); | ||
| 164 | |||
| 165 | public static EmacsFontDriver | ||
| 166 | createFontDriver () | ||
| 167 | { | ||
| 168 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) | ||
| 169 | return new EmacsSdk23FontDriver (); | ||
| 170 | |||
| 171 | return new EmacsSdk7FontDriver (); | ||
| 172 | } | ||
| 173 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsGC.java b/java/org/gnu/emacs/EmacsGC.java new file mode 100644 index 00000000000..a7467cb9bd0 --- /dev/null +++ b/java/org/gnu/emacs/EmacsGC.java | |||
| @@ -0,0 +1,121 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Rect; | ||
| 23 | import android.graphics.Paint; | ||
| 24 | |||
| 25 | import android.graphics.PorterDuff.Mode; | ||
| 26 | import android.graphics.PorterDuffXfermode; | ||
| 27 | import android.graphics.Xfermode; | ||
| 28 | |||
| 29 | /* X like graphics context structures. Keep the enums in synch with | ||
| 30 | androidgui.h! */ | ||
| 31 | |||
| 32 | public final class EmacsGC extends EmacsHandleObject | ||
| 33 | { | ||
| 34 | public static final int GC_COPY = 0; | ||
| 35 | public static final int GC_XOR = 1; | ||
| 36 | |||
| 37 | public static final int GC_FILL_SOLID = 0; | ||
| 38 | public static final int GC_FILL_OPAQUE_STIPPLED = 1; | ||
| 39 | |||
| 40 | public static final Xfermode xorAlu, srcInAlu; | ||
| 41 | |||
| 42 | public int function, fill_style; | ||
| 43 | public int foreground, background; | ||
| 44 | public int clip_x_origin, clip_y_origin; | ||
| 45 | public int ts_origin_x, ts_origin_y; | ||
| 46 | public Rect clip_rects[], real_clip_rects[]; | ||
| 47 | public EmacsPixmap clip_mask, stipple; | ||
| 48 | public Paint gcPaint; | ||
| 49 | |||
| 50 | /* ID incremented every time the clipping rectangles of any GC | ||
| 51 | changes. */ | ||
| 52 | private static long clip_serial; | ||
| 53 | |||
| 54 | /* The value of clipRectID after the last time this GCs clip | ||
| 55 | rectangles changed. 0 if there are no clip rectangles. */ | ||
| 56 | public long clipRectID; | ||
| 57 | |||
| 58 | static | ||
| 59 | { | ||
| 60 | xorAlu = new PorterDuffXfermode (Mode.XOR); | ||
| 61 | srcInAlu = new PorterDuffXfermode (Mode.SRC_IN); | ||
| 62 | } | ||
| 63 | |||
| 64 | /* The following fields are only set on immutable GCs. */ | ||
| 65 | |||
| 66 | public | ||
| 67 | EmacsGC (short handle) | ||
| 68 | { | ||
| 69 | /* For historical reasons the C code has an extra layer of | ||
| 70 | indirection above this GC handle. struct android_gc is the GC | ||
| 71 | used by Emacs code, while android_gcontext is the type of the | ||
| 72 | handle. */ | ||
| 73 | super (handle); | ||
| 74 | |||
| 75 | fill_style = GC_FILL_SOLID; | ||
| 76 | function = GC_COPY; | ||
| 77 | foreground = 0; | ||
| 78 | background = 0xffffff; | ||
| 79 | gcPaint = new Paint (); | ||
| 80 | } | ||
| 81 | |||
| 82 | /* Mark this GC as dirty. Apply parameters to the paint and | ||
| 83 | recompute real_clip_rects. */ | ||
| 84 | |||
| 85 | public void | ||
| 86 | markDirty (boolean clipRectsChanged) | ||
| 87 | { | ||
| 88 | int i; | ||
| 89 | |||
| 90 | if (clipRectsChanged) | ||
| 91 | { | ||
| 92 | if ((ts_origin_x != 0 || ts_origin_y != 0) | ||
| 93 | && clip_rects != null) | ||
| 94 | { | ||
| 95 | real_clip_rects = new Rect[clip_rects.length]; | ||
| 96 | |||
| 97 | for (i = 0; i < clip_rects.length; ++i) | ||
| 98 | { | ||
| 99 | real_clip_rects[i] = new Rect (clip_rects[i]); | ||
| 100 | real_clip_rects[i].offset (ts_origin_x, ts_origin_y); | ||
| 101 | } | ||
| 102 | } | ||
| 103 | else | ||
| 104 | real_clip_rects = clip_rects; | ||
| 105 | |||
| 106 | clipRectID = ++clip_serial; | ||
| 107 | } | ||
| 108 | |||
| 109 | gcPaint.setStrokeWidth (1f); | ||
| 110 | gcPaint.setColor (foreground | 0xff000000); | ||
| 111 | gcPaint.setXfermode (function == GC_XOR | ||
| 112 | ? xorAlu : srcInAlu); | ||
| 113 | } | ||
| 114 | |||
| 115 | public void | ||
| 116 | resetXfermode () | ||
| 117 | { | ||
| 118 | gcPaint.setXfermode (function == GC_XOR | ||
| 119 | ? xorAlu : srcInAlu); | ||
| 120 | } | ||
| 121 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsHandleObject.java b/java/org/gnu/emacs/EmacsHandleObject.java new file mode 100644 index 00000000000..5b889895337 --- /dev/null +++ b/java/org/gnu/emacs/EmacsHandleObject.java | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.lang.IllegalStateException; | ||
| 23 | |||
| 24 | /* This defines something that is a so-called ``handle''. Handles | ||
| 25 | must be created by C code, and will remain existing until | ||
| 26 | destroyHandle is called. C code then refers to the handle by a | ||
| 27 | number which maps into the Java object representing the handle. | ||
| 28 | |||
| 29 | All handle operations must be done from the Emacs thread. */ | ||
| 30 | |||
| 31 | public abstract class EmacsHandleObject | ||
| 32 | { | ||
| 33 | /* Whether or not this handle has been destroyed. */ | ||
| 34 | volatile boolean destroyed; | ||
| 35 | |||
| 36 | /* The handle associated with this object. */ | ||
| 37 | public short handle; | ||
| 38 | |||
| 39 | public | ||
| 40 | EmacsHandleObject (short handle) | ||
| 41 | { | ||
| 42 | this.handle = handle; | ||
| 43 | } | ||
| 44 | |||
| 45 | public void | ||
| 46 | destroyHandle () throws IllegalStateException | ||
| 47 | { | ||
| 48 | synchronized (this) | ||
| 49 | { | ||
| 50 | destroyed = true; | ||
| 51 | } | ||
| 52 | } | ||
| 53 | |||
| 54 | public boolean | ||
| 55 | isDestroyed () | ||
| 56 | { | ||
| 57 | return destroyed; | ||
| 58 | } | ||
| 59 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsHolder.java b/java/org/gnu/emacs/EmacsHolder.java new file mode 100644 index 00000000000..6cd48ba57ce --- /dev/null +++ b/java/org/gnu/emacs/EmacsHolder.java | |||
| @@ -0,0 +1,30 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | |||
| 23 | |||
| 24 | /* This class serves as a simple reference to an object of type T. | ||
| 25 | Nothing could be found inside the standard library. */ | ||
| 26 | |||
| 27 | public final class EmacsHolder<T> | ||
| 28 | { | ||
| 29 | T thing; | ||
| 30 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsInputConnection.java b/java/org/gnu/emacs/EmacsInputConnection.java new file mode 100644 index 00000000000..c3764a7b29f --- /dev/null +++ b/java/org/gnu/emacs/EmacsInputConnection.java | |||
| @@ -0,0 +1,698 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.os.Build; | ||
| 23 | import android.os.Bundle; | ||
| 24 | import android.os.Handler; | ||
| 25 | |||
| 26 | import android.view.KeyEvent; | ||
| 27 | |||
| 28 | import android.view.inputmethod.CompletionInfo; | ||
| 29 | import android.view.inputmethod.CorrectionInfo; | ||
| 30 | import android.view.inputmethod.ExtractedText; | ||
| 31 | import android.view.inputmethod.ExtractedTextRequest; | ||
| 32 | import android.view.inputmethod.InputConnection; | ||
| 33 | import android.view.inputmethod.InputContentInfo; | ||
| 34 | import android.view.inputmethod.SurroundingText; | ||
| 35 | import android.view.inputmethod.TextAttribute; | ||
| 36 | import android.view.inputmethod.TextSnapshot; | ||
| 37 | |||
| 38 | import android.util.Log; | ||
| 39 | |||
| 40 | /* Android input methods, take number six. See textconv.c for more | ||
| 41 | details; this is more-or-less a thin wrapper around that file. */ | ||
| 42 | |||
| 43 | public final class EmacsInputConnection implements InputConnection | ||
| 44 | { | ||
| 45 | private static final String TAG = "EmacsInputConnection"; | ||
| 46 | |||
| 47 | /* View associated with this input connection. */ | ||
| 48 | private EmacsView view; | ||
| 49 | |||
| 50 | /* The handle ID associated with that view's window. */ | ||
| 51 | private short windowHandle; | ||
| 52 | |||
| 53 | /* Number of batch edits currently underway. Used to avoid | ||
| 54 | synchronizing with the Emacs thread after each | ||
| 55 | `endBatchEdit'. */ | ||
| 56 | private int batchEditCount; | ||
| 57 | |||
| 58 | /* Whether or not to synchronize and call `updateIC' with the | ||
| 59 | selection position after committing text. | ||
| 60 | |||
| 61 | This helps with on screen keyboard programs found in some vendor | ||
| 62 | versions of Android, which rely on immediate updates to the point | ||
| 63 | position after text is commited in order to place the cursor | ||
| 64 | within that text. */ | ||
| 65 | |||
| 66 | private static boolean syncAfterCommit; | ||
| 67 | |||
| 68 | /* Whether or not to return empty text with the offset set to zero | ||
| 69 | if a request arrives that has no flags set and has requested no | ||
| 70 | characters at all. | ||
| 71 | |||
| 72 | This is necessary with on screen keyboard programs found in some | ||
| 73 | vendor versions of Android which don't rely on the documented | ||
| 74 | meaning of `ExtractedText.startOffset', and instead take the | ||
| 75 | selection offset inside at face value. */ | ||
| 76 | |||
| 77 | private static boolean extractAbsoluteOffsets; | ||
| 78 | |||
| 79 | static | ||
| 80 | { | ||
| 81 | if (Build.MANUFACTURER.equalsIgnoreCase ("Huawei") | ||
| 82 | || Build.MANUFACTURER.equalsIgnoreCase ("Honor")) | ||
| 83 | extractAbsoluteOffsets = syncAfterCommit = true; | ||
| 84 | |||
| 85 | /* The Samsung and Vivo keyboards take `selectionStart' at face | ||
| 86 | value if some text is returned, and also searches for words | ||
| 87 | solely within that text. However, when no text is returned, it | ||
| 88 | falls back to getTextAfterCursor and getTextBeforeCursor. */ | ||
| 89 | if (Build.MANUFACTURER.equalsIgnoreCase ("Samsung") | ||
| 90 | || Build.MANUFACTURER.equalsIgnoreCase ("Vivo")) | ||
| 91 | extractAbsoluteOffsets = true; | ||
| 92 | }; | ||
| 93 | |||
| 94 | |||
| 95 | public | ||
| 96 | EmacsInputConnection (EmacsView view) | ||
| 97 | { | ||
| 98 | this.view = view; | ||
| 99 | this.windowHandle = view.window.handle; | ||
| 100 | } | ||
| 101 | |||
| 102 | |||
| 103 | /* The functions below are called by input methods whenever they | ||
| 104 | need to perform an edit. */ | ||
| 105 | |||
| 106 | @Override | ||
| 107 | public boolean | ||
| 108 | beginBatchEdit () | ||
| 109 | { | ||
| 110 | /* Return if the input connection is out of date. */ | ||
| 111 | if (view.icSerial < view.icGeneration) | ||
| 112 | return false; | ||
| 113 | |||
| 114 | if (EmacsService.DEBUG_IC) | ||
| 115 | Log.d (TAG, "beginBatchEdit"); | ||
| 116 | |||
| 117 | EmacsNative.beginBatchEdit (windowHandle); | ||
| 118 | |||
| 119 | /* Keep a record of the number of outstanding batch edits here as | ||
| 120 | well. */ | ||
| 121 | batchEditCount++; | ||
| 122 | return true; | ||
| 123 | } | ||
| 124 | |||
| 125 | @Override | ||
| 126 | public boolean | ||
| 127 | endBatchEdit () | ||
| 128 | { | ||
| 129 | /* Return if the input connection is out of date. */ | ||
| 130 | if (view.icSerial < view.icGeneration) | ||
| 131 | return false; | ||
| 132 | |||
| 133 | if (EmacsService.DEBUG_IC) | ||
| 134 | Log.d (TAG, "endBatchEdit"); | ||
| 135 | |||
| 136 | EmacsNative.endBatchEdit (windowHandle); | ||
| 137 | |||
| 138 | /* Subtract one from the UI thread record of the number of batch | ||
| 139 | edits currently under way. */ | ||
| 140 | |||
| 141 | if (batchEditCount > 0) | ||
| 142 | batchEditCount -= 1; | ||
| 143 | |||
| 144 | return batchEditCount > 0; | ||
| 145 | } | ||
| 146 | |||
| 147 | public boolean | ||
| 148 | commitCompletion (CompletionInfo info) | ||
| 149 | { | ||
| 150 | /* Return if the input connection is out of date. */ | ||
| 151 | if (view.icSerial < view.icGeneration) | ||
| 152 | return false; | ||
| 153 | |||
| 154 | if (EmacsService.DEBUG_IC) | ||
| 155 | Log.d (TAG, "commitCompletion: " + info); | ||
| 156 | |||
| 157 | EmacsNative.commitCompletion (windowHandle, | ||
| 158 | info.getText ().toString (), | ||
| 159 | info.getPosition ()); | ||
| 160 | return true; | ||
| 161 | } | ||
| 162 | |||
| 163 | @Override | ||
| 164 | public boolean | ||
| 165 | commitCorrection (CorrectionInfo info) | ||
| 166 | { | ||
| 167 | /* The input method calls this function not to commit text, but to | ||
| 168 | indicate that a subsequent edit will consist of a correction. | ||
| 169 | Emacs has no use for this information. | ||
| 170 | |||
| 171 | Of course this completely contradicts the provided | ||
| 172 | documentation, but this is how Android actually behaves. */ | ||
| 173 | return false; | ||
| 174 | } | ||
| 175 | |||
| 176 | @Override | ||
| 177 | public boolean | ||
| 178 | commitText (CharSequence text, int newCursorPosition) | ||
| 179 | { | ||
| 180 | int[] selection; | ||
| 181 | |||
| 182 | /* Return if the input connection is out of date. */ | ||
| 183 | if (view.icSerial < view.icGeneration) | ||
| 184 | return false; | ||
| 185 | |||
| 186 | if (EmacsService.DEBUG_IC) | ||
| 187 | Log.d (TAG, "commitText: " + text + " " + newCursorPosition); | ||
| 188 | |||
| 189 | EmacsNative.commitText (windowHandle, text.toString (), | ||
| 190 | newCursorPosition); | ||
| 191 | |||
| 192 | if (syncAfterCommit) | ||
| 193 | { | ||
| 194 | /* Synchronize with the Emacs thread, obtain the new | ||
| 195 | selection, and report it immediately. */ | ||
| 196 | |||
| 197 | selection = EmacsNative.getSelection (windowHandle); | ||
| 198 | |||
| 199 | if (EmacsService.DEBUG_IC && selection != null) | ||
| 200 | Log.d (TAG, "commitText: new selection is " + selection[0] | ||
| 201 | + ", by " + selection[1]); | ||
| 202 | |||
| 203 | if (selection != null) | ||
| 204 | /* N.B. that the composing region is removed after text is | ||
| 205 | committed. */ | ||
| 206 | view.imManager.updateSelection (view, selection[0], | ||
| 207 | selection[1], -1, -1); | ||
| 208 | } | ||
| 209 | |||
| 210 | return true; | ||
| 211 | } | ||
| 212 | |||
| 213 | @Override | ||
| 214 | public boolean | ||
| 215 | commitText (CharSequence text, int newCursorPosition, | ||
| 216 | TextAttribute textAttribute) | ||
| 217 | { | ||
| 218 | return commitText (text, newCursorPosition); | ||
| 219 | } | ||
| 220 | |||
| 221 | @Override | ||
| 222 | public boolean | ||
| 223 | deleteSurroundingText (int leftLength, int rightLength) | ||
| 224 | { | ||
| 225 | /* Return if the input connection is out of date. */ | ||
| 226 | if (view.icSerial < view.icGeneration) | ||
| 227 | return false; | ||
| 228 | |||
| 229 | if (EmacsService.DEBUG_IC) | ||
| 230 | Log.d (TAG, ("deleteSurroundingText: " | ||
| 231 | + leftLength + " " + rightLength)); | ||
| 232 | |||
| 233 | EmacsNative.deleteSurroundingText (windowHandle, leftLength, | ||
| 234 | rightLength); | ||
| 235 | return true; | ||
| 236 | } | ||
| 237 | |||
| 238 | @Override | ||
| 239 | public boolean | ||
| 240 | deleteSurroundingTextInCodePoints (int leftLength, int rightLength) | ||
| 241 | { | ||
| 242 | /* Emacs returns characters which cannot be represented in a Java | ||
| 243 | `char' as NULL characters, so code points always reflect | ||
| 244 | characters themselves. */ | ||
| 245 | return deleteSurroundingText (leftLength, rightLength); | ||
| 246 | } | ||
| 247 | |||
| 248 | @Override | ||
| 249 | public boolean | ||
| 250 | finishComposingText () | ||
| 251 | { | ||
| 252 | /* Return if the input connection is out of date. */ | ||
| 253 | if (view.icSerial < view.icGeneration) | ||
| 254 | return false; | ||
| 255 | |||
| 256 | if (EmacsService.DEBUG_IC) | ||
| 257 | Log.d (TAG, "finishComposingText"); | ||
| 258 | |||
| 259 | EmacsNative.finishComposingText (windowHandle); | ||
| 260 | return true; | ||
| 261 | } | ||
| 262 | |||
| 263 | @Override | ||
| 264 | public String | ||
| 265 | getSelectedText (int flags) | ||
| 266 | { | ||
| 267 | /* Return if the input connection is out of date. */ | ||
| 268 | if (view.icSerial < view.icGeneration) | ||
| 269 | return null; | ||
| 270 | |||
| 271 | if (EmacsService.DEBUG_IC) | ||
| 272 | Log.d (TAG, "getSelectedText: " + flags); | ||
| 273 | |||
| 274 | return EmacsNative.getSelectedText (windowHandle, flags); | ||
| 275 | } | ||
| 276 | |||
| 277 | @Override | ||
| 278 | public String | ||
| 279 | getTextAfterCursor (int length, int flags) | ||
| 280 | { | ||
| 281 | String string; | ||
| 282 | |||
| 283 | /* Return if the input connection is out of date. */ | ||
| 284 | if (view.icSerial < view.icGeneration) | ||
| 285 | return null; | ||
| 286 | |||
| 287 | if (EmacsService.DEBUG_IC) | ||
| 288 | Log.d (TAG, "getTextAfterCursor: " + length + " " + flags); | ||
| 289 | |||
| 290 | string = EmacsNative.getTextAfterCursor (windowHandle, length, | ||
| 291 | flags); | ||
| 292 | |||
| 293 | if (EmacsService.DEBUG_IC) | ||
| 294 | Log.d (TAG, " --> " + string); | ||
| 295 | |||
| 296 | return string; | ||
| 297 | } | ||
| 298 | |||
| 299 | @Override | ||
| 300 | public String | ||
| 301 | getTextBeforeCursor (int length, int flags) | ||
| 302 | { | ||
| 303 | String string; | ||
| 304 | |||
| 305 | /* Return if the input connection is out of date. */ | ||
| 306 | if (view.icSerial < view.icGeneration) | ||
| 307 | return null; | ||
| 308 | |||
| 309 | if (EmacsService.DEBUG_IC) | ||
| 310 | Log.d (TAG, "getTextBeforeCursor: " + length + " " + flags); | ||
| 311 | |||
| 312 | string = EmacsNative.getTextBeforeCursor (windowHandle, length, | ||
| 313 | flags); | ||
| 314 | |||
| 315 | if (EmacsService.DEBUG_IC) | ||
| 316 | Log.d (TAG, " --> " + string); | ||
| 317 | |||
| 318 | return string; | ||
| 319 | } | ||
| 320 | |||
| 321 | @Override | ||
| 322 | public boolean | ||
| 323 | setComposingText (CharSequence text, int newCursorPosition) | ||
| 324 | { | ||
| 325 | /* Return if the input connection is out of date. */ | ||
| 326 | if (view.icSerial < view.icGeneration) | ||
| 327 | return false; | ||
| 328 | |||
| 329 | if (EmacsService.DEBUG_IC) | ||
| 330 | Log.d (TAG, ("setComposingText: " | ||
| 331 | + text + " ## " + newCursorPosition)); | ||
| 332 | |||
| 333 | EmacsNative.setComposingText (windowHandle, text.toString (), | ||
| 334 | newCursorPosition); | ||
| 335 | return true; | ||
| 336 | } | ||
| 337 | |||
| 338 | @Override | ||
| 339 | public boolean | ||
| 340 | setComposingText (CharSequence text, int newCursorPosition, | ||
| 341 | TextAttribute textAttribute) | ||
| 342 | { | ||
| 343 | return setComposingText (text, newCursorPosition); | ||
| 344 | } | ||
| 345 | |||
| 346 | @Override | ||
| 347 | public boolean | ||
| 348 | setComposingRegion (int start, int end) | ||
| 349 | { | ||
| 350 | /* Return if the input connection is out of date. */ | ||
| 351 | if (view.icSerial < view.icGeneration) | ||
| 352 | return false; | ||
| 353 | |||
| 354 | if (EmacsService.DEBUG_IC) | ||
| 355 | Log.d (TAG, "setComposingRegion: " + start + " " + end); | ||
| 356 | |||
| 357 | EmacsNative.setComposingRegion (windowHandle, start, end); | ||
| 358 | return true; | ||
| 359 | } | ||
| 360 | |||
| 361 | @Override | ||
| 362 | public boolean | ||
| 363 | setComposingRegion (int start, int end, TextAttribute textAttribute) | ||
| 364 | { | ||
| 365 | return setComposingRegion (start, end); | ||
| 366 | } | ||
| 367 | |||
| 368 | @Override | ||
| 369 | public boolean | ||
| 370 | performEditorAction (int editorAction) | ||
| 371 | { | ||
| 372 | /* Return if the input connection is out of date. */ | ||
| 373 | if (view.icSerial < view.icGeneration) | ||
| 374 | return false; | ||
| 375 | |||
| 376 | if (EmacsService.DEBUG_IC) | ||
| 377 | Log.d (TAG, "performEditorAction: " + editorAction); | ||
| 378 | |||
| 379 | EmacsNative.performEditorAction (windowHandle, editorAction); | ||
| 380 | return true; | ||
| 381 | } | ||
| 382 | |||
| 383 | @Override | ||
| 384 | public boolean | ||
| 385 | performContextMenuAction (int contextMenuAction) | ||
| 386 | { | ||
| 387 | int action; | ||
| 388 | |||
| 389 | /* Return if the input connection is out of date. */ | ||
| 390 | if (view.icSerial < view.icGeneration) | ||
| 391 | return false; | ||
| 392 | |||
| 393 | if (EmacsService.DEBUG_IC) | ||
| 394 | Log.d (TAG, "performContextMenuAction: " + contextMenuAction); | ||
| 395 | |||
| 396 | /* Translate the action in Java code. That way, a great deal of | ||
| 397 | JNI boilerplate can be avoided. */ | ||
| 398 | |||
| 399 | switch (contextMenuAction) | ||
| 400 | { | ||
| 401 | case android.R.id.selectAll: | ||
| 402 | action = 0; | ||
| 403 | break; | ||
| 404 | |||
| 405 | case android.R.id.startSelectingText: | ||
| 406 | action = 1; | ||
| 407 | break; | ||
| 408 | |||
| 409 | case android.R.id.stopSelectingText: | ||
| 410 | action = 2; | ||
| 411 | break; | ||
| 412 | |||
| 413 | case android.R.id.cut: | ||
| 414 | action = 3; | ||
| 415 | break; | ||
| 416 | |||
| 417 | case android.R.id.copy: | ||
| 418 | action = 4; | ||
| 419 | break; | ||
| 420 | |||
| 421 | case android.R.id.paste: | ||
| 422 | action = 5; | ||
| 423 | break; | ||
| 424 | |||
| 425 | default: | ||
| 426 | return true; | ||
| 427 | } | ||
| 428 | |||
| 429 | EmacsNative.performContextMenuAction (windowHandle, action); | ||
| 430 | return true; | ||
| 431 | } | ||
| 432 | |||
| 433 | @Override | ||
| 434 | public ExtractedText | ||
| 435 | getExtractedText (ExtractedTextRequest request, int flags) | ||
| 436 | { | ||
| 437 | ExtractedText text; | ||
| 438 | int[] selection; | ||
| 439 | |||
| 440 | /* Return if the input connection is out of date. */ | ||
| 441 | if (view.icSerial < view.icGeneration) | ||
| 442 | return null; | ||
| 443 | |||
| 444 | if (EmacsService.DEBUG_IC) | ||
| 445 | Log.d (TAG, "getExtractedText: " + request.hintMaxChars + ", " | ||
| 446 | + request.hintMaxLines + " " + flags); | ||
| 447 | |||
| 448 | /* If a request arrives with hintMaxChars, hintMaxLines and flags | ||
| 449 | set to 0, and the system is known to be buggy, return an empty | ||
| 450 | extracted text object with the absolute selection positions. */ | ||
| 451 | |||
| 452 | if (extractAbsoluteOffsets | ||
| 453 | && request.hintMaxChars == 0 | ||
| 454 | && request.hintMaxLines == 0 | ||
| 455 | && flags == 0) | ||
| 456 | { | ||
| 457 | /* Obtain the selection. */ | ||
| 458 | selection = EmacsNative.getSelection (windowHandle); | ||
| 459 | if (selection == null) | ||
| 460 | return null; | ||
| 461 | |||
| 462 | /* Create the workaround extracted text. */ | ||
| 463 | text = new ExtractedText (); | ||
| 464 | text.partialStartOffset = -1; | ||
| 465 | text.partialEndOffset = -1; | ||
| 466 | text.text = ""; | ||
| 467 | text.selectionStart = selection[0]; | ||
| 468 | text.selectionEnd = selection[1]; | ||
| 469 | } | ||
| 470 | else | ||
| 471 | text = EmacsNative.getExtractedText (windowHandle, request, | ||
| 472 | flags); | ||
| 473 | |||
| 474 | if (text == null) | ||
| 475 | { | ||
| 476 | if (EmacsService.DEBUG_IC) | ||
| 477 | Log.d (TAG, "getExtractedText: text is NULL"); | ||
| 478 | |||
| 479 | return null; | ||
| 480 | } | ||
| 481 | |||
| 482 | if (EmacsService.DEBUG_IC) | ||
| 483 | Log.d (TAG, "getExtractedText: " + text.text + " @" | ||
| 484 | + text.startOffset + ":" + text.selectionStart | ||
| 485 | + ", " + text.selectionEnd); | ||
| 486 | |||
| 487 | return text; | ||
| 488 | } | ||
| 489 | |||
| 490 | @Override | ||
| 491 | public boolean | ||
| 492 | setSelection (int start, int end) | ||
| 493 | { | ||
| 494 | /* Return if the input connection is out of date. */ | ||
| 495 | if (view.icSerial < view.icGeneration) | ||
| 496 | return false; | ||
| 497 | |||
| 498 | if (EmacsService.DEBUG_IC) | ||
| 499 | Log.d (TAG, "setSelection: " + start + " " + end); | ||
| 500 | |||
| 501 | EmacsNative.setSelection (windowHandle, start, end); | ||
| 502 | return true; | ||
| 503 | } | ||
| 504 | |||
| 505 | @Override | ||
| 506 | /* ACTION_MULTIPLE is apparently obsolete. */ | ||
| 507 | @SuppressWarnings ("deprecation") | ||
| 508 | public boolean | ||
| 509 | sendKeyEvent (KeyEvent key) | ||
| 510 | { | ||
| 511 | /* Return if the input connection is out of date. */ | ||
| 512 | if (view.icSerial < view.icGeneration) | ||
| 513 | return false; | ||
| 514 | |||
| 515 | if (EmacsService.DEBUG_IC) | ||
| 516 | Log.d (TAG, "sendKeyEvent: " + key); | ||
| 517 | |||
| 518 | /* Use the standard API if possible. */ | ||
| 519 | |||
| 520 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) | ||
| 521 | view.imManager.dispatchKeyEventFromInputMethod (view, key); | ||
| 522 | else | ||
| 523 | { | ||
| 524 | /* Fall back to dispatching the event manually if not. */ | ||
| 525 | |||
| 526 | switch (key.getAction ()) | ||
| 527 | { | ||
| 528 | case KeyEvent.ACTION_DOWN: | ||
| 529 | view.onKeyDown (key.getKeyCode (), key); | ||
| 530 | break; | ||
| 531 | |||
| 532 | case KeyEvent.ACTION_UP: | ||
| 533 | view.onKeyUp (key.getKeyCode (), key); | ||
| 534 | break; | ||
| 535 | |||
| 536 | case KeyEvent.ACTION_MULTIPLE: | ||
| 537 | view.onKeyMultiple (key.getKeyCode (), | ||
| 538 | key.getRepeatCount (), | ||
| 539 | key); | ||
| 540 | break; | ||
| 541 | } | ||
| 542 | } | ||
| 543 | |||
| 544 | return true; | ||
| 545 | } | ||
| 546 | |||
| 547 | @Override | ||
| 548 | public boolean | ||
| 549 | requestCursorUpdates (int cursorUpdateMode) | ||
| 550 | { | ||
| 551 | /* Return if the input connection is out of date. */ | ||
| 552 | if (view.icSerial < view.icGeneration) | ||
| 553 | return false; | ||
| 554 | |||
| 555 | if (EmacsService.DEBUG_IC) | ||
| 556 | Log.d (TAG, "requestCursorUpdates: " + cursorUpdateMode); | ||
| 557 | |||
| 558 | EmacsNative.requestCursorUpdates (windowHandle, cursorUpdateMode); | ||
| 559 | return true; | ||
| 560 | } | ||
| 561 | |||
| 562 | @Override | ||
| 563 | public boolean | ||
| 564 | requestCursorUpdates (int cursorUpdateMode, int filter) | ||
| 565 | { | ||
| 566 | if (filter != 0) | ||
| 567 | return false; | ||
| 568 | |||
| 569 | return requestCursorUpdates (cursorUpdateMode); | ||
| 570 | } | ||
| 571 | |||
| 572 | @Override | ||
| 573 | public SurroundingText | ||
| 574 | getSurroundingText (int beforeLength, int afterLength, | ||
| 575 | int flags) | ||
| 576 | { | ||
| 577 | SurroundingText text; | ||
| 578 | |||
| 579 | /* Return if the input connection is out of date. */ | ||
| 580 | if (view.icSerial < view.icGeneration) | ||
| 581 | return null; | ||
| 582 | |||
| 583 | if (EmacsService.DEBUG_IC) | ||
| 584 | Log.d (TAG, ("getSurroundingText: " + beforeLength + ", " | ||
| 585 | + afterLength)); | ||
| 586 | |||
| 587 | text = EmacsNative.getSurroundingText (windowHandle, beforeLength, | ||
| 588 | afterLength, flags); | ||
| 589 | |||
| 590 | if (EmacsService.DEBUG_IC && text != null) | ||
| 591 | Log.d (TAG, ("getSurroundingText: " | ||
| 592 | + text.getSelectionStart () | ||
| 593 | + "," | ||
| 594 | + text.getSelectionEnd () | ||
| 595 | + "+" | ||
| 596 | + text.getOffset () | ||
| 597 | + ": " | ||
| 598 | + text.getText ())); | ||
| 599 | |||
| 600 | return text; | ||
| 601 | } | ||
| 602 | |||
| 603 | @Override | ||
| 604 | public TextSnapshot | ||
| 605 | takeSnapshot () | ||
| 606 | { | ||
| 607 | TextSnapshot snapshot; | ||
| 608 | |||
| 609 | /* Return if the input connection is out of date. */ | ||
| 610 | if (view.icSerial < view.icGeneration) | ||
| 611 | return null; | ||
| 612 | |||
| 613 | snapshot = EmacsNative.takeSnapshot (windowHandle); | ||
| 614 | |||
| 615 | if (EmacsService.DEBUG_IC) | ||
| 616 | Log.d (TAG, ("takeSnapshot: " | ||
| 617 | + snapshot.getSurroundingText ().getText () | ||
| 618 | + " @ " + snapshot.getCompositionEnd () | ||
| 619 | + ", " + snapshot.getCompositionStart ())); | ||
| 620 | |||
| 621 | return snapshot; | ||
| 622 | } | ||
| 623 | |||
| 624 | @Override | ||
| 625 | public void | ||
| 626 | closeConnection () | ||
| 627 | { | ||
| 628 | batchEditCount = 0; | ||
| 629 | } | ||
| 630 | |||
| 631 | |||
| 632 | |||
| 633 | public void | ||
| 634 | reset () | ||
| 635 | { | ||
| 636 | batchEditCount = 0; | ||
| 637 | } | ||
| 638 | |||
| 639 | |||
| 640 | /* Override functions which are not implemented. */ | ||
| 641 | |||
| 642 | @Override | ||
| 643 | public Handler | ||
| 644 | getHandler () | ||
| 645 | { | ||
| 646 | return null; | ||
| 647 | } | ||
| 648 | |||
| 649 | @Override | ||
| 650 | public boolean | ||
| 651 | commitContent (InputContentInfo inputContentInfo, int flags, | ||
| 652 | Bundle opts) | ||
| 653 | { | ||
| 654 | return false; | ||
| 655 | } | ||
| 656 | |||
| 657 | @Override | ||
| 658 | public boolean | ||
| 659 | setImeConsumesInput (boolean imeConsumesInput) | ||
| 660 | { | ||
| 661 | return false; | ||
| 662 | } | ||
| 663 | |||
| 664 | @Override | ||
| 665 | public boolean | ||
| 666 | clearMetaKeyStates (int states) | ||
| 667 | { | ||
| 668 | return false; | ||
| 669 | } | ||
| 670 | |||
| 671 | @Override | ||
| 672 | public boolean | ||
| 673 | reportFullscreenMode (boolean enabled) | ||
| 674 | { | ||
| 675 | return false; | ||
| 676 | } | ||
| 677 | |||
| 678 | @Override | ||
| 679 | public boolean | ||
| 680 | performSpellCheck () | ||
| 681 | { | ||
| 682 | return false; | ||
| 683 | } | ||
| 684 | |||
| 685 | @Override | ||
| 686 | public boolean | ||
| 687 | performPrivateCommand (String action, Bundle data) | ||
| 688 | { | ||
| 689 | return false; | ||
| 690 | } | ||
| 691 | |||
| 692 | @Override | ||
| 693 | public int | ||
| 694 | getCursorCapsMode (int reqModes) | ||
| 695 | { | ||
| 696 | return 0; | ||
| 697 | } | ||
| 698 | } | ||
diff --git a/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java b/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java new file mode 100644 index 00000000000..1e1e5d97631 --- /dev/null +++ b/java/org/gnu/emacs/EmacsLauncherPreferencesActivity.java | |||
| @@ -0,0 +1,31 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | /* This class only exists because EmacsPreferencesActivity is already | ||
| 23 | defined as an activity, the system wants a new class in order to | ||
| 24 | define a new activity, and only activities can be enabled or | ||
| 25 | disabled per the API level of the host. */ | ||
| 26 | |||
| 27 | public final class EmacsLauncherPreferencesActivity | ||
| 28 | extends EmacsPreferencesActivity | ||
| 29 | { | ||
| 30 | |||
| 31 | } | ||
diff --git a/java/org/gnu/emacs/EmacsMultitaskActivity.java b/java/org/gnu/emacs/EmacsMultitaskActivity.java new file mode 100644 index 00000000000..b1c48f03fba --- /dev/null +++ b/java/org/gnu/emacs/EmacsMultitaskActivity.java | |||
| @@ -0,0 +1,29 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | /* This class only exists because EmacsActivity is already defined as | ||
| 23 | an activity, and the system wants a new class in order to define a | ||
| 24 | new activity. */ | ||
| 25 | |||
| 26 | public final class EmacsMultitaskActivity extends EmacsActivity | ||
| 27 | { | ||
| 28 | |||
| 29 | } | ||
diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java new file mode 100644 index 00000000000..fae0ba98f86 --- /dev/null +++ b/java/org/gnu/emacs/EmacsNative.java | |||
| @@ -0,0 +1,316 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.content.res.AssetManager; | ||
| 23 | |||
| 24 | import android.graphics.Bitmap; | ||
| 25 | |||
| 26 | import android.view.inputmethod.ExtractedText; | ||
| 27 | import android.view.inputmethod.ExtractedTextRequest; | ||
| 28 | import android.view.inputmethod.SurroundingText; | ||
| 29 | import android.view.inputmethod.TextSnapshot; | ||
| 30 | |||
| 31 | public final class EmacsNative | ||
| 32 | { | ||
| 33 | /* List of native libraries that must be loaded during class | ||
| 34 | initialization. */ | ||
| 35 | private static final String[] libraryDeps; | ||
| 36 | |||
| 37 | |||
| 38 | /* Like `dup' in C. */ | ||
| 39 | public static native int dup (int fd); | ||
| 40 | |||
| 41 | /* Obtain the fingerprint of this build of Emacs. The fingerprint | ||
| 42 | can be used to determine the dump file name. */ | ||
| 43 | public static native String getFingerprint (); | ||
| 44 | |||
| 45 | /* Set certain parameters before initializing Emacs. | ||
| 46 | |||
| 47 | assetManager must be the asset manager associated with the | ||
| 48 | context that is loading Emacs. It is saved and remains for the | ||
| 49 | remainder the lifetime of the Emacs process. | ||
| 50 | |||
| 51 | filesDir must be the package's data storage location for the | ||
| 52 | current Android user. | ||
| 53 | |||
| 54 | libDir must be the package's data storage location for native | ||
| 55 | libraries. It is used as PATH. | ||
| 56 | |||
| 57 | cacheDir must be the package's cache directory. It is used as | ||
| 58 | the `temporary-file-directory'. | ||
| 59 | |||
| 60 | pixelDensityX and pixelDensityY are the DPI values that will be | ||
| 61 | used by Emacs. | ||
| 62 | |||
| 63 | scaledDensity is the DPI value used to translate point sizes to | ||
| 64 | pixel sizes when loading fonts. | ||
| 65 | |||
| 66 | classPath must be the classpath of this app_process process, or | ||
| 67 | NULL. | ||
| 68 | |||
| 69 | emacsService must be the EmacsService singleton, or NULL. | ||
| 70 | |||
| 71 | apiLevel is the version of Android being run. */ | ||
| 72 | public static native void setEmacsParams (AssetManager assetManager, | ||
| 73 | String filesDir, | ||
| 74 | String libDir, | ||
| 75 | String cacheDir, | ||
| 76 | float pixelDensityX, | ||
| 77 | float pixelDensityY, | ||
| 78 | float scaledDensity, | ||
| 79 | String classPath, | ||
| 80 | EmacsService emacsService, | ||
| 81 | int apiLevel); | ||
| 82 | |||
| 83 | /* Initialize Emacs with the argument array ARGV. Each argument | ||
| 84 | must contain a NULL terminated string, or else the behavior is | ||
| 85 | undefined. | ||
| 86 | |||
| 87 | DUMPFILE is the dump file to use, or NULL if Emacs is to load | ||
| 88 | loadup.el itself. */ | ||
| 89 | public static native void initEmacs (String argv[], String dumpFile); | ||
| 90 | |||
| 91 | /* Abort and generate a native core dump. */ | ||
| 92 | public static native void emacsAbort (); | ||
| 93 | |||
| 94 | /* Set Vquit_flag to t, resulting in Emacs quitting as soon as | ||
| 95 | possible. */ | ||
| 96 | public static native void quit (); | ||
| 97 | |||
| 98 | /* Send an ANDROID_CONFIGURE_NOTIFY event. The values of all the | ||
| 99 | functions below are the serials of the events sent. */ | ||
| 100 | public static native long sendConfigureNotify (short window, long time, | ||
| 101 | int x, int y, int width, | ||
| 102 | int height); | ||
| 103 | |||
| 104 | /* Send an ANDROID_KEY_PRESS event. */ | ||
| 105 | public static native long sendKeyPress (short window, long time, int state, | ||
| 106 | int keyCode, int unicodeChar); | ||
| 107 | |||
| 108 | /* Send an ANDROID_KEY_RELEASE event. */ | ||
| 109 | public static native long sendKeyRelease (short window, long time, int state, | ||
| 110 | int keyCode, int unicodeChar); | ||
| 111 | |||
| 112 | /* Send an ANDROID_FOCUS_IN event. */ | ||
| 113 | public static native long sendFocusIn (short window, long time); | ||
| 114 | |||
| 115 | /* Send an ANDROID_FOCUS_OUT event. */ | ||
| 116 | public static native long sendFocusOut (short window, long time); | ||
| 117 | |||
| 118 | /* Send an ANDROID_WINDOW_ACTION event. */ | ||
| 119 | public static native long sendWindowAction (short window, int action); | ||
| 120 | |||
| 121 | /* Send an ANDROID_ENTER_NOTIFY event. */ | ||
| 122 | public static native long sendEnterNotify (short window, int x, int y, | ||
| 123 | long time); | ||
| 124 | |||
| 125 | /* Send an ANDROID_LEAVE_NOTIFY event. */ | ||
| 126 | public static native long sendLeaveNotify (short window, int x, int y, | ||
| 127 | long time); | ||
| 128 | |||
| 129 | /* Send an ANDROID_MOTION_NOTIFY event. */ | ||
| 130 | public static native long sendMotionNotify (short window, int x, int y, | ||
| 131 | long time); | ||
| 132 | |||
| 133 | /* Send an ANDROID_BUTTON_PRESS event. */ | ||
| 134 | public static native long sendButtonPress (short window, int x, int y, | ||
| 135 | long time, int state, | ||
| 136 | int button); | ||
| 137 | |||
| 138 | /* Send an ANDROID_BUTTON_RELEASE event. */ | ||
| 139 | public static native long sendButtonRelease (short window, int x, int y, | ||
| 140 | long time, int state, | ||
| 141 | int button); | ||
| 142 | |||
| 143 | /* Send an ANDROID_TOUCH_DOWN event. */ | ||
| 144 | public static native long sendTouchDown (short window, int x, int y, | ||
| 145 | long time, int pointerID, | ||
| 146 | int flags); | ||
| 147 | |||
| 148 | /* Send an ANDROID_TOUCH_UP event. */ | ||
| 149 | public static native long sendTouchUp (short window, int x, int y, | ||
| 150 | long time, int pointerID, | ||
| 151 | int flags); | ||
| 152 | |||
| 153 | /* Send an ANDROID_TOUCH_MOVE event. */ | ||
| 154 | public static native long sendTouchMove (short window, int x, int y, | ||
| 155 | long time, int pointerID, | ||
| 156 | int flags); | ||
| 157 | |||
| 158 | /* Send an ANDROID_WHEEL event. */ | ||
| 159 | public static native long sendWheel (short window, int x, int y, | ||
| 160 | long time, int state, | ||
| 161 | float xDelta, float yDelta); | ||
| 162 | |||
| 163 | /* Send an ANDROID_ICONIFIED event. */ | ||
| 164 | public static native long sendIconified (short window); | ||
| 165 | |||
| 166 | /* Send an ANDROID_DEICONIFIED event. */ | ||
| 167 | public static native long sendDeiconified (short window); | ||
| 168 | |||
| 169 | /* Send an ANDROID_CONTEXT_MENU event. */ | ||
| 170 | public static native long sendContextMenu (short window, int menuEventID, | ||
| 171 | int menuEventSerial); | ||
| 172 | |||
| 173 | /* Send an ANDROID_EXPOSE event. */ | ||
| 174 | public static native long sendExpose (short window, int x, int y, | ||
| 175 | int width, int height); | ||
| 176 | |||
| 177 | /* Return the file name associated with the specified file | ||
| 178 | descriptor, or NULL if there is none. */ | ||
| 179 | public static native byte[] getProcName (int fd); | ||
| 180 | |||
| 181 | /* Notice that the Emacs thread will now start waiting for the main | ||
| 182 | thread's looper to respond. */ | ||
| 183 | public static native void beginSynchronous (); | ||
| 184 | |||
| 185 | /* Notice that the Emacs thread will has finished waiting for the | ||
| 186 | main thread's looper to respond. */ | ||
| 187 | public static native void endSynchronous (); | ||
| 188 | |||
| 189 | /* Prevent deadlocks while reliably allowing queries from the Emacs | ||
| 190 | thread to the main thread to complete by waiting for a query to | ||
| 191 | start from the main thread, then answer it; assume that a query | ||
| 192 | is certain to start shortly. */ | ||
| 193 | public static native void answerQuerySpin (); | ||
| 194 | |||
| 195 | /* Return whether or not KEYCODE_VOLUME_DOWN, KEYCODE_VOLUME_UP and | ||
| 196 | KEYCODE_VOLUME_MUTE should be forwarded to Emacs. */ | ||
| 197 | public static native boolean shouldForwardMultimediaButtons (); | ||
| 198 | |||
| 199 | /* Initialize the current thread, by blocking signals that do not | ||
| 200 | interest it. */ | ||
| 201 | public static native void setupSystemThread (); | ||
| 202 | |||
| 203 | |||
| 204 | |||
| 205 | /* Input connection functions. These mostly correspond to their | ||
| 206 | counterparts in Android's InputConnection. */ | ||
| 207 | |||
| 208 | public static native void beginBatchEdit (short window); | ||
| 209 | public static native void endBatchEdit (short window); | ||
| 210 | public static native void commitCompletion (short window, String text, | ||
| 211 | int position); | ||
| 212 | public static native void commitText (short window, String text, | ||
| 213 | int position); | ||
| 214 | public static native void deleteSurroundingText (short window, | ||
| 215 | int leftLength, | ||
| 216 | int rightLength); | ||
| 217 | public static native void finishComposingText (short window); | ||
| 218 | public static native String getSelectedText (short window, int flags); | ||
| 219 | public static native String getTextAfterCursor (short window, int length, | ||
| 220 | int flags); | ||
| 221 | public static native String getTextBeforeCursor (short window, int length, | ||
| 222 | int flags); | ||
| 223 | public static native void setComposingText (short window, String text, | ||
| 224 | int newCursorPosition); | ||
| 225 | public static native void setComposingRegion (short window, int start, | ||
| 226 | int end); | ||
| 227 | public static native void setSelection (short window, int start, int end); | ||
| 228 | public static native void performEditorAction (short window, | ||
| 229 | int editorAction); | ||
| 230 | public static native void performContextMenuAction (short window, | ||
| 231 | int contextMenuAction); | ||
| 232 | public static native ExtractedText getExtractedText (short window, | ||
| 233 | ExtractedTextRequest req, | ||
| 234 | int flags); | ||
| 235 | public static native void requestSelectionUpdate (short window); | ||
| 236 | public static native void requestCursorUpdates (short window, int mode); | ||
| 237 | public static native void clearInputFlags (short window); | ||
| 238 | public static native SurroundingText getSurroundingText (short window, | ||
| 239 | int left, int right, | ||
| 240 | int flags); | ||
| 241 | public static native TextSnapshot takeSnapshot (short window); | ||
| 242 | |||
| 243 | |||
| 244 | /* Return the current value of the selection, or -1 upon | ||
| 245 | failure. */ | ||
| 246 | public static native int[] getSelection (short window); | ||
| 247 | |||
| 248 | |||
| 249 | /* Graphics functions used as a replacement for potentially buggy | ||
| 250 | Android APIs. */ | ||
| 251 | |||
| 252 | public static native void blitRect (Bitmap src, Bitmap dest, int x1, | ||
| 253 | int y1, int x2, int y2); | ||
| 254 | |||
| 255 | /* Increment the generation ID of the specified BITMAP, forcing its | ||
| 256 | texture to be re-uploaded to the GPU. */ | ||
| 257 | |||
| 258 | public static native void notifyPixelsChanged (Bitmap bitmap); | ||
| 259 | |||
| 260 | |||
| 261 | /* Functions used to synchronize document provider access with the | ||
| 262 | main thread. */ | ||
| 263 | |||
| 264 | /* Wait for a call to `safPostRequest' while also reading async | ||
| 265 | input. | ||
| 266 | |||
| 267 | If asynchronous input arrives and sets Vquit_flag, return 1. */ | ||
| 268 | public static native int safSyncAndReadInput (); | ||
| 269 | |||
| 270 | /* Wait for a call to `safPostRequest'. */ | ||
| 271 | public static native void safSync (); | ||
| 272 | |||
| 273 | /* Post the semaphore used to await the completion of SAF | ||
| 274 | operations. */ | ||
| 275 | public static native void safPostRequest (); | ||
| 276 | |||
| 277 | /* Detect and return FD is writable. FD may be truncated to 0 bytes | ||
| 278 | in the process. */ | ||
| 279 | public static native boolean ftruncate (int fd); | ||
| 280 | |||
| 281 | static | ||
| 282 | { | ||
| 283 | /* Older versions of Android cannot link correctly with shared | ||
| 284 | libraries that link with other shared libraries built along | ||
| 285 | Emacs unless all requisite shared libraries are explicitly | ||
| 286 | loaded from Java. | ||
| 287 | |||
| 288 | Every time you add a new shared library dependency to Emacs, | ||
| 289 | please add it here as well. */ | ||
| 290 | |||
| 291 | libraryDeps = new String[] { "png_emacs", "selinux_emacs", | ||
| 292 | "crypto_emacs", "pcre_emacs", | ||
| 293 | "packagelistparser_emacs", | ||
| 294 | "gnutls_emacs", "gmp_emacs", | ||
| 295 | "nettle_emacs", "p11-kit_emacs", | ||
| 296 | "tasn1_emacs", "hogweed_emacs", | ||
| 297 | "jansson_emacs", "jpeg_emacs", | ||
| 298 | "tiff_emacs", "xml2_emacs", | ||
| 299 | "icuuc_emacs", | ||
| 300 | "tree-sitter_emacs", }; | ||
| 301 | |||
| 302 | for (String dependency : libraryDeps) | ||
| 303 | { | ||
| 304 | try | ||
| 305 | { | ||
| 306 | System.loadLibrary (dependency); | ||
| 307 | } | ||
| 308 | catch (UnsatisfiedLinkError exception) | ||
| 309 | { | ||
| 310 | /* Ignore this exception. */ | ||
| 311 | } | ||
| 312 | } | ||
| 313 | |||
| 314 | System.loadLibrary ("emacs"); | ||
| 315 | }; | ||
| 316 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsNoninteractive.java b/java/org/gnu/emacs/EmacsNoninteractive.java new file mode 100644 index 00000000000..1c7513e1cc9 --- /dev/null +++ b/java/org/gnu/emacs/EmacsNoninteractive.java | |||
| @@ -0,0 +1,203 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.os.Looper; | ||
| 23 | import android.os.Build; | ||
| 24 | |||
| 25 | import android.content.Context; | ||
| 26 | import android.content.res.AssetManager; | ||
| 27 | |||
| 28 | import java.lang.reflect.Constructor; | ||
| 29 | import java.lang.reflect.Method; | ||
| 30 | |||
| 31 | /* Noninteractive Emacs. | ||
| 32 | |||
| 33 | This is the class that libandroid-emacs.so starts. | ||
| 34 | libandroid-emacs.so figures out the system classpath, then starts | ||
| 35 | dalvikvm with the framework jars. | ||
| 36 | |||
| 37 | At that point, dalvikvm calls main, which sets up the main looper, | ||
| 38 | creates an ActivityThread and attaches it to the main thread. | ||
| 39 | |||
| 40 | Then, it obtains an application context for the LoadedApk in the | ||
| 41 | application thread. | ||
| 42 | |||
| 43 | Finally, it obtains the necessary context specific objects and | ||
| 44 | initializes Emacs. */ | ||
| 45 | |||
| 46 | @SuppressWarnings ("unchecked") | ||
| 47 | public final class EmacsNoninteractive | ||
| 48 | { | ||
| 49 | public static void | ||
| 50 | main (String[] args) | ||
| 51 | { | ||
| 52 | Object activityThread, loadedApk; | ||
| 53 | Class activityThreadClass, loadedApkClass, contextImplClass; | ||
| 54 | Class compatibilityInfoClass; | ||
| 55 | Method method; | ||
| 56 | Context context; | ||
| 57 | AssetManager assets; | ||
| 58 | String filesDir, libDir, cacheDir; | ||
| 59 | |||
| 60 | Looper.prepare (); | ||
| 61 | context = null; | ||
| 62 | assets = null; | ||
| 63 | filesDir = libDir = cacheDir = null; | ||
| 64 | |||
| 65 | try | ||
| 66 | { | ||
| 67 | /* Get the activity thread. */ | ||
| 68 | activityThreadClass = Class.forName ("android.app.ActivityThread"); | ||
| 69 | |||
| 70 | /* Get the systemMain method. */ | ||
| 71 | method = activityThreadClass.getMethod ("systemMain"); | ||
| 72 | |||
| 73 | /* Create and attach the activity thread. */ | ||
| 74 | activityThread = method.invoke (null); | ||
| 75 | context = null; | ||
| 76 | |||
| 77 | /* Now get an LoadedApk. */ | ||
| 78 | |||
| 79 | try | ||
| 80 | { | ||
| 81 | loadedApkClass = Class.forName ("android.app.LoadedApk"); | ||
| 82 | } | ||
| 83 | catch (ClassNotFoundException exception) | ||
| 84 | { | ||
| 85 | /* Android 2.2 has no LoadedApk class, but fortunately it | ||
| 86 | does not need to be used, since contexts can be | ||
| 87 | directly created. */ | ||
| 88 | |||
| 89 | loadedApkClass = null; | ||
| 90 | contextImplClass = Class.forName ("android.app.ContextImpl"); | ||
| 91 | |||
| 92 | method = activityThreadClass.getDeclaredMethod ("getSystemContext"); | ||
| 93 | context = (Context) method.invoke (activityThread); | ||
| 94 | method = contextImplClass.getDeclaredMethod ("createPackageContext", | ||
| 95 | String.class, | ||
| 96 | int.class); | ||
| 97 | method.setAccessible (true); | ||
| 98 | context = (Context) method.invoke (context, "org.gnu.emacs", | ||
| 99 | 0); | ||
| 100 | } | ||
| 101 | |||
| 102 | /* If the context has not already been created, then do what | ||
| 103 | is appropriate for newer versions of Android. */ | ||
| 104 | |||
| 105 | if (context == null) | ||
| 106 | { | ||
| 107 | /* Get a LoadedApk. How to do this varies by Android version. | ||
| 108 | On Android 2.3.3 and earlier, there is no | ||
| 109 | ``compatibilityInfo'' argument to getPackageInfo. */ | ||
| 110 | |||
| 111 | if (Build.VERSION.SDK_INT | ||
| 112 | <= Build.VERSION_CODES.GINGERBREAD_MR1) | ||
| 113 | { | ||
| 114 | method | ||
| 115 | = activityThreadClass.getMethod ("getPackageInfo", | ||
| 116 | String.class, | ||
| 117 | int.class); | ||
| 118 | loadedApk = method.invoke (activityThread, "org.gnu.emacs", | ||
| 119 | 0); | ||
| 120 | } | ||
| 121 | else | ||
| 122 | { | ||
| 123 | compatibilityInfoClass | ||
| 124 | = Class.forName ("android.content.res.CompatibilityInfo"); | ||
| 125 | |||
| 126 | method | ||
| 127 | = activityThreadClass.getMethod ("getPackageInfo", | ||
| 128 | String.class, | ||
| 129 | compatibilityInfoClass, | ||
| 130 | int.class); | ||
| 131 | loadedApk = method.invoke (activityThread, "org.gnu.emacs", | ||
| 132 | null, 0); | ||
| 133 | } | ||
| 134 | |||
| 135 | if (loadedApk == null) | ||
| 136 | throw new RuntimeException ("getPackageInfo returned NULL"); | ||
| 137 | |||
| 138 | /* Now, get a context. */ | ||
| 139 | contextImplClass = Class.forName ("android.app.ContextImpl"); | ||
| 140 | |||
| 141 | try | ||
| 142 | { | ||
| 143 | method | ||
| 144 | = contextImplClass.getDeclaredMethod ("createAppContext", | ||
| 145 | activityThreadClass, | ||
| 146 | loadedApkClass); | ||
| 147 | method.setAccessible (true); | ||
| 148 | context = (Context) method.invoke (null, activityThread, | ||
| 149 | loadedApk); | ||
| 150 | } | ||
| 151 | catch (NoSuchMethodException exception) | ||
| 152 | { | ||
| 153 | /* Older Android versions don't have createAppContext, but | ||
| 154 | instead require creating a ContextImpl, and then | ||
| 155 | calling createPackageContext. */ | ||
| 156 | method | ||
| 157 | = activityThreadClass.getDeclaredMethod ("getSystemContext"); | ||
| 158 | context = (Context) method.invoke (activityThread); | ||
| 159 | method | ||
| 160 | = contextImplClass.getDeclaredMethod ("createPackageContext", | ||
| 161 | String.class, | ||
| 162 | int.class); | ||
| 163 | method.setAccessible (true); | ||
| 164 | context = (Context) method.invoke (context, "org.gnu.emacs", | ||
| 165 | 0); | ||
| 166 | } | ||
| 167 | } | ||
| 168 | |||
| 169 | /* Don't actually start the looper or anything. Instead, obtain | ||
| 170 | an AssetManager. */ | ||
| 171 | assets = context.getAssets (); | ||
| 172 | |||
| 173 | /* Now configure Emacs. The class path should already be set. */ | ||
| 174 | |||
| 175 | filesDir = context.getFilesDir ().getCanonicalPath (); | ||
| 176 | libDir = EmacsService.getLibraryDirectory (context); | ||
| 177 | cacheDir = context.getCacheDir ().getCanonicalPath (); | ||
| 178 | } | ||
| 179 | catch (Exception e) | ||
| 180 | { | ||
| 181 | System.err.println ("Internal error: " + e); | ||
| 182 | System.err.println ("This means that the Android platform changed,"); | ||
| 183 | System.err.println ("and that Emacs needs adjustments in order to"); | ||
| 184 | System.err.println ("obtain required system internal resources."); | ||
| 185 | System.err.println ("Please report this bug to bug-gnu-emacs@gnu.org."); | ||
| 186 | e.printStackTrace (); | ||
| 187 | |||
| 188 | System.exit (1); | ||
| 189 | } | ||
| 190 | |||
| 191 | EmacsNative.setEmacsParams (assets, filesDir, | ||
| 192 | libDir, cacheDir, 0.0f, | ||
| 193 | 0.0f, 0.0f, null, null, | ||
| 194 | Build.VERSION.SDK_INT); | ||
| 195 | |||
| 196 | /* Now find the dump file that Emacs should use, if it has already | ||
| 197 | been dumped. */ | ||
| 198 | EmacsApplication.findDumpFile (context); | ||
| 199 | |||
| 200 | /* Start Emacs. */ | ||
| 201 | EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); | ||
| 202 | } | ||
| 203 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java new file mode 100644 index 00000000000..3832cd2faab --- /dev/null +++ b/java/org/gnu/emacs/EmacsOpenActivity.java | |||
| @@ -0,0 +1,552 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | /* This class makes the Emacs server work reasonably on Android. | ||
| 23 | |||
| 24 | There is no way to make the Unix socket publicly available on | ||
| 25 | Android. | ||
| 26 | |||
| 27 | Instead, this activity tries to connect to the Emacs server, to | ||
| 28 | make it open files the system asks Emacs to open, and to emulate | ||
| 29 | some reasonable behavior when Emacs has not yet started. | ||
| 30 | |||
| 31 | First, Emacs registers itself as an application that can open text | ||
| 32 | and image files. | ||
| 33 | |||
| 34 | Then, when the user is asked to open a file and selects ``Emacs'' | ||
| 35 | as the application that will open the file, the system pops up a | ||
| 36 | window, this activity, and calls the `onCreate' function. | ||
| 37 | |||
| 38 | `onCreate' then tries very to find the file name of the file that | ||
| 39 | was selected, and give it to emacsclient. | ||
| 40 | |||
| 41 | If emacsclient successfully opens the file, then this activity | ||
| 42 | starts EmacsActivity (to bring it on to the screen); otherwise, it | ||
| 43 | displays the output of emacsclient or any error message that occurs | ||
| 44 | and exits. */ | ||
| 45 | |||
| 46 | import android.app.AlertDialog; | ||
| 47 | import android.app.Activity; | ||
| 48 | |||
| 49 | import android.content.ContentResolver; | ||
| 50 | import android.content.DialogInterface; | ||
| 51 | import android.content.Intent; | ||
| 52 | |||
| 53 | import android.net.Uri; | ||
| 54 | |||
| 55 | import android.os.Build; | ||
| 56 | import android.os.Bundle; | ||
| 57 | import android.os.ParcelFileDescriptor; | ||
| 58 | |||
| 59 | import android.util.Log; | ||
| 60 | |||
| 61 | import java.io.File; | ||
| 62 | import java.io.FileInputStream; | ||
| 63 | import java.io.FileNotFoundException; | ||
| 64 | import java.io.FileOutputStream; | ||
| 65 | import java.io.FileReader; | ||
| 66 | import java.io.IOException; | ||
| 67 | import java.io.InputStream; | ||
| 68 | import java.io.UnsupportedEncodingException; | ||
| 69 | |||
| 70 | public final class EmacsOpenActivity extends Activity | ||
| 71 | implements DialogInterface.OnClickListener, | ||
| 72 | DialogInterface.OnCancelListener | ||
| 73 | { | ||
| 74 | private static final String TAG = "EmacsOpenActivity"; | ||
| 75 | |||
| 76 | /* The name of any file that should be opened as EmacsThread starts | ||
| 77 | Emacs. This is never cleared, even if EmacsOpenActivity is | ||
| 78 | started a second time, as EmacsThread only starts once. */ | ||
| 79 | public static String fileToOpen; | ||
| 80 | |||
| 81 | /* Any currently focused EmacsOpenActivity. Used to show pop ups | ||
| 82 | while the activity is active and Emacs doesn't have permission to | ||
| 83 | display over other programs. */ | ||
| 84 | public static EmacsOpenActivity currentActivity; | ||
| 85 | |||
| 86 | private class EmacsClientThread extends Thread | ||
| 87 | { | ||
| 88 | private ProcessBuilder builder; | ||
| 89 | |||
| 90 | public | ||
| 91 | EmacsClientThread (ProcessBuilder processBuilder) | ||
| 92 | { | ||
| 93 | builder = processBuilder; | ||
| 94 | } | ||
| 95 | |||
| 96 | @Override | ||
| 97 | public void | ||
| 98 | run () | ||
| 99 | { | ||
| 100 | Process process; | ||
| 101 | InputStream error; | ||
| 102 | String errorText; | ||
| 103 | |||
| 104 | try | ||
| 105 | { | ||
| 106 | /* Start emacsclient. */ | ||
| 107 | process = builder.start (); | ||
| 108 | process.waitFor (); | ||
| 109 | |||
| 110 | /* Now figure out whether or not starting the process was | ||
| 111 | successful. */ | ||
| 112 | if (process.exitValue () == 0) | ||
| 113 | finishSuccess (); | ||
| 114 | else | ||
| 115 | finishFailure ("Error opening file", null); | ||
| 116 | } | ||
| 117 | catch (IOException exception) | ||
| 118 | { | ||
| 119 | finishFailure ("Internal error", exception.toString ()); | ||
| 120 | } | ||
| 121 | catch (InterruptedException exception) | ||
| 122 | { | ||
| 123 | finishFailure ("Internal error", exception.toString ()); | ||
| 124 | } | ||
| 125 | } | ||
| 126 | } | ||
| 127 | |||
| 128 | @Override | ||
| 129 | public void | ||
| 130 | onClick (DialogInterface dialog, int which) | ||
| 131 | { | ||
| 132 | finish (); | ||
| 133 | } | ||
| 134 | |||
| 135 | @Override | ||
| 136 | public void | ||
| 137 | onCancel (DialogInterface dialog) | ||
| 138 | { | ||
| 139 | finish (); | ||
| 140 | } | ||
| 141 | |||
| 142 | public String | ||
| 143 | readEmacsClientLog () | ||
| 144 | { | ||
| 145 | File file, cache; | ||
| 146 | FileReader reader; | ||
| 147 | char[] buffer; | ||
| 148 | int rc; | ||
| 149 | StringBuilder builder; | ||
| 150 | |||
| 151 | /* Because the ProcessBuilder functions necessary to redirect | ||
| 152 | process output are not implemented on Android 7 and earlier, | ||
| 153 | print a generic error message. */ | ||
| 154 | |||
| 155 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) | ||
| 156 | return ("This is likely because the Emacs server" | ||
| 157 | + " is not running, or because you did" | ||
| 158 | + " not grant Emacs permission to access" | ||
| 159 | + " external storage."); | ||
| 160 | |||
| 161 | cache = getCacheDir (); | ||
| 162 | file = new File (cache, "emacsclient.log"); | ||
| 163 | builder = new StringBuilder (); | ||
| 164 | reader = null; | ||
| 165 | |||
| 166 | try | ||
| 167 | { | ||
| 168 | reader = new FileReader (file); | ||
| 169 | buffer = new char[2048]; | ||
| 170 | |||
| 171 | while ((rc = reader.read (buffer, 0, 2048)) != -1) | ||
| 172 | builder.append (buffer, 0, rc); | ||
| 173 | |||
| 174 | reader.close (); | ||
| 175 | return builder.toString (); | ||
| 176 | } | ||
| 177 | catch (IOException exception) | ||
| 178 | { | ||
| 179 | /* Close the reader if it's already been opened. */ | ||
| 180 | |||
| 181 | try | ||
| 182 | { | ||
| 183 | if (reader != null) | ||
| 184 | reader.close (); | ||
| 185 | } | ||
| 186 | catch (IOException e) | ||
| 187 | { | ||
| 188 | /* Not sure what to do here. */ | ||
| 189 | } | ||
| 190 | |||
| 191 | return ("Couldn't read emacsclient.log: " | ||
| 192 | + exception.toString ()); | ||
| 193 | } | ||
| 194 | } | ||
| 195 | |||
| 196 | private void | ||
| 197 | displayFailureDialog (String title, String text) | ||
| 198 | { | ||
| 199 | AlertDialog.Builder builder; | ||
| 200 | AlertDialog dialog; | ||
| 201 | |||
| 202 | builder = new AlertDialog.Builder (this); | ||
| 203 | dialog = builder.create (); | ||
| 204 | dialog.setTitle (title); | ||
| 205 | |||
| 206 | if (text == null) | ||
| 207 | /* Read in emacsclient.log instead. */ | ||
| 208 | text = readEmacsClientLog (); | ||
| 209 | |||
| 210 | dialog.setMessage (text); | ||
| 211 | dialog.setButton (DialogInterface.BUTTON_POSITIVE, "OK", this); | ||
| 212 | dialog.setOnCancelListener (this); | ||
| 213 | dialog.show (); | ||
| 214 | } | ||
| 215 | |||
| 216 | /* Check that the specified FILE is readable. If Android 4.4 or | ||
| 217 | later is being used, return URI formatted into a `/content/' file | ||
| 218 | name. | ||
| 219 | |||
| 220 | If it is not, then copy the file in FD to a location in the | ||
| 221 | system cache directory and return the name of that file. */ | ||
| 222 | |||
| 223 | private String | ||
| 224 | checkReadableOrCopy (String file, ParcelFileDescriptor fd, | ||
| 225 | Uri uri) | ||
| 226 | throws IOException, FileNotFoundException | ||
| 227 | { | ||
| 228 | File inFile; | ||
| 229 | FileOutputStream outStream; | ||
| 230 | InputStream stream; | ||
| 231 | byte buffer[]; | ||
| 232 | int read; | ||
| 233 | String content; | ||
| 234 | |||
| 235 | Log.d (TAG, "checkReadableOrCopy: " + file); | ||
| 236 | |||
| 237 | inFile = new File (file); | ||
| 238 | |||
| 239 | if (inFile.canRead ()) | ||
| 240 | return file; | ||
| 241 | |||
| 242 | Log.d (TAG, "checkReadableOrCopy: NO"); | ||
| 243 | |||
| 244 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) | ||
| 245 | { | ||
| 246 | content = EmacsService.buildContentName (uri); | ||
| 247 | Log.d (TAG, "checkReadableOrCopy: " + content); | ||
| 248 | return content; | ||
| 249 | } | ||
| 250 | |||
| 251 | /* inFile is now the file being written to. */ | ||
| 252 | inFile = new File (getCacheDir (), inFile.getName ()); | ||
| 253 | buffer = new byte[4098]; | ||
| 254 | |||
| 255 | /* Initialize both streams to NULL. */ | ||
| 256 | outStream = null; | ||
| 257 | stream = null; | ||
| 258 | |||
| 259 | try | ||
| 260 | { | ||
| 261 | outStream = new FileOutputStream (inFile); | ||
| 262 | stream = new FileInputStream (fd.getFileDescriptor ()); | ||
| 263 | |||
| 264 | while ((read = stream.read (buffer)) >= 0) | ||
| 265 | outStream.write (buffer, 0, read); | ||
| 266 | } | ||
| 267 | finally | ||
| 268 | { | ||
| 269 | /* Note that this does not close FD. | ||
| 270 | |||
| 271 | Keep in mind that execution is transferred to ``finally'' | ||
| 272 | even if an exception happens inside the while loop | ||
| 273 | above. */ | ||
| 274 | |||
| 275 | if (stream != null) | ||
| 276 | stream.close (); | ||
| 277 | |||
| 278 | if (outStream != null) | ||
| 279 | outStream.close (); | ||
| 280 | } | ||
| 281 | |||
| 282 | return inFile.getCanonicalPath (); | ||
| 283 | } | ||
| 284 | |||
| 285 | /* Finish this activity in response to emacsclient having | ||
| 286 | successfully opened a file. | ||
| 287 | |||
| 288 | In the main thread, close this window, and open a window | ||
| 289 | belonging to an Emacs frame. */ | ||
| 290 | |||
| 291 | public void | ||
| 292 | finishSuccess () | ||
| 293 | { | ||
| 294 | runOnUiThread (new Runnable () { | ||
| 295 | @Override | ||
| 296 | public void | ||
| 297 | run () | ||
| 298 | { | ||
| 299 | Intent intent; | ||
| 300 | |||
| 301 | intent = new Intent (EmacsOpenActivity.this, | ||
| 302 | EmacsActivity.class); | ||
| 303 | |||
| 304 | /* This means only an existing frame will be displayed. */ | ||
| 305 | intent.addFlags (Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); | ||
| 306 | startActivity (intent); | ||
| 307 | |||
| 308 | EmacsOpenActivity.this.finish (); | ||
| 309 | } | ||
| 310 | }); | ||
| 311 | } | ||
| 312 | |||
| 313 | /* Finish this activity after displaying a dialog associated with | ||
| 314 | failure to open a file. | ||
| 315 | |||
| 316 | Use TITLE as the title of the dialog. If TEXT is non-NULL, | ||
| 317 | display that text in the dialog. Otherwise, use the contents of | ||
| 318 | emacsclient.log in the cache directory instead, or describe why | ||
| 319 | that file cannot be read. */ | ||
| 320 | |||
| 321 | public void | ||
| 322 | finishFailure (final String title, final String text) | ||
| 323 | { | ||
| 324 | runOnUiThread (new Runnable () { | ||
| 325 | @Override | ||
| 326 | public void | ||
| 327 | run () | ||
| 328 | { | ||
| 329 | displayFailureDialog (title, text); | ||
| 330 | } | ||
| 331 | }); | ||
| 332 | } | ||
| 333 | |||
| 334 | public void | ||
| 335 | startEmacsClient (String fileName) | ||
| 336 | { | ||
| 337 | String libDir; | ||
| 338 | ProcessBuilder builder; | ||
| 339 | Process process; | ||
| 340 | EmacsClientThread thread; | ||
| 341 | File file; | ||
| 342 | Intent intent; | ||
| 343 | |||
| 344 | /* If the Emacs service is not running, then start Emacs and make | ||
| 345 | it open this file. */ | ||
| 346 | |||
| 347 | if (EmacsService.SERVICE == null) | ||
| 348 | { | ||
| 349 | fileToOpen = fileName; | ||
| 350 | intent = new Intent (EmacsOpenActivity.this, | ||
| 351 | EmacsActivity.class); | ||
| 352 | finish (); | ||
| 353 | startActivity (intent); | ||
| 354 | return; | ||
| 355 | } | ||
| 356 | |||
| 357 | libDir = EmacsService.getLibraryDirectory (this); | ||
| 358 | builder = new ProcessBuilder (libDir + "/libemacsclient.so", | ||
| 359 | fileName, "--reuse-frame", | ||
| 360 | "--timeout=10", "--no-wait"); | ||
| 361 | |||
| 362 | /* Redirection is unfortunately not possible in Android 7 and | ||
| 363 | earlier. */ | ||
| 364 | |||
| 365 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) | ||
| 366 | { | ||
| 367 | file = new File (getCacheDir (), "emacsclient.log"); | ||
| 368 | |||
| 369 | /* Redirect standard error to a file so that errors can be | ||
| 370 | meaningfully reported. */ | ||
| 371 | |||
| 372 | if (file.exists ()) | ||
| 373 | file.delete (); | ||
| 374 | |||
| 375 | builder.redirectError (file); | ||
| 376 | } | ||
| 377 | |||
| 378 | /* Track process output in a new thread, since this is the UI | ||
| 379 | thread and doing so here can cause deadlocks when EmacsService | ||
| 380 | decides to wait for something. */ | ||
| 381 | |||
| 382 | thread = new EmacsClientThread (builder); | ||
| 383 | thread.start (); | ||
| 384 | } | ||
| 385 | |||
| 386 | /* Run emacsclient to open the file specified in the Intent that | ||
| 387 | caused this activity to start. | ||
| 388 | |||
| 389 | Determine the name of the file corresponding to the URI specified | ||
| 390 | in that intent; then, run emacsclient and wait for it to finish. | ||
| 391 | |||
| 392 | Finally, display any error message, transfer the focus to an | ||
| 393 | Emacs frame, and finish the activity. */ | ||
| 394 | |||
| 395 | @Override | ||
| 396 | public void | ||
| 397 | onCreate (Bundle savedInstanceState) | ||
| 398 | { | ||
| 399 | String action, fileName; | ||
| 400 | Intent intent; | ||
| 401 | Uri uri; | ||
| 402 | ContentResolver resolver; | ||
| 403 | ParcelFileDescriptor fd; | ||
| 404 | byte[] names; | ||
| 405 | String errorBlurb; | ||
| 406 | |||
| 407 | super.onCreate (savedInstanceState); | ||
| 408 | |||
| 409 | /* Obtain the intent that started Emacs. */ | ||
| 410 | intent = getIntent (); | ||
| 411 | action = intent.getAction (); | ||
| 412 | |||
| 413 | if (action == null) | ||
| 414 | { | ||
| 415 | finish (); | ||
| 416 | return; | ||
| 417 | } | ||
| 418 | |||
| 419 | /* Now see if the action specified is supported by Emacs. */ | ||
| 420 | |||
| 421 | if (action.equals ("android.intent.action.VIEW") | ||
| 422 | || action.equals ("android.intent.action.EDIT") | ||
| 423 | || action.equals ("android.intent.action.PICK")) | ||
| 424 | { | ||
| 425 | /* Obtain the URI of the action. */ | ||
| 426 | uri = intent.getData (); | ||
| 427 | |||
| 428 | if (uri == null) | ||
| 429 | { | ||
| 430 | finish (); | ||
| 431 | return; | ||
| 432 | } | ||
| 433 | |||
| 434 | /* Now, try to get the file name. */ | ||
| 435 | |||
| 436 | if (uri.getScheme ().equals ("file")) | ||
| 437 | fileName = uri.getPath (); | ||
| 438 | else | ||
| 439 | { | ||
| 440 | fileName = null; | ||
| 441 | |||
| 442 | if (uri.getScheme ().equals ("content")) | ||
| 443 | { | ||
| 444 | /* This is one of the annoying Android ``content'' | ||
| 445 | URIs. Most of the time, there is actually an | ||
| 446 | underlying file, but it cannot be found without | ||
| 447 | opening the file and doing readlink on its file | ||
| 448 | descriptor in /proc/self/fd. */ | ||
| 449 | resolver = getContentResolver (); | ||
| 450 | fd = null; | ||
| 451 | |||
| 452 | try | ||
| 453 | { | ||
| 454 | fd = resolver.openFileDescriptor (uri, "r"); | ||
| 455 | names = EmacsNative.getProcName (fd.getFd ()); | ||
| 456 | |||
| 457 | /* What is the right encoding here? */ | ||
| 458 | |||
| 459 | if (names != null) | ||
| 460 | fileName = new String (names, "UTF-8"); | ||
| 461 | |||
| 462 | fileName = checkReadableOrCopy (fileName, fd, uri); | ||
| 463 | } | ||
| 464 | catch (FileNotFoundException exception) | ||
| 465 | { | ||
| 466 | /* Do nothing. */ | ||
| 467 | } | ||
| 468 | catch (IOException exception) | ||
| 469 | { | ||
| 470 | /* Do nothing. */ | ||
| 471 | } | ||
| 472 | |||
| 473 | if (fd != null) | ||
| 474 | { | ||
| 475 | try | ||
| 476 | { | ||
| 477 | fd.close (); | ||
| 478 | } | ||
| 479 | catch (IOException exception) | ||
| 480 | { | ||
| 481 | /* Do nothing. */ | ||
| 482 | } | ||
| 483 | } | ||
| 484 | } | ||
| 485 | |||
| 486 | if (fileName == null) | ||
| 487 | { | ||
| 488 | errorBlurb = ("The URI: " + uri + " could not be opened" | ||
| 489 | + ", as it does not encode file name inform" | ||
| 490 | + "ation."); | ||
| 491 | displayFailureDialog ("Error opening file", errorBlurb); | ||
| 492 | return; | ||
| 493 | } | ||
| 494 | } | ||
| 495 | |||
| 496 | /* And start emacsclient. Set `currentActivity' to this now. | ||
| 497 | Presumably, it will shortly become capable of displaying | ||
| 498 | dialogs. */ | ||
| 499 | currentActivity = this; | ||
| 500 | startEmacsClient (fileName); | ||
| 501 | } | ||
| 502 | else | ||
| 503 | finish (); | ||
| 504 | } | ||
| 505 | |||
| 506 | |||
| 507 | |||
| 508 | @Override | ||
| 509 | public void | ||
| 510 | onDestroy () | ||
| 511 | { | ||
| 512 | Log.d (TAG, "onDestroy: " + this); | ||
| 513 | |||
| 514 | /* Clear `currentActivity' if it refers to the activity being | ||
| 515 | destroyed. */ | ||
| 516 | |||
| 517 | if (currentActivity == this) | ||
| 518 | this.currentActivity = null; | ||
| 519 | |||
| 520 | super.onDestroy (); | ||
| 521 | } | ||
| 522 | |||
| 523 | @Override | ||
| 524 | public void | ||
| 525 | onWindowFocusChanged (boolean isFocused) | ||
| 526 | { | ||
| 527 | Log.d (TAG, "onWindowFocusChanged: " + this + ", is now focused: " | ||
| 528 | + isFocused); | ||
| 529 | |||
| 530 | if (isFocused) | ||
| 531 | currentActivity = this; | ||
| 532 | else if (currentActivity == this) | ||
| 533 | currentActivity = null; | ||
| 534 | |||
| 535 | super.onWindowFocusChanged (isFocused); | ||
| 536 | } | ||
| 537 | |||
| 538 | @Override | ||
| 539 | public void | ||
| 540 | onPause () | ||
| 541 | { | ||
| 542 | Log.d (TAG, "onPause: " + this); | ||
| 543 | |||
| 544 | /* XXX: clear currentActivity here as well; I don't know whether | ||
| 545 | or not onWindowFocusChanged is always called prior to this. */ | ||
| 546 | |||
| 547 | if (currentActivity == this) | ||
| 548 | currentActivity = null; | ||
| 549 | |||
| 550 | super.onPause (); | ||
| 551 | } | ||
| 552 | } | ||
diff --git a/java/org/gnu/emacs/EmacsPixmap.java b/java/org/gnu/emacs/EmacsPixmap.java new file mode 100644 index 00000000000..eb011bc5e65 --- /dev/null +++ b/java/org/gnu/emacs/EmacsPixmap.java | |||
| @@ -0,0 +1,192 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.lang.IllegalArgumentException; | ||
| 23 | |||
| 24 | import android.graphics.Bitmap; | ||
| 25 | import android.graphics.Canvas; | ||
| 26 | import android.graphics.Rect; | ||
| 27 | |||
| 28 | import android.os.Build; | ||
| 29 | |||
| 30 | /* Drawable backed by bitmap. */ | ||
| 31 | |||
| 32 | public final class EmacsPixmap extends EmacsHandleObject | ||
| 33 | implements EmacsDrawable | ||
| 34 | { | ||
| 35 | /* The depth of the bitmap. This is not actually used, just defined | ||
| 36 | in order to be consistent with X. */ | ||
| 37 | public int depth, width, height; | ||
| 38 | |||
| 39 | /* The bitmap itself. */ | ||
| 40 | public Bitmap bitmap; | ||
| 41 | |||
| 42 | /* The canvas used to draw to BITMAP. */ | ||
| 43 | public Canvas canvas; | ||
| 44 | |||
| 45 | /* Whether or not GC should be explicitly triggered upon | ||
| 46 | release. */ | ||
| 47 | private boolean needCollect; | ||
| 48 | |||
| 49 | /* ID used to determine whether or not the GC clip rects | ||
| 50 | changed. */ | ||
| 51 | private long gcClipRectID; | ||
| 52 | |||
| 53 | public | ||
| 54 | EmacsPixmap (short handle, int colors[], int width, | ||
| 55 | int height, int depth) | ||
| 56 | { | ||
| 57 | super (handle); | ||
| 58 | |||
| 59 | if (depth != 1 && depth != 24) | ||
| 60 | throw new IllegalArgumentException ("Invalid depth specified" | ||
| 61 | + " for pixmap: " + depth); | ||
| 62 | |||
| 63 | switch (depth) | ||
| 64 | { | ||
| 65 | case 1: | ||
| 66 | bitmap = Bitmap.createBitmap (colors, width, height, | ||
| 67 | Bitmap.Config.ALPHA_8); | ||
| 68 | break; | ||
| 69 | |||
| 70 | case 24: | ||
| 71 | bitmap = Bitmap.createBitmap (colors, width, height, | ||
| 72 | Bitmap.Config.ARGB_8888); | ||
| 73 | bitmap.setHasAlpha (false); | ||
| 74 | break; | ||
| 75 | } | ||
| 76 | |||
| 77 | this.width = width; | ||
| 78 | this.height = height; | ||
| 79 | this.depth = depth; | ||
| 80 | } | ||
| 81 | |||
| 82 | public | ||
| 83 | EmacsPixmap (short handle, int width, int height, int depth) | ||
| 84 | { | ||
| 85 | super (handle); | ||
| 86 | |||
| 87 | if (depth != 1 && depth != 24) | ||
| 88 | throw new IllegalArgumentException ("Invalid depth specified" | ||
| 89 | + " for pixmap: " + depth); | ||
| 90 | |||
| 91 | switch (depth) | ||
| 92 | { | ||
| 93 | case 1: | ||
| 94 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) | ||
| 95 | bitmap = Bitmap.createBitmap (width, height, | ||
| 96 | Bitmap.Config.ALPHA_8, | ||
| 97 | false); | ||
| 98 | else | ||
| 99 | bitmap = Bitmap.createBitmap (width, height, | ||
| 100 | Bitmap.Config.ALPHA_8); | ||
| 101 | break; | ||
| 102 | |||
| 103 | case 24: | ||
| 104 | |||
| 105 | /* Emacs doesn't just use the first kind of `createBitmap' | ||
| 106 | because the latter allows specifying that the pixmap is | ||
| 107 | always opaque, which really increases efficiency. */ | ||
| 108 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) | ||
| 109 | bitmap = Bitmap.createBitmap (width, height, | ||
| 110 | Bitmap.Config.ARGB_8888); | ||
| 111 | else | ||
| 112 | bitmap = Bitmap.createBitmap (width, height, | ||
| 113 | Bitmap.Config.ARGB_8888, | ||
| 114 | false); | ||
| 115 | break; | ||
| 116 | } | ||
| 117 | |||
| 118 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.HONEYCOMB_MR1) | ||
| 119 | /* On these old versions of Android, Bitmap.recycle frees bitmap | ||
| 120 | contents immediately. */ | ||
| 121 | needCollect = false; | ||
| 122 | else if (Build.VERSION.SDK_INT < Build.VERSION_CODES.KITKAT) | ||
| 123 | needCollect = (bitmap.getByteCount () | ||
| 124 | >= 1024 * 512); | ||
| 125 | else | ||
| 126 | needCollect = (bitmap.getAllocationByteCount () | ||
| 127 | >= 1024 * 512); | ||
| 128 | |||
| 129 | bitmap.eraseColor (0xff000000); | ||
| 130 | |||
| 131 | this.width = width; | ||
| 132 | this.height = height; | ||
| 133 | this.depth = depth; | ||
| 134 | } | ||
| 135 | |||
| 136 | @Override | ||
| 137 | public Canvas | ||
| 138 | lockCanvas (EmacsGC gc) | ||
| 139 | { | ||
| 140 | int i; | ||
| 141 | |||
| 142 | if (canvas == null) | ||
| 143 | { | ||
| 144 | canvas = new Canvas (bitmap); | ||
| 145 | canvas.save (); | ||
| 146 | } | ||
| 147 | |||
| 148 | /* Now see if clipping has to be redone. */ | ||
| 149 | if (gc.clipRectID == gcClipRectID) | ||
| 150 | return canvas; | ||
| 151 | |||
| 152 | /* It does have to be redone. Reapply gc.real_clip_rects. */ | ||
| 153 | canvas.restore (); | ||
| 154 | canvas.save (); | ||
| 155 | |||
| 156 | if (gc.real_clip_rects != null) | ||
| 157 | { | ||
| 158 | for (i = 0; i < gc.real_clip_rects.length; ++i) | ||
| 159 | canvas.clipRect (gc.real_clip_rects[i]); | ||
| 160 | } | ||
| 161 | |||
| 162 | /* Save the clip rect ID again. */ | ||
| 163 | gcClipRectID = gc.clipRectID; | ||
| 164 | return canvas; | ||
| 165 | } | ||
| 166 | |||
| 167 | @Override | ||
| 168 | public void | ||
| 169 | damageRect (Rect damageRect) | ||
| 170 | { | ||
| 171 | |||
| 172 | } | ||
| 173 | |||
| 174 | @Override | ||
| 175 | public Bitmap | ||
| 176 | getBitmap () | ||
| 177 | { | ||
| 178 | return bitmap; | ||
| 179 | } | ||
| 180 | |||
| 181 | @Override | ||
| 182 | public void | ||
| 183 | destroyHandle () | ||
| 184 | { | ||
| 185 | bitmap.recycle (); | ||
| 186 | bitmap = null; | ||
| 187 | |||
| 188 | /* Collect the bitmap storage if the bitmap is big. */ | ||
| 189 | if (needCollect) | ||
| 190 | Runtime.getRuntime ().gc (); | ||
| 191 | } | ||
| 192 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsPreferencesActivity.java b/java/org/gnu/emacs/EmacsPreferencesActivity.java new file mode 100644 index 00000000000..7e67cc3679b --- /dev/null +++ b/java/org/gnu/emacs/EmacsPreferencesActivity.java | |||
| @@ -0,0 +1,168 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.io.File; | ||
| 23 | |||
| 24 | import android.app.Activity; | ||
| 25 | |||
| 26 | import android.content.Intent; | ||
| 27 | |||
| 28 | import android.os.Bundle; | ||
| 29 | import android.os.Build; | ||
| 30 | |||
| 31 | import android.widget.Toast; | ||
| 32 | |||
| 33 | import android.preference.*; | ||
| 34 | |||
| 35 | /* This module provides a ``preferences'' display for Emacs. It is | ||
| 36 | supposed to be launched from inside the Settings application to | ||
| 37 | perform various actions, such as starting Emacs with the ``-Q'' | ||
| 38 | option, which would not be possible otherwise, as there is no | ||
| 39 | command line on Android. | ||
| 40 | |||
| 41 | Android provides a preferences activity, but it is deprecated. | ||
| 42 | Unfortunately, there is no alternative that looks the same way. */ | ||
| 43 | |||
| 44 | @SuppressWarnings ("deprecation") | ||
| 45 | public class EmacsPreferencesActivity extends PreferenceActivity | ||
| 46 | { | ||
| 47 | /* Restart Emacs with -Q. Call EmacsThread.exit to kill Emacs now, | ||
| 48 | and tell the system to start EmacsActivity with some parameters | ||
| 49 | later. */ | ||
| 50 | |||
| 51 | private void | ||
| 52 | startEmacsQ () | ||
| 53 | { | ||
| 54 | Intent intent; | ||
| 55 | |||
| 56 | intent = new Intent (this, EmacsActivity.class); | ||
| 57 | intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK | ||
| 58 | | Intent.FLAG_ACTIVITY_CLEAR_TASK); | ||
| 59 | intent.putExtra ("org.gnu.emacs.STARTUP_ARGUMENT", "--quick"); | ||
| 60 | startActivity (intent); | ||
| 61 | System.exit (0); | ||
| 62 | } | ||
| 63 | |||
| 64 | /* Restart Emacs with `--debug-init'. Call EmacsThread.exit to kill | ||
| 65 | Emacs now, and tell the system to EmacsActivity with some | ||
| 66 | parameters later. */ | ||
| 67 | |||
| 68 | private void | ||
| 69 | startEmacsDebugInit () | ||
| 70 | { | ||
| 71 | Intent intent; | ||
| 72 | |||
| 73 | intent = new Intent (this, EmacsActivity.class); | ||
| 74 | intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK | ||
| 75 | | Intent.FLAG_ACTIVITY_CLEAR_TASK); | ||
| 76 | intent.putExtra ("org.gnu.emacs.STARTUP_ARGUMENT", "--debug-init"); | ||
| 77 | startActivity (intent); | ||
| 78 | System.exit (0); | ||
| 79 | } | ||
| 80 | |||
| 81 | /* Erase Emacs's dump file. */ | ||
| 82 | |||
| 83 | private void | ||
| 84 | eraseDumpFile () | ||
| 85 | { | ||
| 86 | String wantedDumpFile; | ||
| 87 | File file; | ||
| 88 | Toast toast; | ||
| 89 | |||
| 90 | wantedDumpFile = ("emacs-" + EmacsNative.getFingerprint () | ||
| 91 | + ".pdmp"); | ||
| 92 | file = new File (getFilesDir (), wantedDumpFile); | ||
| 93 | |||
| 94 | if (file.exists ()) | ||
| 95 | file.delete (); | ||
| 96 | |||
| 97 | /* Make sure to clear EmacsApplication.dumpFileName, or | ||
| 98 | starting Emacs without restarting this program will | ||
| 99 | make Emacs try to load a nonexistent dump file. */ | ||
| 100 | EmacsApplication.dumpFileName = null; | ||
| 101 | |||
| 102 | /* Display a message stating that the dump file has been | ||
| 103 | erased. */ | ||
| 104 | toast = Toast.makeText (this, "Dump file removed", | ||
| 105 | Toast.LENGTH_SHORT); | ||
| 106 | toast.show (); | ||
| 107 | } | ||
| 108 | |||
| 109 | @Override | ||
| 110 | public final void | ||
| 111 | onCreate (Bundle savedInstanceState) | ||
| 112 | { | ||
| 113 | Preference tem; | ||
| 114 | Preference.OnPreferenceClickListener listener; | ||
| 115 | |||
| 116 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) | ||
| 117 | setTheme (android.R.style.Theme_DeviceDefault_Settings); | ||
| 118 | else if (Build.VERSION.SDK_INT | ||
| 119 | >= Build.VERSION_CODES.ICE_CREAM_SANDWICH) | ||
| 120 | setTheme (android.R.style.Theme_DeviceDefault); | ||
| 121 | |||
| 122 | /* This must come before using any preference APIs. */ | ||
| 123 | super.onCreate (savedInstanceState); | ||
| 124 | |||
| 125 | /* Add preferences from the XML file where they are defined. */ | ||
| 126 | addPreferencesFromResource (R.xml.preferences); | ||
| 127 | |||
| 128 | /* Now, set up on click handlers for each of the preferences | ||
| 129 | items. */ | ||
| 130 | |||
| 131 | tem = findPreference ("start_quick"); | ||
| 132 | listener = new Preference.OnPreferenceClickListener () { | ||
| 133 | @Override | ||
| 134 | public boolean | ||
| 135 | onPreferenceClick (Preference preference) | ||
| 136 | { | ||
| 137 | startEmacsQ (); | ||
| 138 | return true; | ||
| 139 | } | ||
| 140 | }; | ||
| 141 | |||
| 142 | tem.setOnPreferenceClickListener (listener); | ||
| 143 | tem = findPreference ("start_debug_init"); | ||
| 144 | listener = new Preference.OnPreferenceClickListener () { | ||
| 145 | @Override | ||
| 146 | public boolean | ||
| 147 | onPreferenceClick (Preference preference) | ||
| 148 | { | ||
| 149 | startEmacsDebugInit (); | ||
| 150 | return true; | ||
| 151 | } | ||
| 152 | }; | ||
| 153 | |||
| 154 | tem.setOnPreferenceClickListener (listener); | ||
| 155 | tem = findPreference ("erase_dump"); | ||
| 156 | listener = new Preference.OnPreferenceClickListener () { | ||
| 157 | @Override | ||
| 158 | public boolean | ||
| 159 | onPreferenceClick (Preference preference) | ||
| 160 | { | ||
| 161 | eraseDumpFile (); | ||
| 162 | return true; | ||
| 163 | } | ||
| 164 | }; | ||
| 165 | |||
| 166 | tem.setOnPreferenceClickListener (listener); | ||
| 167 | } | ||
| 168 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java new file mode 100644 index 00000000000..3ae3c0839ce --- /dev/null +++ b/java/org/gnu/emacs/EmacsSafThread.java | |||
| @@ -0,0 +1,1687 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.util.Collection; | ||
| 23 | import java.util.HashMap; | ||
| 24 | import java.util.Iterator; | ||
| 25 | |||
| 26 | import java.io.FileNotFoundException; | ||
| 27 | import java.io.IOException; | ||
| 28 | |||
| 29 | import android.content.ContentResolver; | ||
| 30 | import android.database.Cursor; | ||
| 31 | import android.net.Uri; | ||
| 32 | |||
| 33 | import android.os.Build; | ||
| 34 | import android.os.CancellationSignal; | ||
| 35 | import android.os.Handler; | ||
| 36 | import android.os.HandlerThread; | ||
| 37 | import android.os.OperationCanceledException; | ||
| 38 | import android.os.ParcelFileDescriptor; | ||
| 39 | import android.os.SystemClock; | ||
| 40 | |||
| 41 | import android.util.Log; | ||
| 42 | |||
| 43 | import android.provider.DocumentsContract; | ||
| 44 | import android.provider.DocumentsContract.Document; | ||
| 45 | |||
| 46 | |||
| 47 | |||
| 48 | /* Emacs runs long-running SAF operations on a second thread running | ||
| 49 | its own handler. These operations include opening files and | ||
| 50 | maintaining the path to document ID cache. | ||
| 51 | |||
| 52 | Because Emacs paths are based on file display names, while Android | ||
| 53 | document identifiers have no discernible hierarchy of their own, | ||
| 54 | each file name lookup must carry out a repeated search for | ||
| 55 | directory documents with the names of all of the file name's | ||
| 56 | constituent components, where each iteration searches within the | ||
| 57 | directory document identified by the previous iteration. | ||
| 58 | |||
| 59 | A time limited cache tying components to document IDs is maintained | ||
| 60 | in order to speed up consecutive searches for file names sharing | ||
| 61 | the same components. Since listening for changes to each document | ||
| 62 | in the cache is prohibitively expensive, Emacs instead elects to | ||
| 63 | periodically remove entries that are older than a predetermined | ||
| 64 | amount of a time. | ||
| 65 | |||
| 66 | The cache is split into two levels: the first caches the | ||
| 67 | relationships between display names and document IDs, while the | ||
| 68 | second caches individual document IDs and their contents (children, | ||
| 69 | type, etc.) | ||
| 70 | |||
| 71 | Long-running operations are also run on this thread for another | ||
| 72 | reason: Android uses special cancellation objects to terminate | ||
| 73 | ongoing IPC operations. However, the functions that perform these | ||
| 74 | operations block instead of providing mechanisms for the caller to | ||
| 75 | wait for their completion while also reading async input, as a | ||
| 76 | consequence of which the calling thread is unable to signal the | ||
| 77 | cancellation objects that it provides. Performing the blocking | ||
| 78 | operations in this auxiliary thread enables the main thread to wait | ||
| 79 | for completion itself, signaling the cancellation objects when it | ||
| 80 | deems necessary. */ | ||
| 81 | |||
| 82 | |||
| 83 | |||
| 84 | public final class EmacsSafThread extends HandlerThread | ||
| 85 | { | ||
| 86 | private static final String TAG = "EmacsSafThread"; | ||
| 87 | |||
| 88 | /* The content resolver used by this thread. */ | ||
| 89 | private final ContentResolver resolver; | ||
| 90 | |||
| 91 | /* Map between tree URIs and the cache entry representing its | ||
| 92 | toplevel directory. */ | ||
| 93 | private final HashMap<Uri, CacheToplevel> cacheToplevels; | ||
| 94 | |||
| 95 | /* Handler for this thread's main loop. */ | ||
| 96 | private Handler handler; | ||
| 97 | |||
| 98 | /* File access mode constants. See `man 7 inode'. */ | ||
| 99 | public static final int S_IRUSR = 0000400; | ||
| 100 | public static final int S_IWUSR = 0000200; | ||
| 101 | public static final int S_IXUSR = 0000100; | ||
| 102 | public static final int S_IFCHR = 0020000; | ||
| 103 | public static final int S_IFDIR = 0040000; | ||
| 104 | public static final int S_IFREG = 0100000; | ||
| 105 | |||
| 106 | /* Number of seconds in between each attempt to prune the storage | ||
| 107 | cache. */ | ||
| 108 | public static final int CACHE_PRUNE_TIME = 10; | ||
| 109 | |||
| 110 | /* Number of seconds after which an entry in the cache is to be | ||
| 111 | considered invalid. */ | ||
| 112 | public static final int CACHE_INVALID_TIME = 10; | ||
| 113 | |||
| 114 | public | ||
| 115 | EmacsSafThread (ContentResolver resolver) | ||
| 116 | { | ||
| 117 | super ("Document provider access thread"); | ||
| 118 | this.resolver = resolver; | ||
| 119 | this.cacheToplevels = new HashMap<Uri, CacheToplevel> (); | ||
| 120 | } | ||
| 121 | |||
| 122 | |||
| 123 | |||
| 124 | @Override | ||
| 125 | public void | ||
| 126 | start () | ||
| 127 | { | ||
| 128 | super.start (); | ||
| 129 | |||
| 130 | /* Set up the handler after the thread starts. */ | ||
| 131 | handler = new Handler (getLooper ()); | ||
| 132 | |||
| 133 | /* And start periodically pruning the cache. */ | ||
| 134 | postPruneMessage (); | ||
| 135 | } | ||
| 136 | |||
| 137 | |||
| 138 | private static final class CacheToplevel | ||
| 139 | { | ||
| 140 | /* Map between document names and children. */ | ||
| 141 | HashMap<String, DocIdEntry> children; | ||
| 142 | |||
| 143 | /* Map between document names and file status. */ | ||
| 144 | HashMap<String, StatCacheEntry> statCache; | ||
| 145 | |||
| 146 | /* Map between document IDs and cache items. */ | ||
| 147 | HashMap<String, CacheEntry> idCache; | ||
| 148 | }; | ||
| 149 | |||
| 150 | private static final class StatCacheEntry | ||
| 151 | { | ||
| 152 | /* The time at which this cache entry was created. */ | ||
| 153 | long time; | ||
| 154 | |||
| 155 | /* Flags, size, and modification time of this file. */ | ||
| 156 | long flags, size, mtime; | ||
| 157 | |||
| 158 | /* Whether or not this file is a directory. */ | ||
| 159 | boolean isDirectory; | ||
| 160 | |||
| 161 | public | ||
| 162 | StatCacheEntry () | ||
| 163 | { | ||
| 164 | time = SystemClock.uptimeMillis (); | ||
| 165 | } | ||
| 166 | |||
| 167 | public boolean | ||
| 168 | isValid () | ||
| 169 | { | ||
| 170 | return ((SystemClock.uptimeMillis () - time) | ||
| 171 | < CACHE_INVALID_TIME * 1000); | ||
| 172 | } | ||
| 173 | }; | ||
| 174 | |||
| 175 | private static final class DocIdEntry | ||
| 176 | { | ||
| 177 | /* The document ID. */ | ||
| 178 | String documentId; | ||
| 179 | |||
| 180 | /* The time this entry was created. */ | ||
| 181 | long time; | ||
| 182 | |||
| 183 | public | ||
| 184 | DocIdEntry () | ||
| 185 | { | ||
| 186 | time = SystemClock.uptimeMillis (); | ||
| 187 | } | ||
| 188 | |||
| 189 | /* Return a cache entry comprised of the state of the file | ||
| 190 | identified by `documentId'. TREE is the URI of the tree | ||
| 191 | containing this entry, and TOPLEVEL is the toplevel | ||
| 192 | representing it. SIGNAL is a cancellation signal. | ||
| 193 | |||
| 194 | RESOLVER is the content provider used to retrieve file | ||
| 195 | information. | ||
| 196 | |||
| 197 | Value is NULL if the file cannot be found. */ | ||
| 198 | |||
| 199 | public CacheEntry | ||
| 200 | getCacheEntry (ContentResolver resolver, Uri tree, | ||
| 201 | CacheToplevel toplevel, | ||
| 202 | CancellationSignal signal) | ||
| 203 | { | ||
| 204 | Uri uri; | ||
| 205 | String[] projection; | ||
| 206 | String type; | ||
| 207 | Cursor cursor; | ||
| 208 | int column; | ||
| 209 | CacheEntry entry; | ||
| 210 | |||
| 211 | /* Create a document URI representing DOCUMENTID within URI's | ||
| 212 | authority. */ | ||
| 213 | |||
| 214 | uri = DocumentsContract.buildDocumentUriUsingTree (tree, | ||
| 215 | documentId); | ||
| 216 | projection = new String[] { | ||
| 217 | Document.COLUMN_MIME_TYPE, | ||
| 218 | }; | ||
| 219 | |||
| 220 | cursor = null; | ||
| 221 | |||
| 222 | try | ||
| 223 | { | ||
| 224 | cursor = resolver.query (uri, projection, null, | ||
| 225 | null, null, signal); | ||
| 226 | |||
| 227 | if (!cursor.moveToFirst ()) | ||
| 228 | return null; | ||
| 229 | |||
| 230 | column = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 231 | |||
| 232 | if (column < 0) | ||
| 233 | return null; | ||
| 234 | |||
| 235 | type = cursor.getString (column); | ||
| 236 | |||
| 237 | if (type == null) | ||
| 238 | return null; | ||
| 239 | |||
| 240 | entry = new CacheEntry (); | ||
| 241 | entry.type = type; | ||
| 242 | toplevel.idCache.put (documentId, entry); | ||
| 243 | return entry; | ||
| 244 | } | ||
| 245 | catch (OperationCanceledException e) | ||
| 246 | { | ||
| 247 | throw e; | ||
| 248 | } | ||
| 249 | catch (Throwable e) | ||
| 250 | { | ||
| 251 | return null; | ||
| 252 | } | ||
| 253 | finally | ||
| 254 | { | ||
| 255 | if (cursor != null) | ||
| 256 | cursor.close (); | ||
| 257 | } | ||
| 258 | } | ||
| 259 | |||
| 260 | public boolean | ||
| 261 | isValid () | ||
| 262 | { | ||
| 263 | return ((SystemClock.uptimeMillis () - time) | ||
| 264 | < CACHE_INVALID_TIME * 1000); | ||
| 265 | } | ||
| 266 | }; | ||
| 267 | |||
| 268 | private static final class CacheEntry | ||
| 269 | { | ||
| 270 | /* The type of this document. */ | ||
| 271 | String type; | ||
| 272 | |||
| 273 | /* Map between document names and children. */ | ||
| 274 | HashMap<String, DocIdEntry> children; | ||
| 275 | |||
| 276 | /* The time this entry was created. */ | ||
| 277 | long time; | ||
| 278 | |||
| 279 | public | ||
| 280 | CacheEntry () | ||
| 281 | { | ||
| 282 | children = new HashMap<String, DocIdEntry> (); | ||
| 283 | time = SystemClock.uptimeMillis (); | ||
| 284 | } | ||
| 285 | |||
| 286 | public boolean | ||
| 287 | isValid () | ||
| 288 | { | ||
| 289 | return ((SystemClock.uptimeMillis () - time) | ||
| 290 | < CACHE_INVALID_TIME * 1000); | ||
| 291 | } | ||
| 292 | }; | ||
| 293 | |||
| 294 | /* Create or return a toplevel for the given tree URI. */ | ||
| 295 | |||
| 296 | private CacheToplevel | ||
| 297 | getCache (Uri uri) | ||
| 298 | { | ||
| 299 | CacheToplevel toplevel; | ||
| 300 | |||
| 301 | toplevel = cacheToplevels.get (uri); | ||
| 302 | |||
| 303 | if (toplevel != null) | ||
| 304 | return toplevel; | ||
| 305 | |||
| 306 | toplevel = new CacheToplevel (); | ||
| 307 | toplevel.children = new HashMap<String, DocIdEntry> (); | ||
| 308 | toplevel.statCache = new HashMap<String, StatCacheEntry> (); | ||
| 309 | toplevel.idCache = new HashMap<String, CacheEntry> (); | ||
| 310 | cacheToplevels.put (uri, toplevel); | ||
| 311 | return toplevel; | ||
| 312 | } | ||
| 313 | |||
| 314 | /* Remove each cache entry within COLLECTION older than | ||
| 315 | CACHE_INVALID_TIME. */ | ||
| 316 | |||
| 317 | private void | ||
| 318 | pruneCache1 (Collection<DocIdEntry> collection) | ||
| 319 | { | ||
| 320 | Iterator<DocIdEntry> iter; | ||
| 321 | DocIdEntry tem; | ||
| 322 | |||
| 323 | iter = collection.iterator (); | ||
| 324 | while (iter.hasNext ()) | ||
| 325 | { | ||
| 326 | /* Get the cache entry. */ | ||
| 327 | tem = iter.next (); | ||
| 328 | |||
| 329 | /* If it's not valid anymore, remove it. Iterating over a | ||
| 330 | collection whose contents are being removed is undefined | ||
| 331 | unless the removal is performed using the iterator's own | ||
| 332 | `remove' function, so tem.remove cannot be used here. */ | ||
| 333 | |||
| 334 | if (tem.isValid ()) | ||
| 335 | continue; | ||
| 336 | |||
| 337 | iter.remove (); | ||
| 338 | } | ||
| 339 | } | ||
| 340 | |||
| 341 | /* Remove every entry older than CACHE_INVALID_TIME from each | ||
| 342 | toplevel inside `cachedToplevels'. */ | ||
| 343 | |||
| 344 | private void | ||
| 345 | pruneCache () | ||
| 346 | { | ||
| 347 | Iterator<CacheEntry> iter; | ||
| 348 | Iterator<StatCacheEntry> statIter; | ||
| 349 | CacheEntry tem; | ||
| 350 | StatCacheEntry stat; | ||
| 351 | |||
| 352 | for (CacheToplevel toplevel : cacheToplevels.values ()) | ||
| 353 | { | ||
| 354 | /* First, clean up expired cache entries. */ | ||
| 355 | iter = toplevel.idCache.values ().iterator (); | ||
| 356 | |||
| 357 | while (iter.hasNext ()) | ||
| 358 | { | ||
| 359 | /* Get the cache entry. */ | ||
| 360 | tem = iter.next (); | ||
| 361 | |||
| 362 | /* If it's not valid anymore, remove it. Iterating over a | ||
| 363 | collection whose contents are being removed is | ||
| 364 | undefined unless the removal is performed using the | ||
| 365 | iterator's own `remove' function, so tem.remove cannot | ||
| 366 | be used here. */ | ||
| 367 | |||
| 368 | if (tem.isValid ()) | ||
| 369 | { | ||
| 370 | /* Otherwise, clean up expired items in its document | ||
| 371 | ID cache. */ | ||
| 372 | pruneCache1 (tem.children.values ()); | ||
| 373 | continue; | ||
| 374 | } | ||
| 375 | |||
| 376 | iter.remove (); | ||
| 377 | } | ||
| 378 | |||
| 379 | statIter = toplevel.statCache.values ().iterator (); | ||
| 380 | |||
| 381 | while (statIter.hasNext ()) | ||
| 382 | { | ||
| 383 | /* Get the cache entry. */ | ||
| 384 | stat = statIter.next (); | ||
| 385 | |||
| 386 | /* If it's not valid anymore, remove it. Iterating over a | ||
| 387 | collection whose contents are being removed is | ||
| 388 | undefined unless the removal is performed using the | ||
| 389 | iterator's own `remove' function, so tem.remove cannot | ||
| 390 | be used here. */ | ||
| 391 | |||
| 392 | if (stat.isValid ()) | ||
| 393 | continue; | ||
| 394 | |||
| 395 | statIter.remove (); | ||
| 396 | } | ||
| 397 | } | ||
| 398 | |||
| 399 | postPruneMessage (); | ||
| 400 | } | ||
| 401 | |||
| 402 | /* Cache file information within TOPLEVEL, under the list of | ||
| 403 | children CHILDREN. | ||
| 404 | |||
| 405 | NAME, ID, and TYPE should respectively be the display name of the | ||
| 406 | document within its parent document (the CacheEntry whose | ||
| 407 | `children' field is CHILDREN), its document ID, and its MIME | ||
| 408 | type. | ||
| 409 | |||
| 410 | If ID_ENTRY_EXISTS, don't create a new document ID entry within | ||
| 411 | CHILDREN indexed by NAME. | ||
| 412 | |||
| 413 | Value is the cache entry saved for the document ID. */ | ||
| 414 | |||
| 415 | private CacheEntry | ||
| 416 | cacheChild (CacheToplevel toplevel, | ||
| 417 | HashMap<String, DocIdEntry> children, | ||
| 418 | String name, String id, String type, | ||
| 419 | boolean id_entry_exists) | ||
| 420 | { | ||
| 421 | DocIdEntry idEntry; | ||
| 422 | CacheEntry cacheEntry; | ||
| 423 | |||
| 424 | if (!id_entry_exists) | ||
| 425 | { | ||
| 426 | idEntry = new DocIdEntry (); | ||
| 427 | idEntry.documentId = id; | ||
| 428 | children.put (name, idEntry); | ||
| 429 | } | ||
| 430 | |||
| 431 | cacheEntry = new CacheEntry (); | ||
| 432 | cacheEntry.type = type; | ||
| 433 | toplevel.idCache.put (id, cacheEntry); | ||
| 434 | return cacheEntry; | ||
| 435 | } | ||
| 436 | |||
| 437 | /* Cache file status for DOCUMENTID within TOPLEVEL. Value is the | ||
| 438 | new cache entry. CURSOR is the cursor from where to retrieve the | ||
| 439 | file status, in the form of the columns COLUMN_FLAGS, | ||
| 440 | COLUMN_SIZE, COLUMN_MIME_TYPE and COLUMN_LAST_MODIFIED. */ | ||
| 441 | |||
| 442 | private StatCacheEntry | ||
| 443 | cacheFileStatus (String documentId, CacheToplevel toplevel, | ||
| 444 | Cursor cursor) | ||
| 445 | { | ||
| 446 | StatCacheEntry entry; | ||
| 447 | int flagsIndex, columnIndex, typeIndex; | ||
| 448 | int sizeIndex, mtimeIndex; | ||
| 449 | String type; | ||
| 450 | |||
| 451 | /* Obtain the indices for columns wanted from this cursor. */ | ||
| 452 | flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS); | ||
| 453 | sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE); | ||
| 454 | typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 455 | mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED); | ||
| 456 | |||
| 457 | /* COLUMN_LAST_MODIFIED is allowed to be absent in a | ||
| 458 | conforming documents provider. */ | ||
| 459 | if (flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0) | ||
| 460 | return null; | ||
| 461 | |||
| 462 | /* Get the file status from CURSOR. */ | ||
| 463 | entry = new StatCacheEntry (); | ||
| 464 | entry.flags = cursor.getInt (flagsIndex); | ||
| 465 | type = cursor.getString (typeIndex); | ||
| 466 | |||
| 467 | if (type == null) | ||
| 468 | return null; | ||
| 469 | |||
| 470 | entry.isDirectory = type.equals (Document.MIME_TYPE_DIR); | ||
| 471 | |||
| 472 | if (cursor.isNull (sizeIndex)) | ||
| 473 | /* The size is unknown. */ | ||
| 474 | entry.size = -1; | ||
| 475 | else | ||
| 476 | entry.size = cursor.getLong (sizeIndex); | ||
| 477 | |||
| 478 | /* mtimeIndex is potentially unset, since document providers | ||
| 479 | aren't obligated to provide modification times. */ | ||
| 480 | |||
| 481 | if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex)) | ||
| 482 | entry.mtime = cursor.getLong (mtimeIndex); | ||
| 483 | |||
| 484 | /* Finally, add this entry to the cache and return. */ | ||
| 485 | toplevel.statCache.put (documentId, entry); | ||
| 486 | return entry; | ||
| 487 | } | ||
| 488 | |||
| 489 | /* Cache the type and as many of the children of the directory | ||
| 490 | designated by DOCUMENTID as possible into TOPLEVEL. | ||
| 491 | |||
| 492 | CURSOR should be a cursor representing an open directory stream, | ||
| 493 | with its projection consisting of at least the display name, | ||
| 494 | document ID and MIME type columns. | ||
| 495 | |||
| 496 | Rewind the position of CURSOR to before its first element after | ||
| 497 | completion. */ | ||
| 498 | |||
| 499 | private void | ||
| 500 | cacheDirectoryFromCursor (CacheToplevel toplevel, String documentId, | ||
| 501 | Cursor cursor) | ||
| 502 | { | ||
| 503 | CacheEntry entry, constitutent; | ||
| 504 | int nameColumn, idColumn, typeColumn; | ||
| 505 | String id, name, type; | ||
| 506 | DocIdEntry idEntry; | ||
| 507 | |||
| 508 | /* Find the numbers of the columns wanted. */ | ||
| 509 | |||
| 510 | nameColumn | ||
| 511 | = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); | ||
| 512 | idColumn | ||
| 513 | = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); | ||
| 514 | typeColumn | ||
| 515 | = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 516 | |||
| 517 | if (nameColumn < 0 || idColumn < 0 || typeColumn < 0) | ||
| 518 | return; | ||
| 519 | |||
| 520 | entry = new CacheEntry (); | ||
| 521 | |||
| 522 | /* We know this is a directory already. */ | ||
| 523 | entry.type = Document.MIME_TYPE_DIR; | ||
| 524 | toplevel.idCache.put (documentId, entry); | ||
| 525 | |||
| 526 | /* Now, try to cache each of its constituents. */ | ||
| 527 | |||
| 528 | while (cursor.moveToNext ()) | ||
| 529 | { | ||
| 530 | try | ||
| 531 | { | ||
| 532 | name = cursor.getString (nameColumn); | ||
| 533 | id = cursor.getString (idColumn); | ||
| 534 | type = cursor.getString (typeColumn); | ||
| 535 | |||
| 536 | if (name == null || id == null || type == null) | ||
| 537 | continue; | ||
| 538 | |||
| 539 | /* First, add the name and ID to ENTRY's map of | ||
| 540 | children. */ | ||
| 541 | idEntry = new DocIdEntry (); | ||
| 542 | idEntry.documentId = id; | ||
| 543 | entry.children.put (id, idEntry); | ||
| 544 | |||
| 545 | /* Cache the file status for ID within TOPELVEL too; if a | ||
| 546 | directory listing is being requested, it's very likely | ||
| 547 | that a series of calls for file status will follow. */ | ||
| 548 | |||
| 549 | cacheFileStatus (id, toplevel, cursor); | ||
| 550 | |||
| 551 | /* If this constituent is a directory, don't cache any | ||
| 552 | information about it. It cannot be cached without | ||
| 553 | knowing its children. */ | ||
| 554 | |||
| 555 | if (type.equals (Document.MIME_TYPE_DIR)) | ||
| 556 | continue; | ||
| 557 | |||
| 558 | /* Otherwise, create a new cache entry comprised of its | ||
| 559 | type. */ | ||
| 560 | constitutent = new CacheEntry (); | ||
| 561 | constitutent.type = type; | ||
| 562 | toplevel.idCache.put (documentId, entry); | ||
| 563 | } | ||
| 564 | catch (Exception e) | ||
| 565 | { | ||
| 566 | e.printStackTrace (); | ||
| 567 | continue; | ||
| 568 | } | ||
| 569 | } | ||
| 570 | |||
| 571 | /* Rewind cursor back to the beginning. */ | ||
| 572 | cursor.moveToPosition (-1); | ||
| 573 | } | ||
| 574 | |||
| 575 | /* Post a message to run `pruneCache' every CACHE_PRUNE_TIME | ||
| 576 | seconds. */ | ||
| 577 | |||
| 578 | private void | ||
| 579 | postPruneMessage () | ||
| 580 | { | ||
| 581 | handler.postDelayed (new Runnable () { | ||
| 582 | @Override | ||
| 583 | public void | ||
| 584 | run () | ||
| 585 | { | ||
| 586 | pruneCache (); | ||
| 587 | } | ||
| 588 | }, CACHE_PRUNE_TIME * 1000); | ||
| 589 | } | ||
| 590 | |||
| 591 | /* Invalidate the cache entry denoted by DOCUMENT_ID, within the | ||
| 592 | document tree URI. | ||
| 593 | Call this after deleting a document or directory. | ||
| 594 | |||
| 595 | At the same time, remove the final component within the file name | ||
| 596 | CACHENAME from the cache if it exists. */ | ||
| 597 | |||
| 598 | public void | ||
| 599 | postInvalidateCache (final Uri uri, final String documentId, | ||
| 600 | final String cacheName) | ||
| 601 | { | ||
| 602 | handler.post (new Runnable () { | ||
| 603 | @Override | ||
| 604 | public void | ||
| 605 | run () | ||
| 606 | { | ||
| 607 | CacheToplevel toplevel; | ||
| 608 | HashMap<String, DocIdEntry> children; | ||
| 609 | String[] components; | ||
| 610 | CacheEntry entry; | ||
| 611 | DocIdEntry idEntry; | ||
| 612 | |||
| 613 | toplevel = getCache (uri); | ||
| 614 | toplevel.idCache.remove (documentId); | ||
| 615 | toplevel.statCache.remove (documentId); | ||
| 616 | |||
| 617 | /* If the parent of CACHENAME is cached, remove it. */ | ||
| 618 | |||
| 619 | children = toplevel.children; | ||
| 620 | components = cacheName.split ("/"); | ||
| 621 | |||
| 622 | for (String component : components) | ||
| 623 | { | ||
| 624 | /* Java `split' removes trailing empty matches but not | ||
| 625 | leading or intermediary ones. */ | ||
| 626 | if (component.isEmpty ()) | ||
| 627 | continue; | ||
| 628 | |||
| 629 | if (component == components[components.length - 1]) | ||
| 630 | { | ||
| 631 | /* This is the last component, so remove it from | ||
| 632 | children. */ | ||
| 633 | children.remove (component); | ||
| 634 | return; | ||
| 635 | } | ||
| 636 | else | ||
| 637 | { | ||
| 638 | /* Search for this component within the last level | ||
| 639 | of the cache. */ | ||
| 640 | |||
| 641 | idEntry = children.get (component); | ||
| 642 | |||
| 643 | if (idEntry == null) | ||
| 644 | /* Not cached, so return. */ | ||
| 645 | return; | ||
| 646 | |||
| 647 | entry = toplevel.idCache.get (idEntry.documentId); | ||
| 648 | |||
| 649 | if (entry == null) | ||
| 650 | /* Not cached, so return. */ | ||
| 651 | return; | ||
| 652 | |||
| 653 | /* Locate the next component within this | ||
| 654 | directory. */ | ||
| 655 | children = entry.children; | ||
| 656 | } | ||
| 657 | } | ||
| 658 | } | ||
| 659 | }); | ||
| 660 | } | ||
| 661 | |||
| 662 | /* Invalidate the cache entry denoted by DOCUMENT_ID, within the | ||
| 663 | document tree URI. | ||
| 664 | Call this after deleting a document or directory. | ||
| 665 | |||
| 666 | At the same time, remove the child referring to DOCUMENTID from | ||
| 667 | within CACHENAME's cache entry if it exists. */ | ||
| 668 | |||
| 669 | public void | ||
| 670 | postInvalidateCacheDir (final Uri uri, final String documentId, | ||
| 671 | final String cacheName) | ||
| 672 | { | ||
| 673 | handler.post (new Runnable () { | ||
| 674 | @Override | ||
| 675 | public void | ||
| 676 | run () | ||
| 677 | { | ||
| 678 | CacheToplevel toplevel; | ||
| 679 | HashMap<String, DocIdEntry> children; | ||
| 680 | String[] components; | ||
| 681 | CacheEntry entry; | ||
| 682 | DocIdEntry idEntry; | ||
| 683 | Iterator<DocIdEntry> iter; | ||
| 684 | |||
| 685 | toplevel = getCache (uri); | ||
| 686 | toplevel.idCache.remove (documentId); | ||
| 687 | toplevel.statCache.remove (documentId); | ||
| 688 | |||
| 689 | /* Now remove DOCUMENTID from CACHENAME's cache entry, if | ||
| 690 | any. */ | ||
| 691 | |||
| 692 | children = toplevel.children; | ||
| 693 | components = cacheName.split ("/"); | ||
| 694 | |||
| 695 | for (String component : components) | ||
| 696 | { | ||
| 697 | /* Java `split' removes trailing empty matches but not | ||
| 698 | leading or intermediary ones. */ | ||
| 699 | if (component.isEmpty ()) | ||
| 700 | continue; | ||
| 701 | |||
| 702 | /* Search for this component within the last level | ||
| 703 | of the cache. */ | ||
| 704 | |||
| 705 | idEntry = children.get (component); | ||
| 706 | |||
| 707 | if (idEntry == null) | ||
| 708 | /* Not cached, so return. */ | ||
| 709 | return; | ||
| 710 | |||
| 711 | entry = toplevel.idCache.get (idEntry.documentId); | ||
| 712 | |||
| 713 | if (entry == null) | ||
| 714 | /* Not cached, so return. */ | ||
| 715 | return; | ||
| 716 | |||
| 717 | /* Locate the next component within this | ||
| 718 | directory. */ | ||
| 719 | children = entry.children; | ||
| 720 | } | ||
| 721 | |||
| 722 | iter = children.values ().iterator (); | ||
| 723 | while (iter.hasNext ()) | ||
| 724 | { | ||
| 725 | idEntry = iter.next (); | ||
| 726 | |||
| 727 | if (idEntry.documentId.equals (documentId)) | ||
| 728 | { | ||
| 729 | iter.remove (); | ||
| 730 | break; | ||
| 731 | } | ||
| 732 | } | ||
| 733 | } | ||
| 734 | }); | ||
| 735 | } | ||
| 736 | |||
| 737 | /* Invalidate the file status cache entry for DOCUMENTID within URI. | ||
| 738 | Call this when the contents of a file (i.e. the constituents of a | ||
| 739 | directory file) may have changed, but the document's display name | ||
| 740 | has not. */ | ||
| 741 | |||
| 742 | public void | ||
| 743 | postInvalidateStat (final Uri uri, final String documentId) | ||
| 744 | { | ||
| 745 | handler.post (new Runnable () { | ||
| 746 | @Override | ||
| 747 | public void | ||
| 748 | run () | ||
| 749 | { | ||
| 750 | CacheToplevel toplevel; | ||
| 751 | |||
| 752 | toplevel = getCache (uri); | ||
| 753 | toplevel.statCache.remove (documentId); | ||
| 754 | } | ||
| 755 | }); | ||
| 756 | } | ||
| 757 | |||
| 758 | |||
| 759 | |||
| 760 | /* ``Prototypes'' for nested functions that are run within the SAF | ||
| 761 | thread and accepts a cancellation signal. They differ in their | ||
| 762 | return types. */ | ||
| 763 | |||
| 764 | private abstract class SafIntFunction | ||
| 765 | { | ||
| 766 | /* The ``throws Throwable'' here is a Java idiosyncracy that tells | ||
| 767 | the compiler to allow arbitrary error objects to be signaled | ||
| 768 | from within this function. | ||
| 769 | |||
| 770 | Later, runIntFunction will try to re-throw any error object | ||
| 771 | generated by this function in the Emacs thread, using a trick | ||
| 772 | to avoid the compiler requirement to expressly declare that an | ||
| 773 | error (and which types of errors) will be signaled. */ | ||
| 774 | |||
| 775 | public abstract int runInt (CancellationSignal signal) | ||
| 776 | throws Throwable; | ||
| 777 | }; | ||
| 778 | |||
| 779 | private abstract class SafObjectFunction | ||
| 780 | { | ||
| 781 | /* The ``throws Throwable'' here is a Java idiosyncracy that tells | ||
| 782 | the compiler to allow arbitrary error objects to be signaled | ||
| 783 | from within this function. | ||
| 784 | |||
| 785 | Later, runObjectFunction will try to re-throw any error object | ||
| 786 | generated by this function in the Emacs thread, using a trick | ||
| 787 | to avoid the compiler requirement to expressly declare that an | ||
| 788 | error (and which types of errors) will be signaled. */ | ||
| 789 | |||
| 790 | public abstract Object runObject (CancellationSignal signal) | ||
| 791 | throws Throwable; | ||
| 792 | }; | ||
| 793 | |||
| 794 | |||
| 795 | |||
| 796 | /* Functions that run cancel-able queries. These functions are | ||
| 797 | internally run within the SAF thread. */ | ||
| 798 | |||
| 799 | /* Throw the specified EXCEPTION. The type template T is erased by | ||
| 800 | the compiler before the object is compiled, so the compiled code | ||
| 801 | simply throws EXCEPTION without the cast being verified. | ||
| 802 | |||
| 803 | T should be RuntimeException to obtain the desired effect of | ||
| 804 | throwing an exception without a compiler check. */ | ||
| 805 | |||
| 806 | @SuppressWarnings("unchecked") | ||
| 807 | private static <T extends Throwable> void | ||
| 808 | throwException (Throwable exception) | ||
| 809 | throws T | ||
| 810 | { | ||
| 811 | throw (T) exception; | ||
| 812 | } | ||
| 813 | |||
| 814 | /* Run the given function (or rather, its `runInt' field) within the | ||
| 815 | SAF thread, waiting for it to complete. | ||
| 816 | |||
| 817 | If async input arrives in the meantime and sets Vquit_flag, | ||
| 818 | signal the cancellation signal supplied to that function. | ||
| 819 | |||
| 820 | Rethrow any exception thrown from that function, and return its | ||
| 821 | value otherwise. */ | ||
| 822 | |||
| 823 | private int | ||
| 824 | runIntFunction (final SafIntFunction function) | ||
| 825 | { | ||
| 826 | final EmacsHolder<Object> result; | ||
| 827 | final CancellationSignal signal; | ||
| 828 | Throwable throwable; | ||
| 829 | |||
| 830 | result = new EmacsHolder<Object> (); | ||
| 831 | signal = new CancellationSignal (); | ||
| 832 | |||
| 833 | handler.post (new Runnable () { | ||
| 834 | @Override | ||
| 835 | public void | ||
| 836 | run () | ||
| 837 | { | ||
| 838 | try | ||
| 839 | { | ||
| 840 | result.thing | ||
| 841 | = Integer.valueOf (function.runInt (signal)); | ||
| 842 | } | ||
| 843 | catch (Throwable throwable) | ||
| 844 | { | ||
| 845 | result.thing = throwable; | ||
| 846 | } | ||
| 847 | |||
| 848 | EmacsNative.safPostRequest (); | ||
| 849 | } | ||
| 850 | }); | ||
| 851 | |||
| 852 | if (EmacsNative.safSyncAndReadInput () != 0) | ||
| 853 | { | ||
| 854 | signal.cancel (); | ||
| 855 | |||
| 856 | /* Now wait for the function to finish. Either the signal has | ||
| 857 | arrived after the query took place, in which case it will | ||
| 858 | finish normally, or an OperationCanceledException will be | ||
| 859 | thrown. */ | ||
| 860 | |||
| 861 | EmacsNative.safSync (); | ||
| 862 | } | ||
| 863 | |||
| 864 | if (result.thing instanceof Throwable) | ||
| 865 | { | ||
| 866 | throwable = (Throwable) result.thing; | ||
| 867 | EmacsSafThread.<RuntimeException>throwException (throwable); | ||
| 868 | } | ||
| 869 | |||
| 870 | return (Integer) result.thing; | ||
| 871 | } | ||
| 872 | |||
| 873 | /* Run the given function (or rather, its `runObject' field) within | ||
| 874 | the SAF thread, waiting for it to complete. | ||
| 875 | |||
| 876 | If async input arrives in the meantime and sets Vquit_flag, | ||
| 877 | signal the cancellation signal supplied to that function. | ||
| 878 | |||
| 879 | Rethrow any exception thrown from that function, and return its | ||
| 880 | value otherwise. */ | ||
| 881 | |||
| 882 | private Object | ||
| 883 | runObjectFunction (final SafObjectFunction function) | ||
| 884 | { | ||
| 885 | final EmacsHolder<Object> result; | ||
| 886 | final CancellationSignal signal; | ||
| 887 | Throwable throwable; | ||
| 888 | |||
| 889 | result = new EmacsHolder<Object> (); | ||
| 890 | signal = new CancellationSignal (); | ||
| 891 | |||
| 892 | handler.post (new Runnable () { | ||
| 893 | @Override | ||
| 894 | public void | ||
| 895 | run () | ||
| 896 | { | ||
| 897 | try | ||
| 898 | { | ||
| 899 | result.thing = function.runObject (signal); | ||
| 900 | } | ||
| 901 | catch (Throwable throwable) | ||
| 902 | { | ||
| 903 | result.thing = throwable; | ||
| 904 | } | ||
| 905 | |||
| 906 | EmacsNative.safPostRequest (); | ||
| 907 | } | ||
| 908 | }); | ||
| 909 | |||
| 910 | if (EmacsNative.safSyncAndReadInput () != 0) | ||
| 911 | { | ||
| 912 | signal.cancel (); | ||
| 913 | |||
| 914 | /* Now wait for the function to finish. Either the signal has | ||
| 915 | arrived after the query took place, in which case it will | ||
| 916 | finish normally, or an OperationCanceledException will be | ||
| 917 | thrown. */ | ||
| 918 | |||
| 919 | EmacsNative.safSync (); | ||
| 920 | } | ||
| 921 | |||
| 922 | if (result.thing instanceof Throwable) | ||
| 923 | { | ||
| 924 | throwable = (Throwable) result.thing; | ||
| 925 | EmacsSafThread.<RuntimeException>throwException (throwable); | ||
| 926 | } | ||
| 927 | |||
| 928 | return result.thing; | ||
| 929 | } | ||
| 930 | |||
| 931 | /* The crux of `documentIdFromName1', run within the SAF thread. | ||
| 932 | SIGNAL should be a cancellation signal run upon quitting. */ | ||
| 933 | |||
| 934 | private int | ||
| 935 | documentIdFromName1 (String tree_uri, String name, | ||
| 936 | String[] id_return, CancellationSignal signal) | ||
| 937 | { | ||
| 938 | Uri uri, treeUri; | ||
| 939 | String id, type, newId, newType; | ||
| 940 | String[] components, projection; | ||
| 941 | Cursor cursor; | ||
| 942 | int nameColumn, idColumn, typeColumn; | ||
| 943 | CacheToplevel toplevel; | ||
| 944 | DocIdEntry idEntry; | ||
| 945 | HashMap<String, DocIdEntry> children, next; | ||
| 946 | CacheEntry cache; | ||
| 947 | |||
| 948 | projection = new String[] { | ||
| 949 | Document.COLUMN_DISPLAY_NAME, | ||
| 950 | Document.COLUMN_DOCUMENT_ID, | ||
| 951 | Document.COLUMN_MIME_TYPE, | ||
| 952 | }; | ||
| 953 | |||
| 954 | /* Parse the URI identifying the tree first. */ | ||
| 955 | uri = Uri.parse (tree_uri); | ||
| 956 | |||
| 957 | /* Now, split NAME into its individual components. */ | ||
| 958 | components = name.split ("/"); | ||
| 959 | |||
| 960 | /* Set id and type to the value at the root of the tree. */ | ||
| 961 | type = id = null; | ||
| 962 | cursor = null; | ||
| 963 | |||
| 964 | /* Obtain the top level of this cache. */ | ||
| 965 | toplevel = getCache (uri); | ||
| 966 | |||
| 967 | /* Set the current map of children to this top level. */ | ||
| 968 | children = toplevel.children; | ||
| 969 | |||
| 970 | /* For each component... */ | ||
| 971 | |||
| 972 | try | ||
| 973 | { | ||
| 974 | for (String component : components) | ||
| 975 | { | ||
| 976 | /* Java split doesn't behave very much like strtok when it | ||
| 977 | comes to trailing and leading delimiters... */ | ||
| 978 | if (component.isEmpty ()) | ||
| 979 | continue; | ||
| 980 | |||
| 981 | /* Search for component within the currently cached list | ||
| 982 | of children. */ | ||
| 983 | |||
| 984 | idEntry = children.get (component); | ||
| 985 | |||
| 986 | if (idEntry != null) | ||
| 987 | { | ||
| 988 | /* The document ID is known. Now find the | ||
| 989 | corresponding document ID cache. */ | ||
| 990 | |||
| 991 | cache = toplevel.idCache.get (idEntry.documentId); | ||
| 992 | |||
| 993 | /* Fetch just the information for this document. */ | ||
| 994 | |||
| 995 | if (cache == null) | ||
| 996 | cache = idEntry.getCacheEntry (resolver, uri, toplevel, | ||
| 997 | signal); | ||
| 998 | |||
| 999 | if (cache == null) | ||
| 1000 | { | ||
| 1001 | /* File status matching idEntry could not be | ||
| 1002 | obtained. Treat this as if the file does not | ||
| 1003 | exist. */ | ||
| 1004 | |||
| 1005 | children.remove (component); | ||
| 1006 | |||
| 1007 | if (id == null) | ||
| 1008 | id = DocumentsContract.getTreeDocumentId (uri); | ||
| 1009 | |||
| 1010 | id_return[0] = id; | ||
| 1011 | |||
| 1012 | if ((type == null | ||
| 1013 | || type.equals (Document.MIME_TYPE_DIR)) | ||
| 1014 | /* ... and type and id currently represent the | ||
| 1015 | penultimate component. */ | ||
| 1016 | && component == components[components.length - 1]) | ||
| 1017 | return -2; | ||
| 1018 | |||
| 1019 | return -1; | ||
| 1020 | } | ||
| 1021 | |||
| 1022 | /* Otherwise, use the cached information. */ | ||
| 1023 | id = idEntry.documentId; | ||
| 1024 | type = cache.type; | ||
| 1025 | children = cache.children; | ||
| 1026 | continue; | ||
| 1027 | } | ||
| 1028 | |||
| 1029 | /* Create the tree URI for URI from ID if it exists, or | ||
| 1030 | the root otherwise. */ | ||
| 1031 | |||
| 1032 | if (id == null) | ||
| 1033 | id = DocumentsContract.getTreeDocumentId (uri); | ||
| 1034 | |||
| 1035 | treeUri | ||
| 1036 | = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id); | ||
| 1037 | |||
| 1038 | /* Look for a file in this directory by the name of | ||
| 1039 | component. */ | ||
| 1040 | |||
| 1041 | cursor = resolver.query (treeUri, projection, | ||
| 1042 | (Document.COLUMN_DISPLAY_NAME | ||
| 1043 | + " = ?"), | ||
| 1044 | new String[] { component, }, | ||
| 1045 | null, signal); | ||
| 1046 | |||
| 1047 | if (cursor == null) | ||
| 1048 | return -1; | ||
| 1049 | |||
| 1050 | /* Find the column numbers for each of the columns that | ||
| 1051 | are wanted. */ | ||
| 1052 | |||
| 1053 | nameColumn | ||
| 1054 | = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); | ||
| 1055 | idColumn | ||
| 1056 | = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID); | ||
| 1057 | typeColumn | ||
| 1058 | = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 1059 | |||
| 1060 | if (nameColumn < 0 || idColumn < 0 || typeColumn < 0) | ||
| 1061 | return -1; | ||
| 1062 | |||
| 1063 | next = null; | ||
| 1064 | |||
| 1065 | while (true) | ||
| 1066 | { | ||
| 1067 | /* Even though the query selects for a specific | ||
| 1068 | display name, some content providers nevertheless | ||
| 1069 | return every file within the directory. */ | ||
| 1070 | |||
| 1071 | if (!cursor.moveToNext ()) | ||
| 1072 | { | ||
| 1073 | /* If a component has been found, break out of the | ||
| 1074 | loop. */ | ||
| 1075 | |||
| 1076 | if (next != null) | ||
| 1077 | break; | ||
| 1078 | |||
| 1079 | /* If the last component considered is a | ||
| 1080 | directory... */ | ||
| 1081 | if ((type == null | ||
| 1082 | || type.equals (Document.MIME_TYPE_DIR)) | ||
| 1083 | /* ... and type and id currently represent the | ||
| 1084 | penultimate component. */ | ||
| 1085 | && component == components[components.length - 1]) | ||
| 1086 | { | ||
| 1087 | /* The cursor is empty. In this case, return | ||
| 1088 | -2 and the current document ID (belonging | ||
| 1089 | to the previous component) in | ||
| 1090 | ID_RETURN. */ | ||
| 1091 | |||
| 1092 | id_return[0] = id; | ||
| 1093 | |||
| 1094 | /* But return -1 on the off chance that id is | ||
| 1095 | null. */ | ||
| 1096 | |||
| 1097 | if (id == null) | ||
| 1098 | return -1; | ||
| 1099 | |||
| 1100 | return -2; | ||
| 1101 | } | ||
| 1102 | |||
| 1103 | /* The last component found is not a directory, so | ||
| 1104 | return -1. */ | ||
| 1105 | return -1; | ||
| 1106 | } | ||
| 1107 | |||
| 1108 | /* So move CURSOR to a row with the right display | ||
| 1109 | name. */ | ||
| 1110 | |||
| 1111 | name = cursor.getString (nameColumn); | ||
| 1112 | newId = cursor.getString (idColumn); | ||
| 1113 | newType = cursor.getString (typeColumn); | ||
| 1114 | |||
| 1115 | /* Any of the three variables above may be NULL if the | ||
| 1116 | column data is of the wrong type depending on how | ||
| 1117 | the Cursor returned is implemented. */ | ||
| 1118 | |||
| 1119 | if (name == null || newId == null || newType == null) | ||
| 1120 | return -1; | ||
| 1121 | |||
| 1122 | /* Cache this name, even if it isn't the document | ||
| 1123 | that's being searched for. */ | ||
| 1124 | |||
| 1125 | cache = cacheChild (toplevel, children, name, | ||
| 1126 | newId, newType, | ||
| 1127 | idEntry != null); | ||
| 1128 | |||
| 1129 | /* Record the desired component once it is located, | ||
| 1130 | but continue reading and caching items from the | ||
| 1131 | cursor. */ | ||
| 1132 | |||
| 1133 | if (name.equals (component)) | ||
| 1134 | { | ||
| 1135 | id = newId; | ||
| 1136 | next = cache.children; | ||
| 1137 | type = newType; | ||
| 1138 | } | ||
| 1139 | } | ||
| 1140 | |||
| 1141 | children = next; | ||
| 1142 | |||
| 1143 | /* Now close the cursor. */ | ||
| 1144 | cursor.close (); | ||
| 1145 | cursor = null; | ||
| 1146 | |||
| 1147 | /* ID may have become NULL if the data is in an invalid | ||
| 1148 | format. */ | ||
| 1149 | if (id == null) | ||
| 1150 | return -1; | ||
| 1151 | } | ||
| 1152 | } | ||
| 1153 | finally | ||
| 1154 | { | ||
| 1155 | /* If an error is thrown within the block above, let | ||
| 1156 | android_saf_exception_check handle it, but make sure the | ||
| 1157 | cursor is closed. */ | ||
| 1158 | |||
| 1159 | if (cursor != null) | ||
| 1160 | cursor.close (); | ||
| 1161 | } | ||
| 1162 | |||
| 1163 | /* Here, id is either NULL (meaning the same as TREE_URI), and | ||
| 1164 | type is either NULL (in which case id should also be NULL) or | ||
| 1165 | the MIME type of the file. */ | ||
| 1166 | |||
| 1167 | /* First return the ID. */ | ||
| 1168 | |||
| 1169 | if (id == null) | ||
| 1170 | id_return[0] = DocumentsContract.getTreeDocumentId (uri); | ||
| 1171 | else | ||
| 1172 | id_return[0] = id; | ||
| 1173 | |||
| 1174 | /* Next, return whether or not this is a directory. */ | ||
| 1175 | if (type == null || type.equals (Document.MIME_TYPE_DIR)) | ||
| 1176 | return 1; | ||
| 1177 | |||
| 1178 | return 0; | ||
| 1179 | } | ||
| 1180 | |||
| 1181 | /* Find the document ID of the file within TREE_URI designated by | ||
| 1182 | NAME. | ||
| 1183 | |||
| 1184 | NAME is a ``file name'' comprised of the display names of | ||
| 1185 | individual files. Each constituent component prior to the last | ||
| 1186 | must name a directory file within TREE_URI. | ||
| 1187 | |||
| 1188 | Upon success, return 0 or 1 (contingent upon whether or not the | ||
| 1189 | last component within NAME is a directory) and place the document | ||
| 1190 | ID of the named file in ID_RETURN[0]. | ||
| 1191 | |||
| 1192 | If the designated file can't be located, but each component of | ||
| 1193 | NAME up to the last component can and is a directory, return -2 | ||
| 1194 | and the ID of the last component located in ID_RETURN[0]. | ||
| 1195 | |||
| 1196 | If the designated file can't be located, return -1, or signal one | ||
| 1197 | of OperationCanceledException, SecurityException, | ||
| 1198 | FileNotFoundException, or UnsupportedOperationException. */ | ||
| 1199 | |||
| 1200 | public int | ||
| 1201 | documentIdFromName (final String tree_uri, final String name, | ||
| 1202 | final String[] id_return) | ||
| 1203 | { | ||
| 1204 | return runIntFunction (new SafIntFunction () { | ||
| 1205 | @Override | ||
| 1206 | public int | ||
| 1207 | runInt (CancellationSignal signal) | ||
| 1208 | { | ||
| 1209 | return documentIdFromName1 (tree_uri, name, id_return, | ||
| 1210 | signal); | ||
| 1211 | } | ||
| 1212 | }); | ||
| 1213 | } | ||
| 1214 | |||
| 1215 | /* The bulk of `statDocument'. SIGNAL should be a cancelation | ||
| 1216 | signal. */ | ||
| 1217 | |||
| 1218 | private long[] | ||
| 1219 | statDocument1 (String uri, String documentId, | ||
| 1220 | CancellationSignal signal) | ||
| 1221 | { | ||
| 1222 | Uri uriObject, tree; | ||
| 1223 | String[] projection; | ||
| 1224 | long[] stat; | ||
| 1225 | Cursor cursor; | ||
| 1226 | CacheToplevel toplevel; | ||
| 1227 | StatCacheEntry cache; | ||
| 1228 | |||
| 1229 | tree = Uri.parse (uri); | ||
| 1230 | |||
| 1231 | if (documentId == null) | ||
| 1232 | documentId = DocumentsContract.getTreeDocumentId (tree); | ||
| 1233 | |||
| 1234 | /* Create a document URI representing DOCUMENTID within URI's | ||
| 1235 | authority. */ | ||
| 1236 | |||
| 1237 | uriObject | ||
| 1238 | = DocumentsContract.buildDocumentUriUsingTree (tree, documentId); | ||
| 1239 | |||
| 1240 | /* See if the file status cache currently contains this | ||
| 1241 | document. */ | ||
| 1242 | |||
| 1243 | toplevel = getCache (tree); | ||
| 1244 | cache = toplevel.statCache.get (documentId); | ||
| 1245 | |||
| 1246 | if (cache == null || !cache.isValid ()) | ||
| 1247 | { | ||
| 1248 | /* Stat this document and enter its information into the | ||
| 1249 | cache. */ | ||
| 1250 | |||
| 1251 | projection = new String[] { | ||
| 1252 | Document.COLUMN_FLAGS, | ||
| 1253 | Document.COLUMN_LAST_MODIFIED, | ||
| 1254 | Document.COLUMN_MIME_TYPE, | ||
| 1255 | Document.COLUMN_SIZE, | ||
| 1256 | }; | ||
| 1257 | |||
| 1258 | cursor = resolver.query (uriObject, projection, null, | ||
| 1259 | null, null, signal); | ||
| 1260 | |||
| 1261 | if (cursor == null) | ||
| 1262 | return null; | ||
| 1263 | |||
| 1264 | try | ||
| 1265 | { | ||
| 1266 | if (!cursor.moveToFirst ()) | ||
| 1267 | return null; | ||
| 1268 | |||
| 1269 | cache = cacheFileStatus (documentId, toplevel, cursor); | ||
| 1270 | } | ||
| 1271 | finally | ||
| 1272 | { | ||
| 1273 | cursor.close (); | ||
| 1274 | } | ||
| 1275 | |||
| 1276 | /* If cache is still null, return null. */ | ||
| 1277 | |||
| 1278 | if (cache == null) | ||
| 1279 | return null; | ||
| 1280 | } | ||
| 1281 | |||
| 1282 | /* Create the array of file status and populate it with the | ||
| 1283 | information within cache. */ | ||
| 1284 | stat = new long[3]; | ||
| 1285 | |||
| 1286 | stat[0] |= S_IRUSR; | ||
| 1287 | if ((cache.flags & Document.FLAG_SUPPORTS_WRITE) != 0) | ||
| 1288 | stat[0] |= S_IWUSR; | ||
| 1289 | |||
| 1290 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N | ||
| 1291 | && (cache.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0) | ||
| 1292 | stat[0] |= S_IFCHR; | ||
| 1293 | |||
| 1294 | stat[1] = cache.size; | ||
| 1295 | |||
| 1296 | /* Check if this is a directory file. */ | ||
| 1297 | if (cache.isDirectory | ||
| 1298 | /* Files shouldn't be specials and directories at the same | ||
| 1299 | time, but Android doesn't forbid document providers | ||
| 1300 | from returning this information. */ | ||
| 1301 | && (stat[0] & S_IFCHR) == 0) | ||
| 1302 | { | ||
| 1303 | /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories, | ||
| 1304 | just assume they're writable. */ | ||
| 1305 | stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR; | ||
| 1306 | |||
| 1307 | /* Directory files cannot be modified if | ||
| 1308 | FLAG_DIR_SUPPORTS_CREATE is not set. */ | ||
| 1309 | |||
| 1310 | if ((cache.flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) | ||
| 1311 | stat[0] &= ~S_IWUSR; | ||
| 1312 | } | ||
| 1313 | |||
| 1314 | /* If this file is neither a character special nor a | ||
| 1315 | directory, indicate that it's a regular file. */ | ||
| 1316 | |||
| 1317 | if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0) | ||
| 1318 | stat[0] |= S_IFREG; | ||
| 1319 | |||
| 1320 | stat[2] = cache.mtime; | ||
| 1321 | return stat; | ||
| 1322 | } | ||
| 1323 | |||
| 1324 | /* Return file status for the document designated by the given | ||
| 1325 | DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document | ||
| 1326 | ID in URI itself. | ||
| 1327 | |||
| 1328 | Value is null upon failure, or an array of longs [MODE, SIZE, | ||
| 1329 | MTIM] upon success, where MODE contains the file type and access | ||
| 1330 | modes of the file as in `struct stat', SIZE is the size of the | ||
| 1331 | file in BYTES or -1 if not known, and MTIM is the time of the | ||
| 1332 | last modification to this file in milliseconds since 00:00, | ||
| 1333 | January 1st, 1970. | ||
| 1334 | |||
| 1335 | OperationCanceledException and other typical exceptions may be | ||
| 1336 | signaled upon receiving async input or other errors. */ | ||
| 1337 | |||
| 1338 | public long[] | ||
| 1339 | statDocument (final String uri, final String documentId) | ||
| 1340 | { | ||
| 1341 | return (long[]) runObjectFunction (new SafObjectFunction () { | ||
| 1342 | @Override | ||
| 1343 | public Object | ||
| 1344 | runObject (CancellationSignal signal) | ||
| 1345 | { | ||
| 1346 | return statDocument1 (uri, documentId, signal); | ||
| 1347 | } | ||
| 1348 | }); | ||
| 1349 | } | ||
| 1350 | |||
| 1351 | /* The bulk of `accessDocument'. SIGNAL should be a cancellation | ||
| 1352 | signal. */ | ||
| 1353 | |||
| 1354 | private int | ||
| 1355 | accessDocument1 (String uri, String documentId, boolean writable, | ||
| 1356 | CancellationSignal signal) | ||
| 1357 | { | ||
| 1358 | Uri uriObject; | ||
| 1359 | String[] projection; | ||
| 1360 | int tem, index; | ||
| 1361 | String tem1; | ||
| 1362 | Cursor cursor; | ||
| 1363 | CacheToplevel toplevel; | ||
| 1364 | CacheEntry entry; | ||
| 1365 | |||
| 1366 | uriObject = Uri.parse (uri); | ||
| 1367 | |||
| 1368 | if (documentId == null) | ||
| 1369 | documentId = DocumentsContract.getTreeDocumentId (uriObject); | ||
| 1370 | |||
| 1371 | /* If WRITABLE is false and the document ID is cached, use its | ||
| 1372 | cached value instead. This speeds up | ||
| 1373 | `directory-files-with-attributes' a little. */ | ||
| 1374 | |||
| 1375 | if (!writable) | ||
| 1376 | { | ||
| 1377 | toplevel = getCache (uriObject); | ||
| 1378 | entry = toplevel.idCache.get (documentId); | ||
| 1379 | |||
| 1380 | if (entry != null) | ||
| 1381 | return 0; | ||
| 1382 | } | ||
| 1383 | |||
| 1384 | /* Create a document URI representing DOCUMENTID within URI's | ||
| 1385 | authority. */ | ||
| 1386 | |||
| 1387 | uriObject | ||
| 1388 | = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId); | ||
| 1389 | |||
| 1390 | /* Now stat this document. */ | ||
| 1391 | |||
| 1392 | projection = new String[] { | ||
| 1393 | Document.COLUMN_FLAGS, | ||
| 1394 | Document.COLUMN_MIME_TYPE, | ||
| 1395 | }; | ||
| 1396 | |||
| 1397 | cursor = resolver.query (uriObject, projection, null, | ||
| 1398 | null, null, signal); | ||
| 1399 | |||
| 1400 | if (cursor == null) | ||
| 1401 | return -1; | ||
| 1402 | |||
| 1403 | try | ||
| 1404 | { | ||
| 1405 | if (!cursor.moveToFirst ()) | ||
| 1406 | return -1; | ||
| 1407 | |||
| 1408 | if (!writable) | ||
| 1409 | return 0; | ||
| 1410 | |||
| 1411 | index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 1412 | if (index < 0) | ||
| 1413 | return -3; | ||
| 1414 | |||
| 1415 | /* Get the type of this file to check if it's a directory. */ | ||
| 1416 | tem1 = cursor.getString (index); | ||
| 1417 | |||
| 1418 | /* Check if this is a directory file. */ | ||
| 1419 | if (tem1.equals (Document.MIME_TYPE_DIR)) | ||
| 1420 | { | ||
| 1421 | /* If so, don't check for FLAG_SUPPORTS_WRITE. | ||
| 1422 | Check for FLAG_DIR_SUPPORTS_CREATE instead. */ | ||
| 1423 | |||
| 1424 | if (!writable) | ||
| 1425 | return 0; | ||
| 1426 | |||
| 1427 | index = cursor.getColumnIndex (Document.COLUMN_FLAGS); | ||
| 1428 | if (index < 0) | ||
| 1429 | return -3; | ||
| 1430 | |||
| 1431 | tem = cursor.getInt (index); | ||
| 1432 | if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0) | ||
| 1433 | return -3; | ||
| 1434 | |||
| 1435 | return 0; | ||
| 1436 | } | ||
| 1437 | |||
| 1438 | index = cursor.getColumnIndex (Document.COLUMN_FLAGS); | ||
| 1439 | if (index < 0) | ||
| 1440 | return -3; | ||
| 1441 | |||
| 1442 | tem = cursor.getInt (index); | ||
| 1443 | if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0) | ||
| 1444 | return -3; | ||
| 1445 | } | ||
| 1446 | finally | ||
| 1447 | { | ||
| 1448 | /* Close the cursor if an exception occurs. */ | ||
| 1449 | cursor.close (); | ||
| 1450 | } | ||
| 1451 | |||
| 1452 | return 0; | ||
| 1453 | } | ||
| 1454 | |||
| 1455 | /* Find out whether Emacs has access to the document designated by | ||
| 1456 | the specified DOCUMENTID within the tree URI. If DOCUMENTID is | ||
| 1457 | NULL, use the document ID in URI itself. | ||
| 1458 | |||
| 1459 | If WRITABLE, also check that the file is writable, which is true | ||
| 1460 | if it is either a directory or its flags contains | ||
| 1461 | FLAG_SUPPORTS_WRITE. | ||
| 1462 | |||
| 1463 | Value is 0 if the file is accessible, and one of the following if | ||
| 1464 | not: | ||
| 1465 | |||
| 1466 | -1, if the file does not exist. | ||
| 1467 | -2, if WRITABLE and the file is not writable. | ||
| 1468 | -3, upon any other error. | ||
| 1469 | |||
| 1470 | In addition, arbitrary runtime exceptions (such as | ||
| 1471 | SecurityException or UnsupportedOperationException) may be | ||
| 1472 | thrown. */ | ||
| 1473 | |||
| 1474 | public int | ||
| 1475 | accessDocument (final String uri, final String documentId, | ||
| 1476 | final boolean writable) | ||
| 1477 | { | ||
| 1478 | return runIntFunction (new SafIntFunction () { | ||
| 1479 | @Override | ||
| 1480 | public int | ||
| 1481 | runInt (CancellationSignal signal) | ||
| 1482 | { | ||
| 1483 | return accessDocument1 (uri, documentId, writable, | ||
| 1484 | signal); | ||
| 1485 | } | ||
| 1486 | }); | ||
| 1487 | } | ||
| 1488 | |||
| 1489 | /* The crux of openDocumentDirectory. SIGNAL must be a cancellation | ||
| 1490 | signal. */ | ||
| 1491 | |||
| 1492 | private Cursor | ||
| 1493 | openDocumentDirectory1 (String uri, String documentId, | ||
| 1494 | CancellationSignal signal) | ||
| 1495 | { | ||
| 1496 | Uri uriObject, tree; | ||
| 1497 | Cursor cursor; | ||
| 1498 | String projection[]; | ||
| 1499 | CacheToplevel toplevel; | ||
| 1500 | |||
| 1501 | tree = uriObject = Uri.parse (uri); | ||
| 1502 | |||
| 1503 | /* If documentId is not set, use the document ID of the tree URI | ||
| 1504 | itself. */ | ||
| 1505 | |||
| 1506 | if (documentId == null) | ||
| 1507 | documentId = DocumentsContract.getTreeDocumentId (uriObject); | ||
| 1508 | |||
| 1509 | /* Build a URI representing each directory entry within | ||
| 1510 | DOCUMENTID. */ | ||
| 1511 | |||
| 1512 | uriObject | ||
| 1513 | = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject, | ||
| 1514 | documentId); | ||
| 1515 | |||
| 1516 | projection = new String [] { | ||
| 1517 | Document.COLUMN_DISPLAY_NAME, | ||
| 1518 | Document.COLUMN_DOCUMENT_ID, | ||
| 1519 | Document.COLUMN_MIME_TYPE, | ||
| 1520 | Document.COLUMN_FLAGS, | ||
| 1521 | Document.COLUMN_LAST_MODIFIED, | ||
| 1522 | Document.COLUMN_SIZE, | ||
| 1523 | }; | ||
| 1524 | |||
| 1525 | cursor = resolver.query (uriObject, projection, null, null, | ||
| 1526 | null, signal); | ||
| 1527 | |||
| 1528 | /* Create a new cache entry tied to this document ID. */ | ||
| 1529 | |||
| 1530 | if (cursor != null) | ||
| 1531 | { | ||
| 1532 | toplevel = getCache (tree); | ||
| 1533 | cacheDirectoryFromCursor (toplevel, documentId, | ||
| 1534 | cursor); | ||
| 1535 | } | ||
| 1536 | |||
| 1537 | /* Return the cursor. */ | ||
| 1538 | return cursor; | ||
| 1539 | } | ||
| 1540 | |||
| 1541 | /* Open a cursor representing each entry within the directory | ||
| 1542 | designated by the specified DOCUMENTID within the tree URI. | ||
| 1543 | |||
| 1544 | If DOCUMENTID is NULL, use the document ID within URI itself. | ||
| 1545 | Value is NULL upon failure. | ||
| 1546 | |||
| 1547 | In addition, arbitrary runtime exceptions (such as | ||
| 1548 | SecurityException or UnsupportedOperationException) may be | ||
| 1549 | thrown. */ | ||
| 1550 | |||
| 1551 | public Cursor | ||
| 1552 | openDocumentDirectory (final String uri, final String documentId) | ||
| 1553 | { | ||
| 1554 | return (Cursor) runObjectFunction (new SafObjectFunction () { | ||
| 1555 | @Override | ||
| 1556 | public Object | ||
| 1557 | runObject (CancellationSignal signal) | ||
| 1558 | { | ||
| 1559 | return openDocumentDirectory1 (uri, documentId, signal); | ||
| 1560 | } | ||
| 1561 | }); | ||
| 1562 | } | ||
| 1563 | |||
| 1564 | /* The crux of `openDocument'. SIGNAL must be a cancellation | ||
| 1565 | signal. */ | ||
| 1566 | |||
| 1567 | public ParcelFileDescriptor | ||
| 1568 | openDocument1 (String uri, String documentId, boolean write, | ||
| 1569 | boolean truncate, CancellationSignal signal) | ||
| 1570 | throws Throwable | ||
| 1571 | { | ||
| 1572 | Uri treeUri, documentUri; | ||
| 1573 | String mode; | ||
| 1574 | ParcelFileDescriptor fileDescriptor; | ||
| 1575 | CacheToplevel toplevel; | ||
| 1576 | |||
| 1577 | treeUri = Uri.parse (uri); | ||
| 1578 | |||
| 1579 | /* documentId must be set for this request, since it doesn't make | ||
| 1580 | sense to ``open'' the root of the directory tree. */ | ||
| 1581 | |||
| 1582 | documentUri | ||
| 1583 | = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId); | ||
| 1584 | |||
| 1585 | /* Select the mode used to open the file. */ | ||
| 1586 | |||
| 1587 | if (write) | ||
| 1588 | { | ||
| 1589 | if (truncate) | ||
| 1590 | mode = "rwt"; | ||
| 1591 | else | ||
| 1592 | mode = "rw"; | ||
| 1593 | } | ||
| 1594 | else | ||
| 1595 | mode = "r"; | ||
| 1596 | |||
| 1597 | fileDescriptor | ||
| 1598 | = resolver.openFileDescriptor (documentUri, mode, | ||
| 1599 | signal); | ||
| 1600 | |||
| 1601 | /* If a writable file descriptor is requested and TRUNCATE is set, | ||
| 1602 | then probe the file descriptor to detect if it is actually | ||
| 1603 | readable. If not, close this file descriptor and reopen it | ||
| 1604 | with MODE set to rw; some document providers granting access to | ||
| 1605 | Samba shares don't implement rwt, but these document providers | ||
| 1606 | invariably truncate the file opened even when the mode is | ||
| 1607 | merely rw. | ||
| 1608 | |||
| 1609 | This may be ascribed to a mix-up in Android's documentation | ||
| 1610 | regardin DocumentsProvider: the `openDocument' function is only | ||
| 1611 | documented to accept r or rw, whereas the default | ||
| 1612 | implementation of the `openFile' function (which documents rwt) | ||
| 1613 | delegates to `openDocument'. */ | ||
| 1614 | |||
| 1615 | if (write && truncate && fileDescriptor != null | ||
| 1616 | && !EmacsNative.ftruncate (fileDescriptor.getFd ())) | ||
| 1617 | { | ||
| 1618 | try | ||
| 1619 | { | ||
| 1620 | fileDescriptor.closeWithError ("File descriptor requested" | ||
| 1621 | + " is not writable"); | ||
| 1622 | } | ||
| 1623 | catch (IOException e) | ||
| 1624 | { | ||
| 1625 | Log.w (TAG, "Leaking unclosed file descriptor " + e); | ||
| 1626 | } | ||
| 1627 | |||
| 1628 | fileDescriptor | ||
| 1629 | = resolver.openFileDescriptor (documentUri, "rw", signal); | ||
| 1630 | |||
| 1631 | /* Try to truncate fileDescriptor just to stay on the safe | ||
| 1632 | side. */ | ||
| 1633 | if (fileDescriptor != null) | ||
| 1634 | EmacsNative.ftruncate (fileDescriptor.getFd ()); | ||
| 1635 | } | ||
| 1636 | |||
| 1637 | /* Every time a document is opened, remove it from the file status | ||
| 1638 | cache. */ | ||
| 1639 | toplevel = getCache (treeUri); | ||
| 1640 | toplevel.statCache.remove (documentId); | ||
| 1641 | |||
| 1642 | return fileDescriptor; | ||
| 1643 | } | ||
| 1644 | |||
| 1645 | /* Open a file descriptor for a file document designated by | ||
| 1646 | DOCUMENTID within the document tree identified by URI. If | ||
| 1647 | TRUNCATE and the document already exists, truncate its contents | ||
| 1648 | before returning. | ||
| 1649 | |||
| 1650 | On Android 9.0 and earlier, always open the document in | ||
| 1651 | ``read-write'' mode; this instructs the document provider to | ||
| 1652 | return a seekable file that is stored on disk and returns correct | ||
| 1653 | file status. | ||
| 1654 | |||
| 1655 | Under newer versions of Android, open the document in a | ||
| 1656 | non-writable mode if WRITE is false. This is possible because | ||
| 1657 | these versions allow Emacs to explicitly request a seekable | ||
| 1658 | on-disk file. | ||
| 1659 | |||
| 1660 | Value is NULL upon failure or a parcel file descriptor upon | ||
| 1661 | success. Call `ParcelFileDescriptor.close' on this file | ||
| 1662 | descriptor instead of using the `close' system call. | ||
| 1663 | |||
| 1664 | FileNotFoundException and/or SecurityException and/or | ||
| 1665 | UnsupportedOperationException and/or OperationCanceledException | ||
| 1666 | may be thrown upon failure. */ | ||
| 1667 | |||
| 1668 | public ParcelFileDescriptor | ||
| 1669 | openDocument (final String uri, final String documentId, | ||
| 1670 | final boolean write, final boolean truncate) | ||
| 1671 | { | ||
| 1672 | Object tem; | ||
| 1673 | |||
| 1674 | tem = runObjectFunction (new SafObjectFunction () { | ||
| 1675 | @Override | ||
| 1676 | public Object | ||
| 1677 | runObject (CancellationSignal signal) | ||
| 1678 | throws Throwable | ||
| 1679 | { | ||
| 1680 | return openDocument1 (uri, documentId, write, truncate, | ||
| 1681 | signal); | ||
| 1682 | } | ||
| 1683 | }); | ||
| 1684 | |||
| 1685 | return (ParcelFileDescriptor) tem; | ||
| 1686 | } | ||
| 1687 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsSdk11Clipboard.java b/java/org/gnu/emacs/EmacsSdk11Clipboard.java new file mode 100644 index 00000000000..4959ec36eed --- /dev/null +++ b/java/org/gnu/emacs/EmacsSdk11Clipboard.java | |||
| @@ -0,0 +1,290 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.content.ClipboardManager; | ||
| 23 | import android.content.Context; | ||
| 24 | import android.content.ContentResolver; | ||
| 25 | import android.content.ClipData; | ||
| 26 | import android.content.ClipDescription; | ||
| 27 | |||
| 28 | import android.content.res.AssetFileDescriptor; | ||
| 29 | |||
| 30 | import android.net.Uri; | ||
| 31 | |||
| 32 | import android.util.Log; | ||
| 33 | |||
| 34 | import android.os.Build; | ||
| 35 | |||
| 36 | import java.io.FileNotFoundException; | ||
| 37 | import java.io.IOException; | ||
| 38 | import java.io.UnsupportedEncodingException; | ||
| 39 | |||
| 40 | /* This class implements EmacsClipboard for Android 3.0 and later | ||
| 41 | systems. */ | ||
| 42 | |||
| 43 | public final class EmacsSdk11Clipboard extends EmacsClipboard | ||
| 44 | implements ClipboardManager.OnPrimaryClipChangedListener | ||
| 45 | { | ||
| 46 | private static final String TAG = "EmacsSdk11Clipboard"; | ||
| 47 | private ClipboardManager manager; | ||
| 48 | private boolean ownsClipboard; | ||
| 49 | private int clipboardChangedCount; | ||
| 50 | private int monitoredClipboardChangedCount; | ||
| 51 | private ContentResolver resolver; | ||
| 52 | |||
| 53 | public | ||
| 54 | EmacsSdk11Clipboard () | ||
| 55 | { | ||
| 56 | manager = EmacsService.SERVICE.getClipboardManager (); | ||
| 57 | |||
| 58 | /* The system forbids Emacs from reading clipboard data in the | ||
| 59 | background under Android 10 or later. */ | ||
| 60 | |||
| 61 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) | ||
| 62 | manager.addPrimaryClipChangedListener (this); | ||
| 63 | |||
| 64 | /* Now obtain the content resolver used to open file | ||
| 65 | descriptors. */ | ||
| 66 | |||
| 67 | resolver = EmacsService.SERVICE.getContentResolver (); | ||
| 68 | } | ||
| 69 | |||
| 70 | @Override | ||
| 71 | public synchronized void | ||
| 72 | onPrimaryClipChanged () | ||
| 73 | { | ||
| 74 | Log.d (TAG, ("onPrimaryClipChanged: " | ||
| 75 | + monitoredClipboardChangedCount | ||
| 76 | + " " + clipboardChangedCount)); | ||
| 77 | |||
| 78 | /* Increment monitoredClipboardChangeCount. If it is now greater | ||
| 79 | than clipboardChangedCount, then Emacs no longer owns the | ||
| 80 | clipboard. */ | ||
| 81 | monitoredClipboardChangedCount++; | ||
| 82 | |||
| 83 | if (monitoredClipboardChangedCount > clipboardChangedCount) | ||
| 84 | { | ||
| 85 | ownsClipboard = false; | ||
| 86 | |||
| 87 | /* Reset both values back to 0. */ | ||
| 88 | monitoredClipboardChangedCount = 0; | ||
| 89 | clipboardChangedCount = 0; | ||
| 90 | } | ||
| 91 | } | ||
| 92 | |||
| 93 | /* Set the clipboard text to CLIPBOARD, a string in UTF-8 | ||
| 94 | encoding. */ | ||
| 95 | |||
| 96 | @Override | ||
| 97 | public synchronized void | ||
| 98 | setClipboard (byte[] bytes) | ||
| 99 | { | ||
| 100 | ClipData data; | ||
| 101 | String string; | ||
| 102 | |||
| 103 | try | ||
| 104 | { | ||
| 105 | string = new String (bytes, "UTF-8"); | ||
| 106 | data = ClipData.newPlainText ("Emacs", string); | ||
| 107 | manager.setPrimaryClip (data); | ||
| 108 | ownsClipboard = true; | ||
| 109 | |||
| 110 | /* onPrimaryClipChanged will be called again. Use this | ||
| 111 | variable to keep track of how many times the clipboard has | ||
| 112 | been changed. */ | ||
| 113 | ++clipboardChangedCount; | ||
| 114 | } | ||
| 115 | catch (UnsupportedEncodingException exception) | ||
| 116 | { | ||
| 117 | Log.w (TAG, "setClipboard: " + exception); | ||
| 118 | } | ||
| 119 | } | ||
| 120 | |||
| 121 | /* Return whether or not Emacs owns the clipboard. Value is 1 if | ||
| 122 | Emacs does, 0 if Emacs does not, and -1 if that information is | ||
| 123 | unavailable. */ | ||
| 124 | |||
| 125 | @Override | ||
| 126 | public synchronized int | ||
| 127 | ownsClipboard () | ||
| 128 | { | ||
| 129 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) | ||
| 130 | return -1; | ||
| 131 | |||
| 132 | return ownsClipboard ? 1 : 0; | ||
| 133 | } | ||
| 134 | |||
| 135 | /* Return whether or not clipboard content currently exists. */ | ||
| 136 | |||
| 137 | @Override | ||
| 138 | public boolean | ||
| 139 | clipboardExists () | ||
| 140 | { | ||
| 141 | return manager.hasPrimaryClip (); | ||
| 142 | } | ||
| 143 | |||
| 144 | /* Return the current content of the clipboard, as plain text, or | ||
| 145 | NULL if no content is available. */ | ||
| 146 | |||
| 147 | @Override | ||
| 148 | public byte[] | ||
| 149 | getClipboard () | ||
| 150 | { | ||
| 151 | ClipData clip; | ||
| 152 | CharSequence text; | ||
| 153 | Context context; | ||
| 154 | |||
| 155 | clip = manager.getPrimaryClip (); | ||
| 156 | |||
| 157 | if (clip == null || clip.getItemCount () < 1) | ||
| 158 | return null; | ||
| 159 | |||
| 160 | context = EmacsService.SERVICE; | ||
| 161 | |||
| 162 | try | ||
| 163 | { | ||
| 164 | text = clip.getItemAt (0).coerceToText (context); | ||
| 165 | return text.toString ().getBytes ("UTF-8"); | ||
| 166 | } | ||
| 167 | catch (UnsupportedEncodingException exception) | ||
| 168 | { | ||
| 169 | Log.w (TAG, "getClipboard: " + exception); | ||
| 170 | } | ||
| 171 | |||
| 172 | return null; | ||
| 173 | } | ||
| 174 | |||
| 175 | /* Return an array of targets currently provided by the | ||
| 176 | clipboard, or NULL if there are none. */ | ||
| 177 | |||
| 178 | @Override | ||
| 179 | public byte[][] | ||
| 180 | getClipboardTargets () | ||
| 181 | { | ||
| 182 | ClipData clip; | ||
| 183 | ClipDescription description; | ||
| 184 | byte[][] typeArray; | ||
| 185 | int i; | ||
| 186 | |||
| 187 | /* N.B. that Android calls the clipboard the ``primary clip''; it | ||
| 188 | is not related to the X primary selection. */ | ||
| 189 | clip = manager.getPrimaryClip (); | ||
| 190 | description = clip.getDescription (); | ||
| 191 | i = description.getMimeTypeCount (); | ||
| 192 | typeArray = new byte[i][i]; | ||
| 193 | |||
| 194 | try | ||
| 195 | { | ||
| 196 | for (i = 0; i < description.getMimeTypeCount (); ++i) | ||
| 197 | typeArray[i] = description.getMimeType (i).getBytes ("UTF-8"); | ||
| 198 | } | ||
| 199 | catch (UnsupportedEncodingException exception) | ||
| 200 | { | ||
| 201 | return null; | ||
| 202 | } | ||
| 203 | |||
| 204 | return typeArray; | ||
| 205 | } | ||
| 206 | |||
| 207 | /* Return the clipboard data for the given target, or NULL if it | ||
| 208 | does not exist. | ||
| 209 | |||
| 210 | Value is normally an array of three longs: the file descriptor, | ||
| 211 | the start offset of the data, and its length; length may be | ||
| 212 | AssetFileDescriptor.UNKOWN_LENGTH, meaning that the data extends | ||
| 213 | from that offset to the end of the file. | ||
| 214 | |||
| 215 | Do not use this function to open text targets; use `getClipboard' | ||
| 216 | for that instead, as it will handle selection data consisting | ||
| 217 | solely of a URI. */ | ||
| 218 | |||
| 219 | @Override | ||
| 220 | public long[] | ||
| 221 | getClipboardData (byte[] target) | ||
| 222 | { | ||
| 223 | ClipData data; | ||
| 224 | String mimeType; | ||
| 225 | int fd; | ||
| 226 | AssetFileDescriptor assetFd; | ||
| 227 | Uri uri; | ||
| 228 | long[] value; | ||
| 229 | |||
| 230 | /* Decode the target given by Emacs. */ | ||
| 231 | try | ||
| 232 | { | ||
| 233 | mimeType = new String (target, "UTF-8"); | ||
| 234 | } | ||
| 235 | catch (UnsupportedEncodingException exception) | ||
| 236 | { | ||
| 237 | return null; | ||
| 238 | } | ||
| 239 | |||
| 240 | Log.d (TAG, "getClipboardData: "+ mimeType); | ||
| 241 | |||
| 242 | /* Now obtain the clipboard data and the data corresponding to | ||
| 243 | that MIME type. */ | ||
| 244 | |||
| 245 | data = manager.getPrimaryClip (); | ||
| 246 | |||
| 247 | if (data.getItemCount () < 1) | ||
| 248 | return null; | ||
| 249 | |||
| 250 | try | ||
| 251 | { | ||
| 252 | uri = data.getItemAt (0).getUri (); | ||
| 253 | |||
| 254 | if (uri == null) | ||
| 255 | return null; | ||
| 256 | |||
| 257 | Log.d (TAG, "getClipboardData: "+ uri); | ||
| 258 | |||
| 259 | /* Now open the file descriptor. */ | ||
| 260 | assetFd = resolver.openTypedAssetFileDescriptor (uri, mimeType, | ||
| 261 | null); | ||
| 262 | |||
| 263 | /* Duplicate the file descriptor. */ | ||
| 264 | fd = assetFd.getParcelFileDescriptor ().getFd (); | ||
| 265 | fd = EmacsNative.dup (fd); | ||
| 266 | |||
| 267 | /* Return the relevant information. */ | ||
| 268 | value = new long[] { fd, assetFd.getStartOffset (), | ||
| 269 | assetFd.getLength (), }; | ||
| 270 | |||
| 271 | /* Close the original offset. */ | ||
| 272 | assetFd.close (); | ||
| 273 | |||
| 274 | Log.d (TAG, "getClipboardData: "+ value); | ||
| 275 | } | ||
| 276 | catch (FileNotFoundException e) | ||
| 277 | { | ||
| 278 | return null; | ||
| 279 | } | ||
| 280 | catch (IOException e) | ||
| 281 | { | ||
| 282 | return null; | ||
| 283 | } | ||
| 284 | |||
| 285 | /* Don't return value if the file descriptor couldn't be | ||
| 286 | created. */ | ||
| 287 | |||
| 288 | return fd != -1 ? value : null; | ||
| 289 | } | ||
| 290 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsSdk23FontDriver.java b/java/org/gnu/emacs/EmacsSdk23FontDriver.java new file mode 100644 index 00000000000..aaba8dbd166 --- /dev/null +++ b/java/org/gnu/emacs/EmacsSdk23FontDriver.java | |||
| @@ -0,0 +1,114 @@ | |||
| 1 | /* Font backend for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.graphics.Paint; | ||
| 23 | import android.graphics.Rect; | ||
| 24 | |||
| 25 | public final class EmacsSdk23FontDriver extends EmacsSdk7FontDriver | ||
| 26 | { | ||
| 27 | private void | ||
| 28 | textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics, | ||
| 29 | Paint paint, Rect bounds) | ||
| 30 | { | ||
| 31 | char[] text; | ||
| 32 | |||
| 33 | text = new char[2]; | ||
| 34 | text[0] = (char) code; | ||
| 35 | text[1] = 'c'; | ||
| 36 | |||
| 37 | paint.getTextBounds (text, 0, 1, bounds); | ||
| 38 | |||
| 39 | metrics.lbearing = (short) bounds.left; | ||
| 40 | metrics.rbearing = (short) bounds.right; | ||
| 41 | metrics.ascent = (short) -bounds.top; | ||
| 42 | metrics.descent = (short) bounds.bottom; | ||
| 43 | metrics.width | ||
| 44 | = (short) paint.getRunAdvance (text, 0, 1, 0, 1, false, 1); | ||
| 45 | } | ||
| 46 | |||
| 47 | @Override | ||
| 48 | public void | ||
| 49 | textExtents (FontObject font, int code[], FontMetrics fontMetrics) | ||
| 50 | { | ||
| 51 | int i; | ||
| 52 | Paint paintCache; | ||
| 53 | Rect boundsCache; | ||
| 54 | Sdk7FontObject fontObject; | ||
| 55 | char[] text; | ||
| 56 | float width; | ||
| 57 | |||
| 58 | fontObject = (Sdk7FontObject) font; | ||
| 59 | paintCache = fontObject.typeface.typefacePaint; | ||
| 60 | paintCache.setTextSize (fontObject.pixelSize); | ||
| 61 | boundsCache = new Rect (); | ||
| 62 | |||
| 63 | if (code.length == 0) | ||
| 64 | { | ||
| 65 | fontMetrics.lbearing = 0; | ||
| 66 | fontMetrics.rbearing = 0; | ||
| 67 | fontMetrics.ascent = 0; | ||
| 68 | fontMetrics.descent = 0; | ||
| 69 | fontMetrics.width = 0; | ||
| 70 | } | ||
| 71 | else if (code.length == 1) | ||
| 72 | textExtents1 ((Sdk7FontObject) font, code[0], fontMetrics, | ||
| 73 | paintCache, boundsCache); | ||
| 74 | else | ||
| 75 | { | ||
| 76 | text = new char[code.length + 1]; | ||
| 77 | |||
| 78 | for (i = 0; i < code.length; ++i) | ||
| 79 | text[i] = (char) code[i]; | ||
| 80 | |||
| 81 | text[code.length] = 'c'; | ||
| 82 | |||
| 83 | paintCache.getTextBounds (text, 0, code.length, | ||
| 84 | boundsCache); | ||
| 85 | width = paintCache.getRunAdvance (text, 0, code.length, 0, | ||
| 86 | code.length, | ||
| 87 | false, code.length); | ||
| 88 | |||
| 89 | fontMetrics.lbearing = (short) boundsCache.left; | ||
| 90 | fontMetrics.rbearing = (short) boundsCache.right; | ||
| 91 | fontMetrics.ascent = (short) -boundsCache.top; | ||
| 92 | fontMetrics.descent = (short) boundsCache.bottom; | ||
| 93 | fontMetrics.width = (short) width; | ||
| 94 | } | ||
| 95 | } | ||
| 96 | |||
| 97 | @Override | ||
| 98 | public int | ||
| 99 | hasChar (FontSpec font, char charCode) | ||
| 100 | { | ||
| 101 | Sdk7FontObject fontObject; | ||
| 102 | Paint paint; | ||
| 103 | |||
| 104 | if (font instanceof Sdk7FontObject) | ||
| 105 | { | ||
| 106 | fontObject = (Sdk7FontObject) font; | ||
| 107 | paint = fontObject.typeface.typefacePaint; | ||
| 108 | } | ||
| 109 | else | ||
| 110 | paint = ((Sdk7FontEntity) font).typeface.typefacePaint; | ||
| 111 | |||
| 112 | return paint.hasGlyph (String.valueOf (charCode)) ? 1 : 0; | ||
| 113 | } | ||
| 114 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsSdk7FontDriver.java b/java/org/gnu/emacs/EmacsSdk7FontDriver.java new file mode 100644 index 00000000000..97969585d16 --- /dev/null +++ b/java/org/gnu/emacs/EmacsSdk7FontDriver.java | |||
| @@ -0,0 +1,539 @@ | |||
| 1 | /* Font backend for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.io.File; | ||
| 23 | |||
| 24 | import java.util.LinkedList; | ||
| 25 | import java.util.List; | ||
| 26 | |||
| 27 | import android.graphics.Paint; | ||
| 28 | import android.graphics.Rect; | ||
| 29 | import android.graphics.Typeface; | ||
| 30 | import android.graphics.Canvas; | ||
| 31 | |||
| 32 | import android.util.Log; | ||
| 33 | |||
| 34 | public class EmacsSdk7FontDriver extends EmacsFontDriver | ||
| 35 | { | ||
| 36 | private static final String TOFU_STRING = "\uDB3F\uDFFD"; | ||
| 37 | private static final String EM_STRING = "m"; | ||
| 38 | private static final String TAG = "EmacsSdk7FontDriver"; | ||
| 39 | |||
| 40 | protected static final class Sdk7Typeface | ||
| 41 | { | ||
| 42 | /* The typeface and paint. */ | ||
| 43 | public Typeface typeface; | ||
| 44 | public Paint typefacePaint; | ||
| 45 | public String familyName; | ||
| 46 | public int slant, width, weight, spacing; | ||
| 47 | |||
| 48 | public | ||
| 49 | Sdk7Typeface (String fileName, Typeface typeface) | ||
| 50 | { | ||
| 51 | String style, testString; | ||
| 52 | int index, measured, i; | ||
| 53 | float[] widths; | ||
| 54 | |||
| 55 | slant = NORMAL; | ||
| 56 | weight = REGULAR; | ||
| 57 | width = UNSPECIFIED; | ||
| 58 | spacing = PROPORTIONAL; | ||
| 59 | |||
| 60 | this.typeface = typeface; | ||
| 61 | |||
| 62 | typefacePaint = new Paint (); | ||
| 63 | typefacePaint.setAntiAlias (true); | ||
| 64 | typefacePaint.setTypeface (typeface); | ||
| 65 | |||
| 66 | /* For the calls to measureText below. */ | ||
| 67 | typefacePaint.setTextSize (10.0f); | ||
| 68 | |||
| 69 | /* Parse the file name into some useful data. First, strip off | ||
| 70 | the extension. */ | ||
| 71 | fileName = fileName.split ("\\.", 2)[0]; | ||
| 72 | |||
| 73 | /* Next, split the file name by dashes. Everything before the | ||
| 74 | last dash is part of the family name. */ | ||
| 75 | index = fileName.lastIndexOf ("-"); | ||
| 76 | |||
| 77 | if (index > 0) | ||
| 78 | { | ||
| 79 | style = fileName.substring (index + 1, fileName.length ()); | ||
| 80 | familyName = fileName.substring (0, index); | ||
| 81 | |||
| 82 | /* Look for something describing the weight. */ | ||
| 83 | if (style.contains ("Thin")) | ||
| 84 | weight = THIN; | ||
| 85 | else if (style.contains ("UltraLight")) | ||
| 86 | weight = ULTRA_LIGHT; | ||
| 87 | else if (style.contains ("SemiLight")) | ||
| 88 | weight = SEMI_LIGHT; | ||
| 89 | else if (style.contains ("Light")) | ||
| 90 | weight = LIGHT; | ||
| 91 | else if (style.contains ("Medium")) | ||
| 92 | weight = MEDIUM; | ||
| 93 | else if (style.contains ("SemiBold")) | ||
| 94 | weight = SEMI_BOLD; | ||
| 95 | else if (style.contains ("ExtraBold")) | ||
| 96 | weight = EXTRA_BOLD; | ||
| 97 | else if (style.contains ("Bold")) | ||
| 98 | weight = BOLD; | ||
| 99 | else if (style.contains ("Black")) | ||
| 100 | weight = BLACK; | ||
| 101 | else if (style.contains ("UltraHeavy")) | ||
| 102 | weight = ULTRA_HEAVY; | ||
| 103 | |||
| 104 | /* And the slant. */ | ||
| 105 | if (style.contains ("ReverseOblique")) | ||
| 106 | slant = OBLIQUE; | ||
| 107 | else if (style.contains ("ReverseItalic")) | ||
| 108 | slant = REVERSE_ITALIC; | ||
| 109 | else if (style.contains ("Italic")) | ||
| 110 | slant = ITALIC; | ||
| 111 | else if (style.contains ("Oblique")) | ||
| 112 | slant = OBLIQUE; | ||
| 113 | |||
| 114 | /* Finally, the width. */ | ||
| 115 | if (style.contains ("UltraCondensed")) | ||
| 116 | width = ULTRA_CONDENSED; | ||
| 117 | else if (style.contains ("ExtraCondensed")) | ||
| 118 | width = EXTRA_CONDENSED; | ||
| 119 | else if (style.contains ("SemiCondensed")) | ||
| 120 | width = SEMI_CONDENSED; | ||
| 121 | else if (style.contains ("Condensed")) | ||
| 122 | width = CONDENSED; | ||
| 123 | else if (style.contains ("SemiExpanded")) | ||
| 124 | width = SEMI_EXPANDED; | ||
| 125 | else if (style.contains ("ExtraExpanded")) | ||
| 126 | width = EXTRA_EXPANDED; | ||
| 127 | else if (style.contains ("UltraExpanded")) | ||
| 128 | width = ULTRA_EXPANDED; | ||
| 129 | else if (style.contains ("Expanded")) | ||
| 130 | width = EXPANDED; | ||
| 131 | |||
| 132 | /* Guess the spacing information. */ | ||
| 133 | testString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; | ||
| 134 | widths = new float[testString.length ()]; | ||
| 135 | |||
| 136 | measured = typefacePaint.getTextWidths (testString, | ||
| 137 | 0, testString.length (), | ||
| 138 | widths); | ||
| 139 | spacing = MONO; | ||
| 140 | for (i = 0; i < measured; ++i) | ||
| 141 | { | ||
| 142 | if (i != 0 && widths[i - 1] != widths[i]) | ||
| 143 | /* This isn't a monospace font. */ | ||
| 144 | spacing = PROPORTIONAL; | ||
| 145 | } | ||
| 146 | } | ||
| 147 | else | ||
| 148 | familyName = fileName; | ||
| 149 | } | ||
| 150 | |||
| 151 | @Override | ||
| 152 | public String | ||
| 153 | toString () | ||
| 154 | { | ||
| 155 | return ("Sdk7Typeface (" | ||
| 156 | + String.valueOf (familyName) + ", " | ||
| 157 | + String.valueOf (slant) + ", " | ||
| 158 | + String.valueOf (width) + ", " | ||
| 159 | + String.valueOf (weight) + ", " | ||
| 160 | + String.valueOf (spacing) + ")"); | ||
| 161 | } | ||
| 162 | }; | ||
| 163 | |||
| 164 | protected static final class Sdk7FontEntity extends FontEntity | ||
| 165 | { | ||
| 166 | /* The typeface. */ | ||
| 167 | public Sdk7Typeface typeface; | ||
| 168 | |||
| 169 | public | ||
| 170 | Sdk7FontEntity (Sdk7Typeface typeface) | ||
| 171 | { | ||
| 172 | foundry = "Google"; | ||
| 173 | family = typeface.familyName; | ||
| 174 | adstyle = null; | ||
| 175 | weight = typeface.weight; | ||
| 176 | slant = typeface.slant; | ||
| 177 | spacing = typeface.spacing; | ||
| 178 | width = typeface.width; | ||
| 179 | dpi = Math.round (EmacsService.SERVICE.metrics.scaledDensity * 160f); | ||
| 180 | |||
| 181 | this.typeface = typeface; | ||
| 182 | } | ||
| 183 | }; | ||
| 184 | |||
| 185 | protected final class Sdk7FontObject extends FontObject | ||
| 186 | { | ||
| 187 | /* The typeface. */ | ||
| 188 | public Sdk7Typeface typeface; | ||
| 189 | |||
| 190 | public | ||
| 191 | Sdk7FontObject (Sdk7Typeface typeface, int pixelSize) | ||
| 192 | { | ||
| 193 | float totalWidth; | ||
| 194 | String testWidth, testString; | ||
| 195 | |||
| 196 | this.typeface = typeface; | ||
| 197 | this.pixelSize = pixelSize; | ||
| 198 | |||
| 199 | family = typeface.familyName; | ||
| 200 | adstyle = null; | ||
| 201 | weight = typeface.weight; | ||
| 202 | slant = typeface.slant; | ||
| 203 | spacing = typeface.spacing; | ||
| 204 | width = typeface.width; | ||
| 205 | dpi = Math.round (EmacsService.SERVICE.metrics.scaledDensity * 160f); | ||
| 206 | |||
| 207 | /* Compute the ascent and descent. */ | ||
| 208 | typeface.typefacePaint.setTextSize (pixelSize); | ||
| 209 | ascent | ||
| 210 | = Math.round (-typeface.typefacePaint.ascent ()); | ||
| 211 | descent | ||
| 212 | = Math.round (typeface.typefacePaint.descent ()); | ||
| 213 | |||
| 214 | /* Compute the average width. */ | ||
| 215 | testString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; | ||
| 216 | totalWidth = typeface.typefacePaint.measureText (testString); | ||
| 217 | |||
| 218 | if (totalWidth > 0) | ||
| 219 | avgwidth = Math.round (totalWidth | ||
| 220 | / testString.length ()); | ||
| 221 | |||
| 222 | /* Android doesn't expose the font average width and height | ||
| 223 | information, so this will have to do. */ | ||
| 224 | minWidth = maxWidth = avgwidth; | ||
| 225 | |||
| 226 | /* This is different from avgwidth in the font spec! */ | ||
| 227 | averageWidth = avgwidth; | ||
| 228 | |||
| 229 | /* Set the space width. */ | ||
| 230 | totalWidth = typeface.typefacePaint.measureText (" "); | ||
| 231 | spaceWidth = Math.round (totalWidth); | ||
| 232 | |||
| 233 | /* Set the height and default ascent. */ | ||
| 234 | height = ascent + descent; | ||
| 235 | defaultAscent = ascent; | ||
| 236 | } | ||
| 237 | }; | ||
| 238 | |||
| 239 | private String[] fontFamilyList; | ||
| 240 | private Sdk7Typeface[] typefaceList; | ||
| 241 | private Sdk7Typeface fallbackTypeface; | ||
| 242 | |||
| 243 | public | ||
| 244 | EmacsSdk7FontDriver () | ||
| 245 | { | ||
| 246 | int i; | ||
| 247 | File systemFontsDirectory, fontFile; | ||
| 248 | Typeface typeface; | ||
| 249 | |||
| 250 | systemFontsDirectory = new File ("/system/fonts"); | ||
| 251 | |||
| 252 | fontFamilyList = systemFontsDirectory.list (); | ||
| 253 | |||
| 254 | /* If that returned null, replace it with an empty array. */ | ||
| 255 | fontFamilyList = new String[0]; | ||
| 256 | |||
| 257 | typefaceList = new Sdk7Typeface[fontFamilyList.length + 3]; | ||
| 258 | |||
| 259 | /* It would be nice to avoid opening each and every font upon | ||
| 260 | startup. But that doesn't seem to be possible on | ||
| 261 | Android. */ | ||
| 262 | |||
| 263 | for (i = 0; i < fontFamilyList.length; ++i) | ||
| 264 | { | ||
| 265 | fontFile = new File (systemFontsDirectory, | ||
| 266 | fontFamilyList[i]); | ||
| 267 | typeface = Typeface.createFromFile (fontFile); | ||
| 268 | typefaceList[i] = new Sdk7Typeface (fontFile.getName (), | ||
| 269 | typeface); | ||
| 270 | } | ||
| 271 | |||
| 272 | /* Initialize the default monospace and serif typefaces. */ | ||
| 273 | fallbackTypeface = new Sdk7Typeface ("monospace", | ||
| 274 | Typeface.MONOSPACE); | ||
| 275 | typefaceList[fontFamilyList.length] = fallbackTypeface; | ||
| 276 | |||
| 277 | fallbackTypeface = new Sdk7Typeface ("Monospace", | ||
| 278 | Typeface.MONOSPACE); | ||
| 279 | typefaceList[fontFamilyList.length + 1] = fallbackTypeface; | ||
| 280 | |||
| 281 | fallbackTypeface = new Sdk7Typeface ("Sans Serif", | ||
| 282 | Typeface.DEFAULT); | ||
| 283 | typefaceList[fontFamilyList.length + 2] = fallbackTypeface; | ||
| 284 | } | ||
| 285 | |||
| 286 | private boolean | ||
| 287 | checkMatch (Sdk7Typeface typeface, FontSpec fontSpec) | ||
| 288 | { | ||
| 289 | if (fontSpec.family != null | ||
| 290 | && !fontSpec.family.equals (typeface.familyName)) | ||
| 291 | return false; | ||
| 292 | |||
| 293 | if (fontSpec.slant != null | ||
| 294 | && !fontSpec.weight.equals (typeface.weight)) | ||
| 295 | return false; | ||
| 296 | |||
| 297 | if (fontSpec.spacing != null | ||
| 298 | && !fontSpec.spacing.equals (typeface.spacing)) | ||
| 299 | return false; | ||
| 300 | |||
| 301 | if (fontSpec.weight != null | ||
| 302 | && !fontSpec.weight.equals (typeface.weight)) | ||
| 303 | return false; | ||
| 304 | |||
| 305 | if (fontSpec.width != null | ||
| 306 | && !fontSpec.width.equals (typeface.width)) | ||
| 307 | return false; | ||
| 308 | |||
| 309 | return true; | ||
| 310 | } | ||
| 311 | |||
| 312 | @Override | ||
| 313 | public FontEntity[] | ||
| 314 | list (FontSpec fontSpec) | ||
| 315 | { | ||
| 316 | LinkedList<FontEntity> list; | ||
| 317 | int i; | ||
| 318 | |||
| 319 | list = new LinkedList<FontEntity> (); | ||
| 320 | |||
| 321 | for (i = 0; i < typefaceList.length; ++i) | ||
| 322 | { | ||
| 323 | if (checkMatch (typefaceList[i], fontSpec)) | ||
| 324 | list.add (new Sdk7FontEntity (typefaceList[i])); | ||
| 325 | } | ||
| 326 | |||
| 327 | return list.toArray (new FontEntity[0]); | ||
| 328 | } | ||
| 329 | |||
| 330 | @Override | ||
| 331 | public FontEntity | ||
| 332 | match (FontSpec fontSpec) | ||
| 333 | { | ||
| 334 | FontEntity[] entities; | ||
| 335 | int i; | ||
| 336 | |||
| 337 | entities = this.list (fontSpec); | ||
| 338 | |||
| 339 | if (entities.length == 0) | ||
| 340 | return new Sdk7FontEntity (fallbackTypeface); | ||
| 341 | |||
| 342 | return entities[0]; | ||
| 343 | } | ||
| 344 | |||
| 345 | @Override | ||
| 346 | public String[] | ||
| 347 | listFamilies () | ||
| 348 | { | ||
| 349 | return fontFamilyList; | ||
| 350 | } | ||
| 351 | |||
| 352 | @Override | ||
| 353 | public FontObject | ||
| 354 | openFont (FontEntity fontEntity, int pixelSize) | ||
| 355 | { | ||
| 356 | return new Sdk7FontObject (((Sdk7FontEntity) fontEntity).typeface, | ||
| 357 | pixelSize); | ||
| 358 | } | ||
| 359 | |||
| 360 | @Override | ||
| 361 | public int | ||
| 362 | hasChar (FontSpec font, char charCode) | ||
| 363 | { | ||
| 364 | float missingGlyphWidth, width; | ||
| 365 | Rect rect1, rect2; | ||
| 366 | Paint paint; | ||
| 367 | Sdk7FontObject fontObject; | ||
| 368 | |||
| 369 | if (font instanceof Sdk7FontObject) | ||
| 370 | { | ||
| 371 | fontObject = (Sdk7FontObject) font; | ||
| 372 | paint = fontObject.typeface.typefacePaint; | ||
| 373 | } | ||
| 374 | else | ||
| 375 | paint = ((Sdk7FontEntity) font).typeface.typefacePaint; | ||
| 376 | |||
| 377 | paint.setTextSize (10); | ||
| 378 | |||
| 379 | if (Character.isWhitespace (charCode)) | ||
| 380 | return 1; | ||
| 381 | |||
| 382 | missingGlyphWidth = paint.measureText (TOFU_STRING); | ||
| 383 | width = paint.measureText ("" + charCode); | ||
| 384 | |||
| 385 | if (width == 0f) | ||
| 386 | return 0; | ||
| 387 | |||
| 388 | if (width != missingGlyphWidth) | ||
| 389 | return 1; | ||
| 390 | |||
| 391 | rect1 = new Rect (); | ||
| 392 | rect2 = new Rect (); | ||
| 393 | |||
| 394 | paint.getTextBounds (TOFU_STRING, 0, TOFU_STRING.length (), | ||
| 395 | rect1); | ||
| 396 | paint.getTextBounds ("" + charCode, 0, 1, rect2); | ||
| 397 | return rect1.equals (rect2) ? 0 : 1; | ||
| 398 | } | ||
| 399 | |||
| 400 | private void | ||
| 401 | textExtents1 (Sdk7FontObject font, int code, FontMetrics metrics, | ||
| 402 | Paint paint, Rect bounds) | ||
| 403 | { | ||
| 404 | char[] text; | ||
| 405 | |||
| 406 | text = new char[1]; | ||
| 407 | text[0] = (char) code; | ||
| 408 | |||
| 409 | paint.getTextBounds (text, 0, 1, bounds); | ||
| 410 | |||
| 411 | /* bounds is the bounding box of the glyph corresponding to CODE. | ||
| 412 | Translate these into XCharStruct values. | ||
| 413 | |||
| 414 | The origin is at 0, 0, and lbearing is the distance counting | ||
| 415 | rightwards from the origin to the left most pixel in the glyph | ||
| 416 | raster. rbearing is the distance between the origin and the | ||
| 417 | rightmost pixel in the glyph raster. ascent is the distance | ||
| 418 | counting upwards between the the topmost pixel in the glyph | ||
| 419 | raster. descent is the distance (once again counting | ||
| 420 | downwards) between the origin and the bottommost pixel in the | ||
| 421 | glyph raster. | ||
| 422 | |||
| 423 | width is the distance between the origin and the origin of any | ||
| 424 | character to the right. */ | ||
| 425 | |||
| 426 | metrics.lbearing = (short) bounds.left; | ||
| 427 | metrics.rbearing = (short) bounds.right; | ||
| 428 | metrics.ascent = (short) -bounds.top; | ||
| 429 | metrics.descent = (short) bounds.bottom; | ||
| 430 | metrics.width = (short) paint.measureText ("" + text[0]); | ||
| 431 | } | ||
| 432 | |||
| 433 | @Override | ||
| 434 | public void | ||
| 435 | textExtents (FontObject font, int code[], FontMetrics fontMetrics) | ||
| 436 | { | ||
| 437 | int i; | ||
| 438 | Paint paintCache; | ||
| 439 | Rect boundsCache; | ||
| 440 | Sdk7FontObject fontObject; | ||
| 441 | char[] text; | ||
| 442 | float width; | ||
| 443 | |||
| 444 | fontObject = (Sdk7FontObject) font; | ||
| 445 | paintCache = fontObject.typeface.typefacePaint; | ||
| 446 | paintCache.setTextSize (fontObject.pixelSize); | ||
| 447 | boundsCache = new Rect (); | ||
| 448 | |||
| 449 | if (code.length == 0) | ||
| 450 | { | ||
| 451 | fontMetrics.lbearing = 0; | ||
| 452 | fontMetrics.rbearing = 0; | ||
| 453 | fontMetrics.ascent = 0; | ||
| 454 | fontMetrics.descent = 0; | ||
| 455 | fontMetrics.width = 0; | ||
| 456 | } | ||
| 457 | else if (code.length == 1) | ||
| 458 | textExtents1 ((Sdk7FontObject) font, code[0], fontMetrics, | ||
| 459 | paintCache, boundsCache); | ||
| 460 | else | ||
| 461 | { | ||
| 462 | text = new char[code.length]; | ||
| 463 | |||
| 464 | for (i = 0; i < code.length; ++i) | ||
| 465 | text[i] = (char) code[i]; | ||
| 466 | |||
| 467 | paintCache.getTextBounds (text, 0, code.length, | ||
| 468 | boundsCache); | ||
| 469 | width = paintCache.measureText (text, 0, code.length); | ||
| 470 | |||
| 471 | fontMetrics.lbearing = (short) boundsCache.left; | ||
| 472 | fontMetrics.rbearing = (short) boundsCache.right; | ||
| 473 | fontMetrics.ascent = (short) -boundsCache.top; | ||
| 474 | fontMetrics.descent = (short) boundsCache.bottom; | ||
| 475 | fontMetrics.width = (short) Math.round (width); | ||
| 476 | } | ||
| 477 | } | ||
| 478 | |||
| 479 | @Override | ||
| 480 | public int | ||
| 481 | encodeChar (FontObject fontObject, char charCode) | ||
| 482 | { | ||
| 483 | return charCode; | ||
| 484 | } | ||
| 485 | |||
| 486 | @Override | ||
| 487 | public int | ||
| 488 | draw (FontObject fontObject, EmacsGC gc, EmacsDrawable drawable, | ||
| 489 | int[] chars, int x, int y, int backgroundWidth, | ||
| 490 | boolean withBackground) | ||
| 491 | { | ||
| 492 | Rect backgroundRect, bounds; | ||
| 493 | Sdk7FontObject sdk7FontObject; | ||
| 494 | char[] charsArray; | ||
| 495 | int i; | ||
| 496 | Canvas canvas; | ||
| 497 | Paint paint; | ||
| 498 | |||
| 499 | sdk7FontObject = (Sdk7FontObject) fontObject; | ||
| 500 | charsArray = new char[chars.length]; | ||
| 501 | |||
| 502 | for (i = 0; i < chars.length; ++i) | ||
| 503 | charsArray[i] = (char) chars[i]; | ||
| 504 | |||
| 505 | backgroundRect = new Rect (); | ||
| 506 | backgroundRect.top = y - sdk7FontObject.ascent; | ||
| 507 | backgroundRect.left = x; | ||
| 508 | backgroundRect.right = x + backgroundWidth; | ||
| 509 | backgroundRect.bottom = y + sdk7FontObject.descent; | ||
| 510 | |||
| 511 | canvas = drawable.lockCanvas (gc); | ||
| 512 | |||
| 513 | if (canvas == null) | ||
| 514 | return 0; | ||
| 515 | |||
| 516 | paint = gc.gcPaint; | ||
| 517 | paint.setStyle (Paint.Style.FILL); | ||
| 518 | |||
| 519 | if (withBackground) | ||
| 520 | { | ||
| 521 | paint.setColor (gc.background | 0xff000000); | ||
| 522 | canvas.drawRect (backgroundRect, paint); | ||
| 523 | paint.setColor (gc.foreground | 0xff000000); | ||
| 524 | } | ||
| 525 | |||
| 526 | paint.setTextSize (sdk7FontObject.pixelSize); | ||
| 527 | paint.setTypeface (sdk7FontObject.typeface.typeface); | ||
| 528 | paint.setAntiAlias (true); | ||
| 529 | canvas.drawText (charsArray, 0, chars.length, x, y, paint); | ||
| 530 | |||
| 531 | bounds = new Rect (); | ||
| 532 | paint.getTextBounds (charsArray, 0, chars.length, bounds); | ||
| 533 | bounds.offset (x, y); | ||
| 534 | bounds.union (backgroundRect); | ||
| 535 | drawable.damageRect (bounds); | ||
| 536 | paint.setAntiAlias (false); | ||
| 537 | return 1; | ||
| 538 | } | ||
| 539 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsSdk8Clipboard.java b/java/org/gnu/emacs/EmacsSdk8Clipboard.java new file mode 100644 index 00000000000..9622641810f --- /dev/null +++ b/java/org/gnu/emacs/EmacsSdk8Clipboard.java | |||
| @@ -0,0 +1,147 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | /* Importing the entire package instead of just the legacy | ||
| 23 | ClipboardManager class avoids the deprecation warning. */ | ||
| 24 | |||
| 25 | import android.text.*; | ||
| 26 | |||
| 27 | import android.content.Context; | ||
| 28 | import android.util.Log; | ||
| 29 | |||
| 30 | import java.io.UnsupportedEncodingException; | ||
| 31 | |||
| 32 | /* This class implements EmacsClipboard for Android 2.2 and other | ||
| 33 | similarly old systems. */ | ||
| 34 | |||
| 35 | @SuppressWarnings ("deprecation") | ||
| 36 | public final class EmacsSdk8Clipboard extends EmacsClipboard | ||
| 37 | { | ||
| 38 | private static final String TAG = "EmacsSdk8Clipboard"; | ||
| 39 | private ClipboardManager manager; | ||
| 40 | |||
| 41 | public | ||
| 42 | EmacsSdk8Clipboard () | ||
| 43 | { | ||
| 44 | String what; | ||
| 45 | Context context; | ||
| 46 | |||
| 47 | what = Context.CLIPBOARD_SERVICE; | ||
| 48 | context = EmacsService.SERVICE; | ||
| 49 | manager | ||
| 50 | = (ClipboardManager) context.getSystemService (what); | ||
| 51 | } | ||
| 52 | |||
| 53 | /* Set the clipboard text to CLIPBOARD, a string in UTF-8 | ||
| 54 | encoding. */ | ||
| 55 | |||
| 56 | @Override | ||
| 57 | public void | ||
| 58 | setClipboard (byte[] bytes) | ||
| 59 | { | ||
| 60 | try | ||
| 61 | { | ||
| 62 | manager.setText (new String (bytes, "UTF-8")); | ||
| 63 | } | ||
| 64 | catch (UnsupportedEncodingException exception) | ||
| 65 | { | ||
| 66 | Log.w (TAG, "setClipboard: " + exception); | ||
| 67 | } | ||
| 68 | } | ||
| 69 | |||
| 70 | /* Return whether or not Emacs owns the clipboard. Value is 1 if | ||
| 71 | Emacs does, 0 if Emacs does not, and -1 if that information is | ||
| 72 | unavailable. */ | ||
| 73 | |||
| 74 | @Override | ||
| 75 | public int | ||
| 76 | ownsClipboard () | ||
| 77 | { | ||
| 78 | return -1; | ||
| 79 | } | ||
| 80 | |||
| 81 | /* Return whether or not clipboard content currently exists. */ | ||
| 82 | |||
| 83 | @Override | ||
| 84 | public boolean | ||
| 85 | clipboardExists () | ||
| 86 | { | ||
| 87 | return manager.hasText (); | ||
| 88 | } | ||
| 89 | |||
| 90 | /* Return the current content of the clipboard, as plain text, or | ||
| 91 | NULL if no content is available. */ | ||
| 92 | |||
| 93 | @Override | ||
| 94 | public byte[] | ||
| 95 | getClipboard () | ||
| 96 | { | ||
| 97 | String string; | ||
| 98 | CharSequence text; | ||
| 99 | |||
| 100 | text = manager.getText (); | ||
| 101 | |||
| 102 | if (text == null) | ||
| 103 | return null; | ||
| 104 | |||
| 105 | string = text.toString (); | ||
| 106 | |||
| 107 | try | ||
| 108 | { | ||
| 109 | return string.getBytes ("UTF-8"); | ||
| 110 | } | ||
| 111 | catch (UnsupportedEncodingException exception) | ||
| 112 | { | ||
| 113 | Log.w (TAG, "getClipboard: " + exception); | ||
| 114 | } | ||
| 115 | |||
| 116 | return null; | ||
| 117 | } | ||
| 118 | |||
| 119 | /* Return an array of targets currently provided by the | ||
| 120 | clipboard, or NULL if there are none. */ | ||
| 121 | |||
| 122 | @Override | ||
| 123 | public byte[][] | ||
| 124 | getClipboardTargets () | ||
| 125 | { | ||
| 126 | return null; | ||
| 127 | } | ||
| 128 | |||
| 129 | /* Return the clipboard data for the given target, or NULL if it | ||
| 130 | does not exist. | ||
| 131 | |||
| 132 | Value is normally an array of three longs: the file descriptor, | ||
| 133 | the start offset of the data, and its length; length may be | ||
| 134 | AssetFileDescriptor.UNKOWN_LENGTH, meaning that the data extends | ||
| 135 | from that offset to the end of the file. | ||
| 136 | |||
| 137 | Do not use this function to open text targets; use `getClipboard' | ||
| 138 | for that instead, as it will handle selection data consisting | ||
| 139 | solely of a URI. */ | ||
| 140 | |||
| 141 | @Override | ||
| 142 | public long[] | ||
| 143 | getClipboardData (byte[] target) | ||
| 144 | { | ||
| 145 | return null; | ||
| 146 | } | ||
| 147 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java new file mode 100644 index 00000000000..d91d8f66009 --- /dev/null +++ b/java/org/gnu/emacs/EmacsService.java | |||
| @@ -0,0 +1,1820 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.io.FileNotFoundException; | ||
| 23 | import java.io.IOException; | ||
| 24 | import java.io.UnsupportedEncodingException; | ||
| 25 | |||
| 26 | import java.util.ArrayList; | ||
| 27 | import java.util.HashSet; | ||
| 28 | import java.util.List; | ||
| 29 | |||
| 30 | import java.util.concurrent.atomic.AtomicInteger; | ||
| 31 | |||
| 32 | import android.database.Cursor; | ||
| 33 | |||
| 34 | import android.graphics.Matrix; | ||
| 35 | import android.graphics.Point; | ||
| 36 | |||
| 37 | import android.webkit.MimeTypeMap; | ||
| 38 | |||
| 39 | import android.view.InputDevice; | ||
| 40 | import android.view.KeyEvent; | ||
| 41 | import android.view.inputmethod.CursorAnchorInfo; | ||
| 42 | import android.view.inputmethod.ExtractedText; | ||
| 43 | |||
| 44 | import android.app.Notification; | ||
| 45 | import android.app.NotificationManager; | ||
| 46 | import android.app.NotificationChannel; | ||
| 47 | import android.app.Service; | ||
| 48 | |||
| 49 | import android.content.ClipboardManager; | ||
| 50 | import android.content.Context; | ||
| 51 | import android.content.ContentResolver; | ||
| 52 | import android.content.Intent; | ||
| 53 | import android.content.IntentFilter; | ||
| 54 | import android.content.UriPermission; | ||
| 55 | |||
| 56 | import android.content.pm.ApplicationInfo; | ||
| 57 | import android.content.pm.PackageManager.ApplicationInfoFlags; | ||
| 58 | import android.content.pm.PackageManager; | ||
| 59 | |||
| 60 | import android.content.res.AssetManager; | ||
| 61 | |||
| 62 | import android.hardware.input.InputManager; | ||
| 63 | |||
| 64 | import android.net.Uri; | ||
| 65 | |||
| 66 | import android.os.BatteryManager; | ||
| 67 | import android.os.Build; | ||
| 68 | import android.os.Looper; | ||
| 69 | import android.os.IBinder; | ||
| 70 | import android.os.Handler; | ||
| 71 | import android.os.ParcelFileDescriptor; | ||
| 72 | import android.os.Vibrator; | ||
| 73 | import android.os.VibratorManager; | ||
| 74 | import android.os.VibrationEffect; | ||
| 75 | |||
| 76 | import android.provider.DocumentsContract; | ||
| 77 | import android.provider.DocumentsContract.Document; | ||
| 78 | |||
| 79 | import android.util.Log; | ||
| 80 | import android.util.DisplayMetrics; | ||
| 81 | |||
| 82 | import android.widget.Toast; | ||
| 83 | |||
| 84 | /* EmacsService is the service that starts the thread running Emacs | ||
| 85 | and handles requests by that Emacs instance. */ | ||
| 86 | |||
| 87 | public final class EmacsService extends Service | ||
| 88 | { | ||
| 89 | public static final String TAG = "EmacsService"; | ||
| 90 | |||
| 91 | /* The started Emacs service object. */ | ||
| 92 | public static EmacsService SERVICE; | ||
| 93 | |||
| 94 | /* If non-NULL, an extra argument to pass to | ||
| 95 | `android_emacs_init'. */ | ||
| 96 | public static String extraStartupArgument; | ||
| 97 | |||
| 98 | /* The thread running Emacs C code. */ | ||
| 99 | private EmacsThread thread; | ||
| 100 | |||
| 101 | /* Handler used to run tasks on the main thread. */ | ||
| 102 | private Handler handler; | ||
| 103 | |||
| 104 | /* Content resolver used to access URIs. */ | ||
| 105 | private ContentResolver resolver; | ||
| 106 | |||
| 107 | /* Keep this in synch with androidgui.h. */ | ||
| 108 | public static final int IC_MODE_NULL = 0; | ||
| 109 | public static final int IC_MODE_ACTION = 1; | ||
| 110 | public static final int IC_MODE_TEXT = 2; | ||
| 111 | |||
| 112 | /* Display metrics used by font backends. */ | ||
| 113 | public DisplayMetrics metrics; | ||
| 114 | |||
| 115 | /* Flag that says whether or not to print verbose debugging | ||
| 116 | information when responding to an input method. */ | ||
| 117 | public static final boolean DEBUG_IC = false; | ||
| 118 | |||
| 119 | /* Flag that says whether or not to stringently check that only the | ||
| 120 | Emacs thread is performing drawing calls. */ | ||
| 121 | private static final boolean DEBUG_THREADS = false; | ||
| 122 | |||
| 123 | /* Atomic integer used for synchronization between | ||
| 124 | icBeginSynchronous/icEndSynchronous and viewGetSelection. | ||
| 125 | |||
| 126 | Value is 0 if no query is in progress, 1 if viewGetSelection is | ||
| 127 | being called, and 2 if icBeginSynchronous was called. */ | ||
| 128 | public static final AtomicInteger servicingQuery; | ||
| 129 | |||
| 130 | /* Thread used to query document providers, or null if it hasn't | ||
| 131 | been created yet. */ | ||
| 132 | private EmacsSafThread storageThread; | ||
| 133 | |||
| 134 | static | ||
| 135 | { | ||
| 136 | servicingQuery = new AtomicInteger (); | ||
| 137 | }; | ||
| 138 | |||
| 139 | /* Return the directory leading to the directory in which native | ||
| 140 | library files are stored on behalf of CONTEXT. */ | ||
| 141 | |||
| 142 | public static String | ||
| 143 | getLibraryDirectory (Context context) | ||
| 144 | { | ||
| 145 | int apiLevel; | ||
| 146 | |||
| 147 | apiLevel = Build.VERSION.SDK_INT; | ||
| 148 | |||
| 149 | if (apiLevel >= Build.VERSION_CODES.GINGERBREAD) | ||
| 150 | return context.getApplicationInfo ().nativeLibraryDir; | ||
| 151 | |||
| 152 | return context.getApplicationInfo ().dataDir + "/lib"; | ||
| 153 | } | ||
| 154 | |||
| 155 | @Override | ||
| 156 | public int | ||
| 157 | onStartCommand (Intent intent, int flags, int startId) | ||
| 158 | { | ||
| 159 | Notification notification; | ||
| 160 | NotificationManager manager; | ||
| 161 | NotificationChannel channel; | ||
| 162 | String infoBlurb; | ||
| 163 | Object tem; | ||
| 164 | |||
| 165 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) | ||
| 166 | { | ||
| 167 | tem = getSystemService (Context.NOTIFICATION_SERVICE); | ||
| 168 | manager = (NotificationManager) tem; | ||
| 169 | infoBlurb = ("This notification is displayed to keep Emacs" | ||
| 170 | + " running while it is in the background. You" | ||
| 171 | + " may disable it if you want;" | ||
| 172 | + " see (emacs)Android Environment."); | ||
| 173 | channel | ||
| 174 | = new NotificationChannel ("emacs", "Emacs persistent notification", | ||
| 175 | NotificationManager.IMPORTANCE_DEFAULT); | ||
| 176 | manager.createNotificationChannel (channel); | ||
| 177 | notification = (new Notification.Builder (this, "emacs") | ||
| 178 | .setContentTitle ("Emacs") | ||
| 179 | .setContentText (infoBlurb) | ||
| 180 | .setSmallIcon (android.R.drawable.sym_def_app_icon) | ||
| 181 | .build ()); | ||
| 182 | manager.notify (1, notification); | ||
| 183 | startForeground (1, notification); | ||
| 184 | } | ||
| 185 | |||
| 186 | return START_NOT_STICKY; | ||
| 187 | } | ||
| 188 | |||
| 189 | @Override | ||
| 190 | public IBinder | ||
| 191 | onBind (Intent intent) | ||
| 192 | { | ||
| 193 | return null; | ||
| 194 | } | ||
| 195 | |||
| 196 | @SuppressWarnings ("deprecation") | ||
| 197 | private String | ||
| 198 | getApkFile () | ||
| 199 | { | ||
| 200 | PackageManager manager; | ||
| 201 | ApplicationInfo info; | ||
| 202 | |||
| 203 | manager = getPackageManager (); | ||
| 204 | |||
| 205 | try | ||
| 206 | { | ||
| 207 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) | ||
| 208 | info = manager.getApplicationInfo ("org.gnu.emacs", 0); | ||
| 209 | else | ||
| 210 | info = manager.getApplicationInfo ("org.gnu.emacs", | ||
| 211 | ApplicationInfoFlags.of (0)); | ||
| 212 | |||
| 213 | /* Return an empty string upon failure. */ | ||
| 214 | |||
| 215 | if (info.sourceDir != null) | ||
| 216 | return info.sourceDir; | ||
| 217 | |||
| 218 | return ""; | ||
| 219 | } | ||
| 220 | catch (Exception e) | ||
| 221 | { | ||
| 222 | return ""; | ||
| 223 | } | ||
| 224 | } | ||
| 225 | |||
| 226 | @Override | ||
| 227 | public void | ||
| 228 | onCreate () | ||
| 229 | { | ||
| 230 | final AssetManager manager; | ||
| 231 | Context app_context; | ||
| 232 | final String filesDir, libDir, cacheDir, classPath; | ||
| 233 | final double pixelDensityX; | ||
| 234 | final double pixelDensityY; | ||
| 235 | final double scaledDensity; | ||
| 236 | double tempScaledDensity; | ||
| 237 | |||
| 238 | SERVICE = this; | ||
| 239 | handler = new Handler (Looper.getMainLooper ()); | ||
| 240 | manager = getAssets (); | ||
| 241 | app_context = getApplicationContext (); | ||
| 242 | metrics = getResources ().getDisplayMetrics (); | ||
| 243 | pixelDensityX = metrics.xdpi; | ||
| 244 | pixelDensityY = metrics.ydpi; | ||
| 245 | tempScaledDensity = ((metrics.scaledDensity | ||
| 246 | / metrics.density) | ||
| 247 | * pixelDensityX); | ||
| 248 | resolver = getContentResolver (); | ||
| 249 | |||
| 250 | /* If the density used to compute the text size is lesser than | ||
| 251 | 160, there's likely a bug with display density computation. | ||
| 252 | Reset it to 160 in that case. | ||
| 253 | |||
| 254 | Note that Android uses 160 ``dpi'' as the density where 1 point | ||
| 255 | corresponds to 1 pixel, not 72 or 96 as used elsewhere. This | ||
| 256 | difference is codified in PT_PER_INCH defined in font.h. */ | ||
| 257 | |||
| 258 | if (tempScaledDensity < 160) | ||
| 259 | tempScaledDensity = 160; | ||
| 260 | |||
| 261 | /* scaledDensity is const as required to refer to it from within | ||
| 262 | the nested function below. */ | ||
| 263 | scaledDensity = tempScaledDensity; | ||
| 264 | |||
| 265 | try | ||
| 266 | { | ||
| 267 | /* Configure Emacs with the asset manager and other necessary | ||
| 268 | parameters. */ | ||
| 269 | filesDir = app_context.getFilesDir ().getCanonicalPath (); | ||
| 270 | libDir = getLibraryDirectory (this); | ||
| 271 | cacheDir = app_context.getCacheDir ().getCanonicalPath (); | ||
| 272 | |||
| 273 | /* Now provide this application's apk file, so a recursive | ||
| 274 | invocation of app_process (through android-emacs) can | ||
| 275 | find EmacsNoninteractive. */ | ||
| 276 | classPath = getApkFile (); | ||
| 277 | |||
| 278 | Log.d (TAG, "Initializing Emacs, where filesDir = " + filesDir | ||
| 279 | + ", libDir = " + libDir + ", and classPath = " + classPath | ||
| 280 | + "; fileToOpen = " + EmacsOpenActivity.fileToOpen | ||
| 281 | + "; display density: " + pixelDensityX + " by " | ||
| 282 | + pixelDensityY + " scaled to " + scaledDensity); | ||
| 283 | |||
| 284 | /* Start the thread that runs Emacs. */ | ||
| 285 | thread = new EmacsThread (this, new Runnable () { | ||
| 286 | @Override | ||
| 287 | public void | ||
| 288 | run () | ||
| 289 | { | ||
| 290 | EmacsNative.setEmacsParams (manager, filesDir, libDir, | ||
| 291 | cacheDir, (float) pixelDensityX, | ||
| 292 | (float) pixelDensityY, | ||
| 293 | (float) scaledDensity, | ||
| 294 | classPath, EmacsService.this, | ||
| 295 | Build.VERSION.SDK_INT); | ||
| 296 | } | ||
| 297 | }, extraStartupArgument, | ||
| 298 | /* If any file needs to be opened, open it now. */ | ||
| 299 | EmacsOpenActivity.fileToOpen); | ||
| 300 | thread.start (); | ||
| 301 | } | ||
| 302 | catch (IOException exception) | ||
| 303 | { | ||
| 304 | EmacsNative.emacsAbort (); | ||
| 305 | return; | ||
| 306 | } | ||
| 307 | } | ||
| 308 | |||
| 309 | |||
| 310 | |||
| 311 | /* Functions from here on must only be called from the Emacs | ||
| 312 | thread. */ | ||
| 313 | |||
| 314 | public void | ||
| 315 | runOnUiThread (Runnable runnable) | ||
| 316 | { | ||
| 317 | handler.post (runnable); | ||
| 318 | } | ||
| 319 | |||
| 320 | public EmacsView | ||
| 321 | getEmacsView (final EmacsWindow window, final int visibility, | ||
| 322 | final boolean isFocusedByDefault) | ||
| 323 | { | ||
| 324 | Runnable runnable; | ||
| 325 | final EmacsHolder<EmacsView> view; | ||
| 326 | |||
| 327 | view = new EmacsHolder<EmacsView> (); | ||
| 328 | |||
| 329 | runnable = new Runnable () { | ||
| 330 | @Override | ||
| 331 | public void | ||
| 332 | run () | ||
| 333 | { | ||
| 334 | synchronized (this) | ||
| 335 | { | ||
| 336 | view.thing = new EmacsView (window); | ||
| 337 | view.thing.setVisibility (visibility); | ||
| 338 | |||
| 339 | /* The following function is only present on Android 26 | ||
| 340 | or later. */ | ||
| 341 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) | ||
| 342 | view.thing.setFocusedByDefault (isFocusedByDefault); | ||
| 343 | |||
| 344 | notify (); | ||
| 345 | } | ||
| 346 | } | ||
| 347 | }; | ||
| 348 | |||
| 349 | syncRunnable (runnable); | ||
| 350 | return view.thing; | ||
| 351 | } | ||
| 352 | |||
| 353 | public void | ||
| 354 | getLocationOnScreen (final EmacsView view, final int[] coordinates) | ||
| 355 | { | ||
| 356 | Runnable runnable; | ||
| 357 | |||
| 358 | runnable = new Runnable () { | ||
| 359 | public void | ||
| 360 | run () | ||
| 361 | { | ||
| 362 | synchronized (this) | ||
| 363 | { | ||
| 364 | view.getLocationOnScreen (coordinates); | ||
| 365 | notify (); | ||
| 366 | } | ||
| 367 | } | ||
| 368 | }; | ||
| 369 | |||
| 370 | syncRunnable (runnable); | ||
| 371 | } | ||
| 372 | |||
| 373 | |||
| 374 | |||
| 375 | public static void | ||
| 376 | checkEmacsThread () | ||
| 377 | { | ||
| 378 | if (DEBUG_THREADS) | ||
| 379 | { | ||
| 380 | if (Thread.currentThread () instanceof EmacsThread) | ||
| 381 | return; | ||
| 382 | |||
| 383 | throw new RuntimeException ("Emacs thread function" | ||
| 384 | + " called from other thread!"); | ||
| 385 | } | ||
| 386 | } | ||
| 387 | |||
| 388 | /* These drawing functions must only be called from the Emacs | ||
| 389 | thread. */ | ||
| 390 | |||
| 391 | public void | ||
| 392 | fillRectangle (EmacsDrawable drawable, EmacsGC gc, | ||
| 393 | int x, int y, int width, int height) | ||
| 394 | { | ||
| 395 | checkEmacsThread (); | ||
| 396 | EmacsFillRectangle.perform (drawable, gc, x, y, | ||
| 397 | width, height); | ||
| 398 | } | ||
| 399 | |||
| 400 | public void | ||
| 401 | fillPolygon (EmacsDrawable drawable, EmacsGC gc, | ||
| 402 | Point points[]) | ||
| 403 | { | ||
| 404 | checkEmacsThread (); | ||
| 405 | EmacsFillPolygon.perform (drawable, gc, points); | ||
| 406 | } | ||
| 407 | |||
| 408 | public void | ||
| 409 | drawRectangle (EmacsDrawable drawable, EmacsGC gc, | ||
| 410 | int x, int y, int width, int height) | ||
| 411 | { | ||
| 412 | checkEmacsThread (); | ||
| 413 | EmacsDrawRectangle.perform (drawable, gc, x, y, | ||
| 414 | width, height); | ||
| 415 | } | ||
| 416 | |||
| 417 | public void | ||
| 418 | drawLine (EmacsDrawable drawable, EmacsGC gc, | ||
| 419 | int x, int y, int x2, int y2) | ||
| 420 | { | ||
| 421 | checkEmacsThread (); | ||
| 422 | EmacsDrawLine.perform (drawable, gc, x, y, | ||
| 423 | x2, y2); | ||
| 424 | } | ||
| 425 | |||
| 426 | public void | ||
| 427 | drawPoint (EmacsDrawable drawable, EmacsGC gc, | ||
| 428 | int x, int y) | ||
| 429 | { | ||
| 430 | checkEmacsThread (); | ||
| 431 | EmacsDrawPoint.perform (drawable, gc, x, y); | ||
| 432 | } | ||
| 433 | |||
| 434 | public void | ||
| 435 | clearWindow (EmacsWindow window) | ||
| 436 | { | ||
| 437 | checkEmacsThread (); | ||
| 438 | window.clearWindow (); | ||
| 439 | } | ||
| 440 | |||
| 441 | public void | ||
| 442 | clearArea (EmacsWindow window, int x, int y, int width, | ||
| 443 | int height) | ||
| 444 | { | ||
| 445 | checkEmacsThread (); | ||
| 446 | window.clearArea (x, y, width, height); | ||
| 447 | } | ||
| 448 | |||
| 449 | @SuppressWarnings ("deprecation") | ||
| 450 | public void | ||
| 451 | ringBell () | ||
| 452 | { | ||
| 453 | Vibrator vibrator; | ||
| 454 | VibrationEffect effect; | ||
| 455 | VibratorManager vibratorManager; | ||
| 456 | Object tem; | ||
| 457 | |||
| 458 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) | ||
| 459 | { | ||
| 460 | tem = getSystemService (Context.VIBRATOR_MANAGER_SERVICE); | ||
| 461 | vibratorManager = (VibratorManager) tem; | ||
| 462 | vibrator = vibratorManager.getDefaultVibrator (); | ||
| 463 | } | ||
| 464 | else | ||
| 465 | vibrator | ||
| 466 | = (Vibrator) getSystemService (Context.VIBRATOR_SERVICE); | ||
| 467 | |||
| 468 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) | ||
| 469 | { | ||
| 470 | effect | ||
| 471 | = VibrationEffect.createOneShot (50, | ||
| 472 | VibrationEffect.DEFAULT_AMPLITUDE); | ||
| 473 | vibrator.vibrate (effect); | ||
| 474 | } | ||
| 475 | else | ||
| 476 | vibrator.vibrate (50); | ||
| 477 | } | ||
| 478 | |||
| 479 | public short[] | ||
| 480 | queryTree (EmacsWindow window) | ||
| 481 | { | ||
| 482 | short[] array; | ||
| 483 | List<EmacsWindow> windowList; | ||
| 484 | int i; | ||
| 485 | |||
| 486 | if (window == null) | ||
| 487 | /* Just return all the windows without a parent. */ | ||
| 488 | windowList = EmacsWindowAttachmentManager.MANAGER.copyWindows (); | ||
| 489 | else | ||
| 490 | windowList = window.children; | ||
| 491 | |||
| 492 | array = new short[windowList.size () + 1]; | ||
| 493 | i = 1; | ||
| 494 | |||
| 495 | array[0] = (window == null | ||
| 496 | ? 0 : (window.parent != null | ||
| 497 | ? window.parent.handle : 0)); | ||
| 498 | |||
| 499 | for (EmacsWindow treeWindow : windowList) | ||
| 500 | array[i++] = treeWindow.handle; | ||
| 501 | |||
| 502 | return array; | ||
| 503 | } | ||
| 504 | |||
| 505 | public int | ||
| 506 | getScreenWidth (boolean mmWise) | ||
| 507 | { | ||
| 508 | DisplayMetrics metrics; | ||
| 509 | |||
| 510 | metrics = getResources ().getDisplayMetrics (); | ||
| 511 | |||
| 512 | if (!mmWise) | ||
| 513 | return metrics.widthPixels; | ||
| 514 | else | ||
| 515 | return (int) ((metrics.widthPixels / metrics.xdpi) * 2540.0); | ||
| 516 | } | ||
| 517 | |||
| 518 | public int | ||
| 519 | getScreenHeight (boolean mmWise) | ||
| 520 | { | ||
| 521 | DisplayMetrics metrics; | ||
| 522 | |||
| 523 | metrics = getResources ().getDisplayMetrics (); | ||
| 524 | |||
| 525 | if (!mmWise) | ||
| 526 | return metrics.heightPixels; | ||
| 527 | else | ||
| 528 | return (int) ((metrics.heightPixels / metrics.ydpi) * 2540.0); | ||
| 529 | } | ||
| 530 | |||
| 531 | public boolean | ||
| 532 | detectMouse () | ||
| 533 | { | ||
| 534 | InputManager manager; | ||
| 535 | InputDevice device; | ||
| 536 | int[] ids; | ||
| 537 | int i; | ||
| 538 | |||
| 539 | if (Build.VERSION.SDK_INT | ||
| 540 | /* Android 4.0 and earlier don't support mouse input events at | ||
| 541 | all. */ | ||
| 542 | < Build.VERSION_CODES.JELLY_BEAN) | ||
| 543 | return false; | ||
| 544 | |||
| 545 | manager = (InputManager) getSystemService (Context.INPUT_SERVICE); | ||
| 546 | ids = manager.getInputDeviceIds (); | ||
| 547 | |||
| 548 | for (i = 0; i < ids.length; ++i) | ||
| 549 | { | ||
| 550 | device = manager.getInputDevice (ids[i]); | ||
| 551 | |||
| 552 | if (device == null) | ||
| 553 | continue; | ||
| 554 | |||
| 555 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) | ||
| 556 | { | ||
| 557 | if (device.supportsSource (InputDevice.SOURCE_MOUSE)) | ||
| 558 | return true; | ||
| 559 | } | ||
| 560 | else | ||
| 561 | { | ||
| 562 | /* `supportsSource' is only present on API level 21 and | ||
| 563 | later, but earlier versions provide a bit mask | ||
| 564 | containing each supported source. */ | ||
| 565 | |||
| 566 | if ((device.getSources () & InputDevice.SOURCE_MOUSE) != 0) | ||
| 567 | return true; | ||
| 568 | } | ||
| 569 | } | ||
| 570 | |||
| 571 | return false; | ||
| 572 | } | ||
| 573 | |||
| 574 | public String | ||
| 575 | nameKeysym (int keysym) | ||
| 576 | { | ||
| 577 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) | ||
| 578 | return KeyEvent.keyCodeToString (keysym); | ||
| 579 | |||
| 580 | return String.valueOf (keysym); | ||
| 581 | } | ||
| 582 | |||
| 583 | |||
| 584 | |||
| 585 | /* Start the Emacs service if necessary. On Android 26 and up, | ||
| 586 | start Emacs as a foreground service with a notification, to avoid | ||
| 587 | it being killed by the system. | ||
| 588 | |||
| 589 | On older systems, simply start it as a normal background | ||
| 590 | service. */ | ||
| 591 | |||
| 592 | public static void | ||
| 593 | startEmacsService (Context context) | ||
| 594 | { | ||
| 595 | if (EmacsService.SERVICE == null) | ||
| 596 | { | ||
| 597 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) | ||
| 598 | /* Start the Emacs service now. */ | ||
| 599 | context.startService (new Intent (context, | ||
| 600 | EmacsService.class)); | ||
| 601 | else | ||
| 602 | /* Display the permanant notification and start Emacs as a | ||
| 603 | foreground service. */ | ||
| 604 | context.startForegroundService (new Intent (context, | ||
| 605 | EmacsService.class)); | ||
| 606 | } | ||
| 607 | } | ||
| 608 | |||
| 609 | /* Ask the system to open the specified URL in an application that | ||
| 610 | understands how to open it. | ||
| 611 | |||
| 612 | If SEND, tell the system to also open applications that can | ||
| 613 | ``send'' the URL (through mail, for example), instead of only | ||
| 614 | those that can view the URL. | ||
| 615 | |||
| 616 | Value is NULL upon success, or a string describing the error | ||
| 617 | upon failure. */ | ||
| 618 | |||
| 619 | public String | ||
| 620 | browseUrl (String url, boolean send) | ||
| 621 | { | ||
| 622 | Intent intent; | ||
| 623 | Uri uri; | ||
| 624 | |||
| 625 | try | ||
| 626 | { | ||
| 627 | /* Parse the URI. */ | ||
| 628 | if (!send) | ||
| 629 | { | ||
| 630 | uri = Uri.parse (url); | ||
| 631 | |||
| 632 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) | ||
| 633 | { | ||
| 634 | /* On Android 4.4 and later, check if URI is actually | ||
| 635 | a file name. If so, rewrite it into a content | ||
| 636 | provider URI, so that it can be accessed by other | ||
| 637 | programs. */ | ||
| 638 | |||
| 639 | if (uri.getScheme ().equals ("file") | ||
| 640 | && uri.getPath () != null) | ||
| 641 | uri | ||
| 642 | = DocumentsContract.buildDocumentUri ("org.gnu.emacs", | ||
| 643 | uri.getPath ()); | ||
| 644 | } | ||
| 645 | |||
| 646 | Log.d (TAG, ("browseUri: browsing " + url | ||
| 647 | + " --> " + uri.getPath () | ||
| 648 | + " --> " + uri)); | ||
| 649 | |||
| 650 | intent = new Intent (Intent.ACTION_VIEW, uri); | ||
| 651 | intent.setFlags (Intent.FLAG_ACTIVITY_NEW_TASK | ||
| 652 | | Intent.FLAG_GRANT_READ_URI_PERMISSION); | ||
| 653 | } | ||
| 654 | else | ||
| 655 | { | ||
| 656 | intent = new Intent (Intent.ACTION_SEND); | ||
| 657 | intent.setType ("text/plain"); | ||
| 658 | intent.putExtra (Intent.EXTRA_SUBJECT, "Sharing link"); | ||
| 659 | intent.putExtra (Intent.EXTRA_TEXT, url); | ||
| 660 | |||
| 661 | /* Display a list of programs able to send this URL. */ | ||
| 662 | intent = Intent.createChooser (intent, "Send"); | ||
| 663 | |||
| 664 | /* Apparently flags need to be set after a choser is | ||
| 665 | created. */ | ||
| 666 | intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK); | ||
| 667 | } | ||
| 668 | |||
| 669 | startActivity (intent); | ||
| 670 | } | ||
| 671 | catch (Exception e) | ||
| 672 | { | ||
| 673 | return e.toString (); | ||
| 674 | } | ||
| 675 | |||
| 676 | return null; | ||
| 677 | } | ||
| 678 | |||
| 679 | /* Get a SDK 11 ClipboardManager. | ||
| 680 | |||
| 681 | Android 4.0.x requires that this be called from the main | ||
| 682 | thread. */ | ||
| 683 | |||
| 684 | public ClipboardManager | ||
| 685 | getClipboardManager () | ||
| 686 | { | ||
| 687 | final EmacsHolder<ClipboardManager> manager; | ||
| 688 | Runnable runnable; | ||
| 689 | |||
| 690 | manager = new EmacsHolder<ClipboardManager> (); | ||
| 691 | |||
| 692 | runnable = new Runnable () { | ||
| 693 | public void | ||
| 694 | run () | ||
| 695 | { | ||
| 696 | Object tem; | ||
| 697 | |||
| 698 | synchronized (this) | ||
| 699 | { | ||
| 700 | tem = getSystemService (Context.CLIPBOARD_SERVICE); | ||
| 701 | manager.thing = (ClipboardManager) tem; | ||
| 702 | notify (); | ||
| 703 | } | ||
| 704 | } | ||
| 705 | }; | ||
| 706 | |||
| 707 | syncRunnable (runnable); | ||
| 708 | return manager.thing; | ||
| 709 | } | ||
| 710 | |||
| 711 | public void | ||
| 712 | restartEmacs () | ||
| 713 | { | ||
| 714 | Intent intent; | ||
| 715 | |||
| 716 | intent = new Intent (this, EmacsActivity.class); | ||
| 717 | intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK | ||
| 718 | | Intent.FLAG_ACTIVITY_CLEAR_TASK); | ||
| 719 | startActivity (intent); | ||
| 720 | System.exit (0); | ||
| 721 | } | ||
| 722 | |||
| 723 | /* Wait synchronously for the specified RUNNABLE to complete in the | ||
| 724 | UI thread. Must be called from the Emacs thread. */ | ||
| 725 | |||
| 726 | public static void | ||
| 727 | syncRunnable (Runnable runnable) | ||
| 728 | { | ||
| 729 | EmacsNative.beginSynchronous (); | ||
| 730 | |||
| 731 | synchronized (runnable) | ||
| 732 | { | ||
| 733 | SERVICE.runOnUiThread (runnable); | ||
| 734 | |||
| 735 | while (true) | ||
| 736 | { | ||
| 737 | try | ||
| 738 | { | ||
| 739 | runnable.wait (); | ||
| 740 | break; | ||
| 741 | } | ||
| 742 | catch (InterruptedException e) | ||
| 743 | { | ||
| 744 | continue; | ||
| 745 | } | ||
| 746 | } | ||
| 747 | } | ||
| 748 | |||
| 749 | EmacsNative.endSynchronous (); | ||
| 750 | } | ||
| 751 | |||
| 752 | |||
| 753 | |||
| 754 | /* IMM functions such as `updateSelection' holds an internal lock | ||
| 755 | that is also taken before `onCreateInputConnection' (in | ||
| 756 | EmacsView.java) is called; when that then asks the UI thread for | ||
| 757 | the current selection, a dead lock results. To remedy this, | ||
| 758 | reply to any synchronous queries now -- and prohibit more queries | ||
| 759 | for the duration of `updateSelection' -- if EmacsView may have | ||
| 760 | been asking for the value of the region. */ | ||
| 761 | |||
| 762 | public static void | ||
| 763 | icBeginSynchronous () | ||
| 764 | { | ||
| 765 | /* Set servicingQuery to 2, so viewGetSelection knows it shouldn't | ||
| 766 | proceed. */ | ||
| 767 | |||
| 768 | if (servicingQuery.getAndSet (2) == 1) | ||
| 769 | /* But if viewGetSelection is already in progress, answer it | ||
| 770 | first. */ | ||
| 771 | EmacsNative.answerQuerySpin (); | ||
| 772 | } | ||
| 773 | |||
| 774 | public static void | ||
| 775 | icEndSynchronous () | ||
| 776 | { | ||
| 777 | if (servicingQuery.getAndSet (0) != 2) | ||
| 778 | throw new RuntimeException ("incorrect value of `servicingQuery': " | ||
| 779 | + "likely 1"); | ||
| 780 | } | ||
| 781 | |||
| 782 | public static int[] | ||
| 783 | viewGetSelection (short window) | ||
| 784 | { | ||
| 785 | int[] selection; | ||
| 786 | |||
| 787 | /* See if a query is already in progress from the other | ||
| 788 | direction. */ | ||
| 789 | if (!servicingQuery.compareAndSet (0, 1)) | ||
| 790 | return null; | ||
| 791 | |||
| 792 | /* Now call the regular getSelection. Note that this can't race | ||
| 793 | with answerQuerySpin, as `android_servicing_query' can never be | ||
| 794 | 2 when icBeginSynchronous is called, so a query will always be | ||
| 795 | started. */ | ||
| 796 | selection = EmacsNative.getSelection (window); | ||
| 797 | |||
| 798 | /* Finally, clear servicingQuery if its value is still 1. If a | ||
| 799 | query has started from the other side, it ought to be 2. */ | ||
| 800 | |||
| 801 | servicingQuery.compareAndSet (1, 0); | ||
| 802 | return selection; | ||
| 803 | } | ||
| 804 | |||
| 805 | |||
| 806 | |||
| 807 | public void | ||
| 808 | updateIC (EmacsWindow window, int newSelectionStart, | ||
| 809 | int newSelectionEnd, int composingRegionStart, | ||
| 810 | int composingRegionEnd) | ||
| 811 | { | ||
| 812 | if (DEBUG_IC) | ||
| 813 | Log.d (TAG, ("updateIC: " + window + " " + newSelectionStart | ||
| 814 | + " " + newSelectionEnd + " " | ||
| 815 | + composingRegionStart + " " | ||
| 816 | + composingRegionEnd)); | ||
| 817 | |||
| 818 | icBeginSynchronous (); | ||
| 819 | window.view.imManager.updateSelection (window.view, | ||
| 820 | newSelectionStart, | ||
| 821 | newSelectionEnd, | ||
| 822 | composingRegionStart, | ||
| 823 | composingRegionEnd); | ||
| 824 | icEndSynchronous (); | ||
| 825 | } | ||
| 826 | |||
| 827 | public void | ||
| 828 | resetIC (EmacsWindow window, int icMode) | ||
| 829 | { | ||
| 830 | int oldMode; | ||
| 831 | |||
| 832 | if (DEBUG_IC) | ||
| 833 | Log.d (TAG, "resetIC: " + window + ", " + icMode); | ||
| 834 | |||
| 835 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU | ||
| 836 | && (oldMode = window.view.getICMode ()) == icMode | ||
| 837 | /* Don't do this if there is currently no input | ||
| 838 | connection. */ | ||
| 839 | && oldMode != IC_MODE_NULL) | ||
| 840 | { | ||
| 841 | if (DEBUG_IC) | ||
| 842 | Log.d (TAG, "resetIC: calling invalidateInput"); | ||
| 843 | |||
| 844 | /* Android 33 and later allow the IM reset to be optimized out | ||
| 845 | and replaced by a call to `invalidateInput', which is much | ||
| 846 | faster, as it does not involve resetting the input | ||
| 847 | connection. */ | ||
| 848 | |||
| 849 | icBeginSynchronous (); | ||
| 850 | window.view.imManager.invalidateInput (window.view); | ||
| 851 | icEndSynchronous (); | ||
| 852 | |||
| 853 | return; | ||
| 854 | } | ||
| 855 | |||
| 856 | window.view.setICMode (icMode); | ||
| 857 | |||
| 858 | icBeginSynchronous (); | ||
| 859 | window.view.icGeneration++; | ||
| 860 | window.view.imManager.restartInput (window.view); | ||
| 861 | icEndSynchronous (); | ||
| 862 | } | ||
| 863 | |||
| 864 | public void | ||
| 865 | updateCursorAnchorInfo (EmacsWindow window, float x, | ||
| 866 | float y, float yBaseline, | ||
| 867 | float yBottom) | ||
| 868 | { | ||
| 869 | CursorAnchorInfo info; | ||
| 870 | CursorAnchorInfo.Builder builder; | ||
| 871 | Matrix matrix; | ||
| 872 | int[] offsets; | ||
| 873 | |||
| 874 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) | ||
| 875 | return; | ||
| 876 | |||
| 877 | offsets = new int[2]; | ||
| 878 | builder = new CursorAnchorInfo.Builder (); | ||
| 879 | matrix = new Matrix (window.view.getMatrix ()); | ||
| 880 | window.view.getLocationOnScreen (offsets); | ||
| 881 | matrix.postTranslate (offsets[0], offsets[1]); | ||
| 882 | builder.setMatrix (matrix); | ||
| 883 | builder.setInsertionMarkerLocation (x, y, yBaseline, yBottom, | ||
| 884 | 0); | ||
| 885 | info = builder.build (); | ||
| 886 | |||
| 887 | |||
| 888 | |||
| 889 | if (DEBUG_IC) | ||
| 890 | Log.d (TAG, ("updateCursorAnchorInfo: " + x + " " + y | ||
| 891 | + " " + yBaseline + "-" + yBottom)); | ||
| 892 | |||
| 893 | icBeginSynchronous (); | ||
| 894 | window.view.imManager.updateCursorAnchorInfo (window.view, info); | ||
| 895 | icEndSynchronous (); | ||
| 896 | } | ||
| 897 | |||
| 898 | |||
| 899 | |||
| 900 | /* Content provider functions. */ | ||
| 901 | |||
| 902 | /* Open a content URI described by the bytes BYTES, a non-terminated | ||
| 903 | string; make it writable if WRITABLE, and readable if READABLE. | ||
| 904 | Truncate the file if TRUNCATE. | ||
| 905 | |||
| 906 | Value is the resulting file descriptor or -1 upon failure. */ | ||
| 907 | |||
| 908 | public int | ||
| 909 | openContentUri (byte[] bytes, boolean writable, boolean readable, | ||
| 910 | boolean truncate) | ||
| 911 | { | ||
| 912 | String name, mode; | ||
| 913 | ParcelFileDescriptor fd; | ||
| 914 | int i; | ||
| 915 | |||
| 916 | /* Figure out the file access mode. */ | ||
| 917 | |||
| 918 | mode = ""; | ||
| 919 | |||
| 920 | if (readable) | ||
| 921 | mode += "r"; | ||
| 922 | |||
| 923 | if (writable) | ||
| 924 | mode += "w"; | ||
| 925 | |||
| 926 | if (truncate) | ||
| 927 | mode += "t"; | ||
| 928 | |||
| 929 | /* Try to open an associated ParcelFileDescriptor. */ | ||
| 930 | |||
| 931 | try | ||
| 932 | { | ||
| 933 | /* The usual file name encoding question rears its ugly head | ||
| 934 | again. */ | ||
| 935 | |||
| 936 | name = new String (bytes, "UTF-8"); | ||
| 937 | fd = resolver.openFileDescriptor (Uri.parse (name), mode); | ||
| 938 | |||
| 939 | /* Use detachFd on newer versions of Android or plain old | ||
| 940 | dup. */ | ||
| 941 | |||
| 942 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR1) | ||
| 943 | { | ||
| 944 | i = fd.detachFd (); | ||
| 945 | fd.close (); | ||
| 946 | |||
| 947 | return i; | ||
| 948 | } | ||
| 949 | else | ||
| 950 | { | ||
| 951 | i = EmacsNative.dup (fd.getFd ()); | ||
| 952 | fd.close (); | ||
| 953 | |||
| 954 | return i; | ||
| 955 | } | ||
| 956 | } | ||
| 957 | catch (Exception exception) | ||
| 958 | { | ||
| 959 | return -1; | ||
| 960 | } | ||
| 961 | } | ||
| 962 | |||
| 963 | public boolean | ||
| 964 | checkContentUri (byte[] string, boolean readable, boolean writable) | ||
| 965 | { | ||
| 966 | String mode, name; | ||
| 967 | ParcelFileDescriptor fd; | ||
| 968 | |||
| 969 | /* Decode this into a URI. */ | ||
| 970 | |||
| 971 | try | ||
| 972 | { | ||
| 973 | /* The usual file name encoding question rears its ugly head | ||
| 974 | again. */ | ||
| 975 | name = new String (string, "UTF-8"); | ||
| 976 | } | ||
| 977 | catch (UnsupportedEncodingException exception) | ||
| 978 | { | ||
| 979 | name = null; | ||
| 980 | throw new RuntimeException (exception); | ||
| 981 | } | ||
| 982 | |||
| 983 | mode = "r"; | ||
| 984 | |||
| 985 | if (writable) | ||
| 986 | mode += "w"; | ||
| 987 | |||
| 988 | try | ||
| 989 | { | ||
| 990 | fd = resolver.openFileDescriptor (Uri.parse (name), mode); | ||
| 991 | fd.close (); | ||
| 992 | |||
| 993 | return true; | ||
| 994 | } | ||
| 995 | catch (Exception exception) | ||
| 996 | { | ||
| 997 | /* Fall through. */ | ||
| 998 | } | ||
| 999 | |||
| 1000 | return false; | ||
| 1001 | } | ||
| 1002 | |||
| 1003 | /* Build a content file name for URI. | ||
| 1004 | |||
| 1005 | Return a file name within the /contents/by-authority | ||
| 1006 | pseudo-directory that `android_get_content_name' can then | ||
| 1007 | transform back into an encoded URI. | ||
| 1008 | |||
| 1009 | A content name consists of any number of unencoded path segments | ||
| 1010 | separated by `/' characters, possibly followed by a question mark | ||
| 1011 | and an encoded query string. */ | ||
| 1012 | |||
| 1013 | public static String | ||
| 1014 | buildContentName (Uri uri) | ||
| 1015 | { | ||
| 1016 | StringBuilder builder; | ||
| 1017 | |||
| 1018 | builder = new StringBuilder ("/content/by-authority/"); | ||
| 1019 | builder.append (uri.getAuthority ()); | ||
| 1020 | |||
| 1021 | /* First, append each path segment. */ | ||
| 1022 | |||
| 1023 | for (String segment : uri.getPathSegments ()) | ||
| 1024 | { | ||
| 1025 | /* FIXME: what if segment contains a slash character? */ | ||
| 1026 | builder.append ('/'); | ||
| 1027 | builder.append (uri.encode (segment)); | ||
| 1028 | } | ||
| 1029 | |||
| 1030 | /* Now, append the query string if necessary. */ | ||
| 1031 | |||
| 1032 | if (uri.getEncodedQuery () != null) | ||
| 1033 | builder.append ('?').append (uri.getEncodedQuery ()); | ||
| 1034 | |||
| 1035 | return builder.toString (); | ||
| 1036 | } | ||
| 1037 | |||
| 1038 | |||
| 1039 | |||
| 1040 | private long[] | ||
| 1041 | queryBattery19 () | ||
| 1042 | { | ||
| 1043 | IntentFilter filter; | ||
| 1044 | Intent battery; | ||
| 1045 | long capacity, chargeCounter, currentAvg, currentNow; | ||
| 1046 | long status, remaining, plugged, temp; | ||
| 1047 | |||
| 1048 | filter = new IntentFilter (Intent.ACTION_BATTERY_CHANGED); | ||
| 1049 | battery = registerReceiver (null, filter); | ||
| 1050 | |||
| 1051 | if (battery == null) | ||
| 1052 | return null; | ||
| 1053 | |||
| 1054 | capacity = battery.getIntExtra (BatteryManager.EXTRA_LEVEL, 0); | ||
| 1055 | chargeCounter | ||
| 1056 | = (battery.getIntExtra (BatteryManager.EXTRA_SCALE, 0) | ||
| 1057 | / battery.getIntExtra (BatteryManager.EXTRA_LEVEL, 100) * 100); | ||
| 1058 | currentAvg = 0; | ||
| 1059 | currentNow = 0; | ||
| 1060 | status = battery.getIntExtra (BatteryManager.EXTRA_STATUS, 0); | ||
| 1061 | remaining = -1; | ||
| 1062 | plugged = battery.getIntExtra (BatteryManager.EXTRA_PLUGGED, 0); | ||
| 1063 | temp = battery.getIntExtra (BatteryManager.EXTRA_TEMPERATURE, 0); | ||
| 1064 | |||
| 1065 | return new long[] { capacity, chargeCounter, currentAvg, | ||
| 1066 | currentNow, remaining, status, plugged, | ||
| 1067 | temp, }; | ||
| 1068 | } | ||
| 1069 | |||
| 1070 | /* Return the status of the battery. See struct | ||
| 1071 | android_battery_status for the order of the elements | ||
| 1072 | returned. | ||
| 1073 | |||
| 1074 | Value may be null upon failure. */ | ||
| 1075 | |||
| 1076 | public long[] | ||
| 1077 | queryBattery () | ||
| 1078 | { | ||
| 1079 | Object tem; | ||
| 1080 | BatteryManager manager; | ||
| 1081 | long capacity, chargeCounter, currentAvg, currentNow; | ||
| 1082 | long status, remaining, plugged, temp; | ||
| 1083 | int prop; | ||
| 1084 | IntentFilter filter; | ||
| 1085 | Intent battery; | ||
| 1086 | |||
| 1087 | /* Android 4.4 or earlier require applications to use a different | ||
| 1088 | API to query the battery status. */ | ||
| 1089 | |||
| 1090 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) | ||
| 1091 | return queryBattery19 (); | ||
| 1092 | |||
| 1093 | tem = getSystemService (Context.BATTERY_SERVICE); | ||
| 1094 | manager = (BatteryManager) tem; | ||
| 1095 | remaining = -1; | ||
| 1096 | |||
| 1097 | prop = BatteryManager.BATTERY_PROPERTY_CAPACITY; | ||
| 1098 | capacity = manager.getLongProperty (prop); | ||
| 1099 | prop = BatteryManager.BATTERY_PROPERTY_CHARGE_COUNTER; | ||
| 1100 | chargeCounter = manager.getLongProperty (prop); | ||
| 1101 | prop = BatteryManager.BATTERY_PROPERTY_CURRENT_AVERAGE; | ||
| 1102 | currentAvg = manager.getLongProperty (prop); | ||
| 1103 | prop = BatteryManager.BATTERY_PROPERTY_CURRENT_NOW; | ||
| 1104 | currentNow = manager.getLongProperty (prop); | ||
| 1105 | |||
| 1106 | /* Return the battery status. N.B. that Android 7.1 and earlier | ||
| 1107 | only return ``charging'' or ``discharging''. */ | ||
| 1108 | |||
| 1109 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) | ||
| 1110 | status | ||
| 1111 | = manager.getIntProperty (BatteryManager.BATTERY_PROPERTY_STATUS); | ||
| 1112 | else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) | ||
| 1113 | status = (manager.isCharging () | ||
| 1114 | ? BatteryManager.BATTERY_STATUS_CHARGING | ||
| 1115 | : BatteryManager.BATTERY_STATUS_DISCHARGING); | ||
| 1116 | else | ||
| 1117 | status = (currentNow > 0 | ||
| 1118 | ? BatteryManager.BATTERY_STATUS_CHARGING | ||
| 1119 | : BatteryManager.BATTERY_STATUS_DISCHARGING); | ||
| 1120 | |||
| 1121 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) | ||
| 1122 | remaining = manager.computeChargeTimeRemaining (); | ||
| 1123 | |||
| 1124 | plugged = -1; | ||
| 1125 | temp = -1; | ||
| 1126 | |||
| 1127 | /* Now obtain additional information from the battery manager. */ | ||
| 1128 | |||
| 1129 | filter = new IntentFilter (Intent.ACTION_BATTERY_CHANGED); | ||
| 1130 | battery = registerReceiver (null, filter); | ||
| 1131 | |||
| 1132 | if (battery != null) | ||
| 1133 | { | ||
| 1134 | plugged = battery.getIntExtra (BatteryManager.EXTRA_PLUGGED, 0); | ||
| 1135 | temp = battery.getIntExtra (BatteryManager.EXTRA_TEMPERATURE, 0); | ||
| 1136 | |||
| 1137 | /* Make status more reliable. */ | ||
| 1138 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M) | ||
| 1139 | status = battery.getIntExtra (BatteryManager.EXTRA_STATUS, 0); | ||
| 1140 | } | ||
| 1141 | |||
| 1142 | return new long[] { capacity, chargeCounter, currentAvg, | ||
| 1143 | currentNow, remaining, status, plugged, | ||
| 1144 | temp, }; | ||
| 1145 | } | ||
| 1146 | |||
| 1147 | public void | ||
| 1148 | updateExtractedText (EmacsWindow window, ExtractedText text, | ||
| 1149 | int token) | ||
| 1150 | { | ||
| 1151 | if (DEBUG_IC) | ||
| 1152 | Log.d (TAG, "updateExtractedText: @" + token + ", " + text); | ||
| 1153 | |||
| 1154 | window.view.imManager.updateExtractedText (window.view, | ||
| 1155 | token, text); | ||
| 1156 | } | ||
| 1157 | |||
| 1158 | |||
| 1159 | |||
| 1160 | /* Document tree management functions. These functions shouldn't be | ||
| 1161 | called before Android 5.0. */ | ||
| 1162 | |||
| 1163 | /* Return an array of each document authority providing at least one | ||
| 1164 | tree URI that Emacs holds the rights to persistently access. */ | ||
| 1165 | |||
| 1166 | public String[] | ||
| 1167 | getDocumentAuthorities () | ||
| 1168 | { | ||
| 1169 | List<UriPermission> permissions; | ||
| 1170 | HashSet<String> allProviders; | ||
| 1171 | Uri uri; | ||
| 1172 | |||
| 1173 | permissions = resolver.getPersistedUriPermissions (); | ||
| 1174 | allProviders = new HashSet<String> (); | ||
| 1175 | |||
| 1176 | for (UriPermission permission : permissions) | ||
| 1177 | { | ||
| 1178 | uri = permission.getUri (); | ||
| 1179 | |||
| 1180 | if (DocumentsContract.isTreeUri (uri) | ||
| 1181 | && permission.isReadPermission ()) | ||
| 1182 | allProviders.add (uri.getAuthority ()); | ||
| 1183 | } | ||
| 1184 | |||
| 1185 | return allProviders.toArray (new String[0]); | ||
| 1186 | } | ||
| 1187 | |||
| 1188 | /* Start a file chooser activity to request access to a directory | ||
| 1189 | tree. | ||
| 1190 | |||
| 1191 | Value is 1 if the activity couldn't be started for some reason, | ||
| 1192 | and 0 in any other case. */ | ||
| 1193 | |||
| 1194 | public int | ||
| 1195 | requestDirectoryAccess () | ||
| 1196 | { | ||
| 1197 | Runnable runnable; | ||
| 1198 | final EmacsHolder<Integer> rc; | ||
| 1199 | |||
| 1200 | /* Return 1 if Android is too old to support this feature. */ | ||
| 1201 | |||
| 1202 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) | ||
| 1203 | return 1; | ||
| 1204 | |||
| 1205 | rc = new EmacsHolder<Integer> (); | ||
| 1206 | rc.thing = Integer.valueOf (1); | ||
| 1207 | |||
| 1208 | runnable = new Runnable () { | ||
| 1209 | @Override | ||
| 1210 | public void | ||
| 1211 | run () | ||
| 1212 | { | ||
| 1213 | EmacsActivity activity; | ||
| 1214 | Intent intent; | ||
| 1215 | int id; | ||
| 1216 | |||
| 1217 | synchronized (this) | ||
| 1218 | { | ||
| 1219 | /* Try to obtain an activity that will receive the | ||
| 1220 | response from the file chooser dialog. */ | ||
| 1221 | |||
| 1222 | if (EmacsActivity.focusedActivities.isEmpty ()) | ||
| 1223 | { | ||
| 1224 | /* If focusedActivities is empty then this dialog | ||
| 1225 | may have been displayed immediately after another | ||
| 1226 | popup dialog was dismissed. Try the | ||
| 1227 | EmacsActivity to be focused. */ | ||
| 1228 | |||
| 1229 | activity = EmacsActivity.lastFocusedActivity; | ||
| 1230 | |||
| 1231 | if (activity == null) | ||
| 1232 | { | ||
| 1233 | /* Still no luck. Return failure. */ | ||
| 1234 | notify (); | ||
| 1235 | return; | ||
| 1236 | } | ||
| 1237 | } | ||
| 1238 | else | ||
| 1239 | activity = EmacsActivity.focusedActivities.get (0); | ||
| 1240 | |||
| 1241 | /* Now create the intent. */ | ||
| 1242 | intent = new Intent (Intent.ACTION_OPEN_DOCUMENT_TREE); | ||
| 1243 | |||
| 1244 | try | ||
| 1245 | { | ||
| 1246 | id = EmacsActivity.ACCEPT_DOCUMENT_TREE; | ||
| 1247 | activity.startActivityForResult (intent, id, null); | ||
| 1248 | rc.thing = Integer.valueOf (0); | ||
| 1249 | } | ||
| 1250 | catch (Exception e) | ||
| 1251 | { | ||
| 1252 | e.printStackTrace (); | ||
| 1253 | } | ||
| 1254 | |||
| 1255 | notify (); | ||
| 1256 | } | ||
| 1257 | } | ||
| 1258 | }; | ||
| 1259 | |||
| 1260 | syncRunnable (runnable); | ||
| 1261 | return rc.thing; | ||
| 1262 | } | ||
| 1263 | |||
| 1264 | /* Return an array of each tree provided by the document PROVIDER | ||
| 1265 | that Emacs has permission to access. | ||
| 1266 | |||
| 1267 | Value is an array if the provider really does exist, NULL | ||
| 1268 | otherwise. */ | ||
| 1269 | |||
| 1270 | public String[] | ||
| 1271 | getDocumentTrees (byte provider[]) | ||
| 1272 | { | ||
| 1273 | String providerName; | ||
| 1274 | List<String> treeList; | ||
| 1275 | List<UriPermission> permissions; | ||
| 1276 | Uri uri; | ||
| 1277 | |||
| 1278 | try | ||
| 1279 | { | ||
| 1280 | providerName = new String (provider, "US-ASCII"); | ||
| 1281 | } | ||
| 1282 | catch (UnsupportedEncodingException exception) | ||
| 1283 | { | ||
| 1284 | return null; | ||
| 1285 | } | ||
| 1286 | |||
| 1287 | permissions = resolver.getPersistedUriPermissions (); | ||
| 1288 | treeList = new ArrayList<String> (); | ||
| 1289 | |||
| 1290 | for (UriPermission permission : permissions) | ||
| 1291 | { | ||
| 1292 | uri = permission.getUri (); | ||
| 1293 | |||
| 1294 | if (DocumentsContract.isTreeUri (uri) | ||
| 1295 | && uri.getAuthority ().equals (providerName) | ||
| 1296 | && permission.isReadPermission ()) | ||
| 1297 | /* Make sure the tree document ID is encoded. Refrain from | ||
| 1298 | encoding characters such as +:&?#, since they don't | ||
| 1299 | conflict with file name separators or other special | ||
| 1300 | characters. */ | ||
| 1301 | treeList.add (Uri.encode (DocumentsContract.getTreeDocumentId (uri), | ||
| 1302 | " +:&?#")); | ||
| 1303 | } | ||
| 1304 | |||
| 1305 | return treeList.toArray (new String[0]); | ||
| 1306 | } | ||
| 1307 | |||
| 1308 | /* Find the document ID of the file within TREE_URI designated by | ||
| 1309 | NAME. | ||
| 1310 | |||
| 1311 | NAME is a ``file name'' comprised of the display names of | ||
| 1312 | individual files. Each constituent component prior to the last | ||
| 1313 | must name a directory file within TREE_URI. | ||
| 1314 | |||
| 1315 | Upon success, return 0 or 1 (contingent upon whether or not the | ||
| 1316 | last component within NAME is a directory) and place the document | ||
| 1317 | ID of the named file in ID_RETURN[0]. | ||
| 1318 | |||
| 1319 | If the designated file can't be located, but each component of | ||
| 1320 | NAME up to the last component can and is a directory, return -2 | ||
| 1321 | and the ID of the last component located in ID_RETURN[0]. | ||
| 1322 | |||
| 1323 | If the designated file can't be located, return -1, or signal one | ||
| 1324 | of OperationCanceledException, SecurityException, | ||
| 1325 | FileNotFoundException, or UnsupportedOperationException. */ | ||
| 1326 | |||
| 1327 | private int | ||
| 1328 | documentIdFromName (String tree_uri, String name, String[] id_return) | ||
| 1329 | { | ||
| 1330 | /* Start the thread used to run SAF requests if it isn't already | ||
| 1331 | running. */ | ||
| 1332 | |||
| 1333 | if (storageThread == null) | ||
| 1334 | { | ||
| 1335 | storageThread = new EmacsSafThread (resolver); | ||
| 1336 | storageThread.start (); | ||
| 1337 | } | ||
| 1338 | |||
| 1339 | return storageThread.documentIdFromName (tree_uri, name, | ||
| 1340 | id_return); | ||
| 1341 | } | ||
| 1342 | |||
| 1343 | /* Return an encoded document URI representing a tree with the | ||
| 1344 | specified IDENTIFIER supplied by the authority AUTHORITY. | ||
| 1345 | |||
| 1346 | Return null instead if Emacs does not have permanent access | ||
| 1347 | to the specified document tree recorded on disk. */ | ||
| 1348 | |||
| 1349 | public String | ||
| 1350 | getTreeUri (String tree, String authority) | ||
| 1351 | { | ||
| 1352 | Uri uri, grantedUri; | ||
| 1353 | List<UriPermission> permissions; | ||
| 1354 | |||
| 1355 | /* First, build the URI. */ | ||
| 1356 | tree = Uri.decode (tree); | ||
| 1357 | uri = DocumentsContract.buildTreeDocumentUri (authority, tree); | ||
| 1358 | |||
| 1359 | /* Now, search for it within the list of persisted URI | ||
| 1360 | permissions. */ | ||
| 1361 | permissions = resolver.getPersistedUriPermissions (); | ||
| 1362 | |||
| 1363 | for (UriPermission permission : permissions) | ||
| 1364 | { | ||
| 1365 | /* If the permission doesn't entitle Emacs to read access, | ||
| 1366 | skip it. */ | ||
| 1367 | |||
| 1368 | if (!permission.isReadPermission ()) | ||
| 1369 | continue; | ||
| 1370 | |||
| 1371 | grantedUri = permission.getUri (); | ||
| 1372 | |||
| 1373 | if (grantedUri.equals (uri)) | ||
| 1374 | return uri.toString (); | ||
| 1375 | } | ||
| 1376 | |||
| 1377 | /* Emacs doesn't have permission to access this tree URI. */ | ||
| 1378 | return null; | ||
| 1379 | } | ||
| 1380 | |||
| 1381 | /* Return file status for the document designated by the given | ||
| 1382 | DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document | ||
| 1383 | ID in URI itself. | ||
| 1384 | |||
| 1385 | Value is null upon failure, or an array of longs [MODE, SIZE, | ||
| 1386 | MTIM] upon success, where MODE contains the file type and access | ||
| 1387 | modes of the file as in `struct stat', SIZE is the size of the | ||
| 1388 | file in BYTES or -1 if not known, and MTIM is the time of the | ||
| 1389 | last modification to this file in milliseconds since 00:00, | ||
| 1390 | January 1st, 1970. | ||
| 1391 | |||
| 1392 | OperationCanceledException and other typical exceptions may be | ||
| 1393 | signaled upon receiving async input or other errors. */ | ||
| 1394 | |||
| 1395 | public long[] | ||
| 1396 | statDocument (String uri, String documentId) | ||
| 1397 | { | ||
| 1398 | /* Start the thread used to run SAF requests if it isn't already | ||
| 1399 | running. */ | ||
| 1400 | |||
| 1401 | if (storageThread == null) | ||
| 1402 | { | ||
| 1403 | storageThread = new EmacsSafThread (resolver); | ||
| 1404 | storageThread.start (); | ||
| 1405 | } | ||
| 1406 | |||
| 1407 | return storageThread.statDocument (uri, documentId); | ||
| 1408 | } | ||
| 1409 | |||
| 1410 | /* Find out whether Emacs has access to the document designated by | ||
| 1411 | the specified DOCUMENTID within the tree URI. If DOCUMENTID is | ||
| 1412 | NULL, use the document ID in URI itself. | ||
| 1413 | |||
| 1414 | If WRITABLE, also check that the file is writable, which is true | ||
| 1415 | if it is either a directory or its flags contains | ||
| 1416 | FLAG_SUPPORTS_WRITE. | ||
| 1417 | |||
| 1418 | Value is 0 if the file is accessible, and one of the following if | ||
| 1419 | not: | ||
| 1420 | |||
| 1421 | -1, if the file does not exist. | ||
| 1422 | -2, if WRITABLE and the file is not writable. | ||
| 1423 | -3, upon any other error. | ||
| 1424 | |||
| 1425 | In addition, arbitrary runtime exceptions (such as | ||
| 1426 | SecurityException or UnsupportedOperationException) may be | ||
| 1427 | thrown. */ | ||
| 1428 | |||
| 1429 | public int | ||
| 1430 | accessDocument (String uri, String documentId, boolean writable) | ||
| 1431 | { | ||
| 1432 | /* Start the thread used to run SAF requests if it isn't already | ||
| 1433 | running. */ | ||
| 1434 | |||
| 1435 | if (storageThread == null) | ||
| 1436 | { | ||
| 1437 | storageThread = new EmacsSafThread (resolver); | ||
| 1438 | storageThread.start (); | ||
| 1439 | } | ||
| 1440 | |||
| 1441 | return storageThread.accessDocument (uri, documentId, writable); | ||
| 1442 | } | ||
| 1443 | |||
| 1444 | /* Open a cursor representing each entry within the directory | ||
| 1445 | designated by the specified DOCUMENTID within the tree URI. | ||
| 1446 | |||
| 1447 | If DOCUMENTID is NULL, use the document ID within URI itself. | ||
| 1448 | Value is NULL upon failure. | ||
| 1449 | |||
| 1450 | In addition, arbitrary runtime exceptions (such as | ||
| 1451 | SecurityException or UnsupportedOperationException) may be | ||
| 1452 | thrown. */ | ||
| 1453 | |||
| 1454 | public Cursor | ||
| 1455 | openDocumentDirectory (String uri, String documentId) | ||
| 1456 | { | ||
| 1457 | /* Start the thread used to run SAF requests if it isn't already | ||
| 1458 | running. */ | ||
| 1459 | |||
| 1460 | if (storageThread == null) | ||
| 1461 | { | ||
| 1462 | storageThread = new EmacsSafThread (resolver); | ||
| 1463 | storageThread.start (); | ||
| 1464 | } | ||
| 1465 | |||
| 1466 | return storageThread.openDocumentDirectory (uri, documentId); | ||
| 1467 | } | ||
| 1468 | |||
| 1469 | /* Read a single directory entry from the specified CURSOR. Return | ||
| 1470 | NULL if at the end of the directory stream, and a directory entry | ||
| 1471 | with `d_name' set to NULL if an error occurs. */ | ||
| 1472 | |||
| 1473 | public EmacsDirectoryEntry | ||
| 1474 | readDirectoryEntry (Cursor cursor) | ||
| 1475 | { | ||
| 1476 | EmacsDirectoryEntry entry; | ||
| 1477 | int index; | ||
| 1478 | String name, type; | ||
| 1479 | |||
| 1480 | entry = new EmacsDirectoryEntry (); | ||
| 1481 | |||
| 1482 | while (true) | ||
| 1483 | { | ||
| 1484 | if (!cursor.moveToNext ()) | ||
| 1485 | return null; | ||
| 1486 | |||
| 1487 | /* First, retrieve the display name. */ | ||
| 1488 | index = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME); | ||
| 1489 | |||
| 1490 | if (index < 0) | ||
| 1491 | /* Return an invalid directory entry upon failure. */ | ||
| 1492 | return entry; | ||
| 1493 | |||
| 1494 | try | ||
| 1495 | { | ||
| 1496 | name = cursor.getString (index); | ||
| 1497 | } | ||
| 1498 | catch (Exception exception) | ||
| 1499 | { | ||
| 1500 | return entry; | ||
| 1501 | } | ||
| 1502 | |||
| 1503 | /* Skip this entry if its name cannot be represented. */ | ||
| 1504 | |||
| 1505 | if (name.equals ("..") || name.equals (".") || name.contains ("/")) | ||
| 1506 | continue; | ||
| 1507 | |||
| 1508 | /* Now, look for its type. */ | ||
| 1509 | |||
| 1510 | index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); | ||
| 1511 | |||
| 1512 | if (index < 0) | ||
| 1513 | /* Return an invalid directory entry upon failure. */ | ||
| 1514 | return entry; | ||
| 1515 | |||
| 1516 | try | ||
| 1517 | { | ||
| 1518 | type = cursor.getString (index); | ||
| 1519 | } | ||
| 1520 | catch (Exception exception) | ||
| 1521 | { | ||
| 1522 | return entry; | ||
| 1523 | } | ||
| 1524 | |||
| 1525 | if (type != null | ||
| 1526 | && type.equals (Document.MIME_TYPE_DIR)) | ||
| 1527 | entry.d_type = 1; | ||
| 1528 | entry.d_name = name; | ||
| 1529 | return entry; | ||
| 1530 | } | ||
| 1531 | |||
| 1532 | /* Not reached. */ | ||
| 1533 | } | ||
| 1534 | |||
| 1535 | /* Open a file descriptor for a file document designated by | ||
| 1536 | DOCUMENTID within the document tree identified by URI. If | ||
| 1537 | TRUNCATE and the document already exists, truncate its contents | ||
| 1538 | before returning. | ||
| 1539 | |||
| 1540 | On Android 9.0 and earlier, always open the document in | ||
| 1541 | ``read-write'' mode; this instructs the document provider to | ||
| 1542 | return a seekable file that is stored on disk and returns correct | ||
| 1543 | file status. | ||
| 1544 | |||
| 1545 | Under newer versions of Android, open the document in a | ||
| 1546 | non-writable mode if WRITE is false. This is possible because | ||
| 1547 | these versions allow Emacs to explicitly request a seekable | ||
| 1548 | on-disk file. | ||
| 1549 | |||
| 1550 | Value is NULL upon failure or a parcel file descriptor upon | ||
| 1551 | success. Call `ParcelFileDescriptor.close' on this file | ||
| 1552 | descriptor instead of using the `close' system call. | ||
| 1553 | |||
| 1554 | FileNotFoundException and/or SecurityException and | ||
| 1555 | UnsupportedOperationException may be thrown upon failure. */ | ||
| 1556 | |||
| 1557 | public ParcelFileDescriptor | ||
| 1558 | openDocument (String uri, String documentId, boolean write, | ||
| 1559 | boolean truncate) | ||
| 1560 | { | ||
| 1561 | /* Start the thread used to run SAF requests if it isn't already | ||
| 1562 | running. */ | ||
| 1563 | |||
| 1564 | if (storageThread == null) | ||
| 1565 | { | ||
| 1566 | storageThread = new EmacsSafThread (resolver); | ||
| 1567 | storageThread.start (); | ||
| 1568 | } | ||
| 1569 | |||
| 1570 | return storageThread.openDocument (uri, documentId, write, | ||
| 1571 | truncate); | ||
| 1572 | } | ||
| 1573 | |||
| 1574 | /* Create a new document with the given display NAME within the | ||
| 1575 | directory identified by DOCUMENTID inside the document tree | ||
| 1576 | designated by URI. | ||
| 1577 | |||
| 1578 | If DOCUMENTID is NULL, create the document inside the root of | ||
| 1579 | that tree. | ||
| 1580 | |||
| 1581 | Either FileNotFoundException, SecurityException or | ||
| 1582 | UnsupportedOperationException may be thrown upon failure. | ||
| 1583 | |||
| 1584 | Return the document ID of the new file upon success, NULL | ||
| 1585 | otherwise. */ | ||
| 1586 | |||
| 1587 | public String | ||
| 1588 | createDocument (String uri, String documentId, String name) | ||
| 1589 | throws FileNotFoundException | ||
| 1590 | { | ||
| 1591 | String mimeType, separator, mime, extension; | ||
| 1592 | int index; | ||
| 1593 | MimeTypeMap singleton; | ||
| 1594 | Uri treeUri, directoryUri, docUri; | ||
| 1595 | |||
| 1596 | /* Try to get the MIME type for this document. | ||
| 1597 | Default to ``application/octet-stream''. */ | ||
| 1598 | |||
| 1599 | mimeType = "application/octet-stream"; | ||
| 1600 | |||
| 1601 | /* Abuse WebView stuff to get the file's MIME type. */ | ||
| 1602 | |||
| 1603 | index = name.lastIndexOf ('.'); | ||
| 1604 | |||
| 1605 | if (index > 0) | ||
| 1606 | { | ||
| 1607 | singleton = MimeTypeMap.getSingleton (); | ||
| 1608 | extension = name.substring (index + 1); | ||
| 1609 | mime = singleton.getMimeTypeFromExtension (extension); | ||
| 1610 | |||
| 1611 | if (mime != null) | ||
| 1612 | mimeType = mime; | ||
| 1613 | } | ||
| 1614 | |||
| 1615 | /* Now parse URI. */ | ||
| 1616 | treeUri = Uri.parse (uri); | ||
| 1617 | |||
| 1618 | if (documentId == null) | ||
| 1619 | documentId = DocumentsContract.getTreeDocumentId (treeUri); | ||
| 1620 | |||
| 1621 | /* And build a file URI referring to the directory. */ | ||
| 1622 | |||
| 1623 | directoryUri | ||
| 1624 | = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri, | ||
| 1625 | documentId); | ||
| 1626 | |||
| 1627 | docUri = DocumentsContract.createDocument (resolver, | ||
| 1628 | directoryUri, | ||
| 1629 | mimeType, name); | ||
| 1630 | |||
| 1631 | if (docUri == null) | ||
| 1632 | return null; | ||
| 1633 | |||
| 1634 | /* Invalidate the file status of the containing directory. */ | ||
| 1635 | |||
| 1636 | if (storageThread != null) | ||
| 1637 | storageThread.postInvalidateStat (treeUri, documentId); | ||
| 1638 | |||
| 1639 | /* Return the ID of the new document. */ | ||
| 1640 | return DocumentsContract.getDocumentId (docUri); | ||
| 1641 | } | ||
| 1642 | |||
| 1643 | /* Like `createDocument', but create a directory instead of an | ||
| 1644 | ordinary document. */ | ||
| 1645 | |||
| 1646 | public String | ||
| 1647 | createDirectory (String uri, String documentId, String name) | ||
| 1648 | throws FileNotFoundException | ||
| 1649 | { | ||
| 1650 | int index; | ||
| 1651 | Uri treeUri, directoryUri, docUri; | ||
| 1652 | |||
| 1653 | /* Now parse URI. */ | ||
| 1654 | treeUri = Uri.parse (uri); | ||
| 1655 | |||
| 1656 | if (documentId == null) | ||
| 1657 | documentId = DocumentsContract.getTreeDocumentId (treeUri); | ||
| 1658 | |||
| 1659 | /* And build a file URI referring to the directory. */ | ||
| 1660 | |||
| 1661 | directoryUri | ||
| 1662 | = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri, | ||
| 1663 | documentId); | ||
| 1664 | |||
| 1665 | /* If name ends with a directory separator character, delete | ||
| 1666 | it. */ | ||
| 1667 | |||
| 1668 | if (name.endsWith ("/")) | ||
| 1669 | name = name.substring (0, name.length () - 1); | ||
| 1670 | |||
| 1671 | /* From Android's perspective, directories are just ordinary | ||
| 1672 | documents with the `MIME_TYPE_DIR' type. */ | ||
| 1673 | |||
| 1674 | docUri = DocumentsContract.createDocument (resolver, | ||
| 1675 | directoryUri, | ||
| 1676 | Document.MIME_TYPE_DIR, | ||
| 1677 | name); | ||
| 1678 | |||
| 1679 | if (docUri == null) | ||
| 1680 | return null; | ||
| 1681 | |||
| 1682 | /* Return the ID of the new document, but first invalidate the | ||
| 1683 | state of the containing directory. */ | ||
| 1684 | |||
| 1685 | if (storageThread != null) | ||
| 1686 | storageThread.postInvalidateStat (treeUri, documentId); | ||
| 1687 | |||
| 1688 | return DocumentsContract.getDocumentId (docUri); | ||
| 1689 | } | ||
| 1690 | |||
| 1691 | /* Delete the document identified by ID from the document tree | ||
| 1692 | identified by URI. Return 0 upon success and -1 upon | ||
| 1693 | failure. | ||
| 1694 | |||
| 1695 | NAME should be the name of the document being deleted, and is | ||
| 1696 | used to invalidate the cache. */ | ||
| 1697 | |||
| 1698 | public int | ||
| 1699 | deleteDocument (String uri, String id, String name) | ||
| 1700 | throws FileNotFoundException | ||
| 1701 | { | ||
| 1702 | Uri uriObject, tree; | ||
| 1703 | |||
| 1704 | tree = Uri.parse (uri); | ||
| 1705 | uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, id); | ||
| 1706 | |||
| 1707 | if (DocumentsContract.deleteDocument (resolver, uriObject)) | ||
| 1708 | { | ||
| 1709 | if (storageThread != null) | ||
| 1710 | storageThread.postInvalidateCache (tree, id, name); | ||
| 1711 | |||
| 1712 | return 0; | ||
| 1713 | } | ||
| 1714 | |||
| 1715 | return -1; | ||
| 1716 | } | ||
| 1717 | |||
| 1718 | /* Rename the document designated by DOCID inside the directory tree | ||
| 1719 | identified by URI, which should be within the directory | ||
| 1720 | designated by DIR, to NAME. If the file can't be renamed because | ||
| 1721 | it doesn't support renaming, return -1, 0 otherwise. */ | ||
| 1722 | |||
| 1723 | public int | ||
| 1724 | renameDocument (String uri, String docId, String dir, String name) | ||
| 1725 | throws FileNotFoundException | ||
| 1726 | { | ||
| 1727 | Uri tree, uriObject; | ||
| 1728 | |||
| 1729 | tree = Uri.parse (uri); | ||
| 1730 | uriObject = DocumentsContract.buildDocumentUriUsingTree (tree, docId); | ||
| 1731 | |||
| 1732 | if (DocumentsContract.renameDocument (resolver, uriObject, | ||
| 1733 | name) | ||
| 1734 | != null) | ||
| 1735 | { | ||
| 1736 | /* Invalidate the cache. */ | ||
| 1737 | if (storageThread != null) | ||
| 1738 | storageThread.postInvalidateCacheDir (tree, docId, | ||
| 1739 | name); | ||
| 1740 | return 0; | ||
| 1741 | } | ||
| 1742 | |||
| 1743 | /* Handle errors specially, so `android_saf_rename_document' can | ||
| 1744 | return ENXDEV. */ | ||
| 1745 | return -1; | ||
| 1746 | } | ||
| 1747 | |||
| 1748 | /* Move the document designated by DOCID from the directory under | ||
| 1749 | DIR_NAME designated by SRCID to the directory designated by | ||
| 1750 | DSTID. If the ID of the document being moved changes as a | ||
| 1751 | consequence of the movement, return the new ID, else NULL. | ||
| 1752 | |||
| 1753 | URI is the document tree containing all three documents. */ | ||
| 1754 | |||
| 1755 | public String | ||
| 1756 | moveDocument (String uri, String docId, String dirName, | ||
| 1757 | String dstId, String srcId) | ||
| 1758 | throws FileNotFoundException | ||
| 1759 | { | ||
| 1760 | Uri uri1, docId1, dstId1, srcId1; | ||
| 1761 | Uri name; | ||
| 1762 | |||
| 1763 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) | ||
| 1764 | throw new UnsupportedOperationException ("Documents aren't capable" | ||
| 1765 | + " of being moved on Android" | ||
| 1766 | + " versions before 7.0."); | ||
| 1767 | |||
| 1768 | uri1 = Uri.parse (uri); | ||
| 1769 | docId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, docId); | ||
| 1770 | dstId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, dstId); | ||
| 1771 | srcId1 = DocumentsContract.buildDocumentUriUsingTree (uri1, srcId); | ||
| 1772 | |||
| 1773 | /* Move the document; this function returns the new ID of the | ||
| 1774 | document should it change. */ | ||
| 1775 | name = DocumentsContract.moveDocument (resolver, docId1, | ||
| 1776 | srcId1, dstId1); | ||
| 1777 | |||
| 1778 | /* Now invalidate the caches for both DIRNAME and DOCID. */ | ||
| 1779 | |||
| 1780 | if (storageThread != null) | ||
| 1781 | { | ||
| 1782 | storageThread.postInvalidateCacheDir (uri1, docId, dirName); | ||
| 1783 | |||
| 1784 | /* Invalidate the stat cache entries for both the source and | ||
| 1785 | destination directories, since their contents have | ||
| 1786 | changed. */ | ||
| 1787 | storageThread.postInvalidateStat (uri1, dstId); | ||
| 1788 | storageThread.postInvalidateStat (uri1, srcId); | ||
| 1789 | } | ||
| 1790 | |||
| 1791 | return (name != null | ||
| 1792 | ? DocumentsContract.getDocumentId (name) | ||
| 1793 | : null); | ||
| 1794 | } | ||
| 1795 | |||
| 1796 | /* Return if there is a content provider by the name of AUTHORITY | ||
| 1797 | supplying at least one tree URI Emacs retains persistent rights | ||
| 1798 | to access. */ | ||
| 1799 | |||
| 1800 | public boolean | ||
| 1801 | validAuthority (String authority) | ||
| 1802 | { | ||
| 1803 | List<UriPermission> permissions; | ||
| 1804 | Uri uri; | ||
| 1805 | |||
| 1806 | permissions = resolver.getPersistedUriPermissions (); | ||
| 1807 | |||
| 1808 | for (UriPermission permission : permissions) | ||
| 1809 | { | ||
| 1810 | uri = permission.getUri (); | ||
| 1811 | |||
| 1812 | if (DocumentsContract.isTreeUri (uri) | ||
| 1813 | && permission.isReadPermission () | ||
| 1814 | && uri.getAuthority ().equals (authority)) | ||
| 1815 | return true; | ||
| 1816 | } | ||
| 1817 | |||
| 1818 | return false; | ||
| 1819 | } | ||
| 1820 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsSurfaceView.java b/java/org/gnu/emacs/EmacsSurfaceView.java new file mode 100644 index 00000000000..c47696b35c0 --- /dev/null +++ b/java/org/gnu/emacs/EmacsSurfaceView.java | |||
| @@ -0,0 +1,223 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.view.View; | ||
| 23 | |||
| 24 | import android.os.Build; | ||
| 25 | |||
| 26 | import android.graphics.Bitmap; | ||
| 27 | import android.graphics.Canvas; | ||
| 28 | import android.graphics.Rect; | ||
| 29 | import android.graphics.Paint; | ||
| 30 | |||
| 31 | import java.lang.ref.WeakReference; | ||
| 32 | |||
| 33 | /* This originally extended SurfaceView. However, doing so proved to | ||
| 34 | be too slow, and Android's surface view keeps up to three of its | ||
| 35 | own back buffers, which use too much memory (up to 96 MB for a | ||
| 36 | single frame.) */ | ||
| 37 | |||
| 38 | public final class EmacsSurfaceView extends View | ||
| 39 | { | ||
| 40 | private static final String TAG = "EmacsSurfaceView"; | ||
| 41 | |||
| 42 | /* The complete buffer contents at the time of the last draw. */ | ||
| 43 | private Bitmap frontBuffer; | ||
| 44 | |||
| 45 | /* Whether frontBuffer has been updated since the last call to | ||
| 46 | `onDraw'. */ | ||
| 47 | private boolean bitmapChanged; | ||
| 48 | |||
| 49 | /* Canvas representing the front buffer. */ | ||
| 50 | private Canvas bitmapCanvas; | ||
| 51 | |||
| 52 | /* Reference to the last bitmap copied to the front buffer. */ | ||
| 53 | private WeakReference<Bitmap> bitmap; | ||
| 54 | |||
| 55 | /* Paint objects used on the main and UI threads, respectively. */ | ||
| 56 | private static final Paint bitmapPaint, uiThreadPaint; | ||
| 57 | |||
| 58 | static | ||
| 59 | { | ||
| 60 | /* Create two different Paint objects; one is used on the main | ||
| 61 | thread for buffer swaps, while the other is used from the UI | ||
| 62 | thread in `onDraw'. This is necessary because Paint objects | ||
| 63 | are not thread-safe, even if their uses are interlocked. */ | ||
| 64 | |||
| 65 | bitmapPaint = new Paint (); | ||
| 66 | uiThreadPaint = new Paint (); | ||
| 67 | }; | ||
| 68 | |||
| 69 | public | ||
| 70 | EmacsSurfaceView (EmacsView view) | ||
| 71 | { | ||
| 72 | super (view.getContext ()); | ||
| 73 | |||
| 74 | this.bitmap = new WeakReference<Bitmap> (null); | ||
| 75 | } | ||
| 76 | |||
| 77 | private void | ||
| 78 | copyToFrontBuffer (Bitmap bitmap, Rect damageRect) | ||
| 79 | { | ||
| 80 | EmacsService.checkEmacsThread (); | ||
| 81 | |||
| 82 | if (Build.VERSION.SDK_INT != Build.VERSION_CODES.O | ||
| 83 | && Build.VERSION.SDK_INT != Build.VERSION_CODES.O_MR1 | ||
| 84 | && Build.VERSION.SDK_INT != Build.VERSION_CODES.N_MR1 | ||
| 85 | && Build.VERSION.SDK_INT != Build.VERSION_CODES.N) | ||
| 86 | { | ||
| 87 | /* If `drawBitmap' can safely be used while a bitmap is locked | ||
| 88 | by another thread, continue here... */ | ||
| 89 | |||
| 90 | if (damageRect != null) | ||
| 91 | bitmapCanvas.drawBitmap (bitmap, damageRect, damageRect, | ||
| 92 | bitmapPaint); | ||
| 93 | else | ||
| 94 | bitmapCanvas.drawBitmap (bitmap, 0f, 0f, bitmapPaint); | ||
| 95 | } | ||
| 96 | else | ||
| 97 | { | ||
| 98 | /* But if it can not, as on Android 7.0 through 8.1, then use | ||
| 99 | a replacement function. */ | ||
| 100 | |||
| 101 | if (damageRect != null) | ||
| 102 | EmacsNative.blitRect (bitmap, frontBuffer, | ||
| 103 | damageRect.left, | ||
| 104 | damageRect.top, | ||
| 105 | damageRect.right, | ||
| 106 | damageRect.bottom); | ||
| 107 | else | ||
| 108 | EmacsNative.blitRect (bitmap, frontBuffer, 0, 0, | ||
| 109 | bitmap.getWidth (), | ||
| 110 | bitmap.getHeight ()); | ||
| 111 | } | ||
| 112 | |||
| 113 | /* See the large comment inside `onDraw'. */ | ||
| 114 | bitmapChanged = true; | ||
| 115 | } | ||
| 116 | |||
| 117 | private void | ||
| 118 | reconfigureFrontBuffer (Bitmap bitmap) | ||
| 119 | { | ||
| 120 | /* First, remove the old front buffer. */ | ||
| 121 | |||
| 122 | if (frontBuffer != null) | ||
| 123 | { | ||
| 124 | frontBuffer.recycle (); | ||
| 125 | frontBuffer = null; | ||
| 126 | bitmapCanvas = null; | ||
| 127 | } | ||
| 128 | |||
| 129 | this.bitmap = new WeakReference<Bitmap> (bitmap); | ||
| 130 | |||
| 131 | /* Next, create the new front buffer if necessary. */ | ||
| 132 | |||
| 133 | if (bitmap != null && frontBuffer == null) | ||
| 134 | { | ||
| 135 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) | ||
| 136 | frontBuffer = Bitmap.createBitmap (bitmap.getWidth (), | ||
| 137 | bitmap.getHeight (), | ||
| 138 | Bitmap.Config.ARGB_8888, | ||
| 139 | false); | ||
| 140 | else | ||
| 141 | frontBuffer = Bitmap.createBitmap (bitmap.getWidth (), | ||
| 142 | bitmap.getHeight (), | ||
| 143 | Bitmap.Config.ARGB_8888); | ||
| 144 | |||
| 145 | bitmapCanvas = new Canvas (frontBuffer); | ||
| 146 | |||
| 147 | /* And copy over the bitmap contents. */ | ||
| 148 | copyToFrontBuffer (bitmap, null); | ||
| 149 | } | ||
| 150 | else if (bitmap != null) | ||
| 151 | /* Just copy over the bitmap contents. */ | ||
| 152 | copyToFrontBuffer (bitmap, null); | ||
| 153 | } | ||
| 154 | |||
| 155 | public synchronized void | ||
| 156 | setBitmap (Bitmap bitmap, Rect damageRect) | ||
| 157 | { | ||
| 158 | if (bitmap != this.bitmap.get ()) | ||
| 159 | reconfigureFrontBuffer (bitmap); | ||
| 160 | else if (bitmap != null) | ||
| 161 | copyToFrontBuffer (bitmap, damageRect); | ||
| 162 | |||
| 163 | if (bitmap != null) | ||
| 164 | { | ||
| 165 | /* In newer versions of Android, the invalid rectangle is | ||
| 166 | supposedly internally calculated by the system. How that | ||
| 167 | is done is unknown, but calling `invalidateRect' is now | ||
| 168 | deprecated. | ||
| 169 | |||
| 170 | Fortunately, nobody has deprecated the version of | ||
| 171 | `postInvalidate' that accepts a dirty rectangle. */ | ||
| 172 | |||
| 173 | if (damageRect != null) | ||
| 174 | postInvalidate (damageRect.left, damageRect.top, | ||
| 175 | damageRect.right, damageRect.bottom); | ||
| 176 | else | ||
| 177 | postInvalidate (); | ||
| 178 | } | ||
| 179 | } | ||
| 180 | |||
| 181 | @Override | ||
| 182 | public synchronized void | ||
| 183 | onDraw (Canvas canvas) | ||
| 184 | { | ||
| 185 | /* Paint the view's bitmap; the bitmap might be recycled right | ||
| 186 | now. */ | ||
| 187 | |||
| 188 | if (frontBuffer != null) | ||
| 189 | { | ||
| 190 | /* The first time the bitmap is drawn after a buffer swap, | ||
| 191 | mark its contents as having changed. This increments the | ||
| 192 | ``generation ID'' used by Android to avoid uploading buffer | ||
| 193 | textures for unchanged bitmaps. | ||
| 194 | |||
| 195 | When a buffer swap takes place, the bitmap is initially | ||
| 196 | updated from the Emacs thread, resulting in the generation | ||
| 197 | ID being increased. If the render thread is texturizing | ||
| 198 | the bitmap while the swap takes place, it might record the | ||
| 199 | generation ID after the update for a texture containing the | ||
| 200 | contents of the bitmap prior to the swap, leaving the | ||
| 201 | texture tied to the bitmap partially updated. | ||
| 202 | |||
| 203 | Android never calls `onDraw' if the render thread is still | ||
| 204 | processing the bitmap. Update the generation ID here to | ||
| 205 | ensure that a new texture will be uploaded if the bitmap | ||
| 206 | has changed. | ||
| 207 | |||
| 208 | Uploading the bitmap contents to the GPU uses an excessive | ||
| 209 | amount of memory, as the entire bitmap is placed into the | ||
| 210 | graphics command queue, but this memory is actually shared | ||
| 211 | among all other applications and reclaimed by the system | ||
| 212 | when necessary. */ | ||
| 213 | |||
| 214 | if (bitmapChanged) | ||
| 215 | { | ||
| 216 | EmacsNative.notifyPixelsChanged (frontBuffer); | ||
| 217 | bitmapChanged = false; | ||
| 218 | } | ||
| 219 | |||
| 220 | canvas.drawBitmap (frontBuffer, 0f, 0f, uiThreadPaint); | ||
| 221 | } | ||
| 222 | } | ||
| 223 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsThread.java b/java/org/gnu/emacs/EmacsThread.java new file mode 100644 index 00000000000..5307015b46f --- /dev/null +++ b/java/org/gnu/emacs/EmacsThread.java | |||
| @@ -0,0 +1,82 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.lang.Thread; | ||
| 23 | import java.util.Arrays; | ||
| 24 | |||
| 25 | import android.util.Log; | ||
| 26 | |||
| 27 | public final class EmacsThread extends Thread | ||
| 28 | { | ||
| 29 | private static final String TAG = "EmacsThread"; | ||
| 30 | |||
| 31 | /* Whether or not Emacs should be started with an additional | ||
| 32 | argument, and that additional argument if non-NULL. */ | ||
| 33 | private String extraStartupArgument; | ||
| 34 | |||
| 35 | /* Runnable run to initialize Emacs. */ | ||
| 36 | private Runnable paramsClosure; | ||
| 37 | |||
| 38 | /* Whether or not to open a file after starting Emacs. */ | ||
| 39 | private String fileToOpen; | ||
| 40 | |||
| 41 | public | ||
| 42 | EmacsThread (EmacsService service, Runnable paramsClosure, | ||
| 43 | String extraStartupArgument, String fileToOpen) | ||
| 44 | { | ||
| 45 | super ("Emacs main thread"); | ||
| 46 | this.extraStartupArgument = extraStartupArgument; | ||
| 47 | this.paramsClosure = paramsClosure; | ||
| 48 | this.fileToOpen = fileToOpen; | ||
| 49 | } | ||
| 50 | |||
| 51 | @Override | ||
| 52 | public void | ||
| 53 | run () | ||
| 54 | { | ||
| 55 | String args[]; | ||
| 56 | |||
| 57 | if (fileToOpen == null) | ||
| 58 | { | ||
| 59 | if (extraStartupArgument == null) | ||
| 60 | args = new String[] { "libandroid-emacs.so", }; | ||
| 61 | else | ||
| 62 | args = new String[] { "libandroid-emacs.so", | ||
| 63 | extraStartupArgument, }; | ||
| 64 | } | ||
| 65 | else | ||
| 66 | { | ||
| 67 | if (extraStartupArgument == null) | ||
| 68 | args = new String[] { "libandroid-emacs.so", | ||
| 69 | fileToOpen, }; | ||
| 70 | else | ||
| 71 | args = new String[] { "libandroid-emacs.so", | ||
| 72 | extraStartupArgument, | ||
| 73 | fileToOpen, }; | ||
| 74 | } | ||
| 75 | |||
| 76 | paramsClosure.run (); | ||
| 77 | |||
| 78 | /* Run the native code now. */ | ||
| 79 | Log.d (TAG, "run: " + Arrays.toString (args)); | ||
| 80 | EmacsNative.initEmacs (args, EmacsApplication.dumpFileName); | ||
| 81 | } | ||
| 82 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsView.java b/java/org/gnu/emacs/EmacsView.java new file mode 100644 index 00000000000..12d8ff4da56 --- /dev/null +++ b/java/org/gnu/emacs/EmacsView.java | |||
| @@ -0,0 +1,777 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import android.content.Context; | ||
| 23 | |||
| 24 | import android.text.InputType; | ||
| 25 | |||
| 26 | import android.view.ContextMenu; | ||
| 27 | import android.view.View; | ||
| 28 | import android.view.KeyEvent; | ||
| 29 | import android.view.MotionEvent; | ||
| 30 | import android.view.ViewGroup; | ||
| 31 | import android.view.ViewTreeObserver; | ||
| 32 | |||
| 33 | import android.view.inputmethod.EditorInfo; | ||
| 34 | import android.view.inputmethod.InputConnection; | ||
| 35 | import android.view.inputmethod.InputMethodManager; | ||
| 36 | |||
| 37 | import android.graphics.Bitmap; | ||
| 38 | import android.graphics.Canvas; | ||
| 39 | import android.graphics.Rect; | ||
| 40 | import android.graphics.Region; | ||
| 41 | import android.graphics.Paint; | ||
| 42 | |||
| 43 | import android.os.Build; | ||
| 44 | import android.util.Log; | ||
| 45 | |||
| 46 | /* This is an Android view which has a back and front buffer. When | ||
| 47 | swapBuffers is called, the back buffer is swapped to the front | ||
| 48 | buffer, and any damage is invalidated. frontBitmap and backBitmap | ||
| 49 | are modified and used both from the UI and the Emacs thread. As a | ||
| 50 | result, there is a lock held during all drawing operations. | ||
| 51 | |||
| 52 | It is also a ViewGroup, as it also lays out children. */ | ||
| 53 | |||
| 54 | public final class EmacsView extends ViewGroup | ||
| 55 | implements ViewTreeObserver.OnGlobalLayoutListener | ||
| 56 | { | ||
| 57 | public static final String TAG = "EmacsView"; | ||
| 58 | |||
| 59 | /* The associated EmacsWindow. */ | ||
| 60 | public EmacsWindow window; | ||
| 61 | |||
| 62 | /* The buffer bitmap. */ | ||
| 63 | public Bitmap bitmap; | ||
| 64 | |||
| 65 | /* The associated canvases. */ | ||
| 66 | public Canvas canvas; | ||
| 67 | |||
| 68 | /* The damage region. */ | ||
| 69 | public Region damageRegion; | ||
| 70 | |||
| 71 | /* The associated surface view. */ | ||
| 72 | private EmacsSurfaceView surfaceView; | ||
| 73 | |||
| 74 | /* Whether or not a configure event must be sent for the next layout | ||
| 75 | event regardless of what changed. */ | ||
| 76 | public boolean mustReportLayout; | ||
| 77 | |||
| 78 | /* Whether or not bitmaps must be recreated upon the next call to | ||
| 79 | getBitmap. */ | ||
| 80 | private boolean bitmapDirty; | ||
| 81 | |||
| 82 | /* Whether or not a popup is active. */ | ||
| 83 | private boolean popupActive; | ||
| 84 | |||
| 85 | /* The current context menu. */ | ||
| 86 | private EmacsContextMenu contextMenu; | ||
| 87 | |||
| 88 | /* The last measured width and height. */ | ||
| 89 | private int measuredWidth, measuredHeight; | ||
| 90 | |||
| 91 | /* Object acting as a lock for those values. */ | ||
| 92 | private Object dimensionsLock; | ||
| 93 | |||
| 94 | /* The serial of the last clip rectangle change. */ | ||
| 95 | private long lastClipSerial; | ||
| 96 | |||
| 97 | /* The InputMethodManager for this view's context. */ | ||
| 98 | public InputMethodManager imManager; | ||
| 99 | |||
| 100 | /* Whether or not this view is attached to a window. */ | ||
| 101 | public boolean isAttachedToWindow; | ||
| 102 | |||
| 103 | /* Whether or not this view should have the on screen keyboard | ||
| 104 | displayed whenever possible. */ | ||
| 105 | public boolean isCurrentlyTextEditor; | ||
| 106 | |||
| 107 | /* The associated input connection. */ | ||
| 108 | private EmacsInputConnection inputConnection; | ||
| 109 | |||
| 110 | /* The current IC mode. See `android_reset_ic' for more | ||
| 111 | details. */ | ||
| 112 | private int icMode; | ||
| 113 | |||
| 114 | /* The number of calls to `resetIC' to have taken place the last | ||
| 115 | time an InputConnection was created. */ | ||
| 116 | public long icSerial; | ||
| 117 | |||
| 118 | /* The number of calls to `recetIC' that have taken place. */ | ||
| 119 | public volatile long icGeneration; | ||
| 120 | |||
| 121 | public | ||
| 122 | EmacsView (EmacsWindow window) | ||
| 123 | { | ||
| 124 | super (EmacsService.SERVICE); | ||
| 125 | |||
| 126 | Object tem; | ||
| 127 | Context context; | ||
| 128 | |||
| 129 | this.window = window; | ||
| 130 | this.damageRegion = new Region (); | ||
| 131 | |||
| 132 | setFocusable (true); | ||
| 133 | setFocusableInTouchMode (true); | ||
| 134 | |||
| 135 | /* Create the surface view. */ | ||
| 136 | this.surfaceView = new EmacsSurfaceView (this); | ||
| 137 | addView (this.surfaceView); | ||
| 138 | |||
| 139 | /* Get rid of the default focus highlight. */ | ||
| 140 | if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O) | ||
| 141 | setDefaultFocusHighlightEnabled (false); | ||
| 142 | |||
| 143 | /* Obtain the input method manager. */ | ||
| 144 | context = getContext (); | ||
| 145 | tem = context.getSystemService (Context.INPUT_METHOD_SERVICE); | ||
| 146 | imManager = (InputMethodManager) tem; | ||
| 147 | |||
| 148 | /* Add this view as its own global layout listener. */ | ||
| 149 | getViewTreeObserver ().addOnGlobalLayoutListener (this); | ||
| 150 | |||
| 151 | /* Create an object used as a lock. */ | ||
| 152 | this.dimensionsLock = new Object (); | ||
| 153 | } | ||
| 154 | |||
| 155 | private void | ||
| 156 | handleDirtyBitmap () | ||
| 157 | { | ||
| 158 | Bitmap oldBitmap; | ||
| 159 | int measuredWidth, measuredHeight; | ||
| 160 | |||
| 161 | synchronized (dimensionsLock) | ||
| 162 | { | ||
| 163 | /* Load measuredWidth and measuredHeight. */ | ||
| 164 | measuredWidth = this.measuredWidth; | ||
| 165 | measuredHeight = this.measuredHeight; | ||
| 166 | } | ||
| 167 | |||
| 168 | if (measuredWidth == 0 || measuredHeight == 0) | ||
| 169 | return; | ||
| 170 | |||
| 171 | if (!isAttachedToWindow) | ||
| 172 | return; | ||
| 173 | |||
| 174 | /* If bitmap is the same width and height as the measured width | ||
| 175 | and height, there is no need to do anything. Avoid allocating | ||
| 176 | the extra bitmap. */ | ||
| 177 | if (bitmap != null | ||
| 178 | && (bitmap.getWidth () == measuredWidth | ||
| 179 | && bitmap.getHeight () == measuredHeight)) | ||
| 180 | { | ||
| 181 | bitmapDirty = false; | ||
| 182 | return; | ||
| 183 | } | ||
| 184 | |||
| 185 | /* Save the old bitmap. */ | ||
| 186 | oldBitmap = bitmap; | ||
| 187 | |||
| 188 | /* Recreate the back buffer bitmap. */ | ||
| 189 | bitmap | ||
| 190 | = Bitmap.createBitmap (measuredWidth, | ||
| 191 | measuredHeight, | ||
| 192 | Bitmap.Config.ARGB_8888); | ||
| 193 | bitmap.eraseColor (window.background | 0xff000000); | ||
| 194 | |||
| 195 | /* And canvases. */ | ||
| 196 | canvas = new Canvas (bitmap); | ||
| 197 | canvas.save (); | ||
| 198 | |||
| 199 | /* Since the clip rectangles have been cleared, clear the clip | ||
| 200 | rectangle ID. */ | ||
| 201 | lastClipSerial = 0; | ||
| 202 | |||
| 203 | /* Copy over the contents of the old bitmap. */ | ||
| 204 | if (oldBitmap != null) | ||
| 205 | canvas.drawBitmap (oldBitmap, 0f, 0f, new Paint ()); | ||
| 206 | |||
| 207 | bitmapDirty = false; | ||
| 208 | |||
| 209 | /* Explicitly free the old bitmap's memory. */ | ||
| 210 | |||
| 211 | if (oldBitmap != null) | ||
| 212 | oldBitmap.recycle (); | ||
| 213 | |||
| 214 | /* Some Android versions still don't free the bitmap until the | ||
| 215 | next GC. */ | ||
| 216 | Runtime.getRuntime ().gc (); | ||
| 217 | } | ||
| 218 | |||
| 219 | public synchronized void | ||
| 220 | explicitlyDirtyBitmap () | ||
| 221 | { | ||
| 222 | bitmapDirty = true; | ||
| 223 | } | ||
| 224 | |||
| 225 | public synchronized Bitmap | ||
| 226 | getBitmap () | ||
| 227 | { | ||
| 228 | if (bitmapDirty || bitmap == null) | ||
| 229 | handleDirtyBitmap (); | ||
| 230 | |||
| 231 | return bitmap; | ||
| 232 | } | ||
| 233 | |||
| 234 | public synchronized Canvas | ||
| 235 | getCanvas (EmacsGC gc) | ||
| 236 | { | ||
| 237 | int i; | ||
| 238 | |||
| 239 | if (bitmapDirty || bitmap == null) | ||
| 240 | handleDirtyBitmap (); | ||
| 241 | |||
| 242 | if (canvas == null) | ||
| 243 | return null; | ||
| 244 | |||
| 245 | /* Update clip rectangles if necessary. */ | ||
| 246 | if (gc.clipRectID != lastClipSerial) | ||
| 247 | { | ||
| 248 | canvas.restore (); | ||
| 249 | canvas.save (); | ||
| 250 | |||
| 251 | if (gc.real_clip_rects != null) | ||
| 252 | { | ||
| 253 | for (i = 0; i < gc.real_clip_rects.length; ++i) | ||
| 254 | canvas.clipRect (gc.real_clip_rects[i]); | ||
| 255 | } | ||
| 256 | |||
| 257 | lastClipSerial = gc.clipRectID; | ||
| 258 | } | ||
| 259 | |||
| 260 | return canvas; | ||
| 261 | } | ||
| 262 | |||
| 263 | public void | ||
| 264 | prepareForLayout (int wantedWidth, int wantedHeight) | ||
| 265 | { | ||
| 266 | synchronized (dimensionsLock) | ||
| 267 | { | ||
| 268 | measuredWidth = wantedWidth; | ||
| 269 | measuredHeight = wantedWidth; | ||
| 270 | } | ||
| 271 | } | ||
| 272 | |||
| 273 | @Override | ||
| 274 | protected void | ||
| 275 | onMeasure (int widthMeasureSpec, int heightMeasureSpec) | ||
| 276 | { | ||
| 277 | Rect measurements; | ||
| 278 | int width, height; | ||
| 279 | |||
| 280 | /* Return the width and height of the window regardless of what | ||
| 281 | the parent says. */ | ||
| 282 | measurements = window.getGeometry (); | ||
| 283 | |||
| 284 | width = measurements.width (); | ||
| 285 | height = measurements.height (); | ||
| 286 | |||
| 287 | /* Now apply any extra requirements in widthMeasureSpec and | ||
| 288 | heightMeasureSpec. */ | ||
| 289 | |||
| 290 | if (MeasureSpec.getMode (widthMeasureSpec) == MeasureSpec.EXACTLY) | ||
| 291 | width = MeasureSpec.getSize (widthMeasureSpec); | ||
| 292 | else if (MeasureSpec.getMode (widthMeasureSpec) == MeasureSpec.AT_MOST | ||
| 293 | && width > MeasureSpec.getSize (widthMeasureSpec)) | ||
| 294 | width = MeasureSpec.getSize (widthMeasureSpec); | ||
| 295 | |||
| 296 | if (MeasureSpec.getMode (heightMeasureSpec) == MeasureSpec.EXACTLY) | ||
| 297 | height = MeasureSpec.getSize (heightMeasureSpec); | ||
| 298 | else if (MeasureSpec.getMode (heightMeasureSpec) == MeasureSpec.AT_MOST | ||
| 299 | && height > MeasureSpec.getSize (heightMeasureSpec)) | ||
| 300 | height = MeasureSpec.getSize (heightMeasureSpec); | ||
| 301 | |||
| 302 | super.setMeasuredDimension (width, height); | ||
| 303 | } | ||
| 304 | |||
| 305 | /* Note that the monitor lock for the window must never be held from | ||
| 306 | within the lock for the view, because the window also locks the | ||
| 307 | other way around. */ | ||
| 308 | |||
| 309 | @Override | ||
| 310 | protected void | ||
| 311 | onLayout (boolean changed, int left, int top, int right, | ||
| 312 | int bottom) | ||
| 313 | { | ||
| 314 | int count, i, oldMeasuredWidth, oldMeasuredHeight; | ||
| 315 | View child; | ||
| 316 | Rect windowRect; | ||
| 317 | boolean needExpose; | ||
| 318 | |||
| 319 | count = getChildCount (); | ||
| 320 | needExpose = false; | ||
| 321 | |||
| 322 | synchronized (dimensionsLock) | ||
| 323 | { | ||
| 324 | /* Load measuredWidth and measuredHeight. */ | ||
| 325 | oldMeasuredWidth = measuredWidth; | ||
| 326 | oldMeasuredHeight = measuredHeight; | ||
| 327 | |||
| 328 | /* Set measuredWidth and measuredHeight. */ | ||
| 329 | measuredWidth = right - left; | ||
| 330 | measuredHeight = bottom - top; | ||
| 331 | } | ||
| 332 | |||
| 333 | /* Dirty the back buffer if the layout change resulted in the view | ||
| 334 | being resized. */ | ||
| 335 | |||
| 336 | if (changed && (right - left != oldMeasuredWidth | ||
| 337 | || bottom - top != oldMeasuredHeight)) | ||
| 338 | { | ||
| 339 | explicitlyDirtyBitmap (); | ||
| 340 | |||
| 341 | /* Expose the window upon a change in the view's size. */ | ||
| 342 | |||
| 343 | if (right - left > oldMeasuredWidth | ||
| 344 | || bottom - top > oldMeasuredHeight) | ||
| 345 | needExpose = true; | ||
| 346 | } | ||
| 347 | |||
| 348 | for (i = 0; i < count; ++i) | ||
| 349 | { | ||
| 350 | child = getChildAt (i); | ||
| 351 | |||
| 352 | Log.d (TAG, "onLayout: " + child); | ||
| 353 | |||
| 354 | if (child == surfaceView) | ||
| 355 | child.layout (0, 0, right - left, bottom - top); | ||
| 356 | else if (child.getVisibility () != GONE) | ||
| 357 | { | ||
| 358 | if (!(child instanceof EmacsView)) | ||
| 359 | continue; | ||
| 360 | |||
| 361 | /* What to do: lay out the view precisely according to its | ||
| 362 | window rect. */ | ||
| 363 | windowRect = ((EmacsView) child).window.getGeometry (); | ||
| 364 | child.layout (windowRect.left, windowRect.top, | ||
| 365 | windowRect.right, windowRect.bottom); | ||
| 366 | } | ||
| 367 | } | ||
| 368 | |||
| 369 | /* Now report the layout change to the window. */ | ||
| 370 | |||
| 371 | if (changed || mustReportLayout) | ||
| 372 | { | ||
| 373 | mustReportLayout = false; | ||
| 374 | window.viewLayout (left, top, right, bottom); | ||
| 375 | } | ||
| 376 | |||
| 377 | if (needExpose) | ||
| 378 | EmacsNative.sendExpose (this.window.handle, 0, 0, | ||
| 379 | right - left, bottom - top); | ||
| 380 | } | ||
| 381 | |||
| 382 | public void | ||
| 383 | damageRect (Rect damageRect) | ||
| 384 | { | ||
| 385 | EmacsService.checkEmacsThread (); | ||
| 386 | damageRegion.union (damageRect); | ||
| 387 | } | ||
| 388 | |||
| 389 | /* This method is called from both the UI thread and the Emacs | ||
| 390 | thread. */ | ||
| 391 | |||
| 392 | public void | ||
| 393 | swapBuffers () | ||
| 394 | { | ||
| 395 | Canvas canvas; | ||
| 396 | Rect damageRect; | ||
| 397 | Bitmap bitmap; | ||
| 398 | |||
| 399 | /* Make sure this function is called only from the Emacs | ||
| 400 | thread. */ | ||
| 401 | EmacsService.checkEmacsThread (); | ||
| 402 | |||
| 403 | damageRect = null; | ||
| 404 | |||
| 405 | /* Now see if there is a damage region. */ | ||
| 406 | |||
| 407 | if (damageRegion.isEmpty ()) | ||
| 408 | return; | ||
| 409 | |||
| 410 | /* And extract and clear the damage region. */ | ||
| 411 | |||
| 412 | damageRect = damageRegion.getBounds (); | ||
| 413 | damageRegion.setEmpty (); | ||
| 414 | |||
| 415 | bitmap = getBitmap (); | ||
| 416 | |||
| 417 | /* Transfer the bitmap to the surface view, then invalidate | ||
| 418 | it. */ | ||
| 419 | surfaceView.setBitmap (bitmap, damageRect); | ||
| 420 | } | ||
| 421 | |||
| 422 | @Override | ||
| 423 | public boolean | ||
| 424 | onKeyDown (int keyCode, KeyEvent event) | ||
| 425 | { | ||
| 426 | if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP | ||
| 427 | || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN | ||
| 428 | || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) | ||
| 429 | && !EmacsNative.shouldForwardMultimediaButtons ()) | ||
| 430 | return false; | ||
| 431 | |||
| 432 | window.onKeyDown (keyCode, event); | ||
| 433 | return true; | ||
| 434 | } | ||
| 435 | |||
| 436 | @Override | ||
| 437 | public boolean | ||
| 438 | onKeyMultiple (int keyCode, int repeatCount, KeyEvent event) | ||
| 439 | { | ||
| 440 | if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP | ||
| 441 | || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN | ||
| 442 | || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) | ||
| 443 | && !EmacsNative.shouldForwardMultimediaButtons ()) | ||
| 444 | return false; | ||
| 445 | |||
| 446 | window.onKeyDown (keyCode, event); | ||
| 447 | return true; | ||
| 448 | } | ||
| 449 | |||
| 450 | @Override | ||
| 451 | public boolean | ||
| 452 | onKeyUp (int keyCode, KeyEvent event) | ||
| 453 | { | ||
| 454 | if ((keyCode == KeyEvent.KEYCODE_VOLUME_UP | ||
| 455 | || keyCode == KeyEvent.KEYCODE_VOLUME_DOWN | ||
| 456 | || keyCode == KeyEvent.KEYCODE_VOLUME_MUTE) | ||
| 457 | && !EmacsNative.shouldForwardMultimediaButtons ()) | ||
| 458 | return false; | ||
| 459 | |||
| 460 | window.onKeyUp (keyCode, event); | ||
| 461 | return true; | ||
| 462 | } | ||
| 463 | |||
| 464 | @Override | ||
| 465 | public void | ||
| 466 | onFocusChanged (boolean gainFocus, int direction, | ||
| 467 | Rect previouslyFocusedRect) | ||
| 468 | { | ||
| 469 | window.onFocusChanged (gainFocus); | ||
| 470 | super.onFocusChanged (gainFocus, direction, | ||
| 471 | previouslyFocusedRect); | ||
| 472 | } | ||
| 473 | |||
| 474 | @Override | ||
| 475 | public boolean | ||
| 476 | onGenericMotionEvent (MotionEvent motion) | ||
| 477 | { | ||
| 478 | return window.onGenericMotionEvent (motion); | ||
| 479 | } | ||
| 480 | |||
| 481 | @Override | ||
| 482 | public boolean | ||
| 483 | onTouchEvent (MotionEvent motion) | ||
| 484 | { | ||
| 485 | return window.onTouchEvent (motion); | ||
| 486 | } | ||
| 487 | |||
| 488 | private void | ||
| 489 | moveChildToBack (View child) | ||
| 490 | { | ||
| 491 | int index; | ||
| 492 | |||
| 493 | index = indexOfChild (child); | ||
| 494 | |||
| 495 | if (index > 0) | ||
| 496 | { | ||
| 497 | detachViewFromParent (index); | ||
| 498 | |||
| 499 | /* The view at 0 is the surface view. */ | ||
| 500 | attachViewToParent (child, 1, | ||
| 501 | child.getLayoutParams()); | ||
| 502 | } | ||
| 503 | } | ||
| 504 | |||
| 505 | /* The following two functions must not be called if the view has no | ||
| 506 | parent, or is parented to an activity. */ | ||
| 507 | |||
| 508 | public void | ||
| 509 | raise () | ||
| 510 | { | ||
| 511 | EmacsView parent; | ||
| 512 | |||
| 513 | parent = (EmacsView) getParent (); | ||
| 514 | |||
| 515 | Log.d (TAG, "raise: parent " + parent); | ||
| 516 | |||
| 517 | if (parent.indexOfChild (this) | ||
| 518 | == parent.getChildCount () - 1) | ||
| 519 | return; | ||
| 520 | |||
| 521 | parent.bringChildToFront (this); | ||
| 522 | } | ||
| 523 | |||
| 524 | public void | ||
| 525 | lower () | ||
| 526 | { | ||
| 527 | EmacsView parent; | ||
| 528 | |||
| 529 | parent = (EmacsView) getParent (); | ||
| 530 | |||
| 531 | Log.d (TAG, "lower: parent " + parent); | ||
| 532 | |||
| 533 | if (parent.indexOfChild (this) == 1) | ||
| 534 | return; | ||
| 535 | |||
| 536 | parent.moveChildToBack (this); | ||
| 537 | } | ||
| 538 | |||
| 539 | @Override | ||
| 540 | protected void | ||
| 541 | onCreateContextMenu (ContextMenu menu) | ||
| 542 | { | ||
| 543 | if (contextMenu == null) | ||
| 544 | return; | ||
| 545 | |||
| 546 | contextMenu.expandTo (menu, this); | ||
| 547 | } | ||
| 548 | |||
| 549 | public boolean | ||
| 550 | popupMenu (EmacsContextMenu menu, int xPosition, | ||
| 551 | int yPosition, boolean force) | ||
| 552 | { | ||
| 553 | if (popupActive && !force) | ||
| 554 | return false; | ||
| 555 | |||
| 556 | contextMenu = menu; | ||
| 557 | popupActive = true; | ||
| 558 | |||
| 559 | Log.d (TAG, "popupMenu: " + menu + " @" + xPosition | ||
| 560 | + ", " + yPosition + " " + force); | ||
| 561 | |||
| 562 | /* Use showContextMenu (float, float) on N to get actual popup | ||
| 563 | behavior. */ | ||
| 564 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) | ||
| 565 | return showContextMenu ((float) xPosition, (float) yPosition); | ||
| 566 | else | ||
| 567 | return showContextMenu (); | ||
| 568 | } | ||
| 569 | |||
| 570 | public void | ||
| 571 | cancelPopupMenu () | ||
| 572 | { | ||
| 573 | if (!popupActive) | ||
| 574 | throw new IllegalStateException ("cancelPopupMenu called without" | ||
| 575 | + " popupActive set"); | ||
| 576 | |||
| 577 | contextMenu = null; | ||
| 578 | popupActive = false; | ||
| 579 | |||
| 580 | /* It is not possible to know with 100% certainty which activity | ||
| 581 | is currently displaying the context menu. Loop through each | ||
| 582 | activity and call `closeContextMenu' instead. */ | ||
| 583 | |||
| 584 | for (EmacsWindowAttachmentManager.WindowConsumer consumer | ||
| 585 | : EmacsWindowAttachmentManager.MANAGER.consumers) | ||
| 586 | { | ||
| 587 | if (consumer instanceof EmacsActivity) | ||
| 588 | ((EmacsActivity) consumer).closeContextMenu (); | ||
| 589 | } | ||
| 590 | } | ||
| 591 | |||
| 592 | @Override | ||
| 593 | public synchronized void | ||
| 594 | onDetachedFromWindow () | ||
| 595 | { | ||
| 596 | isAttachedToWindow = false; | ||
| 597 | |||
| 598 | /* Recycle the bitmap and call GC. */ | ||
| 599 | |||
| 600 | if (bitmap != null) | ||
| 601 | bitmap.recycle (); | ||
| 602 | |||
| 603 | bitmap = null; | ||
| 604 | canvas = null; | ||
| 605 | surfaceView.setBitmap (null, null); | ||
| 606 | |||
| 607 | /* Collect the bitmap storage; it could be large. */ | ||
| 608 | Runtime.getRuntime ().gc (); | ||
| 609 | |||
| 610 | super.onDetachedFromWindow (); | ||
| 611 | } | ||
| 612 | |||
| 613 | @Override | ||
| 614 | public synchronized void | ||
| 615 | onAttachedToWindow () | ||
| 616 | { | ||
| 617 | isAttachedToWindow = true; | ||
| 618 | |||
| 619 | /* Dirty the bitmap, as it was destroyed when onDetachedFromWindow | ||
| 620 | was called. */ | ||
| 621 | bitmapDirty = true; | ||
| 622 | |||
| 623 | synchronized (dimensionsLock) | ||
| 624 | { | ||
| 625 | /* Now expose the view contents again. */ | ||
| 626 | EmacsNative.sendExpose (this.window.handle, 0, 0, | ||
| 627 | measuredWidth, measuredHeight); | ||
| 628 | } | ||
| 629 | |||
| 630 | super.onAttachedToWindow (); | ||
| 631 | } | ||
| 632 | |||
| 633 | public void | ||
| 634 | showOnScreenKeyboard () | ||
| 635 | { | ||
| 636 | /* Specifying no flags at all tells the system the user asked for | ||
| 637 | the input method to be displayed. */ | ||
| 638 | |||
| 639 | imManager.showSoftInput (this, 0); | ||
| 640 | isCurrentlyTextEditor = true; | ||
| 641 | } | ||
| 642 | |||
| 643 | public void | ||
| 644 | hideOnScreenKeyboard () | ||
| 645 | { | ||
| 646 | imManager.hideSoftInputFromWindow (this.getWindowToken (), | ||
| 647 | 0); | ||
| 648 | isCurrentlyTextEditor = false; | ||
| 649 | } | ||
| 650 | |||
| 651 | @Override | ||
| 652 | public InputConnection | ||
| 653 | onCreateInputConnection (EditorInfo info) | ||
| 654 | { | ||
| 655 | int mode; | ||
| 656 | int[] selection; | ||
| 657 | |||
| 658 | /* Figure out what kind of IME behavior Emacs wants. */ | ||
| 659 | mode = getICMode (); | ||
| 660 | |||
| 661 | /* Make sure the input method never displays a full screen input | ||
| 662 | box that obscures Emacs. */ | ||
| 663 | info.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; | ||
| 664 | info.imeOptions |= EditorInfo.IME_FLAG_NO_EXTRACT_UI; | ||
| 665 | |||
| 666 | /* Set a reasonable inputType. */ | ||
| 667 | info.inputType = InputType.TYPE_CLASS_TEXT; | ||
| 668 | |||
| 669 | /* If this fails or ANDROID_IC_MODE_NULL was requested, then don't | ||
| 670 | initialize the input connection. */ | ||
| 671 | |||
| 672 | if (mode == EmacsService.IC_MODE_NULL) | ||
| 673 | { | ||
| 674 | info.inputType = InputType.TYPE_NULL; | ||
| 675 | return null; | ||
| 676 | } | ||
| 677 | |||
| 678 | /* Set icSerial. If icSerial < icGeneration, the input connection | ||
| 679 | has been reset, and future input should be ignored until a new | ||
| 680 | connection is created. */ | ||
| 681 | |||
| 682 | icSerial = icGeneration; | ||
| 683 | |||
| 684 | /* Reset flags set by the previous input method. */ | ||
| 685 | |||
| 686 | EmacsNative.clearInputFlags (window.handle); | ||
| 687 | |||
| 688 | /* Obtain the current position of point and set it as the | ||
| 689 | selection. Don't do this under one specific situation: if | ||
| 690 | `android_update_ic' is being called in the main thread, trying | ||
| 691 | to synchronize with it can cause a dead lock in the IM manager. | ||
| 692 | See icBeginSynchronous in EmacsService.java for more | ||
| 693 | details. */ | ||
| 694 | |||
| 695 | selection = EmacsService.viewGetSelection (window.handle); | ||
| 696 | |||
| 697 | if (selection != null) | ||
| 698 | Log.d (TAG, "onCreateInputConnection: current selection is: " | ||
| 699 | + selection[0] + ", by " + selection[1]); | ||
| 700 | else | ||
| 701 | { | ||
| 702 | Log.d (TAG, "onCreateInputConnection: current selection could" | ||
| 703 | + " not be retrieved."); | ||
| 704 | |||
| 705 | /* If the selection could not be obtained, return 0 by 0. | ||
| 706 | However, ask for the selection position to be updated as | ||
| 707 | soon as possible. */ | ||
| 708 | |||
| 709 | selection = new int[] { 0, 0, }; | ||
| 710 | EmacsNative.requestSelectionUpdate (window.handle); | ||
| 711 | } | ||
| 712 | |||
| 713 | if (mode == EmacsService.IC_MODE_ACTION) | ||
| 714 | info.imeOptions |= EditorInfo.IME_ACTION_DONE; | ||
| 715 | |||
| 716 | /* Set the initial selection fields. */ | ||
| 717 | info.initialSelStart = selection[0]; | ||
| 718 | info.initialSelEnd = selection[1]; | ||
| 719 | |||
| 720 | /* Create the input connection if necessary. */ | ||
| 721 | |||
| 722 | if (inputConnection == null) | ||
| 723 | inputConnection = new EmacsInputConnection (this); | ||
| 724 | else | ||
| 725 | /* Clear several pieces of state in the input connection. */ | ||
| 726 | inputConnection.reset (); | ||
| 727 | |||
| 728 | /* Return the input connection. */ | ||
| 729 | return inputConnection; | ||
| 730 | } | ||
| 731 | |||
| 732 | @Override | ||
| 733 | public synchronized boolean | ||
| 734 | onCheckIsTextEditor () | ||
| 735 | { | ||
| 736 | /* If value is true, then the system will display the on screen | ||
| 737 | keyboard. */ | ||
| 738 | return isCurrentlyTextEditor; | ||
| 739 | } | ||
| 740 | |||
| 741 | @Override | ||
| 742 | public boolean | ||
| 743 | isOpaque () | ||
| 744 | { | ||
| 745 | /* Returning true here allows the system to not draw the contents | ||
| 746 | of windows underneath this view, thereby improving | ||
| 747 | performance. */ | ||
| 748 | return true; | ||
| 749 | } | ||
| 750 | |||
| 751 | public synchronized void | ||
| 752 | setICMode (int icMode) | ||
| 753 | { | ||
| 754 | this.icMode = icMode; | ||
| 755 | } | ||
| 756 | |||
| 757 | public synchronized int | ||
| 758 | getICMode () | ||
| 759 | { | ||
| 760 | return icMode; | ||
| 761 | } | ||
| 762 | |||
| 763 | @Override | ||
| 764 | public void | ||
| 765 | onGlobalLayout () | ||
| 766 | { | ||
| 767 | int[] locations; | ||
| 768 | |||
| 769 | /* Get the absolute offset of this view and specify its left and | ||
| 770 | top position in subsequent ConfigureNotify events. */ | ||
| 771 | |||
| 772 | locations = new int[2]; | ||
| 773 | getLocationInWindow (locations); | ||
| 774 | window.notifyContentRectPosition (locations[0], | ||
| 775 | locations[1]); | ||
| 776 | } | ||
| 777 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsWindow.java b/java/org/gnu/emacs/EmacsWindow.java new file mode 100644 index 00000000000..a1f70644e16 --- /dev/null +++ b/java/org/gnu/emacs/EmacsWindow.java | |||
| @@ -0,0 +1,1445 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.lang.IllegalStateException; | ||
| 23 | import java.util.ArrayList; | ||
| 24 | import java.util.List; | ||
| 25 | import java.util.HashMap; | ||
| 26 | import java.util.LinkedHashMap; | ||
| 27 | import java.util.Map; | ||
| 28 | |||
| 29 | import android.content.Context; | ||
| 30 | |||
| 31 | import android.graphics.Rect; | ||
| 32 | import android.graphics.Canvas; | ||
| 33 | import android.graphics.Bitmap; | ||
| 34 | import android.graphics.PixelFormat; | ||
| 35 | |||
| 36 | import android.view.View; | ||
| 37 | import android.view.ViewManager; | ||
| 38 | import android.view.Gravity; | ||
| 39 | import android.view.KeyEvent; | ||
| 40 | import android.view.MotionEvent; | ||
| 41 | import android.view.InputDevice; | ||
| 42 | import android.view.WindowManager; | ||
| 43 | |||
| 44 | import android.util.Log; | ||
| 45 | |||
| 46 | import android.os.Build; | ||
| 47 | |||
| 48 | /* This defines a window, which is a handle. Windows represent a | ||
| 49 | rectangular subset of the screen with their own contents. | ||
| 50 | |||
| 51 | Windows either have a parent window, in which case their views are | ||
| 52 | attached to the parent's view, or are "floating", in which case | ||
| 53 | their views are attached to the parent activity (if any), else | ||
| 54 | nothing. | ||
| 55 | |||
| 56 | Views are also drawables, meaning they can accept drawing | ||
| 57 | requests. */ | ||
| 58 | |||
| 59 | public final class EmacsWindow extends EmacsHandleObject | ||
| 60 | implements EmacsDrawable | ||
| 61 | { | ||
| 62 | private static final String TAG = "EmacsWindow"; | ||
| 63 | |||
| 64 | private static class Coordinate | ||
| 65 | { | ||
| 66 | /* Integral coordinate. */ | ||
| 67 | int x, y; | ||
| 68 | |||
| 69 | /* Button associated with the coordinate, or 0 if it is a touch | ||
| 70 | event. */ | ||
| 71 | int button; | ||
| 72 | |||
| 73 | /* Pointer ID associated with the coordinate. */ | ||
| 74 | int id; | ||
| 75 | |||
| 76 | public | ||
| 77 | Coordinate (int x, int y, int button, int id) | ||
| 78 | { | ||
| 79 | this.x = x; | ||
| 80 | this.y = y; | ||
| 81 | this.button = button; | ||
| 82 | this.id = id; | ||
| 83 | } | ||
| 84 | }; | ||
| 85 | |||
| 86 | /* The view associated with the window. */ | ||
| 87 | public EmacsView view; | ||
| 88 | |||
| 89 | /* The geometry of the window. */ | ||
| 90 | private Rect rect; | ||
| 91 | |||
| 92 | /* The parent window, or null if it is the root window. */ | ||
| 93 | public EmacsWindow parent; | ||
| 94 | |||
| 95 | /* List of all children in stacking order. This must be kept | ||
| 96 | consistent with their Z order! */ | ||
| 97 | public ArrayList<EmacsWindow> children; | ||
| 98 | |||
| 99 | /* Map between pointer identifiers and last known position. Used to | ||
| 100 | compute which pointer changed upon a touch event. */ | ||
| 101 | private HashMap<Integer, Coordinate> pointerMap; | ||
| 102 | |||
| 103 | /* The window consumer currently attached, if it exists. */ | ||
| 104 | private EmacsWindowAttachmentManager.WindowConsumer attached; | ||
| 105 | |||
| 106 | /* The window background scratch GC. foreground is always the | ||
| 107 | window background. */ | ||
| 108 | private EmacsGC scratchGC; | ||
| 109 | |||
| 110 | /* The button state and keyboard modifier mask at the time of the | ||
| 111 | last button press or release event. */ | ||
| 112 | public int lastButtonState; | ||
| 113 | |||
| 114 | /* Whether or not the window is mapped. */ | ||
| 115 | private volatile boolean isMapped; | ||
| 116 | |||
| 117 | /* Whether or not to ask for focus upon being mapped. */ | ||
| 118 | private boolean dontFocusOnMap; | ||
| 119 | |||
| 120 | /* Whether or not the window is override-redirect. An | ||
| 121 | override-redirect window always has its own system window. */ | ||
| 122 | private boolean overrideRedirect; | ||
| 123 | |||
| 124 | /* The window manager that is the parent of this window. NULL if | ||
| 125 | there is no such window manager. */ | ||
| 126 | private WindowManager windowManager; | ||
| 127 | |||
| 128 | /* The time of the last KEYCODE_VOLUME_DOWN release. This is used | ||
| 129 | to quit Emacs upon two rapid clicks of the volume down | ||
| 130 | button. */ | ||
| 131 | private long lastVolumeButtonRelease; | ||
| 132 | |||
| 133 | /* Linked list of character strings which were recently sent as | ||
| 134 | events. */ | ||
| 135 | public LinkedHashMap<Integer, String> eventStrings; | ||
| 136 | |||
| 137 | /* Whether or not this window is fullscreen. */ | ||
| 138 | public boolean fullscreen; | ||
| 139 | |||
| 140 | /* The window background pixel. This is used by EmacsView when | ||
| 141 | creating new bitmaps. */ | ||
| 142 | public volatile int background; | ||
| 143 | |||
| 144 | /* The position of this window relative to the root window. */ | ||
| 145 | public int xPosition, yPosition; | ||
| 146 | |||
| 147 | public | ||
| 148 | EmacsWindow (short handle, final EmacsWindow parent, int x, int y, | ||
| 149 | int width, int height, boolean overrideRedirect) | ||
| 150 | { | ||
| 151 | super (handle); | ||
| 152 | |||
| 153 | rect = new Rect (x, y, x + width, y + height); | ||
| 154 | pointerMap = new HashMap<Integer, Coordinate> (); | ||
| 155 | |||
| 156 | /* Create the view from the context's UI thread. The window is | ||
| 157 | unmapped, so the view is GONE. */ | ||
| 158 | view = EmacsService.SERVICE.getEmacsView (this, View.GONE, | ||
| 159 | parent == null); | ||
| 160 | this.parent = parent; | ||
| 161 | this.overrideRedirect = overrideRedirect; | ||
| 162 | |||
| 163 | /* Create the list of children. */ | ||
| 164 | children = new ArrayList<EmacsWindow> (); | ||
| 165 | |||
| 166 | if (parent != null) | ||
| 167 | { | ||
| 168 | parent.children.add (this); | ||
| 169 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 170 | @Override | ||
| 171 | public void | ||
| 172 | run () | ||
| 173 | { | ||
| 174 | parent.view.addView (view); | ||
| 175 | } | ||
| 176 | }); | ||
| 177 | } | ||
| 178 | |||
| 179 | scratchGC = new EmacsGC ((short) 0); | ||
| 180 | |||
| 181 | /* Create the map of input method-committed strings. Keep at most | ||
| 182 | ten strings in the map. */ | ||
| 183 | |||
| 184 | eventStrings | ||
| 185 | = new LinkedHashMap<Integer, String> () { | ||
| 186 | @Override | ||
| 187 | protected boolean | ||
| 188 | removeEldestEntry (Map.Entry<Integer, String> entry) | ||
| 189 | { | ||
| 190 | return size () > 10; | ||
| 191 | } | ||
| 192 | }; | ||
| 193 | } | ||
| 194 | |||
| 195 | public void | ||
| 196 | changeWindowBackground (int pixel) | ||
| 197 | { | ||
| 198 | /* scratchGC is used as the argument to a FillRectangles req. */ | ||
| 199 | scratchGC.foreground = pixel; | ||
| 200 | scratchGC.markDirty (false); | ||
| 201 | |||
| 202 | /* Make the background known to the view as well. */ | ||
| 203 | background = pixel; | ||
| 204 | } | ||
| 205 | |||
| 206 | public synchronized Rect | ||
| 207 | getGeometry () | ||
| 208 | { | ||
| 209 | return new Rect (rect); | ||
| 210 | } | ||
| 211 | |||
| 212 | @Override | ||
| 213 | public synchronized void | ||
| 214 | destroyHandle () throws IllegalStateException | ||
| 215 | { | ||
| 216 | if (parent != null) | ||
| 217 | parent.children.remove (this); | ||
| 218 | |||
| 219 | EmacsActivity.invalidateFocus (); | ||
| 220 | |||
| 221 | if (!children.isEmpty ()) | ||
| 222 | throw new IllegalStateException ("Trying to destroy window with " | ||
| 223 | + "children!"); | ||
| 224 | |||
| 225 | /* Remove the view from its parent and make it invisible. */ | ||
| 226 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 227 | public void | ||
| 228 | run () | ||
| 229 | { | ||
| 230 | ViewManager parent; | ||
| 231 | EmacsWindowAttachmentManager manager; | ||
| 232 | |||
| 233 | if (EmacsActivity.focusedWindow == EmacsWindow.this) | ||
| 234 | EmacsActivity.focusedWindow = null; | ||
| 235 | |||
| 236 | manager = EmacsWindowAttachmentManager.MANAGER; | ||
| 237 | view.setVisibility (View.GONE); | ||
| 238 | |||
| 239 | /* If the window manager is set, use that instead. */ | ||
| 240 | if (windowManager != null) | ||
| 241 | parent = windowManager; | ||
| 242 | else | ||
| 243 | parent = (ViewManager) view.getParent (); | ||
| 244 | windowManager = null; | ||
| 245 | |||
| 246 | if (parent != null) | ||
| 247 | parent.removeView (view); | ||
| 248 | |||
| 249 | manager.detachWindow (EmacsWindow.this); | ||
| 250 | } | ||
| 251 | }); | ||
| 252 | |||
| 253 | super.destroyHandle (); | ||
| 254 | } | ||
| 255 | |||
| 256 | public void | ||
| 257 | setConsumer (EmacsWindowAttachmentManager.WindowConsumer consumer) | ||
| 258 | { | ||
| 259 | attached = consumer; | ||
| 260 | } | ||
| 261 | |||
| 262 | public EmacsWindowAttachmentManager.WindowConsumer | ||
| 263 | getAttachedConsumer () | ||
| 264 | { | ||
| 265 | return attached; | ||
| 266 | } | ||
| 267 | |||
| 268 | public synchronized long | ||
| 269 | viewLayout (int left, int top, int right, int bottom) | ||
| 270 | { | ||
| 271 | int rectWidth, rectHeight; | ||
| 272 | |||
| 273 | rect.left = left; | ||
| 274 | rect.top = top; | ||
| 275 | rect.right = right; | ||
| 276 | rect.bottom = bottom; | ||
| 277 | |||
| 278 | rectWidth = right - left; | ||
| 279 | rectHeight = bottom - top; | ||
| 280 | |||
| 281 | /* If parent is null, use xPosition and yPosition instead of the | ||
| 282 | geometry rectangle positions. */ | ||
| 283 | |||
| 284 | if (parent == null) | ||
| 285 | { | ||
| 286 | left = xPosition; | ||
| 287 | top = yPosition; | ||
| 288 | } | ||
| 289 | |||
| 290 | return EmacsNative.sendConfigureNotify (this.handle, | ||
| 291 | System.currentTimeMillis (), | ||
| 292 | left, top, rectWidth, | ||
| 293 | rectHeight); | ||
| 294 | } | ||
| 295 | |||
| 296 | public void | ||
| 297 | requestViewLayout () | ||
| 298 | { | ||
| 299 | view.explicitlyDirtyBitmap (); | ||
| 300 | |||
| 301 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 302 | @Override | ||
| 303 | public void | ||
| 304 | run () | ||
| 305 | { | ||
| 306 | if (overrideRedirect) | ||
| 307 | /* Set the layout parameters again. */ | ||
| 308 | view.setLayoutParams (getWindowLayoutParams ()); | ||
| 309 | |||
| 310 | view.mustReportLayout = true; | ||
| 311 | view.requestLayout (); | ||
| 312 | } | ||
| 313 | }); | ||
| 314 | } | ||
| 315 | |||
| 316 | public synchronized void | ||
| 317 | resizeWindow (int width, int height) | ||
| 318 | { | ||
| 319 | rect.right = rect.left + width; | ||
| 320 | rect.bottom = rect.top + height; | ||
| 321 | |||
| 322 | requestViewLayout (); | ||
| 323 | } | ||
| 324 | |||
| 325 | public synchronized void | ||
| 326 | moveWindow (int x, int y) | ||
| 327 | { | ||
| 328 | int width, height; | ||
| 329 | |||
| 330 | width = rect.width (); | ||
| 331 | height = rect.height (); | ||
| 332 | |||
| 333 | rect.left = x; | ||
| 334 | rect.top = y; | ||
| 335 | rect.right = x + width; | ||
| 336 | rect.bottom = y + height; | ||
| 337 | |||
| 338 | requestViewLayout (); | ||
| 339 | } | ||
| 340 | |||
| 341 | private WindowManager.LayoutParams | ||
| 342 | getWindowLayoutParams () | ||
| 343 | { | ||
| 344 | WindowManager.LayoutParams params; | ||
| 345 | int flags, type; | ||
| 346 | Rect rect; | ||
| 347 | |||
| 348 | flags = 0; | ||
| 349 | rect = getGeometry (); | ||
| 350 | flags |= WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; | ||
| 351 | flags |= WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE; | ||
| 352 | type = WindowManager.LayoutParams.TYPE_APPLICATION_ATTACHED_DIALOG; | ||
| 353 | |||
| 354 | params | ||
| 355 | = new WindowManager.LayoutParams (rect.width (), rect.height (), | ||
| 356 | rect.left, rect.top, | ||
| 357 | type, flags, | ||
| 358 | PixelFormat.RGBA_8888); | ||
| 359 | params.gravity = Gravity.TOP | Gravity.LEFT; | ||
| 360 | return params; | ||
| 361 | } | ||
| 362 | |||
| 363 | private Context | ||
| 364 | findSuitableActivityContext () | ||
| 365 | { | ||
| 366 | /* Find a recently focused activity. */ | ||
| 367 | if (!EmacsActivity.focusedActivities.isEmpty ()) | ||
| 368 | return EmacsActivity.focusedActivities.get (0); | ||
| 369 | |||
| 370 | /* Return the service context, which probably won't work. */ | ||
| 371 | return EmacsService.SERVICE; | ||
| 372 | } | ||
| 373 | |||
| 374 | public synchronized void | ||
| 375 | mapWindow () | ||
| 376 | { | ||
| 377 | final int width, height; | ||
| 378 | |||
| 379 | if (isMapped) | ||
| 380 | return; | ||
| 381 | |||
| 382 | isMapped = true; | ||
| 383 | width = rect.width (); | ||
| 384 | height = rect.height (); | ||
| 385 | |||
| 386 | if (parent == null) | ||
| 387 | { | ||
| 388 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 389 | @Override | ||
| 390 | public void | ||
| 391 | run () | ||
| 392 | { | ||
| 393 | EmacsWindowAttachmentManager manager; | ||
| 394 | WindowManager windowManager; | ||
| 395 | Context ctx; | ||
| 396 | Object tem; | ||
| 397 | WindowManager.LayoutParams params; | ||
| 398 | |||
| 399 | /* Make the view visible, first of all. */ | ||
| 400 | view.setVisibility (View.VISIBLE); | ||
| 401 | |||
| 402 | if (!overrideRedirect) | ||
| 403 | { | ||
| 404 | manager = EmacsWindowAttachmentManager.MANAGER; | ||
| 405 | |||
| 406 | /* If parent is the root window, notice that there are new | ||
| 407 | children available for interested activites to pick | ||
| 408 | up. */ | ||
| 409 | manager.registerWindow (EmacsWindow.this); | ||
| 410 | |||
| 411 | if (!getDontFocusOnMap ()) | ||
| 412 | /* Eventually this should check no-focus-on-map. */ | ||
| 413 | view.requestFocus (); | ||
| 414 | } | ||
| 415 | else | ||
| 416 | { | ||
| 417 | /* But if the window is an override-redirect window, | ||
| 418 | then: | ||
| 419 | |||
| 420 | - Find an activity that is currently active. | ||
| 421 | |||
| 422 | - Map the window as a panel on top of that | ||
| 423 | activity using the system window manager. */ | ||
| 424 | |||
| 425 | ctx = findSuitableActivityContext (); | ||
| 426 | tem = ctx.getSystemService (Context.WINDOW_SERVICE); | ||
| 427 | windowManager = (WindowManager) tem; | ||
| 428 | |||
| 429 | /* Calculate layout parameters. */ | ||
| 430 | params = getWindowLayoutParams (); | ||
| 431 | view.setLayoutParams (params); | ||
| 432 | |||
| 433 | /* Attach the view. */ | ||
| 434 | try | ||
| 435 | { | ||
| 436 | view.prepareForLayout (width, height); | ||
| 437 | windowManager.addView (view, params); | ||
| 438 | |||
| 439 | /* Record the window manager being used in the | ||
| 440 | EmacsWindow object. */ | ||
| 441 | EmacsWindow.this.windowManager = windowManager; | ||
| 442 | } | ||
| 443 | catch (Exception e) | ||
| 444 | { | ||
| 445 | Log.w (TAG, | ||
| 446 | "failed to attach override-redirect window, " + e); | ||
| 447 | } | ||
| 448 | } | ||
| 449 | } | ||
| 450 | }); | ||
| 451 | } | ||
| 452 | else | ||
| 453 | { | ||
| 454 | /* Do the same thing as above, but don't register this | ||
| 455 | window. */ | ||
| 456 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 457 | @Override | ||
| 458 | public void | ||
| 459 | run () | ||
| 460 | { | ||
| 461 | /* Prior to mapping the view, set its measuredWidth and | ||
| 462 | measuredHeight to some reasonable value, in order to | ||
| 463 | avoid excessive bitmap dirtying. */ | ||
| 464 | |||
| 465 | view.prepareForLayout (width, height); | ||
| 466 | view.setVisibility (View.VISIBLE); | ||
| 467 | |||
| 468 | if (!getDontFocusOnMap ()) | ||
| 469 | view.requestFocus (); | ||
| 470 | } | ||
| 471 | }); | ||
| 472 | } | ||
| 473 | } | ||
| 474 | |||
| 475 | public synchronized void | ||
| 476 | unmapWindow () | ||
| 477 | { | ||
| 478 | if (!isMapped) | ||
| 479 | return; | ||
| 480 | |||
| 481 | isMapped = false; | ||
| 482 | |||
| 483 | view.post (new Runnable () { | ||
| 484 | @Override | ||
| 485 | public void | ||
| 486 | run () | ||
| 487 | { | ||
| 488 | EmacsWindowAttachmentManager manager; | ||
| 489 | |||
| 490 | manager = EmacsWindowAttachmentManager.MANAGER; | ||
| 491 | |||
| 492 | view.setVisibility (View.GONE); | ||
| 493 | |||
| 494 | /* Detach the view from the window manager if possible. */ | ||
| 495 | if (windowManager != null) | ||
| 496 | windowManager.removeView (view); | ||
| 497 | windowManager = null; | ||
| 498 | |||
| 499 | /* Now that the window is unmapped, unregister it as | ||
| 500 | well. */ | ||
| 501 | manager.detachWindow (EmacsWindow.this); | ||
| 502 | } | ||
| 503 | }); | ||
| 504 | } | ||
| 505 | |||
| 506 | @Override | ||
| 507 | public Canvas | ||
| 508 | lockCanvas (EmacsGC gc) | ||
| 509 | { | ||
| 510 | return view.getCanvas (gc); | ||
| 511 | } | ||
| 512 | |||
| 513 | @Override | ||
| 514 | public void | ||
| 515 | damageRect (Rect damageRect) | ||
| 516 | { | ||
| 517 | view.damageRect (damageRect); | ||
| 518 | } | ||
| 519 | |||
| 520 | public void | ||
| 521 | swapBuffers () | ||
| 522 | { | ||
| 523 | view.swapBuffers (); | ||
| 524 | } | ||
| 525 | |||
| 526 | public void | ||
| 527 | clearWindow () | ||
| 528 | { | ||
| 529 | EmacsService.SERVICE.fillRectangle (this, scratchGC, | ||
| 530 | 0, 0, rect.width (), | ||
| 531 | rect.height ()); | ||
| 532 | } | ||
| 533 | |||
| 534 | public void | ||
| 535 | clearArea (int x, int y, int width, int height) | ||
| 536 | { | ||
| 537 | EmacsService.SERVICE.fillRectangle (this, scratchGC, | ||
| 538 | x, y, width, height); | ||
| 539 | } | ||
| 540 | |||
| 541 | @Override | ||
| 542 | public Bitmap | ||
| 543 | getBitmap () | ||
| 544 | { | ||
| 545 | return view.getBitmap (); | ||
| 546 | } | ||
| 547 | |||
| 548 | /* event.getCharacters is used because older input methods still | ||
| 549 | require it. */ | ||
| 550 | @SuppressWarnings ("deprecation") | ||
| 551 | public int | ||
| 552 | getEventUnicodeChar (KeyEvent event, int state) | ||
| 553 | { | ||
| 554 | String characters; | ||
| 555 | |||
| 556 | if (event.getUnicodeChar (state) != 0) | ||
| 557 | return event.getUnicodeChar (state); | ||
| 558 | |||
| 559 | characters = event.getCharacters (); | ||
| 560 | |||
| 561 | if (characters != null && characters.length () == 1) | ||
| 562 | return characters.charAt (0); | ||
| 563 | |||
| 564 | return characters == null ? 0 : -1; | ||
| 565 | } | ||
| 566 | |||
| 567 | public void | ||
| 568 | saveUnicodeString (int serial, String string) | ||
| 569 | { | ||
| 570 | eventStrings.put (serial, string); | ||
| 571 | } | ||
| 572 | |||
| 573 | |||
| 574 | |||
| 575 | /* Return the modifier mask associated with the specified keyboard | ||
| 576 | input EVENT. Replace bits corresponding to Left or Right keys | ||
| 577 | with their corresponding general modifier bits. */ | ||
| 578 | |||
| 579 | private int | ||
| 580 | eventModifiers (KeyEvent event) | ||
| 581 | { | ||
| 582 | int state; | ||
| 583 | |||
| 584 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB_MR2) | ||
| 585 | state = event.getModifiers (); | ||
| 586 | else | ||
| 587 | { | ||
| 588 | /* Replace this with getMetaState and manual | ||
| 589 | normalization. */ | ||
| 590 | state = event.getMetaState (); | ||
| 591 | |||
| 592 | /* Normalize the state by setting the generic modifier bit if | ||
| 593 | either a left or right modifier is pressed. */ | ||
| 594 | |||
| 595 | if ((state & KeyEvent.META_ALT_LEFT_ON) != 0 | ||
| 596 | || (state & KeyEvent.META_ALT_RIGHT_ON) != 0) | ||
| 597 | state |= KeyEvent.META_ALT_MASK; | ||
| 598 | |||
| 599 | if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0 | ||
| 600 | || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0) | ||
| 601 | state |= KeyEvent.META_CTRL_MASK; | ||
| 602 | } | ||
| 603 | |||
| 604 | return state; | ||
| 605 | } | ||
| 606 | |||
| 607 | /* event.getCharacters is used because older input methods still | ||
| 608 | require it. */ | ||
| 609 | @SuppressWarnings ("deprecation") | ||
| 610 | public void | ||
| 611 | onKeyDown (int keyCode, KeyEvent event) | ||
| 612 | { | ||
| 613 | int state, state_1; | ||
| 614 | long serial; | ||
| 615 | String characters; | ||
| 616 | |||
| 617 | state = eventModifiers (event); | ||
| 618 | |||
| 619 | /* Ignore meta-state understood by Emacs for now, or key presses | ||
| 620 | such as Ctrl+C and Meta+C will not be recognized as an ASCII | ||
| 621 | key press event. */ | ||
| 622 | |||
| 623 | state_1 | ||
| 624 | = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK | ||
| 625 | | KeyEvent.META_SYM_ON | KeyEvent.META_META_MASK); | ||
| 626 | |||
| 627 | synchronized (eventStrings) | ||
| 628 | { | ||
| 629 | serial | ||
| 630 | = EmacsNative.sendKeyPress (this.handle, | ||
| 631 | event.getEventTime (), | ||
| 632 | state, keyCode, | ||
| 633 | getEventUnicodeChar (event, | ||
| 634 | state_1)); | ||
| 635 | |||
| 636 | characters = event.getCharacters (); | ||
| 637 | |||
| 638 | if (characters != null && characters.length () > 1) | ||
| 639 | saveUnicodeString ((int) serial, characters); | ||
| 640 | } | ||
| 641 | } | ||
| 642 | |||
| 643 | public void | ||
| 644 | onKeyUp (int keyCode, KeyEvent event) | ||
| 645 | { | ||
| 646 | int state, state_1; | ||
| 647 | long time; | ||
| 648 | |||
| 649 | /* Compute the event's modifier mask. */ | ||
| 650 | state = eventModifiers (event); | ||
| 651 | |||
| 652 | /* Ignore meta-state understood by Emacs for now, or key presses | ||
| 653 | such as Ctrl+C and Meta+C will not be recognized as an ASCII | ||
| 654 | key press event. */ | ||
| 655 | |||
| 656 | state_1 | ||
| 657 | = state & ~(KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK | ||
| 658 | | KeyEvent.META_SYM_ON | KeyEvent.META_META_MASK); | ||
| 659 | |||
| 660 | EmacsNative.sendKeyRelease (this.handle, | ||
| 661 | event.getEventTime (), | ||
| 662 | state, keyCode, | ||
| 663 | getEventUnicodeChar (event, | ||
| 664 | state_1)); | ||
| 665 | |||
| 666 | if (keyCode == KeyEvent.KEYCODE_VOLUME_DOWN) | ||
| 667 | { | ||
| 668 | /* Check if this volume down press should quit Emacs. | ||
| 669 | Most Android devices have no physical keyboard, so it | ||
| 670 | is unreasonably hard to press C-g. */ | ||
| 671 | |||
| 672 | time = event.getEventTime (); | ||
| 673 | |||
| 674 | if (time - lastVolumeButtonRelease < 350) | ||
| 675 | EmacsNative.quit (); | ||
| 676 | |||
| 677 | lastVolumeButtonRelease = time; | ||
| 678 | } | ||
| 679 | } | ||
| 680 | |||
| 681 | public void | ||
| 682 | onFocusChanged (boolean gainFocus) | ||
| 683 | { | ||
| 684 | EmacsActivity.invalidateFocus (); | ||
| 685 | } | ||
| 686 | |||
| 687 | /* Notice that the activity has been detached or destroyed. | ||
| 688 | |||
| 689 | ISFINISHING is set if the activity is not the main activity, or | ||
| 690 | if the activity was not destroyed in response to explicit user | ||
| 691 | action. */ | ||
| 692 | |||
| 693 | public void | ||
| 694 | onActivityDetached (boolean isFinishing) | ||
| 695 | { | ||
| 696 | /* Destroy the associated frame when the activity is detached in | ||
| 697 | response to explicit user action. */ | ||
| 698 | |||
| 699 | if (isFinishing) | ||
| 700 | EmacsNative.sendWindowAction (this.handle, 0); | ||
| 701 | } | ||
| 702 | |||
| 703 | |||
| 704 | |||
| 705 | /* Mouse and touch event handling. | ||
| 706 | |||
| 707 | Android does not conceptually distinguish between mouse events | ||
| 708 | (those coming from a device whose movement affects the on-screen | ||
| 709 | pointer image) and touch screen events. Each click or touch | ||
| 710 | starts a single pointer gesture sequence, and subsequent motion | ||
| 711 | of the device will result in updates being reported relative to | ||
| 712 | that sequence until the mouse button or touch is released. | ||
| 713 | |||
| 714 | When a touch, click, or pointer motion takes place, several kinds | ||
| 715 | of event can be sent: | ||
| 716 | |||
| 717 | ACTION_DOWN or ACTION_POINTER_DOWN is sent with a new coordinate | ||
| 718 | and an associated ``pointer ID'' identifying the event and its | ||
| 719 | gesture sequence when a click or touch takes place. Emacs is | ||
| 720 | responsible for recording both the position and pointer ID of | ||
| 721 | this click for the purpose of determining future changes to its | ||
| 722 | position. | ||
| 723 | |||
| 724 | ACTION_UP or ACTION_POINTER_UP is sent with a pointer ID when the | ||
| 725 | click associated with a previous ACTION_DOWN event is released. | ||
| 726 | |||
| 727 | ACTION_CANCEL (or ACTION_POINTER_UP with FLAG_CANCELED) is sent | ||
| 728 | if a similar situation transpires: the window system has chosen | ||
| 729 | to grab the click, and future changes to its position will no | ||
| 730 | longer be reported to Emacs. | ||
| 731 | |||
| 732 | ACTION_MOVE is sent if a coordinate tied to a click that has not | ||
| 733 | been released changes. Emacs processes this event by comparing | ||
| 734 | each of the coordinates within the event with its recollection of | ||
| 735 | those contained within prior ACTION_DOWN and ACTION_MOVE events; | ||
| 736 | the pointer ID of the differing coordinate is then reported | ||
| 737 | within a touch or pointer motion event along with its new | ||
| 738 | position. | ||
| 739 | |||
| 740 | The events described above are all sent for both touch and mouse | ||
| 741 | click events. Determining whether an ACTION_DOWN event is | ||
| 742 | associated with a button event is performed by inspecting the | ||
| 743 | mouse button state associated with that event. If it contains | ||
| 744 | any mouse buttons that were not contained in the button state at | ||
| 745 | the time of the last ACTION_DOWN or ACTION_UP event, the | ||
| 746 | coordinate contained within is assumed to be a mouse click, | ||
| 747 | leading to it and associated motion or ACTION_UP events being | ||
| 748 | reported as mouse button or motion events. Otherwise, those | ||
| 749 | events are reported as touch screen events, with the touch ID set | ||
| 750 | to the pointer ID. | ||
| 751 | |||
| 752 | In addition to the events illustrated above, Android also sends | ||
| 753 | several other types of event upon select types of activity from a | ||
| 754 | mouse device: | ||
| 755 | |||
| 756 | ACTION_HOVER_MOVE is sent with the coordinate of the mouse | ||
| 757 | pointer if it moves above a frame prior to any click taking | ||
| 758 | place. Emacs sends a mouse motion event containing the | ||
| 759 | coordinate. | ||
| 760 | |||
| 761 | ACTION_HOVER_ENTER and ACTION_HOVER_LEAVE are respectively sent | ||
| 762 | when the mouse pointer enters and leaves a frame. Moreover, | ||
| 763 | ACTION_HOVER_LEAVE events are sent immediately before an | ||
| 764 | ACTION_DOWN event associated with a mouse click. These | ||
| 765 | extraneous events are distinct in that their button states always | ||
| 766 | contain an additional button compared to the button state | ||
| 767 | recorded at the time of the last ACTION_UP event. | ||
| 768 | |||
| 769 | On Android 6.0 and later, ACTION_BUTTON_PRESS is sent with the | ||
| 770 | coordinate of the mouse pointer if a mouse click occurs, | ||
| 771 | alongside a ACTION_DOWN event. ACTION_BUTTON_RELEASE is sent | ||
| 772 | with the same information upon a mouse click being released, also | ||
| 773 | accompanying an ACTION_UP event. | ||
| 774 | |||
| 775 | However, both types of button events are implemented in a buggy | ||
| 776 | fashion and cannot be used to report button events. */ | ||
| 777 | |||
| 778 | /* Look through the button state to determine what button EVENT was | ||
| 779 | generated from. DOWN is true if EVENT is a button press event, | ||
| 780 | false otherwise. Value is the X number of the button. */ | ||
| 781 | |||
| 782 | private int | ||
| 783 | whatButtonWasIt (MotionEvent event, boolean down) | ||
| 784 | { | ||
| 785 | int eventState, notIn; | ||
| 786 | |||
| 787 | /* Obtain the new button state. */ | ||
| 788 | eventState = event.getButtonState (); | ||
| 789 | |||
| 790 | /* Compute which button is now set or no longer set. */ | ||
| 791 | |||
| 792 | notIn = (down ? eventState & ~lastButtonState | ||
| 793 | : lastButtonState & ~eventState); | ||
| 794 | |||
| 795 | if ((notIn & (MotionEvent.BUTTON_PRIMARY | ||
| 796 | | MotionEvent.BUTTON_SECONDARY | ||
| 797 | | MotionEvent.BUTTON_TERTIARY)) == 0) | ||
| 798 | /* No buttons have been pressed, so this is a touch event. */ | ||
| 799 | return 0; | ||
| 800 | |||
| 801 | if ((notIn & MotionEvent.BUTTON_PRIMARY) != 0) | ||
| 802 | return 1; | ||
| 803 | |||
| 804 | if ((notIn & MotionEvent.BUTTON_SECONDARY) != 0) | ||
| 805 | return 3; | ||
| 806 | |||
| 807 | if ((notIn & MotionEvent.BUTTON_TERTIARY) != 0) | ||
| 808 | return 2; | ||
| 809 | |||
| 810 | /* Buttons 4, 5, 6 and 7 are actually scroll wheels under X. | ||
| 811 | Thus, report additional buttons starting at 8. */ | ||
| 812 | |||
| 813 | if ((notIn & MotionEvent.BUTTON_BACK) != 0) | ||
| 814 | return 8; | ||
| 815 | |||
| 816 | if ((notIn & MotionEvent.BUTTON_FORWARD) != 0) | ||
| 817 | return 9; | ||
| 818 | |||
| 819 | /* Report stylus events as touch screen events. */ | ||
| 820 | |||
| 821 | if ((notIn & MotionEvent.BUTTON_STYLUS_PRIMARY) != 0) | ||
| 822 | return 0; | ||
| 823 | |||
| 824 | if ((notIn & MotionEvent.BUTTON_STYLUS_SECONDARY) != 0) | ||
| 825 | return 0; | ||
| 826 | |||
| 827 | /* Not a real value. */ | ||
| 828 | return 11; | ||
| 829 | } | ||
| 830 | |||
| 831 | /* Return the mouse button associated with the specified ACTION_DOWN | ||
| 832 | or ACTION_POINTER_DOWN EVENT. | ||
| 833 | |||
| 834 | Value is 0 if no mouse button was pressed, or the X number of | ||
| 835 | that mouse button. */ | ||
| 836 | |||
| 837 | private int | ||
| 838 | buttonForEvent (MotionEvent event) | ||
| 839 | { | ||
| 840 | /* ICS and earlier don't support true mouse button events, so | ||
| 841 | treat all down events as touch screen events. */ | ||
| 842 | |||
| 843 | if (Build.VERSION.SDK_INT | ||
| 844 | < Build.VERSION_CODES.ICE_CREAM_SANDWICH) | ||
| 845 | return 0; | ||
| 846 | |||
| 847 | return whatButtonWasIt (event, true); | ||
| 848 | } | ||
| 849 | |||
| 850 | /* Return the coordinate object associated with the specified | ||
| 851 | EVENT, or null if it is not known. */ | ||
| 852 | |||
| 853 | private Coordinate | ||
| 854 | figureChange (MotionEvent event) | ||
| 855 | { | ||
| 856 | int i, truncatedX, truncatedY, pointerIndex, pointerID, count; | ||
| 857 | Coordinate coordinate; | ||
| 858 | |||
| 859 | /* Initialize this variable now. */ | ||
| 860 | coordinate = null; | ||
| 861 | |||
| 862 | switch (event.getActionMasked ()) | ||
| 863 | { | ||
| 864 | case MotionEvent.ACTION_DOWN: | ||
| 865 | /* Primary pointer pressed with index 0. */ | ||
| 866 | |||
| 867 | pointerID = event.getPointerId (0); | ||
| 868 | coordinate = new Coordinate ((int) event.getX (0), | ||
| 869 | (int) event.getY (0), | ||
| 870 | buttonForEvent (event), | ||
| 871 | pointerID); | ||
| 872 | pointerMap.put (pointerID, coordinate); | ||
| 873 | break; | ||
| 874 | |||
| 875 | case MotionEvent.ACTION_UP: | ||
| 876 | case MotionEvent.ACTION_CANCEL: | ||
| 877 | /* Primary pointer released with index 0. */ | ||
| 878 | pointerID = event.getPointerId (0); | ||
| 879 | coordinate = pointerMap.remove (pointerID); | ||
| 880 | break; | ||
| 881 | |||
| 882 | case MotionEvent.ACTION_POINTER_DOWN: | ||
| 883 | /* New pointer. Find the pointer ID from the index and place | ||
| 884 | it in the map. */ | ||
| 885 | pointerIndex = event.getActionIndex (); | ||
| 886 | pointerID = event.getPointerId (pointerIndex); | ||
| 887 | coordinate = new Coordinate ((int) event.getX (0), | ||
| 888 | (int) event.getY (0), | ||
| 889 | buttonForEvent (event), | ||
| 890 | pointerID); | ||
| 891 | pointerMap.put (pointerID, coordinate); | ||
| 892 | break; | ||
| 893 | |||
| 894 | case MotionEvent.ACTION_POINTER_UP: | ||
| 895 | /* Pointer removed. Remove it from the map. */ | ||
| 896 | pointerIndex = event.getActionIndex (); | ||
| 897 | pointerID = event.getPointerId (pointerIndex); | ||
| 898 | coordinate = pointerMap.remove (pointerID); | ||
| 899 | break; | ||
| 900 | |||
| 901 | default: | ||
| 902 | |||
| 903 | /* Loop through each pointer in the event. */ | ||
| 904 | |||
| 905 | count = event.getPointerCount (); | ||
| 906 | for (i = 0; i < count; ++i) | ||
| 907 | { | ||
| 908 | pointerID = event.getPointerId (i); | ||
| 909 | |||
| 910 | /* Look up that pointer in the map. */ | ||
| 911 | coordinate = pointerMap.get (pointerID); | ||
| 912 | |||
| 913 | if (coordinate != null) | ||
| 914 | { | ||
| 915 | /* See if coordinates have changed. */ | ||
| 916 | truncatedX = (int) event.getX (i); | ||
| 917 | truncatedY = (int) event.getY (i); | ||
| 918 | |||
| 919 | if (truncatedX != coordinate.x | ||
| 920 | || truncatedY != coordinate.y) | ||
| 921 | { | ||
| 922 | /* The pointer changed. Update the coordinate and | ||
| 923 | break out of the loop. */ | ||
| 924 | coordinate.x = truncatedX; | ||
| 925 | coordinate.y = truncatedY; | ||
| 926 | |||
| 927 | break; | ||
| 928 | } | ||
| 929 | } | ||
| 930 | } | ||
| 931 | |||
| 932 | /* Set coordinate to NULL if the loop failed to find any | ||
| 933 | matching pointer. */ | ||
| 934 | |||
| 935 | if (i == count) | ||
| 936 | coordinate = null; | ||
| 937 | } | ||
| 938 | |||
| 939 | /* Return the pointer ID. */ | ||
| 940 | return coordinate; | ||
| 941 | } | ||
| 942 | |||
| 943 | /* Return the modifier mask associated with the specified motion | ||
| 944 | EVENT. Replace bits corresponding to Left or Right keys with | ||
| 945 | their corresponding general modifier bits. */ | ||
| 946 | |||
| 947 | private int | ||
| 948 | motionEventModifiers (MotionEvent event) | ||
| 949 | { | ||
| 950 | int state; | ||
| 951 | |||
| 952 | state = event.getMetaState (); | ||
| 953 | |||
| 954 | /* Normalize the state by setting the generic modifier bit if | ||
| 955 | either a left or right modifier is pressed. */ | ||
| 956 | |||
| 957 | if ((state & KeyEvent.META_ALT_LEFT_ON) != 0 | ||
| 958 | || (state & KeyEvent.META_ALT_RIGHT_ON) != 0) | ||
| 959 | state |= KeyEvent.META_ALT_MASK; | ||
| 960 | |||
| 961 | if ((state & KeyEvent.META_CTRL_LEFT_ON) != 0 | ||
| 962 | || (state & KeyEvent.META_CTRL_RIGHT_ON) != 0) | ||
| 963 | state |= KeyEvent.META_CTRL_MASK; | ||
| 964 | |||
| 965 | return state; | ||
| 966 | } | ||
| 967 | |||
| 968 | /* Process a single ACTION_DOWN, ACTION_POINTER_DOWN, ACTION_UP, | ||
| 969 | ACTION_POINTER_UP, ACTION_CANCEL, or ACTION_MOVE event. | ||
| 970 | |||
| 971 | Ascertain which coordinate changed and send an appropriate mouse | ||
| 972 | or touch screen event. */ | ||
| 973 | |||
| 974 | private void | ||
| 975 | motionEvent (MotionEvent event) | ||
| 976 | { | ||
| 977 | Coordinate coordinate; | ||
| 978 | int modifiers; | ||
| 979 | long time; | ||
| 980 | |||
| 981 | /* Find data associated with this event's pointer. Namely, its | ||
| 982 | current location, whether or not a change has taken place, and | ||
| 983 | whether or not it is a button event. */ | ||
| 984 | |||
| 985 | coordinate = figureChange (event); | ||
| 986 | |||
| 987 | if (coordinate == null) | ||
| 988 | return; | ||
| 989 | |||
| 990 | time = event.getEventTime (); | ||
| 991 | |||
| 992 | if (coordinate.button != 0) | ||
| 993 | { | ||
| 994 | /* This event is tied to a mouse click, so report mouse motion | ||
| 995 | and button events. */ | ||
| 996 | |||
| 997 | modifiers = motionEventModifiers (event); | ||
| 998 | |||
| 999 | switch (event.getAction ()) | ||
| 1000 | { | ||
| 1001 | case MotionEvent.ACTION_POINTER_DOWN: | ||
| 1002 | case MotionEvent.ACTION_DOWN: | ||
| 1003 | EmacsNative.sendButtonPress (this.handle, coordinate.x, | ||
| 1004 | coordinate.y, time, modifiers, | ||
| 1005 | coordinate.button); | ||
| 1006 | break; | ||
| 1007 | |||
| 1008 | case MotionEvent.ACTION_POINTER_UP: | ||
| 1009 | case MotionEvent.ACTION_UP: | ||
| 1010 | case MotionEvent.ACTION_CANCEL: | ||
| 1011 | EmacsNative.sendButtonRelease (this.handle, coordinate.x, | ||
| 1012 | coordinate.y, time, modifiers, | ||
| 1013 | coordinate.button); | ||
| 1014 | break; | ||
| 1015 | |||
| 1016 | case MotionEvent.ACTION_MOVE: | ||
| 1017 | EmacsNative.sendMotionNotify (this.handle, coordinate.x, | ||
| 1018 | coordinate.y, time); | ||
| 1019 | break; | ||
| 1020 | } | ||
| 1021 | } | ||
| 1022 | else | ||
| 1023 | { | ||
| 1024 | /* This event is a touch event, and the touch ID is the | ||
| 1025 | pointer ID. */ | ||
| 1026 | |||
| 1027 | switch (event.getActionMasked ()) | ||
| 1028 | { | ||
| 1029 | case MotionEvent.ACTION_DOWN: | ||
| 1030 | case MotionEvent.ACTION_POINTER_DOWN: | ||
| 1031 | /* Touch down event. */ | ||
| 1032 | EmacsNative.sendTouchDown (this.handle, coordinate.x, | ||
| 1033 | coordinate.y, time, | ||
| 1034 | coordinate.id, 0); | ||
| 1035 | break; | ||
| 1036 | |||
| 1037 | case MotionEvent.ACTION_UP: | ||
| 1038 | case MotionEvent.ACTION_POINTER_UP: | ||
| 1039 | /* Touch up event. */ | ||
| 1040 | EmacsNative.sendTouchUp (this.handle, coordinate.x, | ||
| 1041 | coordinate.y, time, | ||
| 1042 | coordinate.id, 0); | ||
| 1043 | break; | ||
| 1044 | |||
| 1045 | case MotionEvent.ACTION_CANCEL: | ||
| 1046 | /* Touch sequence cancellation event. */ | ||
| 1047 | EmacsNative.sendTouchUp (this.handle, coordinate.x, | ||
| 1048 | coordinate.y, time, | ||
| 1049 | coordinate.id, | ||
| 1050 | 1 /* ANDROID_TOUCH_SEQUENCE_CANCELED */); | ||
| 1051 | break; | ||
| 1052 | |||
| 1053 | case MotionEvent.ACTION_MOVE: | ||
| 1054 | /* Pointer motion event. */ | ||
| 1055 | EmacsNative.sendTouchMove (this.handle, coordinate.x, | ||
| 1056 | coordinate.y, time, | ||
| 1057 | coordinate.id, 0); | ||
| 1058 | break; | ||
| 1059 | } | ||
| 1060 | } | ||
| 1061 | |||
| 1062 | if (Build.VERSION.SDK_INT | ||
| 1063 | < Build.VERSION_CODES.ICE_CREAM_SANDWICH) | ||
| 1064 | return; | ||
| 1065 | |||
| 1066 | /* Now update the button state. */ | ||
| 1067 | lastButtonState = event.getButtonState (); | ||
| 1068 | return; | ||
| 1069 | } | ||
| 1070 | |||
| 1071 | public boolean | ||
| 1072 | onTouchEvent (MotionEvent event) | ||
| 1073 | { | ||
| 1074 | switch (event.getActionMasked ()) | ||
| 1075 | { | ||
| 1076 | case MotionEvent.ACTION_DOWN: | ||
| 1077 | case MotionEvent.ACTION_POINTER_DOWN: | ||
| 1078 | case MotionEvent.ACTION_UP: | ||
| 1079 | case MotionEvent.ACTION_POINTER_UP: | ||
| 1080 | case MotionEvent.ACTION_CANCEL: | ||
| 1081 | case MotionEvent.ACTION_MOVE: | ||
| 1082 | motionEvent (event); | ||
| 1083 | return true; | ||
| 1084 | } | ||
| 1085 | |||
| 1086 | return false; | ||
| 1087 | } | ||
| 1088 | |||
| 1089 | public boolean | ||
| 1090 | onGenericMotionEvent (MotionEvent event) | ||
| 1091 | { | ||
| 1092 | switch (event.getAction ()) | ||
| 1093 | { | ||
| 1094 | case MotionEvent.ACTION_HOVER_ENTER: | ||
| 1095 | EmacsNative.sendEnterNotify (this.handle, (int) event.getX (), | ||
| 1096 | (int) event.getY (), | ||
| 1097 | event.getEventTime ()); | ||
| 1098 | return true; | ||
| 1099 | |||
| 1100 | case MotionEvent.ACTION_HOVER_MOVE: | ||
| 1101 | EmacsNative.sendMotionNotify (this.handle, (int) event.getX (), | ||
| 1102 | (int) event.getY (), | ||
| 1103 | event.getEventTime ()); | ||
| 1104 | return true; | ||
| 1105 | |||
| 1106 | case MotionEvent.ACTION_HOVER_EXIT: | ||
| 1107 | |||
| 1108 | /* If the exit event comes from a button press, its button | ||
| 1109 | state will have extra bits compared to the last known | ||
| 1110 | button state. Since the exit event will interfere with | ||
| 1111 | tool bar button presses, ignore such splurious events. */ | ||
| 1112 | |||
| 1113 | if ((event.getButtonState () & ~lastButtonState) == 0) | ||
| 1114 | EmacsNative.sendLeaveNotify (this.handle, (int) event.getX (), | ||
| 1115 | (int) event.getY (), | ||
| 1116 | event.getEventTime ()); | ||
| 1117 | |||
| 1118 | return true; | ||
| 1119 | |||
| 1120 | case MotionEvent.ACTION_DOWN: | ||
| 1121 | case MotionEvent.ACTION_POINTER_DOWN: | ||
| 1122 | case MotionEvent.ACTION_UP: | ||
| 1123 | case MotionEvent.ACTION_POINTER_UP: | ||
| 1124 | case MotionEvent.ACTION_CANCEL: | ||
| 1125 | case MotionEvent.ACTION_MOVE: | ||
| 1126 | /* MotionEvents may either be sent to onGenericMotionEvent or | ||
| 1127 | onTouchEvent depending on if Android thinks it is a mouse | ||
| 1128 | event or not, but we detect them ourselves. */ | ||
| 1129 | motionEvent (event); | ||
| 1130 | return true; | ||
| 1131 | |||
| 1132 | case MotionEvent.ACTION_SCROLL: | ||
| 1133 | /* Send a scroll event with the specified deltas. */ | ||
| 1134 | EmacsNative.sendWheel (this.handle, (int) event.getX (), | ||
| 1135 | (int) event.getY (), | ||
| 1136 | event.getEventTime (), | ||
| 1137 | motionEventModifiers (event), | ||
| 1138 | event.getAxisValue (MotionEvent.AXIS_HSCROLL), | ||
| 1139 | event.getAxisValue (MotionEvent.AXIS_VSCROLL)); | ||
| 1140 | return true; | ||
| 1141 | } | ||
| 1142 | |||
| 1143 | return false; | ||
| 1144 | } | ||
| 1145 | |||
| 1146 | |||
| 1147 | |||
| 1148 | public synchronized void | ||
| 1149 | reparentTo (final EmacsWindow otherWindow, int x, int y) | ||
| 1150 | { | ||
| 1151 | int width, height; | ||
| 1152 | |||
| 1153 | /* Reparent this window to the other window. */ | ||
| 1154 | |||
| 1155 | if (parent != null) | ||
| 1156 | parent.children.remove (this); | ||
| 1157 | |||
| 1158 | if (otherWindow != null) | ||
| 1159 | otherWindow.children.add (this); | ||
| 1160 | |||
| 1161 | parent = otherWindow; | ||
| 1162 | |||
| 1163 | /* Move this window to the new location. */ | ||
| 1164 | width = rect.width (); | ||
| 1165 | height = rect.height (); | ||
| 1166 | rect.left = x; | ||
| 1167 | rect.top = y; | ||
| 1168 | rect.right = x + width; | ||
| 1169 | rect.bottom = y + height; | ||
| 1170 | |||
| 1171 | /* Now do the work necessary on the UI thread to reparent the | ||
| 1172 | window. */ | ||
| 1173 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 1174 | @Override | ||
| 1175 | public void | ||
| 1176 | run () | ||
| 1177 | { | ||
| 1178 | EmacsWindowAttachmentManager manager; | ||
| 1179 | ViewManager parent; | ||
| 1180 | |||
| 1181 | /* First, detach this window if necessary. */ | ||
| 1182 | manager = EmacsWindowAttachmentManager.MANAGER; | ||
| 1183 | manager.detachWindow (EmacsWindow.this); | ||
| 1184 | |||
| 1185 | /* Also unparent this view. */ | ||
| 1186 | |||
| 1187 | /* If the window manager is set, use that instead. */ | ||
| 1188 | if (windowManager != null) | ||
| 1189 | parent = windowManager; | ||
| 1190 | else | ||
| 1191 | parent = (ViewManager) view.getParent (); | ||
| 1192 | windowManager = null; | ||
| 1193 | |||
| 1194 | if (parent != null) | ||
| 1195 | parent.removeView (view); | ||
| 1196 | |||
| 1197 | /* Next, either add this window as a child of the new | ||
| 1198 | parent's view, or make it available again. */ | ||
| 1199 | if (otherWindow != null) | ||
| 1200 | otherWindow.view.addView (view); | ||
| 1201 | else if (EmacsWindow.this.isMapped) | ||
| 1202 | manager.registerWindow (EmacsWindow.this); | ||
| 1203 | |||
| 1204 | /* Request relayout. */ | ||
| 1205 | view.requestLayout (); | ||
| 1206 | } | ||
| 1207 | }); | ||
| 1208 | } | ||
| 1209 | |||
| 1210 | public void | ||
| 1211 | makeInputFocus (long time) | ||
| 1212 | { | ||
| 1213 | /* TIME is currently ignored. Request the input focus now. */ | ||
| 1214 | |||
| 1215 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 1216 | @Override | ||
| 1217 | public void | ||
| 1218 | run () | ||
| 1219 | { | ||
| 1220 | view.requestFocus (); | ||
| 1221 | } | ||
| 1222 | }); | ||
| 1223 | } | ||
| 1224 | |||
| 1225 | public synchronized void | ||
| 1226 | raise () | ||
| 1227 | { | ||
| 1228 | /* This does nothing here. */ | ||
| 1229 | if (parent == null) | ||
| 1230 | return; | ||
| 1231 | |||
| 1232 | /* Remove and add this view again. */ | ||
| 1233 | parent.children.remove (this); | ||
| 1234 | parent.children.add (this); | ||
| 1235 | |||
| 1236 | /* Request a relayout. */ | ||
| 1237 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 1238 | @Override | ||
| 1239 | public void | ||
| 1240 | run () | ||
| 1241 | { | ||
| 1242 | view.raise (); | ||
| 1243 | } | ||
| 1244 | }); | ||
| 1245 | } | ||
| 1246 | |||
| 1247 | public synchronized void | ||
| 1248 | lower () | ||
| 1249 | { | ||
| 1250 | /* This does nothing here. */ | ||
| 1251 | if (parent == null) | ||
| 1252 | return; | ||
| 1253 | |||
| 1254 | /* Remove and add this view again. */ | ||
| 1255 | parent.children.remove (this); | ||
| 1256 | parent.children.add (this); | ||
| 1257 | |||
| 1258 | /* Request a relayout. */ | ||
| 1259 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 1260 | @Override | ||
| 1261 | public void | ||
| 1262 | run () | ||
| 1263 | { | ||
| 1264 | view.lower (); | ||
| 1265 | } | ||
| 1266 | }); | ||
| 1267 | } | ||
| 1268 | |||
| 1269 | public synchronized int[] | ||
| 1270 | getWindowGeometry () | ||
| 1271 | { | ||
| 1272 | int[] array; | ||
| 1273 | |||
| 1274 | array = new int[4]; | ||
| 1275 | |||
| 1276 | array[0] = parent != null ? rect.left : xPosition; | ||
| 1277 | array[1] = parent != null ? rect.top : yPosition; | ||
| 1278 | array[2] = rect.width (); | ||
| 1279 | array[3] = rect.height (); | ||
| 1280 | |||
| 1281 | return array; | ||
| 1282 | } | ||
| 1283 | |||
| 1284 | public void | ||
| 1285 | noticeIconified () | ||
| 1286 | { | ||
| 1287 | EmacsNative.sendIconified (this.handle); | ||
| 1288 | } | ||
| 1289 | |||
| 1290 | public void | ||
| 1291 | noticeDeiconified () | ||
| 1292 | { | ||
| 1293 | EmacsNative.sendDeiconified (this.handle); | ||
| 1294 | } | ||
| 1295 | |||
| 1296 | public synchronized void | ||
| 1297 | setDontAcceptFocus (final boolean dontAcceptFocus) | ||
| 1298 | { | ||
| 1299 | /* Update the view's focus state. */ | ||
| 1300 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 1301 | @Override | ||
| 1302 | public void | ||
| 1303 | run () | ||
| 1304 | { | ||
| 1305 | view.setFocusable (!dontAcceptFocus); | ||
| 1306 | view.setFocusableInTouchMode (!dontAcceptFocus); | ||
| 1307 | } | ||
| 1308 | }); | ||
| 1309 | } | ||
| 1310 | |||
| 1311 | public synchronized void | ||
| 1312 | setDontFocusOnMap (final boolean dontFocusOnMap) | ||
| 1313 | { | ||
| 1314 | this.dontFocusOnMap = dontFocusOnMap; | ||
| 1315 | } | ||
| 1316 | |||
| 1317 | public synchronized boolean | ||
| 1318 | getDontFocusOnMap () | ||
| 1319 | { | ||
| 1320 | return dontFocusOnMap; | ||
| 1321 | } | ||
| 1322 | |||
| 1323 | public int[] | ||
| 1324 | translateCoordinates (int x, int y) | ||
| 1325 | { | ||
| 1326 | int[] array; | ||
| 1327 | |||
| 1328 | /* This is supposed to translate coordinates to the root | ||
| 1329 | window. */ | ||
| 1330 | array = new int[2]; | ||
| 1331 | EmacsService.SERVICE.getLocationOnScreen (view, array); | ||
| 1332 | |||
| 1333 | /* Now, the coordinates of the view should be in array. Offset X | ||
| 1334 | and Y by them. */ | ||
| 1335 | array[0] += x; | ||
| 1336 | array[1] += y; | ||
| 1337 | |||
| 1338 | /* Return the resulting coordinates. */ | ||
| 1339 | return array; | ||
| 1340 | } | ||
| 1341 | |||
| 1342 | public void | ||
| 1343 | toggleOnScreenKeyboard (final boolean on) | ||
| 1344 | { | ||
| 1345 | /* Even though InputMethodManager functions are thread safe, | ||
| 1346 | `showOnScreenKeyboard' etc must be called from the UI thread in | ||
| 1347 | order to avoid deadlocks if the calls happen in tandem with a | ||
| 1348 | call to a synchronizing function within | ||
| 1349 | `onCreateInputConnection'. */ | ||
| 1350 | |||
| 1351 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 1352 | @Override | ||
| 1353 | public void | ||
| 1354 | run () | ||
| 1355 | { | ||
| 1356 | if (on) | ||
| 1357 | view.showOnScreenKeyboard (); | ||
| 1358 | else | ||
| 1359 | view.hideOnScreenKeyboard (); | ||
| 1360 | } | ||
| 1361 | }); | ||
| 1362 | } | ||
| 1363 | |||
| 1364 | public String | ||
| 1365 | lookupString (int eventSerial) | ||
| 1366 | { | ||
| 1367 | String any; | ||
| 1368 | |||
| 1369 | synchronized (eventStrings) | ||
| 1370 | { | ||
| 1371 | any = eventStrings.remove (eventSerial); | ||
| 1372 | } | ||
| 1373 | |||
| 1374 | return any; | ||
| 1375 | } | ||
| 1376 | |||
| 1377 | public void | ||
| 1378 | setFullscreen (final boolean isFullscreen) | ||
| 1379 | { | ||
| 1380 | EmacsService.SERVICE.runOnUiThread (new Runnable () { | ||
| 1381 | @Override | ||
| 1382 | public void | ||
| 1383 | run () | ||
| 1384 | { | ||
| 1385 | EmacsActivity activity; | ||
| 1386 | Object tem; | ||
| 1387 | |||
| 1388 | fullscreen = isFullscreen; | ||
| 1389 | tem = getAttachedConsumer (); | ||
| 1390 | |||
| 1391 | if (tem != null) | ||
| 1392 | { | ||
| 1393 | activity = (EmacsActivity) tem; | ||
| 1394 | activity.syncFullscreenWith (EmacsWindow.this); | ||
| 1395 | } | ||
| 1396 | } | ||
| 1397 | }); | ||
| 1398 | } | ||
| 1399 | |||
| 1400 | public void | ||
| 1401 | defineCursor (final EmacsCursor cursor) | ||
| 1402 | { | ||
| 1403 | /* Don't post this message if pointer icons aren't supported. */ | ||
| 1404 | |||
| 1405 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) | ||
| 1406 | view.post (new Runnable () { | ||
| 1407 | @Override | ||
| 1408 | public void | ||
| 1409 | run () | ||
| 1410 | { | ||
| 1411 | if (cursor != null) | ||
| 1412 | view.setPointerIcon (cursor.icon); | ||
| 1413 | else | ||
| 1414 | view.setPointerIcon (null); | ||
| 1415 | } | ||
| 1416 | }); | ||
| 1417 | } | ||
| 1418 | |||
| 1419 | public synchronized void | ||
| 1420 | notifyContentRectPosition (int xPosition, int yPosition) | ||
| 1421 | { | ||
| 1422 | Rect geometry; | ||
| 1423 | |||
| 1424 | /* Ignore these notifications if not a child of the root | ||
| 1425 | window. */ | ||
| 1426 | if (parent != null) | ||
| 1427 | return; | ||
| 1428 | |||
| 1429 | /* xPosition and yPosition are the position of this window | ||
| 1430 | relative to the screen. Set them and request a ConfigureNotify | ||
| 1431 | event. */ | ||
| 1432 | |||
| 1433 | if (this.xPosition != xPosition | ||
| 1434 | || this.yPosition != yPosition) | ||
| 1435 | { | ||
| 1436 | this.xPosition = xPosition; | ||
| 1437 | this.yPosition = yPosition; | ||
| 1438 | |||
| 1439 | EmacsNative.sendConfigureNotify (this.handle, | ||
| 1440 | System.currentTimeMillis (), | ||
| 1441 | xPosition, yPosition, | ||
| 1442 | rect.width (), rect.height ()); | ||
| 1443 | } | ||
| 1444 | } | ||
| 1445 | }; | ||
diff --git a/java/org/gnu/emacs/EmacsWindowAttachmentManager.java b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java new file mode 100644 index 00000000000..bc96de7fe1a --- /dev/null +++ b/java/org/gnu/emacs/EmacsWindowAttachmentManager.java | |||
| @@ -0,0 +1,230 @@ | |||
| 1 | /* Communication module for Android terminals. -*- c-file-style: "GNU" -*- | ||
| 2 | |||
| 3 | Copyright (C) 2023 Free Software Foundation, Inc. | ||
| 4 | |||
| 5 | This file is part of GNU Emacs. | ||
| 6 | |||
| 7 | GNU Emacs is free software: you can redistribute it and/or modify | ||
| 8 | it under the terms of the GNU General Public License as published by | ||
| 9 | the Free Software Foundation, either version 3 of the License, or (at | ||
| 10 | your option) any later version. | ||
| 11 | |||
| 12 | GNU Emacs is distributed in the hope that it will be useful, | ||
| 13 | but WITHOUT ANY WARRANTY; without even the implied warranty of | ||
| 14 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | ||
| 15 | GNU General Public License for more details. | ||
| 16 | |||
| 17 | You should have received a copy of the GNU General Public License | ||
| 18 | along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */ | ||
| 19 | |||
| 20 | package org.gnu.emacs; | ||
| 21 | |||
| 22 | import java.util.ArrayList; | ||
| 23 | import java.util.List; | ||
| 24 | |||
| 25 | import android.app.ActivityOptions; | ||
| 26 | import android.content.Intent; | ||
| 27 | import android.os.Build; | ||
| 28 | import android.util.Log; | ||
| 29 | |||
| 30 | /* Code to paper over the differences in lifecycles between | ||
| 31 | "activities" and windows. There are four interfaces to an instance | ||
| 32 | of this class: | ||
| 33 | |||
| 34 | registerWindowConsumer (WindowConsumer) | ||
| 35 | registerWindow (EmacsWindow) | ||
| 36 | removeWindowConsumer (WindowConsumer) | ||
| 37 | removeWindow (EmacsWindow) | ||
| 38 | |||
| 39 | A WindowConsumer is expected to allow an EmacsWindow to be attached | ||
| 40 | to it, and be created or destroyed. | ||
| 41 | |||
| 42 | Every time a window is created, registerWindow checks the list of | ||
| 43 | window consumers. If a consumer exists and does not currently have | ||
| 44 | a window of its own attached, it gets the new window. Otherwise, | ||
| 45 | the window attachment manager starts a new consumer. | ||
| 46 | |||
| 47 | Every time a consumer is registered, registerWindowConsumer checks | ||
| 48 | the list of available windows. If a window exists and is not | ||
| 49 | currently attached to a consumer, then the consumer gets it. | ||
| 50 | |||
| 51 | Finally, every time a window is removed, the consumer is | ||
| 52 | destroyed. */ | ||
| 53 | |||
| 54 | public final class EmacsWindowAttachmentManager | ||
| 55 | { | ||
| 56 | private final static String TAG = "EmacsWindowAttachmentManager"; | ||
| 57 | |||
| 58 | /* The single window attachment manager ``object''. */ | ||
| 59 | public static final EmacsWindowAttachmentManager MANAGER; | ||
| 60 | |||
| 61 | static | ||
| 62 | { | ||
| 63 | MANAGER = new EmacsWindowAttachmentManager (); | ||
| 64 | }; | ||
| 65 | |||
| 66 | public interface WindowConsumer | ||
| 67 | { | ||
| 68 | public void attachWindow (EmacsWindow window); | ||
| 69 | public EmacsWindow getAttachedWindow (); | ||
| 70 | public void detachWindow (); | ||
| 71 | public void destroy (); | ||
| 72 | }; | ||
| 73 | |||
| 74 | /* List of currently attached window consumers. */ | ||
| 75 | public List<WindowConsumer> consumers; | ||
| 76 | |||
| 77 | /* List of currently attached windows. */ | ||
| 78 | public List<EmacsWindow> windows; | ||
| 79 | |||
| 80 | public | ||
| 81 | EmacsWindowAttachmentManager () | ||
| 82 | { | ||
| 83 | consumers = new ArrayList<WindowConsumer> (); | ||
| 84 | windows = new ArrayList<EmacsWindow> (); | ||
| 85 | } | ||
| 86 | |||
| 87 | public void | ||
| 88 | registerWindowConsumer (WindowConsumer consumer) | ||
| 89 | { | ||
| 90 | Log.d (TAG, "registerWindowConsumer " + consumer); | ||
| 91 | |||
| 92 | consumers.add (consumer); | ||
| 93 | |||
| 94 | for (EmacsWindow window : windows) | ||
| 95 | { | ||
| 96 | if (window.getAttachedConsumer () == null) | ||
| 97 | { | ||
| 98 | Log.d (TAG, "registerWindowConsumer: attaching " + window); | ||
| 99 | consumer.attachWindow (window); | ||
| 100 | return; | ||
| 101 | } | ||
| 102 | } | ||
| 103 | |||
| 104 | Log.d (TAG, "registerWindowConsumer: sendWindowAction 0, 0"); | ||
| 105 | EmacsNative.sendWindowAction ((short) 0, 0); | ||
| 106 | } | ||
| 107 | |||
| 108 | public synchronized void | ||
| 109 | registerWindow (EmacsWindow window) | ||
| 110 | { | ||
| 111 | Intent intent; | ||
| 112 | ActivityOptions options; | ||
| 113 | |||
| 114 | Log.d (TAG, "registerWindow (maybe): " + window); | ||
| 115 | |||
| 116 | if (windows.contains (window)) | ||
| 117 | /* The window is already registered. */ | ||
| 118 | return; | ||
| 119 | |||
| 120 | Log.d (TAG, "registerWindow: " + window); | ||
| 121 | |||
| 122 | windows.add (window); | ||
| 123 | |||
| 124 | for (WindowConsumer consumer : consumers) | ||
| 125 | { | ||
| 126 | if (consumer.getAttachedWindow () == null) | ||
| 127 | { | ||
| 128 | Log.d (TAG, "registerWindow: attaching " + consumer); | ||
| 129 | consumer.attachWindow (window); | ||
| 130 | return; | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | intent = new Intent (EmacsService.SERVICE, | ||
| 135 | EmacsMultitaskActivity.class); | ||
| 136 | intent.addFlags (Intent.FLAG_ACTIVITY_NEW_DOCUMENT | ||
| 137 | | Intent.FLAG_ACTIVITY_NEW_TASK | ||
| 138 | | Intent.FLAG_ACTIVITY_MULTIPLE_TASK); | ||
| 139 | |||
| 140 | if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) | ||
| 141 | EmacsService.SERVICE.startActivity (intent); | ||
| 142 | else | ||
| 143 | { | ||
| 144 | /* Specify the desired window size. */ | ||
| 145 | options = ActivityOptions.makeBasic (); | ||
| 146 | options.setLaunchBounds (window.getGeometry ()); | ||
| 147 | EmacsService.SERVICE.startActivity (intent, | ||
| 148 | options.toBundle ()); | ||
| 149 | } | ||
| 150 | |||
| 151 | Log.d (TAG, "registerWindow: startActivity"); | ||
| 152 | } | ||
| 153 | |||
| 154 | public void | ||
| 155 | removeWindowConsumer (WindowConsumer consumer, boolean isFinishing) | ||
| 156 | { | ||
| 157 | EmacsWindow window; | ||
| 158 | |||
| 159 | Log.d (TAG, "removeWindowConsumer " + consumer); | ||
| 160 | |||
| 161 | window = consumer.getAttachedWindow (); | ||
| 162 | |||
| 163 | if (window != null) | ||
| 164 | { | ||
| 165 | Log.d (TAG, "removeWindowConsumer: detaching " + window); | ||
| 166 | |||
| 167 | consumer.detachWindow (); | ||
| 168 | window.onActivityDetached (isFinishing); | ||
| 169 | } | ||
| 170 | |||
| 171 | Log.d (TAG, "removeWindowConsumer: removing " + consumer); | ||
| 172 | consumers.remove (consumer); | ||
| 173 | } | ||
| 174 | |||
| 175 | public synchronized void | ||
| 176 | detachWindow (EmacsWindow window) | ||
| 177 | { | ||
| 178 | WindowConsumer consumer; | ||
| 179 | |||
| 180 | Log.d (TAG, "detachWindow " + window); | ||
| 181 | |||
| 182 | if (window.getAttachedConsumer () != null) | ||
| 183 | { | ||
| 184 | consumer = window.getAttachedConsumer (); | ||
| 185 | |||
| 186 | Log.d (TAG, "detachWindow: removing" + consumer); | ||
| 187 | |||
| 188 | consumers.remove (consumer); | ||
| 189 | consumer.destroy (); | ||
| 190 | } | ||
| 191 | |||
| 192 | windows.remove (window); | ||
| 193 | } | ||
| 194 | |||
| 195 | public void | ||
| 196 | noticeIconified (WindowConsumer consumer) | ||
| 197 | { | ||
| 198 | EmacsWindow window; | ||
| 199 | |||
| 200 | Log.d (TAG, "noticeIconified " + consumer); | ||
| 201 | |||
| 202 | /* If a window is attached, send the appropriate iconification | ||
| 203 | events. */ | ||
| 204 | window = consumer.getAttachedWindow (); | ||
| 205 | |||
| 206 | if (window != null) | ||
| 207 | window.noticeIconified (); | ||
| 208 | } | ||
| 209 | |||
| 210 | public void | ||
| 211 | noticeDeiconified (WindowConsumer consumer) | ||
| 212 | { | ||
| 213 | EmacsWindow window; | ||
| 214 | |||
| 215 | Log.d (TAG, "noticeDeiconified " + consumer); | ||
| 216 | |||
| 217 | /* If a window is attached, send the appropriate iconification | ||
| 218 | events. */ | ||
| 219 | window = consumer.getAttachedWindow (); | ||
| 220 | |||
| 221 | if (window != null) | ||
| 222 | window.noticeDeiconified (); | ||
| 223 | } | ||
| 224 | |||
| 225 | public synchronized List<EmacsWindow> | ||
| 226 | copyWindows () | ||
| 227 | { | ||
| 228 | return new ArrayList<EmacsWindow> (windows); | ||
| 229 | } | ||
| 230 | }; | ||
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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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 | |||
| 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> | ||