I switched to Android a few weeks ago, and being an Emacs user, I immediately installed and started tweaking Emacs’ new Android port (specifically the version which shares a UID with Termux, as uploaded by Po Lu to SourceForge).

Many things didn’t work out of the box, but I got large parts of my setup working under Android now, so I thought I’d share some stuff since I found little to no information online about any of this.

Running programs installed through Termux

This is covered well by the FAQ on SourceForge, but it seems to give some wrong advice for users of Android 7+: Setting LD_LIBRARY_PATH on these devices likely causes failures to link some executables installed through Termux.

I’m not sure what other ramifications doing this has, but I have been able to run all Termux programs (with a single exception, git-annex) through Emacs this way. To use git-annex through Emacs I run tmux on Termux, and connect to it from an Emacs terminal buffer. This could be somewhat automated using the ‘am’ command. git-annex can be used through Emacs on Android 10+ by disabling Emacs’ default behavior of running executables under system call tracing. You need to ensure every directory containing executable files is read only though (this can be done through emacs using (set-file-modes DIR 320)). It actually works for me without setting directories read-only (contrary to the FAQ).

‘info’ and ‘man’

You can use Emacs’ built-in viewers for info manuals and manpages by setting INFOPATH and MANPATH appropriately. When setting INFOPATH, add the directory “/assets/info” otherwise you won’t be able to see Emacs’ manuals.

SSH and GPG Agents

After installing GPG through Termux, set ‘epg-pinentry-mode’ to ‘loopback’ to prompt for the GPG passphrase through Emacs. The agent should start automatically, I didn’t do anything special other than this.

The SSH agent is a bit more tricky - it seems to not cleanup after itself when Termux and Emacs close, and for some reason doesn’t appear in the process list through Emacs when launched through Termux.

For these reasons, I decided to launch a new agent whenever I launch Emacs, and configure Termux to connect to it, or start a new agent only if the socket file doesn’t exist at all:

.profile:

export SSH_AUTH_SOCK="${SSH_AUTH_SOCK:-$HOME/.ssh/auth.socket}"

if [ ! -S "$SSH_AUTH_SOCK" ]; then
    eval "$(ssh-agent -a "$SSH_AUTH_SOCK")" > /dev/null 2>&1
    echo "$SSH_AGENT_PID" > "$HOME/.ssh/pid"
fi

export SSH_AGENT_PID="${SSH_AGENT_PID:-$(cat "$HOME/.ssh/pid")}"

profile.el:

(let* ((sr (expand-file-name ".ssh" "~"))
       (ss (setenv "SSH_AUTH_SOCK" (expand-file-name "auth.socket" sr)))
       p)
  (unless (file-exists-p sr) (make-directory sr t))
  (delete-file (expand-file-name "pid" sr))
  (delete-file ss)
  (with-temp-buffer
    (call-process "ssh-agent" nil (current-buffer) nil "-a" ss)
    (goto-char (point-min))
    (forward-line)
    (search-forward "SSH_AGENT_PID=")
    (setq p (point))
    (goto-char (line-end-position))
    (search-backward "; export SSH_AGENT_PID;")
    (setenv "SSH_AGENT_PID" (buffer-substring-no-properties p (point)))
    (write-region p (point) (expand-file-name "pid" sr))))

And, as a bonus, here’s a pair of functions I use to prompt for the passphrase to an SSH key through Emacs when pulling/pushing Git repositories with VC: (it tries to use a graphical askpass program by default, which isn’t possible on Android)

(defun init-interfaces-ssh-add (keys)
  "Attempt to add SSH KEYS to the agent interactively."
  (dolist (key (cond ((and keys (atom keys)) (list keys))
                     ((length= keys 1) keys)
                     (t (completing-read-multiple "SSH Keys: " keys))))
    (when-let ((key (expand-file-name key "~/.ssh"))
               ((file-readable-p key)))
      (with-temp-buffer
        (insert-file-contents (concat key ".pub"))
        (call-process "ssh-add" nil (current-buffer) nil "-L")
        (unless (search-backward
                 (buffer-substring-no-properties (point) (line-end-position))
                 nil t)
          (async-shell-command (format "ssh-add %s" key)
                               (current-buffer))
          (setq key (get-buffer-process (current-buffer)))
          (while (accept-process-output key)))))))

