summaryrefslogtreecommitdiff
path: root/java/org/gnu/emacs/EmacsDocumentsProvider.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/org/gnu/emacs/EmacsDocumentsProvider.java')
-rw-r--r--java/org/gnu/emacs/EmacsDocumentsProvider.java578
1 files changed, 578 insertions, 0 deletions
diff --git a/java/org/gnu/emacs/EmacsDocumentsProvider.java b/java/org/gnu/emacs/EmacsDocumentsProvider.java
new file mode 100644
index 00000000000..7c5de9e0e14
--- /dev/null
+++ b/java/org/gnu/emacs/EmacsDocumentsProvider.java
@@ -0,0 +1,578 @@
+/* Communication module for Android terminals. -*- c-file-style: "GNU" -*-
+
+Copyright (C) 2023-2024 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.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+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 final 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_ICON,
+ 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
+ | Root.FLAG_SUPPORTS_IS_CHILD));
+ row.add (Root.COLUMN_ICON, R.drawable.emacs);
+ row.add (Root.FLAG_LOCAL_ONLY);
+ row.add (Root.COLUMN_TITLE, "Emacs");
+ row.add (Root.COLUMN_DOCUMENT_ID, baseDir.getAbsolutePath ());
+
+ return result;
+ }
+
+ private Uri
+ getNotificationUri (File file)
+ {
+ Uri updatedUri;
+
+ updatedUri
+ = buildChildDocumentsUri ("org.gnu.emacs",
+ file.getAbsolutePath ());
+
+ return updatedUri;
+ }
+
+ /* Inform the system that FILE's contents (or FILE itself) has
+ changed. */
+
+ private void
+ notifyChange (File file)
+ {
+ Uri updatedUri;
+ Context context;
+
+ context = getContext ();
+ updatedUri
+ = buildChildDocumentsUri ("org.gnu.emacs",
+ file.getAbsolutePath ());
+ context.getContentResolver ().notifyChange (updatedUri, null);
+ }
+
+ /* Inform the system that FILE's contents (or FILE itself) has
+ changed. FILE is a string describing containing the file name of
+ a directory as opposed to a File. */
+
+ private void
+ notifyChangeByName (String file)
+ {
+ Uri updatedUri;
+ Context context;
+
+ context = getContext ();
+ updatedUri
+ = buildChildDocumentsUri ("org.gnu.emacs", file);
+ context.getContentResolver ().notifyChange (updatedUri, null);
+ }
+
+ /* Return the MIME type of a file FILE. */
+
+ private String
+ getMimeType (File file)
+ {
+ String name, extension, mime;
+ int extensionSeparator;
+ MimeTypeMap singleton;
+
+ 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)
+ {
+ singleton = MimeTypeMap.getSingleton ();
+ extension = name.substring (extensionSeparator + 1);
+ mime = singleton.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;
+
+ 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_MOVE;
+ }
+ }
+ else if (file.canWrite ())
+ {
+ /* Apply the correct flags for a writable file. */
+ flags |= Document.FLAG_SUPPORTS_WRITE;
+ flags |= Document.FLAG_SUPPORTS_DELETE;
+
+ 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;
+ flags |= Document.FLAG_SUPPORTS_MOVE;
+ }
+ }
+
+ 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;
+ File file;
+ Context context;
+
+ file = new File (documentId);
+ context = getContext ();
+
+ if (projection == null)
+ projection = DEFAULT_DOCUMENT_PROJECTION;
+
+ result = new MatrixCursor (projection);
+ queryDocument1 (result, file);
+
+ /* Now allow interested applications to detect changes. */
+ result.setNotificationUri (context.getContentResolver (),
+ getNotificationUri (file));
+
+ return result;
+ }
+
+ @Override
+ public Cursor
+ queryChildDocuments (String parentDocumentId, String[] projection,
+ String sortOrder) throws FileNotFoundException
+ {
+ MatrixCursor result;
+ File directory;
+ File[] files;
+ Context context;
+
+ 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);
+
+ /* Look up each child. */
+ files = directory.listFiles ();
+
+ if (files != null)
+ {
+ /* Now add each child. */
+ for (File child : files)
+ queryDocument1 (result, child);
+ }
+
+ context = getContext ();
+
+ /* Now allow interested applications to detect changes. */
+ result.setNotificationUri (context.getContentResolver (),
+ getNotificationUri (directory));
+
+ 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, parentFile;
+ boolean rc;
+
+ 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 ());
+ }
+
+ parentFile = file.getParentFile ();
+
+ if (parentFile != null)
+ notifyChange (parentFile);
+
+ 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;
+
+ /* Java makes recursively deleting a file hard. File name
+ encoding issues also prevent easily calling into C... */
+
+ file = new File (documentId);
+ parent = file.getParentFile ();
+
+ if (parent == null)
+ throw new RuntimeException ("trying to delete file without"
+ + " parent!");
+
+ if (file.delete ())
+ {
+ /* Tell the system about the change. */
+ notifyChange (parent);
+ return;
+ }
+
+ children = file.listFiles ();
+
+ if (children != null)
+ {
+ for (File child : children)
+ deleteDocument1 (child);
+ }
+
+ if (file.delete ())
+ /* Tell the system about the change. */
+ notifyChange (parent);
+ }
+
+ @Override
+ public void
+ removeDocument (String documentId, String parentDocumentId)
+ throws FileNotFoundException
+ {
+ deleteDocument (documentId);
+ }
+
+ @Override
+ public String
+ getDocumentType (String documentId)
+ {
+ return getMimeType (new File (documentId));
+ }
+
+ @Override
+ public String
+ renameDocument (String documentId, String displayName)
+ throws FileNotFoundException
+ {
+ File file, newName;
+ File parent;
+
+ file = new File (documentId);
+ parent = file.getParentFile ();
+ newName = new File (parent, displayName);
+
+ if (parent == null)
+ throw new FileNotFoundException ("parent is null");
+
+ file = new File (documentId);
+
+ if (!file.renameTo (newName))
+ return null;
+
+ notifyChange (parent);
+ return newName.getAbsolutePath ();
+ }
+
+ @Override
+ public boolean
+ isChildDocument (String parentDocumentId, String documentId)
+ {
+ return documentId.startsWith (parentDocumentId);
+ }
+
+ @Override
+ public String
+ moveDocument (String sourceDocumentId,
+ String sourceParentDocumentId,
+ String targetParentDocumentId)
+ throws FileNotFoundException
+ {
+ File file, newName;
+ FileInputStream inputStream;
+ FileOutputStream outputStream;
+ byte buffer[];
+ int length;
+
+ file = new File (sourceDocumentId);
+
+ /* Now, create the file name of the parent document. */
+ newName = new File (targetParentDocumentId,
+ file.getName ());
+
+ /* Try to perform a simple rename, before falling back to
+ copying. */
+
+ if (file.renameTo (newName))
+ {
+ notifyChangeByName (file.getParent ());
+ notifyChangeByName (targetParentDocumentId);
+ return newName.getAbsolutePath ();
+ }
+
+ /* If that doesn't work, create the new file and copy over the old
+ file's contents. */
+
+ inputStream = null;
+ outputStream = null;
+
+ try
+ {
+ if (!newName.createNewFile ()
+ || !newName.setWritable (true)
+ || !newName.setReadable (true))
+ throw new FileNotFoundException ("failed to create new file");
+
+ /* Open the file in preparation for a copy. */
+
+ inputStream = new FileInputStream (file);
+ outputStream = new FileOutputStream (newName);
+
+ /* Allocate the buffer used to hold data. */
+
+ buffer = new byte[4096];
+
+ while ((length = inputStream.read (buffer)) > 0)
+ outputStream.write (buffer, 0, length);
+ }
+ catch (IOException e)
+ {
+ throw new FileNotFoundException ("IOException: " + e);
+ }
+ finally
+ {
+ try
+ {
+ if (inputStream != null)
+ inputStream.close ();
+ }
+ catch (IOException e)
+ {
+
+ }
+
+ try
+ {
+ if (outputStream != null)
+ outputStream.close ();
+ }
+ catch (IOException e)
+ {
+
+ }
+ }
+
+ file.delete ();
+ notifyChangeByName (file.getParent ());
+ notifyChangeByName (targetParentDocumentId);
+
+ return newName.getAbsolutePath ();
+ }
+}