Files
ds-galo/openspec/changes/add-android-app/specs/android-client/spec.md
T
iu aa2f7552bc feat(android): add native Android client
Kotlin + Jetpack Compose client for a self-hosted instance: manual
server setup and health check, auth with Keystore-backed tokens, album
and photo browsing with SSE live updates, upload queue in WorkManager
with HEIC-to-JPEG conversion, and Room/Coil offline cache. Add an
android Makefile target and revise project.md §17.
2026-10-12 12:14:54 +03:00

13 KiB

Spec Delta

Purpose

Android-клиент для self-hosted galo: настройка подключения к серверу, авторизация, просмотр альбомов и медиа, а также фоновая загрузка фотографий и видео с телефона с офлайн-кэшем.

ADDED Requirements

Requirement: Настройка подключения к серверу

Приложение SHALL требовать явного указания базового URL инстанса galo при первом запуске и SHALL проверять доступность сервера перед переходом к работе.

Scenario: Успешная настройка

  • WHEN пользователь вводит достижимый базовый URL и подтверждает
  • THEN приложение проверяет GET /api/health и при успехе переходит к входу

Scenario: Сервер недоступен

  • WHEN сервер недоступен или health вернул ошибку
  • THEN приложение показывает ошибку и остаётся на экране настройки, сохраняя введённый URL

Requirement: Небезопасное соединение по LAN

Приложение SHALL по умолчанию запрещать незашифрованный HTTP и SHALL разрешать его только после явного включения пользователем режима небезопасного соединения.

Scenario: HTTP без разрешения

  • WHEN пользователь указывает http-URL и режим небезопасного соединения выключен
  • THEN приложение блокирует подключение и предлагает включить режим

Scenario: Режим включён

  • WHEN пользователь включил режим небезопасного соединения
  • THEN HTTP-подключение разрешено, а интерфейс явно отмечает небезопасное соединение

Requirement: Разрешение относительных URL объектов

Приложение SHALL трактовать относительные URL объектов из REST-ответов и SSE как пути на выбранном базовом URL сервера.

Scenario: Отдача объекта

  • WHEN API возвращает thumb_url вида /api/objects/<key>?...
  • THEN приложение запрашивает объект с того же инстанса, что и базовый URL

Requirement: Вход и хранение сессии

Приложение SHALL позволять вход и регистрацию по email и паролю и SHALL хранить токены только в защищённом хранилище платформы.

Scenario: Успешный вход

  • WHEN вход проходит успешно
  • THEN токены сохраняются в защищённом хранилище, пользователь видит список альбомов

Scenario: Перезапуск с сессией

  • WHEN приложение перезапущено с сохранённой сессией
  • THEN пользователь остаётся авторизованным без повторного ввода пароля

Requirement: Автоматическое обновление токена

Приложение SHALL автоматически обновлять access-токен при ответе 401, пока refresh-токен действителен, и SHALL завершать сессию при неудачном обновлении.

Scenario: Прозрачное обновление

  • WHEN запрос получает 401 и refresh-токен действителен
  • THEN приложение обновляет токен и повторяет исходный запрос без участия пользователя

Scenario: Обновление не удалось

  • WHEN обновление токена завершилось ошибкой
  • THEN сессия очищается и пользователь возвращается на экран входа

Requirement: Список альбомов

Приложение SHALL показывать доступные пользователю альбомы с обложкой и счётчиками, беря данные из API, а при отсутствии сети — из локального кэша.

Scenario: Онлайн

  • WHEN авторизованный пользователь открывает приложение
  • THEN отображается список альбомов с обложками или плейсхолдером

Scenario: Офлайн из кэша

  • WHEN сеть недоступна, но кэш существует
  • THEN список альбомов отображается из кэша с признаком офлайн-режима

Requirement: Постраничный просмотр альбома

Приложение SHALL показывать сетку медиа альбома с постраничной загрузкой по курсору и SHALL подгружать следующую страницу при достижении конца списка.

Scenario: Догрузка страницы

  • WHEN пользователь прокручивает сетку до конца
  • THEN приложение запрашивает следующую страницу по курсору и добавляет её к сетке

Scenario: Пустой альбом

  • WHEN в альбоме нет медиа
  • THEN отображается пустое состояние

Requirement: Просмотр медиа на весь экран

