]> git.eshelyaron.com Git - emacs.git/commitdiff
Update Android port
authorPo Lu <luangruo@yahoo.com>
Thu, 3 Aug 2023 02:41:40 +0000 (10:41 +0800)
committerPo Lu <luangruo@yahoo.com>
Thu, 3 Aug 2023 02:41:40 +0000 (10:41 +0800)
* java/org/gnu/emacs/EmacsSafThread.java (CacheToplevel):
(EmacsSafThread):
(DocIdEntry):
(getCache):
(pruneCache):
(cacheDirectoryFromCursor):
(run):
(documentIdFromName1):
(statDocument1):
(openDocumentDirectory1):
(openDocument1): Introduce a file status cache and populate
it with files within directories as they are opened.
* java/org/gnu/emacs/EmacsService.java (createDocument):
(createDirectory):
(moveDocument): Invalidate the file status cache wherever
needed.
* src/fileio.c (check_vfs_filename):
(Fset_file_modes): Permit `set-file-modes' to silently fail
on asset and content files.

java/org/gnu/emacs/EmacsSafThread.java
java/org/gnu/emacs/EmacsService.java
src/fileio.c

index 007ea9acfbd9ce15bd4c03fcedc130a9d0915215..29cd3fa6bc72284f16423b9771ff174cdcc445e4 100644 (file)
@@ -139,11 +139,39 @@ public final class EmacsSafThread extends HandlerThread
     /* Map between document names and children.  */
     HashMap<String, DocIdEntry> children;
 
+    /* Map between document names and file status.  */
+    HashMap<String, StatCacheEntry> statCache;
+
     /* Map between document IDs and cache items.  */
     HashMap<String, CacheEntry> idCache;
   };
 
-  private final class DocIdEntry
+  private static final class StatCacheEntry
+  {
+    /* The time at which this cache entry was created.  */
+    long time;
+
+    /* Flags, size, and modification time of this file.  */
+    long flags, size, mtime;
+
+    /* Whether or not this file is a directory.  */
+    boolean isDirectory;
+
+    public
+    StatCacheEntry ()
+    {
+      time = SystemClock.uptimeMillis ();
+    }
+
+    public boolean
+    isValid ()
+    {
+      return ((SystemClock.uptimeMillis () - time)
+             < CACHE_INVALID_TIME * 1000);
+    }
+  };
+
+  private static final class DocIdEntry
   {
     /* The document ID.  */
     String documentId;
@@ -162,10 +190,14 @@ public final class EmacsSafThread extends HandlerThread
        containing this entry, and TOPLEVEL is the toplevel
        representing it.  SIGNAL is a cancellation signal.
 
+       RESOLVER is the content provider used to retrieve file
+       information.
+
        Value is NULL if the file cannot be found.  */
 
     public CacheEntry
-    getCacheEntry (Uri tree, CacheToplevel toplevel,
+    getCacheEntry (ContentResolver resolver, Uri tree,
+                  CacheToplevel toplevel,
                   CancellationSignal signal)
     {
       Uri uri;
@@ -272,6 +304,7 @@ public final class EmacsSafThread extends HandlerThread
 
     toplevel = new CacheToplevel ();
     toplevel.children = new HashMap<String, DocIdEntry> ();
+    toplevel.statCache = new HashMap<String, StatCacheEntry> ();
     toplevel.idCache = new HashMap<String, CacheEntry> ();
     cacheToplevels.put (uri, toplevel);
     return toplevel;
@@ -311,7 +344,9 @@ public final class EmacsSafThread extends HandlerThread
   pruneCache ()
   {
     Iterator<CacheEntry> iter;
+    Iterator<StatCacheEntry> statIter;
     CacheEntry tem;
+    StatCacheEntry stat;
 
     for (CacheToplevel toplevel : cacheToplevels.values ())
       {
@@ -339,6 +374,25 @@ public final class EmacsSafThread extends HandlerThread
 
            iter.remove ();
          }
+
+       statIter = toplevel.statCache.values ().iterator ();
+
+       while (statIter.hasNext ())
+         {
+           /* Get the cache entry.  */
+           stat = statIter.next ();
+
+           /* If it's not valid anymore, remove it.  Iterating over a
+              collection whose contents are being removed is
+              undefined unless the removal is performed using the
+              iterator's own `remove' function, so tem.remove cannot
+              be used here.  */
+
+           if (stat.isValid ())
+             continue;
+
+           statIter.remove ();
+         }
       }
 
     postPruneMessage ();
