diff --git a/docs/superpowers/plans/2026-06-08-rustore-release.md b/docs/superpowers/plans/2026-06-08-rustore-release.md new file mode 100644 index 0000000..ca16e99 --- /dev/null +++ b/docs/superpowers/plans/2026-06-08-rustore-release.md @@ -0,0 +1,877 @@ +# Подготовка 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`. Дождаться подтверждения. + +Проверка резолвинга: +```bash +nslookup api.radiola.nexaweb.su +``` +Ожидаемо: возвращает `121.127.37.212`. Если нет — подождать распространения DNS. + +- [ ] **Шаг 2: Найти Caddyfile на сервере** + +```bash +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, с бэкапом): +```bash +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** + +```bash +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** + +```bash +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`). Скелет: +```typescript +import { Controller, Get, Header } from '@nestjs/common'; + +const PRIVACY_HTML = ` + +Политика конфиденциальности radiOLA + + +

Политика конфиденциальности radiOLA

+

Дата вступления в силу: 08.06.2026

+ +

Контакты

По вопросам обработки данных: blinnafeg@gmail.com

+`; + +@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`: +```typescript +import { Module } from '@nestjs/common'; +import { PrivacyController } from './privacy.controller'; + +@Module({ controllers: [PrivacyController] }) +export class PrivacyModule {} +``` +В `backend/src/app.module.ts` импортировать `PrivacyModule` и добавить в `imports`. + +- [ ] **Шаг 3: Компиляция бэкенда** + +```bash +cd C:/radiOLA/backend && npx tsc --noEmit 2>&1 | grep -viE "sharp|undici" | head; echo "tsc done" +``` +Ожидаемо: без ошибок (sharp/undici — предсуществующие, игнорируем). + +- [ ] **Шаг 4: Закоммитить (backend, main)** + +```bash +cd C:/radiOLA/backend +git add src/privacy/ src/app.module.ts +git commit -m "feat(privacy): страница политики конфиденциальности на /privacy" +git push +``` + +- [ ] **Шаг 5: Задеплоить и проверить** + +```bash +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)** + +```bash +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** + +```bash +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** + +```bash +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)** + +```bash +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** + +```bash +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 { }` +добавить: +```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") + } + } +``` + +- [ ] **Шаг 2: Проверить, что Gradle видит флейворы** + +```bash +cd C:/radiOLA && ./gradlew :app:tasks --all -q 2>&1 | grep -iE "assembleStore|assembleSideload" | head +``` +Ожидаемо: присутствуют задачи `assembleStoreDebug`, `assembleSideloadDebug`, +`assembleStoreRelease`, `assembleSideloadRelease`. + +- [ ] **Шаг 3: Компиляция (BuildConfig поля сгенерированы)** + +```bash +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: Коммит** + +```bash +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` (baseUrl `radiola`) + +- [ ] **Шаг 1: Gate вызова проверки обновления в MainActivity** + +Заменить блок (строки ~119-122): +```kotlin + LaunchedEffect(Unit) { + val info = com.radiola.update.UpdateManager.checkUpdate() ?: return@LaunchedEffect + if (info.versionCode > BuildConfig.VERSION_CODE) pendingUpdate = info + } +``` +на: +```kotlin + 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: +```kotlin + private const val BASE_URL = "http://121.127.37.212:3000" +``` +заменить на: +```kotlin + 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` +и заменить: +```kotlin + .baseUrl("http://121.127.37.212:3000/") +``` +на: +```kotlin + .baseUrl("https://api.radiola.nexaweb.su/") +``` + +- [ ] **Шаг 4: Компиляция обоих флейворов** + +```bash +cd C:/radiOLA && ./gradlew :app:compileStoreDebugKotlin :app:compileSideloadDebugKotlin -q 2>&1 | tail -10; echo done +``` +Ожидаемо: без ошибок. + +- [ ] **Шаг 5: Коммит** + +```bash +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 { }` добавить: +```kotlin +val keystorePropsFile = rootProject.file("keystore.properties") +val keystoreProps = java.util.Properties().apply { + if (keystorePropsFile.exists()) load(keystorePropsFile.inputStream()) +} +``` +Внутри `android { }` (перед `buildTypes`) добавить: +```kotlin + 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 { } }` добавить первой строкой: +```kotlin + signingConfig = signingConfigs.getByName("release") +``` + +- [ ] **Шаг 4: Проверить подпись release-сборки** + +```bash +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, без секретов)** + +```bash +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` удалить строки: +```xml + + +``` + +- [ ] **Шаг 2: Создать sideload-манифест с разрешением** + +Создать `app/src/sideload/AndroidManifest.xml`: +```xml + + + + + +``` + +- [ ] **Шаг 3: Проверить итоговые разрешения каждого флейвора** + +```bash +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: Коммит** + +```bash +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): +```kotlin + val isRecordingEnabled by viewModel.isRecordingEnabled.collectAsState() +``` + +- [ ] **Шаг 3: Удалить recording из SettingsViewModel** + +В `SettingsViewModel.kt` удалить: +```kotlin + val isRecordingEnabled: StateFlow = settingsRepository.isRecordingEnabled() + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), false) +``` +и метод: +```kotlin + fun setRecordingEnabled(enabled: Boolean) { + viewModelScope.launch { settingsRepository.setRecordingEnabled(enabled) } + } +``` + +- [ ] **Шаг 4: Удалить из интерфейса SettingsRepository** + +В `SettingsRepository.kt` удалить строки: +```kotlin + fun isRecordingEnabled(): Flow + suspend fun setRecordingEnabled(enabled: Boolean) +``` + +- [ ] **Шаг 5: Удалить из SettingsRepositoryImpl** + +В `SettingsRepositoryImpl.kt` удалить ключ (строка 31): +```kotlin + private val RECORDING_ENABLED = booleanPreferencesKey("recording_enabled") +``` +и реализации (строки 60-61): +```kotlin + override fun isRecordingEnabled(): Flow = dataStore.data.map { it[RECORDING_ENABLED] ?: false } + override suspend fun setRecordingEnabled(enabled: Boolean) { dataStore.edit { it[RECORDING_ENABLED] = enabled } } +``` + +- [ ] **Шаг 6: Компиляция (проверка, что ничего не ссылается)** + +```bash +cd C:/radiOLA && ./gradlew :app:compileSideloadDebugKotlin -q 2>&1 | tail -10; echo done +``` +Ожидаемо: без ошибок (если есть «unresolved reference: isRecordingEnabled» — +осталась ссылка, удалить её). + +- [ ] **Шаг 7: Коммит** + +```bash +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 {` на: +```kotlin + // Диагностический тестер станций — только в 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: Сборка обоих флейворов + проверка** + +```bash +cd C:/radiOLA && ./gradlew :app:compileStoreDebugKotlin :app:compileSideloadDebugKotlin -q 2>&1 | tail -10; echo done +``` +Ожидаемо: без ошибок в обоих. + +- [ ] **Шаг 4: Коммит** + +```bash +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-й параметр со значением +по умолчанию (существующие записи не меняются): +```kotlin +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`: +```kotlin + 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: Компиляция** + +```bash +cd C:/radiOLA && ./gradlew :app:compileSideloadDebugKotlin -q 2>&1 | tail -10; echo done +``` +Ожидаемо: без ошибок (поведение существующих сервисов не изменилось). + +- [ ] **Шаг 4: Коммит** + +```bash +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** + +```bash +export PATH="$PATH:/c/Users/nk/AppData/Local/Android/Sdk/platform-tools" +adb connect +adb shell pm list packages -3 | sort +``` +Определить пакет SOVA (как мод VK 6.12 — вероятно `com.vkontakte.android` или +вариант вроде `com.vk.sova`). Записать точное значение как `SOVA_PACKAGE`. + +- [ ] **Шаг 2: Узнать, какой URL/схему SOVA перехватывает** + +Дамп intent-фильтров пакета: +```bash +adb shell dumpsys 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: +```kotlin + SOVA("sova", "SOVA", "", packageName = ""); +``` +(Запятую после DEEZER поставить, `;` перенести на строку SOVA.) + +- [ ] **Шаг 4: Проверить на устройстве, что открывает поиск в SOVA** + +```bash +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 ->` на проход по +отфильтрованному списку. Перед циклом добавить: +```kotlin + 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 }`), +добавить тот же флаг. Найти место: +```bash +grep -rn "DeeplinkService.entries.filter" app/src/main/java/com/radiola/ui/player/PlayerViewModel.kt +``` +и заменить на: +```kotlin + _enabledServices.value = DeeplinkService.entries.filter { + it.serviceId in ids && + (com.radiola.BuildConfig.SHOW_DEV_TOOLS || it != DeeplinkService.SOVA) + } +``` + +- [ ] **Шаг 6: Проверить оба флейвора** + +```bash +cd C:/radiOLA && ./gradlew :app:compileStoreDebugKotlin :app:compileSideloadDebugKotlin -q 2>&1 | tail -10; echo done +``` +Ожидаемо: без ошибок. (Ручная проверка: в store-сборке SOVA нет в настройках; +в sideload — есть.) + +- [ ] **Шаг 7: Коммит** + +```bash +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-флейворов** + +```bash +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)** + +```bash +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: Коммит** + +```bash +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** + +```bash +sha256sum app/build/outputs/apk/sideload/release/app-sideload-release.apk +``` +Записать sha. + +- [ ] **Шаг 2: Залить APK и обновить манифест версии** + +```bash +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** + +```bash +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` (фоновое воспроизведение в машине). + +--- + +## Критерии готовности (проверяются по завершении) +1. `:app:assembleStoreRelease` — подписанный APK без `REQUEST_INSTALL_PACKAGES`, флаги false. +2. `:app:assembleSideloadRelease` — апдейтер на месте, SOVA и тестер присутствуют. +3. `https://api.radiola.nexaweb.su/app-version` — валидный TLS, version_code 7. +4. store-сборка работает против https-API (станции, now-playing, авторизация, Shazam). +5. `https://api.radiola.nexaweb.su/privacy` открывается. +6. Секреты (keystore, пароли) не в git (`git status` чист). +7. store: нет секции «Тестирование станций» и кнопки SOVA; sideload: есть. +8. Тумблера «Запись эфира» нет; запись эфира работает (кнопка плеера + вкладка).