Полноэкранный просмотр SHALL поддерживать изображение, видео с постером и управлением воспроизведением, а также панораму 360; навигация между элементами альбома SHALL выполняться свайпом.

Scenario: Свайп между элементами

  • WHEN пользователь свайпает в просмотре
  • THEN показывается следующий или предыдущий элемент альбома без выхода из просмотра

Scenario: Видео

  • WHEN открыт видеоэлемент
  • THEN показывается постер и доступно воспроизведение с перемоткой

Requirement: Обновления в реальном времени

Пока экран альбома активен, приложение SHALL отображать изменения по SSE (новые, обновлённые и удалённые медиа, социальные события) и SHALL догонять пропущенное при переподключении.

Scenario: Новое фото с другого устройства

  • WHEN другое устройство добавляет фото в открытый альбом
  • THEN новое фото появляется в сетке без ручного обновления

Scenario: Переподключение

  • WHEN SSE-соединение разорвалось и восстановилось
  • THEN приложение дозагружает изменения, произошедшие за время разрыва

Requirement: Выбор медиа и альбома для загрузки

Приложение SHALL предоставлять выбор одного или нескольких фото и видео из системной галереи и SHALL требовать выбора альбома назначения перед постановкой в очередь.

Scenario: Постановка в очередь

  • WHEN пользователь выбрал файлы и альбом назначения
  • THEN задания загрузки ставятся в очередь и пользователь видит их статус

Scenario: Отказ сервера

  • WHEN сервер отклоняет загрузку (например, из-за прав или размера)
  • THEN задание помечается ошибкой с причиной от сервера

Requirement: Фоновая очередь загрузки

Загрузка SHALL выполняться по одному файлу за HTTP-запрос и SHALL продолжаться при уходе приложения из foreground, показывая прогресс в уведомлении; при ошибке SHALL выполняться повтор с нарастающей задержкой.

Scenario: Уход в фон

  • WHEN приложение уходит в фон во время загрузки
  • THEN загрузка продолжается и прогресс отображается в уведомлении

Scenario: Повтор при ошибке

  • WHEN загрузка файла завершилась сетевой ошибкой
  • THEN задание повторяется, а после исчерпания попыток помечается ошибкой

Requirement: Условия сети для загрузки

Приложение SHALL позволять ограничить загрузку сетью Wi-Fi; при ожидании подходящей сети задание остаётся в очереди и не выполняется.

Scenario: Только Wi-Fi

  • WHEN ограничение «только Wi-Fi» включено, а доступна только сотовая сеть
  • THEN загрузка не начинается до появления Wi-Fi, а задание остаётся в очереди

Requirement: Сохранность очереди между запусками

Очередь загрузки SHALL сохраняться на устройстве и SHALL продолжаться после перезапуска приложения или устройства.

Scenario: Возобновление после перезапуска

  • WHEN приложение или устройство перезапускается при незавершённой очереди
  • THEN незавершённые задания возобновляются

Requirement: Конвертация HEIC в JPEG

Файлы HEIC/HEIF SHALL конвертироваться на устройстве в JPEG перед загрузкой с сохранением даты съёмки и геолокации.

Scenario: Загрузка HEIC

  • WHEN пользователь ставит в очередь HEIC-файл
  • THEN загружается JPEG-версия, а дата съёмки и координаты из EXIF сохраняются

Scenario: Ошибка конвертации

  • WHEN конвертация завершилась ошибкой
  • THEN задание помечается ошибкой, повреждённый файл не загружается

Requirement: Кэш медиа по устойчивому ключу

Приложение SHALL кэшировать полученные превью и оригиналы по устойчивому идентификатору объекта, не зависящему от подписи и срока действия URL.

Scenario: Смена подписи

  • WHEN URL одного и того же объекта со временем получает новую подпись
  • THEN приложение переиспользует закэшированный объект вместо повторной загрузки

Scenario: Офлайн-отдача из кэша

  • WHEN сеть недоступна, а объект есть в кэше
  • THEN объект отображается из кэша

Requirement: Единый формат ошибок API

Приложение SHALL распознавать ошибки API формата {error:{code,message}} и SHALL показывать пользователю понятное сообщение.

Scenario: Ошибка с кодом и сообщением

  • WHEN сервер возвращает ошибку с code и message
  • THEN приложение показывает сообщение, при необходимости адаптированное под язык интерфейса