]> git.eshelyaron.com Git - emacs.git/commitdiff
Allow other text editors to edit files in Emacs' home directory
authorPo Lu <luangruo@yahoo.com>
Thu, 9 Feb 2023 14:56:41 +0000 (22:56 +0800)
committerPo Lu <luangruo@yahoo.com>
Thu, 9 Feb 2023 14:56:41 +0000 (22:56 +0800)
* java/AndroidManifest.xml.in: Declare the new documents
provider.
* java/README: Describe the meaning of files in res/values.
* java/org/gnu/emacs/EmacsDocumentsProvider.java
(EmacsDocumentsProvider): New file.
* java/res/values-v19/bool.xml:
* java/res/values/bool.xml: New files.

java/AndroidManifest.xml.in
java/README
java/org/gnu/emacs/EmacsDocumentsProvider.java [new file with mode: 0644]
java/res/values-v19/bool.xml [new file with mode: 0644]
java/res/values/bool.xml [new file with mode: 0644]

index 3c9e30713b6b2c54aeeb17ac9591743fc7af24a3..1da3646e2f590311547343f568c665388b8d0588 100644 (file)
@@ -179,6 +179,18 @@ along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>. -->
       </intent-filter>
     </activity>
 
+    <provider android:name="org.gnu.emacs.EmacsDocumentsProvider"
+             android:authorities="org.gnu.emacs"
+             android:exported="true"
+             android:grantUriPermissions="true"
+             android:permission="android.permission.MANAGE_DOCUMENTS"
+             android:enabled="@bool/isAtLeastKitKat">
+      <intent-filter>
+       <action
+           android:name="android.content.action.DOCUMENTS_PROVIDER"/>
+      </intent-filter>
+    </provider>
+
     <service android:name="org.gnu.emacs.EmacsService"
             android:directBootAware="false"
             android:enabled="true"
index 5539871cc2cf18a0b59054eb9a6f6f5899ab60a3..fd4aa770f4b6e39e7f95d95a62ae306ac544a59b 100644 (file)
@@ -5,7 +5,12 @@ package out of them.
 The ``org/gnu/emacs'' subdirectory contains the Java sources under the
 ``org.gnu.emacs'' package identifier.
 
