feat: media settings ptime warning#1053
Merged
Merged
Conversation
SERDUN
approved these changes
Apr 2, 2026
SERDUN
pushed a commit
that referenced
this pull request
Apr 6, 2026
SERDUN
added a commit
that referenced
this pull request
May 22, 2026
* fix: skip static payloads sanitizing if no rtpmap (#968)
* fix: media preset translation (#969)
* fix: fix splash config generation and add android_12 image support (#865)
- Escape # in Makefile color variables to prevent shell comment issues
- Remove redundant shell parameter expansion, use Make-only expansion
- Add ANDROID_12_SPLASH_IMAGE variable for separate Android 12 splash
- Use single quotes in echo to avoid shell interpretation problems
Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* chore: add diagnostic logging for recents number mismatch (#970)
Add hash-based and raw value logging to trace why recent calls list
shows two different phone numbers per entry. Raw values are visible
when Logz.io anonymization is disabled, hashes work with it enabled.
* feat: add support a blurred appbar surface (#971)
* feat: add BlurredSurface widget and apply blur effect to all app bars
Extract common ClipRect+BackdropFilter pattern into reusable BlurredSurface widget.
Apply consistent blur background with extendBodyBehindAppBar to all screens
using MainAppBar or AppBar. Remove isComplexBackground conditional logic.
* refactor: standardize MainAppBar field order across screens
* feat: apply blurred appbar surface to ConversationsScreen
* fix: remove unused theme/theme.dart imports
* fix: add top padding to prevent content overlap with blurred appbar
Screens using extendBodyBehindAppBar lacked compensating padding,
causing body content to be hidden behind the blurred AppBar. Added
appropriate top padding to recents, favorites, about, contacts, and
CDR screens consistent with the existing settings screen approach.
* feat(screenshots): make IgnorePointer configurable in ScreenshotApp (#972)
Add ignorePointer parameter (default true) to allow enabling interaction
for specific screenshots that require it.
* feat: add screenshot mocks and docs for all features (#973)
* feat: add screenshot mocks for all features
Add ~20 new screenshot definitions covering all user-visible screens:
- Core flows: contact, chat conversation, SMS conversation, system
notifications, recent CDRs, number CDRs, call log
- Settings: network, language, diagnostic, caller ID, presence,
theme mode, voicemail
- Login: switch screen
- Utility: log records console, contacts agreement, teardown,
permissions, user agreement
Includes shared mock data, mock blocs/cubits/repositories,
screenshot widgets, router entries, and integration test registrations.
* docs: add screenshots package documentation
Add docs for screenshots architecture, mock reference, data reference,
and step-by-step guide for adding new screenshots. Update README with
links to the new documentation.
* fix: address review feedback for screenshot mocks and docs
- Add provider as direct dependency in screenshots/pubspec.yaml
and remove ignore: depend_on_referenced_packages from 7 files
- Fix mock type/factory mismatches in mocks.md documentation
- Fix broken code formatting in mocks.md and adding_screenshots.md
- Remove shadowed ChatTypingCubit/SmsTypingCubit BlocProviders
from conversation screenshot wrappers
- Guard registerFallbackValue with static flag to prevent
duplicate registration
- Fix duplicated phrase in README.md Firebase Hosting section
- Document both flutter test and flutter drive workflows in
overview.md
* feat: update keystores path to new applications subdirectory (#974)
* feat: add text style background decoration support (#975)
* feat: add backgroundBorderRadius and backgroundPadding to TextStyleConfig
* feat: add greetingTextStyle to LoginModeSelectPageConfig
* feat: add ExtendedText widget with background decoration support
* feat: add greetingTextStyle to login.modeSelect docs
* feat: add tests for ExtendedText widget
* feat: update theme template configs with missing components and fixes (#976)
- Fix calStatuses typo to callStatuses in both widget configs
- Add missing widget sections as examples: button, group, bar, input, text, confirmDialog
- Fix non-existent dark SVG asset references to use existing logos
- Remove unsupported fontFeatures from page text styles
- Clean up dark page config duplicate keys in login.switchPage
- Add greetingTextStyle for dark login modeSelect visibility
- Update dark widget decoration gradient config
* refactor: replace deprecated GradientColorsConfig with PageBackground (#977)
* refactor: replace deprecated GradientColorsConfig with PageBackground
Remove GradientColorsConfig, DecorationConfig, GradientsStyleFactory,
and Gradients theme extension. Migrate login and call screens to use
PageBackground via ThemedScaffold.
* refactor: enable gradient backgrounds for login and call page configs
Activate background for login modeSelect and add gradient background
to dialing section using colors from the removed GradientColorsConfig.
* refactor: adjust dark login gradient to contrast with buttons
Change bottom gradient color from #000000 to #0A1929 to avoid
blending with neutralOnDark button surface color.
* refactor: reverse dark login gradient direction
Start with dark color on top, lighter blue on bottom to keep
contrast with buttons at the bottom of the screen.
* refactor: rename misleading onSurface variable to surfaceColor
* feat: appBar blurred surface config and theme updates (#979)
* feat: extract appBar background color and blurred surface to config pipeline
Add configurable appBarBackgroundColor and appBarBlurredSurface (color, sigmaX, sigmaY)
to the theme config→style pipeline across all 7 main screens, replacing hardcoded values.
* feat: update page configuration docs with appBar fields
Add Common page fields section documenting appBarBackgroundColor
and appBarBlurredSurface, update Keypad page example.
* feat: remove appBarBackgroundColor from config pipeline
AppBar backgroundColor is already handled by MainAppBar fallback chain
(explicit → appBarTheme → canvasColor.withAlpha). Remove redundant
appBarBackgroundColor from BasePageConfig, all page configs, screen styles,
factories, and screens to avoid confusion.
* feat: add BlurredSurface.fromStyle factory and simplify screen usage
Replace inline BlurredSurface construction with fromStyle() factory
that returns null when no config is present (no blur applied) or a
configured BlurredSurface with resolved sigma defaults of 10.
* feat: enable appBar blur and activate widget config sections
- Add appBarBlurredSurface to all 7 page sections (dark/light)
- Activate previously ignored widget config sections (_bar, _button,
_group, _input, _text -> remove underscore prefix)
- Set appBar backgroundColor/surfaceTintColor to transparent for blur
- Fix tab indicator label contrast and remove divider line
* feat: update light theme appBarBlurredSurface color to soft blue-white
* feat: set dark status bar icons for light theme appBar
* feat: fix appBarBlurredSurface sigma defaults and add tests
- Make sigmaX/sigmaY nullable in BlurredSurfaceConfig so omitted fields
resolve to 10 via BlurredSurface.fromStyle instead of staying 0
- Regenerate Freezed/JSON for BlurredSurfaceConfig
- Restore blur on RecentCdrs and SystemNotifications screens by passing
sigmaX: 10, sigmaY: 10 explicitly to const BlurredSurface()
- Explicit Colors.transparent fallback in BlurredSurface Container child
- Update docs to reflect null defaults with 10 resolved at widget layer
- Add BlurredSurface.fromStyle widget tests (4 cases)
* fix: system back button blocked in EmbeddedRequestErrorDialog (#935)
- Remove canPop: false from EmbeddedRequestErrorDialog — default is true,
and blocking pop prevented the system back button from working in
Settings → Terms & Conditions when the page failed to load (WT-890).
- Add onPopInvokedWithResult to call onBack when system back dismisses
the dialog in Mode B (pushed route), ensuring _errorDialogShown is reset.
- Reset _errorDialogShown in _handleEmbeddedErrorState when error is
cleared, so repeated errors after system-back dismissal are shown again.
Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
* fix: prevent keepalive write-after-close and harden transaction lifecycle (#870)
* fix: prevent keepalive write-after-close and add regression test
* fix: clean up transaction on send failure in _executeTransaction
Move _addMessage inside the try-catch block so that if writing to a
closed socket throws WebtritSignalingBadStateException, the transaction
is properly removed from the _transactions map instead of leaking.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* test: add transaction cleanup and keepalive timer tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: add comment for closeCode guard in keepalive loop
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: address Copilot review comments on keepalive integration tests
- Fix compile error: replace called(greaterThan(0)) with called(1)
- Fix lint warning: await streamController.close() in tearDown
- Fix false-positive test: move closeCode mock before flushMicrotasks
and add verify closeCode called(2) to catch timer-restart regression
---------
Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* fix: fixed app is locked in Bluetooth call profile after first call (#982)
* fix: fixed app is locked in Bluetooth call profile after first call
Fixed situation when user returns to YouTube or music playback, the audio remains degraded, like during a call. This happens only on Android.
* fix: improve comment for _onLastCallEnded to cover iOS and Android audio routing
---------
Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
* feat: add melos v7 integration (#980)
* feat: add melos integration with codegen, test, format, and dependency scripts
* feat: fix melos scripts syntax for v7 and rename format to fmt
* feat: configure melos v7 with workspace and scripts in pubspec.yaml
- Add workspace: list to pubspec.yaml for Dart pub workspace / Melos v7 package discovery
- Add melos: section to pubspec.yaml with scripts for codegen, tests, formatting, dependencies
- Add resolution: workspace to all sub-package pubspec.yaml files
- Fix ssl_certificates publish_to typo (none; → 'none')
- Update screenshots SDK constraint to ^3.8.0
- Remove melos.yaml (ignored by Melos v7 — config lives in pubspec.yaml)
* feat: include root package in melos workspace via useRootAsPackage
* fix: replace non-ASCII em dashes with hyphens in pubspec.yaml comments
* feat: migrate makefile targets to melos scripts and mark deprecated
* fix: replace __ with _ in screenshots to fix unnecessary_underscores lint
* fix: rename melos script from run to start to avoid CLI conflict
The melos v7 CLI command `melos run <script>` conflicted with the script
named `run`. When running `melos run fmt`, melos treated `run` as the
script name and `fmt` as an argument, causing flutter to look for a file
named `fmt` instead of formatting code.
Renaming the script to `start` (and `run:ios` to `start:ios`) resolves
the ambiguity. Use `melos run start` to launch the app.
* feat: add melos smoke tests and update docs with melos commands
- Add tool/scripts/melos_smoke_test.sh for smoke testing safe scripts
- Add smoke:test melos script to pubspec.yaml
- Rename start to start:android for clarity
- Replace Makefile references with melos commands in docs/build.md
- Rewrite docs/make_file.md as full melos commands reference
- Update README.md to link to Melos Commands
* fix: address Copilot review comments
- Switch get/upgrade/outdated melos scripts from exec to run
to correctly resolve from workspace root (not per-package)
- Align screenshots flutter constraint with workspace root (^3.41.2)
to fix sdk/flutter constraint inconsistency (Dart 3.8 requires Flutter 3.41+)
* fix: reverse date divider in chat (#983)
Placed date divider before bunch of messages. Also changed displaying date in this divider. Added displaying options "Today", "Yesterday", day of week, like "Monday", and date in E, d MMM format, like "Tue, 6 Jan" , depending on comparison between date of message and today's date.
* feat: claude code setup (#981)
* feat: replace .rules with CLAUDE.md for Claude Code
* feat: configure .claude with hooks and allowed commands
* feat: add melos usage rules and update common commands
* feat: reduce CLAUDE.md duplication and soften melos usage rules
* feat: add webtrit_callkeep docs and package-level CLAUDE.md files
* feat: document call architecture, flows, isolates, and key patterns in CLAUDE.md
* feat: restructure AI agent memory files (AGENTS.md + CLAUDE.md split)
- Add root AGENTS.md (<100 lines): universal instructions for any AI tool
- Slim root CLAUDE.md (<50 lines): @imports + Claude-specific gotchas only
- Extract CallBloc architecture to docs/call_architecture.md
- Add AGENTS.md for all 10 packages (shared content, any agent)
- Remove package CLAUDE.md where content fully moved to AGENTS.md
- Keep package CLAUDE.md only where Claude-specific gotchas exist (data, ssl_certificates)
- Add CLAUDE.local.md to .gitignore
* feat: address Copilot review comments in PR #981
- Fix md_formatter.py docstring: was 'prettier', now correctly says 'markdownlint-cli2 --fix'
- Expand settings.json deny list to cover keystores (.jks, .keystore, .p12) and signing keys (.pem, .key, .p8)
- Fix AGENTS.md import groups: two '2.' entries corrected to sequential 1–6 numbering
- Fix packages/data/CLAUDE.md migration steps: misnumbered list corrected to 1–5
- Add working directory note to packages/data/AGENTS.md commands section
- Fix _web_socket_channel/AGENTS.md: 'pinned to' → 'constrained to' for caret constraint
* feat: remove webtrit_phone_keystores from deny list (dir is outside project scope)
* feat: configure gitignore, formatter, and analysis for generated files (#985)
* feat: configure gitignore, formatter, and analysis for generated files
- Add **/build/ and **/.claude/ to .gitignore
- Update lefthook pre-commit to skip *.g.dart / *.freezed.dart / *.gr.dart
- Replace melos fmt/fmt:check with find-based scripts that exclude generated files
* feat: update melos v7 integration across features, DAOs, and screenshots
* feat: allow feat/ as valid branch prefix alongside feature/ (#987)
* feat: allow feat/ as valid branch prefix alongside feature/
Update branch-name-check.sh and git-lint.yml to accept both feat/ and
feature/ prefixes. Add CONTRIBUTING.md documenting branch naming rules
and commit conventions.
* docs: fix table alignment in CONTRIBUTING.md
* docs: consolidate git conventions into CONTRIBUTING.md as single source of truth
- Remove .rules.md (referenced non-existent .rules/ directory)
- Update .github/copilot-instructions.md to reference CONTRIBUTING.md and AGENTS.md
- Add feat/ to branch pattern in copilot-instructions.md
- Fix pre-commit hook description in docs/development.md (dart format, not flutter analyze)
- Add CONTRIBUTING.md reference in docs/development.md
- Add docs/development.md pointer in CONTRIBUTING.md hooks section
* feat: favorites remote syncable (#984)
* refactor: cdr ui improvements, disconnect reason translations (#989)
* fix: use firstWhere instead of first in getAllContacts test to avoid ordering assumption (#991)
* fix: change contact_phones UNIQUE constraint to (number, label, contact_id) (#992)
* feat: change contact_phones UNIQUE constraint to (number, label, contact_id)
* feat: update ContactPhonesDao to use (number, label) pairs for stale-row deletion
* feat: persist per-label phone rows in ContactsLocalDataSource for DID-only accounts
* feat: add tests for DID-only contact phone handling and schema migration v20
* docs: add inline comment explaining grouping and merge logic in displayPhones
* refactor: rename lambda param p to phone in deleteOtherContactPhonesOfContactId call
* style: apply dart format to changed files
* docs: add DartDoc to deleteOtherContactPhonesOfContactId
* docs: add inline comments to deleteOtherContactPhonesOfContactId body
* style: replace non-ASCII arrow with hyphen in comment
* fix: use canonical label in displayPhones to prevent merged label from reaching favorites
* fix: recreate contact_phones via new table to preserve favorites FK reference
* test: add favorites FK integrity assertion for migration v20
* fix: resolve canonical ContactPhone by id before passing to favorites
* docs: document merged label display and canonical label favorites behavior
* refactor: replace ContactPhone display with flat fields in ContactPhoneDisplayEntry
- Replace synthetic ContactPhone display field with displayLabel (String)
and displayFavorite (bool) - only the two fields that actually differ
from the canonical phone
- Rename canonical field to phone for clarity
- Update displayPhones convenience getter to reconstruct ContactPhone
from phone + flat display fields
- Update ContactPhoneTileAdapter to accept only primitives and closures,
removing all model object dependencies
- Update ContactScreen loop to use displayPhoneEntries with closures
capturing entry.phone directly
- Add widget tests for ContactPhoneTileAdapter covering all enable flags,
callback wiring, transfer logic, and popup menu entries
- Add direct tests for displayPhoneEntries covering displayLabel,
displayFavorite, and phone field contracts
* feat: melos exported env support (#993)
* fix: log records file stability improvements (#998)
* fix: guard File.delete() with exists() check in shareLogRecords
Prevents PathNotFoundException on Android 15 when the OS clears the
app cache directory under background memory pressure while the native
share sheet is open, causing the finally block to attempt deleting a
file that no longer exists (Crashlytics issue 25d22f4de9016c556b46cfacea29a20b).
* fix: delegate dispose Future in LogRecordsFileRepositoryImpl
Without awaiting or returning the Future from RotatingFileAppender.dispose(),
the file handle could remain unclosed after the caller awaited repository
disposal. Use direct return to delegate the Future without an async wrapper.
* fix: replace existsSync retry with async exists() in _getAllLogFilesWithRetry
existsSync() reads from the OS filesystem cache and can return false
immediately after forceFlush() even though the file is on disk.
Switching to async File.exists() forces a fresh stat() call that
bypasses the cache. Also reduces max wait from 10s (5x2s) to 1s (10x100ms).
* fix: guard forceFlush() with try/catch in readAllLogs
An I/O error during forceFlush() would propagate and abort readAllLogs
entirely, returning no logs to the caller. Catching the error allows
reading whatever files are already on disk.
* fix: guard forceFlush() with try/catch in cleanLogs
An I/O error during forceFlush() would abort cleanLogs entirely,
leaving stale log files on disk. Catching the error allows deletion
to proceed on whatever files are already present.
* fix: replace retry loop with async file discovery in log records
- Replace _getAllLogFilesWithRetry (10×100ms polling loop) with
_getAllLogFilesAsync — single async pass using await file.exists()
per rotation slot, bypassing OS filesystem cache without any delay
- Extend file discovery range to 0..keepRotateCount inclusive so
rotated files (e.g. app_logs.log.1) are found even when base file
is absent immediately after rotation
- Fix cleanLogs to use await _getAllLogFilesAsync() instead of
synchronous getAllLogFiles() which relied on existsSync()
- Fix readAllLogs iteration order: remove files.reversed so newer
file (rotation 0) is read before older rotated file (rotation 1)
- Remove redundant file.exists() guard inside readAllLogs loop since
_getAllLogFilesAsync already guarantees file presence
- Add full test coverage: LogRecordsMemoryRepositoryImpl,
ReadableRotatingFileAppender (readAllLogs + cleanLogs),
LogRecordsFileRepositoryImpl — 26 tests total
* fix: update outdated comment in readAllLogs test
* feat: add Claude Code PostToolUse hooks (dart formatter + newline enforcer) (#995)
* fix: call dropped when user taps before app fully initializes (#997)
* fix: wait for signaling and registration before failing outgoing call
In __onCallPerformEventStarted, the early registration guard ran before
the signaling wait, causing calls to drop immediately when the user tapped
call before the socket initialized. Move the guard after the signaling wait
and extend the wait predicate to also require registration status to be
known (isHandshakeEstablished && isSignalingEstablished).
* fix: hold outgoing call as pending until routing state is available
When the user taps call before CallRoutingCubit has initialized (app just
launched, user info not yet fetched), wait for the first non-null routing
state instead of immediately failing. The call proceeds automatically once
routing state becomes available. If the cubit is disposed while waiting,
the call is silently dropped.
* fix: remove unused notification import from CallController
* test: add CallController.createCall unit tests
Cover immediate dispatch, pending wait, cubit disposal, and
CallUndefinedLineNotification scenarios.
* fix: remove unused optional params in _FakeCallRoutingState
* fix: remove unnecessary call_controller.dart import in test
* fix: address Copilot review — unawaited createCall and fast-fail on signaling failure
- Make createCall void by delegating to private _createCallAsync via unawaited, avoiding unawaited_futures lint at all call sites
- Add isFailure condition to signaling firstWhere predicate so outgoing calls fail immediately on signaling failure instead of waiting full timeout
* fix: update call_controller tests to use void createCall
Replace await controller.createCall(...) with call + await Future.delayed(Duration.zero)
to pump microtasks after createCall became void
* refactor: extract signaling wait predicate into named variables
* fix: use currentState instead of state for registration check after signaling wait
* fix: replace non-ASCII em dash with semicolon in comment
* refactor: remove redundant signalingConnected/registrationKnown vars, use CallState getters directly
* fix: await all addTrack calls before createOffer to prevent empty offer
* refactor: add TODO to provide CallController as singleton via RepositoryProvider
* fix: log warning when callRoutingCubit closes before routing state arrives
* refactor: add comment explaining callRoutingState await logic
* fix: add timeout to routing state wait and show NoInternetConnectionNotification on expiry
* refactor: extract _waitForRoutingState helper to clean up routing state await
* fix: catch unexpected errors from _createCallAsync and add timeout notification test
* fix: await reportNewIncomingCall in background FCM handler (#1001)
Without await the Pigeon IPC Future was fire-and-forget — the background
isolate was destroyed before the call reached the Kotlin side, so
PhoneConnectionService.startIncomingCall() was never invoked and the
incoming call UI never appeared.
* feat: provide CallController as singleton via RepositoryProvider in MainShell (#1000)
* feat: provide CallController as singleton via RepositoryProvider in MainShell
Register CallController once in MainShell widget tree after CallBloc,
CallRoutingCubit and NotificationsBloc are available. All seven call
sites now obtain the shared instance via late final field initializer
(context.read<CallController>()) instead of constructing a new object
per StatefulWidget.
* feat: replace RepositoryProvider with CallControllerScope InheritedWidget
RepositoryProvider is semantically for the data layer; CallController
is a UI-tier coordinator. Introduce CallControllerScope (InheritedWidget)
following the PresenceViewParams pattern already in the codebase.
All seven consumers now resolve the controller via CallControllerScope.of(context).
* fix: prevent stale CallController on MainShell rebuild
Store CallController in _MainShellState via ??= so it is created once
regardless of how many times build() reruns. Switch CallControllerScope.of
to getElementForInheritedWidgetOfExactType to avoid registering a rebuild
dependency that would never fire (updateShouldNotify is always false).
* feat: add doc comment for _callController field in _MainShellState
* fix: feature access mapper logic (#1007)
* fix: correct icon and text case assertions in integration tests (#1010)
* fix: correct icon and text case assertions in integration tests
* revert: remove accidentally committed logger from call_bloc.dart
* feat: add integration tests for webtrit_signaling keepalive and disconnect (#1009)
* feat: add integration tests for webtrit_signaling keepalive and disconnect
Add mock-based and live integration tests covering:
- Keepalive timeout (WebtritSignalingKeepaliveTransactionTimeoutException)
- Normal keepalive cycles (echo → timer restarts)
- Graceful disconnect (onDisconnect callback)
- Stream error (onError callback)
- Execute after disconnect (WebtritSignalingDisconnectedException)
- Request transaction timeout (non-keepalive variant)
- Live tests against a real server (skipped when credentials not set)
- Network simulation via pure-Dart WebSocket proxy with pause/resume
to verify keepalive timeout fires on real packet drop
* feat: replace WebSocket proxy with raw TCP proxy in live signaling test
Drop packets at the TCP byte level instead of WebSocket frame level
for more realistic network simulation. The proxy rewrites the HTTP
Host header before forwarding so the server accepts the upgrade request.
* feat: extract TcpProxy into reusable _tcp_proxy package
Move the raw TCP proxy from the signaling test into a standalone
internal package packages/_tcp_proxy so it can be used as a
dev dependency in both webtrit_signaling tests and app-level tests.
* fix: address Copilot review comments on signaling tests and tcp proxy
- Fix expectLater() to pass Future directly instead of closure
- Make live test client nullable to prevent LateInitializationError in tearDown
- Use local signalingClient variable in each live test for clarity
- Replace localhost with 127.0.0.1 to avoid IPv6-first resolution issues
- Remove unconditional TLS certificate bypass in live test HTTP client
- Discard bytes while paused in _relayWithHostRewrite to prevent unbounded buffer growth
- Add 64 KB header size guard with socket teardown on overflow
- Add AGENTS.md, README.md, analysis_options.yaml to _tcp_proxy package
* fix: add lints dev_dependency to _tcp_proxy so analysis_options.yaml resolves
* fix: replace magic timing numbers with constants in integration tests
* docs: add DartDoc to _findCrlfCrlf explaining HTTP header boundary detection
* fix(signaling): reconnect silently on server force-close (code 4441) (#1012)
When the server sends disconnect code 4441 (controllerForceAttachClose)
it means two signaling sessions from the same account were open at the
same time — a race that can happen when a background push isolate is
still connected as the main engine comes to the foreground and
reconnects signaling.
Previously this code path set lastSignalingDisconnectCode, which
triggered CallStatus.connectIssue and showed a "Connection issue"
snackbar to the user even though the situation was transient and
self-healing.
Fix: on 4441, emit lastSignalingDisconnectCode=null (no connectIssue
UI), skip the notification, and reconnect with the fast 1s delay
instead of the default 3s. A warning log is emitted so the race remains
visible in logs for debugging.
* refactor: decouple AppLogger from LogzioLoggingService via RemoteLoggingService abstraction (#1011)
* refactor: decouple AppLogger from LogzioLoggingService via RemoteLoggingService abstraction
* refactor: move remoteMinLevel extraction to init to avoid cast in applyConfig
* refactor: add minLevel to RemoteLoggingService and remove cast from AppLogger
* refactor: reorder LogzioLoggingService fields to match RemoteLoggingService interface order
* refactor: remove labelsProvider from AppLogger state, pass labels explicitly
* refactor: rename regenerateRemoteLabels to updateRemoteLabels and dispose before reinitialize
* refactor: replace labels map with lazy callback in AppLogger to encapsulate labels retrieval
* fix: attach remote appender before applyConfig so early logs are forwarded to remote
* feat: enable and disable log anonymization with env (#1004)
* feat: enable and disable log anonymization with env
Added functionality to disable and enable log anonymization via environment variable
* refactor: simplify AnonymizationType to none/full enum with bool flag
Replace the mutable list-based anonymization approach with a single
AnonymizationType enum (none/full), removing the race condition risk
and simplifying the API surface.
* docs: document intentional anonymization scope in AppLogger
* fix: add @override annotation to setAnonymizationEnabled in LogzioLoggingService
---------
Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
* fix: api healthcheck path (#1014)
* fix: use shared static logger in WebtritSignalingClient (#1013)
* fix: use shared static logger in WebtritSignalingClient with instance id prefix
* fix: restore custom logger support via inner constructor parameter
* fix: simplify logger to use fixed name without instance counter
* fix: remove custom logger parameter from inner constructor
* fix: signaling errors (#1016)
* fix: snackbar duration
* fix: new persist option usage
* fix: reconnect message repeating
* fix: reconnect on keepalive timeout
* feat: user info cache (#1015)
* feat: main impl
* refactor: call to actions fix
* fix: call to actions provider
* docs: getAndListed explain
* fix: integration tests rescue (#1025)
* fix: migrate to 4.5
* fix: various fixes
* fix: remove log file from standart test
* fix: no audio if lone codec (#1028)
* fix: WebRTC signaling state guards — ICE restart, renegotiation, and glare (WT-986) (#1003)
* refactor: extract RenegotiationHandler with stable-state and concurrency guards
- Extract renegotiation logic from CallBloc into a standalone RenegotiationHandler class
- Add two stable-state guards: pre-offer check and TOCTOU guard after createOffer
- Add _isHandling/_pendingRetry flags to serialize concurrent onRenegotiationNeeded firings
- Catch WebtritSignalingErrorException for server-side error logging without swallowing
- Catch plain String errors (flutter_webrtc native) separately from Dart exceptions
- Add unit tests covering stable-state skip, concurrency serialization, and error paths
- Document server-mediated vs P2P topology constraints and Perfect Negotiation limitation
* fix: add signalingState guards and Perfect Negotiation rollback to call flow
ICE restart handler:
- Skip setLocalDescription when signalingState != stable to prevent native crash
- Log warning instead of silently skipping
Renegotiation / accepted handler:
- Guard setRemoteDescription(answer) against wrong state after glare resolution
- Log transceivers after setRemoteDescription for SDP debugging
- Catch String errors from setRemoteDescription to prevent unhandled exception escalation
Updating handler (Perfect Negotiation rollback):
- Pre-check signalingState for glare: if have-local-offer, roll back local offer before setRemoteDescription
- Catch String errors containing have-local-offer as fallback for stale flutter_webrtc signalingState cache
- Roll back and retry setRemoteDescription on confirmed glare
Renderer and state:
- Always refresh srcObject in RTCStreamView.didUpdateWidget to handle renegotiation-replaced tracks
- Fix remoteVideo getter to use logical OR (stream tracks || video flag) instead of short-circuit
Add call_state_test.dart covering ActiveCall equality and remoteVideo edge cases
* refactor: replace on String catch with typed RTC exceptions via RtcJsepErrorParser
* fix: guard RTCVideoRenderer srcObject assignment until initialize() completes (#1027)
* fix: guard RTCVideoRenderer srcObject assignment until initialize() completes
Prevents a crash where didUpdateWidget set srcObject before the renderer
was initialized, causing 'Call initialize before setting the stream'.
The _initialized flag ensures srcObject is only set after initialize()
resolves. Also always refreshes srcObject (not just on stream identity
change) to handle track replacement during renegotiation.
* fix: use setState and guard build before renderer initialization
Wrap _initialized and srcObject assignment in setState so the flag change
is properly synchronized with Flutter's build cycle. Guard build() to
avoid passing an uninitialized renderer to RTCVideoView — the
placeholderBuilder (or empty SizedBox) is shown until initialize()
completes.
* fix: update ExternalContactsSyncBloc tests to mock getAndListen instead of getLocalInfo (#1029)
* feat: handle 'voicemail_not_configured' error (#1005)
* feat: handle 'voicemail_not_configured' error
Added handling 'voicemail_not_configured' error from voicemail api
* fix: properly handle voicemail_not_configured and endpoint_not_supported errors across all layers
- WebtritApiClient: skip SEVERE log for expected VoicemailNotConfiguredException / EndpointNotSupportedException
- VoicemailRepositoryImpl: add _featureSupported flag to skip API calls once feature is known unavailable; add _fetchingCompleter.future.ignore() to prevent unhandled future errors; suppress stack trace in warning log for expected exceptions; expose isFeatureSupported getter
- VoicemailRepository: add isFeatureSupported to abstract interface; EmptyVoicemailRepository returns false
- VoicemailCubit: check isFeatureSupported on init to immediately emit featureNotSupported without an API call
- SettingsBloc: catch expected exceptions from unawaited fetchVoicemails() to prevent runZonedGuarded propagation
- PollingService / ConnectivityLifecycleService: remove stack trace from WARNING logs in generic catch blocks
* fix: always allow navigation to voicemail screen regardless of feature support
Previously the settings tile was disabled with reduced opacity when voicemail
was not configured, which was confusing. Now the tile is always tappable and
the voicemail screen shows a placeholder explaining the reason.
* fix: move VoicemailCubit provider from SettingsScreenPage to VoicemailScreenPage
VoicemailCubit was provided at the settings level but consumed in a separate
route, causing ProviderNotFoundException on navigation. Moving it to
VoicemailScreenPage makes the provider scope match the consumer.
* fix: add missing call feature import to VoicemailScreenPage
* fix: remove unused stack trace variables from catch blocks in polling and connectivity services
* fix: address Copilot review comments
- VoicemailCubit: guard watchVoicemails subscription against overwriting featureNotSupported state
- VoicemailNotConfiguredException: pass token and error fields to super; remove commented-out placeholder
- WebtritApiClient: log xRequestId (never null) instead of requestId parameter; pass token/error to VoicemailNotConfiguredException
- SettingsTile: add assert that opacity is in [0.0, 1.0]
---------
Co-authored-by: Dmytro Serdun <d.serdun@webtrit.com>
* feat: add toJson() to all Event subclasses and fix NotifyEvent constructor inconsistency (#1030)
* fix: suppress transient network error SnackBar in RegisterStatusCubit (#1031)
Auto-triggered fetches (startup and connectivity restore) now use
_fetchStatusSilently(), which skips handleError for SocketException,
TimeoutException, and TlsException. The cubit retries automatically
on the next connectivity change, so surfacing these transient failures
to the user is misleading. User-initiated fetchStatus() retains the
original behaviour and still calls handleError for all errors.
* fix: add 10s timeout to reportNewIncomingCall in background message handler (WT-1061) (#1032)
Telecom on affected devices can become overloaded with phantom PhoneAccount
registrations, causing reportNewIncomingCall() to hang indefinitely. This
keeps FlutterFirebaseMessagingBackgroundService alive and blocks subsequent
cold starts. Adding a 10s timeout bounds the handler lifetime and logs a
warning when Telecom is slow to respond.
* fix: add 5s timeout to getInitialMessage() to prevent splash freeze (WT-1061) (#1033)
* refactor: remove tryUse from AppDatabaseScope, migrate callers to useOrNull (#1034)
* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061) (#1035)
* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061)
Spawns a single dedicated DriftIsolate server in the main isolate bootstrap
and registers its SendPort in IsolateNameServer under a fixed key.
Background isolates (FCM handler, WorkManager) now connect to the same server
via IsolateDatabase.connectOrCreate(), which looks up the port and creates a
client connection — falling back to a direct NativeDatabase connection when the
main app is not running (cold push with no foreground app).
All writes are serialized through the single server isolate, making
write-write SQLITE_BUSY (code=5) between concurrent isolates impossible.
Changes:
- app_database: add createAppDatabaseNative() for synchronous NativeDatabase
creation inside the server isolate (no createInBackground needed)
- IsolateDatabase: add spawnServer(), connectOrCreate(), kDbPortName
- AppDatabaseScope.use(): connect via connectOrCreate() instead of create()
- bootstrap(): spawn DriftIsolate server, register DriftIsolate in InstanceRegistry
- AppDatabaseLifecycleHolder: connect to DriftIsolate, shutdown server on dispose
* fix: address Copilot review — robust spawnServer error handling and stale port cleanup (WT-1061)
* test: add integration tests for IsolateDatabase stale port handling (WT-1061)
* refactor: introduce SignalingModule stream abstraction (phase 1) (#1024)
* refactor: introduce SignalingModule stream abstraction (phase 1)
Replace SignalingManager callback-based API with SignalingModule — a
sealed-event broadcast stream that owns the WebtritSignalingClient
lifecycle without any BLoC, CallState, or UI dependency.
Key changes:
- Add SignalingModule with fire-and-forget connect(), disconnect(),
dispose() and a sealed SignalingModuleEvent hierarchy
(Connecting, Connected, ConnectionFailed, Disconnecting,
Disconnected, HandshakeReceived, ProtocolEvent)
- Add isRepeated deduplication on ConnectionFailed to suppress
repeated identical error notifications
- Map disconnect codes to recommendedReconnectDelay:
4441 → Duration.zero, protocolError → null, all others → 3 s
- Migrate CallBloc from direct WebtritSignalingClient callbacks to
SignalingModule stream subscription; new _SignalingClientEvent
variants: connecting, connected, failed, disconnecting, disconnected
- Migrate IsolateManager (Push + Foreground) to SignalingModule,
replacing SignalingManager; add connectivity monitoring and pending
request queue inside IsolateManager
- Construct SignalingModule in main_shell.dart and inject into CallBloc
- Delete SignalingManager and remove its export from common.dart
- Add 31 unit and integration tests for SignalingModule
* fix(test): update ExternalContactsSyncBloc tests for getAndListen API
The BLoC was updated to call userRepository.getAndListen() instead of
getLocalInfo(), but the mocks were never updated. Fix the setUp mock
and correct the RefreshFailure test to use load() failure (which is
the actual trigger for that state) rather than userRepository failure.
* fix(test): rename local function to avoid leading underscore lint warning
* docs: translate signaling architecture doc to English
* docs: remove phase 1 requirements planning doc from repo
* refactor: remove coreUrl/tenantId/token/trustedCertificates from CallBloc
These four fields were passed through CallBloc only to construct
SignalingModule internally. Now that SignalingModule is constructed
externally and injected via the constructor, the fields are dead code.
Remove them and the corresponding import from ssl_certificates.
* fix: address Copilot review comments on SignalingModule/IsolateManager
- Guard delayed reconnect callbacks with signalingClient == null check to
avoid tearing down a healthy connection that connected during the delay
- Populate _incomingCallEvents from handshake and protocol events so
_findIncomingEventLog returns real caller data instead of null
- Use disconnect() instead of dispose() in handleLifecycleStatus so the
module remains reusable when the app returns to the foreground
- Fix post-dispose connect() test to actually subscribe to the event stream
and assert Connecting/Connected events are absent after dispose
* feat: replay session events to late subscribers in SignalingModule
Adds a per-subscriber replay buffer so that consumers created after
connect() (e.g. CallBloc constructed after SignalingModule already
connected and received a handshake) do not miss any events from the
current session.
- events getter now returns a single-subscription stream that first
replays all events buffered since the last connect() call, then
pipes live events from the broadcast controller
- connect() clears the buffer so late subscribers see only the
current session, not stale events from previous reconnect cycles
- dispose() also clears the buffer on teardown
- Uses sync: true on the intermediate StreamController to avoid an
extra async hop and keep delivery ordering consistent with callers
that await module operations
- Adds two integration tests covering the late-subscriber replay
and the buffer-clear-on-reconnect behaviours
* feat: connect SignalingModule early in initState to reduce call setup latency
SignalingModule is now created and connected in _MainShellState.initState(),
running the WebSocket handshake in parallel while the widget tree and
CallBloc are being built. When CallBloc is eventually created it subscribes
to the replay stream and receives all buffered session events without missing
anything.
_MainShellState.dispose() owns the module lifecycle; CallBloc.close()
still calls dispose() on the module (idempotent, safe).
* docs: update signaling architecture doc with layer descriptions and diagrams
* fix: add concurrency lock to _connectAsync to prevent parallel connects
* fix: await disconnect ack in dispose() to prevent SignalingDisconnected drop on race
* fix: suppress reconnect hint on intentional disconnect to prevent spurious reconnect
* fix: remove SignalingModule.dispose() from CallBloc.close() — ownership belongs to MainShellState
* fix: snapshot buffer before live subscribe to prevent replay duplicates in events getter
* fix: store reconnect Timer in IsolateManager so it can be cancelled on close()
* fix: forward recommendedReconnectDelay from SignalingDisconnected to _scheduleReconnect in CallBloc
* fix: suppress _onDisconnect after _onError to prevent double reconnect scheduling
* fix: exclude SignalingProtocolEvent from session buffer to prevent unbounded growth
* fix: replace force-unwrap of session.coreUrl/token with null-safe logout in initState
* fix: remove performEndCall early return so pre-handshake declines are queued
* fix: close liveController on subscription cancel to prevent StreamController leak
* fix: use _networkNone state instead of stale results snapshot in connectivity timer closure
* docs: fix _scheduleReconnectIfNeeded → _scheduleReconnect in signaling architecture doc
* test: replace Future.delayed(Duration.zero) with pumpEventQueue() in signaling module tests
* fix: make _controller sync:true to eliminate async-dispatch event duplication
* docs: clarify disconnect() docstring — SignalingDisconnected is callback-driven, not synchronous
* fix: wrap _signalingModule.disconnect() in unawaited() in _disconnectInitiated
* fix: remove unused shouldReconnect variable from __onSignalingClientEventDisconnected
* test: add 8 tests to reach 100% SignalingModule coverage
- concurrent connect() dropped while factory in-flight (_connecting guard)
- intentional disconnect() emits SignalingDisconnected with null delay
- disconnect() passes goingAway code to the underlying client
- _onError suppresses subsequent _onDisconnect (_errorHandled flag)
- SignalingProtocolEvent excluded from replay buffer
- cancelled subscription receives no further events
- dispose() awaits disconnect ack before closing the stream
- _onHandshake/_onEvent are no-ops after dispose()
* test: add scenario-driven SignalingModule tests from CallBloc usage analysis
Covers scenarios observed in CallBloc's signaling subscription:
internet dropped mid-session:
- _onError after handshake → ConnectionFailed not Disconnected, signalingClient cleared
- Unexpected socket close (null code) → Disconnected with kSignalingClientReconnectDelay
- ConnectionFailed buffered so late subscribers reconstruct last-known failure
handshake not completed:
- Disconnect before handshake → no HandshakeReceived in buffer, Disconnected with delay
- Late subscriber after no-handshake disconnect → gets Connecting+Connected+Disconnected only
- Error before handshake → ConnectionFailed buffered, no HandshakeReceived
- Reconnect after no-handshake failure delivers fresh session events
late subscriber mid-session:
- Factory still pending → gets Connecting from buffer, Connected arrives live
- After full connect+handshake → all three lifecycle events replayed, no protocol events
disconnect() robustness:
- client.disconnect() throws → dispose() still completes without hanging
- Second disconnect() with no active client is a silent no-op
* fix: restore callkeep_signaling_status_converter.dart lost during rebase
* fix: remove unused fields in _ThrowingDisconnectClient test helper
* fix: use normalClosure (1000) instead of goingAway (1001) when client disconnects WebSocket
* test: update disconnect test to expect normalClosure (1000) instead of goingAway (1001)
* fix: address Copilot review comments in SignalingModule and IsolateManager
- Fix doc comment on events getter: protocol events are not replayed, only lifecycle/handshake events
- Guard connect() buffer clear behind _connecting check to avoid clearing on redundant calls
- Remove stale comment in _onDisconnect that contradicted the !_disposed guard
- Treat empty connectivity result as offline in _monitorConnectivity (results.isEmpty || any(none))
- Treat empty connectivity result as offline in performAnswerCall (isNotEmpty && !contains(none))
* fix: address post-review issues in SignalingModule and IsolateManager
- isolate_manager: fix connectivityNoneCounter reset — error now fires
exactly once at maxConnectivityNoneRepeats; subsequent none-events are
silently ignored until connectivity is restored and counter resets to 0
- main_shell: split SignalingModule construction into valid/invalid-creds
branches to remove ?? '' fallbacks and make intent explicit
- signaling_module: document sync:true reentrancy assumption, single-use
constraint on events getter, and _errorHandled ordering invariant
* revert: restore main_shell.dart SignalingModule construction with ?? '' fallback
The split-branch approach still created a module with empty strings in the
null-creds case — identical in behaviour to the original. Reverted to the
original form which is honest about the fallback until a proper nullable
refactor is done.
* fix: remove dead null-guard in MainShellState.initState for SignalingModule
The router guard (onMainShellRouteGuardNavigation) redirects to login
when state.status != authenticated, so coreUrl and token are always
non-null when MainShell is mounted. Replace ?? '' fallbacks and the
unreachable null-branch with direct ! unwraps.
* docs: sync signaling_architecture_target.md and call_architecture.md with current code
- signaling_architecture_target: add timer cancellation to IsolateManager
and CallBloc _scheduleReconnect snippets (Future.delayed → Timer with cancel)
- signaling_architecture_target: add missing SignalingDisconnecting case to
CallBloc subscription snippet
- call_architecture: update ownership — CallBloc owns SignalingModule, not
WebtritSignalingClient directly
* fix: replace non-ASCII characters with ASCII equivalents in Dart sources
Replace em dash (U+2014) with '-' and right arrow (U+2192) with '->'
in comments and test descriptions across signaling_module.dart,
isolate_manager.dart, signaling_module_test.dart, and call_bloc.dart.
* fix: incorrect styling of status bar on app start (#1006)
Fixed status bar rendering with incorrect styling on initial app launch when theme is light
* feat: show progress indicator while sharing logs (#1036)
* feat: show progress indicator while sharing logs
Co-Authored-By: Dmytro Serdun <serdun@webtrit.com>
* refactor: replace inline SizedBox+CircularProgressIndicator with SizedCircularProgressIndicator
---------
Co-authored-by: Dmytro Serdun <serdun@webtrit.com>
* fix: upgrade to video resets hold (#1038)
* fix: media settings parsing (#1039)
* fix: call drops after theme or lang change (#1041)
* fix: cannot make calls after blind transfer — skip hangup + reconnect safety net (WT-1214) (#1040)
* fix: trigger reconnect when starting outgoing call with no signaling (WT-1214)
After a blind transfer, the signaling WebSocket is closed with code 4610
and the disconnect is marked intentional by SignalingModule, so no
reconnect is scheduled. Subsequent outgoing call attempts enter
outgoingConnectingToSignaling and wait passively — neither signalingReady
nor signalingFailed ever fires, causing the call to fail on timeout.
Add _scheduleReconnect(Duration.zero) at the start of the waiting block
so that initiating an outgoing call always recovers the signaling
connection, regardless of whether the previous disconnect was intentional.
* fix: skip hangup after successful blind transfer to avoid 4610 disconnect (WT-1214)
When a blind transfer completes (NOTIFY SIP/2.0 200 OK +
subscription_state: terminated), the SIP dialog is already closed
server-side via REFER. Sending a hangup request on the freed dialog
causes the server to close the WebSocket with code 4610 ("call request
on wrong line error"), triggering an unintended signaling disconnect.
Check the call's transfer state: if it is Transfering(fromBlindTransfer:
true), skip the hangup request and clean up the peer connection locally
only. This removes the root cause of the 4610 disconnect that led to
signaling not being reconnected for subsequent outgoing calls.
* test: cover blind-transfer hangup skip and 4610 reconnect hint
Add tests for two fixes from WT-1214:
- Transfer — isBlindTransferCompleted detection (call_state_test.dart):
verifies the switch pattern that decides whether to skip the hangup
request after a blind transfer. Covers Transfering(fromBlindTransfer:
true/false), earlier transfer states, and null.
- SignalingModule — requestCallIdError (4610) reconnect hint
(signaling_module_test.dart): verifies that a non-intentional 4610
carries a non-null recommendedReconnectDelay (reconnect scheduled),
while an intentional disconnect() followed by a server 4610 emits
null (reconnect suppressed — the scenario that triggered WT-1214).
* refactor: rename isBlindTransferCompleted → isBlindTransferInTransferingState
Transfering state in the model means "server started to process the
transfer", not "transfer completed". Rename the local variable and update
the surrounding comments and test group names to match the actual Transfer
model semantics, avoiding misinterpretation of the hangup guard.
Addresses Copilot review comments on PR #1040.
* fix: call or transfer to myself handling (#1046)
* fix: hide video for held call (#1048)
* fix: hide video for held call
* fix: tap area
* fix: transfer to same recipient (#1049)
* refactor: extract SignalingModuleInterface; migrate CallBloc and IsolateManager; extract toLinesState (#1045)
* refactor: extract SignalingModuleInterface; decouple IsolateManager from SignalingModule
Add local SignalingModuleInterface abstract class to signaling_module.dart
with the contract needed by IsolateManager: events, isConnected, connect(),
disconnect(), execute(Request), dispose().
SignalingModule implements the interface, gaining isConnected and execute()
alongside the existing signalingClient getter (kept for backward compat).
IsolateManager field type changed from SignalingModule to SignalingModuleInterface.
All signalingClient null-checks replaced with isConnected; direct client.execute()
calls replaced with module.execute(). IsolateManager no longer depends on the
concrete class, making it ready for a plugin-backed implementation.
* fix: decouple NetworkCubit from WebtritSignalingService; fix push dedup race
NetworkCubit held a concrete WebtritSignalingService only to call
updateMode(). Replaced with a Future<void> Function(SignalingServiceMode)
callback so the cubit has no plugin dependency and is mock-testable.
Call site passes WebtritSignalingService().updateMode as a tear-off.
_onCallPushEventIncoming checked the incomingFromOffer guard before
awaiting contactNameResolver.resolveWithNumber(). The signaling path
could create an ActiveCall during that async gap, causing both paths
to emit separate entries for the same callId. Added a post-await guard
that checks for any existing ActiveCall with the same callId before emitting.
* fix: prevent premature call routing state emission before signaling handshake (#1044)
LinesState.blank() is emitted at app startup before the signaling handshake
arrives. combineLatest fires immediately with cached UserInfo + blank LinesState,
causing CallRoutingCubit to emit a non-null state with empty mainLines.
CallController then skips _waitForRoutingState() and fails with
"no idle lines available".
Fix: add LinesState.isBlank discriminator (guestLine == null is an unambiguous
pre-handshake marker — CallBloc always sets guestLine to non-null after any
handshake). Return null from _combineInfo when linesState.isBlank so the cubit
stays in its unready state and CallController waits correctly.
* fix: preserve LinesState.blank in onChange until signaling handshake arrives
The previous fix relied on guestLine == null as a pre-handshake discriminator,
but CallBloc.onChange always set guestLine = LineState.idle regardless of
linesCount, overwriting LinesState.blank() almost immediately after startup.
Root cause: onChange fired with linesCount = 0 on any early state change
(e.g. connecting status) and produced LinesState([], LineState.idle).
isBlank returned false, so _combineInfo emitted a non-null CallRoutingState
with empty mainLines, and hasIdleMainLine = false blocked the call.
Fix: when linesCount == 0 (handshake not yet received), onChange now stores
LinesState.blank() explicitly. Once the handshake sets linesCount > 0,
normal LinesState with non-null guestLine is produced as before.
* fix: remove unused models import from NetworkScreenPage
* fix: wire BackgroundSignalingBootstrapService into NetworkCubit callback
* refactor: migrate CallBloc to SignalingModuleInterface
Apply the same interface migration already done for IsolateManager:
- change _signalingModule field/param type from SignalingModule to SignalingModuleInterface
- replace signalingClient?.execute() with execute() from the interface
- replace signalingClient != null checks with isConnected
* fix: address Copilot review comments on signaling module interface
- Handle null execute() in _executePendingRequests: complete completer
with error and clean up instead of leaving request to time out silently
- Handle null execute() in _sendRequest: log warning and return early
instead of awaiting null when module disconnects after isConnected check
- Fix linesCount == 0 guard: use isHandshakeEstablished to distinguish
pre-handshake blank state from valid post-handshake 0-lines state
- Update LinesState.isBlank doc comment to remove inaccurate claim that
guestLine == null is an unambiguous pre-handshake marker
* refactor: extract toLinesState() from CallBloc.onChange into CallState
Pure deterministic logic moved to CallState.toLinesState() so it can be
tested without standing up CallBloc. onChange becomes a single line.
Tests cover: pre-handshake blank, 0-lines post-handshake with guest line,
main line idle/inUse combinations, guest line inUse alongside main calls.
* fix: rename _kRegistered to kRegistered (lint: no_leading_underscores_for_local)
* refactor: rename SignalingModuleInterface → SignalingModule, impl → SignalingModuleIsolateImpl
Drop the `Interface` postfix from the abstract contract and suffix the
concrete WebSocket implementation with `IsolateImpl` to clarify its role.
Add doc comments to every member of the SignalingModule interface.
Files touched:
- lib/features/call/services/signaling_module.dart
- lib/features/call/services/isolate_manager.dart
- lib/features/call/bloc/call_bloc.dart
- lib/app/router/main_shell.dart
* fix: update signaling_module_test to use SignalingModuleIsolateImpl concrete type
* fix: use SignalingModule.execute instead of signalingClient getter (#1050)
signalingClient is not part of the SignalingModule interface — only
SignalingModuleIsolateImpl exposes it. Replace with execute() which is
defined on the interface and returns null when not connected.
* fix: skip decline when push-registered call receives signaling line (#1051)
* fix: skip decline when push-registered call receives signaling line (WT-1091)
When an incoming call arrives via FCM push, it is registered with
line _kUndefinedLine (-1) as a placeholder. When the signaling
WebSocket subsequently delivers the same call with a real line (e.g. 0),
the guard that detects 'call to myself' incorrectly fires because
-1 != 0, causing a DECLINE to be sent on the wrong line and the call
to be dropped with server error 4610.
Fix: exclude _kUndefinedLine from the line-mismatch check so that
push-placeholder calls are updated with the real line rather than
declined.
* test: add push → signaling line handoff tests for WT-1091 fix
* feat: media settings ptime warning (#1053)
* fix: call glare (#1052)
* fix: call glare
* fix: commented code
* feat: call interaction guard if any updating (#1056)
* fix: stop ringback sound on forced call termination (#1055)
__onResetStateEventCompleteCall is the path taken when signaling disconnects
unexpectedly (network loss). Unlike other termination paths it was not calling
_stopRingbackSound(), leaving the ringback tone playing after the call UI disappeared.
* fix: sanitize keypad input before initiating a call (WT-1026) (#1057)
* fix: sanitize keypad input before initiating a call (WT-1026)
Apply PhoneParser.normalize() in _popNumber() so that Unicode lookalike
digits and stray formatting characters are stripped before the number
reaches CallController/CallBloc.
* fix: normalize keypad input at entry point via TextInputFormatter (WT-1026)
Replace the call-time normalize approach with a TextInputFormatter so
pasted or typed Unicode lookalike characters are sanitized immediately
in the TextField, keeping the displayed text and the dialed number
consistent.
* refactor: move PhoneNormalizingFormatter to keypad/utils (WT-1026)
Extract formatter into its own file under features/keypad/utils/ with a
barrel export, following the existing package structure convention.
* fix: strip non-dialable characters from keypad input (WT-1026)
After Unicode normalization, remove any character outside [0-9*#+] so
pasting arbitrary text (e.g. *"{) leaves only valid phone number chars
in the field.
* fix: address Copilot review comments on PhoneNormalizingFormatter (WT-1026)
- Cache _nonDialableChars as static final to avoid per-keystroke allocation
- Translate both baseOffset and extentOffset (preserving affinity/isDirectional)
instead of always returning a collapsed selection
- Extract sanitize() static helper and reuse it in _popNumber() as a
belt-and-suspenders guard for programmatic controller updates
* test: add unit tests for PhoneNormalizingFormatter (WT-1026)
Covers sanitize() and formatEditUpdate() — including Unicode normalization,
non-dialable character stripping, cursor translation, and selection range
preservation.
* refactor: extract renegotiation support (#1058)
* fix: prevent null crash in InkWell on hardware keyboard event during navigation (WT-1012) (#1060)
* fix: prevent null crash in InkWell on hardware keyboard event during navigation (WT-1012)
Remove debug focusColor/hoverColor from CdrTile InkWell to stop it
subscribing to _HighlightModeManager, and add FocusScope.unfocus()
before all fullscreenDialog navigations to cover ListTile-based widgets.
* fix: use FocusManager and mounted guard in openNotificationsScreen (WT-1012)
Replace FocusScope.of(context).unfocus() with
FocusManager.instance.primaryFocus?.unfocus() to avoid depending on
a potentially unmounted context, and add a mounted guard before
navigating, since the callback can fire from a stream after disposal.
* fix: voicemail audio cache collision on Android (WT-1016) (#1061)
* fix: resolve voicemail audio cache collision on Android (WT-1016)
All voicemails shared the same cache file path because the last URL
path segment is always 'attachment'. Concurrent LockCachingAudioSource
instances raced to rename the same .part file, causing a crash.
- Add cacheKey param to AudioView; VoicemailTile passes voicemail.id
- _getCacheFile falls back to joined URI segments when cacheKey is absent
- Ensure media_cache directory exists before screen initialises
* refactor: ensure media_cache dir exists in AppPath.init
Move Directory.create to AppPath.init so the path is ready to use
by the time any feature accesses mediaCacheBasePath, consistent with
how getApplicationDocumentsDirectory / getTemporaryDirectory work.
* refactor: move media_cache dir creation into AudioView._getCacheFile
AppPath is platform-agnostic and must not import dart:io (web support).
Directory is now created in _getCacheFile, co-located with the code
that writes the cache file and already guarded by the Platform.isIOS check.
* fix: address Copilot review comments on AudioView cache handling
- Move Directory.create to _initialize() as async IO, avoiding
synchronous filesystem call in _getCacheFile on every retry
- Guard empty rawKey with early return null so just_audio handles
caching when URI has no path segments
- Sanitize cacheKey against path separators to prevent path traversal
from server-provided voicemail IDs
* refactor: renegotiation handlers per call (#1065)
* fix: fall back to audio-only when camera permission is denied on incoming video call (WT-1049) (#1062)
* fix: fall back to audio-only when camera permission is denied on incoming video call (WT-1049)
When answering an incoming video call without camera permission, the app
no longer drops the call. Instead, it retries media acquisition with
video disabled and answers audio-only. If the microphone itself is
unavailable the error still propagates as before.
* refactor: check camera permission before requesting media instead of relying on error fallback (WT-1049)
Added `isVideoAvailable()` to the `UserMediaBuilder` contract so the BLoC
can explicitly query camera permission status before deciding whether to
request video. Replaces the previous error-based fallback approach with a
proactive permission check.
* refactor: inject camera permission check as callback into UserMediaBuilder (WT-1049)
Replaced the direct `permission_handler` dependency in `user_media_builder.dart`
with an optional `isCameraPermissionGranted` callback. `DefaultUserMediaBuilder`
remains free of plugin dependencies; the check is wired at construction time in
`main_shell.dart` via `AppPermissions.isPermissionGranted(Permission.camera)`.
* refactor: move permission-aware video resolution into UserMediaBuilder.build() (WT-1049)
`DefaultUserMediaBuilder.build()` now resolves the effective video flag
internally via `_isCameraAvailable()` before calling `getUserMedia`, so
callers pass their intent (`video: offer.hasVideo`) and the builder handles
the permission check transparently.
`call_bloc.dart` derives the resulting video state from the actual stream
tracks (`localStream.getVideoTracks().isNotEmpty`) instead of pre-computing
it, removing all permission logic from the BLoC layer.
* fix: scope audio fallback to incoming answer only via allowAudioFallback flag (WT-1049)
The unconditional permission-check fallback inside `build()` was silently
downgrading video for all callers, including the camera-enable action which
relies on `UserMediaError` to show a notification and skip the `video: true`
state update.
Introduced `allowAudioFallback: bool = false` on `build()`. Default behaviour
(throw on failure) is preserved for existing callers; `__onCallPerformEventAnswered`
opts in with `allowAudioFallback: true` to get the permission-aware fallback.
* test: add unit tests for DefaultUserMediaBuilder permission-aware fallback (WT-1049)
* fix: retry getUserMedia audio-only when video acquisition fails with allowAudioFallback (WT-1049)
* fix: stop voicemail polling after server responds with unsup…
SERDUN
added a commit
that referenced
this pull request
Jun 21, 2026
* fix: add 5s timeout to getInitialMessage() to prevent splash freeze (WT-1061) (#1033)
* refactor: remove tryUse from AppDatabaseScope, migrate callers to useOrNull (#1034)
* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061) (#1035)
* fix: eliminate write-write SQLite contention via shared DriftIsolate server (WT-1061)
Spawns a single dedicated DriftIsolate server in the main isolate bootstrap
and registers its SendPort in IsolateNameServer under a fixed key.
Background isolates (FCM handler, WorkManager) now connect to the same server
via IsolateDatabase.connectOrCreate(), which looks up the port and creates a
client connection — falling back to a direct NativeDatabase connection when the
main app is not running (cold push with no foreground app).
All writes are serialized through the single server isolate, making
write-write SQLITE_BUSY (code=5) between concurrent isolates impossible.
Changes:
- app_database: add createAppDatabaseNative() for synchronous NativeDatabase
creation inside the server isolate (no createInBackground needed)
- IsolateDatabase: add spawnServer(), connectOrCreate(), kDbPortName
- AppDatabaseScope.use(): connect via connectOrCreate() instead of create()
- bootstrap(): spawn DriftIsolate server, register DriftIsolate in InstanceRegistry
- AppDatabaseLifecycleHolder: connect to DriftIsolate, shutdown server on dispose
* fix: address Copilot review — robust spawnServer error handling and stale port cleanup (WT-1061)
* test: add integration tests for IsolateDatabase stale port handling (WT-1061)
* refactor: introduce SignalingModule stream abstraction (phase 1) (#1024)
* refactor: introduce SignalingModule stream abstraction (phase 1)
Replace SignalingManager callback-based API with SignalingModule — a
sealed-event broadcast stream that owns the WebtritSignalingClient
lifecycle without any BLoC, CallState, or UI dependency.
Key changes:
- Add SignalingModule with fire-and-forget connect(), disconnect(),
dispose() and a sealed SignalingModuleEvent hierarchy
(Connecting, Connected, ConnectionFailed, Disconnecting,
Disconnected, HandshakeReceived, ProtocolEvent)
- Add isRepeated deduplication on ConnectionFailed to suppress
repeated identical error notifications
- Map disconnect codes to recommendedReconnectDelay:
4441 → Duration.zero, protocolError → null, all others → 3 s
- Migrate CallBloc from direct WebtritSignalingClient callbacks to
SignalingModule stream subscription; new _SignalingClientEvent
variants: connecting, connected, failed, disconnecting, disconnected
- Migrate IsolateManager (Push + Foreground) to SignalingModule,
replacing SignalingManager; add connectivity monitoring and pending
request queue inside IsolateManager
- Construct SignalingModule in main_shell.dart and inject into CallBloc
- Delete SignalingManager and remove its export from common.dart
- Add 31 unit and integration tests for SignalingModule
* fix(test): update ExternalContactsSyncBloc tests for getAndListen API
The BLoC was updated to call userRepository.getAndListen() instead of
getLocalInfo(), but the mocks were never updated. Fix the setUp mock
and correct the RefreshFailure test to use load() failure (which is
the actual trigger for that state) rather than userRepository failure.
* fix(test): rename local function to avoid leading underscore lint warning
* docs: translate signaling architecture doc to English
* docs: remove phase 1 requirements planning doc from repo
* refactor: remove coreUrl/tenantId/token/trustedCertificates from CallBloc
These four fields were passed through CallBloc only to construct
SignalingModule internally. Now that SignalingModule is constructed
externally and injected via the constructor, the fields are dead code.
Remove them and the corresponding import from ssl_certificates.
* fix: address Copilot review comments on SignalingModule/IsolateManager
- Guard delayed reconnect callbacks with signalingClient == null check to
avoid tearing down a healthy connection that connected during the delay
- Populate _incomingCallEvents from handshake and protocol events so
_findIncomingEventLog returns real caller data instead of null
- Use disconnect() instead of dispose() in handleLifecycleStatus so the
module remains reusable when the app returns to the foreground
- Fix post-dispose connect() test to actually subscribe to the event stream
and assert Connecting/Connected events are absent after dispose
* feat: replay session events to late subscribers in SignalingModule
Adds a per-subscriber replay buffer so that consumers created after
connect() (e.g. CallBloc constructed after SignalingModule already
connected and received a handshake) do not miss any events from the
current session.
- events getter now returns a single-subscription stream that first
replays all events buffered since the last connect() call, then
pipes live events from the broadcast controller
- connect() clears the buffer so late subscribers see only the
current session, not stale events from previous reconnect cycles
- dispose() also clears the buffer on teardown
- Uses sync: true on the intermediate StreamController to avoid an
extra async hop and keep delivery ordering consistent with callers
that await module operations
- Adds two integration tests covering the late-subscriber replay
and the buffer-clear-on-reconnect behaviours
* feat: connect SignalingModule early in initState to reduce call setup latency
SignalingModule is now created and connected in _MainShellState.initState(),
running the WebSocket handshake in parallel while the widget tree and
CallBloc are being built. When CallBloc is eventually created it subscribes
to the replay stream and receives all buffered session events without missing
anything.
_MainShellState.dispose() owns the module lifecycle; CallBloc.close()
still calls dispose() on the module (idempotent, safe).
* docs: update signaling architecture doc with layer descriptions and diagrams
* fix: add concurrency lock to _connectAsync to prevent parallel connects
* fix: await disconnect ack in dispose() to prevent SignalingDisconnected drop on race
* fix: suppress reconnect hint on intentional disconnect to prevent spurious reconnect
* fix: remove SignalingModule.dispose() from CallBloc.close() — ownership belongs to MainShellState
* fix: snapshot buffer before live subscribe to prevent replay duplicates in events getter
* fix: store reconnect Timer in IsolateManager so it can be cancelled on close()
* fix: forward recommendedReconnectDelay from SignalingDisconnected to _scheduleReconnect in CallBloc
* fix: suppress _onDisconnect after _onError to prevent double reconnect scheduling
* fix: exclude SignalingProtocolEvent from session buffer to prevent unbounded growth
* fix: replace force-unwrap of session.coreUrl/token with null-safe logout in initState
* fix: remove performEndCall early return so pre-handshake declines are queued
* fix: close liveController on subscription cancel to prevent StreamController leak
* fix: use _networkNone state instead of stale results snapshot in connectivity timer closure
* docs: fix _scheduleReconnectIfNeeded → _scheduleReconnect in signaling architecture doc
* test: replace Future.delayed(Duration.zero) with pumpEventQueue() in signaling module tests
* fix: make _controller sync:true to eliminate async-dispatch event duplication
* docs: clarify disconnect() docstring — SignalingDisconnected is callback-driven, not synchronous
* fix: wrap _signalingModule.disconnect() in unawaited() in _disconnectInitiated
* fix: remove unused shouldReconnect variable from __onSignalingClientEventDisconnected
* test: add 8 tests to reach 100% SignalingModule coverage
- concurrent connect() dropped while factory in-flight (_connecting guard)
- intentional disconnect() emits SignalingDisconnected with null delay
- disconnect() passes goingAway code to the underlying client
- _onError suppresses subsequent _onDisconnect (_errorHandled flag)
- SignalingProtocolEvent excluded from replay buffer
- cancelled subscription receives no further events
- dispose() awaits disconnect ack before closing the stream
- _onHandshake/_onEvent are no-ops after dispose()
* test: add scenario-driven SignalingModule tests from CallBloc usage analysis
Covers scenarios observed in CallBloc's signaling subscription:
internet dropped mid-session:
- _onError after handshake → ConnectionFailed not Disconnected, signalingClient cleared
- Unexpected socket close (null code) → Disconnected with kSignalingClientReconnectDelay
- ConnectionFailed buffered so late subscribers reconstruct last-known failure
handshake not completed:
- Disconnect before handshake → no HandshakeReceived in buffer, Disconnected with delay
- Late subscriber after no-handshake disconnect → gets Connecting+Connected+Disconnected only
- Error before handshake → ConnectionFailed buffered, no HandshakeReceived
- Reconnect after no-handshake failure delivers fresh session events
late subscriber mid-session:
- Factory still pending → gets Connecting from buffer, Connected arrives live
- After full connect+handshake → all three lifecycle events replayed, no protocol events
disconnect() robustness:
- client.disconnect() throws → dispose() still completes without hanging
- Second disconnect() with no active client is a silent no-op
* fix: restore callkeep_signaling_status_converter.dart lost during rebase
* fix: remove unused fields in _ThrowingDisconnectClient test helper
* fix: use normalClosure (1000) instead of goingAway (1001) when client disconnects WebSocket
* test: update disconnect test to expect normalClosure (1000) instead of goingAway (1001)
* fix: address Copilot review comments in SignalingModule and IsolateManager
- Fix doc comment on events getter: protocol events are not replayed, only lifecycle/handshake events
- Guard connect() buffer clear behind _connecting check to avoid clearing on redundant calls
- Remove stale comment in _onDisconnect that contradicted the !_disposed guard
- Treat empty connectivity result as offline in _monitorConnectivity (results.isEmpty || any(none))
- Treat empty connectivity result as offline in performAnswerCall (isNotEmpty && !contains(none))
* fix: address post-review issues in SignalingModule and IsolateManager
- isolate_manager: fix connectivityNoneCounter reset — error now fires
exactly once at maxConnectivityNoneRepeats; subsequent none-events are
silently ignored until connectivity is restored and counter resets to 0
- main_shell: split SignalingModule construction into valid/invalid-creds
branches to remove ?? '' fallbacks and make intent explicit
- signaling_module: document sync:true reentrancy assumption, single-use
constraint on events getter, and _errorHandled ordering invariant
* revert: restore main_shell.dart SignalingModule construction with ?? '' fallback
The split-branch approach still created a module with empty strings in the
null-creds case — identical in behaviour to the original. Reverted to the
original form which is honest about the fallback until a proper nullable
refactor is done.
* fix: remove dead null-guard in MainShellState.initState for SignalingModule
The router guard (onMainShellRouteGuardNavigation) redirects to login
when state.status != authenticated, so coreUrl and token are always
non-null when MainShell is mounted. Replace ?? '' fallbacks and the
unreachable null-branch with direct ! unwraps.
* docs: sync signaling_architecture_target.md and call_architecture.md with current code
- signaling_architecture_target: add timer cancellation to IsolateManager
and CallBloc _scheduleReconnect snippets (Future.delayed → Timer with cancel)
- signaling_architecture_target: add missing SignalingDisconnecting case to
CallBloc subscription snippet
- call_architecture: update ownership — CallBloc owns SignalingModule, not
WebtritSignalingClient directly
* fix: replace non-ASCII characters with ASCII equivalents in Dart sources
Replace em dash (U+2014) with '-' and right arrow (U+2192) with '->'
in comments and test descriptions across signaling_module.dart,
isolate_manager.dart, signaling_module_test.dart, and call_bloc.dart.
* fix: incorrect styling of status bar on app start (#1006)
Fixed status bar rendering with incorrect styling on initial app launch when theme is light
* feat: show progress indicator while sharing logs (#1036)
* feat: show progress indicator while sharing logs
Co-Authored-By: Dmytro Serdun <serdun@webtrit.com>
* refactor: replace inline SizedBox+CircularProgressIndicator with SizedCircularProgressIndicator
---------
Co-authored-by: Dmytro Serdun <serdun@webtrit.com>
* fix: upgrade to video resets hold (#1038)
* fix: media settings parsing (#1039)
* fix: call drops after theme or lang change (#1041)
* fix: cannot make calls after blind transfer — skip hangup + reconnect safety net (WT-1214) (#1040)
* fix: trigger reconnect when starting outgoing call with no signaling (WT-1214)
After a blind transfer, the signaling WebSocket is closed with code 4610
and the disconnect is marked intentional by SignalingModule, so no
reconnect is scheduled. Subsequent outgoing call attempts enter
outgoingConnectingToSignaling and wait passively — neither signalingReady
nor signalingFailed ever fires, causing the call to fail on timeout.
Add _scheduleReconnect(Duration.zero) at the start of the waiting block
so that initiating an outgoing call always recovers the signaling
connection, regardless of whether the previous disconnect was intentional.
* fix: skip hangup after successful blind transfer to avoid 4610 disconnect (WT-1214)
When a blind transfer completes (NOTIFY SIP/2.0 200 OK +
subscription_state: terminated), the SIP dialog is already closed
server-side via REFER. Sending a hangup request on the freed dialog
causes the server to close the WebSocket with code 4610 ("call request
on wrong line error"), triggering an unintended signaling disconnect.
Check the call's transfer state: if it is Transfering(fromBlindTransfer:
true), skip the hangup request and clean up the peer connection locally
only. This removes the root cause of the 4610 disconnect that led to
signaling not being reconnected for subsequent outgoing calls.
* test: cover blind-transfer hangup skip and 4610 reconnect hint
Add tests for two fixes from WT-1214:
- Transfer — isBlindTransferCompleted detection (call_state_test.dart):
verifies the switch pattern that decides whether to skip the hangup
request after a blind transfer. Covers Transfering(fromBlindTransfer:
true/false), earlier transfer states, and null.
- SignalingModule — requestCallIdError (4610) reconnect hint
(signaling_module_test.dart): verifies that a non-intentional 4610
carries a non-null recommendedReconnectDelay (reconnect scheduled),
while an intentional disconnect() followed by a server 4610 emits
null (reconnect suppressed — the scenario that triggered WT-1214).
* refactor: rename isBlindTransferCompleted → isBlindTransferInTransferingState
Transfering state in the model means "server started to process the
transfer", not "transfer completed". Rename the local variable and update
the surrounding comments and test group names to match the actual Transfer
model semantics, avoiding misinterpretation of the hangup guard.
Addresses Copilot review comments on PR #1040.
* fix: call or transfer to myself handling (#1046)
* fix: hide video for held call (#1048)
* fix: hide video for held call
* fix: tap area
* fix: transfer to same recipient (#1049)
* refactor: extract SignalingModuleInterface; migrate CallBloc and IsolateManager; extract toLinesState (#1045)
* refactor: extract SignalingModuleInterface; decouple IsolateManager from SignalingModule
Add local SignalingModuleInterface abstract class to signaling_module.dart
with the contract needed by IsolateManager: events, isConnected, connect(),
disconnect(), execute(Request), dispose().
SignalingModule implements the interface, gaining isConnected and execute()
alongside the existing signalingClient getter (kept for backward compat).
IsolateManager field type changed from SignalingModule to SignalingModuleInterface.
All signalingClient null-checks replaced with isConnected; direct client.execute()
calls replaced with module.execute(). IsolateManager no longer depends on the
concrete class, making it ready for a plugin-backed implementation.
* fix: decouple NetworkCubit from WebtritSignalingService; fix push dedup race
NetworkCubit held a concrete WebtritSignalingService only to call
updateMode(). Replaced with a Future<void> Function(SignalingServiceMode)
callback so the cubit has no plugin dependency and is mock-testable.
Call site passes WebtritSignalingService().updateMode as a tear-off.
_onCallPushEventIncoming checked the incomingFromOffer guard before
awaiting contactNameResolver.resolveWithNumber(). The signaling path
could create an ActiveCall during that async gap, causing both paths
to emit separate entries for the same callId. Added a post-await guard
that checks for any existing ActiveCall with the same callId before emitting.
* fix: prevent premature call routing state emission before signaling handshake (#1044)
LinesState.blank() is emitted at app startup before the signaling handshake
arrives. combineLatest fires immediately with cached UserInfo + blank LinesState,
causing CallRoutingCubit to emit a non-null state with empty mainLines.
CallController then skips _waitForRoutingState() and fails with
"no idle lines available".
Fix: add LinesState.isBlank discriminator (guestLine == null is an unambiguous
pre-handshake marker — CallBloc always sets guestLine to non-null after any
handshake). Return null from _combineInfo when linesState.isBlank so the cubit
stays in its unready state and CallController waits correctly.
* fix: preserve LinesState.blank in onChange until signaling handshake arrives
The previous fix relied on guestLine == null as a pre-handshake discriminator,
but CallBloc.onChange always set guestLine = LineState.idle regardless of
linesCount, overwriting LinesState.blank() almost immediately after startup.
Root cause: onChange fired with linesCount = 0 on any early state change
(e.g. connecting status) and produced LinesState([], LineState.idle).
isBlank returned false, so _combineInfo emitted a non-null CallRoutingState
with empty mainLines, and hasIdleMainLine = false blocked the call.
Fix: when linesCount == 0 (handshake not yet received), onChange now stores
LinesState.blank() explicitly. Once the handshake sets linesCount > 0,
normal LinesState with non-null guestLine is produced as before.
* fix: remove unused models import from NetworkScreenPage
* fix: wire BackgroundSignalingBootstrapService into NetworkCubit callback
* refactor: migrate CallBloc to SignalingModuleInterface
Apply the same interface migration already done for IsolateManager:
- change _signalingModule field/param type from SignalingModule to SignalingModuleInterface
- replace signalingClient?.execute() with execute() from the interface
- replace signalingClient != null checks with isConnected
* fix: address Copilot review comments on signaling module interface
- Handle null execute() in _executePendingRequests: complete completer
with error and clean up instead of leaving request to time out silently
- Handle null execute() in _sendRequest: log warning and return early
instead of awaiting null when module disconnects after isConnected check
- Fix linesCount == 0 guard: use isHandshakeEstablished to distinguish
pre-handshake blank state from valid post-handshake 0-lines state
- Update LinesState.isBlank doc comment to remove inaccurate claim that
guestLine == null is an unambiguous pre-handshake marker
* refactor: extract toLinesState() from CallBloc.onChange into CallState
Pure deterministic logic moved to CallState.toLinesState() so it can be
tested without standing up CallBloc. onChange becomes a single line.
Tests cover: pre-handshake blank, 0-lines post-handshake with guest line,
main line idle/inUse combinations, guest line inUse alongside main calls.
* fix: rename _kRegistered to kRegistered (lint: no_leading_underscores_for_local)
* refactor: rename SignalingModuleInterface → SignalingModule, impl → SignalingModuleIsolateImpl
Drop the `Interface` postfix from the abstract contract and suffix the
concrete WebSocket implementation with `IsolateImpl` to clarify its role.
Add doc comments to every member of the SignalingModule interface.
Files touched:
- lib/features/call/services/signaling_module.dart
- lib/features/call/services/isolate_manager.dart
- lib/features/call/bloc/call_bloc.dart
- lib/app/router/main_shell.dart
* fix: update signaling_module_test to use SignalingModuleIsolateImpl concrete type
* fix: use SignalingModule.execute instead of signalingClient getter (#1050)
signalingClient is not part of the SignalingModule interface — only
SignalingModuleIsolateImpl exposes it. Replace with execute() which is
defined on the interface and returns null when not connected.
* fix: skip decline when push-registered call receives signaling line (#1051)
* fix: skip decline when push-registered call receives signaling line (WT-1091)
When an incoming call arrives via FCM push, it is registered with
line _kUndefinedLine (-1) as a placeholder. When the signaling
WebSocket subsequently delivers the same call with a real line (e.g. 0),
the guard that detects 'call to myself' incorrectly fires because
-1 != 0, causing a DECLINE to be sent on the wrong line and the call
to be dropped with server error 4610.
Fix: exclude _kUndefinedLine from the line-mismatch check so that
push-placeholder calls are updated with the real line rather than
declined.
* test: add push → signaling line handoff tests for WT-1091 fix
* feat: media settings ptime warning (#1053)
* fix: call glare (#1052)
* fix: call glare
* fix: commented code
* feat: call interaction guard if any updating (#1056)
* fix: stop ringback sound on forced call termination (#1055)
__onResetStateEventCompleteCall is the path taken when signaling disconnects
unexpectedly (network loss). Unlike other termination paths it was not calling
_stopRingbackSound(), leaving the ringback tone playing after the call UI disappeared.
* fix: sanitize keypad input before initiating a call (WT-1026) (#1057)
* fix: sanitize keypad input before initiating a call (WT-1026)
Apply PhoneParser.normalize() in _popNumber() so that Unicode lookalike
digits and stray formatting characters are stripped before the number
reaches CallController/CallBloc.
* fix: normalize keypad input at entry point via TextInputFormatter (WT-1026)
Replace the call-time normalize approach with a TextInputFormatter so
pasted or typed Unicode lookalike characters are sanitized immediately
in the TextField, keeping the displayed text and the dialed number
consistent.
* refactor: move PhoneNormalizingFormatter to keypad/utils (WT-1026)
Extract formatter into its own file under features/keypad/utils/ with a
barrel export, following the existing package structure convention.
* fix: strip non-dialable characters from keypad input (WT-1026)
After Unicode normalization, remove any character outside [0-9*#+] so
pasting arbitrary text (e.g. *"{) leaves only valid phone number chars
in the field.
* fix: address Copilot review comments on PhoneNormalizingFormatter (WT-1026)
- Cache _nonDialableChars as static final to avoid per-keystroke allocation
- Translate both baseOffset and extentOffset (preserving affinity/isDirectional)
instead of always returning a collapsed selection
- Extract sanitize() static helper and reuse it in _popNumber() as a
belt-and-suspenders guard for programmatic controller updates
* test: add unit tests for PhoneNormalizingFormatter (WT-1026)
Covers sanitize() and formatEditUpdate() — including Unicode normalization,
non-dialable character stripping, cursor translation, and selection range
preservation.
* refactor: extract renegotiation support (#1058)
* fix: prevent null crash in InkWell on hardware keyboard event during navigation (WT-1012) (#1060)
* fix: prevent null crash in InkWell on hardware keyboard event during navigation (WT-1012)
Remove debug focusColor/hoverColor from CdrTile InkWell to stop it
subscribing to _HighlightModeManager, and add FocusScope.unfocus()
before all fullscreenDialog navigations to cover ListTile-based widgets.
* fix: use FocusManager and mounted guard in openNotificationsScreen (WT-1012)
Replace FocusScope.of(context).unfocus() with
FocusManager.instance.primaryFocus?.unfocus() to avoid depending on
a potentially unmounted context, and add a mounted guard before
navigating, since the callback can fire from a stream after disposal.
* fix: voicemail audio cache collision on Android (WT-1016) (#1061)
* fix: resolve voicemail audio cache collision on Android (WT-1016)
All voicemails shared the same cache file path because the last URL
path segment is always 'attachment'. Concurrent LockCachingAudioSource
instances raced to rename the same .part file, causing a crash.
- Add cacheKey param to AudioView; VoicemailTile passes voicemail.id
- _getCacheFile falls back to joined URI segments when cacheKey is absent
- Ensure media_cache directory exists before screen initialises
* refactor: ensure media_cache dir exists in AppPath.init
Move Directory.create to AppPath.init so the path is ready to use
by the time any feature accesses mediaCacheBasePath, consistent with
how getApplicationDocumentsDirectory / getTemporaryDirectory work.
* refactor: move media_cache dir creation into AudioView._getCacheFile
AppPath is platform-agnostic and must not import dart:io (web support).
Directory is now created in _getCacheFile, co-located with the code
that writes the cache file and already guarded by the Platform.isIOS check.
* fix: address Copilot review comments on AudioView cache handling
- Move Directory.create to _initialize() as async IO, avoiding
synchronous filesystem call in _getCacheFile on every retry
- Guard empty rawKey with early return null so just_audio handles
caching when URI has no path segments
- Sanitize cacheKey against path separators to prevent path traversal
from server-provided voicemail IDs
* refactor: renegotiation handlers per call (#1065)
* fix: fall back to audio-only when camera permission is denied on incoming video call (WT-1049) (#1062)
* fix: fall back to audio-only when camera permission is denied on incoming video call (WT-1049)
When answering an incoming video call without camera permission, the app
no longer drops the call. Instead, it retries media acquisition with
video disabled and answers audio-only. If the microphone itself is
unavailable the error still propagates as before.
* refactor: check camera permission before requesting media instead of relying on error fallback (WT-1049)
Added `isVideoAvailable()` to the `UserMediaBuilder` contract so the BLoC
can explicitly query camera permission status before deciding whether to
request video. Replaces the previous error-based fallback approach with a
proactive permission check.
* refactor: inject camera permission check as callback into UserMediaBuilder (WT-1049)
Replaced the direct `permission_handler` dependency in `user_media_builder.dart`
with an optional `isCameraPermissionGranted` callback. `DefaultUserMediaBuilder`
remains free of plugin dependencies; the check is wired at construction time in
`main_shell.dart` via `AppPermissions.isPermissionGranted(Permission.camera)`.
* refactor: move permission-aware video resolution into UserMediaBuilder.build() (WT-1049)
`DefaultUserMediaBuilder.build()` now resolves the effective video flag
internally via `_isCameraAvailable()` before calling `getUserMedia`, so
callers pass their intent (`video: offer.hasVideo`) and the builder handles
the permission check transparently.
`call_bloc.dart` derives the resulting video state from the actual stream
tracks (`localStream.getVideoTracks().isNotEmpty`) instead of pre-computing
it, removing all permission logic from the BLoC layer.
* fix: scope audio fallback to incoming answer only via allowAudioFallback flag (WT-1049)
The unconditional permission-check fallback inside `build()` was silently
downgrading video for all callers, including the camera-enable action which
relies on `UserMediaError` to show a notification and skip the `video: true`
state update.
Introduced `allowAudioFallback: bool = false` on `build()`. Default behaviour
(throw on failure) is preserved for existing callers; `__onCallPerformEventAnswered`
opts in with `allowAudioFallback: true` to get the permission-aware fallback.
* test: add unit tests for DefaultUserMediaBuilder permission-aware fallback (WT-1049)
* fix: retry getUserMedia audio-only when video acquisition fails with allowAudioFallback (WT-1049)
* fix: stop voicemail polling after server responds with unsupported (WT-1107) (#1067)
* fix: stop voicemail polling after server returns unsupported (WT-1107)
Add isActive getter to Refreshable (default true). VoicemailRepositoryImpl
returns _featureSupported so polling and connectivity services skip it once
the server responds with voicemail_not_configured or endpoint_not_supported.
PollingService unregisters the listener on the next tick; ConnectivityLifecycleService
filters inactive refreshables before each reconnect cycle.
* fix: add isActive override to all Refreshable implementors (WT-1107)
* fix: address Copilot review comments on isActive polling (WT-1107)
- Remove duplicate @override on isActive in all repository implementors
- Check isActive in leading refresh path (_triggerOnceWithKnownReachability)
- Add isActive field to MockRefreshableRepository for controllable test state
- Add PollingService tests: inactive listener unregistered on tick and skipped on resume
- Add ConnectivityLifecycleService test: inactive refreshable skipped on reconnect
* chore: misc cleanup (#1068)
* chore: regenerate login_cubit.freezed.dart and update pubspec.lock (#1069)
* fix: restore accepted call from signaling handshake on app restart (WT-1167) (#1070)
* fix: restore accepted call from signaling handshake on app restart (WT-1167)
- Add HandshakeAction sealed hierarchy and stateless HandshakeProcessor
- Add _RestoreAcceptedIncomingCall event and handler in CallBloc
- Add incomingRestoringMedia processing status
- Dispatch renegotiationNeeded explicitly after peer connection setup to
bypass null signalingState guard on brand-new RTCPeerConnection
* refactor: move HandshakeProcessor and HandshakeAction to utils/, merge into one file
* refactor: export HandshakeProcessor via utils barrel
* revert: restore device_auto_rotate pubspec.lock to develop state
* refactor: remove task references from comments
* style: replace non-ASCII symbols in comments with ASCII equivalents
* style: clean up Copilot comments in call restoration code
* fix: log warning on duplicate call restoration guard
* fix: restore outgoing calls from handshake by checking AcceptedEvent as latest log
* fix: skip Callkeep incoming registration for outgoing call restoration
- Guard reportNewIncomingCall/answerCall behind direction == incoming check in
_onRestoreAcceptedCall to avoid registering a restored outgoing call as incoming
- Remove connection == null guard from HandshakeProcessor restoration condition:
if stateDisconnected already triggers early exit, stateActive connections should
not block restoration when no BLoC state exists
- Update handshake_processor_test.dart: fix stale guest-line test to check
AcceptedEvent.line (not IncomingCallEvent.line); add outgoing restoration tests;
update import to utils/handshake_processor.dart
* fix: register restored outgoing call in Telecom and guard performStartCall (WT-1167)
- _onRestoreAcceptedCall: added outgoing Callkeep branch (startCall +
warning on error), symmetric to the existing incoming branch
- __onCallPerformEventStarted: added canPerformStart switch guard
(mirrors the existing canPerformAnswer in __onCallPerformEventAnswered)
so that the Telecom-mandatory performStartCall callback for a restored
call skips the normal outgoing flow and only advances Telecom to ACTIVE
via reportConnectedOutgoingCall
* fix: detect restoration after re-INVITE by searching AcceptedEvent in full call_logs (WT-1167)
HandshakeProcessor previously checked only the newest call_logs entry for
AcceptedEvent. After a transfer or SDP re-INVITE the server inserts
UpdatedEvent/UpdatingCallEvent as the newest entry, causing restoration to
be silently skipped.
Changes:
- Restoration: search the full call_logs list for the first AcceptedEvent
instead of checking only firstOrNull; guard against server-terminated
calls via HangupEvent/MissedCallEvent as latest entry
- stateDisconnected: replace AcceptedEvent|ProceedingEvent whitelist with
a blacklist (HangupEvent, MissedCallEvent) so UpdatedEvent and any other
non-terminal event correctly produces HangupSignalingAction
- Tests: replace the obsolete "specific order required" test with two new
cases: re-INVITE restoration and HangupEvent termination guard
* docs: clarify performStartCall interception for restored outgoing call (WT-1167)
* style: replace non-ASCII characters with ASCII equivalents in comments (WT-1167)
* docs: clarify incomingOffer role and media re-establishment path during restoration (WT-1167)
* style: restore UTF-8 symbols in restoration comment (WT-1167)
* style: shorten redundant startCall comment in restoration path (WT-1167)
Revert dart_define.json to main — local dev config values should not be tracked in this PR.
* docs: fix inaccurate docstrings in HandshakeAction subtypes and HandshakeProcessor (WT-1167)
- HangupSignalingAction: clarify trigger condition (any non-terminal event, not only AcceptedEvent/ProceedingEvent)
- RestoreCallAction: clarify that AcceptedEvent is searched in the full log, not just the latest entry
- HandshakeProcessor: align Loop B bullet list with actual implementation
- fix typo 'due stale' -> 'due to stale' in three log messages
* fix: use outgoingRestoringMedia status for restored outgoing calls (WT-1167)
incomingRestoringMedia was used for both incoming and outgoing restored calls.
Introduce outgoingRestoringMedia in the enum and assign it based on direction
in _onRestoreAcceptedCall. Both statuses fall outside canPerformAnswer and
canPerformStart whitelists, preserving the existing bypass logic.
* fix: abort restoration when answerCall fails in _onRestoreAcceptedCall (WT-1167)
* fix: log proceeding status in canPerformStart else branch (WT-1167)
* fix: remove noisy transport DIAG log from RtpTrafficMonitor poll loop (WT-1167)
* fix: resolve unsafe ancestor lookups on deactivated widgets (WT-578) (#1059)
* fix: resolve unsafe ancestor lookups on deactivated widgets (WT-578)
Fixes all locations where InheritedWidget lookups (MediaQuery, Localizations,
Provider) were performed on deactivated BuildContext instances.
- call_to_actions_shell: replace late final lazy initializer (root cause) with
nullable field initialized in didChangeDependencies; guard dispose with null check
- main_shell: cache AppBloc, NotificationsBloc and l10n string in initState
instead of capturing context in RouterLogoutSessionGuard closures
- call_screen: add context.mounted guard at start of BlocConsumer listener
- draggable_thumbnail: add mounted check before setState in addPostFrameCallback
- call_actions: cache MediaQuery and Theme in didChangeDependencies; computeDimensions
reads cached values only
* fix: move l10n lookup from initState to didChangeDependencies in main_shell
context.l10n calls dependOnInheritedWidgetOfExactType which is not allowed
inside initState(). Cache the localized string in didChangeDependencies()
instead and reference the field from the RouterLogoutSessionGuard closure.
* fix: recreate DemoActionOverlay on dependency change when not inserted
When the overlay is not currently visible, recreate it with updated
MediaQuery values so the initial button offset reflects the current
screen size and safe area insets (e.g. after device rotation).
If the overlay is inserted, DraggableThumbnail handles layout updates
via its own didChangeDependencies, so no recreation is needed.
* fix: prevent OOM by exposing log buffer cap and share export (WT-1107) (#1064)
* feat: show record count hint with share prompt in log console (WT-1107)
* chore: regenerate l10n after adding recordsCountHint key (WT-1107)
* fix: use ICU plural forms for recordsCountHint in all locales (WT-1107)
* refactor: move records count hint to AppBar info dialog (WT-1107)
* fix: replace # with {count} in ICU plural forms for correct interpolation (WT-1107)
* refactor: merge info and clear into overflow menu in log console (WT-1107)
* refactor: centralize signaling reconnect logic in SignalingReconnectController (WT-1221) (#1066)
* refactor: centralize signaling reconnect logic in SignalingReconnectController (WT-1221)
Introduces SignalingReconnectController — a single component that owns all
reconnect timer scheduling, failure counting, and notification decisions.
Fixes a spurious 'Connecting to the core failed' toast on screen unlock caused
by a transient DNS failure that auto-resolved within one reconnect cycle.
Key changes:
- Add SignalingConnectionLost event to distinguish a runtime error on an
established session (notify immediately) from an initial connect failure
(notify only after 2+ consecutive attempts).
- Remove isRepeated flag and _lastConnectErrorString from SignalingModule —
deduplication concerns no longer belong at the transport layer.
- Extract _scheduleReconnect / _reconnectInitiated / _disconnectInitiated out
of CallBloc into SignalingReconnectController.
- Replace IsolateManager inline reconnect timer with the same controller
(reconnectEnabled: false for PushNotificationIsolateManager).
- CallBloc and IsolateManager signal listeners now only drive state
transitions; all reconnect and notification logic is in the controller.
* refactor: use barrel import for signaling services in CallBloc
* fix: address Copilot review comments on SignalingReconnectController
- notifyAppPaused: preserve _appActive when hasActiveCalls is true so
reconnects are not suppressed during a backgrounded active call
- threshold notification: use == instead of >= to notify exactly once
per outage rather than on every attempt after the threshold
- add _disposed guard in timer callback to prevent connect() calls
after dispose()
- assert notifyAfterConsecutiveFailures >= 1 in constructor
- add signaling_reconnect_controller_test.dart covering threshold,
hasActiveCalls, app/network guards, force reconnect, dispose safety,
and SignalingConnectionLost immediate notification (23 tests)
* refactor: log signaling connection failure in IsolateManager instead of null callback
* refactor: introduce SignalingReconnectable interface for SignalingReconnectController
* feat: add onConnectionPresenceChanged callback to SignalingReconnectController
Tracks persistent connection availability transitions (available ↔
unavailable) for driving a UI presence indicator such as a banner.
Emits false when: consecutive failures reach threshold,
SignalingConnectionLost occurs, or notifyNetworkUnavailable is called.
Emits true when SignalingConnected follows an unavailable state.
Deduplicates consecutive identical values via _lastPresence guard.
* refactor: log signaling presence changes in CallBloc and IsolateManager
* fix: address remaining Copilot review comments
- Add _hasActiveCalls flag and notifyHasActiveCalls() to allow reconnects
during background active calls (fixes app-active guard blocking recovery)
- Wire notifyHasActiveCalls() in CallBloc.onChange when app is inactive
- Remove isRepeated from SignalingConnectionFailed and SignalingConnectionLost:
identical() comparison doesn't work for typical new exception objects,
and threshold logic in SignalingReconnectController is the correct dedup
- Remove _lastConnectionError tracking from SignalingModuleIsolateImpl
- Use .ignore() on _module.disconnect() Future in _disconnect() to make
fire-and-forget intent explicit
* fix: replace non-ASCII characters with ASCII equivalents in SignalingReconnectController
* fix: address latest Copilot review comments in SignalingReconnectController
* fix: replace non-ASCII arrow characters with ASCII in comments
* chore: restore device_auto_rotate example pubspec.lock to develop version
* feat: presence, blf, callpull remake (#1008)
* feat: signaling networking improvemets (#1072)
* feat: replace callkeep signaling with webtrit_signaling_service plugin (#1047)
* feat: add webtrit_signaling_service plugin packages
* feat: add IsolateContext and update bootstrap for signaling service
* feat: replace SignalingModule with SignalingServiceModuleAdapter
* feat: update network screen for signaling service mode
* docs: update call architecture and signaling docs
* fix: update SignalingReconnectController and tests for new SignalingModule types
* refactor: extract HubConnectionManager from WebtritSignalingServiceAndroid (#1073)
* refactor: extract HubConnectionManager from WebtritSignalingServiceAndroid
Moves hub-init polling loop, generation-based cancellation, and
SignalingHubModule lifecycle into a dedicated HubConnectionManager class.
plugin.dart is reduced from 386 to ~230 lines with a single responsibility:
Android service lifecycle coordination.
* refactor: add TODO for push-based hub discovery to replace polling
* test: add integration tests for HubConnectionManager
* fix: prevent whenComplete from restarting polling loop during tearDown
* refactor: replace raw Map hub commands with SignalingHubCommand sealed class (#1074)
* refactor: replace raw Map hub commands with SignalingHubCommand sealed class
* refactor: fix lint warnings in hub_connection_manager_integration_test
* refactor: remove section divider comments from changed files
* refactor: replace dynamic with concrete types in hub command handlers
* fix: encode hub commands to isolate-safe wire format before sending
* fix: guard SignalingHubCommand.decode against malformed wire payloads
* docs: add comments to hub command wire tag constants
* refactor: split entry_point.dart into bootstrap and sync handler (#1075)
* refactor: split entry_point.dart into bootstrap and sync handler
* refactor: make _pendingSync private, move handler to signaling_sync_handler
* docs: document two-level buffering in SignalingHubModule
* refactor: replace int discriminators with string tags in hub codec (#1078)
* refactor: replace int discriminators with string tags in hub codec
* fix: address Copilot review comments for hub codec
* feat: port request queue and retry logic to SignalingServiceModuleAdapter
Port the queued-request and timeout-retry logic from develop's
SignalingModuleIsolateImpl into SignalingServiceModuleAdapter so that
requests sent during a connectivity gap are not silently dropped.
- When not connected, requests are queued and flushed on the next
SignalingConnected event instead of returning null
- Requests in the queue fail with NotConnectedException after 30 seconds
- Execute retries up to 3 times on WebtritSignalingTransactionTimeoutException
- dispose() fails all pending queued requests immediately
* feat: add request queue and retry to SignalingModuleImpl
Port the queued-request and timeout-retry logic from develop's
SignalingModuleIsolateImpl into SignalingModuleImpl (platform interface)
so that requests sent during a connectivity gap are queued rather than
dropped.
- When not connected, execute() queues the request and flushes it on
the next successful connection instead of returning null
- Requests in the queue fail with NotConnectedException after 30 seconds
- Execute retries up to 3 times on WebtritSignalingTransactionTimeoutException
- dispose() fails all pending queued requests immediately
- NotConnectedException now lives in the platform interface and is
exported from webtrit_signaling_service; removed duplicate definition
from SignalingServiceModuleAdapter
* fix: update toLinesState tests to use LineState factory constructors
LineState.idle and LineState.inUse are factory constructors, not static
const values. Updating assertions to call them with parentheses and
pass required callId arguments.
* fix: repair pre-existing test failures inherited from develop
- feature_access_factories: stub systemInfo.core with CoreInfo after
WebtritSystemInfo.core became a required non-nullable field
- external_contacts_sync_bloc_test: initialize bloc in setUp so
tearDown never hits LateInitializationError regardless of test order
* fix: update StateHandshake test fixtures after field rename
StateHandshake fields were renamed in develop:
userActiveCalls → presenceInfos
contactsPresenceInfo → dialogInfos
Update all affected test fixtures across signaling_service packages
to match the new API.
* fix: resolve remaining analyze issues inherited from develop
- signaling_hub_codec_test: remove unnecessary ! operators on
non-nullable return values
- contact_tile: replace deprecated withOpacity() with withValues()
* fix: replace launchSignaling with run() and unconditional releaseCall (#1080)
* feat: replace launchSignaling with run() and unconditional releaseCall
- Replace launchSignaling with run(metadata) that returns a Future
completing after all isolate work is done (notifications, logs,
releaseCall); caller awaits the future with a 20s Dart-side timeout
- Remove CallkeepPushNotificationSyncStatus status param from
onPushNotificationSyncCallback; isolate now owns its full lifecycle
- Replace endCall/endCalls with releaseCall(callId) in all terminal
paths; native service stops unconditionally regardless of Telecom
connection state
- Add Completer-based work tracking so run() resolves only after
releaseCall completes; _disposeContext in finally handles cleanup
* feat: clean up onPushNotificationSyncCallback — extract timeout constant, simplify _getOrInit return type, improve docs
* fix: call releaseCall in close() to release native service on timeout
* fix: remove unreachable default branch in signaling event switch
* docs: actualize call and signaling architecture docs
- Add missing CallProcessingStatus values (incomingSubmittedAnswer,
incomingRestoringMedia, outgoingCreatedFromRefer, outgoingRestoringMedia,
outgoingRinging) to call_architecture.md
- Add _GlobalEvent row, currentAppLifecycleState field, call restoration
and blind transfer key patterns to call_architecture.md
- Replace inline _scheduleReconnect guard chain in signaling doc with
SignalingReconnectController section (constructor, notify* API, guard
chain, failure notification state machine)
- Update CallBloc consumer subscription to actual simplified switch
- Update IsolateManager section: PushNotificationIsolateManager run()/close()
API, no-reconnect design; SignalingForegroundIsolateManager own timer
- Add SignalingReconnectController to dependency diagram and decisions table
- Add deleted fields (_scheduleReconnect, _signalingClientReconnectTimer)
to what-was-deleted table
* refactor: extract SignalingRequestQueue to eliminate queue duplication
SignalingModuleImpl and SignalingServiceModuleAdapter shared identical
request-queue logic (_QueuedRequest, _enqueueRequest, _flushQueuedRequests,
_executeWithRetry, _onQueuedRequestTimeout, _failAllQueuedRequests) and
NotConnectedException.
Extract all of it into SignalingRequestQueue in the platform-interface
package alongside SignalingEventBuffer. Both consumers now use
_requestQueue.enqueue / executeNow / flush / failAll.
- Add SignalingRequestQueue with enqueue, executeNow, flush, failAll
- Move NotConnectedException into signaling_request_queue.dart
- Remove duplicate code from SignalingModuleImpl and SignalingServiceModuleAdapter
- Export SignalingRequestQueue from platform-interface barrel and signaling_service.dart
* feat: merge SignalingServiceModuleAdapter into WebtritSignalingService
WebtritSignalingService now directly implements SignalingModule, removing
the need for the separate adapter layer. Config and mode are constructor
parameters; static setup methods (setModuleFactory, setIncomingCallHandler,
attach, updateMode) delegate to SignalingServicePlatform.instance.
- Delete SignalingServiceModuleAdapter
- WebtritSignalingService implements SignalingModule with connect/disconnect/execute/dispose
- connect() is idempotent (_startPending guard prevents duplicate hub init on Android)
- disconnect() remains a no-op so signaling stays alive in background
- execute() queues requests when not connected; flush on SignalingConnected
- dispose() fails queued requests with NotConnectedException
- Update all call sites: main_shell.dart, bootstrap.dart, network_screen_page.dart
- Rewrite unit and integration tests for the new contract
* feat: remove example app from webtrit_signaling_service plugin
* feat: network stability improvements for media streams / negotiation (#1079)
* fix: janus video stub sanitizing (#1081)
* fix: initialize logging in signaling background isolate (#1082)
* fix: initialize logging in signaling background isolate
Dart isolates do not share memory, so Logger.root in the background
isolate (signalingServiceCallbackDispatcher) had no listeners attached.
All log records from WebtritSignalingClient, SignalingForegroundIsolateManager,
SignalingHub, and related classes were silently dropped.
Attaches a dart:developer log listener to Logger.root at isolate startup
so all logging is visible in logcat.
* fix: use PrintAppender for logging in signaling background isolate
dart:developer log() is not visible in logcat — it only posts to the
Dart VM service protocol. Replace with PrintAppender/ColorFormatter
(same setup as AppLogger in the main isolate) so all WebtritSignalingClient
and related logs appear in logcat output.
* fix: reconnect signaling when background isolate is already started but disconnected (#1083)
* fix: reconnect signaling when background isolate is already started but disconnected
When the WebSocket closes with code 1002 (protocol error), the background
isolate emits SignalingDisconnected with recommendedReconnectDelay=null and
does not auto-reconnect. The _started flag in SignalingForegroundIsolateManager
remained true, so subsequent handleStatus(enabled: true) calls from the main
isolate triggered _start() which exited early on the _started guard — leaving
the WebSocket permanently disconnected even after WiFi was restored.
Fix: when _start() is called on an already-initialized manager but the module
is no longer connected, call connect() on the existing module instead of
returning silently.
* test: add SignalingForegroundIsolateManager tests covering reconnect after 1002 disconnect
Adds injectable moduleFactory and hubFactory parameters to
SignalingForegroundIsolateManager to bypass PluginUtilities handle
resolution and IsolateNameServer in unit tests.
Extracts handle resolution into _resolveModuleFactory() to keep _start()
readable.
Tests cover: initial start, idempotency while connected, reconnect after
code-1002 (the bug scenario), auto-reconnect via delay hint, and
stop/restart lifecycle.
* refactor: introduce SignalingHubFactory typedef for readability
* fix: cancel pending reconnect timer before manual reconnect in _start()
* fix: stop signaling service on logout to prevent stale token reconnects (#1084)
* fix: stop signaling service on logout to prevent stale token reconnects
On Android the foreground signaling service kept reconnecting with an
expired token after logout because dispose() only tore down the Dart-side
hub, leaving the Kotlin service running.
- Add stopService() to SignalingServicePlatform (no-op default for iOS)
- WebtritSignalingServiceAndroid.stopService() already called _hostApi.stopService();
now properly @override the platform interface method
- Expose WebtritSignalingService.stopService() as a static helper
- MainShell stores _appBloc in initState (safe for dispose()) and calls
WebtritSignalingService.stopService() after dispose() when the status
is teardown (explicit logout), leaving the service untouched on OS kill
so persistent mode continues working after swipe-from-recents
* feat: add test coverage for stopService in signaling service layer
- WebtritSignalingService: add stopServiceCount to _FakePlatform and test
that static stopService() delegates to the platform instance
- WebtritSignalingServiceAndroid: inject BinaryMessenger via forTesting()
constructor so stopService() can be verified without a live Android
service; add two tests: stopService() calls the host channel, and
dispose() does not call the stop channel
* fix: add meta dependency and fix local variable naming in plugin test
* fix: add TODO for SignalingCleanupCoordinator testability refactor
* fix: address copilot review comments
- _tearDownSignaling: wrap in try/finally so stopService() runs even
if signalingModule.dispose() throws; log both failures via Logger
- plugin.dart: use flutter/foundation.dart for @visibleForTesting
instead of a separate meta dependency (consistent with iOS plugin)
- pubspec.yaml: remove meta direct dependency (covered by Flutter SDK)
* fix: capture teardown flag synchronously before first await
The logout check was reading _appBloc.state.status after
await _signalingModule.dispose(). During that await the cleanup
resolver had time to complete and transition the status from
teardown to unauthenticated, so stopService() was never called.
Capturing the flag synchronously at the top of _tearDownSignaling()
(before any await) locks in the correct value at the moment dispose()
is triggered from MainShell.
* fix: stop signaling service from TeardownScreen on logout
Move stopService() call to TeardownScreen.initState() so the native
Android foreground service is stopped synchronously before AppCleanupRequested
is dispatched. This avoids the race where the Kotlin service kept
reconnecting with an expired token after logout.
Remove the isLogout status check from _tearDownSignaling() in MainShell —
it was unreliable because AppBloc may transition to unauthenticated before
the async dispose path executes. TeardownScreen is exclusively rendered
during explicit logout, so no status check is needed there.
* fix: gracefully disconnect WebSocket before stopping signaling service
Add gracefulStop() to SignalingForegroundService that sends
onSynchronize(enabled=false) to the background isolate before stopping
the service. The isolate runs _stop() → signalingModule.dispose() →
client.disconnect(), producing a clean "disconnected" log instead of
being killed mid-connection.
A 3-second timeout calls stopService() anyway if the isolate does not
ACK, so logout is not blocked if the isolate is unresponsive.
Also add a log line to WebtritSignalingServiceAndroid.stopService()
so the call is visible in Flutter logs.
* fix: gracefully disconnect WebSocket on app swipe in pushBound mode
Replace stopSelf() with gracefulStop { stopSelf() } in onTaskRemoved
so the background isolate gets a chance to run _stop() and close the
WebSocket cleanly before the service is destroyed.
Without this, the server only detects the disconnect via TCP timeout
(30-60s), which can delay FCM push delivery for the next incoming call.
* test: add widget tests for TeardownScreen stopService behavior
Verify that:
- stopService() is called synchronously during initState
- AppCleanupRequested is dispatched on the first frame
- stopService() is called before AppCleanupRequested (ordering)
* fix: handle stopService() errors in TeardownScreen
* fix(signaling): eliminate 1s call-setup delay in notifyForceReconnect (WT-1239) (#1086)
* fix(signaling): use Duration.zero in notifyForceReconnect to eliminate 1s call-setup delay (WT-1239)
notifyForceReconnect was using kSignalingClientFastReconnectDelay (1 s) before
scheduling the WebSocket reconnect. This added ~1 s of dead time between the user
tapping "call" (or answering from a push notification) and the SDP offer reaching
the server — visible as a "Connecting to signaling" spinner during that window.
The 1 s delay was originally a copy of the value used by notifyAppResumed, which is
a proactive reconnect (no urgency). Force-reconnect callers (outgoing call start,
push-answer) need the socket ready as fast as possible.
The spurious "connection failed" toast that was the original concern (WT-1221) is
suppressed by the consecutive-failure threshold (notifyAfterConsecutiveFailures),
not by the reconnect delay, so reducing the delay here is safe.
Regression tests added that verify:
- notifyForceReconnect fires on Duration.zero, not kSignalingClientFastReconnectDelay
- screen-unlock → outgoing call sequence gets an immediate reconnect
- WT-1221 toast guard remains intact after the timing change
* refactor(signaling): remove task reference from notifyForceReconnect comment
* test(signaling): address review comments — fix event-loop wording, remove stale TDD notes and task refs
* fix: audio routing if only one device (e.g avd simulator) (WT-1097) (#1087)
* fix(android): minimize app instead of destroying Activity when back pressed at root (#1090)
* fix(android): move app to background on back press instead of destroying Activity
When pressing Back on the root screen, Android was calling finish() on
MainActivity, destroying the Flutter engine and tearing down any active
WebRTC call (DTLS alert -> ice_hangup -> BYE). Override onBackPressed to
call moveTaskToBack(true) so the app minimizes without destroying the engine.
* fix(android): minimize app instead of destroying Activity when Flutter stack is empty
Override popSystemNavigator() which is called only after Flutter exhausts
its own navigation stack. Replaces the default finish() with moveTaskToBack(true)
so the Flutter engine stays alive with any active WebRTC call.
Previous attempt overrode onBackPressed() which bypassed Flutter router entirely.
* fix(android): handle moveTaskToBack failure and document intentional minimize-always behavior
* fix(signaling): bidirectional hub protocol + persistent-mode reconnect (#1091)
* fix(signaling): make hub protocol bidirectional — remove isolate auto-reconnect
The background foreground-service isolate was managing its own reconnect
timer independently of SignalingReconnectController in the main isolate.
This caused a race: the background could reconnect while the app was locked,
producing a SignalingConnected event that set _wasConnected=true, so the
next disconnect triggered an error toast visible on unlock (green + error).
Root fix: extend the hub protocol with connect/disconnect commands so the
main isolate (SignalingReconnectController via SignalingHubModule) is the
single decision-maker for all reconnects.
Changes:
- signaling_hub_command: add SignalingHubConnectCommand / SignalingHubDisconnectCommand
- signaling_hub_client: add sendConnect() / sendDisconnect() fire-and-forget methods
- signaling_hub: handle connect/disconnect commands, forward to SignalingModule
- signaling_hub_module: connect()/disconnect() forward commands to hub client
instead of being no-ops
- signaling_foreground_isolate_manager: remove _reconnectTimer and
_scheduleReconnect(); isolate no longer auto-reconnects on disconnect
or connection failure — it only reconnects when main isolate calls
handleStatus(enabled:true) and the module is not connected
- tests: update hub_module_test and foreground_isolate_manager_test to
cover new behavior; all 158 tests pass
* fix(signaling): handle persistent-service mode reconnect when app is closed
In persistent signaling mode the foreground service outlives the app.
When the app is closed there are no hub subscribers, so
SignalingReconnectController is not running and cannot drive reconnects.
Fix: restore the reconnect timer in SignalingForegroundIsolateManager but
gate it on SignalingHub.hasSubscribers. When subscribers are present (app
open), reconnect is delegated to SignalingReconnectController as before.
When no subscribers are present (app closed, persistent mode), the
background isolate schedules a local reconnect using the delay hint from
the disconnect/failure event. Null delay (e.g. code 1002) is treated as
"do not reconnect" in both modes.
When handleStatus(enabled: true) is called while a timer is pending, the
timer is cancelled so the incoming caller takes responsibility.
- SignalingHub: add hasSubscribers getter
- SignalingForegroundIsolateManager: restore _reconnectTimer and
_scheduleReconnect(); schedule only when !hub.hasSubscribers
- Tests: 4 new persistent-mode tests covering auto-reconnect on disconnect,
auto-reconnect on connection failed, no reconnect on null delay, and
timer cancellation on stop(); all 68 isolate+hub tests pass
* fix(signaling): address Copilot review comments
- signaling_hub: guard connect/disconnect commands with subscriber check —
reject commands from unknown consumers (consistent with execute command)
- signaling_hub: fix hasSubscribers doc — says "any subscriber" not
"main-isolate subscriber" (push-notification isolate can also subscribe)
- signaling_hub_module: update class doc — connect/disconnect now forward
commands to hub client, not no-ops
* refactor(WT-1221): centralize disconnect notification decisions in SignalingReconnectController (#1089)
* refactor(signaling): centralize disconnect notification decisions in SignalingReconnectController
Move all notification decisions out of CallBloc.__onSignalingClientEventDisconnected
into the single onConnectionFailed callback of SignalingReconnectController.
The callback now receives SignalingDisconnectCode? knownCode so the consumer
can decide what to show:
- signalingKeepaliveTimeoutError / controllerForceAttachClose → silent (no toast)
- sessionMissedError → SignalingSessionMissedNotification
- null (connect failure) → SignalingConnectFailedNotification
- other codes → SignalingDisconnectNotification(knownCode)
__onSignalingClientEventDisconnected now only updates CallState.
This aligns the code with the comment that was already there:
"notification decisions are fully handled by _reconnectController".
Fixes keepalive timeout (4502) appearing as a user-visible error on lock-screen
unlock — it is now silently swallowed and the reconnect proceeds transparently.
* refactor(signaling): log silent reconnect codes in onConnectionFailed
* refactor(signaling): add comment for silent reconnect codes in onConnectionFailed
* refactor(signaling): address Copilot review comments on PR#1089
- SignalingFailureInfo record replaces bare knownCode in onConnectionFailed,
forwarding systemCode/systemReason so SignalingDisconnectNotification
retains full diagnostic details
- signalingKeepaliveTimeoutError sets lastSignalingDisconnectCode=null
to prevent connectIssue UI state (same as controllerForceAttachClose)
- onConnectionFailed uses switch expression for clarity
- fix doc comment example and __onSignalingClientEventDisconnected comment wording
* fix(signaling): reset _wasConnected on app pause to prevent spurious toast on unlock (WT-1221)
When notifyAppPaused disconnects intentionally, _wasConnected is now reset to
false. Previously it stayed true after a successful session, so the first
post-unlock SignalingConnectionFailed hit the '_wasConnected' fast-path and
fired onConnectionFailed immediately — bypassing the consecutive-failure
threshold and showing 'Connecting to the core failed' on screen unlock.
With this fix the post-unlock reconnect is treated as a fresh attempt:
the threshold applies and transient DNS/network failures are suppressed.
* fix(signaling): suppress background notifications and reset state on resume
On Android, SignalingHubModule.connect/disconnect are no-ops — the
foreground-service isolate owns the WebSocket lifecycle and reconnects
independently. When the app is backgrounded, background reconnects set
_wasConnected = true. A subsequent failure then fires onConnectionFailed,
which queues a toast that appears incorrectly when the app resumes
(green status + error toast simultaneously).
Two fixes:
1. Guard _onConnectionFailed calls with (_appActive || _hasActiveCalls).
No notifications are queued while the user cannot see them.
2. Reset _wasConnected and _consecutiveFailures in notifyAppResumed so
background reconnect state does not bypass the failure threshold after
the app comes to foreground.
Adds two new regression tests covering both scenarios.
* docs(signaling): update stale comments in SignalingReconnectController
After PR #1091 SignalingHubModule.connect()/disconnect() are no longer
no-ops — the hub protocol is now bidirectional. Update two comments that
still referenced the old "hub handles reconnects independently" behaviour:
- notifyAppResumed(): explain that the _wasConnected reset is needed for
persistent-service mode session buffer replay, not for independent
background reconnects
- _onEvent: replace the outdated no-ops no…
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This pull request adds a warning message to the audio packetization-time settings in the media settings UI, alerting users about potential issues when using non-default values. The warning is fully localized in English, Italian, and Ukrainian, and the necessary localization keys and translations have been added to the codebase.