root/java/org/gnu/emacs/EmacsOpenActivity.java

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

DEFINITIONS

This source file includes following definitions.
  1. run
  2. onClick
  3. onCancel
  4. readEmacsClientLog
  5. displayFailureDialog
  6. checkReadableOrCopy
  7. finishSuccess
  8. finishFailure
  9. startEmacsClient
  10. onCreate
  11. onDestroy
  12. onWindowFocusChanged
  13. onPause

     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 /* This class makes the Emacs server work reasonably on Android.
    23 
    24    There is no way to make the Unix socket publicly available on
    25    Android.
    26 
    27    Instead, this activity tries to connect to the Emacs server, to
    28    make it open files the system asks Emacs to open, and to emulate
    29    some reasonable behavior when Emacs has not yet started.
    30 
    31    First, Emacs registers itself as an application that can open text
    32    and image files.
    33 
    34    Then, when the user is asked to open a file and selects ``Emacs''
    35    as the application that will open the file, the system pops up a
    36    window, this activity, and calls the `onCreate' function.
    37 
    38    `onCreate' then tries very to find the file name of the file that
    39    was selected, and give it to emacsclient.
    40 
    41    If emacsclient successfully opens the file, then this activity
    42    starts EmacsActivity (to bring it on to the screen); otherwise, it
    43    displays the output of emacsclient or any error message that occurs
    44    and exits.  */
    45 
    46 import android.app.AlertDialog;
    47 import android.app.Activity;
    48 
    49 import android.content.ContentResolver;
    50 import android.content.DialogInterface;
    51 import android.content.Intent;
    52 
    53 import android.net.Uri;
    54 
    55 import android.os.Build;
    56 import android.os.Bundle;
    57 import android.os.ParcelFileDescriptor;
    58 
    59 import android.util.Log;
    60 
    61 import java.io.File;
    62 import java.io.FileInputStream;
    63 import java.io.FileNotFoundException;
    64 import java.io.FileOutputStream;
    65 import java.io.FileReader;
    66 import java.io.IOException;
    67 import java.io.InputStream;
    68 import java.io.UnsupportedEncodingException;
    69 
    70 public final class EmacsOpenActivity extends Activity
    71   implements DialogInterface.OnClickListener,
    72   DialogInterface.OnCancelListener
    73 {
    74   private static final String TAG = "EmacsOpenActivity";
    75 
    76   /* The name of any file that should be opened as EmacsThread starts
    77      Emacs.  This is never cleared, even if EmacsOpenActivity is
    78      started a second time, as EmacsThread only starts once.  */
    79   public static String fileToOpen;
    80 
    81   /* Any currently focused EmacsOpenActivity.  Used to show pop ups
    82      while the activity is active and Emacs doesn't have permission to
    83      display over other programs.  */
    84   public static EmacsOpenActivity currentActivity;
    85 
    86   private class EmacsClientThread extends Thread
    87   {
    88     private ProcessBuilder builder;
    89 
    90     public
    91     EmacsClientThread (ProcessBuilder processBuilder)
    92     {
    93       builder = processBuilder;
    94     }
    95 
    96     @Override
    97     public void
    98     run ()
    99     {
   100       Process process;
   101       InputStream error;
   102       String errorText;
   103 
   104       try
   105         {
   106           /* Start emacsclient.  */
   107           process = builder.start ();
   108           process.waitFor ();
   109 
   110           /* Now figure out whether or not starting the process was
   111              successful.  */
   112           if (process.exitValue () == 0)
   113             finishSuccess ();
   114           else
   115             finishFailure ("Error opening file", null);
   116         }
   117       catch (IOException exception)
   118         {
   119           finishFailure ("Internal error", exception.toString ());
   120         }
   121       catch (InterruptedException exception)
   122         {
   123           finishFailure ("Internal error", exception.toString ());
   124         }
   125     }
   126   }
   127 
   128   @Override
   129   public void
   130   onClick (DialogInterface dialog, int which)
   131   {
   132     finish ();
   133   }
   134 
   135   @Override
   136   public void
   137   onCancel (DialogInterface dialog)
   138   {
   139     finish ();
   140   }
   141 
   142   public String
   143   readEmacsClientLog ()
   144   {
   145     File file, cache;
   146     FileReader reader;
   147     char[] buffer;
   148     int rc;
   149     StringBuilder builder;
   150 
   151     /* Because the ProcessBuilder functions necessary to redirect
   152        process output are not implemented on Android 7 and earlier,
   153        print a generic error message.  */
   154 
   155     if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O)
   156       return ("This is likely because the Emacs server"
   157               + " is not running, or because you did"
   158               + " not grant Emacs permission to access"
   159               + " external storage.");
   160 
   161     cache = getCacheDir ();
   162     file = new File (cache, "emacsclient.log");
   163     builder = new StringBuilder ();
   164     reader = null;
   165 
   166     try
   167       {
   168         reader = new FileReader (file);
   169         buffer = new char[2048];
   170 
   171         while ((rc = reader.read (buffer, 0, 2048)) != -1)
   172           builder.append (buffer, 0, rc);
   173 
   174         reader.close ();
   175         return builder.toString ();
   176       }
   177     catch (IOException exception)
   178       {
   179         /* Close the reader if it's already been opened.  */
   180 
   181         try
   182           {
   183             if (reader != null)
   184               reader.close ();
   185           }
   186         catch (IOException e)
   187           {
   188             /* Not sure what to do here.  */
   189           }
   190 
   191         return ("Couldn't read emacsclient.log: "
   192                 + exception.toString ());
   193       }
   194   }
   195 
   196   private void
   197   displayFailureDialog (String title, String text)
   198   {
   199     AlertDialog.Builder builder;
   200     AlertDialog dialog;
   201 
   202     builder = new AlertDialog.Builder (this);
   203     dialog = builder.create ();
   204     dialog.setTitle (title);
   205 
   206     if (text == null)
   207       /* Read in emacsclient.log instead.  */
   208       text = readEmacsClientLog ();
   209 
   210     dialog.setMessage (text);
   211     dialog.setButton (DialogInterface.BUTTON_POSITIVE, "OK", this);
   212     dialog.setOnCancelListener (this);
   213     dialog.show ();
   214   }
   215 
   216   /* Check that the specified FILE is readable.  If Android 4.4 or
   217      later is being used, return URI formatted into a `/content/' file
   218      name.
   219 
   220      If it is not, then copy the file in FD to a location in the
   221      system cache directory and return the name of that file.  */
   222 
   223   private String
   224   checkReadableOrCopy (String file, ParcelFileDescriptor fd,
   225                        Uri uri)
   226     throws IOException, FileNotFoundException
   227   {
   228     File inFile;
   229     FileOutputStream outStream;
   230     InputStream stream;
   231     byte buffer[];
   232     int read;
   233     String content;
   234 
   235     Log.d (TAG, "checkReadableOrCopy: " + file);
   236 
   237     inFile = new File (file);
   238 
   239     if (inFile.canRead ())
   240       return file;
   241 
   242     Log.d (TAG, "checkReadableOrCopy: NO");
   243 
   244     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
   245       {
   246         content = EmacsService.buildContentName (uri);
   247         Log.d (TAG, "checkReadableOrCopy: " + content);
   248         return content;
   249       }
   250 
   251     /* inFile is now the file being written to.  */
   252     inFile = new File (getCacheDir (), inFile.getName ());
   253     buffer = new byte[4098];
   254 
   255     /* Initialize both streams to NULL.  */
   256     outStream = null;
   257     stream = null;
   258 
   259     try
   260       {
   261         outStream = new FileOutputStream (inFile);
   262         stream = new FileInputStream (fd.getFileDescriptor ());
   263 
   264         while ((read = stream.read (buffer)) >= 0)
   265           outStream.write (buffer, 0, read);
   266       }
   267     finally
   268       {
   269         /* Note that this does not close FD.
   270 
   271            Keep in mind that execution is transferred to ``finally''
   272            even if an exception happens inside the while loop
   273            above.  */
   274 
   275         if (stream != null)
   276           stream.close ();
   277 
   278         if (outStream != null)
   279           outStream.close ();
   280       }
   281 
   282     return inFile.getCanonicalPath ();
   283   }
   284 
   285   /* Finish this activity in response to emacsclient having
   286      successfully opened a file.
   287 
   288      In the main thread, close this window, and open a window
   289      belonging to an Emacs frame.  */
   290 
   291   public void
   292   finishSuccess ()
   293   {
   294     runOnUiThread (new Runnable () {
   295         @Override
   296         public void
   297         run ()
   298         {
   299           Intent intent;
   300 
   301           intent = new Intent (EmacsOpenActivity.this,
   302                                EmacsActivity.class);
   303 
   304           /* This means only an existing frame will be displayed.  */
   305           intent.addFlags (Intent.FLAG_ACTIVITY_REORDER_TO_FRONT);
   306           startActivity (intent);
   307 
   308           EmacsOpenActivity.this.finish ();
   309         }
   310       });
   311   }
   312 
   313   /* Finish this activity after displaying a dialog associated with
   314      failure to open a file.
   315 
   316      Use TITLE as the title of the dialog.  If TEXT is non-NULL,
   317      display that text in the dialog.  Otherwise, use the contents of
   318      emacsclient.log in the cache directory instead, or describe why
   319      that file cannot be read.  */
   320 
   321   public void
   322   finishFailure (final String title, final String text)
   323   {
   324     runOnUiThread (new Runnable () {
   325         @Override
   326         public void
   327         run ()
   328         {
   329           displayFailureDialog (title, text);
   330         }
   331       });
   332   }
   333 
   334   public void
   335   startEmacsClient (String fileName)
   336   {
   337     String libDir;
   338     ProcessBuilder builder;
   339     Process process;
   340     EmacsClientThread thread;
   341     File file;
   342     Intent intent;
   343 
   344     /* If the Emacs service is not running, then start Emacs and make
   345        it open this file.  */
   346 
   347     if (EmacsService.SERVICE == null)
   348       {
   349         fileToOpen = fileName;
   350         intent = new Intent (EmacsOpenActivity.this,
   351                              EmacsActivity.class);
   352         finish ();
   353         startActivity (intent);
   354         return;
   355       }
   356 
   357     libDir = EmacsService.getLibraryDirectory (this);
   358     builder = new ProcessBuilder (libDir + "/libemacsclient.so",
   359                                   fileName, "--reuse-frame",
   360                                   "--timeout=10", "--no-wait");
   361 
   362     /* Redirection is unfortunately not possible in Android 7 and
   363        earlier.  */
   364 
   365     if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O)
   366       {
   367         file = new File (getCacheDir (), "emacsclient.log");
   368 
   369         /* Redirect standard error to a file so that errors can be
   370            meaningfully reported.  */
   371 
   372         if (file.exists ())
   373           file.delete ();
   374 
   375         builder.redirectError (file);
   376       }
   377 
   378     /* Track process output in a new thread, since this is the UI
   379        thread and doing so here can cause deadlocks when EmacsService
   380        decides to wait for something.  */
   381 
   382     thread = new EmacsClientThread (builder);
   383     thread.start ();
   384   }
   385 
   386   /* Run emacsclient to open the file specified in the Intent that
   387      caused this activity to start.
   388 
   389      Determine the name of the file corresponding to the URI specified
   390      in that intent; then, run emacsclient and wait for it to finish.
   391 
   392      Finally, display any error message, transfer the focus to an
   393      Emacs frame, and finish the activity.  */
   394 
   395   @Override
   396   public void
   397   onCreate (Bundle savedInstanceState)
   398   {
   399     String action, fileName;
   400     Intent intent;
   401     Uri uri;
   402     ContentResolver resolver;
   403     ParcelFileDescriptor fd;
   404     byte[] names;
   405     String errorBlurb;
   406 
   407     super.onCreate (savedInstanceState);
   408 
   409     /* Obtain the intent that started Emacs.  */
   410     intent = getIntent ();
   411     action = intent.getAction ();
   412 
   413     if (action == null)
   414       {
   415         finish ();
   416         return;
   417       }
   418 
   419     /* Now see if the action specified is supported by Emacs.  */
   420 
   421     if (action.equals ("android.intent.action.VIEW")
   422         || action.equals ("android.intent.action.EDIT")
   423         || action.equals ("android.intent.action.PICK"))
   424       {
   425         /* Obtain the URI of the action.  */
   426         uri = intent.getData ();
   427 
   428         if (uri == null)
   429           {
   430             finish ();
   431             return;
   432           }
   433 
   434         /* Now, try to get the file name.  */
   435 
   436         if (uri.getScheme ().equals ("file"))
   437           fileName = uri.getPath ();
   438         else
   439           {
   440             fileName = null;
   441 
   442             if (uri.getScheme ().equals ("content"))
   443               {
   444                 /* This is one of the annoying Android ``content''
   445                    URIs.  Most of the time, there is actually an
   446                    underlying file, but it cannot be found without
   447                    opening the file and doing readlink on its file
   448                    descriptor in /proc/self/fd.  */
   449                 resolver = getContentResolver ();
   450                 fd = null;
   451 
   452                 try
   453                   {
   454                     fd = resolver.openFileDescriptor (uri, "r");
   455                     names = EmacsNative.getProcName (fd.getFd ());
   456 
   457                     /* What is the right encoding here? */
   458 
   459                     if (names != null)
   460                       fileName = new String (names, "UTF-8");
   461 
   462                     fileName = checkReadableOrCopy (fileName, fd, uri);
   463                   }
   464                 catch (FileNotFoundException exception)
   465                   {
   466                     /* Do nothing.  */
   467                   }
   468                 catch (IOException exception)
   469                   {
   470                     /* Do nothing.  */
   471                   }
   472 
   473                 if (fd != null)
   474                   {
   475                     try
   476                       {
   477                         fd.close ();
   478                       }
   479                     catch (IOException exception)
   480                       {
   481                         /* Do nothing.  */
   482                       }
   483                   }
   484               }
   485 
   486             if (fileName == null)
   487               {
   488                 errorBlurb = ("The URI: " + uri + " could not be opened"
   489                               + ", as it does not encode file name inform"
   490                               + "ation.");
   491                 displayFailureDialog ("Error opening file", errorBlurb);
   492                 return;
   493               }
   494           }
   495 
   496         /* And start emacsclient.  Set `currentActivity' to this now.
   497            Presumably, it will shortly become capable of displaying
   498            dialogs.  */
   499         currentActivity = this;
   500         startEmacsClient (fileName);
   501       }
   502     else
   503       finish ();
   504   }
   505 
   506 
   507 
   508   @Override
   509   public void
   510   onDestroy ()
   511   {
   512     Log.d (TAG, "onDestroy: " + this);
   513 
   514     /* Clear `currentActivity' if it refers to the activity being
   515        destroyed.  */
   516 
   517     if (currentActivity == this)
   518       this.currentActivity = null;
   519 
   520     super.onDestroy ();
   521   }
   522 
   523   @Override
   524   public void
   525   onWindowFocusChanged (boolean isFocused)
   526   {
   527     Log.d (TAG, "onWindowFocusChanged: " + this + ", is now focused: "
   528            + isFocused);
   529 
   530     if (isFocused)
   531       currentActivity = this;
   532     else if (currentActivity == this)
   533       currentActivity = null;
   534 
   535     super.onWindowFocusChanged (isFocused);
   536   }
   537 
   538   @Override
   539   public void
   540   onPause ()
   541   {
   542     Log.d (TAG, "onPause: " + this);
   543 
   544     /* XXX: clear currentActivity here as well; I don't know whether
   545        or not onWindowFocusChanged is always called prior to this.  */
   546 
   547     if (currentActivity == this)
   548       currentActivity = null;
   549 
   550     super.onPause ();
   551   }
   552 }

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