diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 1cf21ca..4096fe1 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -53,14 +53,16 @@ jobs:
- uses: actions/upload-artifact@v7
id: apk
with:
- name: message487-apks
- path: app/build/outputs/apk/**/*.apk
+ name: message487-builds
+ path: |
+ app/build/outputs/apk/**/*.apk
+ app/build/outputs/bundle/**/*.aab
if-no-files-found: error
retention-days: 14
- - name: Link APK artifacts
+ - name: Link build artifacts
env:
ARTIFACT_URL: ${{ steps.apk.outputs.artifact-url }}
- run: echo "[Download debug and unsigned release APKs]($ARTIFACT_URL)" >> "$GITHUB_STEP_SUMMARY"
+ run: echo "[Download debug APK and unsigned release APK/AAB]($ARTIFACT_URL)" >> "$GITHUB_STEP_SUMMARY"
python:
name: Python tests and style
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index cb6de93..d039919 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -16,7 +16,7 @@ concurrency:
jobs:
release:
- name: Build and verify signed APK
+ name: Build and verify signed APK and AAB
runs-on: ubuntu-latest
timeout-minutes: 40
steps:
@@ -97,6 +97,6 @@ jobs:
run: |
cd dist/release
sha256sum -c SHA256SUMS
- gh release create "$GITHUB_REF_NAME" ./*.apk mapping.txt SHA256SUMS \
+ gh release create "$GITHUB_REF_NAME" ./*.apk ./*.aab mapping.txt SHA256SUMS \
--repo "$GITHUB_REPOSITORY" --verify-tag --generate-notes \
--title "Message487 $GITHUB_REF_NAME"
diff --git a/README.md b/README.md
index aba61ec..55a6884 100644
--- a/README.md
+++ b/README.md
@@ -1,18 +1,33 @@
# Message487
+[](https://github.com/andre487/AndroidMessage487/actions/workflows/ci.yml?query=branch%3Amain+event%3Apush)
+[](https://github.com/andre487/AndroidMessage487/releases/latest)
+[](LICENSE)
+[](https://developer.android.com/about/versions/oreo)
+
+
+
+
+
Message487 connects selected Android notifications and incoming SMS to your n8n workflows.
A custom webhook is also supported. Telegram forwarding is one possible workflow; the Android
app does not depend on Telegram.
**Status:** development preview with notification/SMS capture, a persistent encrypted outbox,
-background delivery, automatic retries and a delivery journal. Webhook requests require a Bearer token, stored encrypted on the device. Signed APK release automation is configured; see [Releases](docs/en/releases.md).
+background delivery, automatic retries and a delivery journal. Webhook requests require a Bearer token, stored encrypted on the device. Signed APK/AAB release automation is configured; see [Releases](docs/en/releases.md).
## Getting started
+**[Download the latest signed APK](https://github.com/andre487/AndroidMessage487/releases/latest/download/message487.apk)**
+
+The stable link follows the latest published release; it does not point to development builds.
+Read the [release notes](https://github.com/andre487/AndroidMessage487/releases/latest) for supported features.
+Release 0.0.1 predates Bearer authentication.
+
Guides: [Install from APK](docs/en/apk-installation.md) · [n8n webhook](docs/en/n8n-webhook.md) · [Telegram forwarding](docs/en/n8n-telegram.md).
На русском: [Установка из APK](docs/ru/apk-installation.md) · [n8n webhook](docs/ru/n8n-webhook.md) · [Пересылка в Telegram](docs/ru/n8n-telegram.md).
-1. Save the full published webhook URL and a device code in **Connection**. Send a test event.
+1. Save the full published webhook URL, a Bearer token and a device code in **Connection**. Send a test event.
2. Check **Journal** and find the same event ID in n8n **Executions**.
3. In **Sources**, enable notification forwarding, grant notification access in Android settings,
and select applications. Add a package manually if it has no launcher icon.
@@ -46,7 +61,7 @@ connection failures and transient HTTP errors retry with backoff; other HTTP fai
confirmations need a manual retry from **Journal** after fixing the server. A lost response can
cause duplicate delivery: downstream workflows should deduplicate using `event_id`.
-Queued events retain the URL and confirmation mode from capture time. Changing the connection
+Queued events retain the URL, token and confirmation mode from capture time. Changing the connection
does not reroute them. Undelivered events remain until confirmed or explicitly deleted. The
journal persists across restarts and hides message contents. Tap an event for its ID, HTTP result,
retry and deletion controls. Confirmed payloads are removed;
@@ -95,8 +110,11 @@ bottom navigation on phones and rail navigation on wider windows. See the [desig
for the visual conventions and references.
Fastlane's `debug_artifact` lane builds only the debug APK. `checks` runs JVM/Robolectric tests,
-debug/release lint, and builds debug and unsigned release APKs under `app/build/outputs/apk/`.
+debug/release lint, and builds debug and unsigned release APKs under `app/build/outputs/apk/`, plus an unsigned AAB under
+`app/build/outputs/bundle/`.
PR CI has no release signing credentials and does not require an emulator.
+For signed APK/AAB artifacts and manual Play Console upload, see [Releases](docs/en/releases.md).
+Store graphics and their provenance are documented in [Branding](assets/branding/README.md).
See the [project context](docs/en/project-context.md) for remaining product decisions.
This project succeeds [sms487](https://github.com/andre487/sms487).
diff --git a/app/build.gradle.kts b/app/build.gradle.kts
index b955288..cc72932 100644
--- a/app/build.gradle.kts
+++ b/app/build.gradle.kts
@@ -21,8 +21,8 @@ android {
applicationId = "life.andre.message487"
minSdk = 26
targetSdk = 36
- versionCode = 1
- versionName = "0.0.1"
+ versionCode = 2
+ versionName = "0.0.2"
}
signingConfigs {
if (signingInputs.all { !it.isNullOrBlank() }) {
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index ee4b6d0..ca8ace0 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -22,6 +22,7 @@
android:networkSecurityConfig="@xml/network_security_config"
android:label="@string/app_name"
android:icon="@mipmap/ic_launcher"
+ android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Message487">
-
-
+ android:width="108dp"
+ android:height="108dp"
+ android:viewportWidth="108"
+ android:viewportHeight="108"
+ android:tint="#FFFFFF">
+
+
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
index fffffba..3c0ab76 100644
--- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml
@@ -1,4 +1,5 @@
+
diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
new file mode 100644
index 0000000..3c0ab76
--- /dev/null
+++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..cd3c0b3
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher_round.png b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png
new file mode 100644
index 0000000..08ef3d6
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..f58aa23
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher_round.png b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png
new file mode 100644
index 0000000..82ba040
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..6dfd531
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..0bb4d52
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..e27a241
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..06235e2
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..ee6fa15
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png
new file mode 100644
index 0000000..46a687c
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png differ
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index d477f75..8d45ecf 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -1,3 +1,3 @@
- #315DA8
+ #3F51B5
diff --git a/assets/branding/README.md b/assets/branding/README.md
new file mode 100644
index 0000000..8a23e86
--- /dev/null
+++ b/assets/branding/README.md
@@ -0,0 +1,27 @@
+# Message487 branding
+
+| Asset | Use |
+| --- | --- |
+| `message487-icon.png` | Google Play app icon, 512×512 RGBA PNG; also used in README |
+| `message487-feature.png` | Google Play feature graphic, 1024×500 RGB PNG without alpha; shared by both locales |
+| `message487-feature-master.png` | Original generated banner, retained for future exports |
+| `screenshots/en/`, `screenshots/ru/` | Actual phone screenshots in English and Russian, 1080×1920 |
+
+Launcher PNGs and the adaptive foreground are copied from
+[andre487/sms487](https://github.com/andre487/sms487/tree/d4aca0724c4d8c8cfcfe128c6df6cc93f64625f2/client/app/src/main).
+The monitor, star, phone and indigo background preserve the original app identity.
+The existing vector foreground also supplies the Android themed-icon mask; round launcher
+resources and legacy notification icons are included. The app UI retains its purple Material theme.
+The original artwork is distributed under the [sms487 MIT license](sms487-LICENSE).
+
+The feature illustration was produced with the built-in `image_gen` tool using the old
+Play Store icon as its reference, then exported with `sips -z 500 1024` as an opaque PNG.
+The generation prompt is recorded in [feature-prompt.txt](feature-prompt.txt).
+It extends the icon's device imagery with notification cards and workflow nodes and uses
+no localized text or third-party service marks. It is promotional artwork, not an app screenshot.
+
+For a Play listing, upload `message487-icon.png` as the app icon and `message487-feature.png`
+as the feature graphic. The screenshots were captured from the debug app on the API 35 emulator with synthetic
+local data, using Android app locales and a 1080×1920 display. Refresh them from the release
+being submitted whenever its UI changes; do not use generated illustrations as screenshots. Follow the current
+[Google Play asset requirements](https://support.google.com/googleplay/android-developer/answer/9866151?hl=en).
diff --git a/assets/branding/feature-prompt.txt b/assets/branding/feature-prompt.txt
new file mode 100644
index 0000000..ff7f801
--- /dev/null
+++ b/assets/branding/feature-prompt.txt
@@ -0,0 +1,4 @@
+Tool: built-in image_gen
+Reference: message487-icon.png (original sms487 Play Store icon)
+
+Use case: ads-marketing. Create a finished Google Play feature graphic for the Android application Message487, precisely 1024 x 500 pixels, opaque RGB PNG. Reference image is the old sms487 icon: indigo background and a white outlined desktop monitor with a five-point star, next to a smartphone. Extend this visual language into a polished restrained flat Material Design editorial illustration about forwarding phone notifications and SMS to workflows. Do not simply enlarge or repeat the app icon: use a smaller smartphone with two abstract notification cards connected by graceful simple lines to a desktop monitor and three small workflow nodes. Keep the distinctive star on the monitor. Deep indigo and violet with soft lavender fields, white outlines, very subtle depth, clean geometry, ample empty margins. No text, letters, numbers, badges, logos of other services, people, photos, or fake screenshots. Keep all meaningful content within the central 75 percent width and central 70 percent height so nothing is cropped in store previews. Crisp professional composition, visually balanced, not crowded. This is final promotional illustration, not a mockup of a store page.
diff --git a/assets/branding/message487-feature-master.png b/assets/branding/message487-feature-master.png
new file mode 100644
index 0000000..fba0087
Binary files /dev/null and b/assets/branding/message487-feature-master.png differ
diff --git a/assets/branding/message487-feature.png b/assets/branding/message487-feature.png
new file mode 100644
index 0000000..58cdea4
Binary files /dev/null and b/assets/branding/message487-feature.png differ
diff --git a/assets/branding/message487-icon.png b/assets/branding/message487-icon.png
new file mode 100644
index 0000000..1288b9a
Binary files /dev/null and b/assets/branding/message487-icon.png differ
diff --git a/assets/branding/screenshots/en/overview.png b/assets/branding/screenshots/en/overview.png
new file mode 100644
index 0000000..3731158
Binary files /dev/null and b/assets/branding/screenshots/en/overview.png differ
diff --git a/assets/branding/screenshots/en/sources.png b/assets/branding/screenshots/en/sources.png
new file mode 100644
index 0000000..98b4a1d
Binary files /dev/null and b/assets/branding/screenshots/en/sources.png differ
diff --git a/assets/branding/screenshots/ru/overview.png b/assets/branding/screenshots/ru/overview.png
new file mode 100644
index 0000000..09140ad
Binary files /dev/null and b/assets/branding/screenshots/ru/overview.png differ
diff --git a/assets/branding/screenshots/ru/sources.png b/assets/branding/screenshots/ru/sources.png
new file mode 100644
index 0000000..5e4ba2a
Binary files /dev/null and b/assets/branding/screenshots/ru/sources.png differ
diff --git a/assets/branding/sms487-LICENSE b/assets/branding/sms487-LICENSE
new file mode 100644
index 0000000..c785283
--- /dev/null
+++ b/assets/branding/sms487-LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2018 Andrey Prokopyuk
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/docs/en/n8n-webhook.md b/docs/en/n8n-webhook.md
index 131e47d..18003c6 100644
--- a/docs/en/n8n-webhook.md
+++ b/docs/en/n8n-webhook.md
@@ -4,7 +4,7 @@
Installing on a phone? See [APK installation and Android restrictions](apk-installation.md).
-This guide targets the development version with Bearer authentication (not release 0.0.1). You need the n8n editor and an HTTPS endpoint
+This guide targets Message487 0.0.2 and later, with Bearer authentication. You need the n8n editor and an HTTPS endpoint
reachable from your phone. For a local Android emulator, use the debug build and
[DevServer](../../DevServer/README.md). Release APKs reject HTTP endpoints.
diff --git a/docs/en/releases.md b/docs/en/releases.md
index b8a991a..0ca6bc4 100644
--- a/docs/en/releases.md
+++ b/docs/en/releases.md
@@ -1,13 +1,26 @@
-# Signed APK releases
+# Signed APK and App Bundle releases
[English](../en/releases.md) | [Русский](../ru/releases.md)
Installing on a phone? See [APK installation and Android restrictions](apk-installation.md).
-Run `bundle exec fastlane android release_artifacts` with JDK 21 and Android SDK 36. The lane runs
-Android JVM/Compose tests, debug/release lint and a signed release build, then checks the certificate,
-package ID, version and non-debuggable flag. Outputs are `dist/release/message487-.apk`,
-R8 `mapping.txt` and `SHA256SUMS`. Keep the mapping with its exact APK when diagnosing crashes.
+Run `bundle exec fastlane android release_artifacts` with JDK 21 and Android SDK 36.
+The lane runs Android JVM/Compose tests and debug/release lint, then builds signed APK and AAB
+from the same release variant. It checks the APK certificate, package/version and non-debuggable
+flag, and verifies the certificate and signature of every AAB payload entry.
+
+| Output in `dist/release/` | Purpose |
+| --- | --- |
+| `message487-.apk` | Versioned signed APK |
+| `message487.apk` | Byte-identical APK with a stable download filename |
+| `message487.aab` | Signed Android App Bundle for manual Play Console upload |
+| `mapping.txt` | R8 mapping for this exact build |
+| `SHA256SUMS` | Checksums for both APK names, AAB and mapping |
+
+The [permanent APK link](https://github.com/andre487/AndroidMessage487/releases/latest/download/message487.apk)
+follows GitHub's latest published release, not the current branch. Existing versioned URLs keep
+working. A copy of the original 0.0.1 APK provides the stable filename for that release;
+it does not acquire features added after its tag.
Signing follows MegaProxy's environment contract with the `MESSAGE487_` prefix:
@@ -23,7 +36,7 @@ Signing follows MegaProxy's environment contract with the `MESSAGE487_` prefix:
Gradle reads only the four signing environment variables (path, store password, alias and key
password). Partial signing configuration fails closed. The PR `checks` lane rejects signing inputs
-and verifies an unsigned release APK. Passwords are not command-line arguments and must never be
+and verifies unsigned release APK and AAB files. Passwords are not command-line arguments and must never be
printed, committed or passed through Gradle `-P` properties.
GitHub Secrets use the same names as MegaProxy: `ANDROID_SIGNING_KEY_BASE64`,
@@ -34,14 +47,37 @@ GitHub Release. PR workflows do not consume signing secrets.
## Verification and publication
-- Push a `release-check/*` tag to build and verify a signed APK in GitHub Actions without publishing
- a Release. The signed APK, checksums, mapping and test reports are available as Actions artifacts.
+- Push a `release-check/*` tag to build and verify signed APK/AAB files in GitHub Actions without publishing
+ a Release. The signed APK/AAB files, checksums, mapping and test reports are available as Actions artifacts.
- After the workflow is merged into the default branch, manual dispatch also builds artifacts only.
- For publication, increment `versionCode`, set the intended `versionName` in `app/build.gradle.kts`,
and merge the reviewed change after all required PR checks pass. Push the matching `v`
tag. The workflow requires the tag commit to be contained in `main` and rejects a version mismatch.
- It publishes the verified APK, mapping and checksums to GitHub Releases. An existing Release is
+ It publishes the verified APKs, AAB, mapping and checksums to GitHub Releases. An existing Release is
not overwritten by a rerun.
The first configured version is `0.0.1` with `versionCode = 1`. Later releases must increase
`versionCode` to support Android upgrades. Keep using the same signing key for installed users.
+
+## Upload to Google Play
+
+1. Build release artifacts as above or download them from a verified release workflow run.
+ Use a new `versionCode` for each Play upload; increase it in `app/build.gradle.kts` before building.
+2. Create/select the Play Console app for `life.andre.message487` and configure Play App Signing.
+ The AAB uses the same configured key as the GitHub APK. Confirm that Play accepts it as the
+ upload key. To allow updates between GitHub and Play installs, plan the **app signing key**
+ consistently; an upload key and a Play-generated app signing key are different roles.
+ See [Android signing guidance](https://developer.android.com/studio/publish/app-signing).
+3. Create an internal-testing release and upload `message487.aab`. An AAB is a publishing
+ artifact; install the APK on phones, not the AAB. Keep `mapping.txt` with the build
+ (AGP also embeds the R8 mapping in the bundle).
+4. Add the icon and feature graphic from [Branding](../../assets/branding/README.md), real app
+ screenshots, descriptions, privacy policy and Data safety answers. Complete Play's
+ [SMS permissions declaration/review](https://support.google.com/googleplay/android-developer/answer/10208820?hl=en)
+ for `RECEIVE_SMS`; a successful AAB build does not establish store eligibility.
+5. Review the internal release in Play Console before rolling it out.
+
+CI builds and verifies the artifacts; it does not upload to Google Play or require a Play service
+account. Native symbol packaging from MegaProxy is unnecessary here: this app has no native core.
+The bundle signature verifier also rejects unsigned added entries, modified entries, missing
+required bundle entries and unexpected certificates; its regression fixtures run in Android CI.
diff --git a/docs/en/testing.md b/docs/en/testing.md
index 209093f..24d2c65 100644
--- a/docs/en/testing.md
+++ b/docs/en/testing.md
@@ -6,7 +6,7 @@ Run the same suites locally and in GitHub Actions:
| Suite | Command | Coverage |
| --- | --- | --- |
-| Android | `bundle exec fastlane android checks` | JVM logic, MockWebServer HTTP integration, Robolectric database/preferences/provider integration, Compose interactions, localization, manifest security, light/dark contrast, debug/release lint and APK builds, unsigned release verification |
+| Android | `bundle exec fastlane android checks` | JVM logic, MockWebServer HTTP integration, Robolectric database/preferences/provider integration, Compose interactions, localization, manifest security, light/dark contrast, debug/release lint and APK/AAB builds, unsigned release verification |
| Python | `PYTHON=.venv/bin/python bundle exec fastlane android python_checks` | DevServer HTTP test-client behavior against a local HTTP server; pinned Black/isort style checks |
| n8n | `bundle exec fastlane android server_tests` | Live receive/validation, notification/SMS/test payloads, HTTP failure, invalid ACK and timeout |
@@ -16,7 +16,7 @@ The server suite uses synthetic data and retains it in the development execution
`.github/workflows/ci.yml` runs all three jobs on pull requests, main pushes and manual dispatch.
Android XML/HTML test and lint reports are uploaded even if a check fails. Successful Android jobs
-also publish debug and unsigned release APKs. These checks never sign release artifacts.
+also publish a debug APK and unsigned release APK/AAB files. These checks never sign release artifacts.
Local success does not establish a GitHub run result for uncommitted/unpushed changes.
## Comparison with MegaProxy
diff --git a/docs/ru/n8n-webhook.md b/docs/ru/n8n-webhook.md
index 56c0b7b..6745eaa 100644
--- a/docs/ru/n8n-webhook.md
+++ b/docs/ru/n8n-webhook.md
@@ -4,7 +4,7 @@
Для установки на телефон см. [инструкцию по APK и ограничениям Android](apk-installation.md).
-Инструкция для разрабатываемой версии с Bearer-авторизацией (не для релиза 0.0.1). Нужен доступ к редактору n8n и HTTPS-адрес,
+Инструкция для Message487 0.0.2 и новее, с Bearer-авторизацией. Нужен доступ к редактору n8n и HTTPS-адрес,
доступный с телефона. Для локального Android-эмулятора используйте debug-сборку и
[готовый DevServer](../../DevServer/README.md). Релизный APK не принимает HTTP-адреса.
diff --git a/docs/ru/releases.md b/docs/ru/releases.md
index c8099ec..129d25b 100644
--- a/docs/ru/releases.md
+++ b/docs/ru/releases.md
@@ -1,14 +1,26 @@
-# Выпуск подписанного APK
+# Выпуск подписанных APK и App Bundle
[English](../en/releases.md) | [Русский](../ru/releases.md)
Для установки на телефон см. [инструкцию по APK и ограничениям Android](apk-installation.md).
Запустите `bundle exec fastlane android release_artifacts` с JDK 21 и Android SDK 36.
-Lane выполняет JVM/Compose-тесты, debug/release lint и подписанную release-сборку,
-затем проверяет сертификат, package ID, версию и отсутствие debug-флага. Результаты:
-`dist/release/message487-.apk`, файл R8 `mapping.txt` и `SHA256SUMS`.
-При разборе крешей используйте mapping от конкретного APK.
+Lane выполняет JVM/Compose-тесты и debug/release lint, затем собирает подписанные APK и AAB
+из одного release-варианта. Проверяются сертификат, пакет, версия и отсутствие debug-флага
+APK, а также сертификат и подпись каждого содержательного файла AAB.
+
+| Файл в `dist/release/` | Назначение |
+| --- | --- |
+| `message487-.apk` | Подписанный APK с версией в имени |
+| `message487.apk` | Побайтовая копия APK с постоянным именем для скачивания |
+| `message487.aab` | Подписанный Android App Bundle для ручной загрузки в Play Console |
+| `mapping.txt` | R8 mapping именно этой сборки |
+| `SHA256SUMS` | Контрольные суммы обоих APK, AAB и mapping |
+
+[Постоянная ссылка на APK](https://github.com/andre487/AndroidMessage487/releases/latest/download/message487.apk)
+ведёт на последний опубликованный релиз GitHub, а не на текущую ветку. Старые ссылки
+с версией сохраняются. Для релиза 0.0.1 под постоянным именем размещена копия исходного APK;
+возможности, добавленные после его тега, в неё не попадают.
Подпись следует контракту окружения MegaProxy с префиксом `MESSAGE487_`:
@@ -24,7 +36,7 @@ Lane выполняет JVM/Compose-тесты, debug/release lint и подпи
Gradle читает только четыре signing-переменные: путь, пароль хранилища, alias и пароль
ключа. Частичная конфигурация приводит к ошибке. PR-lane `checks` отвергает signing-параметры
-и проверяет неподписанный release APK. Пароли не передаются аргументами командной строки;
+и проверяет неподписанные release APK и AAB. Пароли не передаются аргументами командной строки;
их нельзя печатать, коммитить или передавать через Gradle `-P`.
Имена GitHub Secrets совпадают с MegaProxy: `ANDROID_SIGNING_KEY_BASE64`,
@@ -35,14 +47,39 @@ Workflow восстанавливает ключ и пароль с приват
## Проверка и публикация
-- Тег `release-check/*` запускает сборку и проверку подписанного APK в GitHub Actions
- без публикации Release. APK, mapping, контрольные суммы и отчёты доступны как артефакты.
+- Тег `release-check/*` запускает сборку и проверку подписанных APK/AAB в GitHub Actions
+ без публикации Release. APK, AAB, mapping, контрольные суммы и отчёты доступны как артефакты.
- После слияния workflow в основную ветку ручной запуск также только собирает артефакты.
- Для публикации увеличьте `versionCode`, задайте нужный `versionName` в `app/build.gradle.kts`
и слейте проверенный PR после обязательных проверок. Отправьте тег `v`.
- Workflow требует наличия коммита в `main` и совпадения версии с тегом. Проверенные APK,
+ Workflow требует наличия коммита в `main` и совпадения версии с тегом. Проверенные APK, AAB,
mapping и контрольные суммы публикуются в GitHub Releases. Повторный запуск не перезаписывает
существующий Release.
Первая настроенная версия — `0.0.1`, `versionCode = 1`. Для последующих версий увеличивайте
`versionCode`, чтобы Android мог обновить приложение. Сохраняйте тот же ключ подписи.
+
+## Загрузить в Google Play
+
+1. Соберите релизные артефакты командой выше или скачайте их из проверенного запуска
+ release-workflow. Для каждой загрузки в Play нужен новый `versionCode`: увеличьте его
+ в `app/build.gradle.kts` до сборки.
+2. Создайте или выберите в Play Console приложение `life.andre.message487` и настройте
+ Play App Signing. AAB подписывается настроенным ключом GitHub APK: убедитесь, что Play
+ принимает его как upload key. Для обновлений между установками из GitHub и Play
+ согласуйте **ключ подписи приложения**; upload key и генерируемый Play ключ приложения
+ выполняют разные роли. См. [документацию Android о подписи](https://developer.android.com/studio/publish/app-signing).
+3. Создайте релиз внутреннего тестирования и загрузите `message487.aab`. AAB предназначен
+ для публикации; на телефон устанавливается APK. Сохраните `mapping.txt` вместе со сборкой
+ (AGP также включает R8 mapping в бандл).
+4. Добавьте иконку и баннер из [Branding](../../assets/branding/README.md), настоящие скриншоты,
+ описания, политику конфиденциальности и ответы Data safety. Для `RECEIVE_SMS` необходимо
+ пройти [декларирование и проверку SMS-разрешений](https://support.google.com/googleplay/android-developer/answer/10208820?hl=ru).
+ Успешная сборка AAB сама по себе не означает соответствие правилам магазина.
+5. Проверьте внутренний релиз в Play Console перед распространением.
+
+CI собирает и проверяет файлы; автоматическая загрузка в Google Play и сервисный аккаунт
+Play не настроены. Архив нативных символов из MegaProxy здесь не нужен: у приложения нет
+нативного ядра. Проверка подписи бандла отвергает добавленные неподписанные файлы,
+изменённые файлы, отсутствие обязательных частей и чужой сертификат; регрессионные
+тесты этой проверки входят в Android CI.
diff --git a/docs/ru/testing.md b/docs/ru/testing.md
index 08e9ead..43b7b8f 100644
--- a/docs/ru/testing.md
+++ b/docs/ru/testing.md
@@ -17,7 +17,7 @@
`.github/workflows/ci.yml` запускает все три job на PR, push в main и вручную.
XML/HTML-отчёты Android-тестов и lint загружаются и при ошибках. Успешный Android-job
-также публикует debug APK и неподписанный release APK. Подпись release в этих проверках
+также публикует debug APK и неподписанные release APK и AAB. Подпись release в этих проверках
не используется. Локальный успех не подтверждает результат GitHub для неотправленных изменений.
## Сравнение с MegaProxy
diff --git a/fastlane/Fastfile b/fastlane/Fastfile
index 58cc2d4..d246bbd 100644
--- a/fastlane/Fastfile
+++ b/fastlane/Fastfile
@@ -5,14 +5,14 @@ ensure_bundle_exec
project_root = File.expand_path("..", __dir__)
platform :android do
- desc "Run JVM tests, Android lint, and build debug and unsigned release APKs"
+ desc "Run JVM tests, Android lint, and build debug APK and unsigned release APK/AAB"
lane :checks do
signing_variables = %w[MESSAGE487_KEYSTORE_PATH MESSAGE487_KEYSTORE_PASSWORD MESSAGE487_KEY_ALIAS MESSAGE487_KEY_PASSWORD MESSAGE487_KEY_PASSWORD_FILE]
if signing_variables.any? { |name| !ENV.fetch(name, "").empty? }
UI.user_error!("Signing configuration must not be available to PR checks")
end
gradle(
- tasks: %w[testDebugUnitTest lintDebug lintRelease assembleDebug assembleRelease],
+ tasks: %w[testDebugUnitTest lintDebug lintRelease assembleDebug assembleRelease bundleRelease],
flags: "--no-daemon",
project_dir: project_root
)
@@ -27,7 +27,10 @@ platform :android do
if system(apksigner, "verify", unsigned_apk, out: File::NULL, err: File::NULL)
UI.user_error!("CI release APK is unexpectedly signed")
end
- UI.success("Verified unsigned release APK")
+ sh("java", File.join(project_root, "scripts/VerifyBundle.java"),
+ File.join(project_root, "app/build/outputs/bundle/release/app-release.aab"), "unsigned")
+ sh(File.join(project_root, "scripts/test-bundle-verification.sh"))
+ UI.success("Verified unsigned release APK and App Bundle")
end
desc "Run Python tests and formatting checks for the development server"
@@ -45,7 +48,7 @@ platform :android do
sh(ENV.fetch("PYTHON", "python3"), File.join(project_root, "DevServer/tests/smoke.py"))
end
- desc "Build, sign and verify the release APK and checksums"
+ desc "Build, sign and verify release APK, App Bundle and checksums"
lane :release_artifacts do
sh(File.join(project_root, "scripts/build-release-apk.sh"))
end
diff --git a/fastlane/README.md b/fastlane/README.md
index 47cd59a..f7d59ab 100644
--- a/fastlane/README.md
+++ b/fastlane/README.md
@@ -21,7 +21,7 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
[bundle exec] fastlane android checks
```
-Run JVM tests, Android lint, and build debug and unsigned release APKs
+Run JVM tests, Android lint, and build debug APK and unsigned release APK/AAB
### android python_checks
@@ -45,7 +45,7 @@ Exercise the running development n8n server
[bundle exec] fastlane android release_artifacts
```
-Build, sign and verify the release APK and checksums
+Build, sign and verify release APK, App Bundle and checksums
### android debug_artifact
diff --git a/scripts/VerifyBundle.java b/scripts/VerifyBundle.java
new file mode 100644
index 0000000..2dd7fae
--- /dev/null
+++ b/scripts/VerifyBundle.java
@@ -0,0 +1,42 @@
+import java.nio.file.Path;
+import java.security.MessageDigest;
+import java.util.HexFormat;
+import java.util.Set;
+import java.util.jar.JarFile;
+
+/** Verify every payload entry, including entries appended after signing. */
+class VerifyBundle {
+ public static void main(String[] args) throws Exception {
+ if (args.length != 2) throw new IllegalArgumentException("Usage: VerifyBundle.java bundle.aab SHA256|unsigned");
+ boolean unsigned = args[1].equals("unsigned");
+ var required = new java.util.HashSet<>(Set.of(
+ "BundleConfig.pb", "base/manifest/AndroidManifest.xml", "base/resources.pb", "base/dex/classes.dex"));
+ int payloadEntries = 0;
+ try (var jar = new JarFile(Path.of(args[0]).toFile(), true)) {
+ var entries = jar.entries();
+ while (entries.hasMoreElements()) {
+ var entry = entries.nextElement();
+ if (entry.isDirectory()) continue;
+ try (var input = jar.getInputStream(entry)) {
+ input.transferTo(java.io.OutputStream.nullOutputStream());
+ }
+ required.remove(entry.getName());
+ String name = entry.getName().toUpperCase(java.util.Locale.ROOT);
+ if (name.equals("META-INF/MANIFEST.MF") ||
+ name.matches("META-INF/[^/]+\\.(SF|RSA|DSA|EC)") ||
+ name.matches("META-INF/SIG-[^/]+")) continue;
+ payloadEntries++;
+ var certificates = entry.getCertificates();
+ if (unsigned) {
+ if (certificates != null && certificates.length > 0) throw new SecurityException("Unexpected signed bundle");
+ } else {
+ if (certificates == null || certificates.length != 1) throw new SecurityException("Unsigned or unexpected bundle entry");
+ var digest = MessageDigest.getInstance("SHA-256").digest(certificates[0].getEncoded());
+ if (!HexFormat.of().formatHex(digest).equals(args[1])) throw new SecurityException("Unexpected bundle signing certificate");
+ }
+ }
+ }
+ if (!required.isEmpty() || payloadEntries == 0) throw new SecurityException("Incomplete app bundle");
+ System.out.println("Verified " + (unsigned ? "unsigned" : "signed") + " App Bundle");
+ }
+}
diff --git a/scripts/build-release-apk.sh b/scripts/build-release-apk.sh
index ea6d4b4..97fe4af 100755
--- a/scripts/build-release-apk.sh
+++ b/scripts/build-release-apk.sh
@@ -41,7 +41,7 @@ export MESSAGE487_KEYSTORE_PASSWORD="$keystore_password"
cd "$project_dir"
# Separate invocations ensure clean finishes before generated-resource tasks start.
./gradlew clean --no-daemon
-./gradlew testDebugUnitTest lintDebug lintRelease assembleRelease --no-daemon
+./gradlew testDebugUnitTest lintDebug lintRelease assembleRelease bundleRelease --no-daemon
apk="$project_dir/app/build/outputs/apk/release/app-release.apk"
actual_fingerprint="$("$apksigner" verify --verbose --print-certs "$apk" | sed -n 's/^Signer #1 certificate SHA-256 digest: //p')"
if [[ "$actual_fingerprint" != "$MESSAGE487_EXPECTED_CERT_SHA256" ]]; then
@@ -57,14 +57,18 @@ if grep -q '^application-debuggable' <<< "$badging"; then
echo "Release APK must not be debuggable" >&2
exit 1
fi
+bundle="$project_dir/app/build/outputs/bundle/release/app-release.aab"
+java "$project_dir/scripts/VerifyBundle.java" "$bundle" "$MESSAGE487_EXPECTED_CERT_SHA256"
mkdir -p "$MESSAGE487_RELEASE_DIR"
# Only remove artifacts owned by this script so old APKs cannot enter a new release.
find "$MESSAGE487_RELEASE_DIR" -maxdepth 1 -type f \
- \( -name 'message487-*.apk' -o -name mapping.txt -o -name SHA256SUMS \) -delete
+ \( -name 'message487-*.apk' -o -name 'message487-*.aab' -o -name message487.apk -o -name message487.aab -o -name mapping.txt -o -name SHA256SUMS \) -delete
cp "$apk" "$MESSAGE487_RELEASE_DIR/message487-$version_name.apk"
+cp "$apk" "$MESSAGE487_RELEASE_DIR/message487.apk"
+cp "$bundle" "$MESSAGE487_RELEASE_DIR/message487.aab"
cp "$project_dir/app/build/outputs/mapping/release/mapping.txt" "$MESSAGE487_RELEASE_DIR/mapping.txt"
(
cd "$MESSAGE487_RELEASE_DIR"
- shasum -a 256 message487-*.apk mapping.txt > SHA256SUMS
+ shasum -a 256 message487*.apk message487.aab mapping.txt > SHA256SUMS
)
-echo "Verified signed release APK and checksums: $MESSAGE487_RELEASE_DIR"
+echo "Verified signed release APK, App Bundle and checksums: $MESSAGE487_RELEASE_DIR"
diff --git a/scripts/test-bundle-verification.sh b/scripts/test-bundle-verification.sh
new file mode 100755
index 0000000..35956d0
--- /dev/null
+++ b/scripts/test-bundle-verification.sh
@@ -0,0 +1,45 @@
+#!/usr/bin/env bash
+set -euo pipefail
+project_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+fixture_dir="$(mktemp -d "${TMPDIR:-/tmp}/message487-bundle-test.XXXXXX")"
+trap 'rm -rf "$fixture_dir"' EXIT
+mkdir -p "$fixture_dir/content/base/manifest" "$fixture_dir/content/base/dex"
+for entry in BundleConfig.pb base/manifest/AndroidManifest.xml base/resources.pb base/dex/classes.dex; do
+ printf 'Synthetic test entry' > "$fixture_dir/content/$entry"
+done
+jar --create --file "$fixture_dir/unsigned.aab" -C "$fixture_dir/content" .
+verify=(java "$project_dir/scripts/VerifyBundle.java")
+"${verify[@]}" "$fixture_dir/unsigned.aab" unsigned >/dev/null
+printf 'Synthetic-test-password' > "$fixture_dir/password"
+keytool -genkeypair -alias fixture -keyalg RSA -keystore "$fixture_dir/key.p12" \
+ -storepass:file "$fixture_dir/password" -dname CN=BundleTest -validity 1 >/dev/null 2>&1
+fingerprint="$(keytool -exportcert -alias fixture -keystore "$fixture_dir/key.p12" \
+ -storepass:file "$fixture_dir/password" | shasum -a 256 | cut -d ' ' -f 1)"
+cp "$fixture_dir/unsigned.aab" "$fixture_dir/signed.aab"
+jarsigner -keystore "$fixture_dir/key.p12" -storepass:file "$fixture_dir/password" \
+ "$fixture_dir/signed.aab" fixture >/dev/null
+"${verify[@]}" "$fixture_dir/signed.aab" "$fingerprint" >/dev/null
+expect_rejected() {
+ if "${verify[@]}" "$@" >/dev/null 2>&1; then
+ echo "Bundle verification accepted an invalid fixture" >&2
+ exit 1
+ fi
+}
+expect_rejected "$fixture_dir/unsigned.aab" "$fingerprint"
+expect_rejected "$fixture_dir/signed.aab" unsigned
+expect_rejected "$fixture_dir/signed.aab" "${fingerprint}00"
+cp "$fixture_dir/signed.aab" "$fixture_dir/appended.aab"
+printf 'Unsigned appended payload' > "$fixture_dir/content/base/extra.pb"
+jar --update --file "$fixture_dir/appended.aab" -C "$fixture_dir/content" base/extra.pb
+expect_rejected "$fixture_dir/appended.aab" "$fingerprint"
+cp "$fixture_dir/signed.aab" "$fixture_dir/metadata.aab"
+mkdir -p "$fixture_dir/content/META-INF"
+printf 'Unsigned appended metadata' > "$fixture_dir/content/META-INF/extra.txt"
+jar --update --file "$fixture_dir/metadata.aab" -C "$fixture_dir/content" META-INF/extra.txt
+expect_rejected "$fixture_dir/metadata.aab" "$fingerprint"
+printf 'Tampered entry' > "$fixture_dir/content/base/dex/classes.dex"
+jar --update --file "$fixture_dir/signed.aab" -C "$fixture_dir/content" base/dex/classes.dex
+expect_rejected "$fixture_dir/signed.aab" "$fingerprint"
+jar --create --file "$fixture_dir/incomplete.aab" -C "$fixture_dir/content" BundleConfig.pb
+expect_rejected "$fixture_dir/incomplete.aab" unsigned
+echo "Passed bundle verification: signatures, certificate, tampering, unsigned additions and structure"