# Подготовка 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-консоль (аккаунт, загрузка, карточка).