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

878 lines
39 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Подготовка 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 = `<!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`:
```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
<!-- Авто-обновление: установка скачанного APK -->
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
```
- [ ] **Шаг 2: Создать sideload-манифест с разрешением**
Создать `app/src/sideload/AndroidManifest.xml`:
```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: Проверить итоговые разрешения каждого флейвора**
```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<Boolean> = 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<Boolean>
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<Boolean> = 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 <IP:порт>
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 <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:
```kotlin
SOVA("sova", "SOVA", "<SOVA_SEARCH_URL>", packageName = "<SOVA_PACKAGE>");
```
(Запятую после 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. Тумблера «Запись эфира» нет; запись эфира работает (кнопка плеера + вкладка).