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

13 KiB
Raw Blame History

Подготовка 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:

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:

// до 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_urlhttps://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.

Вне области (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).
  6. Keystore: пользователь генерирует, кладёт keystore.properties.
  7. Сборка storeRelease, проверка критериев готовности.
  8. Текст политики конфиденциальности.
  9. Пользователь: RuStore-консоль (аккаунт, загрузка, карточка).