diff --git a/CHANGELOG.md b/CHANGELOG.md index 946282e..4e13329 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ Формат: `ГГГГ-ММ-ДД | версия | изменение | контракты | риск` +- 2026-07-07 | v0.5.114 (122) | Этап 1 (единый OkHttpClient, ч.1): NetworkFactory переписан — раньше создавал НОВЫЙ OkHttpClient на КАЖДЫЙ запрос (~32 места) → TLS-handshake + новый пул/диспатчер на запрос + утечка ExecutorService. Теперь один базовый клиент (общий ConnectionPool/Dispatcher + диск-кэш 20МБ), авторизованные варианты через base.newBuilder() (шарят пул/кэш), кэшируются по кредам+таймаутам. Публичный API НЕ менялся — 32 вызова не тронуты. Init диск-кэша — F7MobileApp.onCreate. +3 unit-теста (переиспользование клиента, общий пул). 401→interceptor НЕ делал (меняет семантику исключений — отдельный шаг) | контракты не менялись | средний: затрагивает ВСЕ сетевые вызовы (переиспользование соединений); поведение запросов то же, проверено 15 тестами + assembleRelease - 2026-07-07 | v0.5.114 (122) | Этап 1 (CI + GPL-публикация): добавлен Gitea Actions workflow .gitea/workflows/ci.yml (push/PR → тесты+lint+assembleDebug; тег v* → подписанный release + архив исходников в Gitea Release) — Gitea 1.26.1 поддерживает Actions. Скрипт scripts/package-source.sh (GPL corresponding source §6: архив всего дерева вкл. vendor GPL/LICENSE/NOTICE — проверен, 6.7 МБ) закрывает п.4c, работает и вручную. docs/CI.md. НЕ активно до регистрации self-hosted раннера на инфре B (нужен Gitea-админ для токена — инфра-шаг) | процесс сборки/релиза | нет: конфиг CI, кода приложения не трогает - 2026-07-07 | v0.5.114 (122) | Этап 1 (баги данных, test-first): заведена тестовая инфраструктура (core:network testImplementation junit) + первые 12 unit-тестов. Починено: (1) ICS-парсер дат — суффикс `Z` теперь UTC (был локальным), учитывается TZID, floating→локаль; (2) parseProps сохраняет параметры свойств — несколько ATTENDEE больше НЕ схлопываются, CN/PARTSTAT/ROLE читаются (была порча участников при редактировании→PUT на сервер); (3) unescape single-pass (последовательные replace ломались на `\\n`); (4) CalDavClient.parseIcsInstant делегирует в CalendarIcs — устранён рассинхрон календарь↔задачи; (5) QR-логин: маркер-парсер вместо split('&') — пароль с `&`/`:` больше не теряется; (6) курсор пагинации почты Int→Long (обрезка Unix-времени, ломалось ~2038). Проверено: 12 тестов зелёные + assembleRelease | контракты не менялись; исправлена запись битых ATTENDEE на сервер | низкий: чинит порчу данных, покрыто тестами - 2026-07-07 | v0.5.114 (122) | Этап 0 п.5 (gradle-гигиена): org.gradle.parallel=true (ускорение сборки 20 модулей); удалены мёртвые модули core:data, core:ui (пустые заготовки) и feature:widgets (не был в settings.gradle, не компилировался) + чистка settings.gradle. nonTransitiveRClass и убирание jetifier — ПРОБОВАЛ, откатил: vendor-форк talk-android ломается (R транзитивно + legacy android.support.* в его depS); причины в комментах gradle.properties. Проверено сборкой: assembleRelease BUILD SUCCESSFUL. **Этап 0 завершён** (кроме п.4c — публикация исходников, к CI этапа 1) | контракты не менялись | низкий: удалён неиспользуемый код, ускорена сборка diff --git a/app/src/main/java/ru/forbion/f7cloud/mobile/F7MobileApp.kt b/app/src/main/java/ru/forbion/f7cloud/mobile/F7MobileApp.kt index 8186d59..7eb58a3 100644 --- a/app/src/main/java/ru/forbion/f7cloud/mobile/F7MobileApp.kt +++ b/app/src/main/java/ru/forbion/f7cloud/mobile/F7MobileApp.kt @@ -16,6 +16,7 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import ru.forbion.f7cloud.core.auth.AuthStore import ru.forbion.f7cloud.core.designsystem.AppForegroundTracker +import ru.forbion.f7cloud.core.network.NetworkFactory import ru.forbion.f7cloud.core.push.F7NotificationChannels import ru.forbion.f7cloud.core.push.F7PushRegistrar import ru.forbion.f7cloud.feature.talknative.TalkVendorBootstrap @@ -37,6 +38,7 @@ class F7MobileApp : F7cloudTalkApplication(), ImageLoaderFactory { } }) F7NotificationChannels.ensureAll(this) + NetworkFactory.init(this) // общий HTTP-клиент + диск-кэш (переиспользование соединений) TalkVendorBootstrap.onApplicationCreate(this) val auth = AuthStore(this).load() ?: return try { diff --git a/core/network/src/main/java/ru/forbion/f7cloud/core/network/NetworkFactory.kt b/core/network/src/main/java/ru/forbion/f7cloud/core/network/NetworkFactory.kt index a5f8485..bc4bfa4 100644 --- a/core/network/src/main/java/ru/forbion/f7cloud/core/network/NetworkFactory.kt +++ b/core/network/src/main/java/ru/forbion/f7cloud/core/network/NetworkFactory.kt @@ -1,9 +1,37 @@ package ru.forbion.f7cloud.core.network +import android.content.Context +import okhttp3.Cache import okhttp3.OkHttpClient +import java.io.File +import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.TimeUnit +/** + * Фабрика HTTP-клиентов. Раньше создавала НОВЫЙ OkHttpClient на КАЖДЫЙ запрос (~32 места) → + * TLS-handshake и новый пул/диспатчер на запрос, утечка ExecutorService. Теперь: + * - один базовый клиент (общий ConnectionPool + Dispatcher + диск-кэш); + * - авторизованные варианты — через `base.newBuilder()` (шарят пул/диспатчер/кэш); + * - экземпляры кэшируются по кредам+таймаутам → переиспользуются, соединения живут. + * + * Публичный API НЕ менялся — все вызывающие (`newAuthedClient*`) работают как прежде. + * NB: 401→UnauthorizedException пока проверяется в репозиториях вручную; централизация + * через interceptor — отдельный шаг (меняет семантику исключений). + */ object NetworkFactory { + @Volatile + private var base: OkHttpClient = OkHttpClient() + private val clients = ConcurrentHashMap() + + /** Вызывать один раз из Application.onCreate — добавляет диск-кэш (нужен cacheDir). */ + fun init(context: Context) { + val cacheDir = File(context.applicationContext.cacheDir, "http-cache") + base = OkHttpClient.Builder() + .cache(Cache(cacheDir, 20L * 1024 * 1024)) // 20 МБ + .build() + clients.clear() // пересобрать производные клиенты уже с кэшем + } + fun newAuthedClient( username: String, appPassword: String, @@ -11,13 +39,16 @@ object NetworkFactory { callTimeoutSeconds: Long = 30, readTimeoutSeconds: Long = 30, ): OkHttpClient { - return OkHttpClient.Builder() - .callTimeout(callTimeoutSeconds, TimeUnit.SECONDS) - .connectTimeout(20, TimeUnit.SECONDS) - .readTimeout(readTimeoutSeconds, TimeUnit.SECONDS) - .applyUnsafeSslIfNeeded(trustAllCerts) - .addInterceptor(BasicAuthInterceptor(username, appPassword)) - .build() + val key = "$username|$appPassword|$trustAllCerts|$callTimeoutSeconds|$readTimeoutSeconds" + return clients.getOrPut(key) { + base.newBuilder() // общий пул/диспатчер/кэш базового клиента + .callTimeout(callTimeoutSeconds, TimeUnit.SECONDS) + .connectTimeout(20, TimeUnit.SECONDS) + .readTimeout(readTimeoutSeconds, TimeUnit.SECONDS) + .applyUnsafeSslIfNeeded(trustAllCerts) + .addInterceptor(BasicAuthInterceptor(username, appPassword)) + .build() + } } /** Collabora / richdocuments: cold start and WOPI can be slow on mobile networks. */ diff --git a/core/network/src/test/java/ru/forbion/f7cloud/core/network/NetworkFactoryTest.kt b/core/network/src/test/java/ru/forbion/f7cloud/core/network/NetworkFactoryTest.kt new file mode 100644 index 0000000..c593b24 --- /dev/null +++ b/core/network/src/test/java/ru/forbion/f7cloud/core/network/NetworkFactoryTest.kt @@ -0,0 +1,29 @@ +package ru.forbion.f7cloud.core.network + +import org.junit.Assert.assertNotSame +import org.junit.Assert.assertSame +import org.junit.Test + +/** Фиксирует переиспользование клиента и общий пул соединений (было: новый клиент на запрос). */ +class NetworkFactoryTest { + + @Test fun `одинаковые креды дают ТОТ ЖЕ экземпляр клиента`() { + val a = NetworkFactory.newAuthedClient("user", "pass", false) + val b = NetworkFactory.newAuthedClient("user", "pass", false) + assertSame("клиент должен переиспользоваться, а не создаваться заново", a, b) + } + + @Test fun `клиенты шарят общий ConnectionPool и Dispatcher`() { + val a = NetworkFactory.newAuthedClient("u1", "p1", false) + val b = NetworkFactory.newAuthedClient("u2", "p2", false) // другие креды + assertSame(a.connectionPool, b.connectionPool) + assertSame(a.dispatcher, b.dispatcher) + } + + @Test fun `разные таймауты — разные клиенты, но общий пул`() { + val normal = NetworkFactory.newAuthedClient("u", "p", false) + val office = NetworkFactory.newAuthedClientForOffice("u", "p", false) + assertNotSame(normal, office) + assertSame(normal.connectionPool, office.connectionPool) + } +}