(defun init-interfaces-vc-add-ssh-key (&rest _)
  "Ensure the SSH key of the current repository has been added to the agent."
  (when-let ((root (vc-git-root default-directory))
             (remote (vc-git-repository-url root))
             ((string-match-p "^\\(\\(rsync\\|ssh\\)://\\)?\\([[:alnum:]][[:alnum:]._-]+@\\)?[[:alnum:]+.-]+:.*" remote)))
    (init-interfaces-ssh-add
     (seq-remove (lambda (s) (string-match-p ".pub\\'" s))
                 (directory-files
                  (expand-file-name "~/.ssh") nil
                  (format ".*\\(git\\.\\)?%s.*\\'"
                          (string-trim-left
                           (cadr (string-split remote "[@:]"))
                           "git\\.")))))))

(advice-add 'vc-git--pushpull :before 'init-interfaces-vc-add-ssh-key)

Using Termux:API from Emacs

If you install Po Lu’s version of Termux (which is just Termux signed with Emacs’ key) you won’t be able to install Termux:API from F-Droid, as it needs to be signed by the same key used to sign Termux.

You can use ‘jarsigner’ and ‘zipalign’ to resign the APK (get the keystore from Emacs’ source repository under the ‘java’ directory, use the password ‘emacs1’ with alias ‘Emacs keystore’).

Also, when running ‘emacsclient’ through Termux (whether directly or through the API app) you need to ensure Termux can find the server socket.

The easiest way to do this is to set TMPDIR to Emacs’ TMPDIR (“/data/data/org.gnu.emacs/cache” by default).

Here is an example of using ‘termux-notification’ to display an interactive media notification for EMMS:

(defun init-emms-termux-notify-stopped ()
  "Remove EMMS' Android notification."
  (start-process "init-emms-notification-remove" nil
                 "termux-notification-remove" "emms"))