-The ``res'' directory contains resources, mainly the Emacs icon.
+``AndroidManifest.xml'' contains a manifest describing the Java
+sources to the system.
+
+The ``res'' directory contains resources, mainly the Emacs icon and
+several ``boolean resources'' which are used as a form of conditional
+evaluation for manifest entries.
 
 `emacs.keystore' is the signing key used to build Emacs.  It is kept
 here, and we encourage all people redistributing Emacs to use this
@@ -456,6 +461,12 @@ loaded by the special invocation:
     System.loadLibrary ("emacs");
   };
 
+where ``static'' defines a section of code which will be run upon the
+object (containing class) being loaded.  This is like:
+
+  __attribute__((constructor))
+
+on systems where shared object constructors are supported.
 
 See http://docs.oracle.com/en/java/javase/19/docs/specs/jni/intro.html
 for more details.
diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java
new file mode 100644 (file)
index 0000000..f12b302
--- /dev/null
@@ -0,0 +1,381 @@
+/* Communication module for Android terminals.  -*- c-file-style: "GNU" -*-
+
+Copyright (C) 2023 Free Software Foundation, Inc.
+
+This file is part of GNU Emacs.
+
+GNU Emacs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or (at
+your option) any later version.
+
+GNU Emacs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.  */
+
+package org.gnu.emacs;
+
+import android.content.Context;
+
+import android.database.Cursor;
+import android.database.MatrixCursor;
+
+import android.os.Build;
+import android.os.CancellationSignal;
+import android.os.ParcelFileDescriptor;
+
+import android.provider.DocumentsContract.Document;
+import android.provider.DocumentsContract.Root;
+import static android.provider.DocumentsContract.buildChildDocumentsUri;
+import android.provider.DocumentsProvider;
+
+import android.webkit.MimeTypeMap;
+
+import android.net.Uri;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+/* ``Documents provider''.  This allows Emacs's home directory to be
+   modified by other programs holding permissions to manage system
+   storage, which is useful to (for example) correct misconfigurations
+   which prevent Emacs from starting up.
+
+   This functionality is only available on Android 19 and later.  */
+
+public class EmacsDocumentsProvider extends DocumentsProvider
+{
+  /* Home directory.  This is the directory whose contents are
+     initially returned to requesting applications.  */
+  private File baseDir;
+
+  /* The default projection for requests for the root directory.  */
+  private static final String[] DEFAULT_ROOT_PROJECTION;
+
+  /* The default projection for requests for a file.  */
+  private static final String[] DEFAULT_DOCUMENT_PROJECTION;
+
+  static
+  {
+    DEFAULT_ROOT_PROJECTION = new String[] {
+      Root.COLUMN_ROOT_ID,
+      Root.COLUMN_MIME_TYPES,
+      Root.COLUMN_FLAGS,
+      Root.COLUMN_TITLE,
+      Root.COLUMN_SUMMARY,
+      Root.COLUMN_DOCUMENT_ID,
+      Root.COLUMN_AVAILABLE_BYTES,
+    };
+
+    DEFAULT_DOCUMENT_PROJECTION = new String[] {
+      Document.COLUMN_DOCUMENT_ID,
+      Document.COLUMN_MIME_TYPE,
+      Document.COLUMN_DISPLAY_NAME,
+      Document.COLUMN_LAST_MODIFIED,
+      Document.COLUMN_FLAGS,
+      Document.COLUMN_SIZE,
+    };
+  }
+
+  @Override
+  public boolean
+  onCreate ()
+  {
+    /* Set the base directory to Emacs's files directory.  */
+    baseDir = getContext ().getFilesDir ();
+    return true;
+  }
+
+  @Override
+  public Cursor
+  queryRoots (String[] projection)
+  {
+    MatrixCursor result;
+    MatrixCursor.RowBuilder row;
+
+    /* If the requestor asked for nothing at all, then it wants some
+       data by default.  */
+
+    if (projection == null)
+      projection = DEFAULT_ROOT_PROJECTION;
+
+    result = new MatrixCursor (projection);
+    row = result.newRow ();
+
+    /* Now create and add a row for each file in the base
+       directory.  */
+    row.add (Root.COLUMN_ROOT_ID, baseDir.getAbsolutePath ());
+    row.add (Root.COLUMN_SUMMARY, "Emacs home directory");
+
+    /* Add the appropriate flags.  */
+
+    row.add (Root.COLUMN_FLAGS, Root.FLAG_SUPPORTS_CREATE);
+    row.add (Root.FLAG_LOCAL_ONLY);
+    row.add (Root.COLUMN_TITLE, "Emacs");
+    row.add (Root.COLUMN_DOCUMENT_ID, baseDir.getAbsolutePath ());
+
+    return result;
+  }
+
+  /* Return the MIME type of a file FILE.  */
+
+  private String
+  getMimeType (File file)
+  {
+    String name, extension, mime;
+    int extensionSeparator;
+
+    if (file.isDirectory ())
+      return Document.MIME_TYPE_DIR;
+
+    /* Abuse WebView stuff to get the file's MIME type.  */
+    name = file.getName ();
+    extensionSeparator = name.lastIndexOf ('.');
+
+    if (extensionSeparator > 0)
+      {
+       extension = name.substring (extensionSeparator + 1);
+       mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension (extension);
+
+       if (mime != null)
+         return mime;
+      }
+
+    return "application/octet-stream";
+  }
+
+  /* Append the specified FILE to the query result RESULT.
+     Handle both directories and ordinary files.  */
+
+  private void
+  queryDocument1 (MatrixCursor result, File file)
+  {
+    MatrixCursor.RowBuilder row;
+    String fileName, displayName, mimeType;
+    int flags;
+
+    row = result.newRow ();
+    flags = 0;
+
+    /* fileName is a string that the system will ask for some time in
+       the future.  Here, it is just the absolute name of the file.  */
+    fileName = file.getAbsolutePath ();
+
+    /* If file is a directory, add the right flags for that.  */
+
+    if (file.isDirectory ())
+      {
+       if (file.canWrite ())
+         {
+           flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
+           flags |= Document.FLAG_SUPPORTS_DELETE;
+         }
+      }
+    else if (file.canWrite ())
+      {
+       /* Apply the correct flags for a writable file.  */
+       flags |= Document.FLAG_SUPPORTS_WRITE;
+       flags |= Document.FLAG_SUPPORTS_DELETE;
+
+       /* TODO: implement these
+
+          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
+            flags |= Document.FLAG_SUPPORTS_RENAME;
+
+          if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
+            flags |= Document.FLAG_SUPPORTS_REMOVE; */
+      }
+
+    displayName = file.getName ();
+    mimeType = getMimeType (file);
+
+    row.add (Document.COLUMN_DOCUMENT_ID, fileName);
+    row.add (Document.COLUMN_DISPLAY_NAME, displayName);
+    row.add (Document.COLUMN_SIZE, file.length ());
+    row.add (Document.COLUMN_MIME_TYPE, mimeType);
+    row.add (Document.COLUMN_LAST_MODIFIED, file.lastModified ());
+    row.add (Document.COLUMN_FLAGS, flags);
+  }
+
+  @Override
+  public Cursor
+  queryDocument (String documentId, String[] projection)
+    throws FileNotFoundException
+  {
+    MatrixCursor result;
+
+    if (projection == null)
+      projection = DEFAULT_DOCUMENT_PROJECTION;
+
+    result = new MatrixCursor (projection);
+    queryDocument1 (result, new File (documentId));
+
+    return result;
+  }
+
+  @Override
+  public Cursor
+  queryChildDocuments (String parentDocumentId, String[] projection,
+                      String sortOrder) throws FileNotFoundException
+  {
+    MatrixCursor result;
+    File directory;
+
+    if (projection == null)
+      projection = DEFAULT_DOCUMENT_PROJECTION;
+
+    result = new MatrixCursor (projection);
+
+    /* Try to open the file corresponding to the location being
+       requested.  */
+    directory = new File (parentDocumentId);
+
+    /* Now add each child.  */
+    for (File child : directory.listFiles ())
+      queryDocument1 (result, child);
+
+    return result;
+  }
+
+  @Override
+  public ParcelFileDescriptor
+  openDocument (String documentId, String mode,
+               CancellationSignal signal) throws FileNotFoundException
+  {
+    return ParcelFileDescriptor.open (new File (documentId),
+                                     ParcelFileDescriptor.parseMode (mode));
+  }
+
+  @Override
+  public String
+  createDocument (String documentId, String mimeType,
+                 String displayName) throws FileNotFoundException
+  {
+    File file;
+    boolean rc;
+    Uri updatedUri;
+    Context context;
+
+    context = getContext ();
+    file = new File (documentId, displayName);
+
+    try
+      {
+       rc = false;
+
+       if (Document.MIME_TYPE_DIR.equals (mimeType))
+         {
+           file.mkdirs ();
+
+           if (file.isDirectory ())
+             rc = true;
+         }
+       else
+         {
+           file.createNewFile ();
+
+           if (file.isFile ()
+               && file.setWritable (true)
+               && file.setReadable (true))
+             rc = true;
+         }
+
+       if (!rc)
+         throw new FileNotFoundException ("rc != 1");
+      }
+    catch (IOException e)
+      {
+       throw new FileNotFoundException (e.toString ());
+      }
+
+    updatedUri
+      = buildChildDocumentsUri ("org.gnu.emacs", documentId);
+    /* Tell the system about the change.  */
+    context.getContentResolver ().notifyChange (updatedUri, null);
+
+    return file.getAbsolutePath ();
+  }
+
+  private void
+  deleteDocument1 (File child)
+  {
+    File[] children;
+
+    /* Don't delete symlinks recursively.
+
+       Calling readlink or stat is problematic due to file name
+       encoding problems, so try to delete the file first, and only
+       try to delete files recursively afterword.  */
+
+    if (child.delete ())
+      return;
+
+    children = child.listFiles ();
+
+    if (children != null)
+      {
+       for (File file : children)
+         deleteDocument1 (file);
+      }
+
+    child.delete ();
+  }
+
+  @Override
+  public void
+  deleteDocument (String documentId)
+    throws FileNotFoundException
+  {
+    File file, parent;
+    File[] children;
+    Uri updatedUri;
+    Context context;
+
+    /* Java makes recursively deleting a file hard.  File name
+       encoding issues also prevent easily calling into C...  */
+
+    context = getContext ();
+    file = new File (documentId);
+    parent = file.getParentFile ();
+
+    if (parent == null)
+      throw new RuntimeException ("trying to delete file without"
+                                 + " parent!");
+
+    updatedUri
+      = buildChildDocumentsUri ("org.gnu.emacs",
+                               parent.getAbsolutePath ());
+
+    if (file.delete ())
+      {
+       /* Tell the system about the change.  */
+       context.getContentResolver ().notifyChange (updatedUri, null);
+       return;
+      }
+
+    children = file.listFiles ();
+
+    if (children != null)
+      {
+       for (File child : children)
+         deleteDocument1 (child);
+      }
+
+    if (file.delete ())
+      /* Tell the system about the change.  */
+      context.getContentResolver ().notifyChange (updatedUri, null);
+  }
+
+  @Override
+  public void
+  removeDocument (String documentId, String parentDocumentId)
+    throws FileNotFoundException
+  {
+    deleteDocument (documentId);
+  }
+}
diff --git a/java/res/values-v19/bool.xml b/java/res/values-v19/bool.xml
new file mode 100644 (file)
index 0000000..a4e3a87
--- /dev/null
@@ -0,0 +1,22 @@
+<!-- Boolean resources for GNU Emacs on Android 4.4 or later.
+
+Copyright (C) 2023 Free Software Foundation, Inc.
+
+This file is part of GNU Emacs.
+
+GNU Emacs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+GNU Emacs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>. -->
+
+<resources>
+  <bool name="isAtLeastKitKat">true</bool>
+</resources>
diff --git a/java/res/values/bool.xml b/java/res/values/bool.xml
new file mode 100644 (file)
index 0000000..d37eab7
--- /dev/null
@@ -0,0 +1,22 @@
+<!-- Boolean resources for GNU Emacs on Android.
+
+Copyright (C) 2023 Free Software Foundation, Inc.
+
+This file is part of GNU Emacs.
+
+GNU Emacs is free software: you can redistribute it and/or modify
+it under the terms of the GNU General Public License as published by
+the Free Software Foundation, either version 3 of the License, or
+(at your option) any later version.
+
+GNU Emacs is distributed in the hope that it will be useful,
+but WITHOUT ANY WARRANTY; without even the implied warranty of
+MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+GNU General Public License for more details.
+
+You should have received a copy of the GNU General Public License
+along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>. -->
+
+<resources>
+  <bool name="isAtLeastKitKat">false</bool>
+</resources>