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

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