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