# Полный аудит 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`).
