docs: дизайн-спек подготовки к публикации в RuStore
This commit is contained in:
187
docs/superpowers/specs/2026-06-08-rustore-release-design.md
Normal file
187
docs/superpowers/specs/2026-06-08-rustore-release-design.md
Normal 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-консоль (аккаунт, загрузка, карточка).
|
||||||
Reference in New Issue
Block a user