root/java/org/gnu/emacs/EmacsSafThread.java

/* [<][>][^][v][top][bottom][index][help] */

DEFINITIONS

This source file includes following definitions.
  1. start
  2. isValid
  3. getCacheEntry
  4. isValid
  5. isValid
  6. getCache
  7. pruneCache1
  8. pruneCache
  9. cacheChild
  10. cacheFileStatus
  11. cacheDirectoryFromCursor
  12. postPruneMessage
  13. postInvalidateCache
  14. postInvalidateCacheDir
  15. postInvalidateStat
  16. runInt
  17. runObject
  18. throwException
  19. runIntFunction
  20. runObjectFunction
  21. documentIdFromName1
  22. documentIdFromName
  23. statDocument1
  24. statDocument
  25. accessDocument1
  26. accessDocument
  27. openDocumentDirectory1
  28. openDocumentDirectory
  29. openDocument1
  30. openDocument

     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 };

/* [<][>][^][v][top][bottom][index][help] */