diff --git a/docs/superpowers/specs/2026-06-08-rustore-release-design.md b/docs/superpowers/specs/2026-06-08-rustore-release-design.md new file mode 100644 index 0000000..6566b7b --- /dev/null +++ b/docs/superpowers/specs/2026-06-08-rustore-release-design.md @@ -0,0 +1,187 @@ +# Подготовка radiOLA к публикации в RuStore — дизайн + +Дата: 2026-06-08 +Статус: согласован (дизайн), ждёт ревью спека → план реализации. + +## Цель +Подготовить Android-приложение radiOLA (Kotlin/Compose, `applicationId=com.radiola`) +к публикации в **RuStore**, не сломав текущий сайдлоад-канал. Привести проект в +соответствие требованиям магазина: убрать самостоятельную установку APK из +store-сборки, добавить релизную подпись, перевести API на HTTPS, оформить политику +конфиденциальности. + +## Принятые решения +1. **Два product flavor** (`store` для RuStore, `sideload` — текущий канал с авто-апдейтером). +2. **Новый release-keystore**, генерирует пользователь (пароли у него, в git не уходят). +3. **HTTPS для API** через существующий хостовый Caddy на сервере `121.127.37.212`. +4. Домен API: **`api.radiola.nexaweb.su`** (A-запись → `121.127.37.212`). +5. **Политику конфиденциальности** пишем сами, хостим статикой на API-домене. +6. Подаём **APK** (не AAB); `isMinifyEnabled=false` (не включаем proguard сейчас). + +--- + +## A. Структура сборки — product flavors + +`app/build.gradle.kts`: +```kotlin +flavorDimensions += "distribution" +productFlavors { + create("store") { + dimension = "distribution" + buildConfigField("boolean", "ENABLE_SELF_UPDATE", "false") + } + create("sideload") { + dimension = "distribution" + buildConfigField("boolean", "ENABLE_SELF_UPDATE", "true") + } +} +``` +- Один и тот же `applicationId=com.radiola` в обоих флейворах (разные каналы, не + ставятся одновременно на одно устройство — это норм). +- Сборки: `:app:assembleStoreRelease`, `:app:assembleSideloadDebug` и т.д. + +### Изоляция апдейтера от store-сборки +- Разрешение `REQUEST_INSTALL_PACKAGES` переносится из главного манифеста в + **`app/src/sideload/AndroidManifest.xml`** (manifest merger добавит его только в + sideload). В `src/store/` этого разрешения нет. +- Вызов проверки обновления в `MainActivity` оборачивается: + `if (BuildConfig.ENABLE_SELF_UPDATE) { updateManager.checkUpdate(...) }`. +- Класс `com.radiola.update.UpdateManager` и `ui/update/*` остаются в `main` + (компилируются в обе сборки), но в store никогда не вызываются и разрешений не + просят. (Дешевле и безопаснее, чем дробить sourceSet'ы и ломать компиляцию + MainActivity.) +- `FileProvider` в манифесте можно оставить в `main` (безвреден без апдейтера) — + он используется только установкой APK, которая в store не запускается. + +## B. Релизная подпись + +`app/build.gradle.kts`: +```kotlin +// до android { } +val keystorePropsFile = rootProject.file("keystore.properties") +val keystoreProps = java.util.Properties().apply { + if (keystorePropsFile.exists()) load(keystorePropsFile.inputStream()) +} +// внутри android { } +signingConfigs { + create("release") { + if (keystorePropsFile.exists()) { + storeFile = file(keystoreProps.getProperty("storeFile")) + storePassword = keystoreProps.getProperty("storePassword") + keyAlias = keystoreProps.getProperty("keyAlias") + keyPassword = keystoreProps.getProperty("keyPassword") + } + } +} +buildTypes { + release { + signingConfig = signingConfigs.getByName("release") + // ... existing proguard config, isMinifyEnabled=false + } +} +``` +- `keystore.properties` и `*.jks` добавить в `.gitignore`. +- Команда генерации (выполняет пользователь, пароли свои): + ``` + keytool -genkeypair -v -keystore radiola-release.jks \ + -alias radiola -keyalg RSA -keysize 2048 -validity 10000 + ``` +- `keystore.properties` (локально, не в git): + ``` + storeFile=../radiola-release.jks + storePassword=... + keyAlias=radiola + keyPassword=... + ``` +- ⚠️ Keystore — навсегда: его потеря = невозможность выпускать обновления. Бэкап. + +## C. HTTPS для API + +### DNS +A-запись `api.radiola.nexaweb.su → 121.127.37.212` (добавляет пользователь в +DNS-зоне nexaweb.su). Должна резолвиться до выпуска сертификата Caddy. + +### Сервер (Caddy уже на хосте, слушает 80/443) +Добавить site-блок в Caddyfile: +``` +api.radiola.nexaweb.su { + reverse_proxy localhost:3000 +} +``` +- Caddy сам получит TLS-сертификат Let's Encrypt. +- `/downloads/*` (APK авто-обновления) и `/covers/*` поедут по https автоматически. +- Маршрут `/privacy` (см. F) тоже за этим прокси. +- Точное расположение Caddyfile уточнить на сервере при реализации; `caddy reload`. + +### Приложение +- `di/AppModule.kt`: `baseUrl("https://api.radiola.nexaweb.su/")` для Retrofit + `@Named("radiola")`. (Сейчас `http://121.127.37.212:3000/`.) +- `UpdateManager` BASE_URL (сайдлоад) → https-домен. +- `usesCleartextTraffic=true` **остаётся** в манифесте — нужно для http-потоков + радио (icecast). API/обложки/APK при этом уже по https. + +### Сервер env (docker-compose / .env) +- `PUBLIC_BASE_URL=https://api.radiola.nexaweb.su` (абсолютные ссылки на обложки). +- В `app-version.json`: `download_url` → `https://api.radiola.nexaweb.su/downloads/radiola-latest.apk`. + +## D. Чистка манифеста под модерацию +- `REQUEST_INSTALL_PACKAGES` → только `src/sideload/` (см. A). +- `SCHEDULE_EXACT_ALARM` / `USE_EXACT_ALARM` — оставляем (фича будильника с радио). + В заметке модератору пояснить назначение. +- `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` — оставляем (фоновое воспроизведение в + машине/при выключенном экране). Потенциальный флаг — пояснить; готовы убрать, + если модерация потребует. +- Остальные (INTERNET, FOREGROUND_SERVICE_MEDIA_PLAYBACK, POST_NOTIFICATIONS, + WAKE_LOCK, RECEIVE_BOOT_COMPLETED, WRITE_EXTERNAL_STORAGE maxSdk=28) — стандартны. + +## E. Политика конфиденциальности +Текст на русском, покрывает: +- какие данные собираем: email (magic-link авторизация), история прослушиваний и + распознанных треков, технические логи ошибок (GlitchTip); +- сторонние сервисы: shazam-api.com (фрагмент аудио для распознавания), Discogs + (обогащение обложек), радиопотоки третьих лиц; +- хранение и удаление (запрос на удаление аккаунта/данных); +- контакты оператора. +Хостинг: статическая страница `https://api.radiola.nexaweb.su/privacy` (через Caddy +или статический роут Nest). URL → в карточку RuStore. + +## F. Карточка RuStore (действия пользователя в консоли) +- Аккаунт разработчика RuStore (создаёт пользователь). +- Загрузка `store`-release APK. +- Скриншоты, иконка 512, описание. +- Категория: «Музыка и аудио». +- Возрастной рейтинг: **12+** (возможна ненормативная лирика на потоках). +- Ссылка на политику конфиденциальности. +- Заметка модератору: пояснения по exact-alarm и battery-optimization. + +## Вне области (YAGNI / не сейчас) +- AAB-сборка (подаём APK). +- proguard/minify (оставляем выключенным). +- Пер-юзер квота и платная подписка на распознавание (отдельная задача). +- Перевод радиопотоков на https (невозможно — это чужие icecast-серверы). + +## Риски +- Агрегатор чужих радиопотоков — теоретически модерация может запросить права; + обычно проходит. +- `REQUEST_IGNORE_BATTERY_OPTIMIZATIONS` могут попросить убрать. +- DNS/сертификат: пока A-запись не резолвится, Caddy не выпустит cert (порядок шагов). + +## Критерии готовности +1. `:app:assembleStoreRelease` собирает подписанный APK без `REQUEST_INSTALL_PACKAGES` + (проверить `aapt dump permissions`), `ENABLE_SELF_UPDATE=false`. +2. `:app:assembleSideloadRelease`/`Debug` — апдейтер на месте (текущее поведение). +3. `https://api.radiola.nexaweb.su/app-version` отвечает по валидному TLS. +4. Приложение store-сборки работает против https-API (станции, now-playing, авторизация, Shazam). +5. `https://api.radiola.nexaweb.su/privacy` открывается. +6. Секреты (keystore, пароли) не в git. + +## Порядок реализации (для плана) +1. Инфра HTTPS: DNS A-запись → Caddy vhost → проверка TLS (api + downloads + covers). +2. Сервер: `PUBLIC_BASE_URL` https, `app-version.json` download_url https, страница /privacy. +3. Gradle: flavors `store`/`sideload` + `signingConfig release` + `.gitignore`. +4. Манифест: `REQUEST_INSTALL_PACKAGES` → sideload sourceSet. +5. Код: gate `checkUpdate()` за `BuildConfig.ENABLE_SELF_UPDATE`; baseUrl/BASE_URL → https. +6. Keystore: пользователь генерирует, кладёт `keystore.properties`. +7. Сборка `storeRelease`, проверка критериев готовности. +8. Текст политики конфиденциальности. +9. Пользователь: RuStore-консоль (аккаунт, загрузка, карточка).