16 KiB
Подготовка radiOLA к публикации в RuStore — дизайн
Дата: 2026-06-08 Статус: согласован (дизайн), ждёт ревью спека → план реализации.
Цель
Подготовить Android-приложение radiOLA (Kotlin/Compose, applicationId=com.radiola)
к публикации в RuStore, не сломав текущий сайдлоад-канал. Привести проект в
соответствие требованиям магазина: убрать самостоятельную установку APK из
store-сборки, добавить релизную подпись, перевести API на HTTPS, оформить политику
конфиденциальности.
Принятые решения
- Два product flavor (
storeдля RuStore,sideload— текущий канал с авто-апдейтером). - Новый release-keystore, генерирует пользователь (пароли у него, в git не уходят).
- HTTPS для API через существующий хостовый Caddy на сервере
121.127.37.212. - Домен API:
api.radiola.nexaweb.su(A-запись →121.127.37.212). - Политику конфиденциальности пишем сами, хостим статикой на API-домене.
- Подаём 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/.)UpdateManagerBASE_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 (порядок шагов).
Критерии готовности
:app:assembleStoreReleaseсобирает подписанный APK безREQUEST_INSTALL_PACKAGES(проверитьaapt dump permissions),ENABLE_SELF_UPDATE=false.:app:assembleSideloadRelease/Debug— апдейтер на месте (текущее поведение).https://api.radiola.nexaweb.su/app-versionотвечает по валидному TLS.- Приложение store-сборки работает против https-API (станции, now-playing, авторизация, Shazam).
https://api.radiola.nexaweb.su/privacyоткрывается.- Секреты (keystore, пароли) не в git.
- В store-сборке нет секции «Тестирование станций»; в sideload — есть.
- Тумблера «Запись эфира» нет ни в одной сборке; запись эфира работает.
Порядок реализации (для плана)
- Инфра HTTPS: DNS A-запись → Caddy vhost → проверка TLS (api + downloads + covers).
- Сервер:
PUBLIC_BASE_URLhttps,app-version.jsondownload_url https, страница /privacy. - Gradle: flavors
store/sideload+signingConfig release+.gitignore. - Манифест:
REQUEST_INSTALL_PACKAGES→ sideload sourceSet. - Код: gate
checkUpdate()заBuildConfig.ENABLE_SELF_UPDATE; baseUrl/BASE_URL → https. Чистка настроек: убрать тумблер «Запись эфира» (G1); тестер станций подBuildConfig.SHOW_DEV_TOOLS(G2). Кнопка SOVA (H): расширитьDeeplinkService/DeeplinkNavigator(packageName), добавить SOVA, отфильтровать в store. Пакет/схему уточнить на телефоне. - Keystore: пользователь генерирует, кладёт
keystore.properties. - Сборка
storeRelease, проверка критериев готовности. - Текст политики конфиденциальности.
- Пользователь: RuStore-консоль (аккаунт, загрузка, карточка).