From 1d4b6bbb4b6781c8e2489594221ef914eb1f6028 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 5 Nov 2026 09:48:16 +0000 Subject: [PATCH 1/2] feat: pairux on Android (com.pairux.app) A WebView shell for pairux.com, built for Google Play. minSdk 24, targetSdk 35, and it produces an .aab (what Play takes) alongside an .apk for sideloading. Play rejects a bare web wrapper, so this adds what a browser tab cannot: - pairux.com links open in the app (autoVerify App Links) - the system Back button walks the page's own history - works at all, which needs onShowFileChooser - a WebView silently does nothing on a file input without it - getUserMedia is bridged to Android's own permission prompt - full-screen video, and an offline screen instead of Chrome's dinosaur Upload key in vault profullstack/pairux-android--signing (cert SHA-256 ca22e2122c67...). It is the upload key, not the app signing key: Play holds that one once enrolled, so this one is recoverable and that one is not. Co-Authored-By: Claude Opus 5 (1M context) --- appstores/android/.gitignore | 5 + appstores/android/README.md | 63 +++-- appstores/android/app/build.gradle | 46 ++++ .../android/app/src/main/AndroidManifest.xml | 38 +++ .../android/app/src/main/assets/offline.html | 23 ++ .../java/com/pairux/app/MainActivity.java | 237 ++++++++++++++++++ .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin 0 -> 3232 bytes .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin 0 -> 1858 bytes .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin 0 -> 4421 bytes .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin 0 -> 6633 bytes .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin 0 -> 9168 bytes .../app/src/main/res/values/strings.xml | 4 + .../app/src/main/res/values/styles.xml | 14 ++ appstores/android/build.gradle | 1 + appstores/android/build.sh | 35 +++ appstores/android/gradle.properties | 3 + appstores/android/new-key.sh | 18 ++ appstores/android/settings.gradle | 9 + appstores/android/store/listing.md | 28 +++ appstores/android/store/render.sh | 30 +++ appstores/android/store/src/mark.svg | 1 + appstores/android/store/src/wordmark.svg | 1 + 22 files changed, 538 insertions(+), 18 deletions(-) create mode 100644 appstores/android/.gitignore create mode 100644 appstores/android/app/build.gradle create mode 100644 appstores/android/app/src/main/AndroidManifest.xml create mode 100644 appstores/android/app/src/main/assets/offline.html create mode 100644 appstores/android/app/src/main/java/com/pairux/app/MainActivity.java create mode 100644 appstores/android/app/src/main/res/mipmap-hdpi/ic_launcher.png create mode 100644 appstores/android/app/src/main/res/mipmap-mdpi/ic_launcher.png create mode 100644 appstores/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png create mode 100644 appstores/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png create mode 100644 appstores/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png create mode 100644 appstores/android/app/src/main/res/values/strings.xml create mode 100644 appstores/android/app/src/main/res/values/styles.xml create mode 100644 appstores/android/build.gradle create mode 100755 appstores/android/build.sh create mode 100644 appstores/android/gradle.properties create mode 100755 appstores/android/new-key.sh create mode 100644 appstores/android/settings.gradle create mode 100644 appstores/android/store/listing.md create mode 100755 appstores/android/store/render.sh create mode 100644 appstores/android/store/src/mark.svg create mode 100644 appstores/android/store/src/wordmark.svg diff --git a/appstores/android/.gitignore b/appstores/android/.gitignore new file mode 100644 index 00000000..d544a517 --- /dev/null +++ b/appstores/android/.gitignore @@ -0,0 +1,5 @@ +.gradle/ +app/build/ +build/ +dist/ +local.properties diff --git a/appstores/android/README.md b/appstores/android/README.md index 3c5fa4b4..b78e10ea 100644 --- a/appstores/android/README.md +++ b/appstores/android/README.md @@ -1,20 +1,47 @@ # Google Play (pairux.com) -Not built yet. - -**What it takes:** an Android app (the Fire TV shell in `../firetv` is already a -plain Android app and is the obvious starting point — same WebView, same -`tv.js`, minus the leanback intent filter and plus a phone layout), a Play -Console developer account, an upload key, and a Play App Signing enrolment. - -**Before the first upload** - -- Play requires an **App Bundle** (`.aab`), not an APK, and a `targetSdk` that - meets Play's current floor. The Fire TV build targets 34 for Fire OS; Play - moves its floor every August, so check it rather than assuming. -- `applicationId` must differ from nothing else on Play, and once published it - can never change. -- Play wants a privacy policy URL, a Data safety form, and content rating - answers — the same facts as the Amazon listing, asked differently. -- The listing copy and art in `../firetv/store` mostly carries over; Play's - screenshot sizes and the 1024x500 feature graphic do not. +A WebView shell for https://pairux.com. Package `com.pairux.app`, minSdk 24, targetSdk 35. + +## Build + +```sh +docker run --rm -e OWNER=$(id -u):$(id -g) \ + -v "$PWD/android-sdk:/sdk" -v "$PWD/keys:/keys" \ + -v "$PWD/appstores/android:/w" -w /w gradle:8.7-jdk17 sh build.sh +``` + +`dist/pairux.aab` is what Play takes; `dist/pairux.apk` is for sideloading and testing. +Art: `docker run --rm -v "$PWD/appstores/android:/w" -w /w alpine:3.20 sh store/render.sh`. + +## Signing + +The upload key is in vault `profullstack/pairux-android--signing` +(`ANDROID_KEYSTORE_B64`, `ANDROID_STORE_PASSWORD`, `ANDROID_KEY_ALIAS`). + +This is the **upload** key, not the app signing key. Once enrolled in Play App +Signing, Google holds the signing key and re-signs every upload, so a lost upload +key can be reset by support; a lost Play-managed signing key cannot. `new-key.sh` +refuses to mint a second one. + +## What it adds over the website + +Play rejects a bare web wrapper. This one is a launcher entry plus: `pairux.com` links +opening in the app (`autoVerify` App Links), the system Back button walking the +page's own history, `` working (a WebView does nothing on file +inputs without `onShowFileChooser`), `getUserMedia` bridged to Android's own +permission prompt, full-screen video, and an offline screen instead of Chrome's. + +## Before the first release + +- Play Console account: **anthony@profullstack.com**. +- Play takes an **.aab**, never an APK, and raises its `targetSdk` floor every + August — check it rather than trusting the 35 here. +- `applicationId` can never change after publishing. +- The Data safety form and content rating questionnaire are required, and ask the + same facts as Amazon's listing in a different shape. +- A new personal Play account needs 12 testers on a closed test for 14 days before + production opens. An organisation account needs D-U-N-S verification instead. + +## Listing + +Copy and art live in `store/` — see `store/listing.md`. diff --git a/appstores/android/app/build.gradle b/appstores/android/app/build.gradle new file mode 100644 index 00000000..43a0fbcb --- /dev/null +++ b/appstores/android/app/build.gradle @@ -0,0 +1,46 @@ +plugins { id 'com.android.application' } + +def env = System.getenv() + +android { + namespace 'com.pairux.app' + compileSdk 35 + + defaultConfig { + applicationId 'com.pairux.app' + minSdk 24 // Android 7: what Play still sees in the wild + targetSdk 35 // Play raises its floor every August; check before a release + versionCode 1 + versionName '1.0.0' + } + + // The upload key lives in the vault, never in the repo. build.sh writes it to a + // temp file and points these at it. Play re-signs with its own app signing key. + signingConfigs { + release { + if (env.ANDROID_KEYSTORE) { + storeFile file(env.ANDROID_KEYSTORE) + storePassword env.ANDROID_STORE_PASSWORD + keyAlias env.ANDROID_KEY_ALIAS ?: 'pairux' + keyPassword env.ANDROID_KEY_PASSWORD ?: env.ANDROID_STORE_PASSWORD + } + } + } + + buildTypes { + release { + minifyEnabled false + if (env.ANDROID_KEYSTORE) signingConfig signingConfigs.release + } + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lint { + abortOnError true + checkReleaseBuilds true + } +} diff --git a/appstores/android/app/src/main/AndroidManifest.xml b/appstores/android/app/src/main/AndroidManifest.xml new file mode 100644 index 00000000..1231d72f --- /dev/null +++ b/appstores/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/appstores/android/app/src/main/assets/offline.html b/appstores/android/app/src/main/assets/offline.html new file mode 100644 index 00000000..d9ad2286 --- /dev/null +++ b/appstores/android/app/src/main/assets/offline.html @@ -0,0 +1,23 @@ + + + +PairUX is offline + +

