aboutsummaryrefslogtreecommitdiffstats
path: root/java
diff options
context:
space:
mode:
authorPo Lu2023-02-04 23:32:07 +0800
committerPo Lu2023-02-04 23:32:07 +0800
commit420533a8f9b345699dad9eeafeb3ccecfed516b2 (patch)
tree3dba030a6c91eedfd82866aade5cc3200e865e60 /java
parentbfce0ce57fe0de11a6cbe3ff878a59dd2a0853d4 (diff)
downloademacs-420533a8f9b345699dad9eeafeb3ccecfed516b2.tar.gz
emacs-420533a8f9b345699dad9eeafeb3ccecfed516b2.zip
Add emacsclient desktop file equivalent on Android
* doc/emacs/android.texi (Android File System): * java/AndroidManifest.xml.in: Update with new activity. Remove Android 10 restrictions through a special flag. * java/org/gnu/emacs/EmacsNative.java (getProcName): New function. * java/org/gnu/emacs/EmacsOpenActivity.java (EmacsOpenActivity): New file. * java/org/gnu/emacs/EmacsService.java (getLibraryDirection): Remove unused annotation. * lib-src/emacsclient.c (decode_options): Set alt_display on Android. * src/android.c (android_proc_name): New function. (NATIVE_NAME): Export via JNI.
Diffstat (limited to 'java')
-rw-r--r--java/AndroidManifest.xml.in79
-rw-r--r--java/org/gnu/emacs/EmacsNative.java4
-rw-r--r--java/org/gnu/emacs/EmacsOpenActivity.java357
-rw-r--r--java/org/gnu/emacs/EmacsService.java1
4 files changed, 440 insertions, 1 deletions
diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in
index 544c87e1f1e..923c5a005d5 100644
--- a/java/AndroidManifest.xml.in
+++ b/java/AndroidManifest.xml.in
@@ -24,6 +24,7 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
24 package="org.gnu.emacs" 24 package="org.gnu.emacs"
25 android:targetSandboxVersion="1" 25 android:targetSandboxVersion="1"
26 android:installLocation="auto" 26 android:installLocation="auto"
27 android:requestLegacyExternalStorage="true"
27 android:versionCode="@emacs_major_version@" 28 android:versionCode="@emacs_major_version@"
28 android:versionName="@version@"> 29 android:versionName="@version@">
29 30
@@ -82,6 +83,84 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
82 </intent-filter> 83 </intent-filter>
83 </activity> 84 </activity>
84 85
86 <activity android:name="org.gnu.emacs.EmacsOpenActivity"
87 android:exported="true">
88
89 <!-- Allow Emacs to open all kinds of files known to Android. -->
90
91 <intent-filter>
92 <action android:name="android.intent.action.VIEW"/>
93 <action android:name="android.intent.action.EDIT"/>
94 <action android:name="android.intent.action.PICK"/>
95
96 <category android:name="android.intent.category.DEFAULT"/>
97
98 <data android:mimeType="image/aces"/>
99 <data android:mimeType="image/avci"/>
100 <data android:mimeType="image/avcs"/>
101 <data android:mimeType="image/avif"/>
102 <data android:mimeType="image/bmp"/>
103 <data android:mimeType="image/cgm"/>
104 <data android:mimeType="image/dicom-rle"/>
105 <data android:mimeType="image/dpx"/>
106 <data android:mimeType="image/emf"/>
107 <data android:mimeType="image/example"/>
108 <data android:mimeType="image/fits"/>
109 <data android:mimeType="image/g3fax"/>
110 <data android:mimeType="image/heic"/>
111 <data android:mimeType="image/heic-sequence"/>
112 <data android:mimeType="image/heif"/>
113 <data android:mimeType="image/heif-sequence"/>
114 <data android:mimeType="image/hej2k"/>
115 <data android:mimeType="image/hsj2"/>
116 <data android:mimeType="image/jls"/>
117 <data android:mimeType="image/jp2"/>
118 <data android:mimeType="image/jph"/>
119 <data android:mimeType="image/jphc"/>
120 <data android:mimeType="image/jpm"/>
121 <data android:mimeType="image/jpx"/>
122 <data android:mimeType="image/jxr"/>
123 <data android:mimeType="image/jxrA"/>
124 <data android:mimeType="image/jxrS"/>
125 <data android:mimeType="image/jxs"/>
126 <data android:mimeType="image/jxsc"/>
127 <data android:mimeType="image/jxsi"/>
128 <data android:mimeType="image/jxss"/>
129 <data android:mimeType="image/ktx"/>
130 <data android:mimeType="image/ktx2"/>
131 <data android:mimeType="image/naplps"/>
132 <data android:mimeType="image/png"/>
133 <data android:mimeType="image/prs.btif"/>
134 <data android:mimeType="image/prs.pti"/>
135 <data android:mimeType="image/pwg-raster"/>
136 <data android:mimeType="image/svg+xml"/>
137 <data android:mimeType="image/t38"/>
138 <data android:mimeType="image/tiff"/>
139 <data android:mimeType="image/tiff-fx"/>
140 <data android:mimeType="text/*"/>
141 <data android:mimeType="application/*xml"/>
142 <data android:mimeType="application/atom+xml"/>
143 <data android:mimeType="application/dxf"/>
144 <data android:mimeType="application/ecmascript"/>
145 <data android:mimeType="application/javascript"/>
146 <data android:mimeType="application/json"/>
147 <data android:mimeType="application/*log*"/>
148 <data android:mimeType="application/octet-stream"/>
149 <data android:mimeType="application/soap+xm"/>
150 <data android:mimeType="application/x-caramel"/>
151 <data android:mimeType="application/x-klaunch"/>
152 <data android:mimeType="application/x-latex"/>
153 <data android:mimeType="application/x-sh"/>
154 <data android:mimeType="application/x-tcl"/>
155 <data android:mimeType="application/x-tex*"/>
156 <data android:mimeType="application/x-troff*"/>
157 <data android:mimeType="application/xhtml+xml"/>
158 <data android:mimeType="application/xml*"/>
159 <data android:mimeType="application/zip"/>
160 <data android:mimeType="application/x-zip-compressed"/>
161 </intent-filter>
162 </activity>
163
85 <activity android:name="org.gnu.emacs.EmacsMultitaskActivity" 164 <activity android:name="org.gnu.emacs.EmacsMultitaskActivity"
86 android:windowSoftInputMode="adjustResize" 165 android:windowSoftInputMode="adjustResize"
87 android:exported="true" 166 android:exported="true"
diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java
index 4e91a7be322..aba356051cd 100644
--- a/java/org/gnu/emacs/EmacsNative.java
+++ b/java/org/gnu/emacs/EmacsNative.java
@@ -153,6 +153,10 @@ public class EmacsNative
153 public static native long sendExpose (short window, int x, int y, 153 public static native long sendExpose (short window, int x, int y,
154 int width, int height); 154 int width, int height);
155 155
156 /* Return the file name associated with the specified file
157 descriptor, or NULL if there is none. */
158 public static native byte[] getProcName (int fd);
159
156 static 160 static
157 { 161 {
158 System.loadLibrary ("emacs"); 162 System.loadLibrary ("emacs");
diff --git a/java/org/gnu/emacs/EmacsOpenActivity.java b/java/org/gnu/emacs/EmacsOpenActivity.java
new file mode 100644
index 00000000000..268a9abd7b1
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsOpenActivity.java
@@ -0,0 +1,357 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22/* This class makes the Emacs server work reasonably on Android.
23
24 There is no way to make the Unix socket publicly available on
25 Android.
26
27 Instead, this activity tries to connect to the Emacs server, to
28 make it open files the system asks Emacs to open, and to emulate
29 some reasonable behavior when Emacs has not yet started.
30
31 First, Emacs registers itself as an application that can open text
32 and image files.
33
34 Then, when the user is asked to open a file and selects ``Emacs''
35 as the application that will open the file, the system pops up a
36 window, this activity, and calls the `onCreate' function.
37
38 `onCreate' then tries very to find the file name of the file that
39 was selected, and give it to emacsclient.
40
41 If emacsclient successfully opens the file, then this activity
42 starts EmacsActivity (to bring it on to the screen); otherwise, it
43 displays the output of emacsclient or any error message that occurs
44 and exits. */
45
46import android.app.AlertDialog;
47import android.app.Activity;
48
49import android.content.Context;
50import android.content.ContentResolver;
51import android.content.DialogInterface;
52import android.content.Intent;
53
54import android.net.Uri;
55
56import android.os.Build;
57import android.os.Bundle;
58import android.os.ParcelFileDescriptor;
59
60import java.io.File;
61import java.io.FileReader;
62import java.io.FileNotFoundException;
63import java.io.IOException;
64import java.io.InputStream;
65import java.io.UnsupportedEncodingException;
66
67public class EmacsOpenActivity extends Activity
68 implements DialogInterface.OnClickListener
69{
70 private class EmacsClientThread extends Thread
71 {
72 private ProcessBuilder builder;
73
74 public
75 EmacsClientThread (ProcessBuilder processBuilder)
76 {
77 builder = processBuilder;
78 }
79
80 @Override
81 public void
82 run ()
83 {
84 Process process;
85 InputStream error;
86 String errorText;
87
88 try
89 {
90 /* Start emacsclient. */
91 process = builder.start ();
92 process.waitFor ();
93
94 /* Now figure out whether or not starting the process was
95 successful. */
96 if (process.exitValue () == 0)
97 finishSuccess ();
98 else
99 finishFailure ("Error opening file", null);
100 }
101 catch (IOException exception)
102 {
103 finishFailure ("Internal error", exception.toString ());
104 }
105 catch (InterruptedException exception)
106 {
107 finishFailure ("Internal error", exception.toString ());
108 }
109 }
110 }
111
112 @Override
113 public void
114 onClick (DialogInterface dialog, int which)
115 {
116 finish ();
117 }
118
119 public String
120 readEmacsClientLog ()
121 {
122 File file, cache;
123 FileReader reader;
124 char[] buffer;
125 int rc;
126 String what;
127
128 cache = getCacheDir ();
129 file = new File (cache, "emacsclient.log");
130 what = "";
131
132 try
133 {
134 reader = new FileReader (file);
135 buffer = new char[2048];
136
137 while ((rc = reader.read (buffer, 0, 2048)) != -1)
138 what += String.valueOf (buffer, 0, 2048);
139
140 reader.close ();
141 return what;
142 }
143 catch (IOException exception)
144 {
145 return ("Couldn't read emacsclient.log: "
146 + exception.toString ());
147 }
148 }
149
150 private void
151 displayFailureDialog (String title, String text)
152 {
153 AlertDialog.Builder builder;
154 AlertDialog dialog;
155
156 builder = new AlertDialog.Builder (this);
157 dialog = builder.create ();
158 dialog.setTitle (title);
159
160 if (text == null)
161 /* Read in emacsclient.log instead. */
162 text = readEmacsClientLog ();
163
164 dialog.setMessage (text);
165 dialog.setButton (DialogInterface.BUTTON_POSITIVE, "OK", this);
166 dialog.show ();
167 }
168
169 /* Finish this activity in response to emacsclient having
170 successfully opened a file.
171
172 In the main thread, close this window, and open a window
173 belonging to an Emacs frame. */
174
175 public void
176 finishSuccess ()
177 {
178 runOnUiThread (new Runnable () {
179 @Override
180 public void
181 run ()
182 {
183 Intent intent;
184
185 intent = new Intent (EmacsOpenActivity.this,
186 EmacsActivity.class);
187 intent.addFlags (Intent.FLAG_ACTIVITY_NEW_TASK);
188 startActivity (intent);
189
190 EmacsOpenActivity.this.finish ();
191 }
192 });
193 }
194
195 /* Finish this activity after displaying a dialog associated with
196 failure to open a file.
197
198 Use TITLE as the title of the dialog. If TEXT is non-NULL,
199 display that text in the dialog. Otherwise, use the contents of
200 emacsclient.log in the cache directory instead. */
201
202 public void
203 finishFailure (final String title, final String text)
204 {
205 runOnUiThread (new Runnable () {
206 @Override
207 public void
208 run ()
209 {
210 displayFailureDialog (title, text);
211 }
212 });
213 }
214
215 public String
216 getLibraryDirectory ()
217 {
218 int apiLevel;
219 Context context;
220
221 context = getApplicationContext ();
222 apiLevel = Build.VERSION.SDK_INT;
223
224 if (apiLevel >= Build.VERSION_CODES.GINGERBREAD)
225 return context.getApplicationInfo().nativeLibraryDir;
226 else if (apiLevel >= Build.VERSION_CODES.DONUT)
227 return context.getApplicationInfo().dataDir + "/lib";
228
229 return "/data/data/" + context.getPackageName() + "/lib";
230 }
231
232 public void
233 startEmacsClient (String fileName)
234 {
235 String libDir;
236 ProcessBuilder builder;
237 Process process;
238 EmacsClientThread thread;
239 File file;
240
241 file = new File (getCacheDir (), "emacsclient.log");
242
243 libDir = getLibraryDirectory ();
244 builder = new ProcessBuilder (libDir + "/libemacsclient.so",
245 fileName, "--reuse-frame",
246 "--timeout=10", "--no-wait");
247
248 /* Redirect standard error to a file so that errors can be
249 meaningfully reported. */
250
251 if (file.exists ())
252 file.delete ();
253
254 builder.redirectError (file);
255
256 /* Track process output in a new thread, since this is the UI
257 thread and doing so here can cause deadlocks when EmacsService
258 decides to wait for something. */
259
260 thread = new EmacsClientThread (builder);
261 thread.start ();
262 }
263
264 @Override
265 public void
266 onCreate (Bundle savedInstanceState)
267 {
268 String action, fileName;
269 Intent intent;
270 Uri uri;
271 ContentResolver resolver;
272 ParcelFileDescriptor fd;
273 byte[] names;
274 String errorBlurb;
275
276 super.onCreate (savedInstanceState);
277
278 /* Obtain the intent that started Emacs. */
279 intent = getIntent ();
280 action = intent.getAction ();
281
282 if (action == null)
283 {
284 finish ();
285 return;
286 }
287
288 /* Now see if the action specified is supported by Emacs. */
289
290 if (action.equals ("android.intent.action.VIEW")
291 || action.equals ("android.intent.action.EDIT")
292 || action.equals ("android.intent.action.PICK"))
293 {
294 /* Obtain the URI of the action. */
295 uri = intent.getData ();
296
297 if (uri == null)
298 {
299 finish ();
300 return;
301 }
302
303 /* Now, try to get the file name. */
304
305 if (uri.getScheme ().equals ("file"))
306 fileName = uri.getPath ();
307 else
308 {
309 fileName = null;
310
311 if (uri.getScheme ().equals ("content"))
312 {
313 /* This is one of the annoying Android ``content''
314 URIs. Most of the time, there is actually an
315 underlying file, but it cannot be found without
316 opening the file and doing readlink on its file
317 descriptor in /proc/self/fd. */
318 resolver = getContentResolver ();
319
320 try
321 {
322 fd = resolver.openFileDescriptor (uri, "r");
323 names = EmacsNative.getProcName (fd.getFd ());
324 fd.close ();
325
326 /* What is the right encoding here? */
327
328 if (names != null)
329 fileName = new String (names, "UTF-8");
330 }
331 catch (FileNotFoundException exception)
332 {
333 /* Do nothing. */
334 }
335 catch (IOException exception)
336 {
337 /* Do nothing. */
338 }
339 }
340
341 if (fileName == null)
342 {
343 errorBlurb = ("The URI: " + uri + " could not be opened"
344 + ", as it does not encode file name inform"
345 + "ation.");
346 displayFailureDialog ("Error opening file", errorBlurb);
347 return;
348 }
349 }
350
351 /* And start emacsclient. */
352 startEmacsClient (fileName);
353 }
354 else
355 finish ();
356 }
357}
diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java
index d17f6d1286c..2ec2ddf9bda 100644
--- a/java/org/gnu/emacs/EmacsService.java
+++ b/java/org/gnu/emacs/EmacsService.java
@@ -152,7 +152,6 @@ public class EmacsService extends Service
152 } 152 }
153 } 153 }
154 154
155 @TargetApi (Build.VERSION_CODES.GINGERBREAD)
156 private String 155 private String
157 getLibraryDirectory () 156 getLibraryDirectory ()
158 { 157 {