@@ -379,8 +433,60 @@ public final class EmacsSafThread extends HandlerThread
     return cacheEntry;
   }
 
+  /* Cache file status for DOCUMENTID within TOPLEVEL.  Value is the
+     new cache entry.  CURSOR is the cursor from where to retrieve the
+     file status, in the form of the columns COLUMN_FLAGS,
+     COLUMN_SIZE, COLUMN_MIME_TYPE and COLUMN_LAST_MODIFIED.  */
+
+  private StatCacheEntry
+  cacheFileStatus (String documentId, CacheToplevel toplevel,
+                  Cursor cursor)
+  {
+    StatCacheEntry entry;
+    int flagsIndex, columnIndex, typeIndex;
+    int sizeIndex, mtimeIndex;
+    String type;
+
+    /* Obtain the indices for columns wanted from this cursor.  */
+    flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS);
+    sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE);
+    typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
+    mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED);
+
+    /* COLUMN_LAST_MODIFIED is allowed to be absent in a
+       conforming documents provider.  */
+    if (flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0)
+      return null;
+
+    /* Get the file status from CURSOR.  */
+    entry = new StatCacheEntry ();
+    entry.flags = cursor.getInt (flagsIndex);
+    type = cursor.getString (typeIndex);
+
+    if (type == null)
+      return null;
+
+    entry.isDirectory = type.equals (Document.MIME_TYPE_DIR);
+
+    if (cursor.isNull (sizeIndex))
+      /* The size is unknown.  */
+      entry.size = -1;
+    else
+      entry.size = cursor.getLong (sizeIndex);
+
+    /* mtimeIndex is potentially unset, since document providers
+       aren't obligated to provide modification times.  */
+
+    if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex))
+      entry.mtime = cursor.getLong (mtimeIndex);
+
+    /* Finally, add this entry to the cache and return.  */
+    toplevel.statCache.put (documentId, entry);
+    return entry;
+  }
+
   /* Cache the type and as many of the children of the directory
-     designated by DOC_ID as possible into TOPLEVEL.
+     designated by DOCUMENTID as possible into TOPLEVEL.
 
      CURSOR should be a cursor representing an open directory stream,
      with its projection consisting of at least the display name,
@@ -435,6 +541,12 @@ public final class EmacsSafThread extends HandlerThread
            idEntry.documentId = id;
            entry.children.put (id, idEntry);
 
+           /* Cache the file status for ID within TOPELVEL too; if a
+              directory listing is being requested, it's very likely
+              that a series of calls for file status will follow.  */
+
+           cacheFileStatus (id, toplevel, cursor);
+
            /* If this constituent is a directory, don't cache any
               information about it.  It cannot be cached without
               knowing its children.  */
@@ -499,6 +611,7 @@ public final class EmacsSafThread extends HandlerThread
 
          toplevel = getCache (uri);
          toplevel.idCache.remove (documentId);
+         toplevel.statCache.remove (documentId);
 
          /* If the parent of CACHENAME is cached, remove it.  */
 
@@ -570,6 +683,7 @@ public final class EmacsSafThread extends HandlerThread
 
          toplevel = getCache (uri);
          toplevel.idCache.remove (documentId);
