root/java/org/gnu/emacs/EmacsDocumentsProvider.java

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

DEFINITIONS

This source file includes following definitions.
  1. onCreate
  2. queryRoots
  3. getNotificationUri
  4. notifyChange
  5. notifyChangeByName
  6. getMimeType
  7. queryDocument1
  8. queryDocument
  9. queryChildDocuments
  10. openDocument
  11. createDocument
  12. deleteDocument1
  13. deleteDocument
  14. removeDocument
  15. getDocumentType
  16. renameDocument
  17. isChildDocument
  18. moveDocument

     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 android.content.Context;
    23 
    24 import android.database.Cursor;
    25 import android.database.MatrixCursor;
    26 
    27 import android.os.Build;
    28 import android.os.CancellationSignal;
    29 import android.os.ParcelFileDescriptor;
    30 
    31 import android.provider.DocumentsContract.Document;
    32 import android.provider.DocumentsContract.Root;
    33 import static android.provider.DocumentsContract.buildChildDocumentsUri;
    34 import android.provider.DocumentsProvider;
    35 
    36 import android.webkit.MimeTypeMap;
    37 
    38 import android.net.Uri;
    39 
    40 import java.io.File;
    41 import java.io.FileInputStream;
    42 import java.io.FileNotFoundException;
    43 import java.io.FileOutputStream;
    44 import java.io.IOException;
    45 
    46 /* ``Documents provider''.  This allows Emacs's home directory to be
    47    modified by other programs holding permissions to manage system
    48    storage, which is useful to (for example) correct misconfigurations
    49    which prevent Emacs from starting up.
    50 
    51    This functionality is only available on Android 19 and later.  */
    52 
    53 public final class EmacsDocumentsProvider extends DocumentsProvider
    54 {
    55   /* Home directory.  This is the directory whose contents are
    56      initially returned to requesting applications.  */
    57   private File baseDir;
    58 
    59   /* The default projection for requests for the root directory.  */
    60   private static final String[] DEFAULT_ROOT_PROJECTION;
    61 
    62   /* The default projection for requests for a file.  */
    63   private static final String[] DEFAULT_DOCUMENT_PROJECTION;
    64 
    65   static
    66   {
    67     DEFAULT_ROOT_PROJECTION = new String[] {
    68       Root.COLUMN_ROOT_ID,
    69       Root.COLUMN_MIME_TYPES,
    70       Root.COLUMN_FLAGS,
    71       Root.COLUMN_ICON,
    72       Root.COLUMN_TITLE,
    73       Root.COLUMN_SUMMARY,
    74       Root.COLUMN_DOCUMENT_ID,
    75       Root.COLUMN_AVAILABLE_BYTES,
    76     };
    77 
    78     DEFAULT_DOCUMENT_PROJECTION = new String[] {
    79       Document.COLUMN_DOCUMENT_ID,
    80       Document.COLUMN_MIME_TYPE,
    81       Document.COLUMN_DISPLAY_NAME,
    82       Document.COLUMN_LAST_MODIFIED,
    83       Document.COLUMN_FLAGS,
    84       Document.COLUMN_SIZE,
    85     };
    86   }
    87 
    88   @Override
    89   public boolean
    90   onCreate ()
    91   {
    92     /* Set the base directory to Emacs's files directory.  */
    93     baseDir = getContext ().getFilesDir ();
    94     return true;
    95   }
    96 
    97   @Override
    98   public Cursor
    99   queryRoots (String[] projection)
   100   {
   101     MatrixCursor result;
   102     MatrixCursor.RowBuilder row;
   103 
   104     /* If the requestor asked for nothing at all, then it wants some
   105        data by default.  */
   106 
   107     if (projection == null)
   108       projection = DEFAULT_ROOT_PROJECTION;
   109 
   110     result = new MatrixCursor (projection);
   111     row = result.newRow ();
   112 
   113     /* Now create and add a row for each file in the base
   114        directory.  */
   115     row.add (Root.COLUMN_ROOT_ID, baseDir.getAbsolutePath ());
   116     row.add (Root.COLUMN_SUMMARY, "Emacs home directory");
   117 
   118     /* Add the appropriate flags.  */
   119 
   120     row.add (Root.COLUMN_FLAGS, (Root.FLAG_SUPPORTS_CREATE
   121                                  | Root.FLAG_SUPPORTS_IS_CHILD));
   122     row.add (Root.COLUMN_ICON, R.drawable.emacs);
   123     row.add (Root.FLAG_LOCAL_ONLY);
   124     row.add (Root.COLUMN_TITLE, "Emacs");
   125     row.add (Root.COLUMN_DOCUMENT_ID, baseDir.getAbsolutePath ());
   126 
   127     return result;
   128   }
   129 
   130   private Uri
   131   getNotificationUri (File file)
   132   {
   133     Uri updatedUri;
   134 
   135     updatedUri
   136       = buildChildDocumentsUri ("org.gnu.emacs",
   137                                 file.getAbsolutePath ());
   138 
   139     return updatedUri;
   140   }
   141 
   142   /* Inform the system that FILE's contents (or FILE itself) has
   143      changed.  */
   144 
   145   private void
   146   notifyChange (File file)
   147   {
   148     Uri updatedUri;
   149     Context context;
   150 
   151     context = getContext ();
   152     updatedUri
   153       = buildChildDocumentsUri ("org.gnu.emacs",
   154                                 file.getAbsolutePath ());
   155     context.getContentResolver ().notifyChange (updatedUri, null);
   156   }
   157 
   158   /* Inform the system that FILE's contents (or FILE itself) has
   159      changed.  FILE is a string describing containing the file name of
   160      a directory as opposed to a File.  */
   161 
   162   private void
   163   notifyChangeByName (String file)
   164   {
   165     Uri updatedUri;
   166     Context context;
   167 
   168     context = getContext ();
   169     updatedUri
   170       = buildChildDocumentsUri ("org.gnu.emacs", file);
   171     context.getContentResolver ().notifyChange (updatedUri, null);
   172   }
   173 
   174   /* Return the MIME type of a file FILE.  */
   175 
   176   private String
   177   getMimeType (File file)
   178   {
   179     String name, extension, mime;
   180     int extensionSeparator;
   181     MimeTypeMap singleton;
   182 
   183     if (file.isDirectory ())
   184       return Document.MIME_TYPE_DIR;
   185 
   186     /* Abuse WebView stuff to get the file's MIME type.  */
   187     name = file.getName ();
   188     extensionSeparator = name.lastIndexOf ('.');
   189 
   190     if (extensionSeparator > 0)
   191       {
   192         singleton = MimeTypeMap.getSingleton ();
   193         extension = name.substring (extensionSeparator + 1);
   194         mime = singleton.getMimeTypeFromExtension (extension);
   195 
   196         if (mime != null)
   197           return mime;
   198       }
   199 
   200     return "application/octet-stream";
   201   }
   202 
   203   /* Append the specified FILE to the query result RESULT.
   204      Handle both directories and ordinary files.  */
   205 
   206   private void
   207   queryDocument1 (MatrixCursor result, File file)
   208   {
   209     MatrixCursor.RowBuilder row;
   210     String fileName, displayName, mimeType;
   211     int flags;
   212 
   213     row = result.newRow ();
   214     flags = 0;
   215 
   216     /* fileName is a string that the system will ask for some time in
   217        the future.  Here, it is just the absolute name of the file.  */
   218     fileName = file.getAbsolutePath ();
   219 
   220     /* If file is a directory, add the right flags for that.  */
   221 
   222     if (file.isDirectory ())
   223       {
   224         if (file.canWrite ())
   225           {
   226             flags |= Document.FLAG_DIR_SUPPORTS_CREATE;
   227             flags |= Document.FLAG_SUPPORTS_DELETE;
   228 
   229             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
   230               flags |= Document.FLAG_SUPPORTS_RENAME;
   231 
   232             if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
   233               flags |= Document.FLAG_SUPPORTS_MOVE;
   234           }
   235       }
   236     else if (file.canWrite ())
   237       {
   238         /* Apply the correct flags for a writable file.  */
   239         flags |= Document.FLAG_SUPPORTS_WRITE;
   240         flags |= Document.FLAG_SUPPORTS_DELETE;
   241 
   242         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP)
   243           flags |= Document.FLAG_SUPPORTS_RENAME;
   244 
   245         if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N)
   246           {
   247             flags |= Document.FLAG_SUPPORTS_REMOVE;
   248             flags |= Document.FLAG_SUPPORTS_MOVE;
   249           }
   250       }
   251 
   252     displayName = file.getName ();
   253     mimeType = getMimeType (file);
   254 
   255     row.add (Document.COLUMN_DOCUMENT_ID, fileName);
   256     row.add (Document.COLUMN_DISPLAY_NAME, displayName);
   257     row.add (Document.COLUMN_SIZE, file.length ());
   258     row.add (Document.COLUMN_MIME_TYPE, mimeType);
   259     row.add (Document.COLUMN_LAST_MODIFIED, file.lastModified ());
   260     row.add (Document.COLUMN_FLAGS, flags);
   261   }
   262 
   263   @Override
   264   public Cursor
   265   queryDocument (String documentId, String[] projection)
   266     throws FileNotFoundException
   267   {
   268     MatrixCursor result;
   269     File file;
   270     Context context;
   271 
   272     file = new File (documentId);
   273     context = getContext ();
   274 
   275     if (projection == null)
   276       projection = DEFAULT_DOCUMENT_PROJECTION;
   277 
   278     result = new MatrixCursor (projection);
   279     queryDocument1 (result, file);
   280 
   281     /* Now allow interested applications to detect changes.  */
   282     result.setNotificationUri (context.getContentResolver (),
   283                                getNotificationUri (file));
   284 
   285     return result;
   286   }
   287 
   288   @Override
   289   public Cursor
   290   queryChildDocuments (String parentDocumentId, String[] projection,
   291                        String sortOrder) throws FileNotFoundException
   292   {
   293     MatrixCursor result;
   294     File directory;
   295     File[] files;
   296     Context context;
   297 
   298     if (projection == null)
   299       projection = DEFAULT_DOCUMENT_PROJECTION;
   300 
   301     result = new MatrixCursor (projection);
   302 
   303     /* Try to open the file corresponding to the location being
   304        requested.  */
   305     directory = new File (parentDocumentId);
   306 
   307     /* Look up each child.  */
   308     files = directory.listFiles ();
   309 
   310     if (files != null)
   311       {
   312         /* Now add each child.  */
   313         for (File child : files)
   314           queryDocument1 (result, child);
   315       }
   316 
   317     context = getContext ();
   318 
   319     /* Now allow interested applications to detect changes.  */
   320     result.setNotificationUri (context.getContentResolver (),
   321                                getNotificationUri (directory));
   322 
   323     return result;
   324   }
   325 
   326   @Override
   327   public ParcelFileDescriptor
   328   openDocument (String documentId, String mode,
   329                 CancellationSignal signal) throws FileNotFoundException
   330   {
   331     return ParcelFileDescriptor.open (new File (documentId),
   332                                       ParcelFileDescriptor.parseMode (mode));
   333   }
   334 
   335   @Override
   336   public String
   337   createDocument (String documentId, String mimeType,
   338                   String displayName) throws FileNotFoundException
   339   {
   340     File file, parentFile;
   341     boolean rc;
   342 
   343     file = new File (documentId, displayName);
   344 
   345     try
   346       {
   347         rc = false;
   348 
   349         if (Document.MIME_TYPE_DIR.equals (mimeType))
   350           {
   351             file.mkdirs ();
   352 
   353             if (file.isDirectory ())
   354               rc = true;
   355           }
   356         else
   357           {
   358             file.createNewFile ();
   359 
   360             if (file.isFile ()
   361                 && file.setWritable (true)
   362                 && file.setReadable (true))
   363               rc = true;
   364           }
   365 
   366         if (!rc)
   367           throw new FileNotFoundException ("rc != 1");
   368       }
   369     catch (IOException e)
   370       {
   371         throw new FileNotFoundException (e.toString ());
   372       }
   373 
   374     parentFile = file.getParentFile ();
   375 
   376     if (parentFile != null)
   377       notifyChange (parentFile);
   378 
   379     return file.getAbsolutePath ();
   380   }
   381 
   382   private void
   383   deleteDocument1 (File child)
   384   {
   385     File[] children;
   386 
   387     /* Don't delete symlinks recursively.
   388 
   389        Calling readlink or stat is problematic due to file name
   390        encoding problems, so try to delete the file first, and only
   391        try to delete files recursively afterword.  */
   392 
   393     if (child.delete ())
   394       return;
   395 
   396     children = child.listFiles ();
   397 
   398     if (children != null)
   399       {
   400         for (File file : children)
   401           deleteDocument1 (file);
   402       }
   403 
   404     child.delete ();
   405   }
   406 
   407   @Override
   408   public void
   409   deleteDocument (String documentId)
   410     throws FileNotFoundException
   411   {
   412     File file, parent;
   413     File[] children;
   414 
   415     /* Java makes recursively deleting a file hard.  File name
   416        encoding issues also prevent easily calling into C...  */
   417 
   418     file = new File (documentId);
   419     parent = file.getParentFile ();
   420 
   421     if (parent == null)
   422       throw new RuntimeException ("trying to delete file without"
   423                                   + " parent!");
   424 
   425     if (file.delete ())
   426       {
   427         /* Tell the system about the change.  */
   428         notifyChange (parent);
   429         return;
   430       }
   431 
   432     children = file.listFiles ();
   433 
   434     if (children != null)
   435       {
   436         for (File child : children)
   437           deleteDocument1 (child);
   438       }
   439 
   440     if (file.delete ())
   441       /* Tell the system about the change.  */
   442       notifyChange (parent);
   443   }
   444 
   445   @Override
   446   public void
   447   removeDocument (String documentId, String parentDocumentId)
   448     throws FileNotFoundException
   449   {
   450     deleteDocument (documentId);
   451   }
   452 
   453   @Override
   454   public String
   455   getDocumentType (String documentId)
   456   {
   457     return getMimeType (new File (documentId));
   458   }
   459 
   460   @Override
   461   public String
   462   renameDocument (String documentId, String displayName)
   463     throws FileNotFoundException
   464   {
   465     File file, newName;
   466     File parent;
   467 
   468     file = new File (documentId);
   469     parent = file.getParentFile ();
   470     newName = new File (parent, displayName);
   471 
   472     if (parent == null)
   473       throw new FileNotFoundException ("parent is null");
   474 
   475     file = new File (documentId);
   476 
   477     if (!file.renameTo (newName))
   478       return null;
   479 
   480     notifyChange (parent);
   481     return newName.getAbsolutePath ();
   482   }
   483 
   484   @Override
   485   public boolean
   486   isChildDocument (String parentDocumentId, String documentId)
   487   {
   488     return documentId.startsWith (parentDocumentId);
   489   }
   490 
   491   @Override
   492   public String
   493   moveDocument (String sourceDocumentId,
   494                 String sourceParentDocumentId,
   495                 String targetParentDocumentId)
   496     throws FileNotFoundException
   497   {
   498     File file, newName;
   499     FileInputStream inputStream;
   500     FileOutputStream outputStream;
   501     byte buffer[];
   502     int length;
   503 
   504     file = new File (sourceDocumentId);
   505 
   506     /* Now, create the file name of the parent document.  */
   507     newName = new File (targetParentDocumentId,
   508                         file.getName ());
   509 
   510     /* Try to perform a simple rename, before falling back to
   511        copying.  */
   512 
   513     if (file.renameTo (newName))
   514       {
   515         notifyChangeByName (file.getParent ());
   516         notifyChangeByName (targetParentDocumentId);
   517         return newName.getAbsolutePath ();
   518       }
   519 
   520     /* If that doesn't work, create the new file and copy over the old
   521        file's contents.  */
   522 
   523     inputStream = null;
   524     outputStream = null;
   525 
   526     try
   527       {
   528         if (!newName.createNewFile ()
   529             || !newName.setWritable (true)
   530             || !newName.setReadable (true))
   531           throw new FileNotFoundException ("failed to create new file");
   532 
   533         /* Open the file in preparation for a copy.  */
   534 
   535         inputStream = new FileInputStream (file);
   536         outputStream = new FileOutputStream (newName);
   537 
   538         /* Allocate the buffer used to hold data.  */
   539 
   540         buffer = new byte[4096];
   541 
   542         while ((length = inputStream.read (buffer)) > 0)
   543           outputStream.write (buffer, 0, length);
   544       }
   545     catch (IOException e)
   546       {
   547         throw new FileNotFoundException ("IOException: " + e);
   548       }
   549     finally
   550       {
   551         try
   552           {
   553             if (inputStream != null)
   554               inputStream.close ();
   555           }
   556         catch (IOException e)
   557           {
   558 
   559           }
   560 
   561         try
   562           {
   563             if (outputStream != null)
   564               outputStream.close ();
   565           }
   566         catch (IOException e)
   567           {
   568 
   569           }
   570       }
   571 
   572     file.delete ();
   573     notifyChangeByName (file.getParent ());
   574     notifyChangeByName (targetParentDocumentId);
   575 
   576     return newName.getAbsolutePath ();
   577   }
   578 }

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