aboutsummaryrefslogtreecommitdiffstats
path: root/java
diff options
context:
space:
mode:
authorPo Lu2023-07-28 15:19:37 +0800
committerPo Lu2023-07-28 15:19:37 +0800
commit0709e03f88cdef8f785338cab9315b527db0854e (patch)
treee4f81f91e0f2e18bb60e75aa9cb3d0c61ac4bb8e /java
parent03cf3bbb5c38aa55abd6f7d4860025f7482fcfc3 (diff)
downloademacs-0709e03f88cdef8f785338cab9315b527db0854e.tar.gz
emacs-0709e03f88cdef8f785338cab9315b527db0854e.zip
Allow quitting from Android content provider operations
* doc/emacs/android.texi (Android Document Providers): Say that quitting is now possible. * java/org/gnu/emacs/EmacsNative.java (EmacsNative): New functions `safSyncAndReadInput', `safync' and `safPostRequest'. * java/org/gnu/emacs/EmacsSafThread.java: New file. Move cancel-able SAF operations here. * java/org/gnu/emacs/EmacsService.java (EmacsService): Allow quitting from most SAF operations. * src/androidvfs.c (android_saf_exception_check): Return EINTR if OperationCanceledException is received. (android_saf_stat, android_saf_access) (android_document_id_from_name, android_saf_tree_opendir_1) (android_saf_file_open): Don't allow reentrant calls from async input handlers. (NATIVE_NAME): Implement new synchronization primitives for JNI. (android_vfs_init): Initialize new class. * src/dired.c (open_directory): Handle EINTR from opendir. * src/sysdep.c: Describe which operations may return EINTR on Android.
Diffstat (limited to 'java')
-rw-r--r--java/org/gnu/emacs/EmacsNative.java17
-rw-r--r--java/org/gnu/emacs/EmacsSafThread.java922
-rw-r--r--java/org/gnu/emacs/EmacsService.java520
3 files changed, 986 insertions, 473 deletions
diff --git a/java/org/gnu/emacs/EmacsNative.java b/java/org/gnu/emacs/EmacsNative.java
index d4d502ede5a..ea200037218 100644
--- a/java/org/gnu/emacs/EmacsNative.java
+++ b/java/org/gnu/emacs/EmacsNative.java
@@ -257,6 +257,23 @@ public final class EmacsNative
257 257
258 public static native void notifyPixelsChanged (Bitmap bitmap); 258 public static native void notifyPixelsChanged (Bitmap bitmap);
259 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
260 static 277 static
261 { 278 {
262 /* Older versions of Android cannot link correctly with shared 279 /* Older versions of Android cannot link correctly with shared
diff --git a/java/org/gnu/emacs/EmacsSafThread.java b/java/org/gnu/emacs/EmacsSafThread.java
new file mode 100644
index 00000000000..fd06603fab3
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsSafThread.java
@@ -0,0 +1,922 @@
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
20package org.gnu.emacs;
21
22import android.content.ContentResolver;
23import android.database.Cursor;
24import android.net.Uri;
25
26import android.os.Build;
27import android.os.CancellationSignal;
28import android.os.Handler;
29import android.os.HandlerThread;
30import android.os.ParcelFileDescriptor;
31
32import android.provider.DocumentsContract;
33import android.provider.DocumentsContract.Document;
34
35
36
37/* Emacs runs long-running SAF operations on a second thread running
38 its own handler. These operations include opening files and
39 maintaining the path to document ID cache.
40
41#if 0
42 Because Emacs paths are based on file display names, while Android
43 document identifiers have no discernible hierarchy of their own,
44 each file name lookup must carry out a repeated search for
45 directory documents with the names of all of the file name's
46 constituent components, where each iteration searches within the
47 directory document identified by the previous iteration.
48
49 A time limited cache tying components to document IDs is maintained
50 in order to speed up consecutive searches for file names sharing
51 the same components. Since listening for changes to each document
52 in the cache is prohibitively expensive, Emacs instead elects to
53 periodically remove entries that are older than a predetermined
54 amount of a time.
55
56 The cache is structured much like the directory trees whose
57 information it records, with each entry in the cache containing a
58 list of entries for their children. File name lookup consults the
59 cache and populates it with missing information simultaneously.
60
61 This is not yet implemented.
62#endif
63
64 Long-running operations are also run on this thread for another
65 reason: Android uses special cancellation objects to terminate
66 ongoing IPC operations. However, the functions that perform these
67 operations block instead of providing mechanisms for the caller to
68 wait for their completion while also reading async input, as a
69 consequence of which the calling thread is unable to signal the
70 cancellation objects that it provides. Performing the blocking
71 operations in this auxiliary thread enables the main thread to wait
72 for completion itself, signaling the cancellation objects when it
73 deems necessary. */
74
75
76
77public final class EmacsSafThread extends HandlerThread
78{
79 /* The content resolver used by this thread. */
80 private final ContentResolver resolver;
81
82 /* Handler for this thread's main loop. */
83 private Handler handler;
84
85 /* File access mode constants. See `man 7 inode'. */
86 public static final int S_IRUSR = 0000400;
87 public static final int S_IWUSR = 0000200;
88 public static final int S_IFCHR = 0020000;
89 public static final int S_IFDIR = 0040000;
90 public static final int S_IFREG = 0100000;
91
92 public
93 EmacsSafThread (ContentResolver resolver)
94 {
95 super ("Document provider access thread");
96 this.resolver = resolver;
97 }
98
99
100
101 @Override
102 public void
103 start ()
104 {
105 super.start ();
106
107 /* Set up the handler after the thread starts. */
108 handler = new Handler (getLooper ());
109 }
110
111
112
113 /* ``Prototypes'' for nested functions that are run within the SAF
114 thread and accepts a cancellation signal. They differ in their
115 return types. */
116
117 private abstract class SafIntFunction
118 {
119 /* The ``throws Throwable'' here is a Java idiosyncracy that tells
120 the compiler to allow arbitrary error objects to be signaled
121 from within this function.
122
123 Later, runIntFunction will try to re-throw any error object
124 generated by this function in the Emacs thread, using a trick
125 to avoid the compiler requirement to expressly declare that an
126 error (and which types of errors) will be signaled. */
127
128 public abstract int runInt (CancellationSignal signal)
129 throws Throwable;
130 };
131
132 private abstract class SafObjectFunction
133 {
134 /* The ``throws Throwable'' here is a Java idiosyncracy that tells
135 the compiler to allow arbitrary error objects to be signaled
136 from within this function.
137
138 Later, runObjectFunction will try to re-throw any error object
139 generated by this function in the Emacs thread, using a trick
140 to avoid the compiler requirement to expressly declare that an
141 error (and which types of errors) will be signaled. */
142
143 public abstract Object runObject (CancellationSignal signal)
144 throws Throwable;
145 };
146
147
148
149 /* Functions that run cancel-able queries. These functions are
150 internally run within the SAF thread. */
151
152 /* Throw the specified EXCEPTION. The type template T is erased by
153 the compiler before the object is compiled, so the compiled code
154 simply throws EXCEPTION without the cast being verified.
155
156 T should be RuntimeException to obtain the desired effect of
157 throwing an exception without a compiler check. */
158
159 @SuppressWarnings("unchecked")
160 private static <T extends Throwable> void
161 throwException (Throwable exception)
162 throws T
163 {
164 throw (T) exception;
165 }
166
167 /* Run the given function (or rather, its `runInt' field) within the
168 SAF thread, waiting for it to complete.
169
170 If async input arrives in the meantime and sets Vquit_flag,
171 signal the cancellation signal supplied to that function.
172
173 Rethrow any exception thrown from that function, and return its
174 value otherwise. */
175
176 private int
177 runIntFunction (final SafIntFunction function)
178 {
179 final EmacsHolder<Object> result;
180 final CancellationSignal signal;
181 Throwable throwable;
182
183 result = new EmacsHolder<Object> ();
184 signal = new CancellationSignal ();
185
186 handler.post (new Runnable () {
187 @Override
188 public void
189 run ()
190 {
191 try
192 {
193 result.thing
194 = Integer.valueOf (function.runInt (signal));
195 }
196 catch (Throwable throwable)
197 {
198 result.thing = throwable;
199 }
200
201 EmacsNative.safPostRequest ();
202 }
203 });
204
205 if (EmacsNative.safSyncAndReadInput () != 0)
206 {
207 signal.cancel ();
208
209 /* Now wait for the function to finish. Either the signal has
210 arrived after the query took place, in which case it will
211 finish normally, or an OperationCanceledException will be
212 thrown. */
213
214 EmacsNative.safSync ();
215 }
216
217 if (result.thing instanceof Throwable)
218 {
219 throwable = (Throwable) result.thing;
220 EmacsSafThread.<RuntimeException>throwException (throwable);
221 }
222
223 return (Integer) result.thing;
224 }
225
226 /* Run the given function (or rather, its `runObject' field) within
227 the SAF thread, waiting for it to complete.
228
229 If async input arrives in the meantime and sets Vquit_flag,
230 signal the cancellation signal supplied to that function.
231
232 Rethrow any exception thrown from that function, and return its
233 value otherwise. */
234
235 private Object
236 runObjectFunction (final SafObjectFunction function)
237 {
238 final EmacsHolder<Object> result;
239 final CancellationSignal signal;
240 Throwable throwable;
241
242 result = new EmacsHolder<Object> ();
243 signal = new CancellationSignal ();
244
245 handler.post (new Runnable () {
246 @Override
247 public void
248 run ()
249 {
250 try
251 {
252 result.thing = function.runObject (signal);
253 }
254 catch (Throwable throwable)
255 {
256 result.thing = throwable;
257 }
258
259 EmacsNative.safPostRequest ();
260 }
261 });
262
263 if (EmacsNative.safSyncAndReadInput () != 0)
264 {
265 signal.cancel ();
266
267 /* Now wait for the function to finish. Either the signal has
268 arrived after the query took place, in which case it will
269 finish normally, or an OperationCanceledException will be
270 thrown. */
271
272 EmacsNative.safSync ();
273 }
274
275 if (result.thing instanceof Throwable)
276 {
277 throwable = (Throwable) result.thing;
278 EmacsSafThread.<RuntimeException>throwException (throwable);
279 }
280
281 return result.thing;
282 }
283
284 /* The crux of `documentIdFromName1', run within the SAF thread.
285 SIGNAL should be a cancellation signal run upon quitting. */
286
287 private int
288 documentIdFromName1 (String tree_uri, String name,
289 String[] id_return, CancellationSignal signal)
290 {
291 Uri uri, treeUri;
292 String id, type;
293 String[] components, projection;
294 Cursor cursor;
295 int column;
296
297 projection = new String[] {
298 Document.COLUMN_DISPLAY_NAME,
299 Document.COLUMN_DOCUMENT_ID,
300 Document.COLUMN_MIME_TYPE,
301 };
302
303 /* Parse the URI identifying the tree first. */
304 uri = Uri.parse (tree_uri);
305
306 /* Now, split NAME into its individual components. */
307 components = name.split ("/");
308
309 /* Set id and type to the value at the root of the tree. */
310 type = id = null;
311 cursor = null;
312
313 /* For each component... */
314
315 try
316 {
317 for (String component : components)
318 {
319 /* Java split doesn't behave very much like strtok when it
320 comes to trailing and leading delimiters... */
321 if (component.isEmpty ())
322 continue;
323
324 /* Create the tree URI for URI from ID if it exists, or
325 the root otherwise. */
326
327 if (id == null)
328 id = DocumentsContract.getTreeDocumentId (uri);
329
330 treeUri
331 = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id);
332
333 /* Look for a file in this directory by the name of
334 component. */
335
336 cursor = resolver.query (treeUri, projection,
337 (Document.COLUMN_DISPLAY_NAME
338 + " = ?s"),
339 new String[] { component, },
340 null, signal);
341
342 if (cursor == null)
343 return -1;
344
345 while (true)
346 {
347 /* Even though the query selects for a specific
348 display name, some content providers nevertheless
349 return every file within the directory. */
350
351 if (!cursor.moveToNext ())
352 {
353 /* If the last component considered is a
354 directory... */
355 if ((type == null
356 || type.equals (Document.MIME_TYPE_DIR))
357 /* ... and type and id currently represent the
358 penultimate component. */
359 && component == components[components.length - 1])
360 {
361 /* The cursor is empty. In this case, return
362 -2 and the current document ID (belonging
363 to the previous component) in
364 ID_RETURN. */
365
366 id_return[0] = id;
367
368 /* But return -1 on the off chance that id is
369 null. */
370
371 if (id == null)
372 return -1;
373
374 return -2;
375 }
376
377 /* The last component found is not a directory, so
378 return -1. */
379 return -1;
380 }
381
382 /* So move CURSOR to a row with the right display
383 name. */
384
385 column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
386
387 if (column < 0)
388 continue;
389
390 name = cursor.getString (column);
391
392 /* Break out of the loop only once a matching
393 component is found. */
394
395 if (name.equals (component))
396 break;
397 }
398
399 /* Look for a column by the name of
400 COLUMN_DOCUMENT_ID. */
401
402 column = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
403
404 if (column < 0)
405 return -1;
406
407 /* Now replace ID with the document ID. */
408
409 id = cursor.getString (column);
410
411 /* If this is the last component, be sure to initialize
412 the document type. */
413
414 if (component == components[components.length - 1])
415 {
416 column
417 = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
418
419 if (column < 0)
420 return -1;
421
422 type = cursor.getString (column);
423
424 /* Type may be NULL depending on how the Cursor
425 returned is implemented. */
426
427 if (type == null)
428 return -1;
429 }
430
431 /* Now close the cursor. */
432 cursor.close ();
433 cursor = null;
434
435 /* ID may have become NULL if the data is in an invalid
436 format. */
437 if (id == null)
438 return -1;
439 }
440 }
441 finally
442 {
443 /* If an error is thrown within the block above, let
444 android_saf_exception_check handle it, but make sure the
445 cursor is closed. */
446
447 if (cursor != null)
448 cursor.close ();
449 }
450
451 /* Here, id is either NULL (meaning the same as TREE_URI), and
452 type is either NULL (in which case id should also be NULL) or
453 the MIME type of the file. */
454
455 /* First return the ID. */
456
457 if (id == null)
458 id_return[0] = DocumentsContract.getTreeDocumentId (uri);
459 else
460 id_return[0] = id;
461
462 /* Next, return whether or not this is a directory. */
463 if (type == null || type.equals (Document.MIME_TYPE_DIR))
464 return 1;
465
466 return 0;
467 }
468
469 /* Find the document ID of the file within TREE_URI designated by
470 NAME.
471
472 NAME is a ``file name'' comprised of the display names of
473 individual files. Each constituent component prior to the last
474 must name a directory file within TREE_URI.
475
476 Upon success, return 0 or 1 (contingent upon whether or not the
477 last component within NAME is a directory) and place the document
478 ID of the named file in ID_RETURN[0].
479
480 If the designated file can't be located, but each component of
481 NAME up to the last component can and is a directory, return -2
482 and the ID of the last component located in ID_RETURN[0].
483
484 If the designated file can't be located, return -1, or signal one
485 of OperationCanceledException, SecurityException,
486 FileNotFoundException, or UnsupportedOperationException. */
487
488 public int
489 documentIdFromName (final String tree_uri, final String name,
490 final String[] id_return)
491 {
492 return runIntFunction (new SafIntFunction () {
493 @Override
494 public int
495 runInt (CancellationSignal signal)
496 {
497 return documentIdFromName1 (tree_uri, name, id_return,
498 signal);
499 }
500 });
501 }
502
503 /* The bulk of `statDocument'. SIGNAL should be a cancelation
504 signal. */
505
506 private long[]
507 statDocument1 (String uri, String documentId,
508 CancellationSignal signal)
509 {
510 Uri uriObject;
511 String[] projection;
512 long[] stat;
513 int index;
514 long tem;
515 String tem1;
516 Cursor cursor;
517
518 uriObject = Uri.parse (uri);
519
520 if (documentId == null)
521 documentId = DocumentsContract.getTreeDocumentId (uriObject);
522
523 /* Create a document URI representing DOCUMENTID within URI's
524 authority. */
525
526 uriObject
527 = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
528
529 /* Now stat this document. */
530
531 projection = new String[] {
532 Document.COLUMN_FLAGS,
533 Document.COLUMN_LAST_MODIFIED,
534 Document.COLUMN_MIME_TYPE,
535 Document.COLUMN_SIZE,
536 };
537
538 cursor = resolver.query (uriObject, projection, null,
539 null, null, signal);
540
541 if (cursor == null)
542 return null;
543
544 if (!cursor.moveToFirst ())
545 {
546 cursor.close ();
547 return null;
548 }
549
550 /* Create the array of file status. */
551 stat = new long[3];
552
553 try
554 {
555 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
556 if (index < 0)
557 return null;
558
559 tem = cursor.getInt (index);
560
561 stat[0] |= S_IRUSR;
562 if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0)
563 stat[0] |= S_IWUSR;
564
565 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
566 && (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
567 stat[0] |= S_IFCHR;
568
569 index = cursor.getColumnIndex (Document.COLUMN_SIZE);
570 if (index < 0)
571 return null;
572
573 if (cursor.isNull (index))
574 stat[1] = -1; /* The size is unknown. */
575 else
576 stat[1] = cursor.getLong (index);
577
578 index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
579 if (index < 0)
580 return null;
581
582 tem1 = cursor.getString (index);
583
584 /* Check if this is a directory file. */
585 if (tem1.equals (Document.MIME_TYPE_DIR)
586 /* Files shouldn't be specials and directories at the same
587 time, but Android doesn't forbid document providers
588 from returning this information. */
589 && (stat[0] & S_IFCHR) == 0)
590 /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories,
591 just assume they're writable. */
592 stat[0] |= S_IFDIR | S_IWUSR;
593
594 /* If this file is neither a character special nor a
595 directory, indicate that it's a regular file. */
596
597 if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0)
598 stat[0] |= S_IFREG;
599
600 index = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED);
601
602 if (index >= 0 && !cursor.isNull (index))
603 {
604 /* Content providers are allowed to not provide mtime. */
605 tem = cursor.getLong (index);
606 stat[2] = tem;
607 }
608 }
609 finally
610 {
611 cursor.close ();
612 }
613
614 return stat;
615 }
616
617 /* Return file status for the document designated by the given
618 DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document
619 ID in URI itself.
620
621 Value is null upon failure, or an array of longs [MODE, SIZE,
622 MTIM] upon success, where MODE contains the file type and access
623 modes of the file as in `struct stat', SIZE is the size of the
624 file in BYTES or -1 if not known, and MTIM is the time of the
625 last modification to this file in milliseconds since 00:00,
626 January 1st, 1970.
627
628 OperationCanceledException and other typical exceptions may be
629 signaled upon receiving async input or other errors. */
630
631 public long[]
632 statDocument (final String uri, final String documentId)
633 {
634 return (long[]) runObjectFunction (new SafObjectFunction () {
635 @Override
636 public Object
637 runObject (CancellationSignal signal)
638 {
639 return statDocument1 (uri, documentId, signal);
640 }
641 });
642 }
643
644 /* The bulk of `accessDocument'. SIGNAL should be a cancellation
645 signal. */
646
647 private int
648 accessDocument1 (String uri, String documentId, boolean writable,
649 CancellationSignal signal)
650 {
651 Uri uriObject;
652 String[] projection;
653 int tem, index;
654 String tem1;
655 Cursor cursor;
656
657 uriObject = Uri.parse (uri);
658
659 if (documentId == null)
660 documentId = DocumentsContract.getTreeDocumentId (uriObject);
661
662 /* Create a document URI representing DOCUMENTID within URI's
663 authority. */
664
665 uriObject
666 = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
667
668 /* Now stat this document. */
669
670 projection = new String[] {
671 Document.COLUMN_FLAGS,
672 Document.COLUMN_MIME_TYPE,
673 };
674
675 cursor = resolver.query (uriObject, projection, null,
676 null, null, signal);
677
678 if (cursor == null)
679 return -1;
680
681 try
682 {
683 if (!cursor.moveToFirst ())
684 return -1;
685
686 if (!writable)
687 return 0;
688
689 index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
690 if (index < 0)
691 return -3;
692
693 /* Get the type of this file to check if it's a directory. */
694 tem1 = cursor.getString (index);
695
696 /* Check if this is a directory file. */
697 if (tem1.equals (Document.MIME_TYPE_DIR))
698 {
699 /* If so, don't check for FLAG_SUPPORTS_WRITE.
700 Check for FLAG_DIR_SUPPORTS_CREATE instead. */
701
702 if (!writable)
703 return 0;
704
705 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
706 if (index < 0)
707 return -3;
708
709 tem = cursor.getInt (index);
710 if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
711 return -3;
712
713 return 0;
714 }
715
716 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
717 if (index < 0)
718 return -3;
719
720 tem = cursor.getInt (index);
721 if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0)
722 return -3;
723 }
724 finally
725 {
726 /* Close the cursor if an exception occurs. */
727 cursor.close ();
728 }
729
730 return 0;
731 }
732
733 /* Find out whether Emacs has access to the document designated by
734 the specified DOCUMENTID within the tree URI. If DOCUMENTID is
735 NULL, use the document ID in URI itself.
736
737 If WRITABLE, also check that the file is writable, which is true
738 if it is either a directory or its flags contains
739 FLAG_SUPPORTS_WRITE.
740
741 Value is 0 if the file is accessible, and one of the following if
742 not:
743
744 -1, if the file does not exist.
745 -2, if WRITABLE and the file is not writable.
746 -3, upon any other error.
747
748 In addition, arbitrary runtime exceptions (such as
749 SecurityException or UnsupportedOperationException) may be
750 thrown. */
751
752 public int
753 accessDocument (final String uri, final String documentId,
754 final boolean writable)
755 {
756 return runIntFunction (new SafIntFunction () {
757 @Override
758 public int
759 runInt (CancellationSignal signal)
760 {
761 return accessDocument1 (uri, documentId, writable,
762 signal);
763 }
764 });
765 }
766
767 /* The crux of openDocumentDirectory. SIGNAL must be a cancellation
768 signal. */
769
770 private Cursor
771 openDocumentDirectory1 (String uri, String documentId,
772 CancellationSignal signal)
773 {
774 Uri uriObject;
775 Cursor cursor;
776 String projection[];
777
778 uriObject = Uri.parse (uri);
779
780 /* If documentId is not set, use the document ID of the tree URI
781 itself. */
782
783 if (documentId == null)
784 documentId = DocumentsContract.getTreeDocumentId (uriObject);
785
786 /* Build a URI representing each directory entry within
787 DOCUMENTID. */
788
789 uriObject
790 = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject,
791 documentId);
792
793 projection = new String [] {
794 Document.COLUMN_DISPLAY_NAME,
795 Document.COLUMN_MIME_TYPE,
796 };
797
798 cursor = resolver.query (uriObject, projection, null, null,
799 null, signal);
800 /* Return the cursor. */
801 return cursor;
802 }
803
804 /* Open a cursor representing each entry within the directory
805 designated by the specified DOCUMENTID within the tree URI.
806
807 If DOCUMENTID is NULL, use the document ID within URI itself.
808 Value is NULL upon failure.
809
810 In addition, arbitrary runtime exceptions (such as
811 SecurityException or UnsupportedOperationException) may be
812 thrown. */
813
814 public Cursor
815 openDocumentDirectory (final String uri, final String documentId)
816 {
817 return (Cursor) runObjectFunction (new SafObjectFunction () {
818 @Override
819 public Object
820 runObject (CancellationSignal signal)
821 {
822 return openDocumentDirectory1 (uri, documentId, signal);
823 }
824 });
825 }
826
827 /* The crux of `openDocument'. SIGNAL must be a cancellation
828 signal. */
829
830 public ParcelFileDescriptor
831 openDocument1 (String uri, String documentId, boolean write,
832 boolean truncate, CancellationSignal signal)
833 throws Throwable
834 {
835 Uri treeUri, documentUri;
836 String mode;
837 ParcelFileDescriptor fileDescriptor;
838
839 treeUri = Uri.parse (uri);
840
841 /* documentId must be set for this request, since it doesn't make
842 sense to ``open'' the root of the directory tree. */
843
844 documentUri
845 = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId);
846
847 if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
848 {
849 /* Select the mode used to open the file. `rw' means open
850 a stat-able file, while `rwt' means that and to
851 truncate the file as well. */
852
853 if (truncate)
854 mode = "rwt";
855 else
856 mode = "rw";
857
858 fileDescriptor
859 = resolver.openFileDescriptor (documentUri, mode,
860 signal);
861 }
862 else
863 {
864 /* Select the mode used to open the file. `openFile'
865 below means always open a stat-able file. */
866
867 if (truncate)
868 /* Invalid mode! */
869 return null;
870 else
871 mode = "r";
872
873 fileDescriptor = resolver.openFile (documentUri, mode,
874 signal);
875 }
876
877 return fileDescriptor;
878 }
879
880 /* Open a file descriptor for a file document designated by
881 DOCUMENTID within the document tree identified by URI. If
882 TRUNCATE and the document already exists, truncate its contents
883 before returning.
884
885 On Android 9.0 and earlier, always open the document in
886 ``read-write'' mode; this instructs the document provider to
887 return a seekable file that is stored on disk and returns correct
888 file status.
889
890 Under newer versions of Android, open the document in a
891 non-writable mode if WRITE is false. This is possible because
892 these versions allow Emacs to explicitly request a seekable
893 on-disk file.
894
895 Value is NULL upon failure or a parcel file descriptor upon
896 success. Call `ParcelFileDescriptor.close' on this file
897 descriptor instead of using the `close' system call.
898
899 FileNotFoundException and/or SecurityException and/or
900 UnsupportedOperationException and/or OperationCanceledException
901 may be thrown upon failure. */
902
903 public ParcelFileDescriptor
904 openDocument (final String uri, final String documentId,
905 final boolean write, final boolean truncate)
906 {
907 Object tem;
908
909 tem = runObjectFunction (new SafObjectFunction () {
910 @Override
911 public Object
912 runObject (CancellationSignal signal)
913 throws Throwable
914 {
915 return openDocument1 (uri, documentId, write, truncate,
916 signal);
917 }
918 });
919
920 return (ParcelFileDescriptor) tem;
921 }
922};
diff --git a/java/org/gnu/emacs/EmacsService.java b/java/org/gnu/emacs/EmacsService.java
index aa672994f12..e410754071b 100644
--- a/java/org/gnu/emacs/EmacsService.java
+++ b/java/org/gnu/emacs/EmacsService.java
@@ -109,13 +109,6 @@ public final class EmacsService extends Service
109 public static final int IC_MODE_ACTION = 1; 109 public static final int IC_MODE_ACTION = 1;
110 public static final int IC_MODE_TEXT = 2; 110 public static final int IC_MODE_TEXT = 2;
111 111
112 /* File access mode constants. See `man 7 inode'. */
113 public static final int S_IRUSR = 0000400;
114 public static final int S_IWUSR = 0000200;
115 public static final int S_IFCHR = 0020000;
116 public static final int S_IFDIR = 0040000;
117 public static final int S_IFREG = 0100000;
118
119 /* Display metrics used by font backends. */ 112 /* Display metrics used by font backends. */
120 public DisplayMetrics metrics; 113 public DisplayMetrics metrics;
121 114
@@ -134,6 +127,10 @@ public final class EmacsService extends Service
134 being called, and 2 if icBeginSynchronous was called. */ 127 being called, and 2 if icBeginSynchronous was called. */
135 public static final AtomicInteger servicingQuery; 128 public static final AtomicInteger servicingQuery;
136 129
130 /* Thread used to query document providers, or null if it hasn't
131 been created yet. */
132 private EmacsSafThread storageThread;
133
137 static 134 static
138 { 135 {
139 servicingQuery = new AtomicInteger (); 136 servicingQuery = new AtomicInteger ();
@@ -1160,10 +1157,7 @@ public final class EmacsService extends Service
1160 1157
1161 1158
1162 /* Document tree management functions. These functions shouldn't be 1159 /* Document tree management functions. These functions shouldn't be
1163 called before Android 5.0. 1160 called before Android 5.0. */
1164
1165 TODO: a timeout, let alone quitting, has yet to be implemented
1166 for any of these functions. */
1167 1161
1168 /* Return an array of each document authority providing at least one 1162 /* Return an array of each document authority providing at least one
1169 tree URI that Emacs holds the rights to persistently access. */ 1163 tree URI that Emacs holds the rights to persistently access. */
@@ -1319,223 +1313,26 @@ public final class EmacsService extends Service
1319 1313
1320 If the designated file can't be located, but each component of 1314 If the designated file can't be located, but each component of
1321 NAME up to the last component can and is a directory, return -2 1315 NAME up to the last component can and is a directory, return -2
1322 and the ID of the last component located in ID_RETURN[0]; 1316 and the ID of the last component located in ID_RETURN[0].
1323 1317
1324 If the designated file can't be located, return -1. */ 1318 If the designated file can't be located, return -1, or signal one
1319 of OperationCanceledException, SecurityException,
1320 FileNotFoundException, or UnsupportedOperationException. */
1325 1321
1326 private int 1322 private int
1327 documentIdFromName (String tree_uri, String name, String[] id_return) 1323 documentIdFromName (String tree_uri, String name, String[] id_return)
1328 { 1324 {
1329 Uri uri, treeUri; 1325 /* Start the thread used to run SAF requests if it isn't already
1330 String id, type; 1326 running. */
1331 String[] components, projection;
1332 Cursor cursor;
1333 int column;
1334
1335 projection = new String[] {
1336 Document.COLUMN_DISPLAY_NAME,
1337 Document.COLUMN_DOCUMENT_ID,
1338 Document.COLUMN_MIME_TYPE,
1339 };
1340
1341 /* Parse the URI identifying the tree first. */
1342 uri = Uri.parse (tree_uri);
1343
1344 /* Now, split NAME into its individual components. */
1345 components = name.split ("/");
1346 1327
1347 /* Set id and type to the value at the root of the tree. */ 1328 if (storageThread == null)
1348 type = id = null;
1349
1350 /* For each component... */
1351
1352 for (String component : components)
1353 { 1329 {
1354 /* Java split doesn't behave very much like strtok when it 1330 storageThread = new EmacsSafThread (resolver);
1355 comes to trailing and leading delimiters... */ 1331 storageThread.start ();
1356 if (component.isEmpty ())
1357 continue;
1358
1359 /* Create the tree URI for URI from ID if it exists, or the
1360 root otherwise. */
1361
1362 if (id == null)
1363 id = DocumentsContract.getTreeDocumentId (uri);
1364
1365 treeUri
1366 = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id);
1367
1368 /* Look for a file in this directory by the name of
1369 component. */
1370
1371 try
1372 {
1373 cursor = resolver.query (treeUri, projection,
1374 (Document.COLUMN_DISPLAY_NAME
1375 + " = ?s"),
1376 new String[] { component, }, null);
1377 }
1378 catch (SecurityException exception)
1379 {
1380 /* A SecurityException can be thrown if Emacs doesn't have
1381 access to treeUri. */
1382 return -1;
1383 }
1384 catch (Exception exception)
1385 {
1386 exception.printStackTrace ();
1387
1388 /* Why is this? */
1389 return -1;
1390 }
1391
1392 if (cursor == null)
1393 return -1;
1394
1395 while (true)
1396 {
1397 /* Even though the query selects for a specific display
1398 name, some content providers nevertheless return every
1399 file within the directory. */
1400
1401 if (!cursor.moveToNext ())
1402 {
1403 cursor.close ();
1404
1405 /* If the last component considered is a
1406 directory... */
1407 if ((type == null
1408 || type.equals (Document.MIME_TYPE_DIR))
1409 /* ... and type and id currently represent the
1410 penultimate component. */
1411 && component == components[components.length - 1])
1412 {
1413 /* The cursor is empty. In this case, return -2
1414 and the current document ID (belonging to the
1415 previous component) in ID_RETURN. */
1416
1417 id_return[0] = id;
1418
1419 /* But return -1 on the off chance that id is
1420 null. */
1421
1422 if (id == null)
1423 return -1;
1424
1425 return -2;
1426 }
1427
1428 /* The last component found is not a directory, so
1429 return -1. */
1430 return -1;
1431 }
1432
1433 /* So move CURSOR to a row with the right display
1434 name. */
1435
1436 column = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
1437
1438 if (column < 0)
1439 continue;
1440
1441 try
1442 {
1443 name = cursor.getString (column);
1444 }
1445 catch (Exception exception)
1446 {
1447 cursor.close ();
1448 return -1;
1449 }
1450
1451 /* Break out of the loop only once a matching component is
1452 found. */
1453
1454 if (name.equals (component))
1455 break;
1456 }
1457
1458 /* Look for a column by the name of COLUMN_DOCUMENT_ID. */
1459
1460 column = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
1461
1462 if (column < 0)
1463 {
1464 cursor.close ();
1465 return -1;
1466 }
1467
1468 /* Now replace ID with the document ID. */
1469
1470 try
1471 {
1472 id = cursor.getString (column);
1473 }
1474 catch (Exception exception)
1475 {
1476 cursor.close ();
1477 return -1;
1478 }
1479
1480 /* If this is the last component, be sure to initialize the
1481 document type. */
1482
1483 if (component == components[components.length - 1])
1484 {
1485 column
1486 = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1487
1488 if (column < 0)
1489 {
1490 cursor.close ();
1491 return -1;
1492 }
1493
1494 try
1495 {
1496 type = cursor.getString (column);
1497 }
1498 catch (Exception exception)
1499 {
1500 cursor.close ();
1501 return -1;
1502 }
1503
1504 /* Type may be NULL depending on how the Cursor returned
1505 is implemented. */
1506
1507 if (type == null)
1508 {
1509 cursor.close ();
1510 return -1;
1511 }
1512 }
1513
1514 /* Now close the cursor. */
1515 cursor.close ();
1516
1517 /* ID may have become NULL if the data is in an invalid
1518 format. */
1519 if (id == null)
1520 return -1;
1521 } 1332 }
1522 1333
1523 /* Here, id is either NULL (meaning the same as TREE_URI), and 1334 return storageThread.documentIdFromName (tree_uri, name,
1524 type is either NULL (in which case id should also be NULL) or 1335 id_return);
1525 the MIME type of the file. */
1526
1527 /* First return the ID. */
1528
1529 if (id == null)
1530 id_return[0] = DocumentsContract.getTreeDocumentId (uri);
1531 else
1532 id_return[0] = id;
1533
1534 /* Next, return whether or not this is a directory. */
1535 if (type == null || type.equals (Document.MIME_TYPE_DIR))
1536 return 1;
1537
1538 return 0;
1539 } 1336 }
1540 1337
1541 /* Return an encoded document URI representing a tree with the 1338 /* Return an encoded document URI representing a tree with the
@@ -1585,130 +1382,24 @@ public final class EmacsService extends Service
1585 modes of the file as in `struct stat', SIZE is the size of the 1382 modes of the file as in `struct stat', SIZE is the size of the
1586 file in BYTES or -1 if not known, and MTIM is the time of the 1383 file in BYTES or -1 if not known, and MTIM is the time of the
1587 last modification to this file in milliseconds since 00:00, 1384 last modification to this file in milliseconds since 00:00,
1588 January 1st, 1970. */ 1385 January 1st, 1970.
1386
1387 OperationCanceledException and other typical exceptions may be
1388 signaled upon receiving async input or other errors. */
1589 1389
1590 public long[] 1390 public long[]
1591 statDocument (String uri, String documentId) 1391 statDocument (String uri, String documentId)
1592 { 1392 {
1593 Uri uriObject; 1393 /* Start the thread used to run SAF requests if it isn't already
1594 String[] projection; 1394 running. */
1595 long[] stat;
1596 int index;
1597 long tem;
1598 String tem1;
1599 Cursor cursor;
1600
1601 uriObject = Uri.parse (uri);
1602
1603 if (documentId == null)
1604 documentId = DocumentsContract.getTreeDocumentId (uriObject);
1605
1606 /* Create a document URI representing DOCUMENTID within URI's
1607 authority. */
1608
1609 uriObject
1610 = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
1611
1612 /* Now stat this document. */
1613
1614 projection = new String[] {
1615 Document.COLUMN_FLAGS,
1616 Document.COLUMN_LAST_MODIFIED,
1617 Document.COLUMN_MIME_TYPE,
1618 Document.COLUMN_SIZE,
1619 };
1620
1621 try
1622 {
1623 cursor = resolver.query (uriObject, projection, null,
1624 null, null);
1625 }
1626 catch (SecurityException exception)
1627 {
1628 /* A SecurityException can be thrown if Emacs doesn't have
1629 access to uriObject. */
1630 return null;
1631 }
1632 catch (UnsupportedOperationException exception)
1633 {
1634 exception.printStackTrace ();
1635
1636 /* Why is this? */
1637 return null;
1638 }
1639
1640 if (cursor == null || !cursor.moveToFirst ())
1641 return null;
1642
1643 /* Create the array of file status. */
1644 stat = new long[3];
1645 1395
1646 try 1396 if (storageThread == null)
1647 { 1397 {
1648 index = cursor.getColumnIndex (Document.COLUMN_FLAGS); 1398 storageThread = new EmacsSafThread (resolver);
1649 if (index < 0) 1399 storageThread.start ();
1650 return null;
1651
1652 tem = cursor.getInt (index);
1653
1654 stat[0] |= S_IRUSR;
1655 if ((tem & Document.FLAG_SUPPORTS_WRITE) != 0)
1656 stat[0] |= S_IWUSR;
1657
1658 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
1659 && (tem & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
1660 stat[0] |= S_IFCHR;
1661
1662 index = cursor.getColumnIndex (Document.COLUMN_SIZE);
1663 if (index < 0)
1664 return null;
1665
1666 if (cursor.isNull (index))
1667 stat[1] = -1; /* The size is unknown. */
1668 else
1669 stat[1] = cursor.getLong (index);
1670
1671 index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1672 if (index < 0)
1673 return null;
1674
1675 tem1 = cursor.getString (index);
1676
1677 /* Check if this is a directory file. */
1678 if (tem1.equals (Document.MIME_TYPE_DIR)
1679 /* Files shouldn't be specials and directories at the same
1680 time, but Android doesn't forbid document providers
1681 from returning this information. */
1682 && (stat[0] & S_IFCHR) == 0)
1683 /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories,
1684 just assume they're writable. */
1685 stat[0] |= S_IFDIR | S_IWUSR;
1686
1687 /* If this file is neither a character special nor a
1688 directory, indicate that it's a regular file. */
1689
1690 if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0)
1691 stat[0] |= S_IFREG;
1692
1693 index = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED);
1694
1695 if (index >= 0 && !cursor.isNull (index))
1696 {
1697 /* Content providers are allowed to not provide mtime. */
1698 tem = cursor.getLong (index);
1699 stat[2] = tem;
1700 }
1701 }
1702 catch (Exception exception)
1703 {
1704 /* Whether or not type errors cause exceptions to be signaled
1705 is defined ``by the implementation of Cursor'', whatever
1706 that means. */
1707 exception.printStackTrace ();
1708 return null;
1709 } 1400 }
1710 1401
1711 return stat; 1402 return storageThread.statDocument (uri, documentId);
1712 } 1403 }
1713 1404
1714 /* Find out whether Emacs has access to the document designated by 1405 /* Find out whether Emacs has access to the document designated by
@@ -1733,83 +1424,16 @@ public final class EmacsService extends Service
1733 public int 1424 public int
1734 accessDocument (String uri, String documentId, boolean writable) 1425 accessDocument (String uri, String documentId, boolean writable)
1735 { 1426 {
1736 Uri uriObject; 1427 /* Start the thread used to run SAF requests if it isn't already
1737 String[] projection; 1428 running. */
1738 int tem, index;
1739 String tem1;
1740 Cursor cursor;
1741
1742 uriObject = Uri.parse (uri);
1743
1744 if (documentId == null)
1745 documentId = DocumentsContract.getTreeDocumentId (uriObject);
1746
1747 /* Create a document URI representing DOCUMENTID within URI's
1748 authority. */
1749
1750 uriObject
1751 = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
1752
1753 /* Now stat this document. */
1754 1429
1755 projection = new String[] { 1430 if (storageThread == null)
1756 Document.COLUMN_FLAGS,
1757 Document.COLUMN_MIME_TYPE,
1758 };
1759
1760 cursor = resolver.query (uriObject, projection, null,
1761 null, null);
1762
1763 if (cursor == null || !cursor.moveToFirst ())
1764 return -1;
1765
1766 if (!writable)
1767 return 0;
1768
1769 try
1770 { 1431 {
1771 index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE); 1432 storageThread = new EmacsSafThread (resolver);
1772 if (index < 0) 1433 storageThread.start ();
1773 return -3;
1774
1775 /* Get the type of this file to check if it's a directory. */
1776 tem1 = cursor.getString (index);
1777
1778 /* Check if this is a directory file. */
1779 if (tem1.equals (Document.MIME_TYPE_DIR))
1780 {
1781 /* If so, don't check for FLAG_SUPPORTS_WRITE.
1782 Check for FLAG_DIR_SUPPORTS_CREATE instead. */
1783
1784 if (!writable)
1785 return 0;
1786
1787 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
1788 if (index < 0)
1789 return -3;
1790
1791 tem = cursor.getInt (index);
1792 if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
1793 return -3;
1794
1795 return 0;
1796 }
1797
1798 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
1799 if (index < 0)
1800 return -3;
1801
1802 tem = cursor.getInt (index);
1803 if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0)
1804 return -3;
1805 }
1806 finally
1807 {
1808 /* Close the cursor if an exception occurs. */
1809 cursor.close ();
1810 } 1434 }
1811 1435
1812 return 0; 1436 return storageThread.accessDocument (uri, documentId, writable);
1813 } 1437 }
1814 1438
1815 /* Open a cursor representing each entry within the directory 1439 /* Open a cursor representing each entry within the directory
@@ -1825,34 +1449,16 @@ public final class EmacsService extends Service
1825 public Cursor 1449 public Cursor
1826 openDocumentDirectory (String uri, String documentId) 1450 openDocumentDirectory (String uri, String documentId)
1827 { 1451 {
1828 Uri uriObject; 1452 /* Start the thread used to run SAF requests if it isn't already
1829 Cursor cursor; 1453 running. */
1830 String projection[];
1831
1832 uriObject = Uri.parse (uri);
1833
1834 /* If documentId is not set, use the document ID of the tree URI
1835 itself. */
1836
1837 if (documentId == null)
1838 documentId = DocumentsContract.getTreeDocumentId (uriObject);
1839
1840 /* Build a URI representing each directory entry within
1841 DOCUMENTID. */
1842
1843 uriObject
1844 = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject,
1845 documentId);
1846 1454
1847 projection = new String [] { 1455 if (storageThread == null)
1848 Document.COLUMN_DISPLAY_NAME, 1456 {
1849 Document.COLUMN_MIME_TYPE, 1457 storageThread = new EmacsSafThread (resolver);
1850 }; 1458 storageThread.start ();
1459 }
1851 1460
1852 cursor = resolver.query (uriObject, projection, null, null, 1461 return storageThread.openDocumentDirectory (uri, documentId);
1853 null);
1854 /* Return the cursor. */
1855 return cursor;
1856 } 1462 }
1857 1463
1858 /* Read a single directory entry from the specified CURSOR. Return 1464 /* Read a single directory entry from the specified CURSOR. Return
@@ -1945,50 +1551,18 @@ public final class EmacsService extends Service
1945 public ParcelFileDescriptor 1551 public ParcelFileDescriptor
1946 openDocument (String uri, String documentId, boolean write, 1552 openDocument (String uri, String documentId, boolean write,
1947 boolean truncate) 1553 boolean truncate)
1948 throws FileNotFoundException
1949 { 1554 {
1950 Uri treeUri, documentUri; 1555 /* Start the thread used to run SAF requests if it isn't already
1951 String mode; 1556 running. */
1952 ParcelFileDescriptor fileDescriptor;
1953
1954 treeUri = Uri.parse (uri);
1955
1956 /* documentId must be set for this request, since it doesn't make
1957 sense to ``open'' the root of the directory tree. */
1958 1557
1959 documentUri 1558 if (storageThread == null)
1960 = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId);
1961
1962 if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
1963 { 1559 {
1964 /* Select the mode used to open the file. `rw' means open 1560 storageThread = new EmacsSafThread (resolver);
1965 a stat-able file, while `rwt' means that and to 1561 storageThread.start ();
1966 truncate the file as well. */
1967
1968 if (truncate)
1969 mode = "rwt";
1970 else
1971 mode = "rw";
1972
1973 fileDescriptor
1974 = resolver.openFileDescriptor (documentUri, mode,
1975 null);
1976 }
1977 else
1978 {
1979 /* Select the mode used to open the file. `openFile'
1980 below means always open a stat-able file. */
1981
1982 if (truncate)
1983 /* Invalid mode! */
1984 return null;
1985 else
1986 mode = "r";
1987
1988 fileDescriptor = resolver.openFile (documentUri, mode, null);
1989 } 1562 }
1990 1563
1991 return fileDescriptor; 1564 return storageThread.openDocument (uri, documentId, write,
1565 truncate);
1992 } 1566 }
1993 1567
1994 /* Create a new document with the given display NAME within the 1568 /* Create a new document with the given display NAME within the