config browser default). By default on
Android a small app of ours shows the same pages in Android’s WebView, without a browser:
BROWSER_AGNOSTIC_APK.md.
Claims marked (tested) were checked on the lab VM b16 on 2026-10-27:
- BlissOS 16 (Android 13), with Termux 0.118.3 from F-Droid.
- Browsers: Cromite, Firefox 158 and Jellyfish.
- The probes and raw logs are in
lab/browser-window/. lab/test-lindroids.sh b16passes with SELinux permissive and enforcing (lab/README.md).
What is wanted
- Everything lives inside the container: data, programs, the relay and the window’s page. The container is Termux’s prefix, a proot distro or a Flatpak. One package, installed there.
- Nothing else on Android:
- no root;
- no APK required (the browser path; the window app is now the default on Android);
- no Termux setting (
allow-external-apps); - no special permission;
- nothing that depends on an Android version or vendor.
- Every program gets a window of its own: not a tab, no browser interface, not something installed through the browser. Lindroids opens and manages it.
- The program’s icon is used.
- Single run:
lindroids run Xopens X’s window. The window and the program end together. - Export:
lindroids export Xputs X on the home screen. Tapping it works even when Termux is not running.
What Android allows
On Android, every window on screen is an Activity, and every Activity belongs to an installed app. A shortcut is only an intent and draws nothing. So some installed app draws the window. Without an APK of ours, that leaves Termux, which is installed, and the browser, which every phone has. Everything else was tried:The window: a Custom Tab, opened by Termux
A Custom Tab is the browser’s own mechanism for showing a web page as part of another app. Any app opens one with an ordinary intent that carries a Custom Tabs session extra; no permission and no setting are involved.am cannot put that extra (a Binder) into an intent, so Lindroids sends
it from its app_process helper, as Termux, the way export pins shortcuts
(crates/lindroids-cli/shortcut/Shortcut.java, open; first tried with
lab/browser-window/Open.java).
(tested with Firefox)
- Its own window. With
NEW_TASK | NEW_DOCUMENT, every Custom Tab for another URL is a task of its own (ExternalAppBrowserActivity). Each run has its own URL, so each program gets its own window, side by side in recents. - No browser interface after the first tap. The window shows only a thin bar: close, title, menu. No tabs, no address bar. On the first tap, the page asks for full screen and gets it: nothing of the browser is left, not even the status bar. Firefox briefly shows “Drag from top & use back gesture to exit”.
- Raising a window. Sending the same intent again (the URL it opened with, ticket and all; no
MULTIPLE_TASK) brings that window’s task to the front. Chromium does not: its Custom Tabs always take a new task, so a second window opens, takes over the run, and the old one closes itself (tested with Cromite). Firefox also loads a second copy of the page nobody sees, which the relay tells to stand down (see The handshake). - Closing by the user. The close button, or swiping the window from recents, gives the
relay a clean
pagehideand WebSocket close 1001, and the task goes away. - Back does not close it. The browser takes the window out of full screen on Back and closed
it on the next one, and its program ended: on a phone, Back is a swipe in from the screen’s
edge, which users make to leave, and by accident while using the program (reproduced on b16:
Back twice from full screen, the program ended 5 s later). While its program runs, the page
takes Back (
CloseWatcher, Chrome 120 and later) and shows once in a while that Home or the app switcher leaves the program running and ✕ closes it. When the program has ended, Back closes the window again (tested, Cromite: Back three, four and six times, fast or slow, the window and the program stay;lab/test-windows.sh). Chrome may let a Back through when the page was not touched since the last one; that closes the window as before. - Closing by the program: Chromium yes, Firefox no. In Chromium’s Custom Tabs the page’s
window.close()ends the window when the program exits (tested with Cromite). It is ignored in a Firefox Custom Tab, even a fresh one, and nothing outside Firefox may finish its activity: there the window shows the program’s end screen, and Back closes it.
Lindroids tries, in order:
- the browsers named in
LINDROIDS_BROWSER(comma-separated packages); - the default browser, if it is one of these and not Firefox;
- Chrome, Edge, Brave, Vivaldi, Samsung Internet, Chrome Beta, Chromium, Cromite, then Firefox and its beta and nightly builds. Chromium’s windows show the program in Recents and close themselves; Firefox’s cannot (below), so it comes last even as the default.
WINDOW_BROWSERS in
crates/lindroids-cli/src/shortcut.rs). Custom Tabs are not taken for granted: the helper starts
the window with startActivityAndWait and reads which activity the browser opened. A browser
that opens an ordinary tab instead (Cromite without Allow custom tab intents, WebView-based
browsers) is passed over; the page in that tab closes itself when the window in the next browser
links, because each attempt’s URL names its browser (&via=<package>) and the server keeps the
newer browser’s page (tested: Cromite with the setting off → a Firefox window, the Cromite tab
gone). If no browser opens a window, the first ordinary tab is kept.
That answer used to be lost: Android 11+ makes every process sample the app ops its calls note,
puts them before the reply, and reading them throws outside an app (SyncNotedAppOp). The
helper turns the sampling off (AppOpsManager.sConfig, a config that never expires) and gets
whole replies.
Full screen: from the first input, not before (tested 2026-10-28, b16)
A window opens with the browser’s thin bar and Android’s status and navigation bars, and takes the whole screen on the user’s first input. Nothing tried makes it full screen before that, in Cromite 153 or Firefox 158 Custom Tabs, opened from Termux’s uid like Lindroids’ windows (lab/browser-window/fsprobe.py for the page, FsOpen.java for the intent, lab/test-fullscreen.sh
for Lindroids’ own windows):
So the page asks on the first input browsers allow, anywhere in the window except its own
dialogs and toasts: a touch’s end (
pointerup; touchend and click as fallbacks), a mouse
button’s press, a plain key on a hardware keyboard. A refused request is asked again on the next
input, and after the window comes back on screen. The page traces “opened out of full screen” and
which input took it (“full screen asked on pointerup”) into the server’s log (lab/test-fullscreen.sh
on b16 with the page from before this change: Cromite and Firefox open out of full screen, one tap
in the middle takes it). A tap on the browser’s own bar does not reach the page. Not verified
here: Chrome and Samsung Internet (no Play Store on the lab images), and phones in portrait,
where Chromium’s bar may hide on a finger scroll.
The program’s icon
The Recents icon on One UI: every route, and why none works without an APK
Re-examined 2026-10-28 (lab/test-recents.sh, b16 with Launcher3, Cromite 153, Firefox 158;
neither Chrome nor One UI is in the lab), and again from the terminal app’s side
(ROUTES_ONLY=1 lab/test-recents.sh, lab/browser-window/Route.java: run with the terminal
app’s uid, it takes the package from the uid, so it applies as it is to Termux’s forks or any other
terminal app). The terminal app is an installed package, but outside its own process it cannot act
as one in the activity manager: what decides a card (the task’s root, the TaskDescription) is open
only to a process Android started for the app (an IApplicationThread) or to an activity’s token,
and its app_process helper has neither.
A launcher draws a card from what Android keeps for the task: the TaskDescription (label, icon)
its activities set, and the components it runs (root and top activity). It can draw either the
TaskDescription icon or an installed component’s icon; nothing else is linked to a task (a pinned
shortcut’s icon is not). One UI draws the program’s name on Chrome’s windows (reported), and
that name comes from the same TaskDescription, set in the same call as the icon by
CustomTabTaskDescriptionHelper. So One UI most likely gets the program’s icon and draws the root
activity’s app icon instead (an inference: One UI’s launcher is closed source and not in the lab). Chromium hit the same thing with WebAPKs: “the icon shown in the recents is the
application icon of the root activity in the task stack”, so it made the WebAPK’s own activity the
task’s root (Chromium change 2762873003, 2017). Under One UI only the package at the root of the
task decides the icon, which means an installed package with the program’s icon.
The parts
-
The relay (
lindroids server):- One per uid, started on demand, detached (
setsid). - Serves the page and the WebSocket on loopback.
- Composites windows, plays sound and answers the desktop portal.
- Keeps the launches (what
runstarted, by pid), the tickets, and one link per page. - Stops by itself if its control socket disappears: Termux empties its tmp dir when it starts again, and an unreachable relay would hold the port for good.
- One per uid, started on demand, detached (
-
A run is one start of one program. It consists of:
- the supervisor;
- the program and everything under it;
- its Wayland and X11 windows;
- the one Custom Tab that shows them.
-
The supervisor is
lindroids run(single run) orlindroids launch(exported program).- It is the program’s parent and waits for it.
- It holds the program’s stdio: the terminal for
run,<runtime>/launch-<id>.logforlaunch. - It never carries pixels.
- It exits with the program’s status, or 128 + the signal number if the program was killed.
-
The window is the page at
/?window&run=<pid>, in a Custom Tab of its own. The program’s toplevels and dialogs are drawn inside it, one tab each in the page’s own tab bar. Programs started on a desktop, or where no browser has Custom Tabs, share the plain page (/) as before.
Stdio or a process?
The program is an ordinary process, not a stream.run forks it with its environment pointing at
the relay’s sockets in the runtime dir: WAYLAND_DISPLAY, DISPLAY, PULSE_SERVER and
DBUS_SESSION_BUS_ADDRESS. It is not moved to a process group of its own, so one that reads the
terminal does not stop on SIGTTIN. Its stdin, stdout and stderr stay where a desktop would put
them:
- For
run, the terminal, solindroids run gimpbehaves likegimp. - For
launch, a log file.
icons.rs, x11.rs):
- A Wayland client belongs to the launch it descends from. The relay starts from the client’s
pid (
SO_PEERCRED) and walks up/proc/<pid>/stat. This works under proot, whose tracees are real processes (tested: Xournal++ in a proot Debian). - An X11 window comes through xwayland-satellite, a single client of the relay. For each of
satellite’s toplevels, the relay asks Xwayland which X client owns the window with that title
and class (X-Resource,
XResQueryClientIds), and walks up from that pid (tested: xclock got its own window). This runs on a thread with a 500 ms limit, because Xwayland reaches the compositor through satellite. It is only asked about satellite’s windows, so asking never starts X11. - A Flatpak that asks for X11 and not Wayland (
sockets=x11in its metadata) runs the way flatpak runs it: on the X display only, with SDL, GTK and Qt set to X11 and noWAYLAND_DISPLAY. Some can only draw with X11: SDL 1.2 has no Wayland driver, so underSDL_VIDEODRIVER=waylandit stopped with “No available video device”. Without the integrated X display (Xwayland not installed: Termux’s package only recommends it, from the x11 repository) the run offers to install it in its terminal (lindroids setup x11), restarts the server and goes on; without a terminal it says so (tested: Tux Typing on b15, Android 12L).lindroids exportasks the same when it exports such a program, so its icon does not fail later. A launch with no terminal (its page asked the server) hands itself to Termux the way the icon does (the launch URL shared with Termux runslindroids launchin a session), where it asks. - No video mode switches. The X display has only the modes of the window it draws in. SDL
1.2 switched to its own full-screen mode with XF86VidMode, and a mode the display lacks is an X
error that ends the program: Tux Typing stopped with
BadValueonXF86VidModeSwitchToModeon a 1200x1920 portrait tablet (reproduced on b15 at that size). Programs on the integrated display getSDL_VIDEO_X11_VIDMODE=0(SDL 1.2) andSDL_VIDEO_X11_XVIDMODE=0(SDL 2): full screen is the window’s own size, which Lindroids scales anyway (tested: Tux Typing’s menu and a game at 1200x1920 and 1280x800). - Termux from Google Play runs every program in its storage through the system linker
(
/system/bin/linker64 <file>; termux-exec’sTERMUX_EXEC__SYSTEM_LINKER_EXEC__MODE=forcedoes the same on another Termux)./proc/self/exeis then the linker, so a program that runs itself again through it hands the linker its first argument: Lindroids’ own server did not start (“error: expected absolute path: “server""), and proot’s helper for SysV shared memory printederror: expected absolute path: "--shm-helper", after which everyshmgetin proot fails. Lindroids finds its own file inTERMUX_EXEC__PROC_SELF_EXEthere, and Xwayland runs without MIT-SHM, so X programs do not try shared memory (tested on b15 with the mode forced: Tux Typing, xclock, foot and GNOME Calculator, andexport’s launcher). - A window that appears before its launch is registered moves to its run’s window once it is.
The origin
The page is served athttp://<label>.localhost:<port>/:
<port>is fixed per uid (server_tcp_port: 47000 + the app id’s offset).<label>is 26 base32 characters (130 random bits), made once and kept in~/.local/share/lindroids/origin(0600).
.localhost name:
- Every browser treats it as loopback and as a secure context (tested). Cromite, Firefox and
Jellyfish all resolve
k7f3q9.localhostto 127.0.0.1, reportisSecureContext, and offer WebCrypto. No TLS is needed. - The origin becomes a capability. Browser storage belongs to an origin, which includes the
port, and any app can bind a free loopback port. With the plain
127.0.0.1:<port>origin, an app that binds the port while the relay is down and sends the browser there runs its own page in our origin. It can then read or use what the page stored. With an unguessable label, it can only serve an origin of its own. - Anything without the label gets nothing, not even the label: the relay answers
421 wrong host(tested:curl 127.0.0.1:<port>).- It answers only requests whose
Hostis exactly<label>.localhost:<port>(case aside). - It upgrades a WebSocket only if its
Originis exactlyhttp://<label>.localhost:<port>. - Web sites and DNS rebinding are kept out by the same checks.
- It answers only requests whose
The bridge and its security
Transport: no TLS, no message encryption
The link is HTTP/1.1, plus one WebSocket per window, over loopback TCP.- TLS is not available. For a local name, the browser would accept only a certificate from a CA the user installs in Android’s settings, which is a special setting.
- TLS would protect against nobody. Reading loopback traffic or injecting into an established loopback connection takes root, and root or adb can already read Termux’s files and keys.
- Encrypting each message in the page is rejected for the same reason. AES-GCM over WebCrypto is possible, but it costs CPU on every frame and adds no protection.
Threat model
What is left: such an app’s page could link as the shared page (
/), which shows what was
started without a window of its own. On Android every run has its own window, so that is nothing.
On a desktop, it is the programs shown in the browser tab.
Keys and tokens
Why P-256 and not Ed25519: every browser with WebCrypto has ECDSA P-256. Ed25519 arrived only
recently. On the wire, public keys are uncompressed SEC1 points and signatures are r‖s (what
WebCrypto makes), both base64url. The Rust side is RustCrypto’s
p256.
Why key pairs instead of the shared HMAC secret Lindroids used before:
- The display’s private key never exists in readable form, not even for script in our own origin.
- The relay keeps only public keys, so its files can’t be used to pose as a display.
- No long-lived secret travels in URLs.
- Each browser is listed and forgotten on its own (
lindroids displays [forget <id>]).
- The CLI asks the relay for a ticket over the control socket. The socket lives in the 0700
runtime dir, and the relay also checks the caller’s uid with
SO_PEERCRED. - The CLI puts the ticket in the URL fragment. The helper sends that URL in an intent addressed to the browser package, which other apps cannot see.
- Browsers never send the fragment over HTTP. The page removes it from the address bar at once
with
history.replaceState, and keeps it in that history entry’s state: a page the browser loads again (after dropping it in the background, or a reload) attaches to its run with it. No other page can read another window’s history state.
The handshake (tested; protocol 5)
These are the first messages on every WebSocket (lindroids-proto: ToServer, ToApp, auth.rs).
origin is the page’s origin (empty for debug-app, which is not a page), and ‖ stands for a NUL
separator.
Bye (tested). This is because bringing a window back sends its
intent again, and Firefox then raises the old window and loads a second copy nobody sees. The
shared page keeps “newest wins”.
Lifecycles
Run states
- The relay asks the run’s windows to close (
xdg_toplevel.close), as a desktop’s close button does: a program saves its state then (Electron’s SIGTERM handler does not wait for that). - 3 seconds later, the relay sends SIGTERM to the roots of the run’s process tree, seen through proot (proot does not pass SIGTERM on: its tracee, the program, gets it), so that a program’s main process ends its children itself. Before, every process got SIGTERM, children first, and Electron’s renderers died before its main process could save.
- The relay sends SIGKILL to whatever is left 5 seconds later, every process it saw in the tree. A program under proot is proot’s tracee and outlives proot otherwise (tested: Xournal++ was left running before this).
- The supervisor’s
waitreturns, and it exits with 128 + the signal number (0 if the program closed when asked).
lindroids stop), 2 s and 3 s apart.
When the program goes (and only then: a program with no window open for a while, a dialog in
its place or a window it hides, still runs, and its window says so):
- The supervisor (
lindroids run, a subreaper: it adopts what the program detached) ends what the program left behind: SIGTERM to the roots of what is left below it, SIGKILL 3 s later. Left running, a detached GUI held on to what the next launch needed: VS Code’s single-instance socket handed that launch to it, and its window went to another run’s page (seen on the tablet: VSCodium’s whole tree running, no window anywhere). Not while another run shows a window of it (a second VSCodium handed its work to it, and claimed the window it opened): those windows count as the run’s, and what it left ends with the last such run. - The supervisor tells the server (
RunEnded) and exits with its status. A launcher that detached its GUI and exited (VS Code’s CLI) is followed to the GUI’s end: its processes end, or it has had no window at all for 5 seconds (a window it closes to open another is not its end). - The server ends any process of the run that is still there: it notes the run’s processes every 2 seconds while it runs, so a GUI is found even when its supervisor was killed (Android’s phantom process killer) and the run is lost. Before the next launch, whatever of an ended run is still there gets SIGKILL.
- The page shows the program’s icon and “
<name>closed. The window closes with the program.” - It also calls
window.close()after 1.5 s, in case a splash window gave way to the real one. Browsers that allow it close the window; Firefox’s Custom Tabs ignore it. In a Lindroids window app the page closes its task (lindroids.close()), and the server has the app close it too (lindroids window-end <run>: anorg.lindroids.ENDbroadcast to that app alone, with the run’s ticket), for a page that cannot: frozen in the background, or its process gone while its card stays in Recents. A card left by a run that ended anyway goes when the next run’s window opens (the app asks the server which of its cards’ runs still run, by their tickets).
What the relay can see (tested)
The grace period. A lost connection is ambiguous: the close code does not tell a closed window
from a killed page (Chromium sends 1001 for both). What does is whether the page was on screen:
the page tells the relay when it leaves the screen and comes back (
Shown).
- Out of sight for 3 s or more, and lost without a word: the browser dropped it in the
background. Phones do this all the time: the user switches to another app, and Android kills
the browser’s page process, or the whole browser, for memory. When the user comes back to the
window, the browser loads the page again, and the page links to its run again (the ticket is in
its history entry). The program runs on until then, for 10 minutes at most
(
LINDROIDS_BACKGROUND, in seconds): its notification brings the window back. In a Lindroids window app, the app is asked 3 s after the drop and after each limit (below) whether the window’s card is still in Recents: Android kills the app’s process for memory in the background and keeps its card, and a tap on the card brings the page back. While it is there the run is kept, for the same time again. Before, it ended 5 s later, and the window, back, said it had closed (reported on a Samsung phone; reproduced on b16 by killing Cromite’s page process). Before this round it had no limit at all, and a window swiped away after its page was dropped left its program running for good, with no window anywhere (the tablet: VSCodium’s whole tree under Termux, no Lindroids task). - Closed by the user:
Closing(pagehide: the close button, swiped from Recents with the page alive) or 1001 on screen: 5 s. In a Lindroids window app, also its task removed from Recents, said by the app itself (below): 5 s. - Lost on screen (the browser crashed): 60 seconds (
LINDROIDS_LINGER, in seconds).
lindroids.attached), and the app keeps that per task. When the user removes the task from
Recents, Android tells the app’s CloseWatch service (onTaskRemoved); when the activity is
finished for good, its onDestroy. Either asks the server GET /run/<pid>/closed over loopback
with the ticket (header X-Lindroids-Ticket), and the server treats it as Closing. CloseWatch
is a plain started service (no permission, no notification); Android stops it about a minute
after the app leaves the screen, and each window starts it again when it comes back. A window
swiped away after that, with its page dropped, is the one case left to the 10-minute limit.
Asked about a window dropped in the background. When the limit is over for a run whose window
went to a Lindroids app, the server runs lindroids window-ask <run>: the helper (as Termux, which
needs no permission for it) sends org.lindroids.ASK to that app alone, with the run, its ticket
and the server’s host:port. The app’s End receiver (Android starts the app’s process for it)
looks at its tasks (getAppTasks, as for END) and answers over loopback with the ticket:
/run/<pid>/kept when a task shows that run with that ticket (the run is kept, and the app asked
again after the limit), /run/<pid>/closed when none does (as a removed task: the run ends 5 s
later). No answer within 15 s (the app uninstalled or disabled, or older than template 2), and a
window in a browser: the run ends, as before. Tasks are known by their run and ticket (not by
their URL without the ticket: a card left by an ended run whose pid a new run got would answer,
or say closed, for the new run). A window whose page links again says it closed again later.
The page links again by itself when its link is lost while its program runs, also unseen in
the background (tested: a dropped link, a reload, both relinked to the running Calculator). It
says “<name> closed” only when the program has ended: the server said so (RunEnded), it refused
the window’s ticket on the way back (a run’s ticket goes with its process), or the server cannot
be reached (the program’s display went with it). A refused ticket is always the end, also for a
window Android rebuilt from Recents that never linked on this page: before, only a window that had
linked before took it so, and a rebuilt one sat on “This window was not opened by lindroids run” for good. A program’s own app goes back to its start page instead, which starts the program
again. Before, a window took any lost link for the
end, and said so while its program still ran in Termux (reported: Chrome on a Samsung phone,
after leaving the window and coming back).
Each window traces what happens to it into the server’s log (shown and hidden, full screen,
pagehide, its link closing and how, its program’s last window going, the end it shows, closing
itself), with what it noted while unlinked sent once linked again. lindroids log prints
that, and how each page left (below), with the time of day: what a phone’s browser does can only
be seen there. lindroids log --copy puts it (the last 200 KB) on Android’s clipboard instead, to paste into a bug report (--all for the server’s whole log). The page logs how the link was lost (linked again after the browser dropped the link (code …, hidden|shown)) and the server how each page went (close 1001, no close frame…), in
<runtime>/server.log.
No heartbeat. A dead browser is reported over loopback at once (EOF). And browsers freeze
pages in the background, so a heartbeat would take a window the user left in the background for
a dead one and end its program.
Single run: lindroids run X (tested: Mousepad, xclock, foot, Flatpak Calculator and VSCodium, Xournal++ in proot Debian)
- Make sure the relay is up.
- Fork the program, keeping the terminal as its stdio.
- Register it with the relay: name, icon, and “a window of its own”. A program that ends within
a second (a wrong option, a crash on start) has said why in the terminal: it gets no window,
and
runreports its exit code (tested: wrong options to Mousepad and Calculator). Alaunchfrom a home screen icon still opens its window, which is where its error shows. - Open the window:
- Ask the relay for this run’s ticket.
- The helper (
shortcut.dex open, run withapp_processas Termux) sends the Custom Tab intent forhttp://<label>.localhost:<port>/?window&run=<pid>#t=<ticket>. - The intent goes to the first installed browser of the list in
shortcut.rs(LINDROIDS_BROWSERgoes first). - The intent goes out while Termux is in the foreground, because the user just typed the command. Android 10 and later block activity starts from apps in the background.
- The page pairs (first time) or signs in, and attaches. It shows the program’s icon and name until the first window comes, then the program’s windows, and goes full screen on the first tap.
- Either end ends the run, as above. Ctrl+C in the terminal, or closing the Termux session, ends the program; its window then shows the end screen, out of full screen so that the browser’s ✕ shows (Back only sends a Firefox Custom Tab behind; ✕ removes it (tested)). A run started from a home screen icon ends with Termux’s session, server and all: the page cannot reach the server again, and takes that as the end of the program.
- While it runs, a notification stands for it (its icon in the status bar), posted by the helper as Termux; requests from the program (sound, file pickers, links, notifications) go to its own window, by pid → run.
Export: lindroids export X (tested)
-
Write
<state>/apps/<id>.json: name, icon, and therunarguments with the distro pinned. -
Pin a home screen shortcut with the program’s icon and name, for Termux, through the
app_processhelper. Its intent shareshttp://127.0.0.1:<port>/launch/termux/<id>with Termux’s share receiver, which needs no permission and no Termux setting. Exporting an id that was removed before works: the helper re-enables the shortcut first, because Android keeps a removed pinned shortcut disabled and refuses to pin it again.
- Termux starts if needed, and its share receiver runs
~/bin/termux-url-opener.exportinstalls that opener; it keeps the user’s own opener for every other URL. - The opener runs
lindroids launch <id>in a Termux session. That session keeps Termux’s foreground service, and with it the relay and the program, alive. launchisrunwith the export’s arguments. The window opens over Termux (allowed: Termux is in the foreground at that moment).- When the program ends,
launchexits. If that session was Termux’s only one, Termux closes itself.
launch asks the server for the
program’s instances that run and raises the window of the one used last.
Keeping the relay alive on Android
Termux’s process survives in the background only whileTermuxService is a foreground service,
that is while a terminal session exists or Termux’s wake lock is held. Otherwise Android caches
the process. From Android 11 on, it may freeze the cached process, with the relay and every
program in it, or kill it.
Lindroids needs no wake lock:
- A single run is started from a terminal session, which exists while the program runs.
- An exported program runs in the session the share receiver opened, until it ends.
- After a cold start through the share receiver, a session that exited took the service with it.
- With
termux-wake-locktaken first, the service stayed in the foreground (procState 4, OOM adj 50) with Termux’s window closed, and a detached child kept running.
signal 9 deaths as such.
The requirements, met
Ideas to make it better
These have not been tried yet.- Stop from the running program’s notification. It has the program’s icon and a tap that brings back the window (done). A Stop button needs a way back from Android to the relay without Termux coming to the front (a PendingIntent can only start Termux’s activities).
- Chrome. Chromium’s Custom Tabs are tested through Cromite; Chrome itself only on the user’s Samsung phone so far.
- An app of its own, opt-in.
lindroids export --webapkhas Chrome (with Google Play services) or Samsung Internet (on Samsung phones) install the program as a WebAPK, whose task Android shows with the program’s icon on every launcher, Samsung’s too. Only those two browsers can, so it is never the default and errors where neither is installed: WEBAPK.md. Not yet tried on a phone with Chrome. - Closing Firefox’s window from outside. Its source offers no way: Gecko does ask to close
a one-entry window, but only normal tabs act on it;
ExternalAppBrowserActivityends itself only when its tab is gone, and only its ✕, “Open in Firefox” or its own end remove that tab (the activity is not exported). Every Custom Tab intent it receives gets a new tab, and it stripsFLAG_ACTIVITY_CLEAR_TASKfrom them: re-sending the window’s intent with it left the window as it was (tested, Firefox 158). Its Recents icon is Firefox’s for the same reason: only Firefox’s installed web apps (https only) set a task’s icon. app_processexits 137 after the helper starts an activity (the activity does start; the helper prints its result first). Find out why.