Put the app's own mark on the launcher
ci / build and test (push) Failing after 3s
ci / api image (push) Skipped
ci / android head (push) Failing after 2s

The sign-in and locked screens both draw the same thing — a square outline in the accent
with >_ inside it — and the launcher was still showing the stock Android silhouette, so the
icon somebody taps and the icon the app opens onto had nothing to do with each other.

Redrawn as a vector rather than exported from the screen as a bitmap. There is one geometry
here and no set of density buckets to update four of and forget the fifth, and the accent
stays a number that can be diffed against Palette.axaml rather than a colour baked into a
PNG. The hex is written out because an Android resource cannot reference a XAML dictionary
— the same duplication colors.xml already carries for the window background, with the same
obligation attached.

Adaptive only, no raster fallback. Adaptive icons landed in API 26 and this head requires
28, so there is no device it ships to that would need the bitmaps; density buckets exist to
choose between PNGs and there is nothing to choose. The background layer is the same
@color/dodo_window as the window and the status bar, so the mark sits on the app's own
near-black rather than on a second one almost like it.

Two departures from the screen, both because a launcher is looked at much smaller than a
sign-in header. The box is 42 across rather than the 48 that first suggested itself: 72 of
the 108 survives masking, but that is a width, and a square meets a circular mask at its
corners — at 48 they land 33.9 out against a radius of 36 and read as clipped despite
technically clearing it. And the strokes are 2.2 and 2.8 where proportional fidelity to a
1px border on 44px would be 1.0, which a launcher drawing this at 48dp would render as half
a pixel of nothing.

A monochrome layer too, for the Android 13+ themed-icon setting. Without one a launcher
with themed icons on falls back to the full-colour icon, which would leave this the single
green thing on an otherwise recoloured home screen.

Verified in the packaged APK: the icon resolves at all five densities, the three layers
resolve, and the compiled vector carries the geometry above. The launcher rendering itself
was checked against local renders under circular and squircle masks at 144 and 64 px, not
on the device — the phone was locked.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-01 13:46:40 +02:00
co-authored by Claude Opus 5
parent 8a568117df
commit 57d4b30557
4 changed files with 115 additions and 0 deletions
@@ -26,7 +26,14 @@
phone's outbox onto another, which is the same corruption ClientPaths already refuses by insisting on
a local, non-roaming directory.
-->
<!--
The launcher icon is the same >_ box the sign-in and locked screens draw, as a vector
adaptive icon. See Resources/mipmap-anydpi-v26/ic_launcher.xml. No android:roundIcon:
that attribute exists so a launcher wanting a circle can be handed a second bitmap, and
an adaptive icon is already masked to whatever shape the launcher asks for.
-->
<application android:label="DodoSSH"
android:icon="@mipmap/ic_launcher"
android:theme="@style/DodoTheme"
android:networkSecurityConfig="@xml/network_security_config"
android:allowBackup="false"
@@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
The launcher mark, and it is the same mark ServerScreen and LockedScreen draw: a square
outline in the accent with >_ inside it. Redrawn as a vector rather than exported as a
bitmap so there is one geometry to change and no set of five PNG densities to forget one
of.
#3CE88F is AccentColor from DodoSSH.Client.Shell's Theme/Palette.axaml, written out
because an Android resource cannot reference a XAML dictionary. The same duplication
colors.xml already carries for the window background, and the same rule applies: if the
palette moves, this moves with it.
108x108 with the artwork inside the middle 72 is the adaptive-icon contract — the outer
18 on each edge is what the launcher eats for masking and parallax. That 72 is a width,
though, and the mark is a square: its corners are what a circular mask reaches first. At
48 across the corners land 33.9 out against a radius of 36 and read as clipped even
though they technically clear it. 42 puts them at 29.7, which is margin one can see.
The stroke widths are the one place this deliberately departs from the screen. In the app
the box is a 1px border on 44px; scaled honestly that would be 1.0 here, and a launcher
drawing this at 48dp would render it at half a pixel and show nothing. 2.2 and 2.8 are
what keep it reading as the same hairline mark at the size it is actually looked at.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<!-- The box: 42 across, centred, square-cornered as the Border in the app is. -->
<path
android:pathData="M33,33 L75,33 L75,75 L33,75 Z"
android:fillColor="#00000000"
android:strokeColor="#3CE88F"
android:strokeWidth="2.2" />
<!-- The chevron of >_ -->
<path
android:pathData="M44.8,48.75 L51.9,54 L44.8,59.25"
android:fillColor="#00000000"
android:strokeColor="#3CE88F"
android:strokeWidth="2.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<!-- The underscore, on the baseline the chevron bottoms out at. -->
<path
android:pathData="M54,59.7 L63.2,59.7"
android:fillColor="#00000000"
android:strokeColor="#3CE88F"
android:strokeWidth="2.8"
android:strokeLineCap="round" />
</vector>
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
The themed-icon layer, for the Android 13+ setting that repaints every launcher icon in
the wallpaper's colours. The system tints this by its alpha and discards the colour, so
the geometry is the foreground's and white is only a way of saying "opaque here".
Worth shipping rather than leaving out: a launcher with themed icons on and no monochrome
layer to use falls back to the full-colour icon, so the one app on the home screen still
drawn in green is this one.
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M33,33 L75,33 L75,75 L33,75 Z"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.2" />
<path
android:pathData="M44.8,48.75 L51.9,54 L44.8,59.25"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.8"
android:strokeLineCap="round"
android:strokeLineJoin="round" />
<path
android:pathData="M54,59.7 L63.2,59.7"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeWidth="2.8"
android:strokeLineCap="round" />
</vector>
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Adaptive only, with no raster fallback in mipmap-hdpi and the rest, and that is a
consequence of minSdk rather than a shortcut: this head requires API 28 (see the csproj),
adaptive icons landed in 26, so there is no device this ships to that would need the
bitmaps. Density buckets exist to pick a PNG; a vector has nothing to pick between.
The background is the same @color/dodo_window the window, status bar and navigation bar
use, so the mark sits on the app's own near-black rather than on a second dark that is
almost but not quite it.
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/dodo_window" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>