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