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 If NO_CACHE, don't cache the file status; just return the
443 entry. */
444
445 private StatCacheEntry
446 cacheFileStatus (String documentId, CacheToplevel toplevel,
447 Cursor cursor, boolean no_cache)
448 {
449 StatCacheEntry entry;
450 int flagsIndex, columnIndex, typeIndex;
451 int sizeIndex, mtimeIndex;
452 String type;
453
454 /* Obtain the indices for columns wanted from this cursor. */
455 flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS);
456 sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE);
457 typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
458 mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED);
459
460 /* COLUMN_LAST_MODIFIED is allowed to be absent in a
461 conforming documents provider. */
462 if (flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0)
463 return null;
464
465 /* Get the file status from CURSOR. */
466 entry = new StatCacheEntry ();
467 entry.flags = cursor.getInt (flagsIndex);
468 type = cursor.getString (typeIndex);
469
470 if (type == null)
471 return null;
472
473 entry.isDirectory = type.equals (Document.MIME_TYPE_DIR);
474
475 if (cursor.isNull (sizeIndex))
476 /* The size is unknown. */
477 entry.size = -1;
478 else
479 entry.size = cursor.getLong (sizeIndex);
480
481 /* mtimeIndex is potentially unset, since document providers
482 aren't obligated to provide modification times. */
483
484 if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex))
485 entry.mtime = cursor.getLong (mtimeIndex);
486
487 /* Finally, add this entry to the cache and return. */
488 if (!no_cache)
489 toplevel.statCache.put (documentId, entry);
490 return entry;
491 }
492
493 /* Cache the type and as many of the children of the directory
494 designated by DOCUMENTID as possible into TOPLEVEL.
495
496 CURSOR should be a cursor representing an open directory stream,
497 with its projection consisting of at least the display name,
498 document ID and MIME type columns.
499
500 Rewind the position of CURSOR to before its first element after
501 completion. */
502
503 private void
504 cacheDirectoryFromCursor (CacheToplevel toplevel, String documentId,
505 Cursor cursor)
506 {
507 CacheEntry entry, constitutent;
508 int nameColumn, idColumn, typeColumn;
509 String id, name, type;
510 DocIdEntry idEntry;
511
512 /* Find the numbers of the columns wanted. */
513
514 nameColumn
515 = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
516 idColumn
517 = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
518 typeColumn
519 = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
520
521 if (nameColumn < 0 || idColumn < 0 || typeColumn < 0)
522 return;
523
524 entry = new CacheEntry ();
525
526 /* We know this is a directory already. */
527 entry.type = Document.MIME_TYPE_DIR;
528 toplevel.idCache.put (documentId, entry);
529
530 /* Now, try to cache each of its constituents. */
531
532 while (cursor.moveToNext ())
533 {
534 try
535 {
536 name = cursor.getString (nameColumn);
537 id = cursor.getString (idColumn);
538 type = cursor.getString (typeColumn);
539
540 if (name == null || id == null || type == null)
541 continue;
542
543 /* First, add the name and ID to ENTRY's map of
544 children. */
545 idEntry = new DocIdEntry ();
546 idEntry.documentId = id;
547 entry.children.put (id, idEntry);
548
549 /* Cache the file status for ID within TOPELVEL too; if a
550 directory listing is being requested, it's very likely
551 that a series of calls for file status will follow. */
552
553 cacheFileStatus (id, toplevel, cursor, false);
554
555 /* If this constituent is a directory, don't cache any
556 information about it. It cannot be cached without
557 knowing its children. */
558
559 if (type.equals (Document.MIME_TYPE_DIR))
560 continue;
561
562 /* Otherwise, create a new cache entry comprised of its
563 type. */
564 constitutent = new CacheEntry ();
565 constitutent.type = type;
566 toplevel.idCache.put (documentId, entry);
567 }
568 catch (Exception e)
569 {
570 e.printStackTrace ();
571 continue;
572 }
573 }
574
575 /* Rewind cursor back to the beginning. */
576 cursor.moveToPosition (-1);
577 }
578
579 /* Post a message to run `pruneCache' every CACHE_PRUNE_TIME
580 seconds. */
581
582 private void
583 postPruneMessage ()
584 {
585 handler.postDelayed (new Runnable () {
586 @Override
587 public void
588 run ()
589 {
590 pruneCache ();
591 }
592 }, CACHE_PRUNE_TIME * 1000);
593 }
594
595 /* Invalidate the cache entry denoted by DOCUMENT_ID, within the
596 document tree URI.
597 Call this after deleting a document or directory.
598
599 At the same time, remove the final component within the file name
600 CACHENAME from the cache if it exists. */
601
602 public void
603 postInvalidateCache (final Uri uri, final String documentId,
604 final String cacheName)
605 {
606 handler.post (new Runnable () {
607 @Override
608 public void
609 run ()
610 {
611 CacheToplevel toplevel;
612 HashMap<String, DocIdEntry> children;
613 String[] components;
614 CacheEntry entry;
615 DocIdEntry idEntry;
616
617 toplevel = getCache (uri);
618 toplevel.idCache.remove (documentId);
619 toplevel.statCache.remove (documentId);
620
621 /* If the parent of CACHENAME is cached, remove it. */
622
623 children = toplevel.children;
624 components = cacheName.split ("/");
625
626 for (String component : components)
627 {
628 /* Java `split' removes trailing empty matches but not
629 leading or intermediary ones. */
630 if (component.isEmpty ())
631 continue;
632
633 if (component == components[components.length - 1])
634 {
635 /* This is the last component, so remove it from
636 children. */
637 children.remove (component);
638 return;
639 }
640 else
641 {
642 /* Search for this component within the last level
643 of the cache. */
644
645 idEntry = children.get (component);
646
647 if (idEntry == null)
648 /* Not cached, so return. */
649 return;
650
651 entry = toplevel.idCache.get (idEntry.documentId);
652
653 if (entry == null)
654 /* Not cached, so return. */
655 return;
656
657 /* Locate the next component within this
658 directory. */
659 children = entry.children;
660 }
661 }
662 }
663 });
664 }
665
666 /* Invalidate the cache entry denoted by DOCUMENT_ID, within the
667 document tree URI.
668 Call this after deleting a document or directory.
669
670 At the same time, remove the child referring to DOCUMENTID from
671 within CACHENAME's cache entry if it exists. */
672
673 public void
674 postInvalidateCacheDir (final Uri uri, final String documentId,
675 final String cacheName)
676 {
677 handler.post (new Runnable () {
678 @Override
679 public void
680 run ()
681 {
682 CacheToplevel toplevel;
683 HashMap<String, DocIdEntry> children;
684 String[] components;
685 CacheEntry entry;
686 DocIdEntry idEntry;
687 Iterator<DocIdEntry> iter;
688
689 toplevel = getCache (uri);
690 toplevel.idCache.remove (documentId);
691 toplevel.statCache.remove (documentId);
692
693 /* Now remove DOCUMENTID from CACHENAME's cache entry, if
694 any. */
695
696 children = toplevel.children;
697 components = cacheName.split ("/");
698
699 for (String component : components)
700 {
701 /* Java `split' removes trailing empty matches but not
702 leading or intermediary ones. */
703 if (component.isEmpty ())
704 continue;
705
706 /* Search for this component within the last level
707 of the cache. */
708
709 idEntry = children.get (component);
710
711 if (idEntry == null)
712 /* Not cached, so return. */
713 return;
714
715 entry = toplevel.idCache.get (idEntry.documentId);
716
717 if (entry == null)
718 /* Not cached, so return. */
719 return;
720
721 /* Locate the next component within this
722 directory. */
723 children = entry.children;
724 }
725
726 iter = children.values ().iterator ();
727 while (iter.hasNext ())
728 {
729 idEntry = iter.next ();
730
731 if (idEntry.documentId.equals (documentId))
732 {
733 iter.remove ();
734 break;
735 }
736 }
737 }
738 });
739 }
740
741 /* Invalidate the file status cache entry for DOCUMENTID within URI.
742 Call this when the contents of a file (i.e. the constituents of a
743 directory file) may have changed, but the document's display name
744 has not. */
745
746 public void
747 postInvalidateStat (final Uri uri, final String documentId)
748 {
749 handler.post (new Runnable () {
750 @Override
751 public void
752 run ()
753 {
754 CacheToplevel toplevel;
755
756 toplevel = getCache (uri);
757 toplevel.statCache.remove (documentId);
758 }
759 });
760 }
761
762
763
764 /* ``Prototypes'' for nested functions that are run within the SAF
765 thread and accepts a cancellation signal. They differ in their
766 return types. */
767
768 private abstract class SafIntFunction
769 {
770 /* The ``throws Throwable'' here is a Java idiosyncracy that tells
771 the compiler to allow arbitrary error objects to be signaled
772 from within this function.
773
774 Later, runIntFunction will try to re-throw any error object
775 generated by this function in the Emacs thread, using a trick
776 to avoid the compiler requirement to expressly declare that an
777 error (and which types of errors) will be signaled. */
778
779 public abstract int runInt (CancellationSignal signal)
780 throws Throwable;
781 };
782
783 private abstract class SafObjectFunction
784 {
785 /* The ``throws Throwable'' here is a Java idiosyncracy that tells
786 the compiler to allow arbitrary error objects to be signaled
787 from within this function.
788
789 Later, runObjectFunction will try to re-throw any error object
790 generated by this function in the Emacs thread, using a trick
791 to avoid the compiler requirement to expressly declare that an
792 error (and which types of errors) will be signaled. */
793
794 public abstract Object runObject (CancellationSignal signal)
795 throws Throwable;
796 };
797
798
799
800 /* Functions that run cancel-able queries. These functions are
801 internally run within the SAF thread. */
802
803 /* Throw the specified EXCEPTION. The type template T is erased by
804 the compiler before the object is compiled, so the compiled code
805 simply throws EXCEPTION without the cast being verified.
806
807 T should be RuntimeException to obtain the desired effect of
808 throwing an exception without a compiler check. */
809
810 @SuppressWarnings("unchecked")
811 private static <T extends Throwable> void
812 throwException (Throwable exception)
813 throws T
814 {
815 throw (T) exception;
816 }
817
818 /* Run the given function (or rather, its `runInt' field) within the
819 SAF thread, waiting for it to complete.
820
821 If async input arrives in the meantime and sets Vquit_flag,
822 signal the cancellation signal supplied to that function.
823
824 Rethrow any exception thrown from that function, and return its
825 value otherwise. */
826
827 private int
828 runIntFunction (final SafIntFunction function)
829 {
830 final EmacsHolder<Object> result;
831 final CancellationSignal signal;
832 Throwable throwable;
833
834 result = new EmacsHolder<Object> ();
835 signal = new CancellationSignal ();
836
837 handler.post (new Runnable () {
838 @Override
839 public void
840 run ()
841 {
842 try
843 {
844 result.thing
845 = Integer.valueOf (function.runInt (signal));
846 }
847 catch (Throwable throwable)
848 {
849 result.thing = throwable;
850 }
851
852 EmacsNative.safPostRequest ();
853 }
854 });
855
856 if (EmacsNative.safSyncAndReadInput () != 0)
857 {
858 signal.cancel ();
859
860 /* Now wait for the function to finish. Either the signal has
861 arrived after the query took place, in which case it will
862 finish normally, or an OperationCanceledException will be
863 thrown. */
864
865 EmacsNative.safSync ();
866 }
867
868 if (result.thing instanceof Throwable)
869 {
870 throwable = (Throwable) result.thing;
871 EmacsSafThread.<RuntimeException>throwException (throwable);
872 }
873
874 return (Integer) result.thing;
875 }
876
877 /* Run the given function (or rather, its `runObject' field) within
878 the SAF thread, waiting for it to complete.
879
880 If async input arrives in the meantime and sets Vquit_flag,
881 signal the cancellation signal supplied to that function.
882
883 Rethrow any exception thrown from that function, and return its
884 value otherwise. */
885
886 private Object
887 runObjectFunction (final SafObjectFunction function)
888 {
889 final EmacsHolder<Object> result;
890 final CancellationSignal signal;
891 Throwable throwable;
892
893 result = new EmacsHolder<Object> ();
894 signal = new CancellationSignal ();
895
896 handler.post (new Runnable () {
897 @Override
898 public void
899 run ()
900 {
901 try
902 {
903 result.thing = function.runObject (signal);
904 }
905 catch (Throwable throwable)
906 {
907 result.thing = throwable;
908 }
909
910 EmacsNative.safPostRequest ();
911 }
912 });
913
914 if (EmacsNative.safSyncAndReadInput () != 0)
915 {
916 signal.cancel ();
917
918 /* Now wait for the function to finish. Either the signal has
919 arrived after the query took place, in which case it will
920 finish normally, or an OperationCanceledException will be
921 thrown. */
922
923 EmacsNative.safSync ();
924 }
925
926 if (result.thing instanceof Throwable)
927 {
928 throwable = (Throwable) result.thing;
929 EmacsSafThread.<RuntimeException>throwException (throwable);
930 }
931
932 return result.thing;
933 }
934
935 /* The crux of `documentIdFromName1', run within the SAF thread.
936 SIGNAL should be a cancellation signal run upon quitting. */
937
938 private int
939 documentIdFromName1 (String tree_uri, String name,
940 String[] id_return, CancellationSignal signal)
941 {
942 Uri uri, treeUri;
943 String id, type, newId, newType;
944 String[] components, projection;
945 Cursor cursor;
946 int nameColumn, idColumn, typeColumn;
947 CacheToplevel toplevel;
948 DocIdEntry idEntry;
949 HashMap<String, DocIdEntry> children, next;
950 CacheEntry cache;
951
952 projection = new String[] {
953 Document.COLUMN_DISPLAY_NAME,
954 Document.COLUMN_DOCUMENT_ID,
955 Document.COLUMN_MIME_TYPE,
956 };
957
958 /* Parse the URI identifying the tree first. */
959 uri = Uri.parse (tree_uri);
960
961 /* Now, split NAME into its individual components. */
962 components = name.split ("/");
963
964 /* Set id and type to the value at the root of the tree. */
965 type = id = null;
966 cursor = null;
967
968 /* Obtain the top level of this cache. */
969 toplevel = getCache (uri);
970
971 /* Set the current map of children to this top level. */
972 children = toplevel.children;
973
974 /* For each component... */
975
976 try
977 {
978 for (String component : components)
979 {
980 /* Java split doesn't behave very much like strtok when it
981 comes to trailing and leading delimiters... */
982 if (component.isEmpty ())
983 continue;
984
985 /* Search for component within the currently cached list
986 of children. */
987
988 idEntry = children.get (component);
989
990 if (idEntry != null)
991 {
992 /* The document ID is known. Now find the
993 corresponding document ID cache. */
994
995 cache = toplevel.idCache.get (idEntry.documentId);
996
997 /* Fetch just the information for this document. */
998
999 if (cache == null)
1000 cache = idEntry.getCacheEntry (resolver, uri, toplevel,
1001 signal);
1002
1003 if (cache == null)
1004 {
1005 /* File status matching idEntry could not be
1006 obtained. Treat this as if the file does not
1007 exist. */
1008
1009 children.remove (component);
1010
1011 if (id == null)
1012 id = DocumentsContract.getTreeDocumentId (uri);
1013
1014 id_return[0] = id;
1015
1016 if ((type == null
1017 || type.equals (Document.MIME_TYPE_DIR))
1018 /* ... and type and id currently represent the
1019 penultimate component. */
1020 && component == components[components.length - 1])
1021 return -2;
1022
1023 return -1;
1024 }
1025
1026 /* Otherwise, use the cached information. */
1027 id = idEntry.documentId;
1028 type = cache.type;
1029 children = cache.children;
1030 continue;
1031 }
1032
1033 /* Create the tree URI for URI from ID if it exists, or
1034 the root otherwise. */
1035
1036 if (id == null)
1037 id = DocumentsContract.getTreeDocumentId (uri);
1038
1039 treeUri
1040 = DocumentsContract.buildChildDocumentsUriUsingTree (uri, id);
1041
1042 /* Look for a file in this directory by the name of
1043 component. */
1044
1045 cursor = resolver.query (treeUri, projection,
1046 (Document.COLUMN_DISPLAY_NAME
1047 + " = ?"),
1048 new String[] { component, },
1049 null, signal);
1050
1051 if (cursor == null)
1052 return -1;
1053
1054 /* Find the column numbers for each of the columns that
1055 are wanted. */
1056
1057 nameColumn
1058 = cursor.getColumnIndex (Document.COLUMN_DISPLAY_NAME);
1059 idColumn
1060 = cursor.getColumnIndex (Document.COLUMN_DOCUMENT_ID);
1061 typeColumn
1062 = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1063
1064 if (nameColumn < 0 || idColumn < 0 || typeColumn < 0)
1065 return -1;
1066
1067 next = null;
1068
1069 while (true)
1070 {
1071 /* Even though the query selects for a specific
1072 display name, some content providers nevertheless
1073 return every file within the directory. */
1074
1075 if (!cursor.moveToNext ())
1076 {
1077 /* If a component has been found, break out of the
1078 loop. */
1079
1080 if (next != null)
1081 break;
1082
1083 /* If the last component considered is a
1084 directory... */
1085 if ((type == null
1086 || type.equals (Document.MIME_TYPE_DIR))
1087 /* ... and type and id currently represent the
1088 penultimate component. */
1089 && component == components[components.length - 1])
1090 {
1091 /* The cursor is empty. In this case, return
1092 -2 and the current document ID (belonging
1093 to the previous component) in
1094 ID_RETURN. */
1095
1096 id_return[0] = id;
1097
1098 /* But return -1 on the off chance that id is
1099 null. */
1100
1101 if (id == null)
1102 return -1;
1103
1104 return -2;
1105 }
1106
1107 /* The last component found is not a directory, so
1108 return -1. */
1109 return -1;
1110 }
1111
1112 /* So move CURSOR to a row with the right display
1113 name. */
1114
1115 name = cursor.getString (nameColumn);
1116 newId = cursor.getString (idColumn);
1117 newType = cursor.getString (typeColumn);
1118
1119 /* Any of the three variables above may be NULL if the
1120 column data is of the wrong type depending on how
1121 the Cursor returned is implemented. */
1122
1123 if (name == null || newId == null || newType == null)
1124 return -1;
1125
1126 /* Cache this name, even if it isn't the document
1127 that's being searched for. */
1128
1129 cache = cacheChild (toplevel, children, name,
1130 newId, newType,
1131 idEntry != null);
1132
1133 /* Record the desired component once it is located,
1134 but continue reading and caching items from the
1135 cursor. */
1136
1137 if (name.equals (component))
1138 {
1139 id = newId;
1140 next = cache.children;
1141 type = newType;
1142 }
1143 }
1144
1145 children = next;
1146
1147 /* Now close the cursor. */
1148 cursor.close ();
1149 cursor = null;
1150
1151 /* ID may have become NULL if the data is in an invalid
1152 format. */
1153 if (id == null)
1154 return -1;
1155 }
1156 }
1157 finally
1158 {
1159 /* If an error is thrown within the block above, let
1160 android_saf_exception_check handle it, but make sure the
1161 cursor is closed. */
1162
1163 if (cursor != null)
1164 cursor.close ();
1165 }
1166
1167 /* Here, id is either NULL (meaning the same as TREE_URI), and
1168 type is either NULL (in which case id should also be NULL) or
1169 the MIME type of the file. */
1170
1171 /* First return the ID. */
1172
1173 if (id == null)
1174 id_return[0] = DocumentsContract.getTreeDocumentId (uri);
1175 else
1176 id_return[0] = id;
1177
1178 /* Next, return whether or not this is a directory. */
1179 if (type == null || type.equals (Document.MIME_TYPE_DIR))
1180 return 1;
1181
1182 return 0;
1183 }
1184
1185 /* Find the document ID of the file within TREE_URI designated by
1186 NAME.
1187
1188 NAME is a ``file name'' comprised of the display names of
1189 individual files. Each constituent component prior to the last
1190 must name a directory file within TREE_URI.
1191
1192 Upon success, return 0 or 1 (contingent upon whether or not the
1193 last component within NAME is a directory) and place the document
1194 ID of the named file in ID_RETURN[0].
1195
1196 If the designated file can't be located, but each component of
1197 NAME up to the last component can and is a directory, return -2
1198 and the ID of the last component located in ID_RETURN[0].
1199
1200 If the designated file can't be located, return -1, or signal one
1201 of OperationCanceledException, SecurityException,
1202 FileNotFoundException, or UnsupportedOperationException. */
1203
1204 public int
1205 documentIdFromName (final String tree_uri, final String name,
1206 final String[] id_return)
1207 {
1208 return runIntFunction (new SafIntFunction () {
1209 @Override
1210 public int
1211 runInt (CancellationSignal signal)
1212 {
1213 return documentIdFromName1 (tree_uri, name, id_return,
1214 signal);
1215 }
1216 });
1217 }
1218
1219 /* The bulk of `statDocument'. SIGNAL should be a cancelation
1220 signal. */
1221
1222 private long[]
1223 statDocument1 (String uri, String documentId,
1224 CancellationSignal signal, boolean noCache)
1225 {
1226 Uri uriObject, tree;
1227 String[] projection;
1228 long[] stat;
1229 Cursor cursor;
1230 CacheToplevel toplevel;
1231 StatCacheEntry cache;
1232
1233 tree = Uri.parse (uri);
1234
1235 if (documentId == null)
1236 documentId = DocumentsContract.getTreeDocumentId (tree);
1237
1238 /* Create a document URI representing DOCUMENTID within URI's
1239 authority. */
1240
1241 uriObject
1242 = DocumentsContract.buildDocumentUriUsingTree (tree, documentId);
1243
1244 /* See if the file status cache currently contains this
1245 document. */
1246
1247 toplevel = getCache (tree);
1248 cache = toplevel.statCache.get (documentId);
1249
1250 if (cache == null || !cache.isValid ())
1251 {
1252 /* Stat this document and enter its information into the
1253 cache. */
1254
1255 projection = new String[] {
1256 Document.COLUMN_FLAGS,
1257 Document.COLUMN_LAST_MODIFIED,
1258 Document.COLUMN_MIME_TYPE,
1259 Document.COLUMN_SIZE,
1260 };
1261
1262 cursor = resolver.query (uriObject, projection, null,
1263 null, null, signal);
1264
1265 if (cursor == null)
1266 return null;
1267
1268 try
1269 {
1270 if (!cursor.moveToFirst ())
1271 return null;
1272
1273 cache = cacheFileStatus (documentId, toplevel, cursor,
1274 noCache);
1275 }
1276 finally
1277 {
1278 cursor.close ();
1279 }
1280
1281 /* If cache is still null, return null. */
1282
1283 if (cache == null)
1284 return null;
1285 }
1286
1287 /* Create the array of file status and populate it with the
1288 information within cache. */
1289 stat = new long[3];
1290
1291 stat[0] |= S_IRUSR;
1292 if ((cache.flags & Document.FLAG_SUPPORTS_WRITE) != 0)
1293 stat[0] |= S_IWUSR;
1294
1295 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
1296 && (cache.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
1297 stat[0] |= S_IFCHR;
1298
1299 stat[1] = cache.size;
1300
1301 /* Check if this is a directory file. */
1302 if (cache.isDirectory
1303 /* Files shouldn't be specials and directories at the same
1304 time, but Android doesn't forbid document providers
1305 from returning this information. */
1306 && (stat[0] & S_IFCHR) == 0)
1307 {
1308 /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories,
1309 just assume they're writable. */
1310 stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR;
1311
1312 /* Directory files cannot be modified if
1313 FLAG_DIR_SUPPORTS_CREATE is not set. */
1314
1315 if ((cache.flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
1316 stat[0] &= ~S_IWUSR;
1317 }
1318
1319 /* If this file is neither a character special nor a
1320 directory, indicate that it's a regular file. */
1321
1322 if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0)
1323 stat[0] |= S_IFREG;
1324
1325 stat[2] = cache.mtime;
1326 return stat;
1327 }
1328
1329 /* Return file status for the document designated by the given
1330 DOCUMENTID and tree URI. If DOCUMENTID is NULL, use the document
1331 ID in URI itself.
1332
1333 Value is null upon failure, or an array of longs [MODE, SIZE,
1334 MTIM] upon success, where MODE contains the file type and access
1335 modes of the file as in `struct stat', SIZE is the size of the
1336 file in BYTES or -1 if not known, and MTIM is the time of the
1337 last modification to this file in milliseconds since 00:00,
1338 January 1st, 1970.
1339
1340 If NOCACHE, refrain from placing the file status within the
1341 status cache.
1342
1343 OperationCanceledException and other typical exceptions may be
1344 signaled upon receiving async input or other errors. */
1345
1346 public long[]
1347 statDocument (final String uri, final String documentId,
1348 final boolean noCache)
1349 {
1350 return (long[]) runObjectFunction (new SafObjectFunction () {
1351 @Override
1352 public Object
1353 runObject (CancellationSignal signal)
1354 {
1355 return statDocument1 (uri, documentId, signal, noCache);
1356 }
1357 });
1358 }
1359
1360 /* The bulk of `accessDocument'. SIGNAL should be a cancellation
1361 signal. */
1362
1363 private int
1364 accessDocument1 (String uri, String documentId, boolean writable,
1365 CancellationSignal signal)
1366 {
1367 Uri uriObject;
1368 String[] projection;
1369 int tem, index;
1370 String tem1;
1371 Cursor cursor;
1372 CacheToplevel toplevel;
1373 CacheEntry entry;
1374
1375 uriObject = Uri.parse (uri);
1376
1377 if (documentId == null)
1378 documentId = DocumentsContract.getTreeDocumentId (uriObject);
1379
1380 /* If WRITABLE is false and the document ID is cached, use its
1381 cached value instead. This speeds up
1382 `directory-files-with-attributes' a little. */
1383
1384 if (!writable)
1385 {
1386 toplevel = getCache (uriObject);
1387 entry = toplevel.idCache.get (documentId);
1388
1389 if (entry != null)
1390 return 0;
1391 }
1392
1393 /* Create a document URI representing DOCUMENTID within URI's
1394 authority. */
1395
1396 uriObject
1397 = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
1398
1399 /* Now stat this document. */
1400
1401 projection = new String[] {
1402 Document.COLUMN_FLAGS,
1403 Document.COLUMN_MIME_TYPE,
1404 };
1405
1406 cursor = resolver.query (uriObject, projection, null,
1407 null, null, signal);
1408
1409 if (cursor == null)
1410 return -1;
1411
1412 try
1413 {
1414 if (!cursor.moveToFirst ())
1415 return -1;
1416
1417 if (!writable)
1418 return 0;
1419
1420 index = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
1421 if (index < 0)
1422 return -3;
1423
1424 /* Get the type of this file to check if it's a directory. */
1425 tem1 = cursor.getString (index);
1426
1427 /* Check if this is a directory file. */
1428 if (tem1.equals (Document.MIME_TYPE_DIR))
1429 {
1430 /* If so, don't check for FLAG_SUPPORTS_WRITE.
1431 Check for FLAG_DIR_SUPPORTS_CREATE instead. */
1432
1433 if (!writable)
1434 return 0;
1435
1436 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
1437 if (index < 0)
1438 return -3;
1439
1440 tem = cursor.getInt (index);
1441 if ((tem & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
1442 return -3;
1443
1444 return 0;
1445 }
1446
1447 index = cursor.getColumnIndex (Document.COLUMN_FLAGS);
1448 if (index < 0)
1449 return -3;
1450
1451 tem = cursor.getInt (index);
1452 if (writable && (tem & Document.FLAG_SUPPORTS_WRITE) == 0)
1453 return -3;
1454 }
1455 finally
1456 {
1457 /* Close the cursor if an exception occurs. */
1458 cursor.close ();
1459 }
1460
1461 return 0;
1462 }
1463
1464 /* Find out whether Emacs has access to the document designated by
1465 the specified DOCUMENTID within the tree URI. If DOCUMENTID is
1466 NULL, use the document ID in URI itself.
1467
1468 If WRITABLE, also check that the file is writable, which is true
1469 if it is either a directory or its flags contains
1470 FLAG_SUPPORTS_WRITE.
1471
1472 Value is 0 if the file is accessible, and one of the following if
1473 not:
1474
1475 -1, if the file does not exist.
1476 -2, if WRITABLE and the file is not writable.
1477 -3, upon any other error.
1478
1479 In addition, arbitrary runtime exceptions (such as
1480 SecurityException or UnsupportedOperationException) may be
1481 thrown. */
1482
1483 public int
1484 accessDocument (final String uri, final String documentId,
1485 final boolean writable)
1486 {
1487 return runIntFunction (new SafIntFunction () {
1488 @Override
1489 public int
1490 runInt (CancellationSignal signal)
1491 {
1492 return accessDocument1 (uri, documentId, writable,
1493 signal);
1494 }
1495 });
1496 }
1497
1498 /* The crux of openDocumentDirectory. SIGNAL must be a cancellation
1499 signal. */
1500
1501 private Cursor
1502 openDocumentDirectory1 (String uri, String documentId,
1503 CancellationSignal signal)
1504 {
1505 Uri uriObject, tree;
1506 Cursor cursor;
1507 String projection[];
1508 CacheToplevel toplevel;
1509
1510 tree = uriObject = Uri.parse (uri);
1511
1512 /* If documentId is not set, use the document ID of the tree URI
1513 itself. */
1514
1515 if (documentId == null)
1516 documentId = DocumentsContract.getTreeDocumentId (uriObject);
1517
1518 /* Build a URI representing each directory entry within
1519 DOCUMENTID. */
1520
1521 uriObject
1522 = DocumentsContract.buildChildDocumentsUriUsingTree (uriObject,
1523 documentId);
1524
1525 projection = new String [] {
1526 Document.COLUMN_DISPLAY_NAME,
1527 Document.COLUMN_DOCUMENT_ID,
1528 Document.COLUMN_MIME_TYPE,
1529 Document.COLUMN_FLAGS,
1530 Document.COLUMN_LAST_MODIFIED,
1531 Document.COLUMN_SIZE,
1532 };
1533
1534 cursor = resolver.query (uriObject, projection, null, null,
1535 null, signal);
1536
1537 /* Create a new cache entry tied to this document ID. */
1538
1539 if (cursor != null)
1540 {
1541 toplevel = getCache (tree);
1542 cacheDirectoryFromCursor (toplevel, documentId,
1543 cursor);
1544 }
1545
1546 /* Return the cursor. */
1547 return cursor;
1548 }
1549
1550 /* Open a cursor representing each entry within the directory
1551 designated by the specified DOCUMENTID within the tree URI.
1552
1553 If DOCUMENTID is NULL, use the document ID within URI itself.
1554 Value is NULL upon failure.
1555
1556 In addition, arbitrary runtime exceptions (such as
1557 SecurityException or UnsupportedOperationException) may be
1558 thrown. */
1559
1560 public Cursor
1561 openDocumentDirectory (final String uri, final String documentId)
1562 {
1563 return (Cursor) runObjectFunction (new SafObjectFunction () {
1564 @Override
1565 public Object
1566 runObject (CancellationSignal signal)
1567 {
1568 return openDocumentDirectory1 (uri, documentId, signal);
1569 }
1570 });
1571 }
1572
1573 /* The crux of `openDocument'. SIGNAL must be a cancellation
1574 signal. */
1575
1576 public ParcelFileDescriptor
1577 openDocument1 (String uri, String documentId, boolean read,
1578 boolean write, boolean truncate,
1579 CancellationSignal signal)
1580 throws Throwable
1581 {
1582 Uri treeUri, documentUri;
1583 String mode;
1584 ParcelFileDescriptor fileDescriptor;
1585 CacheToplevel toplevel;
1586
1587 treeUri = Uri.parse (uri);
1588
1589 /* documentId must be set for this request, since it doesn't make
1590 sense to ``open'' the root of the directory tree. */
1591
1592 documentUri
1593 = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId);
1594
1595 /* Select the mode used to open the file. */
1596
1597 if (write)
1598 {
1599 if (read)
1600 {
1601 if (truncate)
1602 mode = "rwt";
1603 else
1604 mode = "rw";
1605 }
1606 else
1607 /* Set mode to w when WRITE && !READ, disregarding TRUNCATE.
1608 In contradiction with the ContentResolver documentation,
1609 document providers seem to truncate files whenever w is
1610 specified, at least superficially. (But see below.) */
1611 mode = "w";
1612 }
1613 else
1614 mode = "r";
1615
1616 fileDescriptor
1617 = resolver.openFileDescriptor (documentUri, mode,
1618 signal);
1619
1620 /* If a writable on-disk file descriptor is requested and TRUNCATE
1621 is set, then probe the file descriptor to detect if it is
1622 actually readable. If not, close this file descriptor and
1623 reopen it with MODE set to rw; some document providers granting
1624 access to Samba shares don't implement rwt, but these document
1625 providers invariably truncate the file opened even when the
1626 mode is merely w.
1627
1628 This may be ascribed to a mix-up in Android's documentation
1629 regardin DocumentsProvider: the `openDocument' function is only
1630 documented to accept r or rw, whereas the default
1631 implementation of the `openFile' function (which documents rwt)
1632 delegates to `openDocument'. */
1633
1634 if (read && write && truncate && fileDescriptor != null
1635 && !EmacsNative.ftruncate (fileDescriptor.getFd ()))
1636 {
1637 try
1638 {
1639 fileDescriptor.closeWithError ("File descriptor requested"
1640 + " is not writable");
1641 }
1642 catch (IOException e)
1643 {
1644 Log.w (TAG, "Leaking unclosed file descriptor " + e);
1645 }
1646
1647 fileDescriptor
1648 = resolver.openFileDescriptor (documentUri, "rw", signal);
1649
1650 /* Try to truncate fileDescriptor just to stay on the safe
1651 side. */
1652 if (fileDescriptor != null)
1653 EmacsNative.ftruncate (fileDescriptor.getFd ());
1654 }
1655 else if (!read && write && truncate && fileDescriptor != null)
1656 /* Moreover, document providers that return actual seekable
1657 files characteristically neglect to truncate the file
1658 returned when the access mode is merely w, so attempt to
1659 truncate it by hand. */
1660 EmacsNative.ftruncate (fileDescriptor.getFd ());
1661
1662 /* Every time a document is opened, remove it from the file status
1663 cache. */
1664 toplevel = getCache (treeUri);
1665 toplevel.statCache.remove (documentId);
1666
1667 return fileDescriptor;
1668 }
1669
1670 /* Open a file descriptor for a file document designated by
1671 DOCUMENTID within the document tree identified by URI. If
1672 TRUNCATE and the document already exists, truncate its contents
1673 before returning.
1674
1675 If READ && WRITE, open the file under either the `rw' or `rwt'
1676 access mode, which implies that the value must be a seekable
1677 on-disk file. If WRITE && !READ or TRUNC && WRITE, also truncate
1678 the file after it is opened.
1679
1680 If only READ or WRITE is set, value may be a non-seekable FIFO or
1681 one end of a socket pair.
1682
1683 Value is NULL upon failure or a parcel file descriptor upon
1684 success. Call `ParcelFileDescriptor.close' on this file
1685 descriptor instead of using the `close' system call.
1686
1687 FileNotFoundException and/or SecurityException and/or
1688 UnsupportedOperationException and/or OperationCanceledException
1689 may be thrown upon failure. */
1690
1691 public ParcelFileDescriptor
1692 openDocument (final String uri, final String documentId,
1693 final boolean read, final boolean write,
1694 final boolean truncate)
1695 {
1696 Object tem;
1697
1698 tem = runObjectFunction (new SafObjectFunction () {
1699 @Override
1700 public Object
1701 runObject (CancellationSignal signal)
1702 throws Throwable
1703 {
1704 return openDocument1 (uri, documentId, read,
1705 write, truncate, signal);
1706 }
1707 });
1708
1709 return (ParcelFileDescriptor) tem;
1710 }
1711 };