aboutsummaryrefslogtreecommitdiffstats
path: root/java
diff options
context:
space:
mode:
authorPo Lu2023-02-09 22:56:41 +0800
committerPo Lu2023-02-09 22:56:41 +0800
commit209ae003b7444d2e9b195db9475ddbdefa8f9c64 (patch)
tree5826ded6dffda28fb76300c3df8222432771c6fd /java
parentf0f45ab10d9599c1dfe44ace8fe6b604de0b2935 (diff)
downloademacs-209ae003b7444d2e9b195db9475ddbdefa8f9c64.tar.gz
emacs-209ae003b7444d2e9b195db9475ddbdefa8f9c64.zip
Allow other text editors to edit files in Emacs' home directory
* java/AndroidManifest.xml.in: Declare the new documents provider. * java/README: Describe the meaning of files in res/values. * java/org/gnu/emacs/EmacsDocumentsProvider.java (EmacsDocumentsProvider): New file. * java/res/values-v19/bool.xml: * java/res/values/bool.xml: New files.
Diffstat (limited to 'java')
-rw-r--r--java/AndroidManifest.xml.in12
-rw-r--r--java/README13
-rw-r--r--java/org/gnu/emacs/EmacsDocumentsProvider.java381
-rw-r--r--java/res/values-v19/bool.xml22
-rw-r--r--java/res/values/bool.xml22
5 files changed, 449 insertions, 1 deletions
diff --git a/java/AndroidManifest.xml.in b/java/AndroidManifest.xml.in
index 3c9e30713b6..1da3646e2f5 100644
--- a/java/AndroidManifest.xml.in
+++ b/java/AndroidManifest.xml.in
@@ -179,6 +179,18 @@ along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
179 </intent-filter> 179 </intent-filter>
180 </activity> 180 </activity>
181 181
182 <provider android:name="org.gnu.emacs.EmacsDocumentsProvider"
183 android:authorities="org.gnu.emacs"
184 android:exported="true"
185 android:grantUriPermissions="true"
186 android:permission="android.permission.MANAGE_DOCUMENTS"
187 android:enabled="@bool/isAtLeastKitKat">
188 <intent-filter>
189 <action
190 android:name="android.content.action.DOCUMENTS_PROVIDER"/>
191 </intent-filter>
192 </provider>
193
182 <service android:name="org.gnu.emacs.EmacsService" 194 <service android:name="org.gnu.emacs.EmacsService"
183 android:directBootAware="false" 195 android:directBootAware="false"
184 android:enabled="true" 196 android:enabled="true"
diff --git a/java/README b/java/README
index 5539871cc2c..fd4aa770f4b 100644
--- a/java/README
+++ b/java/README
@@ -5,7 +5,12 @@ package out of them.
5The ``org/gnu/emacs'' subdirectory contains the Java sources under the 5The ``org/gnu/emacs'' subdirectory contains the Java sources under the
6``org.gnu.emacs'' package identifier. 6``org.gnu.emacs'' package identifier.
7 7
8The ``res'' directory contains resources, mainly the Emacs icon. 8``AndroidManifest.xml'' contains a manifest describing the Java
9sources to the system.
10
11The ``res'' directory contains resources, mainly the Emacs icon and
12several ``boolean resources'' which are used as a form of conditional
13evaluation for manifest entries.
9 14
10`emacs.keystore' is the signing key used to build Emacs. It is kept 15`emacs.keystore' is the signing key used to build Emacs. It is kept
11here, and we encourage all people redistributing Emacs to use this 16here, and we encourage all people redistributing Emacs to use this
@@ -456,6 +461,12 @@ loaded by the special invocation:
456 System.loadLibrary ("emacs"); 461 System.loadLibrary ("emacs");
457 }; 462 };
458 463
464where ``static'' defines a section of code which will be run upon the
465object (containing class) being loaded. This is like:
466
467 __attribute__((constructor))
468
469on systems where shared object constructors are supported.
459 470
460See http://docs.oracle.com/en/java/javase/19/docs/specs/jni/intro.html 471See http://docs.oracle.com/en/java/javase/19/docs/specs/jni/intro.html
461for more details. 472for more details.
diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java
new file mode 100644
index 00000000000..f12b302ff84
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java
@@ -0,0 +1,381 @@
1/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or (at
10your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20package org.gnu.emacs;
21
22import android.content.Context;
23
24import android.database.Cursor;
25import android.database.MatrixCursor;
26
27import android.os.Build;
28import android.os.CancellationSignal;
29import android.os.ParcelFileDescriptor;
30
31import android.provider.DocumentsContract.Document;
32import android.provider.DocumentsContract.Root;
33import static android.provider.DocumentsContract.buildChildDocumentsUri;
34import android.provider.DocumentsProvider;
35
36import android.webkit.MimeTypeMap;
37
38import android.net.Uri;
39
40import java.io.File;
41import java.io.FileNotFoundException;
42import java.io.IOException;
43
44/* ``Documents provider''. This allows Emacs's home directory to be
45 modified by other programs holding permissions to manage system
46 storage, which is useful to (for example) correct misconfigurations
47 which prevent Emacs from starting up.
48
49 This functionality is only available on Android 19 and later. */
50
51public class EmacsDocumentsProvider extends DocumentsProvider
52{
53 /* Home directory. This is the directory whose contents are
54 initially returned to requesting applications. */
55 private File baseDir;
56
57 /* The default projection for requests for the root directory. */
58 private static final String[] DEFAULT_ROOT_PROJECTION;
59
60 /* The default projection for requests for a file. */
61 private static final String[] DEFAULT_DOCUMENT_PROJECTION;
62
63 static
64 {
65 DEFAULT_ROOT_PROJECTION = new String[] {
66 Root.COLUMN_ROOT_ID,
67 Root.COLUMN_MIME_TYPES,
68 Root.COLUMN_FLAGS,
69 Root.COLUMN_TITLE,
70 Root.COLUMN_SUMMARY,
71 Root.COLUMN_DOCUMENT_ID,
72 Root.COLUMN_AVAILABLE_BYTES,
73 };
74
75 DEFAULT_DOCUMENT_PROJECTION = new String[] {
76 Document.COLUMN_DOCUMENT_ID,
77 Document.COLUMN_MIME_TYPE,
78 Document.COLUMN_DISPLAY_NAME,
79 Document.COLUMN_LAST_MODIFIED,
80 Document.COLUMN_FLAGS,
81 Document.COLUMN_SIZE,
82 };
83 }
84
85 @Override
86 public boolean
87 onCreate ()
88 {
89 /* Set the base directory to Emacs's files directory. */
90 baseDir = getContext ().getFilesDir ();
91 return true;
92 }
93
94 @Override
95 public Cursor
96 queryRoots (String[] projection)
97 {
98 MatrixCursor result;
99 MatrixCursor.RowBuilder row;
100
101 /* If the requestor asked for nothing at all, then it wants some
102 data by default. */
103
104 if (projection == null)
105 projection = DEFAULT_ROOT_PROJECTION;
106
107 result = new MatrixCursor (projection);
108 row = result.newRow ();
109
110 /* Now create and add a row for each file in the base
111 directory. */
112 row.add (Root.COLUMN_ROOT_ID, baseDir.getAbsolutePath ());
113 row.add (Root.COLUMN_SUMMARY, "Emacs home directory");
114
115 /* Add the appropriate flags. */
116
117 row.add (Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE);
118 row.add (Root.FLAG_LOCAL_ONLY);
119 row.add (Root.COLUMN_TITLE, "Emacs");
120 row.add (Root.COLUMN_DOCUMENT_ID, baseDir.getAbsolutePath ());
121
122 return result;
123 }
124
125 /* Return the MIME type of a file FILE. */
126
127 private String
128 getMimeType (File file)
129 {
130 String name, extension, mime;
131 int extensionSeparator;
132
133 if (file.isDirectory ())
134 return Document.MIME_TYPE_DIR;
135
136 /* Abuse WebView stuff to get the file's MIME type. */
137 name = file.getName ();
138 extensionSeparator = name.lastIndexOf ('.');
139
140 if (extensionSeparator > 0)
141 {
142 extension = name.substring (extensionSeparator + 1);
143 mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension (extension);
144
145 if (mime != null)
146 return mime;
147 }
148
149 return "application/octet-stream";
150 }
151
152 /* Append the specified FILE to the query result RESULT.
153 Handle both directories and ordinary files. */
154
155 private void
156 queryDocument1 (MatrixCursor result, File file)
157 {
158 MatrixCursor.RowBuilder row;
159 String fileName, displayName, mimeType;
160 int flags;
161
162 row = result.newRow ();
163 flags = 0;
164
165 /* fileName is a string that the system will ask for some time in
166 the future. Here, it is just the absolute name of the file. */
167 fileName = file.getAbsolutePath ();
168
169 /* If file is a directory, add the right flags for that. */
170
171 if (file.isDirectory ())
172 {
173 if (file.canWrite ())
174 {
175 flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
176 flags |= Document.FLAG_SUPPORTS_DELETE;
177 }
178 }
179 else if (file.canWrite ())
180 {
181 /* Apply the correct flags for a writable file. */
182 flags |= Document.FLAG_SUPPORTS_WRITE;
183 flags |= Document.FLAG_SUPPORTS_DELETE;
184
185 /* TODO: implement these
186
187 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
188 flags |= Document.FLAG_SUPPORTS_RENAME;
189
190 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
191 flags |= Document.FLAG_SUPPORTS_REMOVE; */
192 }
193
194 displayName = file.getName ();
195 mimeType = getMimeType (file);
196
197 row.add (Document.COLUMN_DOCUMENT_ID, fileName);
198 row.add (Document.COLUMN_DISPLAY_NAME, displayName);
199 row.add (Document.COLUMN_SIZE, file.length ());
200 row.add (Document.COLUMN_MIME_TYPE, mimeType);
201 row.add (Document.COLUMN_LAST_MODIFIED, file.lastModified ());
202 row.add (Document.COLUMN_FLAGS, flags);
203 }
204
205 @Override
206 public Cursor
207 queryDocument (String documentId, String[] projection)
208 throws FileNotFoundException
209 {
210 MatrixCursor result;
211
212 if (projection == null)
213 projection = DEFAULT_DOCUMENT_PROJECTION;
214
215 result = new MatrixCursor (projection);
216 queryDocument1 (result, new File (documentId));
217
218 return result;
219 }
220
221 @Override
222 public Cursor
223 queryChildDocuments (String parentDocumentId, String[] projection,
224 String sortOrder) throws FileNotFoundException
225 {
226 MatrixCursor result;
227 File directory;
228
229 if (projection == null)
230 projection = DEFAULT_DOCUMENT_PROJECTION;
231
232 result = new MatrixCursor (projection);
233
234 /* Try to open the file corresponding to the location being
235 requested. */
236 directory = new File (parentDocumentId);
237
238 /* Now add each child. */
239 for (File child : directory.listFiles ())
240 queryDocument1 (result, child);
241
242 return result;
243 }
244
245 @Override
246 public ParcelFileDescriptor
247 openDocument (String documentId, String mode,
248 CancellationSignal signal) throws FileNotFoundException
249 {
250 return ParcelFileDescriptor.open (new File (documentId),
251 ParcelFileDescriptor.parseMode (mode));
252 }
253
254 @Override
255 public String
256 createDocument (String documentId, String mimeType,
257 String displayName) throws FileNotFoundException
258 {
259 File file;
260 boolean rc;
261 Uri updatedUri;
262 Context context;
263
264 context = getContext ();
265 file = new File (documentId, displayName);
266
267 try
268 {
269 rc = false;
270
271 if (Document.MIME_TYPE_DIR.equals (mimeType))
272 {
273 file.mkdirs ();
274
275 if (file.isDirectory ())
276 rc = true;
277 }
278 else
279 {
280 file.createNewFile ();
281
282 if (file.isFile ()
283 && file.setWritable (true)
284 && file.setReadable (true))
285 rc = true;
286 }
287
288 if (!rc)
289 throw new FileNotFoundException ("rc != 1");
290 }
291 catch (IOException e)
292 {
293 throw new FileNotFoundException (e.toString ());
294 }
295
296 updatedUri
297 = buildChildDocumentsUri ("org.gnu.emacs", documentId);
298 /* Tell the system about the change. */
299 context.getContentResolver ().notifyChange (updatedUri, null);
300
301 return file.getAbsolutePath ();
302 }
303
304 private void
305 deleteDocument1 (File child)
306 {
307 File[] children;
308
309 /* Don't delete symlinks recursively.
310
311 Calling readlink or stat is problematic due to file name
312 encoding problems, so try to delete the file first, and only
313 try to delete files recursively afterword. */
314
315 if (child.delete ())
316 return;
317
318 children = child.listFiles ();
319
320 if (children != null)
321 {
322 for (File file : children)
323 deleteDocument1 (file);
324 }
325
326 child.delete ();
327 }
328
329 @Override
330 public void
331 deleteDocument (String documentId)
332 throws FileNotFoundException
333 {
334 File file, parent;
335 File[] children;
336 Uri updatedUri;
337 Context context;
338
339 /* Java makes recursively deleting a file hard. File name
340 encoding issues also prevent easily calling into C... */
341
342 context = getContext ();
343 file = new File (documentId);
344 parent = file.getParentFile ();
345
346 if (parent == null)
347 throw new RuntimeException ("trying to delete file without"
348 + " parent!");
349
350 updatedUri
351 = buildChildDocumentsUri ("org.gnu.emacs",
352 parent.getAbsolutePath ());
353
354 if (file.delete ())
355 {
356 /* Tell the system about the change. */
357 context.getContentResolver ().notifyChange (updatedUri, null);
358 return;
359 }
360
361 children = file.listFiles ();
362
363 if (children != null)
364 {
365 for (File child : children)
366 deleteDocument1 (child);
367 }
368
369 if (file.delete ())
370 /* Tell the system about the change. */
371 context.getContentResolver ().notifyChange (updatedUri, null);
372 }
373
374 @Override
375 public void
376 removeDocument (String documentId, String parentDocumentId)
377 throws FileNotFoundException
378 {
379 deleteDocument (documentId);
380 }
381}
diff --git a/java/res/values-v19/bool.xml b/java/res/values-v19/bool.xml
new file mode 100644
index 00000000000..a4e3a87ae71
--- /dev/null
+++ b/java/res/values-v19/bool.xml
@@ -0,0 +1,22 @@
1<!-- Boolean resources for GNU Emacs on Android 4.4 or later.
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<resources>
21 <bool name="isAtLeastKitKat">true</bool>
22</resources>
diff --git a/java/res/values/bool.xml b/java/res/values/bool.xml
new file mode 100644
index 00000000000..d37eab745c0
--- /dev/null
+++ b/java/res/values/bool.xml
@@ -0,0 +1,22 @@
1<!-- Boolean resources for GNU Emacs on Android.
2
3Copyright (C) 2023 Free Software Foundation, Inc.
4
5This file is part of GNU Emacs.
6
7GNU Emacs is free software: you can redistribute it and/or modify
8it under the terms of the GNU General Public License as published by
9the Free Software Foundation, either version 3 of the License, or
10(at your option) any later version.
11
12GNU Emacs is distributed in the hope that it will be useful,
13but WITHOUT ANY WARRANTY; without even the implied warranty of
14MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15GNU General Public License for more details.
16
17You should have received a copy of the GNU General Public License
18along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. -->
19
20<resources>
21 <bool name="isAtLeastKitKat">false</bool>
22</resources>