Files
f7cloud_mobile/docs/AUDIT-2026-07-07.md

253 lines
24 KiB
Markdown
Raw Permalink 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.
# Полный аудит f7cloud_mobile — 2026-07-07
> Автор: `b-dev-mobile` (ячейка B). Метод: 6 параллельных агентов-исследователей по
> направлениям (архитектура, безопасность, сеть, производительность/UX, качество кода,
> сборка+vendored-форк). Все находки — с `file:line`.
>
> **Объём:** ~38 500 строк собственного Kotlin в 178 файлах (app/core/feature) +
> ~92 600 строк vendored-форка talk-android (~704 файла). Тестов — **0**. Статанализа — нет. CI — нет.
## Общий вердикт
Приложение функционально зрелое (8 работающих модулей, дисциплинированные ViewModel+StateFlow,
чистый `.gitignore` без секретов, ноль TODO-долга), но построено как «долгий прототип»: релизной
сборки физически не существует, безопасность на уровне dev-стенда, инфраструктура качества
отсутствует, в календарной логике — баги порчи данных.
---
## 🔴 Критическое (до любой раздачи пользователям)
### C1. Release-сборки не существует
- Нигде нет `signingConfig`/`storeFile` (grep по всем `*.gradle` пуст) → `assembleRelease` даёт
неподписанный APK (`INSTALL_PARSE_FAILED_NO_CERTIFICATES`).
- `app/build.gradle:28-33``minifyEnabled true` + `shrinkResources true`, но
`app/proguard-rules.pro`**1 строка (только комментарий)**; `vendor/.../proguard-rules.pro`
всё закомментировано; у `:vendor:talk-app` нет `consumerProguardFiles`; R8 full mode включён
по умолчанию (AGP 8). Точки отказа при R8: LoganSquare `$$JsonObjectMapper` (vendor build.gradle:265),
WebRTC JNI `org.webrtc.**` (:333), Retrofit/gson-модели, EventBus `@Subscribe`, Parceler,
kotlinx.serialization. Вероятен отказ ещё на этапе сборки (missing classes: conscrypt, RxJava `sun.misc`).
- Проект живёт на debug-APK 130 МБ.
### C2. TLS выключается галочкой навсегда (MITM)
- `core/network/.../UnsafeSsl.kt:12-23` — trust-all `X509TrustManager` (пустые checkServerTrusted/
checkClientTrusted) + `hostnameVerifier { _, _ -> true }`.
- Применяется ко всем клиентам: `NetworkFactory.kt:18`, `LoginFlowClient.kt:82,168,216` (передача
логина/пароля при QR-логине). В WebView продублировано: `OfficeWebViewClient.kt:91-92`
(`onReceivedSslError → proceed()`).
- Включается чекбоксом «Доверять сертификату (dev)» `AppScaffold.kt:880-888`, сохраняется в prefs
(`AuthStore.kt:13`) навсегда, молча применяется ко всем последующим сессиям, без предупреждения.
- Fix: убрать из release (`BuildConfig.DEBUG`), заменить на certificate pinning, диалог-предупреждение.
### C3. Пароль аккаунта открытым текстом + бэкапится
- `core/auth/.../AuthStore.kt:6,12``app_password` в обычном SharedPreferences (`f7_auth`,
MODE_PRIVATE). EncryptedSharedPreferences/MasterKey/Keystore в проекте нет вообще (grep пуст).
- `app/src/main/AndroidManifest.xml:25``allowBackup="true"` без `dataExtractionRules`
`f7_auth` (пароль) и `f7_app_lock` (pin_hash) уходят в облачный/adb-бэкап.
- Пароль передаётся через Intent extras: `MailScreen.kt:128-131` → MailComposeActivity (утечка в recents/dumpsys).
- Logout (`AppScaffold.kt:175-180`) — только локальная очистка; серверная ревокация
`DELETE /ocs/v2.php/core/apppassword` НЕ вызывается — токен живёт на сервере.
### C4. ANR-риск на входящем звонке
- `feature/talk-native/.../TalkVendorUserSync.kt:23``runBlocking(Dispatchers.IO)` с сетью/БД.
- Вызывается из `F7MobileApp.onCreate` (`F7MobileApp.kt:41` — блокирует холодный старт) и из
`TalkNativeCallLauncher.launchRoomCall``MainActivity.onCreate:105,120` (клик «принять звонок» на main).
- `F7MobileApp : F7cloudTalkApplication` (F7MobileApp.kt:27) — `super.onCreate()` инициализирует
весь vendor-стек (Dagger, Room+SQLCipher, WorkManager) на холодном старте.
### C5. Тихая порча календарных данных (ICS)
- **Два расходящихся парсера дат**: `CalendarIcs.parseIcsInstant` (`CalendarIcs.kt:176-193`) парсит
`...T100000Z` как ЛОКАЛЬНОЕ время; `CalDavClient.parseIcsInstant` (`CalDavClient.kt:584-602`) — как
UTC. `TZID` игнорируют оба (regex `CalendarIcs.kt:47` отрезает параметры). → события со сдвигом зоны,
по-разному в календаре и задачах.
- **ATTENDEE-парсер сломан**: `CalendarIcs.parseLines` (`:195-206`) кладёт `ATTENDEE;CN=...;PARTSTAT=...`
под один ключ `"ATTENDEE"` с отрезанными параметрами, а `parseAttendeeLine` (`:208-216`) ищет
`CN=`/`PARTSTAT=` в ключе, где их уже нет → участники схлопываются в одного, статусы дефолтные.
При редактировании события это PUT'ится на сервер (порча данных).
- **unescape неверного порядка**: `CalendarIcs.kt:268-269``\n` заменяется до `\\`.
- `CalDavClient.parseVTodo` (`:925-951`) работает без line-unfolding (длинная свёрнутая SUMMARY обрезается).
- vCard: паттерн `N[^:]*:` (`CardDavClient.kt:32-41`) заматчит и `NOTE:`/`NICKNAME:`.
### C6. GPL-3.0 нарушается уже сейчас
- У корневого репо **нет LICENSE/COPYING** (в корне только CHANGELOG/INTEGRATION/NOTICE/PROJECT-STATUS/
README). Vendored GPL-код линкуется в один APK → всё приложение — производная работа под GPL-3.0-or-later.
- Копирайты Nextcloud **заменены** на «F7cloud contributors»: `vendor/.../AndroidManifest.xml:4`
(`SPDX-FileCopyrightText: 2017-2024 F7cloud...`). GPLv3 §4 требует сохранять оригинальные уведомления.
- Раздача даже debug-APK — уже distribution; архив исходников к сборкам не публикуется.
- Проприетарный Firebase слинкован в GPL-APK (`vendor build.gradle:337`, `core/push`). У ячейки есть
свой `f7push` — основной flavor можно сделать без FCM.
---
## 🟠 Архитектура
- **God-composable**: `app/.../ui/AppScaffold.kt:112-671` — 920 строк, ~35 `mutableStateOf`/34
`rememberSaveable` в одной функции: роутер табов + логин (`:675-760`) + push-роутинг (`:358-383`) +
FCM-регистрация (`:299-309`) + permissions + QR. Рекомпозиция корня на любое изменение.
- Связь app→feature — самодельные «nonce-каунтеры»: `filesUploadRequest++`, `mailPushRequest++`
(`AppScaffold.kt:159-168`) — теряют события при гонках. Лучше SharedFlow.
- Слой пробивается в UI: `AppSheets.kt:109-150` (NotificationsSheet дёргает repo из composable),
`MailScreen.kt:83-89` (composable сам собирает OkHttpClient).
- **DI нет** (Hilt/Koin отсутствуют, Dagger только в vendor): ~25 `object`-синглтонов с сетью
(`CalDavClient.kt:80` — object на 1050 строк), inline-фабрики VM в composable (`MailScreen.kt:75-81`).
- Утечка контекста: `app/.../OfficeWebViewPool.kt:12-40` держит WebView с Activity-контекстом до 15 мин.
- **navigation-compose:2.8.9 подключён, но НЕ используется** (нет NavHost): навигация — ручной
`when(activeTab)` (`AppScaffold.kt:535-624`) + boolean-флаги + свой BackHandler (`:428-447`).
Deep links снаружи не работают (нет intent-filter со схемой; `f7cloud://talk` в `MainActivity.kt:79`
недостижим из браузера). `SavedStateHandle` — 0 вхождений: process death теряет внутриэкранное состояние.
- **Мёртвый код**: `feature/widgets` (538 строк, 4 файла) НЕ в `settings.gradle` — не компилируется;
`core/data`, `core/ui` — пустые заготовки в `settings.gradle:23,28`.
- Топ god-файлов: `MailComponents.kt` 1898, `TalkComponents.kt` 1591, `FilesComponents.kt` 1457,
`CalendarComponents.kt` 1362, `TasksComponents.kt` 1066, `CalDavClient.kt` 1050, `MailViewModel.kt` 1012,
`MailRepository.kt` 991, `AppScaffold.kt` 920.
## 🟠 Сетевой слой
- **Новый `OkHttpClient` на каждый запрос**: `NetworkFactory.kt:14-20` — фабрика строит новый
`.build()`; вызывается в начале каждого метода репозитория (`MailRepository.kt:635-641`,
`TalkRepository.kt:331-332`, `FilesRepository.kt:20,62,79...`, ещё 31 call-site). Нулевое
переиспользование соединений (TLS-handshake на каждый запрос), утечка ExecutorService.
`SupportRepository.kt:340` — вообще без callTimeout.
- HTTP-кэша (`.cache()`) нет; ETag/If-Modified-Since не используется (только If-Match при записи CalDAV).
- **Обработка 401 скопирована ~60 раз** (`if (code == 401) throw UnauthorizedException()` в каждом
методе) вместо interceptor'а. OCS-конверт распарсен независимо 4 раза (`TalkRepository.getOcsResponse:367`,
`WidgetsRepository.getOcsData:124`, `NotificationsRepository:32`, `AppMenuRepository:20`) при живом
`OcsJson.kt`. Заголовок `OCS-APIRequest` вручную в 37 местах. DAV-хелперы (`localTag/readText/toDavPath/
resolveHref`) скопированы трижды (`DavClient.kt:242`, `CalDavClient.kt:867/1024`, `CardDavClient.kt:288`).
- **Парсинг 100% ручной** org.json (`optString` — 206 вызовов, молчаливые дефолты) + ICS/vCard регэксами.
- `F7PushRegistrar.kt:53-77` — сырой `HttpURLConnection`, дублирует Basic-auth и **игнорирует
trustAllCerts** (push сломан на self-signed).
- **Инкрементальной синхронизации DAV нет**: ни sync-token/sync-collection, ни CTag. Контакты качают
**все vCard с base64-фото** и потом `.take(500)` (`CardDavClient.kt:142-157`). Календарь делает
`listCalendars` PROPFIND перед каждой операцией (`CalendarRepository.kt:188-204`); задачи —
`listTaskLists` на каждый toggle (`TasksRepository.kt:138-145`).
- `WidgetsRepository.kt:65``withTimeoutOrNull(2000)` поверх блокирующего `execute()` (отмена не
прерывает сокет). Talk long-poll (`TalkRepository.kt:26-41`) — readTimeout == серверному таймауту (гонка).
- Main-safety на соглашении: репозитории `suspend`, но без `withContext(Dispatchers.IO)` внутри
(`MailRepository`, `TalkRepository.kt:58`); `TasksRepository`/`CalendarRepository`/`AppMenuRepository`
вообще не suspend. Спасает только дисциплина вызывающих (`viewModelScope.launch(Dispatchers.IO)`).
## 🟠 Производительность и UX
- **APK ~130 МБ**: `settings.gradle:35-36` включает весь vendor как библиотеку (`feature/talk-native`
`api project(':vendor:talk-app')`). WebRTC `android-talk-webrtc:132.6834.0` (vendor build.gradle:333) —
`libjingle_peerconnection_so.so` на **4 ABI** (~30-45 МБ × ABI ≈ 60-70 МБ); sqlcipher (:281),
conscrypt (:238) — тоже × 4 ABI; `emoji2-bundled` (~10 МБ); 48 локалей vendor. Ни `splits`, ни
`abiFilters`, ни `bundle{}`, ни `resConfigs` нигде нет.
`abiFilters 'arm64-v8a','armeabi-v7a'` + `resConfigs 'ru','en'` + рабочий R8 = 130 → **30-45 МБ**.
- Compose: ключи чата Talk включают `$index` (`TalkScreen.kt:597-604`) — новое сообщение пересоздаёт
весь список; `formatEnvelopeDate` (`MailComponents.kt:644,1480-1507`) создаёт 2×Calendar +
SimpleDateFormat на каждую строку/рекомпозицию; `F7RelativeTime.kt:22``DateTimeFormatter.ofPattern`
на каждый вызов. `derivedStateOf` — 3 шт. на проект.
- Иконки UI грузятся **с сервера как SVG по сети** (`FilesComponents.kt:265,310,325,356`,
`MailComponents.kt:650` — звезда в каждой строке письма, ~15+ мест) → офлайн списки лысеют.
Coil diskCache = 2% cacheDir (`F7MobileApp.kt:79-94`), `crossfade(false)`.
- Офлайн-кэш: только контакты (Room, с TTL 10 мин — образец), файлы (огрызок: имя+флаг для корня,
`FilesRepository.kt:40-53`), почта (свой JSON-файловый кэш `MailCacheRepository.kt` — третий
самописный механизм). Календарь/задачи/Deck/поддержка/виджеты офлайн — пустой экран.
- `runBlocking` на старте (см. C4). SplashScreen API не используется (белый экран на Android 8-11).
- **Тёмной темы нет**: `F7Theme.kt:29-36` всегда `F7LightScheme`, нет `isSystemInDarkTheme`/`values-night`;
+ 77 инлайновых `Color(0xFF...)` мимо `F7Colors` (`FilesComponents.kt:305`, `SupportComponents.kt:64-66`...).
- **Локализация невозможна**: `stringResource` — 6 раз, в `strings.xml` 15 строк; 73+ русских строк
захардкожены в `Text("...")`.
- Pull-to-refresh только в Файлах (`FilesScreen.kt:219`). Ошибки местами Toast без retry
(`MailScreen.kt:348,405`). Skeleton/shimmer нет (51 CircularProgressIndicator).
`contentDescription=null` в 66 местах (в т.ч. кликабельные иконки). WindowSizeClass/sw600dp — нет.
## 🟠 Качество кода и процессы
- **0 тестов** (нет `src/test`/`src/androidTest` ни в одном модуле). Мешают: 40+ `object` с логикой,
репозитории без интерфейсов, `LocalDate.now()`/`ZoneId.systemDefault()` внутри логики
(`TasksGrouping.kt:23`, `F7RelativeTime.kt:15`, `CalendarIcs.kt:121,184`) — Clock не инжектируется.
Готовые кандидаты на чистый JUnit: `CalendarIcs`, `TasksGrouping`, `LoginFlowClient`.
- Обработка ошибок: 223 `runCatching` (~65 глотают через getOrNull/getOrDefault), ~121 `error()` как
flow-control (нетипизированный IllegalStateException — «нет сети» и «битый JSON» неразличимы),
молчаливые catch (`CardDavClient.kt:437`, `DavClient.kt:237,252`, `F7PushRegistrar.kt:87-89` — пустое
тело). Логирования почти нет (35 Log.*/println на проект).
- `!!` — 70 шт. (UI-гонки: `MailSettingsScreens.kt:557-558` `draft!!`, `TalkComponents.kt:876`).
QR-логин ломается на пароле с `&` (`LoginFlowClient.kt:196+`, лимит `size > 3`). Курсор пагинации
почты обрезает Long→Int (`MailRepository.kt:54-62`). `loadThread``getOrDefault(emptyList())`
(сеть-ошибка = «переписки нет», `MailRepository.kt:88-93`).
- Дубли: относительное время — 4 реализации (`F7RelativeTime.kt`, `TalkFormatting.kt:94`,
`MailComponents.kt:1480`, `SupportComponents.kt:68`); открытие файла с тостом дословно скопировано
(`SupportFileIO.kt:56-66` == `LocalFileOpener.kt:14-25`); `serverUrl.trimEnd('/')` — 40+ мест.
- Магия: 77 `Color(0x...)` вне designsystem; ~1087 `.dp` без spacing-токенов; 52 захардкоженных
`ocs/v2.php|remote.php` пути (нет `ApiPaths`); таймауты литералами (`F7PushRegistrar.kt:54-55`
`15000`, `TalkAttachmentUploader.kt:49-51` 5/5/6 мин).
- **lint/detekt/ktlint не настроены** (нет `lint{}`, baseline, `.editorconfig`).
- **Version catalog отсутствует** (`gradle/libs.versions.toml` нет): coroutines 1.8.1/1.9.0/1.10.1/1.10.2,
Coil 2.6.0/2.7.0 (при Coil 3.x актуальном), два Compose BOM (свой 2025.02 vs vendor 2026.01),
Firebase тремя декларациями (bom 33.7.0 / messaging 25.0.1 / 24.0.1), zxing 3.3.0 (2017 г.),
guava целиком (`app/build.gradle:101`). `org.json:json:20240303` дублирует платформенный (бесполезен).
- `gradle.properties`: `parallel=false` (тормоз 20 модулей), `enableJetifier=true` (legacy),
`nonTransitiveRClass=false`; тестовые `NC_TEST_SERVER_*` — мусор от vendor.
- Git: `.gitignore` полный (build/, local.properties, *.apk, keystores, google-services.json закрыты —
секретов в репо НЕТ). LFS не используется; `design/` — 1.66 МБ (28 PNG); vendor закоммичен целиком
(1484 файла, pack 6.95 МБ) без своей git-истории.
## Vendored-форк talk-android (v23.0.0)
- Подключён только модуль app форка (`settings.gradle:37-38`), режим library (`f7cloudAsLibrary=true`,
flavor `f7`). `feature:talk-native` — тонкая обёртка (8 файлов, 364 строки), реально используется
<10% vendor (звонковый стек: CallActivity, CallForegroundService, F7cloudTalkApplication, F7TalkUserSync).
Чат — свой Compose-модуль `feature:talk` (OCS напрямую). Но компилируется и в APK попадает весь vendor.
- Ребрендинг пакетов полный: `com.nextcloud.talk``ru.f7cloud.talk` (609/704 файла). Остатки
`com.nextcloud` (34 файла) легитимны (внешняя библиотека nextcloud-android-common:ui, mimeType, Transifex).
- **Git-истории форка НЕТ** (один коммит «Initial import», у vendor нет своего `.git`) → обновление на
новый апстрим = повторный ручной rename 700 файлов. Рекомендация: отдельный git-репо с ветками
`upstream` (чистый v23) / `f7` (rename-скрипт + патчи коммитами).
- `build.f7cloud.gradle:6``DEBUG=true` захардкожен и для release (debug-логи vendor в проде).
- `README.F7CLOUD.md:20` — «not yet compiled» устарел (модуль давно в графе).
- Cleartext: `vendor/.../network_security_config.xml:9``cleartextTrafficPermitted="true"` + доверие
user-CA; наследуется в app. Fix: переопределить свой networkSecurityConfig.
- Совместимость v23-клиент ↔ сервер spreed 22 (Talk API v4): vendored-клиент договаривается по
capabilities (`ApiUtils.kt:100-131` conversation-v4→v1; `CapabilitiesUtil.kt:336` SERVER_VERSION_MIN=17) —
безопасно, фичи 23 без поддержки просто не включатся. НО собственный `feature:talk` хардкодит `/api/v4`
без capability-гейтов (`TalkRepository.kt:17,37,46...`) — для spreed 22 ок, регресс-тест звонков на
реальном 22 обязателен.
## GPL-3.0 — что сделать (см. C6)
1. `LICENSE` (GPL-3.0-or-later) в корень; свой код остаётся вашим (копирайт ваш), лицензируется под GPL.
2. Вернуть копирайты Nextcloud в изменённых заголовках vendor (свои — добавлять рядом).
3. Публиковать архив исходников с каждым распространяемым APK (шаг CI, git-тег в Gitea).
4. Firebase+GPL: основной flavor без FCM на базе `f7push`/UnifiedPush, FCM — опциональный flavor.
5. Бренд/дизайн GPL НЕ затрагивает (это товарный знак) — тема и UI сохраняются полностью.
---
## План действий (приоритизированный)
### Этап 0 — санитария релиза (дни) = п.1 плана mail/035
1. Keystore + `signingConfig` из env/CI secrets; proguard-правила (webrtc/LoganSquare/EventBus/
`-dontwarn`) + `consumerProguardFiles` у vendor; до готовности — `minifyEnabled false` как страховка.
2. `abiFilters 'arm64-v8a','armeabi-v7a'` + `resConfigs 'ru','en'` → минус ~половина APK.
3. Безопасность-минимум: trust-all → только debug; `allowBackup="false"`; `AuthStore`
EncryptedSharedPreferences с миграцией; ревокация app password при logout; `FLAG_SECURE` на логин/PIN.
4. GPL: LICENSE в корень, вернуть копирайты Nextcloud, публикация исходников с APK.
5. `gradle.properties`: `parallel=true`, `nonTransitiveRClass=true`, убрать jetifier; удалить мёртвые
`core/data`, `core/ui`, решить судьбу `feature/widgets`.
### Этап 1 — фундамент (2-3 недели)
6. CI (Gitea Actions, инфра B): lint+тесты → assembleDebug на push → по тегу подписанный release +
исходники; versionCode из CI.
7. Единый `OkHttpClient` (пул, `.cache(20MB)`, interceptor 401, OCS-заголовки); `withContext(IO)`
внутрь репозиториев; убрать `runBlocking` из старта и пути звонка.
8. Первые 10 unit-тестов (ICS-парсер первым — зафиксировать баги C5, потом чинить); detekt+ktlint
с baseline; version catalog.
9. Починить баги данных: единый ICS-парсер с TZID/ATTENDEE/unescape; QR-пароль с `&`; курсор почты.
### Этап 2 — систематика (1-2 месяца)
10. DI (Hilt / ручной AppContainer), интерфейсы репозиториев; разбор AppScaffold на MainViewModel +
LoginScreen; navigation-compose по-настоящему или удалить.
11. kotlinx.serialization вместо org.json; sync-token/CTag для DAV; кэш календаря/задач; локальные иконки.
12. UX: pull-to-refresh везде, единый F7ErrorState+retry, SplashScreen, строки→strings.xml→тёмная тема.
13. Vendor: отдельный git-репо (upstream/f7 ветки); flavor без Firebase на базе f7push.
## Что хорошо (сохранять)
ViewModel-слой единообразен (viewModelScope.launch(IO), без блокировок main кроме talk-bootstrap);
просмотр HTML-писем безопасен (`MailMessageBodyView.kt:69` — JS off, domStorage off, фильтр запросов);
кэш контактов с TTL — образец; секретов в git нет; ключи LazyColumn почти везде на месте; ребрендинг
vendor чистый; собственная пагинация почты работает; биометрия корректна (`AppLockGate.kt:67-73`).