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