39 KiB
Подготовка radiOLA к публикации в RuStore — план реализации
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Подготовить Android-приложение radiOLA к публикации в RuStore: разделить
сборку на флейворы store/sideload, добавить релизную подпись, перевести API на
HTTPS, оформить политику конфиденциальности, почистить настройки и добавить
дип-линк-кнопку SOVA (только sideload).
Architecture: Один кодовый базис, два product flavor. store — чистая сборка
для RuStore (без авто-апдейтера и REQUEST_INSTALL_PACKAGES, без dev-тестера и
SOVA). sideload — текущий канал. API уходит за HTTPS-домен api.radiola.nexaweb.su
через уже работающий на сервере хостовый Caddy.
Tech Stack: Kotlin/Compose/Hilt (Android), Gradle product flavors, NestJS (бэкенд-сабмодуль), Caddy (reverse-proxy + авто-TLS), Docker Compose.
Замечание по методу: задачи — конфигурация и инфраструктура, классический
TDD неприменим. Роль теста выполняют команды-проверки (gradle-сборка,
aapt dump permissions, curl, adb). Каждая задача: изменение → проверка →
коммит.
Git: Android — ветка feat/bootstrap-project (репо radiola-android, корень
C:\radiOLA). Бэкенд — ветка main (сабмодуль C:\radiOLA\backend, деплой
scp+docker compose на ru-server 121.127.37.212, /opt/radiola — НЕ git-репо).
Зависимости от пользователя (отметить и дождаться):
- DNS A-запись
api.radiola.nexaweb.su → 121.127.37.212(Задача 1). - Генерация release-keystore +
keystore.properties(Задача 6). - Полный
IP:портadb «Беспроводная отладка» для добычи пакета SOVA (Задача 11). - Действия в консоли RuStore (Задача 14).
Файловая карта
Бэкенд / сервер (сабмодуль + ru-server):
- Caddyfile на сервере (расположение определить) — vhost
api.radiola.nexaweb.su. backend/public/privacy.html(создать) или статический роут — текст политики.backend/src/main.ts— при необходимости статика/privacy./opt/radiola/.env(сервер) —PUBLIC_BASE_URLна https./opt/radiola/appdist/app-version.json(сервер) —download_urlна https.
Android:
app/build.gradle.kts— flavors, BuildConfig-поля, signingConfig.keystore.properties,.gitignore(корень) — подпись.app/src/main/AndroidManifest.xml— убратьREQUEST_INSTALL_PACKAGES.app/src/sideload/AndroidManifest.xml(создать) —REQUEST_INSTALL_PACKAGES.app/src/main/java/com/radiola/MainActivity.kt— gate апдейтера.app/src/main/java/com/radiola/update/UpdateManager.kt— BASE_URL → https.app/src/main/java/com/radiola/di/AppModule.kt— baseUrl → https.app/src/main/java/com/radiola/ui/settings/SettingsScreen.kt— убрать тумблер записи, gate тестера.app/src/main/java/com/radiola/ui/settings/SettingsViewModel.kt— убрать recording.app/src/main/java/com/radiola/domain/repository/SettingsRepository.kt— убрать recording.app/src/main/java/com/radiola/data/repository/SettingsRepositoryImpl.kt— убрать recording.app/src/main/java/com/radiola/domain/model/DeeplinkService.kt— packageName + SOVA.app/src/main/java/com/radiola/deeplink/DeeplinkNavigator.kt— setPackage + фолбэк.
ФАЗА 1 — Инфраструктура HTTPS + политика конфиденциальности
Задача 1: HTTPS-домен для API (DNS + Caddy)
Files: Caddyfile на ru-server (расположение определить в шаге 2).
- Шаг 1: Пользователь добавляет DNS A-запись
Попросить пользователя: в DNS-зоне nexaweb.su создать запись
api.radiola.nexaweb.su A 121.127.37.212. Дождаться подтверждения.
Проверка резолвинга:
nslookup api.radiola.nexaweb.su
Ожидаемо: возвращает 121.127.37.212. Если нет — подождать распространения DNS.
- Шаг 2: Найти Caddyfile на сервере
ssh ru-server 'systemctl status caddy | grep -i caddyfile; ls -la /etc/caddy/Caddyfile 2>/dev/null; caddy version'
Ожидаемо: путь к Caddyfile (обычно /etc/caddy/Caddyfile).
- Шаг 3: Добавить vhost-блок в Caddyfile
Дописать в Caddyfile (через ssh, с бэкапом):
ssh ru-server 'cp /etc/caddy/Caddyfile /etc/caddy/Caddyfile.bak.$(date +%s); cat >> /etc/caddy/Caddyfile <<"EOF"
api.radiola.nexaweb.su {
reverse_proxy localhost:3000
}
EOF'
(Путь подставить из шага 2. date — на сервере, не в JS-окружении.)
- Шаг 4: Перезагрузить Caddy и проверить TLS
ssh ru-server 'caddy validate --config /etc/caddy/Caddyfile && systemctl reload caddy'
sleep 5
curl -s -o /dev/null -w "%{http_code}\n" https://api.radiola.nexaweb.su/app-version
Ожидаемо: валидный TLS, HTTP 200. (Caddy сам выпустит Let's Encrypt cert.)
- Шаг 5: Проверить /downloads и /covers по https
curl -s -o /dev/null -w "downloads:%{http_code}\n" https://api.radiola.nexaweb.su/downloads/radiola-latest.apk
Ожидаемо: downloads:200.
(Коммита нет — изменение на сервере. Caddyfile.bak оставлен.)
Задача 2: Политика конфиденциальности (текст + хостинг)
Files:
-
Create:
backend/src/privacy/privacy.controller.ts -
Create:
backend/src/privacy/privacy.module.ts -
Modify:
backend/src/app.module.ts(импорт PrivacyModule) -
Шаг 1: Создать контроллер с инлайн-HTML политики
Создать backend/src/privacy/privacy.controller.ts — @Get('privacy'), отдаёт
text/html. HTML хранить константой в файле (без внешних зависимостей/файлов —
проще для Docker). Содержание: заголовок «Политика конфиденциальности radiOLA»,
дата; разделы — какие данные собираются (email для входа по magic-link; история
прослушиваний и распознанных треков; технические логи ошибок), сторонние сервисы
(shazam-api.com — фрагмент аудио для распознавания; Discogs — обложки; радиопотоки
третьих лиц), цели обработки, хранение, удаление данных по запросу, контакт
оператора (blinnafeg@gmail.com). Скелет:
import { Controller, Get, Header } from '@nestjs/common';
const PRIVACY_HTML = `<!doctype html><html lang="ru"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Политика конфиденциальности radiOLA</title>
<style>body{font:16px/1.6 system-ui,sans-serif;max-width:760px;margin:40px auto;padding:0 16px;color:#1a1a1a}h1{font-size:1.6rem}h2{font-size:1.15rem;margin-top:2rem}</style>
</head><body>
<h1>Политика конфиденциальности radiOLA</h1>
<p>Дата вступления в силу: 08.06.2026</p>
<!-- ... разделы по содержанию выше ... -->
<h2>Контакты</h2><p>По вопросам обработки данных: blinnafeg@gmail.com</p>
</body></html>`;
@Controller()
export class PrivacyController {
@Get('privacy')
@Header('Content-Type', 'text/html; charset=utf-8')
getPrivacy(): string {
return PRIVACY_HTML;
}
}
(Заполнить разделы полностью — не оставлять <!-- ... -->.)
- Шаг 2: Модуль + регистрация
Создать backend/src/privacy/privacy.module.ts:
import { Module } from '@nestjs/common';
import { PrivacyController } from './privacy.controller';
@Module({ controllers: [PrivacyController] })
export class PrivacyModule {}
В backend/src/app.module.ts импортировать PrivacyModule и добавить в imports.
- Шаг 3: Компиляция бэкенда
cd C:/radiOLA/backend && npx tsc --noEmit 2>&1 | grep -viE "sharp|undici" | head; echo "tsc done"
Ожидаемо: без ошибок (sharp/undici — предсуществующие, игнорируем).
- Шаг 4: Закоммитить (backend, main)
cd C:/radiOLA/backend
git add src/privacy/ src/app.module.ts
git commit -m "feat(privacy): страница политики конфиденциальности на /privacy"
git push
- Шаг 5: Задеплоить и проверить
cd C:/radiOLA/backend
ssh ru-server 'mkdir -p /opt/radiola/src/privacy'
scp src/privacy/privacy.controller.ts src/privacy/privacy.module.ts ru-server:/opt/radiola/src/privacy/
scp src/app.module.ts ru-server:/opt/radiola/src/app.module.ts
ssh ru-server 'cd /opt/radiola && docker compose build app && docker compose up -d app'
sleep 8
curl -s -o /dev/null -w "%{http_code}\n" https://api.radiola.nexaweb.su/privacy
Ожидаемо: 200, страница открывается в браузере.
- Шаг 6: Закоммитить гитлинк сабмодуля (android, feat/bootstrap-project)
cd C:/radiOLA
git add backend
git commit -m "chore: bump backend submodule (privacy)"
git push
Задача 3: Перевести серверные ссылки на HTTPS
Files: /opt/radiola/.env, /opt/radiola/appdist/app-version.json (сервер).
- Шаг 1: PUBLIC_BASE_URL на https
ssh ru-server 'cd /opt/radiola && sed -i "s#PUBLIC_BASE_URL=.*#PUBLIC_BASE_URL=https://api.radiola.nexaweb.su#" .env && grep PUBLIC_BASE_URL .env'
Ожидаемо: PUBLIC_BASE_URL=https://api.radiola.nexaweb.su.
(Если строки нет — добавить echo в .env.)
- Шаг 2: download_url в манифесте версии на https
ssh ru-server 'cd /opt/radiola/appdist && sed -i "s#http://121.127.37.212:3000/downloads#https://api.radiola.nexaweb.su/downloads#" app-version.json && grep download_url app-version.json'
Ожидаемо: download_url начинается с https://api.radiola.nexaweb.su/downloads.
- Шаг 3: Перезапустить контейнер (подхватить PUBLIC_BASE_URL)
ssh ru-server 'cd /opt/radiola && docker compose up -d app'
sleep 8
curl -s https://api.radiola.nexaweb.su/app-version
Ожидаемо: JSON, download_url по https.
- Шаг 4: Проверить обложки по https
curl -s "https://api.radiola.nexaweb.su/now-playing" | head -c 300
Ожидаемо: ответ приходит; coverUrl (если есть) — по https.
(Коммита нет — конфиг на сервере.)
ФАЗА 2 — Android: флейворы, подпись, манифест, апдейтер
Задача 4: Product flavors + BuildConfig-флаги
Files: Modify app/build.gradle.kts.
- Шаг 1: Добавить flavorDimensions + productFlavors
В app/build.gradle.kts, внутри android { }, после блока buildTypes { }
добавить:
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")
}
}
- Шаг 2: Проверить, что Gradle видит флейворы
cd C:/radiOLA && ./gradlew :app:tasks --all -q 2>&1 | grep -iE "assembleStore|assembleSideload" | head
Ожидаемо: присутствуют задачи assembleStoreDebug, assembleSideloadDebug,
assembleStoreRelease, assembleSideloadRelease.
- Шаг 3: Компиляция (BuildConfig поля сгенерированы)
cd C:/radiOLA && ./gradlew :app:compileSideloadDebugKotlin -q 2>&1 | tail -10; echo done
grep -r "ENABLE_SELF_UPDATE\|SHOW_DEV_TOOLS" app/build/generated/source/buildConfig/sideload/debug/com/radiola/BuildConfig.java
Ожидаемо: компиляция без ошибок; поля ENABLE_SELF_UPDATE=true, SHOW_DEV_TOOLS=true.
- Шаг 4: Коммит
cd C:/radiOLA
git add app/build.gradle.kts
git commit -m "build(app): product flavors store/sideload + BuildConfig флаги"
Задача 5: Gate апдейтера + baseUrl/BASE_URL на HTTPS
Files:
-
Modify
app/src/main/java/com/radiola/MainActivity.kt:119-122 -
Modify
app/src/main/java/com/radiola/update/UpdateManager.kt:35 -
Modify
app/src/main/java/com/radiola/di/AppModule.kt(baseUrlradiola) -
Шаг 1: Gate вызова проверки обновления в MainActivity
Заменить блок (строки ~119-122):
LaunchedEffect(Unit) {
val info = com.radiola.update.UpdateManager.checkUpdate() ?: return@LaunchedEffect
if (info.versionCode > BuildConfig.VERSION_CODE) pendingUpdate = info
}
на:
LaunchedEffect(Unit) {
// Авто-обновление только в sideload-сборке (в store обновляет RuStore).
if (!BuildConfig.ENABLE_SELF_UPDATE) return@LaunchedEffect
val info = com.radiola.update.UpdateManager.checkUpdate() ?: return@LaunchedEffect
if (info.versionCode > BuildConfig.VERSION_CODE) pendingUpdate = info
}
- Шаг 2: UpdateManager BASE_URL → https
В app/src/main/java/com/radiola/update/UpdateManager.kt строка 35:
private const val BASE_URL = "http://121.127.37.212:3000"
заменить на:
private const val BASE_URL = "https://api.radiola.nexaweb.su"
- Шаг 3: AppModule Retrofit baseUrl → https
В app/src/main/java/com/radiola/di/AppModule.kt найти provideRadiolaRetrofit
и заменить:
.baseUrl("http://121.127.37.212:3000/")
на:
.baseUrl("https://api.radiola.nexaweb.su/")
- Шаг 4: Компиляция обоих флейворов
cd C:/radiOLA && ./gradlew :app:compileStoreDebugKotlin :app:compileSideloadDebugKotlin -q 2>&1 | tail -10; echo done
Ожидаемо: без ошибок.
- Шаг 5: Коммит
cd C:/radiOLA
git add app/src/main/java/com/radiola/MainActivity.kt app/src/main/java/com/radiola/update/UpdateManager.kt app/src/main/java/com/radiola/di/AppModule.kt
git commit -m "feat(app): апдейтер только в sideload; API/BASE_URL на https"
Задача 6: Релизная подпись (keystore.properties)
Files:
-
Modify
app/build.gradle.kts -
Modify
.gitignore(корень) -
Create (пользователь):
radiola-release.jks,keystore.properties -
*Шаг 1: Добавить keystore.properties и .jks в .gitignore
В корневой .gitignore дописать:
# Релизная подпись (секреты — не в git)
keystore.properties
*.jks
*.keystore
- Шаг 2: Пользователь генерирует keystore
Дать пользователю команду (выполняет в C:\radiOLA, пароли придумывает сам):
keytool -genkeypair -v -keystore radiola-release.jks -alias radiola -keyalg RSA -keysize 2048 -validity 10000
И создать C:\radiOLA\keystore.properties:
storeFile=radiola-release.jks
storePassword=<пароль хранилища>
keyAlias=radiola
keyPassword=<пароль ключа>
⚠️ Предупредить: keystore нужно сохранить навсегда (потеря = нет обновлений). Дождаться подтверждения, что файлы созданы.
- Шаг 3: Добавить signingConfig в build.gradle.kts
В app/build.gradle.kts перед android { } добавить:
val keystorePropsFile = rootProject.file("keystore.properties")
val keystoreProps = java.util.Properties().apply {
if (keystorePropsFile.exists()) load(keystorePropsFile.inputStream())
}
Внутри android { } (перед buildTypes) добавить:
signingConfigs {
create("release") {
if (keystorePropsFile.exists()) {
storeFile = rootProject.file(keystoreProps.getProperty("storeFile"))
storePassword = keystoreProps.getProperty("storePassword")
keyAlias = keystoreProps.getProperty("keyAlias")
keyPassword = keystoreProps.getProperty("keyPassword")
}
}
}
В buildTypes { release { } } добавить первой строкой:
signingConfig = signingConfigs.getByName("release")
- Шаг 4: Проверить подпись release-сборки
cd C:/radiOLA && ./gradlew :app:assembleStoreRelease -q 2>&1 | tail -10; echo done
"$ANDROID_HOME/build-tools/34.0.0/apksigner" verify --print-certs app/build/outputs/apk/store/release/app-store-release.apk 2>&1 | head -5
Ожидаемо: сборка успешна; apksigner показывает сертификат (CN=...), НЕ debug-ключ.
(Если apksigner не в PATH — найти в $ANDROID_HOME/build-tools/*/.)
- Шаг 5: Коммит (только .gitignore и gradle, без секретов)
cd C:/radiOLA
git status --short # убедиться: keystore.properties и *.jks НЕ в списке
git add .gitignore app/build.gradle.kts
git commit -m "build(app): релизная подпись из keystore.properties (в .gitignore)"
Задача 7: REQUEST_INSTALL_PACKAGES → sideload sourceSet
Files:
-
Modify
app/src/main/AndroidManifest.xml:14-15 -
Create
app/src/sideload/AndroidManifest.xml -
Шаг 1: Убрать разрешение из главного манифеста
В app/src/main/AndroidManifest.xml удалить строки:
<!-- Авто-обновление: установка скачанного APK -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
- Шаг 2: Создать sideload-манифест с разрешением
Создать app/src/sideload/AndroidManifest.xml:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- Установка скачанного APK — только в sideload-сборке (авто-апдейтер). -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
</manifest>
- Шаг 3: Проверить итоговые разрешения каждого флейвора
cd C:/radiOLA && ./gradlew :app:assembleStoreDebug :app:assembleSideloadDebug -q 2>&1 | tail -5; echo done
AAPT="$ANDROID_HOME/build-tools/34.0.0/aapt"
echo "=== store (НЕ должно быть REQUEST_INSTALL_PACKAGES) ==="
"$AAPT" dump permissions app/build/outputs/apk/store/debug/app-store-debug.apk | grep -i INSTALL_PACKAGES || echo "нет — верно"
echo "=== sideload (ДОЛЖНО быть) ==="
"$AAPT" dump permissions app/build/outputs/apk/sideload/debug/app-sideload-debug.apk | grep -i INSTALL_PACKAGES
Ожидаемо: в store — «нет — верно»; в sideload — строка с REQUEST_INSTALL_PACKAGES.
- Шаг 4: Коммит
cd C:/radiOLA
git add app/src/main/AndroidManifest.xml app/src/sideload/AndroidManifest.xml
git commit -m "build(app): REQUEST_INSTALL_PACKAGES только в sideload"
ФАЗА 3 — Чистка экрана настроек
Задача 8: Убрать осиротевший тумблер «Запись эфира» (G1)
Files:
-
Modify
app/src/main/java/com/radiola/ui/settings/SettingsScreen.kt:452-493 -
Modify
app/src/main/java/com/radiola/ui/settings/SettingsViewModel.kt -
Modify
app/src/main/java/com/radiola/domain/repository/SettingsRepository.kt:15-16 -
Modify
app/src/main/java/com/radiola/data/repository/SettingsRepositoryImpl.kt -
Шаг 1: Удалить секцию «Запись эфира» из SettingsScreen
В SettingsScreen.kt удалить весь блок // --- Запись эфира --- целиком — это
item { ... } со строки 452 (комментарий) по 493 (закрывающая } item'а),
включающий Column с Switch(checked = isRecordingEnabled, ...).
- Шаг 2: Удалить collectAsState записи в SettingsScreen
В SettingsScreen.kt удалить строку (≈53):
val isRecordingEnabled by viewModel.isRecordingEnabled.collectAsState()
- Шаг 3: Удалить recording из SettingsViewModel
В SettingsViewModel.kt удалить:
val isRecordingEnabled: StateFlow<Boolean> = settingsRepository.isRecordingEnabled()
.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false)
и метод:
fun setRecordingEnabled(enabled: Boolean) {
viewModelScope.launch { settingsRepository.setRecordingEnabled(enabled) }
}
- Шаг 4: Удалить из интерфейса SettingsRepository
В SettingsRepository.kt удалить строки:
fun isRecordingEnabled(): Flow<Boolean>
suspend fun setRecordingEnabled(enabled: Boolean)
- Шаг 5: Удалить из SettingsRepositoryImpl
В SettingsRepositoryImpl.kt удалить ключ (строка 31):
private val RECORDING_ENABLED = booleanPreferencesKey("recording_enabled")
и реализации (строки 60-61):
override fun isRecordingEnabled(): Flow<Boolean> = dataStore.data.map { it[RECORDING_ENABLED] ?: false }
override suspend fun setRecordingEnabled(enabled: Boolean) { dataStore.edit { it[RECORDING_ENABLED] = enabled } }
- Шаг 6: Компиляция (проверка, что ничего не ссылается)
cd C:/radiOLA && ./gradlew :app:compileSideloadDebugKotlin -q 2>&1 | tail -10; echo done
Ожидаемо: без ошибок (если есть «unresolved reference: isRecordingEnabled» — осталась ссылка, удалить её).
- Шаг 7: Коммит
cd C:/radiOLA
git add app/src/main/java/com/radiola/ui/settings/SettingsScreen.kt app/src/main/java/com/radiola/ui/settings/SettingsViewModel.kt app/src/main/java/com/radiola/domain/repository/SettingsRepository.kt app/src/main/java/com/radiola/data/repository/SettingsRepositoryImpl.kt
git commit -m "refactor(settings): убрать осиротевший тумблер «Запись эфира»"
Задача 9: Тестер станций под BuildConfig.SHOW_DEV_TOOLS (G2)
Files: Modify app/src/main/java/com/radiola/ui/settings/SettingsScreen.kt:495-561
- Шаг 1: Обернуть секцию тестирования в флаг
В SettingsScreen.kt секцию // --- Тестирование станций --- (это item { ... },
строки ~495-561) обернуть телом в условие. Изменить начало item { на:
// Диагностический тестер станций — только в sideload (dev-инструмент).
if (com.radiola.BuildConfig.SHOW_DEV_TOOLS) item {
(Остальное тело item'а без изменений.)
- Шаг 2: Скрыть диалог отчёта в store (он зависит от testResults)
Диалог if (showReport) { AlertDialog(...) } (строки ~565-615) оставить как есть —
в store showReport никогда не станет true (кнопка скрыта). Доп. правок не нужно.
Проверить, что StationTestStatus/testResults всё ещё импортируются (используются
диалогом) — компиляция покажет.
- Шаг 3: Сборка обоих флейворов + проверка
cd C:/radiOLA && ./gradlew :app:compileStoreDebugKotlin :app:compileSideloadDebugKotlin -q 2>&1 | tail -10; echo done
Ожидаемо: без ошибок в обоих.
- Шаг 4: Коммит
cd C:/radiOLA
git add app/src/main/java/com/radiola/ui/settings/SettingsScreen.kt
git commit -m "feat(settings): тестер станций только в sideload (SHOW_DEV_TOOLS)"
ФАЗА 4 — Кнопка дип-линк-поиска SOVA (только sideload)
Задача 10: Расширить дип-линк-архитектуру (packageName + прямое открытие)
Files:
-
Modify
app/src/main/java/com/radiola/domain/model/DeeplinkService.kt -
Modify
app/src/main/java/com/radiola/deeplink/DeeplinkNavigator.kt -
Шаг 1: Добавить packageName в DeeplinkService
В DeeplinkService.kt изменить сигнатуру enum, добавив 4-й параметр со значением
по умолчанию (существующие записи не меняются):
enum class DeeplinkService(
val serviceId: String,
val displayName: String,
val searchUrlTemplate: String,
val packageName: String? = null
) {
(Записи YANDEX..DEEZER остаются как есть — packageName у них null.)
- Шаг 2: Учесть packageName в DeeplinkNavigator
В DeeplinkNavigator.kt заменить тело openSearch:
fun openSearch(context: Context, track: Track, service: DeeplinkService) {
val url = service.buildSearchUrl(track.artist, track.song)
val intent = Intent(Intent.ACTION_VIEW, Uri.parse(url))
val pkg = service.packageName
if (pkg != null) {
// Сторонний клиент: открыть напрямую в его пакете, если установлен.
val installed = try {
context.packageManager.getPackageInfo(pkg, 0); true
} catch (e: Exception) { false }
if (installed) {
intent.setPackage(pkg)
try {
context.startActivity(intent)
return
} catch (e: Exception) {
Log.e("DeeplinkNavigator", "Не удалось открыть в $pkg", e)
}
} else {
Toast.makeText(context, "${service.displayName} не установлено", Toast.LENGTH_SHORT).show()
return
}
}
// Обычные сервисы (или фолбэк) — системный выбор приложения.
try {
context.startActivity(Intent.createChooser(intent, "Открыть в..."))
} catch (e: Exception) {
Log.e("DeeplinkNavigator", "Failed to open deeplink", e)
Toast.makeText(context, "Не удалось открыть ссылку", Toast.LENGTH_SHORT).show()
}
}
- Шаг 3: Компиляция
cd C:/radiOLA && ./gradlew :app:compileSideloadDebugKotlin -q 2>&1 | tail -10; echo done
Ожидаемо: без ошибок (поведение существующих сервисов не изменилось).
- Шаг 4: Коммит
cd C:/radiOLA
git add app/src/main/java/com/radiola/domain/model/DeeplinkService.kt app/src/main/java/com/radiola/deeplink/DeeplinkNavigator.kt
git commit -m "feat(deeplink): поддержка прямого открытия в пакете стороннего сервиса"
Задача 11: Добавить SOVA (пакет/схему добыть с телефона) + фильтр в store
Files:
- Modify
app/src/main/java/com/radiola/domain/model/DeeplinkService.kt - Modify
app/src/main/java/com/radiola/ui/player/PlayerBottomSheet.kt(фильтр сервисов) - Modify
app/src/main/java/com/radiola/ui/settings/SettingsScreen.kt(фильтр сервисов)
ЗАВИСИМОСТЬ: нужен телефон с установленной SOVA по adb. Попросить у
пользователя полный IP:порт из «Беспроводная отладка».
- Шаг 1: Подключить телефон и найти пакет SOVA
export PATH="$PATH:/c/Users/nk/AppData/Local/Android/Sdk/platform-tools"
adb connect <IP:порт>
adb shell pm list packages -3 | sort
Определить пакет SOVA (как мод VK 6.12 — вероятно com.vkontakte.android или
вариант вроде com.vk.sova). Записать точное значение как SOVA_PACKAGE.
- Шаг 2: Узнать, какой URL/схему SOVA перехватывает
Дамп intent-фильтров пакета:
adb shell dumpsys package <SOVA_PACKAGE> | grep -iA3 "android.intent.action.VIEW" | grep -iE "scheme|host|vk" | head -30
Определить рабочий шаблон поиска. Кандидаты (проверить на устройстве в шаге 4):
-
https://vk.com/audio?q=%s -
https://vk.com/search?c[q]=%s&c[section]=audioВыбратьSOVA_SEARCH_URL— тот, что открывает поиск музыки В приложении. -
Шаг 3: Добавить запись SOVA в DeeplinkService
В DeeplinkService.kt добавить запись последней в enum (перед ;), подставив
значения из шагов 1-2:
SOVA("sova", "SOVA", "<SOVA_SEARCH_URL>", packageName = "<SOVA_PACKAGE>");
(Запятую после DEEZER поставить, ; перенести на строку SOVA.)
- Шаг 4: Проверить на устройстве, что открывает поиск в SOVA
cd C:/radiOLA && ./gradlew :app:assembleSideloadDebug -q 2>&1 | tail -5; echo built
adb install -r app/build/outputs/apk/sideload/debug/app-sideload-debug.apk
Вручную: включить станцию с треком → плеер → кнопка SOVA → убедиться, что
открылся поиск трека В SOVA (не главный экран). Если открывается не то — вернуться
к шагу 2, подобрать другой SOVA_SEARCH_URL.
- Шаг 5: Отфильтровать SOVA из store-сборки
SOVA не должна показываться в store. В местах, где строится список сервисов,
исключить SOVA при !SHOW_DEV_TOOLS.
В SettingsScreen.kt — секция «МУЗЫКАЛЬНЫЕ СЕРВИСЫ», заменить
DeeplinkService.entries.forEachIndexed { index, service -> на проход по
отфильтрованному списку. Перед циклом добавить:
val services = DeeplinkService.entries.filter {
com.radiola.BuildConfig.SHOW_DEV_TOOLS || it != DeeplinkService.SOVA
}
и использовать services.forEachIndexed { index, service -> и
services.size - 1 в условии разделителя.
В PlayerBottomSheet.kt — servicesSection использует enabledServices (из
настроек). Поскольку в store SOVA нельзя включить (её нет в списке настроек),
дополнительно подстраховаться: там, где формируется enabledServices во
PlayerViewModel (фильтр DeeplinkService.entries.filter { it.serviceId in ids }),
добавить тот же флаг. Найти место:
grep -rn "DeeplinkService.entries.filter" app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt
и заменить на:
_enabledServices.value = DeeplinkService.entries.filter {
it.serviceId in ids &&
(com.radiola.BuildConfig.SHOW_DEV_TOOLS || it != DeeplinkService.SOVA)
}
- Шаг 6: Проверить оба флейвора
cd C:/radiOLA && ./gradlew :app:compileStoreDebugKotlin :app:compileSideloadDebugKotlin -q 2>&1 | tail -10; echo done
Ожидаемо: без ошибок. (Ручная проверка: в store-сборке SOVA нет в настройках; в sideload — есть.)
- Шаг 7: Коммит
cd C:/radiOLA
git add app/src/main/java/com/radiola/domain/model/DeeplinkService.kt app/src/main/java/com/radiola/ui/settings/SettingsScreen.kt app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt
git commit -m "feat(deeplink): кнопка поиска в SOVA (только sideload)"
ФАЗА 5 — Финальная сборка, проверка, релиз sideload
Задача 12: Поднять версию и собрать оба release-флейвора
Files: Modify app/build.gradle.kts (versionCode/Name).
- Шаг 1: Поднять версию
В app/build.gradle.kts: versionCode = 6 → 7, versionName = "1.5" → "1.6".
- Шаг 2: Чистая сборка обоих release-флейворов
cd C:/radiOLA && ./gradlew clean :app:assembleStoreRelease :app:assembleSideloadRelease -q 2>&1 | tail -12; echo done
ls -la app/build/outputs/apk/store/release/ app/build/outputs/apk/sideload/release/
Ожидаемо: оба APK собраны и подписаны release-ключом.
- Шаг 3: Проверить критерии (разрешения + BuildConfig)
AAPT="$ANDROID_HOME/build-tools/34.0.0/aapt"
echo "=== store: НЕТ REQUEST_INSTALL_PACKAGES ==="
"$AAPT" dump permissions app/build/outputs/apk/store/release/app-store-release.apk | grep -i INSTALL_PACKAGES || echo "OK — нет"
echo "=== store BuildConfig ==="
grep -E "ENABLE_SELF_UPDATE|SHOW_DEV_TOOLS" app/build/generated/source/buildConfig/store/release/com/radiola/BuildConfig.java
Ожидаемо: store — нет INSTALL_PACKAGES, ENABLE_SELF_UPDATE=false, SHOW_DEV_TOOLS=false.
- Шаг 4: Коммит
cd C:/radiOLA
git add app/build.gradle.kts
git commit -m "chore(app): bump версии до 7 / 1.6 (RuStore-релиз)"
git push
Задача 13: Выпустить sideload-сборку через авто-обновление
(Чтобы текущие sideload-пользователи получили https-версию. Процесс — см. память radiola-autoupdate: clean-сборка обязательна.)
- Шаг 1: SHA256 sideload-release APK
sha256sum app/build/outputs/apk/sideload/release/app-sideload-release.apk
Записать sha.
- Шаг 2: Залить APK и обновить манифест версии
scp app/build/outputs/apk/sideload/release/app-sideload-release.apk ru-server:/opt/radiola/appdist/downloads/radiola-latest.apk
ssh ru-server 'cd /opt/radiola/appdist && sha256sum downloads/radiola-latest.apk'
Сверить sha с шагом 1. Затем обновить /opt/radiola/appdist/app-version.json:
version_code:7, version_name:"1.6", новый sha256, download_url (уже https),
notes про переезд на https / новые возможности.
- Шаг 3: Проверить манифест по https
curl -s https://api.radiola.nexaweb.su/app-version
Ожидаемо: version_code:7, sha совпадает с залитым APK.
Задача 14: Действия пользователя в консоли RuStore
(Не код. Сопроводить пользователя; отметить выполнение.)
- Создать аккаунт разработчика RuStore.
- Загрузить
app/build/outputs/apk/store/release/app-store-release.apk. - Категория «Музыка и аудио», возрастной рейтинг 12+.
- Скриншоты, иконка 512×512, описание.
- Ссылка на политику:
https://api.radiola.nexaweb.su/privacy. - Заметка модератору: пояснить
USE_EXACT_ALARM(будильник с радио) иREQUEST_IGNORE_BATTERY_OPTIMIZATIONS(фоновое воспроизведение в машине).
Критерии готовности (проверяются по завершении)
:app:assembleStoreRelease— подписанный APK безREQUEST_INSTALL_PACKAGES, флаги false.:app:assembleSideloadRelease— апдейтер на месте, SOVA и тестер присутствуют.https://api.radiola.nexaweb.su/app-version— валидный TLS, version_code 7.- store-сборка работает против https-API (станции, now-playing, авторизация, Shazam).
https://api.radiola.nexaweb.su/privacyоткрывается.- Секреты (keystore, пароли) не в git (
git statusчист). - store: нет секции «Тестирование станций» и кнопки SOVA; sideload: есть.
- Тумблера «Запись эфира» нет; запись эфира работает (кнопка плеера + вкладка).