+         toplevel.statCache.remove (documentId);
 
          /* Now remove DOCUMENTID from CACHENAME's cache entry, if
             any.  */
@@ -619,6 +733,27 @@ public final class EmacsSafThread extends HandlerThread
       });
   }
 
+  /* Invalidate the file status cache entry for DOCUMENTID within URI.
+     Call this when the contents of a file (i.e. the constituents of a
+     directory file) may have changed, but the document's display name
+     has not.  */
+
+  public void
+  postInvalidateStat (final Uri uri, final String documentId)
+  {
+    handler.post (new Runnable () {
+       @Override
+       public void
+       run ()
+       {
+         CacheToplevel toplevel;
+
+         toplevel = getCache (uri);
+         toplevel.statCache.remove (documentId);
+       }
+      });
+  }
+
 \f
 
   /* ``Prototypes'' for nested functions that are run within the SAF
@@ -857,7 +992,8 @@ public final class EmacsSafThread extends HandlerThread
                /* Fetch just the information for this document.  */
 
                if (cache == null)
-                 cache = idEntry.getCacheEntry (uri, toplevel, signal);
+                 cache = idEntry.getCacheEntry (resolver, uri, toplevel,
+                                                signal);
 
                if (cache == null)
                  {
@@ -1082,114 +1218,105 @@ public final class EmacsSafThread extends HandlerThread
   statDocument1 (String uri, String documentId,
                 CancellationSignal signal)
   {
-    Uri uriObject;
+    Uri uriObject, tree;
     String[] projection;
     long[] stat;
-    int flagsIndex, columnIndex, typeIndex;
-    int sizeIndex, mtimeIndex, flags;
-    long tem;
-    String tem1;
     Cursor cursor;
+    CacheToplevel toplevel;
+    StatCacheEntry cache;
 
-    uriObject = Uri.parse (uri);
+    tree = Uri.parse (uri);
 
     if (documentId == null)
-      documentId = DocumentsContract.getTreeDocumentId (uriObject);
+      documentId = DocumentsContract.getTreeDocumentId (tree);
 
     /* Create a document URI representing DOCUMENTID within URI's
        authority.  */
 
     uriObject
-      = DocumentsContract.buildDocumentUriUsingTree (uriObject, documentId);
+      = DocumentsContract.buildDocumentUriUsingTree (tree, documentId);
 
-    /* Now stat this document.  */
+    /* See if the file status cache currently contains this
+       document.  */
 
-    projection = new String[] {
-      Document.COLUMN_FLAGS,
-      Document.COLUMN_LAST_MODIFIED,
-      Document.COLUMN_MIME_TYPE,
-      Document.COLUMN_SIZE,
-    };
+    toplevel = getCache (tree);
+    cache = toplevel.statCache.get (documentId);
 
-    cursor = resolver.query (uriObject, projection, null,
-                            null, null, signal);
-
-    if (cursor == null)
-      return null;
-
-    /* Obtain the indices for columns wanted from this cursor.  */
-    flagsIndex = cursor.getColumnIndex (Document.COLUMN_FLAGS);
-    sizeIndex = cursor.getColumnIndex (Document.COLUMN_SIZE);
-    typeIndex = cursor.getColumnIndex (Document.COLUMN_MIME_TYPE);
-    mtimeIndex = cursor.getColumnIndex (Document.COLUMN_LAST_MODIFIED);
-
-    if (!cursor.moveToFirst ()
-       /* COLUMN_LAST_MODIFIED is allowed to be absent in a
-          conforming documents provider.  */
-       || flagsIndex < 0 || sizeIndex < 0 || typeIndex < 0)
+    if (cache == null || !cache.isValid ())
       {
-       cursor.close ();
-       return null;
-      }
+       /* Stat this document and enter its information into the
+          cache.  */
 
-    /* Create the array of file status.  */
-    stat = new long[3];
+       projection = new String[] {
+         Document.COLUMN_FLAGS,
+         Document.COLUMN_LAST_MODIFIED,
+         Document.COLUMN_MIME_TYPE,
+         Document.COLUMN_SIZE,
+       };
 
-    try
-      {
-       flags = cursor.getInt (flagsIndex);
+       cursor = resolver.query (uriObject, projection, null,
+                                null, null, signal);
 
-       stat[0] |= S_IRUSR;
-       if ((flags & Document.FLAG_SUPPORTS_WRITE) != 0)
-         stat[0] |= S_IWUSR;
+       if (cursor == null)
+         return null;
 
-       if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
-           && (flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
-         stat[0] |= S_IFCHR;
+       try
+         {
+           if (!cursor.moveToFirst ())
+             return null;
 
-       if (cursor.isNull (sizeIndex))
-         stat[1] = -1; /* The size is unknown.  */
-       else
-         stat[1] = cursor.getLong (sizeIndex);
+           cache = cacheFileStatus (documentId, toplevel, cursor);
+         }
+       finally
+         {
+           cursor.close ();
+         }
 
-       tem1 = cursor.getString (typeIndex);
+       /* If cache is still null, return null.  */
 
-       /* Check if this is a directory file.  */
-       if (tem1.equals (Document.MIME_TYPE_DIR)
-           /* Files shouldn't be specials and directories at the same
-              time, but Android doesn't forbid document providers
-              from returning this information.  */
-           && (stat[0] & S_IFCHR) == 0)
-         {
-           /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories,
-              just assume they're writable.  */
-           stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR;
+       if (cache == null)
+         return null;
+      }
 
-           /* Directory files cannot be modified if
-              FLAG_DIR_SUPPORTS_CREATE is not set.  */
+    /* Create the array of file status and populate it with the
+       information within cache.  */
+    stat = new long[3];
 
-           if ((flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
-             stat[0] &= ~S_IWUSR;
-         }
+    stat[0] |= S_IRUSR;
+    if ((cache.flags & Document.FLAG_SUPPORTS_WRITE) != 0)
+      stat[0] |= S_IWUSR;
 
-       /* If this file is neither a character special nor a
-          directory, indicate that it's a regular file.  */
+    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N
+       && (cache.flags & Document.FLAG_VIRTUAL_DOCUMENT) != 0)
+      stat[0] |= S_IFCHR;
 
-       if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0)
-         stat[0] |= S_IFREG;
+    stat[1] = cache.size;
 
-       if (mtimeIndex >= 0 && !cursor.isNull (mtimeIndex))
-         {
-           /* Content providers are allowed to not provide mtime.  */
-           tem = cursor.getLong (mtimeIndex);
-           stat[2] = tem;
-         }
-      }
-    finally
+    /* Check if this is a directory file.  */
+    if (cache.isDirectory
+       /* Files shouldn't be specials and directories at the same
+          time, but Android doesn't forbid document providers
+          from returning this information.  */
+       && (stat[0] & S_IFCHR) == 0)
       {
-       cursor.close ();
+       /* Since FLAG_SUPPORTS_WRITE doesn't apply to directories,
+          just assume they're writable.  */
+       stat[0] |= S_IFDIR | S_IWUSR | S_IXUSR;
+
+       /* Directory files cannot be modified if
+          FLAG_DIR_SUPPORTS_CREATE is not set.  */
+
+       if ((cache.flags & Document.FLAG_DIR_SUPPORTS_CREATE) == 0)
+         stat[0] &= ~S_IWUSR;
       }
 
+    /* If this file is neither a character special nor a
+       directory, indicate that it's a regular file.  */
+
+    if ((stat[0] & (S_IFDIR | S_IFCHR)) == 0)
+      stat[0] |= S_IFREG;
+
+    stat[2] = cache.mtime;
     return stat;
   }
 
@@ -1389,6 +1516,9 @@ public final class EmacsSafThread extends HandlerThread
       Document.COLUMN_DISPLAY_NAME,
       Document.COLUMN_DOCUMENT_ID,
       Document.COLUMN_MIME_TYPE,
+      Document.COLUMN_FLAGS,
+      Document.COLUMN_LAST_MODIFIED,
+      Document.COLUMN_SIZE,
     };
 
     cursor = resolver.query (uriObject, projection, null, null,
@@ -1441,6 +1571,7 @@ public final class EmacsSafThread extends HandlerThread
     Uri treeUri, documentUri;
     String mode;
     ParcelFileDescriptor fileDescriptor;
+    CacheToplevel toplevel;
 
     treeUri = Uri.parse (uri);
 
@@ -1450,35 +1581,26 @@ public final class EmacsSafThread extends HandlerThread
     documentUri
       = DocumentsContract.buildDocumentUriUsingTree (treeUri, documentId);
 
-    if (write || Build.VERSION.SDK_INT < Build.VERSION_CODES.Q)
-      {
-       /* Select the mode used to open the file.  `rw' means open
-          a stat-able file, while `rwt' means that and to
-          truncate the file as well.  */
+    /* Select the mode used to open the file.  */
 
+    if (write)
+      {
        if (truncate)
          mode = "rwt";
        else
          mode = "rw";
-
-       fileDescriptor
-         = resolver.openFileDescriptor (documentUri, mode,
-                                        signal);
       }
     else
-      {
-       /* Select the mode used to open the file.  `openFile'
-          below means always open a stat-able file.  */
+      mode = "r";
 
-       if (truncate)
-         /* Invalid mode! */
-         return null;
-       else
-         mode = "r";
+    fileDescriptor
+      = resolver.openFileDescriptor (documentUri, mode,
+                                    signal);
 
-       fileDescriptor = resolver.openFile (documentUri, mode,
-                                           signal);
-      }
+    /* Every time a document is opened, remove it from the file status
+       cache.  */
+    toplevel = getCache (treeUri);
+    toplevel.statCache.remove (documentId);
 
     return fileDescriptor;
   }
index 8554dadd06e13f2e0d90daff2d20ad3189df8177..a3dea36827256bd19723c2062a3c54f92c9bb824 100644 (file)
@@ -1586,7 +1586,7 @@ public final class EmacsService extends Service
     String mimeType, separator, mime, extension;
     int index;
     MimeTypeMap singleton;
-    Uri directoryUri, docUri;
+    Uri treeUri, directoryUri, docUri;
 
     /* Try to get the MIME type for this document.
        Default to ``application/octet-stream''.  */
@@ -1608,15 +1608,15 @@ public final class EmacsService extends Service
       }
 
     /* Now parse URI.  */
