aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorPo Lu2023-02-04 23:32:07 +0800
committerPo Lu2023-02-04 23:32:07 +0800
commit420533a8f9b345699dad9eeafeb3ccecfed516b2 (patch)
tree3dba030a6c91eedfd82866aade5cc3200e865e60
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.
-rw-r--r--doc/emacs/android.texi16
-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
-rw-r--r--lib-src/emacsclient.c2
-rw-r--r--src/android.c44
7 files changed, 490 insertions, 13 deletions
diff --git a/doc/emacs/android.texi b/doc/emacs/android.texi
index cfdf77454eb..9dc6fddbb72 100644
--- a/doc/emacs/android.texi
+++ b/doc/emacs/android.texi
@@ -167,8 +167,8 @@ system settings.
167 The external storage directory is found at @file{/sdcard}; the other 167 The external storage directory is found at @file{/sdcard}; the other
168directories are not found at any fixed location. 168directories are not found at any fixed location.
169 169
170@cindex file system limitations, Android 10 170@cindex file system limitations, Android 11
171 On Android 10 and later, the Android system restricts applications 171 On Android 11 and later, the Android system restricts applications
172from accessing files in the @file{/sdcard} directory using 172from accessing files in the @file{/sdcard} directory using
173file-related system calls such as @code{open} and @code{readdir}. 173file-related system calls such as @code{open} and @code{readdir}.
174 174
@@ -177,16 +177,8 @@ makes the system more secure. Unfortunately, it also means that Emacs
177cannot access files in those directories, despite holding the 177cannot access files in those directories, despite holding the
178necessary permissions. Thankfully, the Open Handset Alliance's 178necessary permissions. Thankfully, the Open Handset Alliance's
179version of Android allows this restriction to be disabled on a 179version of Android allows this restriction to be disabled on a
180per-program basis; on Android 10, the corresponding option in the 180per-program basis; the corresponding option in the system settings
181system settings panel is: 181panel is:
182
183@indentedblock
184System -> Developer Options -> App Compatibility Changes -> Emacs ->
185DEFAULT_SCOPED_STORAGE
186@end indentedblock
187
188 And on Android 11 and later, the corresponding option in the systems
189settings panel is:
190 182
191@indentedblock 183@indentedblock
192System -> Apps -> Special App Access -> All files access -> Emacs 184System -> Apps -> Special App Access -> All files access -> Emacs
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 {
diff --git a/lib-src/emacsclient.c b/lib-src/emacsclient.c
index 698bf9b50ae..a72fced1bf2 100644
--- a/lib-src/emacsclient.c
+++ b/lib-src/emacsclient.c
@@ -626,6 +626,8 @@ decode_options (int argc, char **argv)
626 alt_display = "w32"; 626 alt_display = "w32";
627#elif defined (HAVE_HAIKU) 627#elif defined (HAVE_HAIKU)
628 alt_display = "be"; 628 alt_display = "be";
629#elif defined (HAVE_ANDROID)
630 alt_display = "android";
629#endif 631#endif
630 632
631#ifdef HAVE_PGTK 633#ifdef HAVE_PGTK
diff --git a/src/android.c b/src/android.c
index 57a95bcd4f9..a0e64471a05 100644
--- a/src/android.c
+++ b/src/android.c
@@ -1369,6 +1369,27 @@ android_get_home_directory (void)
1369 return android_files_dir; 1369 return android_files_dir;
1370} 1370}
1371 1371
1372/* Return the name of the file behind a file descriptor FD by reading
1373 /proc/self/fd/. Place the name in BUFFER, which should be able to
1374 hold size bytes. Value is 0 upon success, and 1 upon failure. */
1375
1376static int
1377android_proc_name (int fd, char *buffer, size_t size)
1378{
1379 char format[sizeof "/proc/self/fd/"
1380 + INT_STRLEN_BOUND (int)];
1381 ssize_t read;
1382
1383 sprintf (format, "/proc/self/fd/%d", fd);
1384 read = readlink (format, buffer, size - 1);
1385
1386 if (read == -1)
1387 return 1;
1388
1389 buffer[read] = '\0';
1390 return 0;
1391}
1392
1372 1393
1373 1394
1374/* JNI functions called by Java. */ 1395/* JNI functions called by Java. */
@@ -1598,6 +1619,29 @@ NATIVE_NAME (setEmacsParams) (JNIEnv *env, jobject object,
1598 now. */ 1619 now. */
1599} 1620}
1600 1621
1622JNIEXPORT jobject JNICALL
1623NATIVE_NAME (getProcName) (JNIEnv *env, jobject object, jint fd)
1624{
1625 char buffer[PATH_MAX + 1];
1626 size_t length;
1627 jbyteArray array;
1628
1629 if (android_proc_name (fd, buffer, PATH_MAX + 1))
1630 return NULL;
1631
1632 /* Return a byte array, as Java strings cannot always encode file
1633 names. */
1634 length = strlen (buffer);
1635 array = (*env)->NewByteArray (env, length);
1636 if (!array)
1637 return NULL;
1638
1639 (*env)->SetByteArrayRegion (env, array, 0, length,
1640 (jbyte *) buffer);
1641
1642 return array;
1643}
1644
1601/* Initialize service_class, aborting if something goes wrong. */ 1645/* Initialize service_class, aborting if something goes wrong. */
1602 1646
1603static void 1647static void