1 /* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
2
3 Copyright (C) 2023 Free Software Foundation, Inc.
4
5 This file is part of GNU Emacs.
6
7 GNU Emacs is free software: you can redistribute it and/or modify
8 it under the terms of the GNU General Public License as published by
9 the Free Software Foundation, either version 3 of the License, or (at
10 your option) any later version.
11
12 GNU Emacs is distributed in the hope that it will be useful,
13 but WITHOUT ANY WARRANTY; without even the implied warranty of
14 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15 GNU General Public License for more details.
16
17 You should have received a copy of the GNU General Public License
18 along with GNU Emacs. If not, see <https://www.gnu.org/licenses/>. */
19
20 package org.gnu.emacs;
21
22 import java.util.Collection;
23 import java.util.HashMap;
24 import java.util.Iterator;
25
26 import java.io.FileNotFoundException;
27 import java.io.IOException;
28
29 import android.content.ContentResolver;
30 import android.database.Cursor;
31 import android.net.Uri;
32
33 import android.os.Build;
34 import android.os.CancellationSignal;
35 import android.os.Handler;
36 import android.os.HandlerThread;
37 import android.os.OperationCanceledException;
38 import android.os.ParcelFileDescriptor;
39 import android.os.SystemClock;
40
41 import android.util.Log;
42
43 import android.provider.DocumentsContract;
44 import android.provider.DocumentsContract.Document;
45
46
47
48 /* Emacs runs long-running SAF operations on a second thread running
49 its own handler. These operations include opening files and
50 maintaining the path to document ID cache.
51
52 Because Emacs paths are based on file display names, while Android
53 document identifiers have no discernible hierarchy of their own,
54 each file name lookup must carry out a repeated search for
55 directory documents with the names of all of the file name's
56 constituent components, where each iteration searches within the
57 directory document identified by the previous iteration.
58
59 A time limited cache tying components to document IDs is maintained
60 in order to speed up consecutive searches for file names sharing
61 the same components. Since listening for changes to each document
62 in the cache is prohibitively expensive, Emacs instead elects to
63 periodically remove entries that are older than a predetermined
64 amount of a time.
65
66 The cache is split into two levels: the first caches the
67 relationships between display names and document IDs, while the
68 second caches individual document IDs and their contents (children,
69 type, etc.)
70
71 Long-running operations are also run on this thread for another
72 reason: Android uses special cancellation objects to terminate
73 ongoing IPC operations. However, the functions that perform these
74 operations block instead of providing mechanisms for the caller to
75 wait for their completion while also reading async input, as a
76 consequence of which the calling thread is unable to signal the
77 cancellation objects that it provides. Performing the blocking
78 operations in this auxiliary thread enables the main thread to wait
79 for completion itself, signaling the cancellation objects when it
80 deems necessary. */
81
82
83
84 public final class EmacsSafThread extends HandlerThread
85 {
86 private static final String TAG = "EmacsSafThread";
87
88 /* The content resolver used by this thread. */
89 private final ContentResolver resolver;
90
91 /* Map between tree URIs and the cache entry representing its
92 toplevel directory. */
93 private final HashMap<Uri, CacheToplevel> cacheToplevels;
94
95 /* Handler for this thread's main loop. */
96 private Handler handler;
97
98 /* File access mode constants. See `man 7 inode'. */
99 public static final int S_IRUSR = 0000400;
100 public static final int S_IWUSR = 0000200;
101 public static final int S_IXUSR = 0000100;
102 public static final int S_IFCHR = 0020000;
103 public static final int S_IFDIR = 0040000;
104 public static final int S_IFREG = 0100000;
105
106 /* Number of seconds in between each attempt to prune the storage
107 cache. */
108 public static final int CACHE_PRUNE_TIME = 10;
109
110 /* Number of seconds after which an entry in the cache is to be
111 considered invalid. */
112 public static final int CACHE_INVALID_TIME = 10;
113
114 public
115 EmacsSafThread (ContentResolver resolver)
116 {
117 super ("Document provider access thread");
118 this.resolver = resolver;
119 this.cacheToplevels = new HashMap<Uri, CacheToplevel> ();
120 }
121
122
123
124 @Override
125 public void
126 start ()
127 {
128 super.start ();
129
130 /* Set up the handler after the thread starts. */
131 handler = new Handler (getLooper ());
132
133 /* And start periodically pruning the cache. */
134 postPruneMessage ();
135 }
136
137
138 private static final class CacheToplevel
139 {
140 /* Map between document names and children. */
141 HashMap<String, DocIdEntry> children;
142
143 /* Map between document names and file status. */
144 HashMap<String, StatCacheEntry> statCache;
145
146 /* Map between document IDs and cache items. */
147 HashMap<String, CacheEntry> idCache;
148 };
149
150 private static final class StatCacheEntry
151 {
152 /* The time at which this cache entry was created. */
153 long time;
154
155 /* Flags, size, and modification time of this file. */
156 long flags, size, mtime;
157
158 /* Whether or not this file is a directory. */
159 boolean isDirectory;
160
161 public
162 StatCacheEntry ()
163 {
164 time = SystemClock.uptimeMillis ();
165 }
166
167 public boolean
168 isValid ()
169 {
170 return ((SystemClock.uptimeMillis () - time)
171 < CACHE_INVALID_TIME * 1000);
172 }
173 };
174
175 private static final class DocIdEntry
176 {
177 /* The document ID. */
178 String documentId;
179
180 /* The time this entry was created. */
181 long time;
182
183 public
184 DocIdEntry ()
185 {
186 time = SystemClock.uptimeMillis ();
187 }
188
189 /* Return a cache entry comprised of the state of the file
190 identified by `documentId'. TREE is the URI of the tree
191 containing this entry, and TOPLEVEL is the toplevel
192 representing it. SIGNAL is a cancellation signal.
193
194 RESOLVER is the content provider used to retrieve file
195 information.
196
197 Value is NULL if the file cannot be found. */
198
199 public CacheEntry
200 getCacheEntry (ContentResolver resolver, Uri tree,
201 CacheToplevel toplevel,
202 CancellationSignal signal)
203 {
204 Uri uri;
205 String[] projection;
206 String type;
207 Cursor cursor;
208 int column;
209 CacheEntry entry;
210
211 /* Create a document URI representing DOCUMENTID within URI's
212 authority. */
213
214 uri = DocumentsContract.buildDocumentUriUsingTree (tree,
215 documentId);
216 projection = new String[] {
217 Document.COLUMN_MIME_TYPE,
218 };
219
220 cursor = null;
221
222 try
223 {
224 cursor = resolver.query (uri, projection, null,
225 null, null, signal);
226
227 if (!cursor.moveToFirst ())
228 return null;
229
230 column = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
231
232 if (column < 0)
233 return null;
234
235 type = cursor.getString (column);
236
237 if (type == null)
238 return null;
239
240 entry = new CacheEntry ();
241 entry.type = type;
242 toplevel.idCache.put (documentId, entry);
243 return entry;
244 }
245 catch (OperationCanceledException e)
246 {
247 throw e;
248 }
249 catch (Throwable e)
250 {
251 return null;
252 }
253 finally
254 {
255 if (cursor != null)
256 cursor.close ();
257 }
258 }
259
260 public boolean
261 isValid ()
262 {
263 return ((SystemClock.uptimeMillis () - time)
264 < CACHE_INVALID_TIME * 1000);
265 }
266 };
267
268 private static final class CacheEntry
269 {
270 /* The type of this document. */
271 String type;
272
273 /* Map between document names and children. */
274 HashMap<String, DocIdEntry> children;
275
276 /* The time this entry was created. */
277 long time;
278
279 public
280 CacheEntry ()
281 {
282 children = new HashMap<String, DocIdEntry> ();
283 time = SystemClock.uptimeMillis ();
284 }
285
286 public boolean
287 isValid ()
288 {
289 return ((SystemClock.uptimeMillis () - time)
290 < CACHE_INVALID_TIME * 1000);
291 }
292 };
293
294 /* Create or return a toplevel for the given tree URI. */
295
296 private CacheToplevel
297 getCache (Uri uri)
298 {
299 CacheToplevel toplevel;
300
301 toplevel = cacheToplevels.get (uri);
302
303 if (toplevel != null)
304 return toplevel;
305
306 toplevel = new CacheToplevel ();
307 toplevel.children = new HashMap<String, DocIdEntry> ();
308 toplevel.statCache = new HashMap<String, StatCacheEntry> ();
309 toplevel.idCache = new HashMap<String, CacheEntry> ();
310 cacheToplevels.put (uri, toplevel);
311 return toplevel;
312 }
313
314 /* Remove each cache entry within COLLECTION older than
315 CACHE_INVALID_TIME. */
316
317 private void
318 pruneCache1 (Collection<DocIdEntry> collection)
319 {
320 Iterator<DocIdEntry> iter;
321 DocIdEntry tem;
322
323 iter = collection.iterator ();
324 while (iter.hasNext ())
325 {
326 /* Get the cache entry. */
327 tem = iter.next ();
328
329 /* If it's not valid anymore, remove it. Iterating over a
330 collection whose contents are being removed is undefined
331 unless the removal is performed using the iterator's own
332 `remove' function, so tem.remove cannot be used here. */
333
334 if (tem.isValid ())
335 continue;
336
337 iter.remove ();
338 }
339 }
340
341 /* Remove every entry older than CACHE_INVALID_TIME from each
342 toplevel inside `cachedToplevels'. */
343
344 private void
345 pruneCache ()
346 {
347 Iterator<CacheEntry> iter;
348 Iterator<StatCacheEntry> statIter;
349 CacheEntry tem;
350 StatCacheEntry stat;
351
352 for (CacheToplevel toplevel : cacheToplevels.values ())
353 {
354 /* First, clean up expired cache entries. */
355 iter = toplevel.idCache.values ().iterator ();
356
357 while (iter.hasNext ())
358 {
359 /* Get the cache entry. */
360 tem = iter.next ();
361
362 /* If it's not valid anymore, remove it. Iterating over a
363 collection whose contents are being removed is
364 undefined unless the removal is performed using the
365 iterator's own `remove' function, so tem.remove cannot
366 be used here. */
367
368 if (tem.isValid ())
369 {
370 /* Otherwise, clean up expired items in its document
371 ID cache. */
372 pruneCache1 (tem.children.values ());
373 continue;
374 }
375
376 iter.remove ();
377 }
378
379 statIter = toplevel.statCache.values ().iterator ();
380
381 while (statIter.hasNext ())
382 {
383 /* Get the cache entry. */
384 stat = statIter.next ();
385
386 /* If it's not valid anymore, remove it. Iterating over a
387 collection whose contents are being removed is
388 undefined unless the removal is performed using the
389 iterator's own `remove' function, so tem.remove cannot
390 be used here. */
391
392 if (stat.isValid ())
393 continue;
394
395 statIter.remove ();
396 }
397 }
398
399 postPruneMessage ();
400 }
401
402 /* Cache file information within TOPLEVEL, under the list of
403 children CHILDREN.
404
405 NAME, ID, and TYPE should respectively be the display name of the
406 document within its parent document (the CacheEntry whose
407 `children' field is CHILDREN), its document ID, and its MIME
408 type.
409
410 If ID_ENTRY_EXISTS, don't create a new document ID entry within
411 CHILDREN indexed by NAME.
412
413 Value is the cache entry saved for the document ID. */
414
415 private CacheEntry
416 cacheChild (CacheToplevel toplevel,
417 HashMap<String, DocIdEntry> children,
418 String name, String id, String type,
419 boolean id_entry_exists)
420 {
421 DocIdEntry idEntry;
422 CacheEntry cacheEntry;
423
424 if (!id_entry_exists)
425 {
426 idEntry = new DocIdEntry ();
427 idEntry.documentId = id;
428 children.put (name, idEntry);
429 }
430
431 cacheEntry = new CacheEntry ();
432 cacheEntry.type = type;
433 toplevel.idCache.put (id, cacheEntry);
434 return cacheEntry;
435 }
436
437 /* Cache file status for DOCUMENTID within TOPLEVEL. Value is the
438 new cache entry. CURSOR is the cursor from where to retrieve the
439 file status, in the form of the columns COLUMN_FLAGS,
440 COLUMN_SIZE, COLUMN_MIME_TYPE and COLUMN_LAST_MODIFIED. */
441
442 private StatCacheEntry
443 cacheFileStatus (String documentId, CacheToplevel toplevel,
444 Cursor cursor)
445 {
446 StatCacheEntry entry;
447 int flagsIndex, columnIndex, typeIndex;
448 int sizeIndex, mtimeIndex;
449 String type;
450
451 /* Obtain the indices for columns wanted from this cursor. */
452 flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS);
453 sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE);
454 typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
455 mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED);
456
457 /* COLUMN_LAST_MODIFIED is allowed to be absent in a
458 conforming documents provider. */
459 if (flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0)
460 return null;
461
462 /* Get the file status from CURSOR. */
463 entry = new StatCacheEntry ();
464 entry.flags = cursor.getInt (flagsIndex);
465 type = cursor.getString (typeIndex);
466
467 if (type == null)
468 return null;
469
470 entry.isDirectory = type.equals (Document.MIME_TYPE_DIR);
471
472 if (cursor.isNull (sizeIndex))
473 /* The size is unknown. */
474 entry.size = -1;
475 else
476 entry.size = cursor.getLong (sizeIndex);
477
478 /* mtimeIndex is potentially unset, since document providers
479 aren't obligated to provide modification times. */
480
481 if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex))
482 entry.mtime = cursor.getLong (mtimeIndex);
483
484 /* Finally, add this entry to the cache and return. */
485 toplevel.statCache.put (documentId, entry);
486 return entry;
487 }
488
489 /* Cache the type and as many of the children of the directory
490 designated by DOCUMENTID as possible into TOPLEVEL.
491
492 CURSOR should be a cursor representing an open directory stream,
493 with its projection consisting of at least the display name,
494 document ID and MIME type columns.
495
496 Rewind the position of CURSOR to before its first element after
497 completion. */
498
499 private void
500 cacheDirectoryFromCursor (CacheToplevel toplevel, String documentId,
501 Cursor cursor)
502 {
503 CacheEntry entry, constitutent;
504 int nameColumn, idColumn, typeColumn;
505 String id, name, type;
506 DocIdEntry idEntry;
507
508 /* Find the numbers of the columns wanted. */
509
510 nameColumn
511 = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
512 idColumn
513 = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
514 typeColumn
515 = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
516
517 if (nameColumn < 0 || idColumn < 0 || typeColumn < 0)
518 return;
519
520 entry = new CacheEntry ();
521
522 /* We know this is a directory already. */
523 entry.type = Document.MIME_TYPE_DIR;
524 toplevel.idCache.put (documentId, entry);
525
526 /* Now, try to cache each of its constituents. */
527
528 while (cursor.moveToNext ())
529 {
530 try
531 {
532 name = cursor.getString (nameColumn);
533 id = cursor.getString (idColumn);
534 type = cursor.getString (typeColumn);
535
536 if (name == null || id == null || type == null)
537 continue;
538
539 /* First, add the name and ID to ENTRY's map of
540 children. */
541 idEntry = new DocIdEntry ();
542 idEntry.documentId = id;
543 entry.children.put (id, idEntry);
544
545 /* Cache the file status for ID within TOPELVEL too; if a
546 directory listing is being requested, it's very likely
547 that a series of calls for file status will follow. */
548
549 cacheFileStatus (id, toplevel, cursor);
550
551 /* If this constituent is a directory, don't cache any
552 information about it. It cannot be cached without
553 knowing its children. */
554
555 if (type.equals (Document.MIME_TYPE_DIR))
556 continue;
557
558 /* Otherwise, create a new cache entry comprised of its
559 type. */
560 constitutent = new CacheEntry ();
561 constitutent.type = type;
562 toplevel.idCache.put (documentId, entry);
563 }
564 catch (Exception e)
565 {
566 e.printStackTrace ();
567 continue;
568 }
569 }
570
571 /* Rewind cursor back to the beginning. */
572 cursor.moveToPosition (-1);
573 }
574
575 /* Post a message to run `pruneCache' every CACHE_PRUNE_TIME
576 seconds. */
577
578 private void
579 postPruneMessage ()
580 {
581 handler.postDelayed (new Runnable () {
582 @Override
583 public void
584 run ()
585 {
586 pruneCache ();
587 }
588 }, CACHE_PRUNE_TIME * 1000);
589 }
590
591 /* Invalidate the cache entry denoted by DOCUMENT_ID, within the
592 document tree URI.
593 Call this after deleting a document or directory.
594
595 At the same time, remove the final component within the file name
596 CACHENAME from the cache if it exists. */
597
598 public void
599 postInvalidateCache (final Uri uri, final String documentId,
600 final String cacheName)
601 {
602 handler.post (new Runnable () {
603 @Override
604 public void
605 run ()
606 {
607 CacheToplevel toplevel;
608 HashMap<String, DocIdEntry> children;
609 String[] components;
610 CacheEntry entry;
611 DocIdEntry idEntry;
612
613 toplevel = getCache (uri);
614 toplevel.idCache.remove (documentId);
615 toplevel.statCache.remove (documentId);
616
617 /* If the parent of CACHENAME is cached, remove it. */
618
619 children = toplevel.children;
620 components = cacheName.split ("/");
621
622 for (String component : components)
623 {
624 /* Java `split' removes trailing empty matches but not
625 leading or intermediary ones. */
626 if (component.isEmpty ())
627 continue;
628
629 if (component == components[components.length - 1])
630 {
631 /* This is the last component, so remove it from
632 children. */
633 children.remove (component);
634 return;
635 }
636 else
637 {
638 /* Search for this component within the last level
639 of the cache. */
640
641 idEntry = children.get (component);
642
643 if (idEntry == null)
644 /* Not cached, so return. */
645 return;
646
647 entry = toplevel.idCache.get (idEntry.documentId);
648
649 if (entry == null)
650 /* Not cached, so return. */
651 return;
652
653 /* Locate the next component within this
654 directory. */
655 children = entry.children;
656 }
657 }
658 }
659 });
660 }
661
662 /* Invalidate the cache entry denoted by DOCUMENT_ID, within the
663 document tree URI.
664 Call this after deleting a document or directory.
665
666 At the same time, remove the child referring to DOCUMENTID from
667 within CACHENAME's cache entry if it exists. */
668
669 public void
670 postInvalidateCacheDir (final Uri uri, final String documentId,
671 final String cacheName)
672 {
673 handler.post (new Runnable () {
674 @Override
675 public void
676 run ()
677 {
678 CacheToplevel toplevel;
679 HashMap<String, DocIdEntry> children;
680 String[] components;
681 CacheEntry entry;
682 DocIdEntry idEntry;
683 Iterator<DocIdEntry> iter;
684
685 toplevel = getCache (uri);
686 toplevel.idCache.remove (documentId);
687 toplevel.statCache.remove (documentId);
688
689 /* Now remove DOCUMENTID from CACHENAME's cache entry, if
690 any. */
691
692 children = toplevel.children;
693 components = cacheName.split ("/");
694
695 for (String component : components)
696 {
697 /* Java `split' removes trailing empty matches but not
698 leading or intermediary ones. */
699 if (component.isEmpty ())
700 continue;
701
702 /* Search for this component within the last level
703 of the cache. */
704
705 idEntry = children.get (component);
706
707 if (idEntry == null)
708 /* Not cached, so return. */
709 return;
710
711 entry = toplevel.idCache.get (idEntry.documentId);
712
713 if (entry == null)
714 /* Not cached, so return. */
715 return;
716
717 /* Locate the next component within this
718 directory. */
719 children = entry.children;
720 }
721
722 iter = children.values ().iterator ();
723 while (iter.hasNext ())
724 {
725 idEntry = iter.next ();
726
727 if (idEntry.documentId.equals (documentId))
728 {
729 iter.remove ();
730 break;
731 }
732 }
733 }
734 });
735 }
736
737 /* Invalidate the file status cache entry for DOCUMENTID within URI.
738 Call this when the contents of a file (i.e. the constituents of a
739 directory file) may have changed, but the document's display name
740 has not. */
741
742 public void
743 postInvalidateStat (final Uri uri, final String documentId)
744 {
745 handler.post (new Runnable () {
746 @Override
747 public void
748 run ()
749 {
750 CacheToplevel toplevel;
751
752 toplevel = getCache (uri);
753 toplevel.statCache.remove (documentId);
754 }
755 });
756 }
757
758
759
760 /* ``Prototypes'' for nested functions that are run within the SAF
761 thread and accepts a cancellation signal. They differ in their
762 return types. */
763
764 private abstract class SafIntFunction
765 {
766 /* The ``throws Throwable'' here is a Java idiosyncracy that tells
767 the compiler to allow arbitrary error objects to be signaled
768 from within this function.
769
770 Later, runIntFunction will try to re-throw any error object
771 generated by this function in the Emacs thread, using a trick
772 to avoid the compiler requirement to expressly declare that an
773 error (and which types of errors) will be signaled. */
774
775 public abstract int runInt (CancellationSignal signal)
776 throws Throwable;
777 };
778
779 private abstract class SafObjectFunction
780 {
781 /* The ``throws Throwable'' here is a Java idiosyncracy that tells
782 the compiler to allow arbitrary error objects to be signaled
783 from within this function.
784
785 Later, runObjectFunction will try to re-throw any error object
786 generated by this function in the Emacs thread, using a trick
787 to avoid the compiler requirement to expressly declare that an
788 error (and which types of errors) will be signaled. */
789
790 public abstract Object runObject (CancellationSignal signal)
791 throws Throwable;
792 };
793
794
795
796 /* Functions that run cancel-able queries. These functions are
797 internally run within the SAF thread. */
798
799 /* Throw the specified EXCEPTION. The type template T is erased by
800 the compiler before the object is compiled, so the compiled code
801 simply throws EXCEPTION without the cast being verified.
802
803 T should be RuntimeException to obtain the desired effect of
804 throwing an exception without a compiler check. */
805
806 @SuppressWarnings("unchecked")
807 private static <T extends Throwable> void
808 throwException (Throwable exception)
809 throws T
810 {
811 throw (T) exception;
812 }
813
814 /* Run the given function (or rather, its `runInt' field) within the
815 SAF thread, waiting for it to complete.
816
817 If async input arrives in the meantime and sets Vquit_flag,
818 signal the cancellation signal supplied to that function.
819
820 Rethrow any exception thrown from that function, and return its
821 value otherwise. */
822
823 private int
824 runIntFunction (final SafIntFunction function)
825 {
826 final EmacsHolder<Object> result;
827 final CancellationSignal signal;
828 Throwable throwable;
829
830 result = new EmacsHolder<Object> ();
831 signal = new CancellationSignal ();
832
833 handler.post (new Runnable () {
834 @Override
835 public void
836 run ()
837 {
838 try
839 {
840 result.thing
841 = Integer.valueOf (function.runInt (signal));
842 }
843 catch (Throwable throwable)
844 {
845 result.thing = throwable;
846 }
847
848 EmacsNative.safPostRequest ();
849 }
850 });
851
852 if (EmacsNative.safSyncAndReadInput () != 0)
853 {
854 signal.cancel ();
855
856 /* Now wait for the function to finish. Either the signal has
857 arrived after the query took place, in which case it will
858 finish normally, or an OperationCanceledException will be
859 thrown. */
860
861 EmacsNative.safSync ();
862 }
863
864 if (result.thing instanceof Throwable)
865 {
866 throwable = (Throwable) result.thing;
867 EmacsSafThread.<RuntimeException>throwException (throwable);
868 }
869
870 return (Integer) result.thing;
871 }
872
873 /* Run the given function (or rather, its `runObject' field) within
874 the SAF thread, waiting for it to complete.
875
876 If async input arrives in the meantime and sets Vquit_flag,
877 signal the cancellation signal supplied to that function.
878
879 Rethrow any exception thrown from that function, and return its
880 value otherwise. */
881
882 private Object
883 runObjectFunction (final SafObjectFunction function)
884 {
885 final EmacsHolder<Object> result;
886 final CancellationSignal signal;
887 Throwable throwable;
888
889 result = new EmacsHolder<Object> ();
890 signal = new CancellationSignal ();
891
892 handler.post (new Runnable () {
893 @Override
894 public void
895 run ()
896 {
897 try
898 {
899 result.thing = function.runObject (signal);
900 }
901 catch (Throwable throwable)
902 {
903 result.thing = throwable;
904 }
905
906 EmacsNative.safPostRequest ();
907 }
908 });
909
910 if (EmacsNative.safSyncAndReadInput () != 0)
911 {
912 signal.cancel ();
913
914 /* Now wait for the function to finish. Either the signal has
915 arrived after the query took place, in which case it will
916 finish normally, or an OperationCanceledException will be
917 thrown. */
918
919 EmacsNative.safSync ();
920 }
921
922 if (result.thing instanceof Throwable)
923 {
924 throwable = (Throwable) result.thing;
925 EmacsSafThread.<RuntimeException>throwException (throwable);
926 }
927
928 return result.thing;
929 }
930
931 /* The crux of `documentIdFromName1', run within the SAF thread.
932 SIGNAL should be a cancellation signal run upon quitting. */
933
934 private int
935 documentIdFromName1 (String tree_uri, String name,
936 String[] id_return, CancellationSignal signal)
937 {
938 Uri uri, treeUri;
939 String id, type, newId, newType;
940 String[] components, projection;
941 Cursor cursor;
942 int nameColumn, idColumn, typeColumn;
943 CacheToplevel toplevel;
944 DocIdEntry idEntry;
945 HashMap<String, DocIdEntry> children, next;
946 CacheEntry cache;
947
948 projection = new String[] {
949 Document.COLUMN_DISPLAY_NAME,
950 Document.COLUMN_DOCUMENT_ID,
951 Document.COLUMN_MIME_TYPE,
952 };
953
954 /* Parse the URI identifying the tree first. */
955 uri = Uri.parse (tree_uri);
956
957 /* Now, split NAME into its individual components. */
958 components = name.split ("/");
959
960 /* Set id and type to the value at the root of the tree. */
961 type = id = null;
962 cursor = null;
963
964 /* Obtain the top level of this cache. */
965 toplevel = getCache (uri);
966
967 /* Set the current map of children to this top level. */
968 children = toplevel.children;
969
970 /* For each component... */
971
972 try
973 {
974 for (String component : components)
975 {
976 /* Java split doesn't behave very much like strtok when it
977 comes to trailing and leading delimiters... */
978 if (component.isEmpty ())
979 continue;
980
981 /* Search for component within the currently cached list
982 of children. */
983
984 idEntry = children.get (component);
985
986 if (idEntry != null)
987 {
988 /* The document ID is known. Now find the
989 corresponding document ID cache. */
990
991 cache = toplevel.idCache.get (idEntry.documentId);
992
993 /* Fetch just the information for this document. */
994
995 if (cache == null)
996 cache = idEntry.getCacheEntry (resolver, uri, toplevel,
997 signal);
998
999 if (cache == null)
1000 {
1001 /* File status matching idEntry could not be
1002 obtained. Treat this as if the file does not
1003 exist. */
1004
1005 children.remove (component);
1006
1007 if (id == null)
1008 id = DocumentsContract.getTreeDocumentId (uri);
1009
1010 id_return[0] = id;
1011
1012 if ((type == null
1013 || type.equals (Document.MIME_TYPE_DIR))
1014 /* ... and type and id currently represent the
1015 penultimate component. */
1016 && component == components[components.length - 1])
1017 return -2;
1018
1019 return -1;
1020 }
1021
1022 /* Otherwise, use the cached information. */
1023 id = idEntry.documentId;
1024 type = cache.type;
1025 children = cache.children;
1026 continue;
1027 }
1028
1029 /* Create the tree URI for URI from ID if it exists, or
1030 the root otherwise. */
1031
1032 if (id == null)
1033 id = DocumentsContract.getTreeDocumentId (uri);
1034
1035 treeUri
1036 = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id);
1037
1038 /* Look for a file in this directory by the name of
1039 component. */
1040
1041 cursor = resolver.query (treeUri, projection,
1042 (Document.COLUMN_DISPLAY_NAME
1043 + " = ?"),
1044 new String[] { component, },
1045 null, signal);
1046
1047 if (cursor == null)
1048 return -1;
1049
1050 /* Find the column numbers for each of the columns that
1051 are wanted. */
1052
1053 nameColumn
1054 = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
1055 idColumn
1056 = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
1057 typeColumn
1058 = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1059
1060 if (nameColumn < 0 || idColumn < 0 || typeColumn < 0)
1061 return -1;
1062
1063 next = null;
1064
1065 while (true)
1066 {
1067 /* Even though the query selects for a specific
1068 display name, some content providers nevertheless
1069 return every file within the directory. */
1070
1071 if (!cursor.moveToNext ())
1072 {
1073 /* If a component has been found, break out of the
1074 loop. */
1075
1076 if (next != null)
1077 break;
1078
1079 /* If the last component considered is a
1080 directory... */
1081 if ((type == null
1082 || type.equals (Document.MIME_TYPE_DIR))
1083 /* ... and type and id currently represent the
1084 penultimate component. */
1085 && component == components[components.length - 1])
1086 {
1087 /* The cursor is empty. In this case, return
1088 -2 and the current document ID (belonging
1089 to the previous component) in
1090 ID_RETURN. */
1091
1092 id_return[0] = id;
1093
1094 /* But return -1 on the off chance that id is
1095 null. */
1096
1097 if (id == null)
1098 return -1;
1099
1100 return -2;
1101 }
1102
1103 /* The last component found is not a directory, so
1104 return -1. */
1105 return -1;
1106 }
1107
1108 /* So move CURSOR to a row with the right display
1109 name. */
1110
1111 name = cursor.getString (nameColumn);
1112 newId = cursor.getString (idColumn);
1113 newType = cursor.getString (typeColumn);
1114
1115 /* Any of the three variables above may be NULL if the
1116 column data is of the wrong type depending on how
1117 the Cursor returned is implemented. */
1118
1119 if (name == null || newId == null || newType == null)
1120 return -1;
1121
1122 /* Cache this name, even if it isn't the document
1123 that's being searched for. */
1124
1125 cache = cacheChild (toplevel, children, name,
1126 newId, newType,
1127 idEntry != null);
1128
1129 /* Record the desired component once it is located,
1130 but continue reading and caching items from the
1131 cursor. */
1132
1133 if (name.equals (component))
1134 {
1135 id = newId;
1136 next = cache.children;
1137 type = newType;
1138 }
1139 }
1140
1141 children = next;
1142
1143 /* Now close the cursor. */
1144 cursor.close ();
1145 cursor = null;
1146
1147 /* ID may have become NULL if the data is in an invalid
1148 format. */
1149 if (id == null)
1150 return -1;
1151 }
1152 }
1153 finally
1154 {
1155 /* If an error is thrown within the block above, let
1156 android_saf_exception_check handle it, but make sure the
1157 cursor is closed. */
1158
1159 if (cursor != null)
1160 cursor.close ();
1161 }
1162
1163 /* Here, id is either NULL (meaning the same as TREE_URI), and
1164 type is either NULL (in which case id should also be NULL) or
1165 the MIME type of the file. */
1166
1167 /* First return the ID. */
1168
1169 if (id == null)
1170 id_return[0] = DocumentsContract.getTreeDocumentId (uri);
1171 else
1172 id_return[0] = id;
1173
1174 /* Next, return whether or not this is a directory. */
1175 if (type == null || type.equals (Document.MIME_TYPE_DIR))
1176 return 1;
1177
1178 return 0;
1179 }
1180
1181 /* Find the document ID of the file within TREE_URI designated by
1182 NAME.
1183
1184 NAME is a ``file name'' comprised of the display names of
1185 individual files. Each constituent component prior to the last
1186 must name a directory file within TREE_URI.
1187
1188 Upon success, return 0 or 1 (contingent upon whether or not the
1189 last component within NAME is a directory) and place the document
1190 ID of the named file in ID_RETURN[0].
1191
1192 If the designated file can't be located, but each component of
1193 NAME up to the last component can and is a directory, return -2
1194 and the ID of the last component located in ID_RETURN[0].
1195
1196 If the designated file can't be located, return -1, or signal one
1197 of OperationCanceledException, SecurityException,
1198 FileNotFoundException, or UnsupportedOperationException. */
1199
1200 public int
1201 documentIdFromName (final String tree_uri, final String name,
1202 final String[] id_return)
1203 {
1204 return runIntFunction (new SafIntFunction () {
1205 @Override
1206 public int
1207 runInt (CancellationSignal signal)
1208 {
1209 return documentIdFromName1 (tree_uri, name, id_return,
1210 signal);
1211 }
1212 });
1213 }
1214
1215 /* The bulk of `statDocument'. SIGNAL should be a cancelation
1216 signal. */
1217
1218 private long[]
1219 statDocument1 (String uri, String documentId,
1220 CancellationSignal signal)
1221 {
1222 Uri uriObject, tree;
1223 String[] projection;
1224 long[] stat;
1225 Cursor cursor;
1226 CacheToplevel toplevel;
1227 StatCacheEntry cache;
1228
1229 tree = Uri.parse (uri);
1230
1231 if (documentId == null)
1232 documentId = DocumentsContract.getTreeDocumentId (tree);
1233
1234 /* Create a document URI representing DOCUMENTID within URI's
1235 authority. */
1236
1237 uriObject
1238 = DocumentsContract.buildDocumentUriUsingTree (tree, documentId);
1239
1240 /* See if the file status cache currently contains this
1241 document. */
1242
1243 toplevel = getCache (tree);
1244 cache = toplevel.statCache.get (documentId);
1245
1246 if (cache == null || !cache.isValid ())
1247 {
1248 /* Stat this document and enter its information into the
1249 cache. */
1250
1251 projection = new String[] {
1252 Document.COLUMN_FLAGS,
1253 Document.COLUMN_LAST_MODIFIED,
1254 Document.COLUMN_MIME_TYPE,
1255 Document.COLUMN_SIZE,
1256 };
1257
1258 cursor = resolver.query (uriObject, projection, null,
1259 null, null, signal);
1260
1261 if (cursor == null)
1262 return null;
1263
1264 try
1265 {
1266 if (!cursor.moveToFirst ())
1267 return null;
1268
1269 cache = cacheFileStatus (documentId, toplevel, cursor);
1270 }
1271 finally
1272 {
1273 cursor.close ();
1274 }
1275
1276 /* If cache is still null, return null. */
1277
1278 if (cache == null)
1279 return null;
1280 }
1281
1282 /* Create the array of file status and populate it with the
1283 information within cache. */
1284 stat = new long[3];
1285
1286 stat[0] |= S_IRUSR;
1287 if ((cache.flags & Document.FLAG_SUPPORTS_WRITE) != 0)
1288 stat[0] |= S_IWUSR;
1289
1290 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
1291 && (cache.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
1292 stat[0] |= S_IFCHR;
1293
1294 stat[1] = cache.size;
1295
1296 /* Check if this is a directory file. */
1297 if (cache.isDirectory
1298 /* Files shouldn't be specials and directories at the same
1299 time, but Android doesn't forbid document providers
1300 from returning this information. */
1301 && (stat[0] & S_IFCHR) == 0)
1302 {
1303 /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories,
1304 just assume they're writable. */
1305 stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR;
1306
1307 /* Directory files cannot be modified if
1308 FLAG_DIR_SUPPORTS_CREATE is not set. */
1309
1310 if ((cache.flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
1311 stat[0] &= ~S_IWUSR;
1312 }
1313
1314 /* If this file is neither a character special nor a
1315 directory, indicate that it's a regular file. */
1316
1317 if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0)
1318 stat[0] |= S_IFREG;
1319
1320 stat[2] = cache.mtime;
1321 return stat;
1322 }
1323
1324 /* Return file status for the document designated by the given
1325 DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document
1326 ID in URI itself.
1327
1328 Value is null upon failure, or an array of longs [MODE, SIZE,
1329 MTIM] upon success, where MODE contains the file type and access
1330 modes of the file as in `struct stat', SIZE is the size of the
1331 file in BYTES or -1 if not known, and MTIM is the time of the
1332 last modification to this file in milliseconds since 00:00,
1333 January 1st, 1970.
1334
1335 OperationCanceledException and other typical exceptions may be
1336 signaled upon receiving async input or other errors. */
1337
1338 public long[]
1339 statDocument (final String uri, final String documentId)
1340 {
1341 return (long[]) runObjectFunction (new SafObjectFunction () {
1342 @Override
1343 public Object
1344 runObject (CancellationSignal signal)
1345 {
1346 return statDocument1 (uri, documentId, signal);
1347 }
1348 });
1349 }
1350
1351 /* The bulk of `accessDocument'. SIGNAL should be a cancellation
1352 signal. */
1353
1354 private int
1355 accessDocument1 (String uri, String documentId, boolean writable,
1356 CancellationSignal signal)
1357 {
1358 Uri uriObject;
1359 String[] projection;
1360 int tem, index;
1361 String tem1;
1362 Cursor cursor;
1363 CacheToplevel toplevel;
1364 CacheEntry entry;
1365
1366 uriObject = Uri.parse (uri);
1367
1368 if (documentId == null)
1369 documentId = DocumentsContract.getTreeDocumentId (uriObject);
1370
1371 /* If WRITABLE is false and the document ID is cached, use its
1372 cached value instead. This speeds up
1373 `directory-files-with-attributes' a little. */
1374
1375 if (!writable)
1376 {
1377 toplevel = getCache (uriObject);
1378 entry = toplevel.idCache.get (documentId);
1379
1380 if (entry != null)
1381 return 0;
1382 }
1383
1384 /* Create a document URI representing DOCUMENTID within URI's
1385 authority. */
1386
1387 uriObject
1388 = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
1389
1390 /* Now stat this document. */
1391
1392 projection = new String[] {
1393 Document.COLUMN_FLAGS,
1394 Document.COLUMN_MIME_TYPE,
1395 };
1396
1397 cursor = resolver.query (uriObject, projection, null,
1398 null, null, signal);
1399
1400 if (cursor == null)
1401 return -1;
1402
1403 try
1404 {
1405 if (!cursor.moveToFirst ())
1406 return -1;
1407
1408 if (!writable)
1409 return 0;
1410
1411 index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1412 if (index < 0)
1413 return -3;
1414
1415 /* Get the type of this file to check if it's a directory. */
1416 tem1 = cursor.getString (index);
1417
1418 /* Check if this is a directory file. */
1419 if (tem1.equals (Document.MIME_TYPE_DIR))
1420 {
1421 /* If so, don't check for FLAG_SUPPORTS_WRITE.
1422 Check for FLAG_DIR_SUPPORTS_CREATE instead. */
1423
1424 if (!writable)
1425 return 0;
1426
1427 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
1428 if (index < 0)
1429 return -3;
1430
1431 tem = cursor.getInt (index);
1432 if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
1433 return -3;
1434
1435 return 0;
1436 }
1437
1438 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
1439 if (index < 0)
1440 return -3;
1441
1442 tem = cursor.getInt (index);
1443 if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0)
1444 return -3;
1445 }
1446 finally
1447 {
1448 /* Close the cursor if an exception occurs. */
1449 cursor.close ();
1450 }
1451
1452 return 0;
1453 }
1454
1455 /* Find out whether Emacs has access to the document designated by
1456 the specified DOCUMENTID within the tree URI. If DOCUMENTID is
1457 NULL, use the document ID in URI itself.
1458
1459 If WRITABLE, also check that the file is writable, which is true
1460 if it is either a directory or its flags contains
1461 FLAG_SUPPORTS_WRITE.
1462
1463 Value is 0 if the file is accessible, and one of the following if
1464 not:
1465
1466 -1, if the file does not exist.
1467 -2, if WRITABLE and the file is not writable.
1468 -3, upon any other error.
1469
1470 In addition, arbitrary runtime exceptions (such as
1471 SecurityException or UnsupportedOperationException) may be
1472 thrown. */
1473
1474 public int
1475 accessDocument (final String uri, final String documentId,
1476 final boolean writable)
1477 {
1478 return runIntFunction (new SafIntFunction () {
1479 @Override
1480 public int
1481 runInt (CancellationSignal signal)
1482 {
1483 return accessDocument1 (uri, documentId, writable,
1484 signal);
1485 }
1486 });
1487 }
1488
1489 /* The crux of openDocumentDirectory. SIGNAL must be a cancellation
1490 signal. */
1491
1492 private Cursor
1493 openDocumentDirectory1 (String uri, String documentId,
1494 CancellationSignal signal)
1495 {
1496 Uri uriObject, tree;
1497 Cursor cursor;
1498 String projection[];
1499 CacheToplevel toplevel;
1500
1501 tree = uriObject = Uri.parse (uri);
1502
1503 /* If documentId is not set, use the document ID of the tree URI
1504 itself. */
1505
1506 if (documentId == null)
1507 documentId = DocumentsContract.getTreeDocumentId (uriObject);
1508
1509 /* Build a URI representing each directory entry within
1510 DOCUMENTID. */
1511
1512 uriObject
1513 = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject,
1514 documentId);
1515
1516 projection = new String [] {
1517 Document.COLUMN_DISPLAY_NAME,
1518 Document.COLUMN_DOCUMENT_ID,
1519 Document.COLUMN_MIME_TYPE,
1520 Document.COLUMN_FLAGS,
1521 Document.COLUMN_LAST_MODIFIED,
1522 Document.COLUMN_SIZE,
1523 };
1524
1525 cursor = resolver.query (uriObject, projection, null, null,
1526 null, signal);
1527
1528 /* Create a new cache entry tied to this document ID. */
1529
1530 if (cursor != null)
1531 {
1532 toplevel = getCache (tree);
1533 cacheDirectoryFromCursor (toplevel, documentId,
1534 cursor);
1535 }
1536
1537 /* Return the cursor. */
1538 return cursor;
1539 }
1540
1541 /* Open a cursor representing each entry within the directory
1542 designated by the specified DOCUMENTID within the tree URI.
1543
1544 If DOCUMENTID is NULL, use the document ID within URI itself.
1545 Value is NULL upon failure.
1546
1547 In addition, arbitrary runtime exceptions (such as
1548 SecurityException or UnsupportedOperationException) may be
1549 thrown. */
1550
1551 public Cursor
1552 openDocumentDirectory (final String uri, final String documentId)
1553 {
1554 return (Cursor) runObjectFunction (new SafObjectFunction () {
1555 @Override
1556 public Object
1557 runObject (CancellationSignal signal)
1558 {
1559 return openDocumentDirectory1 (uri, documentId, signal);
1560 }
1561 });
1562 }
1563
1564 /* The crux of `openDocument'. SIGNAL must be a cancellation
1565 signal. */
1566
1567 public ParcelFileDescriptor
1568 openDocument1 (String uri, String documentId, boolean write,
1569 boolean truncate, CancellationSignal signal)
1570 throws Throwable
1571 {
1572 Uri treeUri, documentUri;
1573 String mode;
1574 ParcelFileDescriptor fileDescriptor;
1575 CacheToplevel toplevel;
1576
1577 treeUri = Uri.parse (uri);
1578
1579 /* documentId must be set for this request, since it doesn't make
1580 sense to ``open'' the root of the directory tree. */
1581
1582 documentUri
1583 = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId);
1584
1585 /* Select the mode used to open the file. */
1586
1587 if (write)
1588 {
1589 if (truncate)
1590 mode = "rwt";
1591 else
1592 mode = "rw";
1593 }
1594 else
1595 mode = "r";
1596
1597 fileDescriptor
1598 = resolver.openFileDescriptor (documentUri, mode,
1599 signal);
1600
1601 /* If a writable file descriptor is requested and TRUNCATE is set,
1602 then probe the file descriptor to detect if it is actually
1603 readable. If not, close this file descriptor and reopen it
1604 with MODE set to rw; some document providers granting access to
1605 Samba shares don't implement rwt, but these document providers
1606 invariably truncate the file opened even when the mode is
1607 merely rw.
1608
1609 This may be ascribed to a mix-up in Android's documentation
1610 regardin DocumentsProvider: the `openDocument' function is only
1611 documented to accept r or rw, whereas the default
1612 implementation of the `openFile' function (which documents rwt)
1613 delegates to `openDocument'. */
1614
1615 if (write && truncate && fileDescriptor != null
1616 && !EmacsNative.ftruncate (fileDescriptor.getFd ()))
1617 {
1618 try
1619 {
1620 fileDescriptor.closeWithError ("File descriptor requested"
1621 + " is not writable");
1622 }
1623 catch (IOException e)
1624 {
1625 Log.w (TAG, "Leaking unclosed file descriptor " + e);
1626 }
1627
1628 fileDescriptor
1629 = resolver.openFileDescriptor (documentUri, "rw", signal);
1630
1631 /* Try to truncate fileDescriptor just to stay on the safe
1632 side. */
1633 if (fileDescriptor != null)
1634 EmacsNative.ftruncate (fileDescriptor.getFd ());
1635 }
1636
1637 /* Every time a document is opened, remove it from the file status
1638 cache. */
1639 toplevel = getCache (treeUri);
1640 toplevel.statCache.remove (documentId);
1641
1642 return fileDescriptor;
1643 }
1644
1645 /* Open a file descriptor for a file document designated by
1646 DOCUMENTID within the document tree identified by URI. If
1647 TRUNCATE and the document already exists, truncate its contents
1648 before returning.
1649
1650 On Android 9.0 and earlier, always open the document in
1651 ``read-write'' mode; this instructs the document provider to
1652 return a seekable file that is stored on disk and returns correct
1653 file status.
1654
1655 Under newer versions of Android, open the document in a
1656 non-writable mode if WRITE is false. This is possible because
1657 these versions allow Emacs to explicitly request a seekable
1658 on-disk file.
1659
1660 Value is NULL upon failure or a parcel file descriptor upon
1661 success. Call `ParcelFileDescriptor.close' on this file
1662 descriptor instead of using the `close' system call.
1663
1664 FileNotFoundException and/or SecurityException and/or
1665 UnsupportedOperationException and/or OperationCanceledException
1666 may be thrown upon failure. */
1667
1668 public ParcelFileDescriptor
1669 openDocument (final String uri, final String documentId,
1670 final boolean write, final boolean truncate)
1671 {
1672 Object tem;
1673
1674 tem = runObjectFunction (new SafObjectFunction () {
1675 @Override
1676 public Object
1677 runObject (CancellationSignal signal)
1678 throws Throwable
1679 {
1680 return openDocument1 (uri, documentId, write, truncate,
1681 signal);
1682 }
1683 });
1684
1685 return (ParcelFileDescriptor) tem;
1686 }
1687 };