Android app (TWA) in android/, built with podman; serve assetlinks.json for it

This commit is contained in:
Astra 2026-10-03 12:44:11 +01:00
parent 44708d5f3c
commit 7900bf5b3e
21 changed files with 313 additions and 0 deletions

6
android/.gitignore vendored Normal file
View file

@ -0,0 +1,6 @@
.gradle/
build/
app/build/
out/
local.properties
*.jks

17
android/Containerfile Normal file
View file

@ -0,0 +1,17 @@
# Build image for the Android app: JDK 17, the Android SDK and Gradle. See build.sh.
FROM docker.io/library/eclipse-temurin:17-jdk
ARG CMDLINE_TOOLS=15859902
ARG GRADLE=8.14.3
ENV ANDROID_HOME=/opt/android-sdk
ENV PATH=/opt/gradle/bin:$ANDROID_HOME/cmdline-tools/latest/bin:$PATH
RUN apt-get update && apt-get install -y --no-install-recommends unzip && rm -rf /var/lib/apt/lists/*
RUN curl -fsSLo /tmp/tools.zip https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS}_latest.zip \
&& mkdir -p $ANDROID_HOME/cmdline-tools && unzip -q /tmp/tools.zip -d /tmp \
&& mv /tmp/cmdline-tools $ANDROID_HOME/cmdline-tools/latest && rm /tmp/tools.zip
RUN yes | sdkmanager --licenses >/dev/null && sdkmanager "platforms;android-36" "build-tools;35.0.0" "platform-tools" >/dev/null
RUN curl -fsSLo /tmp/gradle.zip https://services.gradle.org/distributions/gradle-${GRADLE}-bin.zip \
&& unzip -q /tmp/gradle.zip -d /opt && mv /opt/gradle-${GRADLE} /opt/gradle && rm /tmp/gradle.zip
WORKDIR /src

53
android/app/build.gradle Normal file
View file

@ -0,0 +1,53 @@
plugins {
id "com.android.application"
}
// The site the app opens. It must serve /.well-known/assetlinks.json naming this app's package and
// signing key, or Chrome shows its URL bar over the app (see SAW_ANDROID_* in example.env).
def siteURL = "https://cs2.zio.sh"
def startPath = "/"
android {
namespace "sh.zio.cs2"
compileSdk 36
defaultConfig {
// Never change applicationId once installed anywhere: Android treats it as a different app.
applicationId "sh.zio.cs2"
minSdk 24
targetSdk 36
// Bump versionCode for every APK you hand out, or Android refuses to update over the old one.
versionCode 2
versionName "1.0"
resValue "string", "launch_url", siteURL + startPath
resValue "string", "asset_statements",
"[{\\\"relation\\\": [\\\"delegate_permission/common.handle_all_urls\\\"], \\\"target\\\": {\\\"namespace\\\": \\\"web\\\", \\\"site\\\": \\\"" + siteURL + "\\\"}}]"
}
signingConfigs {
release {
storeFile file(System.getenv("KEYSTORE_FILE") ?: "/keystore/release.jks")
storePassword System.getenv("KEYSTORE_PASSWORD")
keyAlias "release"
keyPassword System.getenv("KEYSTORE_PASSWORD")
}
}
buildTypes {
release {
minifyEnabled true
proguardFiles getDefaultProguardFile("proguard-android-optimize.txt")
signingConfig signingConfigs.release
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
}
dependencies {
implementation "com.google.androidbrowserhelper:androidbrowserhelper:2.7.3"
}

View file

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- A Trusted Web Activity: the app is the panel, opened full screen in the phone's Chrome.
androidbrowserhelper's LauncherActivity does all of it; there is no code of our own. -->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="false"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:manageSpaceActivity="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"
android:supportsRtl="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<meta-data
android:name="asset_statements"
android:resource="@string/asset_statements" />
<activity
android:name="com.google.androidbrowserhelper.trusted.LauncherActivity"
android:exported="true"
android:label="@string/app_name">
<meta-data
android:name="android.support.customtabs.trusted.DEFAULT_URL"
android:value="@string/launch_url" />
<meta-data
android:name="android.support.customtabs.trusted.STATUS_BAR_COLOR"
android:resource="@color/theme" />
<meta-data
android:name="android.support.customtabs.trusted.NAVIGATION_BAR_COLOR"
android:resource="@color/background" />
<meta-data
android:name="android.support.customtabs.trusted.SPLASH_IMAGE_DRAWABLE"
android:resource="@drawable/splash" />
<meta-data
android:name="android.support.customtabs.trusted.SPLASH_SCREEN_BACKGROUND_COLOR"
android:resource="@color/background" />
<meta-data
android:name="android.support.customtabs.trusted.FILE_PROVIDER_AUTHORITY"
android:value="sh.zio.cs2.fileprovider" />
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<!-- LauncherActivity uses these library activities by name, so they must be declared here
(as Bubblewrap's template does), or R8 renames them and the app crashes on launch.
ManageDataLauncherActivity is "Manage space" in Android's app info: the site's data. -->
<activity
android:name="com.google.androidbrowserhelper.trusted.ManageDataLauncherActivity"
android:enabled="true"
android:excludeFromRecents="true"
android:exported="false">
<meta-data
android:name="android.support.customtabs.trusted.MANAGE_SPACE_URL"
android:value="@string/launch_url" />
<intent-filter>
<action android:name="android.intent.action.APPLICATION_PREFERENCES" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<activity android:name="com.google.androidbrowserhelper.trusted.FocusActivity" />
<!-- Used when the phone has no browser that supports TWAs. -->
<activity
android:name="com.google.androidbrowserhelper.trusted.WebViewFallbackActivity"
android:configChanges="orientation|screenSize" />
<!-- Hands the splash image to Chrome while the panel loads. -->
<provider
android:name="androidx.core.content.FileProvider"
android:authorities="sh.zio.cs2.fileprovider"
android:exported="false"
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/filepaths" />
</provider>
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View file

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

View file

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- The panel's colours: the top bar (theme) and page background in styles.css. -->
<resources>
<color name="theme">#1B1E25</color>
<color name="background">#14161B</color>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="app_name">zio.sh</string>
</resources>

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<paths>
<files-path name="twa_splash" path="twa_splash/" />
</paths>

3
android/build.gradle Normal file
View file

@ -0,0 +1,3 @@
plugins {
id "com.android.application" version "8.13.2" apply false
}

37
android/build.sh Executable file
View file

@ -0,0 +1,37 @@
#!/usr/bin/env bash
# Builds the signed release APK into out/ with podman (no local JDK or Android SDK needed).
#
# The signing key lives outside the repo, in $KEYSTORE_DIR (default ~/.config/simpleadmin-web/android),
# and is created on the first run. Back it up: an APK signed with a different key can't update an
# installed one, and the panel's assetlinks.json names the key's fingerprint.
set -euo pipefail
here="$(cd "$(dirname "$0")" && pwd)"
keydir="${KEYSTORE_DIR:-$HOME/.config/simpleadmin-web/android}"
image=simpleadmin-web-android-build
podman build -q -t "$image" "$here" >/dev/null
mkdir -p "$keydir"
chmod 700 "$keydir"
if [[ ! -f "$keydir/release.jks" ]]; then
echo "Creating a signing key in $keydir"
(umask 077 && openssl rand -hex 24 >"$keydir/password")
podman run --rm -v "$keydir":/keystore:Z "$image" keytool -genkeypair -noprompt \
-keystore /keystore/release.jks -storetype PKCS12 -storepass "$(cat "$keydir/password")" \
-alias release -keyalg RSA -keysize 4096 -validity 10000 -dname "CN=zio.sh"
fi
podman run --rm \
-v "$here":/src:Z \
-v "$keydir":/keystore:ro,Z \
-v simpleadmin-web-android-gradle:/root/.gradle \
-e KEYSTORE_PASSWORD="$(cat "$keydir/password")" \
"$image" gradle --no-daemon -q assembleRelease
mkdir -p "$here/out"
cp "$here/app/build/outputs/apk/release/app-release.apk" "$here/out/simpleadmin-web.apk"
echo "APK: $here/out/simpleadmin-web.apk"
echo "Signing key SHA-256 (for SAW_ANDROID_CERT_SHA256):"
podman run --rm -v "$keydir":/keystore:ro,Z "$image" keytool -list -keystore /keystore/release.jks \
-storepass "$(cat "$keydir/password")" -alias release | sed -n 's/.*SHA-256): //p'

View file

@ -0,0 +1,2 @@
org.gradle.jvmargs=-Xmx2g
android.useAndroidX=true

16
android/settings.gradle Normal file
View file

@ -0,0 +1,16 @@
pluginManagement {
repositories {
google()
mavenCentral()
gradlePluginPortal()
}
}
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "simpleadmin-web"
include ":app"