docs: план реализации подготовки к RuStore
This commit is contained in:
877
docs/superpowers/plans/2026-06-08-rustore-release.md
Normal file
877
docs/superpowers/plans/2026-06-08-rustore-release.md
Normal file
@@ -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 = `<!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. Тумблера «Запись эфира» нет; запись эфира работает (кнопка плеера + вкладка).
|
||||||
Reference in New Issue
Block a user