Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions appstores/android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
.gradle/
app/build/
build/
dist/
local.properties
63 changes: 45 additions & 18 deletions appstores/android/README.md
Original file line number Diff line number Diff line change
@@ -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, `<input type=file>` 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`.
46 changes: 46 additions & 0 deletions appstores/android/app/build.gradle
Original file line number Diff line number Diff line change
@@ -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
}
}
38 changes: 38 additions & 0 deletions appstores/android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">

<uses-permission android:name="android.permission.INTERNET" />
<!-- Only asked for when the page asks, and only granted if the viewer says yes. -->
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.microphone" android:required="false" />
<uses-feature android:name="android.hardware.camera" android:required="false" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme"
android:usesCleartextTraffic="false">

<activity
android:name=".MainActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboard|keyboardHidden|navigation|uiMode"
android:exported="true"
android:launchMode="singleTask"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<!-- Opening a pairux.com link picks the app once the viewer allows it. -->
<intent-filter android:autoVerify="true">
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="https" android:host="pairux.com" />
</intent-filter>
</activity>
</application>
</manifest>
23 changes: 23 additions & 0 deletions appstores/android/app/src/main/assets/offline.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>PairUX is offline</title>
<style>
html { color-scheme: dark }
body {
margin: 0; min-height: 100vh; display: grid; place-content: center; gap: 1rem;
padding: 2rem; text-align: center;
background: #0f172a; color: #e7ecf4;
font: 16px/1.5 system-ui, sans-serif;
}
h1 { margin: 0; font-size: 1.4rem }
p { margin: 0; opacity: .72 }
button {
justify-self: center; margin-top: .5rem; padding: .7rem 1.4rem;
font: inherit; font-weight: 600; color: #0f172a; background: #3b82f6;
border: 0; border-radius: 10px;
}
</style>
<h1>No connection</h1>
<p>PairUX needs the network to reach pairux.com.</p>
<button type="button" onclick="location.href='https://pairux.com/'">Try again</button>
Loading
Loading