# Design ## Context Мотивацию см. в `proposal.md`. Здесь — текущее состояние и ограничения, задающие подход. - Android-клиент (`android/`) построен вокруг серверных альбомов: `AlbumsScreen` → `AlbumScreen` (`GET /api/albums/:id/photos`), SSE, viewer. Слой данных (`com.galo.android.data.*`) уже содержит `UploadRepository`/`UploadWorker` (по файлу на multipart POST, повторы, «только Wi-Fi», переживает перезапуск), Room-кэш альбомов/фото, DataStore-настройки, `HeicConverter`. Понятий «альбом устройства», «синхронизация», «устройство» нет. - Сервер (`backend/`) хранит `objects.checksum` (sha256 оригинала), но **не отдаёт его в API**. Фото неизменяемы по контенту: есть `PATCH /photos/:id` (переименование), `POST /photo-batch/move|copy`, удаление/восстановление. Есть `POST /api/albums` (создание). Загрузка — один файл на запрос, fail-fast, ограничение размера тела. - Платформа Android накладывает рамки: изменение/удаление медиа, созданного не приложением, требует согласия пользователя (`MediaStore.createWriteRequest` / `createDeleteRequest` / `RecoverableSecurityException`); фоновая работа — WorkManager (периодические задачи не чаще ~15 мин); широкий доступ к медиа — `READ_MEDIA_IMAGES`/`READ_MEDIA_VIDEO`, который ограничивается политикой Google Play. Текущее приложение живёт на scoped Photo Picker без широких прав. - `AndroidManifest.xml` задаёт `allowBackup="false"`, поэтому авто-бэкап данных приложения недоступен. ## Goals / Non-Goals **Goals:** - Единый конвейер для трёх режимов (A/B/C) поверх существующей инфраструктуры загрузки, с идемпотентностью и корректным возобновлением после сбоя/перезапуска. - Надёжная идентичность «локальный ассет ↔ серверное фото», устойчивая к переустановке в режиме C. - Безопасная политика удалений при общей цели (n:1) и общем серверном альбоме. **Non-Goals:** - Серверная модель устройств/сессий синхронизации; изменения схемы БД. - Транскодирование видео на устройстве, редактирование изображений. - Разрешение конфликтов владения между пользователями; версионирование контента. - Работа с облачными «плейсхолдерами» (Google Photos) как с локальными ассетами сверх того, что отдаёт MediaStore. ## Decisions ### 1. Единица синхронизации и цель: альбом устройства → существующий серверный альбом Единица — bucket MediaStore (телефонный альбом). Пользователь выбирает целевой **существующий** серверный альбом; допускается n:1 (несколько телефонных в один серверный). Связь хранится в локальном индексе и дублируется в EXIF-метке разгруженных превью. *Альтернатива:* серверный альбом-зеркало 1:1 (автосоздание). Отклонено: лишает пользователя контроля над тем, куда попадают фото, и хуже переиспользует уже наполненные альбомы. *Альтернатива:* смешанный список без явной связи. Отклонено: теряется понимание, куда льётся альбом. ### 2. Идентичность и дедуп: content-hash ↔ checksum объекта Клиент хэширует **байты, которые реально уходят** (после HEIC-конвертации), и сопоставляет с `checksum` объекта на сервере. Сервер начинает отдавать `checksum` в данных фото (аддитивная правка, без миграции). *Альтернатива:* локальная таблица `asset ↔ photo_id` — не переживает переустановку. *Альтернатива:* полноценный серверный провенанс (device+asset) — надёжнее, но заметно расширяет объём backend/API. Отклонено в пользу минимальной правки; провенанс после переустановки закрывается EXIF-меткой (решение 5). ### 3. Политика изменений и удалений - Телефон → сервер: добавление, удаление, переименование, перенос, правка контента (как «новый upload + удалить старый, если он наш»). - Удаление на сервере допускается **только для «наших» загрузок**: фото, которое залил этот клиент, и чей hash исчез из всех локальных альбомов, льющихся в этот серверный. Чужие фото (веб, другие устройства, другие участники) не трогаются. - Сервер → телефон: отражаем метаданные и новые элементы; **оригиналы на телефоне не трогаем** (удаление на сервере убирает только локальное представление). - Отвязка и выключение синхронизации **никогда не удаляют** фото с сервера. *Альтернатива:* полное зеркало — разрушительно при общей цели. *Альтернатива:* только добавление — не покрывает «синхронизировать изменения». ### 4. Три режима как состояния одного конвейера - **A** — индекс + генерация превью на телефоне; сервера не касается. - **B** — A + загрузка копий; оригинал остаётся. - **C** — B + разгрузка: после успешной загрузки оригинал на телефоне заменяется превью. Разгрузка в C реализуется записью превью **обратно в MediaStore** тем же `_ID`: `openFileDescriptor(uri, "rwt")` и запись JPEG с сохранением `DATE_TAKEN`. Видео заменяется JPEG-постером: это смена типа, поэтому выполняется как «удалить видео-запись + вставить постер-картинку в ту же папку». Разгрузка выполняется **автоматически** после успешной загрузки, без отдельного действия на экране; чтобы замена не сопровождалась системным диалогом согласия, при включении режима C приложение запрашивает широкий доступ к файлам (`MANAGE_EXTERNAL_STORAGE`, «доступ ко всем файлам»). Загрузка и дедуп идут тихо. *Альтернатива:* удалять оригинал без замены — теряется присутствие в системной галерее. *Альтернатива:* транскодировать видео — дорого/нестабильно. Отклонено: постер в v1. ### 5. Провенанс, переживающий переустановку: EXIF-метка на разгруженных превью Файлы, которые мы пишем сами (превью в C и постеры видео), содержат EXIF-метку (например, `UserComment`/XMP) с `server_album`, `server_photo`, `original_hash`, `device`, `mode`. После переустановки обход галереи читает метку и восстанавливает и связь альбомов, и провенанс наших загрузок. В A/B оригиналы не помечаются — там дедуп идёт через content-hash. *Альтернатива:* расширить серверное поле-источник — расширяет объём API. *Альтернатива:* Android Auto Backup метаданных — не гарантирован, ломается при переносе на другое устройство. Метка выбрана: провенанс путешествует с файлом. ### 6. Движок синхронизации и триггеры - Локальный индекс содержимого каждого синхронизируемого альбома — в Room (снапшот + провенанс). Внешние удаления не имеют «надгробий», поэтому diff — единственный способ их заметить. - Обнаружение изменений: `MediaStore.getGeneration()` / `ContentObserver` в foreground; периодический WorkManager в фоне с ограничениями «только Wi-Fi» / «только на зарядке» (переиспользуя `SettingsRepository`). - Очередь и выполнение — на `UploadRepository`/`UploadWorker`: по файлу, повторы с backoff, продолжение после перезапуска. - Серверные изменения — SSE, пока приложение открыто, плюс догон `?since=`. - Объём изменений v1 — расширенный (add/delete/rename/move/edit). *Альтернатива:* только ручной запуск — противоречит «отслеживать изменения». *Альтернатива:* авто лишь в foreground — не догоняет фон. ### 7. Интерфейс Один смешанный список альбомов (устройство + сервер). Связанная пара показывается **одной карточкой** альбома устройства с указателем связи (`→ <серверный>`) и меткой режима; связанный серверный альбом из списка скрывается, чтобы синхронизируемый альбом читался как один объект. Экран альбома устройства даёт настройки синхронизации по свайпу вправо — **только выбор режима**. Разгруженные элементы в сетке помечены значком облака; статусы элементов: локально / загружено / только превью (разгружено). ### 8. Параметры превью Крупное превью — длинная сторона ~1600 px, JPEG q80; мелкое — для сетки. Превью генерируются на устройстве и лежат в приватном кэше приложения; в C крупное превью дополнительно пишется в галерею. Дата съёмки (EXIF `DateTimeOriginal`/`DATE_TAKEN`) сохраняется при замене. ### 9. Права `READ_MEDIA_IMAGES` и `READ_MEDIA_VIDEO` (плюс legacy `READ_EXTERNAL_STORAGE` для API до 33). Запись в MediaStore — через системные запросы согласия. `ACCESS_MEDIA_LOCATION` — не требуется для метки (геолокации в ней нет). ### 10. Предпосылка: `.gitignore` Правило `data/` в `.gitignore` игнорирует `android/app/src/main/java/com/galo/android/data/` (весь слой данных, отсутствует и в git-истории). Устранить **до** реализации, иначе новые файлы синхронизации не будут отслеживаться git. Отдельная правка, не часть поведения синхронизации. ## Risks / Trade-offs - [Замена файлов в MediaStore требует особого доступа на Android 11+] → при включении режима C запрашивается широкий доступ к файлам, чтобы разгрузка шла автоматически и без системного диалога; без доступа разгрузка пропускается. - [EXIF-метка теряется, если сторонний редактор перезапишет превью без EXIF] → деградация безопасная: элемент становится «неопознанным» (без дублей и без удаления). - [Общая цель (n:1) и наполненный серверный альбом] → удаляем только «наши» загрузки; чужие источники не затрагиваются. - [Content-hash не переживает переустановку в C сам по себе] → закрывается EXIF-меткой; в B переустановка безопасна (оригиналы на месте, дедуп сработает после повторного выбора цели). - [Высокий расход на хэширование видео] → `sha256` считается лениво, только для сравнения/загрузки; дешёвая первичная детекция изменения — по `DATE_MODIFIED`/`SIZE`. - [Замена видео = delete+insert: место освобождается с задержкой (корзина ОС на 30 дней), коротко нужно место под постер] → сбойный элемент пропускается, проход не падает; ограничение известно и принято. - [Широкие `READ_MEDIA_*` и политика Google Play] → риск только при публикации в Play; для self-hosted/сайдлоада не блокирует. Отмечено как внешнее ограничение. - [Разгрузка C требует свободного места под возврат оригиналов при откате] → предлагать возврат с проверкой свободного места. - [Новые файлы синхронизации не попадут в git из-за `.gitignore` `data/`] → устранить правило до начала реализации (см. Decision 10). ## Migration Plan 1. Предпосылка: поправить `.gitignore` (убрать `data/`), убедиться, что слой данных попал в git. 2. Сервер: отдать `checksum` в моделях/запросах/ответах фото. Аддитивно, без миграции БД; откат — убрать поле. 3. Клиент, этапами: (a) источник MediaStore + смешанный список + режим A; (b) загрузка/дедуп + режим B; (c) разгрузка + EXIF-метка + режим C; (d) возврат оригиналов и переходы режимов. 4. Room-миграция под индекс синхронизации и провенанс. 5. Откат: перевод альбомов в режим A не разрушителен (серверное остаётся); серверное изменение аддитивно. ## Open Questions - Конкретное поле для метки (`UserComment` vs XMP) и формат значения — решается при реализации, не влияет на поведение. - Частота периодической фоновой синхронизации и правила пробуждения — тюнинг. - Нужен ли `ACCESS_MEDIA_LOCATION` для неотредактированной геолокации при локальной генерации превью — можно уточнить позже.