docs: дизайн-спек подготовки к публикации в RuStore

This commit is contained in:
nk
2026-06-08 09:47:10 +03:00
parent bb40d26621
commit 4391f3ec33

View File

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