(add-hook 'emms-player-stopped-hook
          #'init-emms-termux-notify-stopped)

(defun init-emms-termux-notify-killed ()
  "Remove EMMS' Android notification (synchronously)."
  (call-process "termux-notification-remove" nil nil nil "emms"))

(add-hook 'kill-emacs-hook #'init-emms-termux-notify-killed)

(defun init-emms-termux-notify-started ()
  "Display a media notification for EMMS through Termux:API."
  (let ((track (emms-playlist-current-selected-track))
        (tmp (getenv "TMPDIR"))
        (client (executable-find emacsclient-program-name)))
    (start-process "init-emms-notification" nil "termux-notification"
                   "--id" "emms" "--alert-once" "--priority" "max"
                   "--on-delete"
                   (format "TMPDIR=%s %s --eval '(emms-stop)'" tmp client)
                   "--title" (emms-track-get track 'info-title)
                   "--content" (format "%s - %s"
                                       (emms-track-get track 'info-artist)
                                       (emms-track-get track 'info-album))
                   "--image-path"
                   (if (eq (emms-track-type track) 'file)
                       (expand-file-name "cover_med.png"
                                         (file-name-directory
                                          (emms-track-name track)))
                     (expand-file-name
                      (locate-user-emacs-file "images/emms.png")))
                   "--type" "media" "--media-next"
                   (format "TMPDIR=%s %s --eval '(emms-next)'"
                           tmp client)
                   "--media-previous"
                   (format "TMPDIR=%s %s --eval '(emms-previous)'"
                           tmp client)
                   "--media-pause"
                   (format "TMPDIR=%s %s --eval '(emms-pause)'"
                           tmp client)
                   "--media-play"
                   (format "TMPDIR=%s %s --eval '(emms-pause)'"
                           tmp client))))

(add-hook 'emms-player-started-hook #'init-emms-termux-notify-started)

Performing actions with the touchscreen

First of all, the following snippet will add a toggle for the on-screen keyboard to the Menu-bar -> Options -> Show/Hide menu:

(easy-menu-add-item nil '("Options" "Show/Hide")
                    ["Keyboard" (if touch-screen-display-keyboard
                                    (setopt touch-screen-display-keyboard nil)
                                  (setopt touch-screen-display-keyboard t))
                       :visible (eq system-type 'android)
                       :style toggle
                       :selected touch-screen-display-keyboard])

Other than that, to bind commands to a touchscreen ‘tap’, use “mouse-1” (which is translated automatically from the touchscreen events), and for a ‘hold’ use “touchscreen-hold”.

Prefer using ESC over Alt if possible, since Android doesn’t seem to support many Alt+ combinations (for example, M-:, ‘eval-expression’). This is possibly only an issue with software keyboards (I haven’t tested with a hardware keyboard, I use Emacs with the Unexpected Keyboard).

Other stuff

Enable ‘confirm-kill-emacs’ to get a popup allowing you to prevent Emacs from being killed by the system in the background.

Sending mail using Emacs’ built-in smtpmail package doesn’t work for me for some reason; I suspect this is solvable but I haven’t figured it out yet. Termux doesn’t seem to include a ‘sendmail’ (but I haven’t looked too hard) so currently I have to use a separate app to send mail from my phone (oh no). This works, my configuration was wrong.

I made one attempt to compile Jinx’ dynamic module and gave up when it failed. I highly suspect this is also possible. I got around to trying Jinx again and it just worked.

Many commands which read a character fail on Android since in most situations the ‘character’ returned will be ‘text-conversion’. Every few times I update the app I notice a new command which now works correctly, so it seems there is an ongoing effort to fix these commands. I will say though, my own package Window Commander doesn’t really work on Android due to this and I tried very hard to make it work recently, so don’t be surprised by issues in external packages (as most probably aren’t even considering this yet).

Most of the code in this post (or code similar to it) can be found in my repository: https://git.sr.ht/~dsemy/emacs-config

  • cinerion@lemmy.world
    link
    fedilink
    arrow-up
    1
    ·
    4 months ago

    Does magit run normally for you? I run emacs on my phone but anytime I have to use git it gets unbearably slow. This includes magit, so i have it unloaded usually.

    Same for the vc package bundled with emacs, i had to include this on my config or files take too long to open:

    (when (eq system-type 'android)
      (setq-default vc-handled-backends nil)
    

    Also, what do you mean by this and how do you do it?

    disabling Emacs’ default behavior of running executables under system call tracing

    • dsemyOP
      link
      fedilink
      English
      arrow-up
      2
      ·
      4 months ago

      I don’t use Magit but I do use VC all the time on my phone and I didn’t encounter this issue.

      Also, what do you mean by this and how do you do it?

      Set ‘android-use-exec-loader’ to nil. This is a workaround used by Emacs past a certain version of Android to run executables. From my testing this workaround isn’t necessary, but it’s there for a reason.

      • cinerion@lemmy.world
        link
        fedilink
        arrow-up
        1
        ·
        edit-2
        4 months ago

        Set ‘android-use-exec-loader’ to nil

        Oh, that did the trick! This reduced my magit-status load times from 15s to 0.7s and now I can reenable VC. Thanks!

  • anabasisxu@lemmy.world
    link
    fedilink
    arrow-up
    1
    ·
    5 months ago

    You can use ‘jarsigner’ and ‘zipalign’ to resign the APK (get the keystore from Emacs’ source repository under the ‘java’ directory, use the password ‘emacs1’ with alias ‘Emacs keystore’).

    Sorry I am very new to the android world. Could you please elaborate on signing termux api with Emacs keystore?

    • dsemyOP
      link
      fedilink
      English
      arrow-up
      2
      ·
      5 months ago

      Open the APK file (it can be opened as a zip file, just change the extension) and delete the META-INF directory (and then change the extension back to “.apk”).

      Get the Emacs keystore file from: https://git.savannah.gnu.org/cgit/emacs.git/plain/java/emacs.keystore

      Sign the (now unsigned) APK (output is an “unaligned” APK):

      jarsigner -sigalg MD5withRSA -digestalg SHA1 -keystore emacs.keystore -storepass emacs1 -signedjar unaligned.apk unsigned.apk "Emacs keystore"
      

      Align it:

      zipalign -pf 4 unaligned.apk signed.apk
      

      Then install “signed.apk” however you want.