No connection

+

PairUX needs the network to reach pairux.com.

+ diff --git a/appstores/android/app/src/main/java/com/pairux/app/MainActivity.java b/appstores/android/app/src/main/java/com/pairux/app/MainActivity.java new file mode 100644 index 00000000..e953bc4d --- /dev/null +++ b/appstores/android/app/src/main/java/com/pairux/app/MainActivity.java @@ -0,0 +1,237 @@ +package com.pairux.app; + +import android.annotation.SuppressLint; +import android.app.Activity; +import android.content.Intent; +import android.net.Uri; +import android.os.Build; +import android.os.Bundle; +import android.view.View; +import android.view.ViewGroup; +import android.webkit.CookieManager; +import android.webkit.PermissionRequest; +import android.webkit.ValueCallback; +import android.webkit.WebChromeClient; +import android.webkit.WebResourceError; +import android.webkit.WebResourceRequest; +import android.webkit.WebSettings; +import android.webkit.WebView; +import android.webkit.WebViewClient; +import android.widget.FrameLayout; + +import android.Manifest; +import android.content.pm.PackageManager; + +import java.util.ArrayList; +import java.util.List; + +/** + * PairUX (pairux.com) on Android: the web app in a full-screen WebView. + * + * The site is already responsive and installable, so this adds only what a + * browser tab cannot: a launcher entry, $DOMAIN links opening here, the system + * Back button walking the page's own history, file uploads, full-screen video, + * and an offline screen instead of Chrome's dinosaur. + */ +public class MainActivity extends Activity { + + static final String HOME = "https://pairux.com/"; + static final String OFFLINE = "file:///android_asset/offline.html"; + static final int MEDIA_PERMISSIONS = 1; + static final int FILE_CHOOSER = 2; + + private WebView web; + private FrameLayout root; + private View fullscreen; + private WebChromeClient.CustomViewCallback fullscreenDone; + private PermissionRequest pendingPermission; + private ValueCallback pendingFiles; + + @SuppressLint("SetJavaScriptEnabled") + @Override + protected void onCreate(Bundle state) { + super.onCreate(state); + + root = new FrameLayout(this); + root.setBackgroundColor(0xFF0F172A); + web = new WebView(this); + web.setBackgroundColor(0xFF0F172A); + root.addView(web, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + setContentView(root); + + WebSettings s = web.getSettings(); + s.setJavaScriptEnabled(true); + s.setDomStorageEnabled(true); + s.setDatabaseEnabled(true); + s.setMediaPlaybackRequiresUserGesture(false); + s.setLoadWithOverviewMode(true); + s.setUseWideViewPort(true); + s.setSupportZoom(false); + s.setBuiltInZoomControls(false); + s.setUserAgentString(s.getUserAgentString() + " pairux-android"); + CookieManager.getInstance().setAcceptThirdPartyCookies(web, true); + + web.setWebViewClient(new Client()); + web.setWebChromeClient(new Chrome()); + + if (state != null) web.restoreState(state); + else handle(getIntent(), true); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + handle(intent, false); + } + + /** A tapped pairux.com link opens that page; the launcher icon opens the home page. */ + private void handle(Intent intent, boolean launching) { + Uri data = intent != null ? intent.getData() : null; + if (data != null && "pairux.com".equalsIgnoreCase(data.getHost())) web.loadUrl(data.toString()); + else if (launching) web.loadUrl(HOME); + } + + @Override + protected void onSaveInstanceState(Bundle out) { + super.onSaveInstanceState(out); + web.saveState(out); + } + + @Override + public void onBackPressed() { + if (fullscreen != null) { exitFullscreen(); return; } + if (web.canGoBack()) { web.goBack(); return; } + super.onBackPressed(); + } + + @Override + protected void onDestroy() { + web.destroy(); + super.onDestroy(); + } + + private class Client extends WebViewClient { + /** Our own pages stay in the app; anything else is someone else's site. */ + @Override + public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) { + Uri url = request.getUrl(); + String host = url.getHost(); + if (host == null || host.equalsIgnoreCase("pairux.com") || host.endsWith(".pairux.com")) return false; + try { + startActivity(new Intent(Intent.ACTION_VIEW, url)); + return true; + } catch (Exception e) { + return false; + } + } + + @Override + public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { + if (request.isForMainFrame()) view.loadUrl(OFFLINE); + } + } + + private class Chrome extends WebChromeClient { + /** getUserMedia: grant only what Android already granted us, else ask first. */ + @Override + public void onPermissionRequest(final PermissionRequest request) { + runOnUiThread(new Runnable() { + @Override + public void run() { + List grant = new ArrayList<>(); + List need = new ArrayList<>(); + for (String r : request.getResources()) { + if (PermissionRequest.RESOURCE_AUDIO_CAPTURE.equals(r)) { + if (held(Manifest.permission.RECORD_AUDIO)) grant.add(r); + else need.add(Manifest.permission.RECORD_AUDIO); + } else if (PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(r)) { + if (held(Manifest.permission.CAMERA)) grant.add(r); + else need.add(Manifest.permission.CAMERA); + } + } + if (!need.isEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + pendingPermission = request; + requestPermissions(need.toArray(new String[0]), MEDIA_PERMISSIONS); + return; + } + if (grant.isEmpty()) request.deny(); + else request.grant(grant.toArray(new String[0])); + } + }); + } + + @Override + public void onPermissionRequestCanceled(PermissionRequest request) { + if (pendingPermission == request) pendingPermission = null; + } + + /** Without this an does nothing at all in a WebView. */ + @Override + public boolean onShowFileChooser(WebView view, ValueCallback files, + FileChooserParams params) { + if (pendingFiles != null) pendingFiles.onReceiveValue(null); + pendingFiles = files; + try { + startActivityForResult(params.createIntent(), FILE_CHOOSER); + return true; + } catch (Exception e) { + pendingFiles = null; + return false; + } + } + + @Override + public void onShowCustomView(View view, CustomViewCallback done) { + if (fullscreen != null) { done.onCustomViewHidden(); return; } + fullscreen = view; + fullscreenDone = done; + root.addView(view, new FrameLayout.LayoutParams( + ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT)); + web.setVisibility(View.GONE); + } + + @Override + public void onHideCustomView() { + exitFullscreen(); + } + } + + private void exitFullscreen() { + if (fullscreen == null) return; + root.removeView(fullscreen); + fullscreen = null; + web.setVisibility(View.VISIBLE); + if (fullscreenDone != null) fullscreenDone.onCustomViewHidden(); + fullscreenDone = null; + } + + private boolean held(String permission) { + return Build.VERSION.SDK_INT < Build.VERSION_CODES.M + || checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED; + } + + @Override + public void onRequestPermissionsResult(int code, String[] permissions, int[] results) { + if (code != MEDIA_PERMISSIONS || pendingPermission == null) return; + PermissionRequest request = pendingPermission; + pendingPermission = null; + List grant = new ArrayList<>(); + for (String r : request.getResources()) { + if (PermissionRequest.RESOURCE_AUDIO_CAPTURE.equals(r) && held(Manifest.permission.RECORD_AUDIO)) + grant.add(r); + else if (PermissionRequest.RESOURCE_VIDEO_CAPTURE.equals(r) && held(Manifest.permission.CAMERA)) + grant.add(r); + } + if (grant.isEmpty()) request.deny(); + else request.grant(grant.toArray(new String[0])); + } + + @Override + protected void onActivityResult(int code, int result, Intent data) { + if (code != FILE_CHOOSER) { super.onActivityResult(code, result, data); return; } + if (pendingFiles == null) return; + pendingFiles.onReceiveValue(WebChromeClient.FileChooserParams.parseResult(result, data)); + pendingFiles = null; + } +} diff --git a/appstores/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/appstores/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..69b891ede548736792417d9941ad796b8588494f GIT binary patch literal 3232 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 literal 0 HcmV?d00001 diff --git a/appstores/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/appstores/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..aa26b689a176abdad1f1c72de1875ee8c28b13ee GIT binary patch literal 1858 zcmcIldpOi-8~#mLr)-0+-3>X%l+zePh^YxNF^;VqLUM@R?Q$B5BD;>&mdZ-LY7?UR zcH6ccdtntr(OQQZj6+5gQ3hK(4q@HJgT z$r|n~*cgMM4>x;ow!z8(Hbyv`hv6Z-nnXh#_PAn)B`QlX{sI%@_`V9xHt)|lntVd8c{%ySZ9sV4cYQn(`qHeGm_-_Sfrs3;?t;T2(pyOAVX(1{I z-&caU2~mNVdN%3_SoS5)&LVqu$QtaK0ua00qgvkj! zXv97*uq-gxj{tX=Xu{DPpM^r$3A-&QIgfXX_^uL;7AQQ0z8+jEMA;=o2HT_Y1awc=Z)a|ucjLcNM5t4H0P2Q*vKCniRE zpPlU|xs(I$uFlTy_!k9aBG2P+UF2UY%PFdvDZ<&4AY}YF&g?yIt>U7IV0AV%Z*+%! zwyau3E+G;-Q7NiyFA>e>>--o=06}3t@phJzQx)6}plGH@LLQtvFe`3o5u2pWyR9kE zNxt9CN@`e0Ih>Lr5{w>ay5=&fGREuMJH*Z1-OXZ^byrF<#zTi$N@3S`UzdKmxR`3$ zPE>l;HUy~?rZ(Q{lU`*hb8LZLX>pFM{D5A(Ez?6ekfspmZ^#e7W}~RJwvSD9)X$}b zOCHtu=qvF?Z4}B1GT$hkC@*ZeLl_lsIUBcz>g#_&J>qEJ$aQm4;PyDt6^}CQLI(5A z`CBs=G(X4;S9-$jxEOue^Mey5!u7hQH$E271wmR@*}OojT1j9eJvffS7bFrBSI4@U z#>!JKJ7pv*atdbq9{3K-6|1gQf0UmQwA?_<+`i zPn4BT*|H4H-CsPc@m60{oTL6yRuehCW#!<{yyO`Ts|#6oeH#rZ89nRQzG$AReY>+h z@1%!M~32|>we3QSf_S8AC+(U6rwwvmu*;e_j>j84b`UHCDG3jyJF<>-J2Q{c+U;z zrz`z-C0gk^99l2j5_Y~OmT2oL{V72!GNSWJgSYIftirgU-JiF*)Q`D$EYm>T9TLm6H_V)&8H;RS6`b@K4+ zWYhi9l2A2s*511M-eXchv+HI~LC1G?P)r+)y&F6%lxwovJaxtx*4v%Bjz&)Z+OSac z;s=`1U>Nma#)-PM--K#LC0B9O?|dRj&AC;6(=5Q6a;+o4)N$5&XWVd5T4wSkRgDs< zSUTg_DawbJXfA`G|%EqhKa#IcCzh~ZyHsvqVeiR?~TJaIS zE$pfir&u`9vU;*OMU69I4JuUGn4WY8nrwoRmx$_8kvMs0^m*Y!Dcb&gSnk%IlaUg# zT!d5e@>cIu{ME8I93y|+CZ0(7D4@D*M!Vks&yGGshFgfrDg5%=pe0%5q Gul@q*gqzX; literal 0 HcmV?d00001 diff --git a/appstores/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/appstores/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..45790cc6a87dacf6c140e0c4d9cb7128b3855710 GIT binary patch literal 4421 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 literal 0 HcmV?d00001 diff --git a/appstores/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/appstores/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..94b3d1fcc4c3fd9832b695f7d83f4e82a2df6d2c GIT binary patch literal 9168 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 + + PairUX + diff --git a/appstores/android/app/src/main/res/values/styles.xml b/appstores/android/app/src/main/res/values/styles.xml new file mode 100644 index 00000000..8f8aa502 --- /dev/null +++ b/appstores/android/app/src/main/res/values/styles.xml @@ -0,0 +1,14 @@ + + + #0f172a + #3b82f6 + + + diff --git a/appstores/android/build.gradle b/appstores/android/build.gradle new file mode 100644 index 00000000..bb97a9ac --- /dev/null +++ b/appstores/android/build.gradle @@ -0,0 +1 @@ +plugins { id 'com.android.application' version '8.5.2' apply false } diff --git a/appstores/android/build.sh b/appstores/android/build.sh new file mode 100755 index 00000000..aff260f3 --- /dev/null +++ b/appstores/android/build.sh @@ -0,0 +1,35 @@ +#!/bin/sh +# Builds the Play bundle and a sideloadable APK. Runs inside gradle:8.7-jdk17: +# docker run --rm -e OWNER=\$(id -u):\$(id -g) \\ +# -v "\$PWD/android-sdk:/sdk" -v "\$PWD/keys:/keys" \\ +# -v "\$PWD/appstores/android:/w" -w /w gradle:8.7-jdk17 sh build.sh +# /sdk Android SDK cache (installed on first run) +# /keys optional: upload.jks + upload.env to sign with +set -eu +SDK=/sdk +TOOLS=commandlinetools-linux-11076708_latest.zip +if [ ! -x "$SDK/cmdline-tools/latest/bin/sdkmanager" ]; then + mkdir -p "$SDK/cmdline-tools" + curl -fsSL "https://dl.google.com/android/repository/$TOOLS" -o /tmp/tools.zip + unzip -q /tmp/tools.zip -d "$SDK/cmdline-tools" + mv "$SDK/cmdline-tools/cmdline-tools" "$SDK/cmdline-tools/latest" +fi +yes | "$SDK/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$SDK" --licenses >/dev/null +"$SDK/cmdline-tools/latest/bin/sdkmanager" --sdk_root="$SDK" "platforms;android-35" "build-tools;35.0.0" >/dev/null +export ANDROID_HOME="$SDK" + +if [ -f /keys/upload.jks ]; then + set -a; . /keys/upload.env; set +a + export ANDROID_KEYSTORE=/keys/upload.jks +fi + +# bundleRelease is what Play takes; assembleRelease is for sideloading and testing. +gradle --no-daemon -q lint bundleRelease assembleRelease +mkdir -p dist +cp app/build/outputs/bundle/release/app-release.aab "dist/pairux.aab" +APK=$(ls app/build/outputs/apk/release/*.apk | head -1) +cp "$APK" "dist/pairux.apk" +"$SDK/build-tools/35.0.0/apksigner" verify --print-certs "dist/pairux.apk" | head -3 || true +"$SDK/build-tools/35.0.0/aapt2" dump badging "dist/pairux.apk" | grep -E "^(package|sdkVersion|targetSdkVersion|application-label)" || true +[ -n "${OWNER:-}" ] && chown -R "$OWNER" dist app/build .gradle 2>/dev/null +ls -l dist diff --git a/appstores/android/gradle.properties b/appstores/android/gradle.properties new file mode 100644 index 00000000..4b178b1d --- /dev/null +++ b/appstores/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx2g +android.nonTransitiveRClass=true +android.useAndroidX=true diff --git a/appstores/android/new-key.sh b/appstores/android/new-key.sh new file mode 100755 index 00000000..ff0ba062 --- /dev/null +++ b/appstores/android/new-key.sh @@ -0,0 +1,18 @@ +#!/bin/sh +# Mints the Play upload key once. Runs inside eclipse-temurin:17-jdk with the output +# dir mounted at /keys. Store both files in the vault (pairux-android--signing as +# ANDROID_KEYSTORE_B64 + ANDROID_STORE_PASSWORD), never in the repo. +# +# This is the UPLOAD key, not the app signing key: Play keeps the signing key once +# you enrol in Play App Signing, so losing this one is recoverable (Google can reset +# it) while losing a Play-managed signing key is not. Do not run this twice anyway. +set -eu +[ -f /keys/upload.jks ] && { echo "refusing: /keys/upload.jks exists"; exit 1; } +umask 077 +PW=$(head -c 24 /dev/urandom | od -An -tx1 | tr -d ' \n') +keytool -genkeypair -keystore /keys/upload.jks -storetype PKCS12 -alias pairux \ + -keyalg RSA -keysize 4096 -validity 36500 -storepass "$PW" \ + -dname "CN=PairUX (pairux.com), O=Profullstack, C=US" >/dev/null 2>&1 +printf 'ANDROID_STORE_PASSWORD=%s\nANDROID_KEY_ALIAS=pairux\n' "$PW" > /keys/upload.env +[ -n "${OWNER:-}" ] && chown "$OWNER" /keys/upload.jks /keys/upload.env +keytool -list -keystore /keys/upload.jks -storepass "$PW" | tail -2 diff --git a/appstores/android/settings.gradle b/appstores/android/settings.gradle new file mode 100644 index 00000000..3b4633ef --- /dev/null +++ b/appstores/android/settings.gradle @@ -0,0 +1,9 @@ +pluginManagement { + repositories { google(); mavenCentral(); gradlePluginPortal() } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { google(); mavenCentral() } +} +rootProject.name = "pairux-android" +include ":app" diff --git a/appstores/android/store/listing.md b/appstores/android/store/listing.md new file mode 100644 index 00000000..1e26a020 --- /dev/null +++ b/appstores/android/store/listing.md @@ -0,0 +1,28 @@ +# Play listing: PairUX + +- **App name:** PairUX +- **Package:** `com.pairux.app` +- **Category:** Productivity +- **Price:** free, no ads, no in-app purchases +- **Contact:** anthony@profullstack.com +- **Privacy policy:** https://pairux.com/privacy + +## Short description (80 characters max) + +Share a screen and hand over the mouse. Open source Screenhero. + +## Full description + +See `../../firetv/store/listing.md` for the long description; it carries over +word for word. What does not carry over is the art: Play wants its own. + +## Art Play requires + +| Slot | Size | File | +| --- | --- | --- | +| App icon | 512x512 PNG | `out/icon-512.png` | +| Feature graphic | 1024x500 PNG, no transparency | `out/feature-1024x500.png` | +| Phone screenshots | 2-8, min 320px, 16:9 or 9:16 | `shots/` | + +Phone screenshots are not the Fire TV ones: those are 1920x1080 landscape of a +TV layout. Capture the responsive site at phone size instead. diff --git a/appstores/android/store/render.sh b/appstores/android/store/render.sh new file mode 100755 index 00000000..dfcacaac --- /dev/null +++ b/appstores/android/store/render.sh @@ -0,0 +1,30 @@ +#!/bin/sh +# Renders the launcher icons and the Play listing art from store/src. +# docker run --rm -v "\$PWD/appstores/android:/w" -w /w alpine:3.20 sh store/render.sh +# Play wants a 512x512 icon and a 1024x500 feature graphic; the feature graphic is +# flattened because Play rejects transparency there. +set -eu +apk add --no-cache imagemagick rsvg-convert >/dev/null +R=app/src/main/res +O=store/out +S=store/src +mkdir -p "$O" + +if [ -f "$S/mark.png" ]; then MARK="$S/mark.png"; else + rsvg-convert -w 1024 -h 1024 "$S/mark.svg" -o /tmp/mark.png; MARK=/tmp/mark.png; fi + +for d in mdpi:48 hdpi:72 xhdpi:96 xxhdpi:144 xxxhdpi:192; do + dpi=${d%%:*}; px=${d##*:} + mkdir -p "$R/mipmap-$dpi" + magick "$MARK" -resize ${px}x${px} "$R/mipmap-$dpi/ic_launcher.png" +done + +magick "$MARK" -resize 512x512 "$O/icon-512.png" + +if [ -f "$S/wordmark.png" ]; then LOCK="$S/wordmark.png"; elif [ -f "$S/wordmark.svg" ]; then + rsvg-convert -w 900 "$S/wordmark.svg" -o /tmp/lock.png; LOCK=/tmp/lock.png; else LOCK="$MARK"; fi +magick -size 1024x500 xc:'#0f172a' \( "$LOCK" -resize 760x340 \) -gravity center -composite \ + -alpha remove -alpha off "$O/feature-1024x500.png" + +[ -n "${OWNER:-}" ] && chown -R "$OWNER" "$R" "$O" +ls -l "$O" diff --git a/appstores/android/store/src/mark.svg b/appstores/android/store/src/mark.svg new file mode 100644 index 00000000..d5e8ef28 --- /dev/null +++ b/appstores/android/store/src/mark.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/appstores/android/store/src/wordmark.svg b/appstores/android/store/src/wordmark.svg new file mode 100644 index 00000000..cbad525d --- /dev/null +++ b/appstores/android/store/src/wordmark.svg @@ -0,0 +1 @@ + \ No newline at end of file From f40df2a9d6fc606aac99596567d43ba4c04a7339 Mon Sep 17 00:00:00 2001 From: Anthony Ettinger Date: Thu, 5 Nov 2026 09:50:17 +0000 Subject: [PATCH 2/2] chore: prettier on the Android docs, explicit button type on the offline page Co-Authored-By: Claude Opus 5 (1M context) --- appstores/android/app/src/main/assets/offline.html | 2 +- appstores/android/store/listing.md | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/appstores/android/app/src/main/assets/offline.html b/appstores/android/app/src/main/assets/offline.html index d9ad2286..a6c125bc 100644 --- a/appstores/android/app/src/main/assets/offline.html +++ b/appstores/android/app/src/main/assets/offline.html @@ -20,4 +20,4 @@

No connection

PairUX needs the network to reach pairux.com.

- + diff --git a/appstores/android/store/listing.md b/appstores/android/store/listing.md index 1e26a020..075f17d8 100644 --- a/appstores/android/store/listing.md +++ b/appstores/android/store/listing.md @@ -18,11 +18,11 @@ word for word. What does not carry over is the art: Play wants its own. ## Art Play requires -| Slot | Size | File | -| --- | --- | --- | -| App icon | 512x512 PNG | `out/icon-512.png` | -| Feature graphic | 1024x500 PNG, no transparency | `out/feature-1024x500.png` | -| Phone screenshots | 2-8, min 320px, 16:9 or 9:16 | `shots/` | +| Slot | Size | File | +| ----------------- | ----------------------------- | -------------------------- | +| App icon | 512x512 PNG | `out/icon-512.png` | +| Feature graphic | 1024x500 PNG, no transparency | `out/feature-1024x500.png` | +| Phone screenshots | 2-8, min 320px, 16:9 or 9:16 | `shots/` | Phone screenshots are not the Fire TV ones: those are 1920x1080 landscape of a TV layout. Capture the responsive site at phone size instead.