-    directoryUri = Uri.parse (uri);
+    treeUri = Uri.parse (uri);
 
     if (documentId == null)
-      documentId = DocumentsContract.getTreeDocumentId (directoryUri);
+      documentId = DocumentsContract.getTreeDocumentId (treeUri);
 
     /* And build a file URI referring to the directory.  */
 
     directoryUri
-      = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri,
+      = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri,
                                                           documentId);
 
     docUri = DocumentsContract.createDocument (resolver,
@@ -1626,6 +1626,11 @@ public final class EmacsService extends Service
     if (docUri == null)
       return null;
 
+    /* Invalidate the file status of the containing directory.  */
+
+    if (storageThread != null)
+      storageThread.postInvalidateStat (treeUri, documentId);
+
     /* Return the ID of the new document.  */
     return DocumentsContract.getDocumentId (docUri);
   }
@@ -1638,18 +1643,18 @@ public final class EmacsService extends Service
     throws FileNotFoundException
   {
     int index;
-    Uri directoryUri, docUri;
+    Uri treeUri, directoryUri, docUri;
 
     /* Now parse URI.  */
-    directoryUri = Uri.parse (uri);
+    treeUri = Uri.parse (uri);
 
     if (documentId == null)
-      documentId = DocumentsContract.getTreeDocumentId (directoryUri);
+      documentId = DocumentsContract.getTreeDocumentId (treeUri);
 
     /* And build a file URI referring to the directory.  */
 
     directoryUri
-      = DocumentsContract.buildChildDocumentsUriUsingTree (directoryUri,
+      = DocumentsContract.buildChildDocumentsUriUsingTree (treeUri,
                                                           documentId);
 
     /* If name ends with a directory separator character, delete
@@ -1669,7 +1674,12 @@ public final class EmacsService extends Service
     if (docUri == null)
       return null;
 
-    /* Return the ID of the new document.  */
+    /* Return the ID of the new document, but first invalidate the
+       state of the containing directory.  */
+
+    if (storageThread != null)
+      storageThread.postInvalidateStat (treeUri, documentId);
+
     return DocumentsContract.getDocumentId (docUri);
   }
 
@@ -1763,7 +1773,15 @@ public final class EmacsService extends Service
     /* Now invalidate the caches for both DIRNAME and DOCID.  */
 
     if (storageThread != null)
-      storageThread.postInvalidateCacheDir (uri1, docId, dirName);
+      {
+       storageThread.postInvalidateCacheDir (uri1, docId, dirName);
+
+       /* Invalidate the stat cache entries for both the source and
+          destination directories, since their contents have
+          changed.  */
+       storageThread.postInvalidateStat (uri1, dstId);
+       storageThread.postInvalidateStat (uri1, srcId);
+      }
 
     return (name != null
            ? DocumentsContract.getDocumentId (name)
index 5ce933ec45b6dd0417fc7e0c505636ed1c7bdcb8..1ccb871ce49582afca5c07335d7605b2e7880f98 100644 (file)
@@ -184,9 +184,12 @@ static bool e_write (int, Lisp_Object, ptrdiff_t, ptrdiff_t,
 
 /* Establish that ENCODED is not contained within a special directory
    whose contents are not eligible for Unix VFS operations.  Signal a
-   `file-error' with REASON if it does.  */
+   `file-error' with REASON if it does.
 
-static void
+   If REASON is NULL, instead return whether ENCODED is contained
+   within such a directory.  */
+
+static bool
 check_vfs_filename (Lisp_Object encoded, const char *reason)
 {
 #if defined HAVE_ANDROID && !defined ANDROID_STUBIFY
@@ -194,11 +197,16 @@ check_vfs_filename (Lisp_Object encoded, const char *reason)
 
   name = SSDATA (encoded);
 
-  if (android_is_special_directory (name, "/assets"))
-    xsignal2 (Qfile_error, build_string (reason), encoded);
+  if (android_is_special_directory (name, "/assets")
+      || android_is_special_directory (name, "/content"))
+    {
+      if (!reason)
+       return true;
+
+      xsignal2 (Qfile_error, build_string (reason), encoded);
+    }
 
-  if (android_is_special_directory (name, "/content"))
-    xsignal2 (Qfile_error, build_string (reason), encoded);
+  return false;
 #endif /* defined HAVE_ANDROID && !defined ANDROID_STUBIFY */
 }
 
@@ -3657,8 +3665,14 @@ command from GNU Coreutils.  */)
     return call4 (handler, Qset_file_modes, absname, mode, flag);
 
   encoded = ENCODE_FILE (absname);
-  check_vfs_filename (encoded, "Trying to change access modes of file"
-                     " within special directory");
+
+  /* Silently ignore attempts to change the access modes of files
+     within /contents on Android, preventing errors within backup file
+     creation.  */
+
+  if (check_vfs_filename (encoded, NULL))
+    return Qnil;
+
   char *fname = SSDATA (encoded);
   mode_t imode = XFIXNUM (mode) & 07777;
   if (fchmodat (AT_FDCWD, fname, imode, nofollow) != 0)