mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-05 00:02:11 +00:00
feat(mobile): run on Android, and release a signed APK (#1044)
* feat(mobile): run on Android The app builds and runs on Android, and fits there. - Back button and gesture close what is open over the screen, then go back a screen, and from the first one send the app to the background. Left to the WebView they closed the app, since it has no history. - The system bars: the page asks their size of the app (`insets`, over JNI), since WebViews before 140 report the safe areas as 0 even edge to edge. The CSS reads them as `--inset-*`, which iOS still fills from `env()`. - The keyboard: edge to edge, the WebView is not resized for it and its visual viewport stays whole, so the keyboard covered the message box. MainActivity hands its height to the page, which lays out as on iOS. - The title folds into the bar from the scroll position on every screen. The observer it used reported a title in plain view as out of sight on Android, so the bar started folded. - src-tauri/gen/android is kept in the repo for that MainActivity. * ci(mobile): release a signed Android APK A `mobile-v<x.y.z>` tag builds the app for arm64 at that version, signs it with the release key and attaches it to a draft release. The mobile app is versioned apart from the desktop's `v*` tags. - The release is never marked latest: the desktop updater reads /releases/latest and would take it for a desktop release. - The APK's certificate is checked against the release key's, since one signed with another key could not be installed over earlier ones. - The NDK is pinned, and the version must be x.y.z: Tauri derives the versionCode from it, and Android installs over a build only when that is higher. - Gradle signs a release build when keystore.properties names a key; CI writes it from secrets. - The release library is stripped: 30 MB to 20, the APK 32 to 23. * ci: tell people how to install the phone app The nightly release notes and each mobile-v release now say how to get the app on a phone: the TestFlight link for iPhone, and the newest mobile-v release's APK for Android. Both read .github/mobile-install.md.
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
## Phone app
|
||||
|
||||
Watch and drive this computer's panes and agents from your phone. On the computer, open **Settings → Mobile**, turn on **Allow phone access** and click **Show code**, then pair from the app by scanning the code or pasting it.
|
||||
|
||||
- **iPhone**: join the beta at https://testflight.apple.com/join/gFcPbPVR. The link asks you to install TestFlight from the App Store first, if you don't have it.
|
||||
- **Android** (arm64): on the phone, download `tty7-<version>-android-arm64.apk` from the newest [`mobile-v*` release](https://github.com/l0ng-ai/tty7/releases?q=mobile-v&expanded=true) and open it. Android asks once to allow installing apps from your browser or file manager. A later APK installs over it and keeps your paired machines.
|
||||
@@ -0,0 +1,140 @@
|
||||
name: Mobile
|
||||
|
||||
# The phone app's Android build: a signed APK people install by hand. A
|
||||
# `mobile-v<x.y.z>` tag builds it at that version and attaches it to a draft
|
||||
# release of the same name; a manual run builds it at the version in
|
||||
# tauri.conf.json and keeps it as a workflow artifact only.
|
||||
#
|
||||
# The mobile app is versioned apart from the desktop's `v*` tags. Android
|
||||
# installs one build over another only when its versionCode is higher, and
|
||||
# Tauri derives it from the version (major * 1000000 + minor * 1000 + patch),
|
||||
# so each tag must be higher than the last.
|
||||
|
||||
on:
|
||||
push:
|
||||
tags: ["mobile-v*"]
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
env:
|
||||
# The release key's certificate. A build signed with any other key could not
|
||||
# be installed over the ones people already have, so it is refused here.
|
||||
CERT_SHA256: c54b8b466e7ac45bac2c9d79e44ec3e6ab8fef1d00818adafa11bb747163d75f
|
||||
NDK_VERSION: 28.2.13676358
|
||||
|
||||
jobs:
|
||||
android:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$GITHUB_REF" == refs/tags/mobile-v* ]]; then
|
||||
VERSION="${GITHUB_REF_NAME#mobile-v}"
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::tag must be mobile-v<major>.<minor>.<patch>, got $GITHUB_REF_NAME"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
VERSION=$(jq -r .version mobile/src-tauri/tauri.conf.json)
|
||||
fi
|
||||
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
distribution: temurin
|
||||
java-version: "17"
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: mobile/package-lock.json
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-linux-android
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: mobile/src-tauri
|
||||
|
||||
# The runner has an SDK and some NDK; the NDK is pinned so a runner image
|
||||
# update cannot change the toolchain under a release.
|
||||
- name: Android NDK
|
||||
run: |
|
||||
set -euo pipefail
|
||||
yes | "$ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager" --install "ndk;$NDK_VERSION" > /dev/null
|
||||
echo "NDK_HOME=$ANDROID_HOME/ndk/$NDK_VERSION" >> "$GITHUB_ENV"
|
||||
|
||||
- name: Signing key
|
||||
env:
|
||||
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
|
||||
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
|
||||
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$ANDROID_KEYSTORE_BASE64" ]; then
|
||||
echo "::error::the ANDROID_KEYSTORE_* secrets are not set"
|
||||
exit 1
|
||||
fi
|
||||
echo "$ANDROID_KEYSTORE_BASE64" | base64 -d > "$RUNNER_TEMP/release.jks"
|
||||
# Read by gen/android/app/build.gradle.kts; git ignores it.
|
||||
{
|
||||
echo "storeFile=$RUNNER_TEMP/release.jks"
|
||||
echo "storePassword=$ANDROID_KEYSTORE_PASSWORD"
|
||||
echo "keyAlias=$ANDROID_KEY_ALIAS"
|
||||
echo "keyPassword=$ANDROID_KEYSTORE_PASSWORD"
|
||||
} > mobile/src-tauri/gen/android/keystore.properties
|
||||
|
||||
# arm64 only: every phone of the last several years, with one copy of
|
||||
# the native library instead of four.
|
||||
- name: Build
|
||||
working-directory: mobile
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm ci
|
||||
npx tauri android build --apk --target aarch64 --config "{\"version\":\"$VERSION\"}"
|
||||
|
||||
- name: Check and name the APK
|
||||
run: |
|
||||
set -euo pipefail
|
||||
APK=mobile/src-tauri/gen/android/app/build/outputs/apk/universal/release/app-universal-release.apk
|
||||
APKSIGNER=$(ls -d "$ANDROID_HOME"/build-tools/* | sort -V | tail -1)/apksigner
|
||||
CERT=$("$APKSIGNER" verify --print-certs "$APK" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p')
|
||||
if [ "$CERT" != "$CERT_SHA256" ]; then
|
||||
echo "::error::APK is signed with $CERT, not the release key"
|
||||
exit 1
|
||||
fi
|
||||
mkdir dist
|
||||
cp "$APK" "dist/tty7-$VERSION-android-arm64.apk"
|
||||
(cd dist && sha256sum *.apk > SHA256SUMS)
|
||||
|
||||
- uses: actions/upload-artifact@v7
|
||||
with:
|
||||
name: tty7-android-${{ env.VERSION }}
|
||||
path: dist/*
|
||||
|
||||
# A draft, published by hand as the desktop's are. Never marked latest:
|
||||
# the desktop updater reads /releases/latest and would take this for a
|
||||
# desktop release.
|
||||
- name: Draft release
|
||||
if: startsWith(github.ref, 'refs/tags/mobile-v')
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
EXISTING=$(gh release list --repo "$GITHUB_REPOSITORY" --limit 100 \
|
||||
--json tagName -q '.[].tagName')
|
||||
if grep -Fxq "$GITHUB_REF_NAME" <<<"$EXISTING"; then
|
||||
echo "release $GITHUB_REF_NAME already exists; reusing it"
|
||||
else
|
||||
gh release create "$GITHUB_REF_NAME" --repo "$GITHUB_REPOSITORY" \
|
||||
--draft --latest=false --title "tty7 mobile $VERSION" \
|
||||
--notes-file .github/mobile-install.md
|
||||
fi
|
||||
gh release upload "$GITHUB_REF_NAME" dist/* --clobber --repo "$GITHUB_REPOSITORY"
|
||||
@@ -546,7 +546,9 @@ jobs:
|
||||
git push -f origin "$GITHUB_SHA:refs/tags/nightly"
|
||||
|
||||
TITLE="Nightly $VERSION"
|
||||
NOTES="Automated nightly build of \`main\` @ ${GITHUB_SHA::7} ($(date -u +%F)). Rolling prerelease — assets are replaced every night; for the latest stable release see https://github.com/${GITHUB_REPOSITORY}/releases/latest."
|
||||
NOTES="Automated nightly build of \`main\` @ ${GITHUB_SHA::7} ($(date -u +%F)). Rolling prerelease — assets are replaced every night; for the latest stable release see https://github.com/${GITHUB_REPOSITORY}/releases/latest.
|
||||
|
||||
$(cat .github/mobile-install.md)"
|
||||
|
||||
if gh release view nightly >/dev/null 2>&1; then
|
||||
gh release edit nightly --prerelease --title "$TITLE" --notes "$NOTES"
|
||||
|
||||
Reference in New Issue
Block a user