Files
radiola-android/docs/superpowers/specs/2026-06-08-rustore-release-design.md

245 lines
16 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Подготовка 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")
buildConfigField("boolean", "SHOW_DEV_TOOLS", "false")
}
create("sideload") {
dimension = "distribution"
buildConfigField("boolean", "ENABLE_SELF_UPDATE", "true")
buildConfigField("boolean", "SHOW_DEV_TOOLS", "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.
## G. Чистка экрана настроек (`ui/settings/SettingsScreen.kt`)
### G1. Убрать тумблер «Запись эфира» (обе сборки)
Осиротевшая настройка: флаг `RECORDING_ENABLED` сохраняется, но НИГДЕ не читается
(запись эфира работает через кнопку в плеере и вкладку «Запись» независимо).
Удалить:
- секцию «Запись эфира» в `SettingsScreen.kt` (item с `isRecordingEnabled`/`setRecordingEnabled`);
- `isRecordingEnabled`/`setRecordingEnabled` в `SettingsViewModel`;
- `isRecordingEnabled()`/`setRecordingEnabled()` в `SettingsRepository` (интерфейс) и
`SettingsRepositoryImpl` + ключ `RECORDING_ENABLED` в DataStore.
- Сама фича записи (кнопка плеера, `RecordingRepository`, вкладка) НЕ трогается.
### G2. Скрыть «Тестирование станций» в store-сборке
Диагностический dev-инструмент (прогон всех станций: OK/без метаданных/оффлайн/
ошибки + отчёт с HTTP/ICY). В store не нужен. Обернуть секцию «ТЕСТИРОВАНИЕ
СТАНЦИЙ» (и связанный диалог отчёта) в `if (BuildConfig.SHOW_DEV_TOOLS) { ... }`.
В sideload остаётся как сейчас. Код тестирования (`SettingsViewModel.startTesting`,
`StationTestStatus`) остаётся в `main`, просто не показывается в store.
## H. Кнопка дип-линк-поиска в SOVA V RE (только sideload)
SOVA V RE — неофициальный мод клиента ВК (на базе VK 6.12), которым пользователь
пользуется лично. **В RuStore-сборку кнопку НЕ включаем** (риск отклонения за
продвижение пиратского мода) — показываем только в sideload-флейворе (тем же
flavor-механизмом, что и dev-tools/тестер: фильтруем список сервисов).
### Архитектура (расширение дип-линков)
Сейчас `DeeplinkService` = `(serviceId, displayName, searchUrlTemplate)`,
`DeeplinkNavigator.openSearch` строит web-URL и открывает через системный chooser.
Для стороннего приложения нужно открывать **напрямую в его пакете**:
- Добавить в `DeeplinkService` опциональное поле `packageName: String? = null`.
- `DeeplinkNavigator`: если `packageName != null` и приложение установлено —
`intent.setPackage(packageName)` (открыть прямо в нём); если не установлено —
фолбэк (chooser/web ВК) или Toast «Установите SOVA». Для остальных сервисов
поведение не меняется (`packageName == null`).
- Добавить запись `SOVA("sova", "SOVA", <url>, packageName=<пакет SOVA>)`.
### Что уточнить на этапе реализации (нужен телефон по adb)
- **Точный пакет** SOVA (`pm list packages -3` на телефоне). Как мод VK 6.12 —
вероятно `com.vkontakte.android` или вариант.
- **Какой URL/схему** SOVA перехватывает как поиск музыки (мод VK обычно ловит
`https://vk.com/...` и `vk://`). Проверить на устройстве, что кнопка реально
открывает поиск в SOVA, а не главный экран. Шаблон поиска подобрать по факту.
### Гейтинг по флейвору
В store-сборке `SOVA` отфильтровывается из списка сервисов (и в настройках, и в
ряду кнопок плеера). В sideload — присутствует. Механизм — `BuildConfig` флаг
флейвора (как у dev-tools).
## Вне области (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.
7. В store-сборке нет секции «Тестирование станций»; в sideload — есть.
8. Тумблера «Запись эфира» нет ни в одной сборке; запись эфира работает.
## Порядок реализации (для плана)
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.
Чистка настроек: убрать тумблер «Запись эфира» (G1); тестер станций под
`BuildConfig.SHOW_DEV_TOOLS` (G2).
Кнопка SOVA (H): расширить `DeeplinkService`/`DeeplinkNavigator` (packageName),
добавить SOVA, отфильтровать в store. Пакет/схему уточнить на телефоне.
6. Keystore: пользователь генерирует, кладёт `keystore.properties`.
7. Сборка `storeRelease`, проверка критериев готовности.
8. Текст политики конфиденциальности.
9. Пользователь: RuStore-консоль (аккаунт, загрузка, карточка).