Skip to content

Commit 90847f9

Browse files
SERDUNOMaudzaDmytro Serdundigiboridev
authored
release: 1.15.4
* 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…
1 parent 53e74cc commit 90847f9

195 files changed

Lines changed: 8178 additions & 8119 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.fvmrc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"flutter": "3.44.0"
3+
}

.github/flutter_version.yaml

Lines changed: 0 additions & 3 deletions
This file was deleted.
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
name: auto-tag-version
2+
3+
# After a release PR lands on main, publish the version tag automatically.
4+
# Reads the app_version field from the root pubspec.yaml and creates an annotated
5+
# tag X.Y.Z on the merged main commit if it does not already exist.
6+
# Idempotent: pushes that do not change app_version (or where the tag already
7+
# exists, e.g. a hotfix X.Y.Z+N reusing the same base) are no-ops.
8+
# Note: app_version (not the standard version: field) is the release version.
9+
10+
on:
11+
push:
12+
branches:
13+
- main
14+
paths:
15+
- pubspec.yaml
16+
17+
permissions:
18+
contents: write
19+
20+
jobs:
21+
tag:
22+
runs-on: ubuntu-latest
23+
steps:
24+
- uses: actions/checkout@v4
25+
with:
26+
fetch-depth: 0
27+
28+
- name: Create version tag if missing
29+
run: |
30+
set -euo pipefail
31+
# awk (not grep|awk): returns 0 even with no match, so a missing
32+
# app_version yields an empty VERSION and reaches the error below
33+
# instead of aborting on pipefail.
34+
VERSION_FIELD=$(awk '/^app_version:/{print $2; exit}' pubspec.yaml)
35+
VERSION="${VERSION_FIELD%%+*}"
36+
if [ -z "$VERSION" ]; then
37+
echo "::error::Could not read app_version from pubspec.yaml"
38+
exit 1
39+
fi
40+
if git rev-parse -q --verify "refs/tags/$VERSION" >/dev/null; then
41+
echo "Tag '$VERSION' already exists - nothing to do."
42+
exit 0
43+
fi
44+
echo "Creating tag '$VERSION' on $GITHUB_SHA"
45+
git config user.name "github-actions[bot]"
46+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
47+
git tag -a "$VERSION" -m "release $VERSION"
48+
git push origin "$VERSION"
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: callkeep-path-guard
2+
3+
# On develop, webtrit_callkeep must stay a path dependency (see docs/release_versioning.md).
4+
# This catches a release `git:` ref accidentally back-merged onto develop.
5+
6+
on:
7+
pull_request:
8+
branches:
9+
- develop
10+
11+
permissions:
12+
contents: read
13+
14+
jobs:
15+
verify:
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
# Uses Ruby's built-in YAML (psych) - always available on GitHub runners, no extra install.
21+
- name: Ensure webtrit_callkeep is a path dependency
22+
run: |
23+
ruby -ryaml -e '
24+
dep = (YAML.load_file("pubspec.yaml")["dependencies"] || {})["webtrit_callkeep"]
25+
ok = dep.is_a?(Hash) && dep.key?("path") && !dep.key?("git")
26+
unless ok
27+
warn "::error::On develop, webtrit_callkeep must use a path dependency, not a git ref. A release pin was likely back-merged. See docs/release_versioning.md."
28+
exit 1
29+
end
30+
puts "OK: webtrit_callkeep is a path dependency."
31+
'
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
name: callkeep-pin-guard
2+
3+
# On release branches and release tags, webtrit_callkeep must be pinned to a git ref
4+
# (see docs/release_versioning.md). Mirror of callkeep-path-guard (which keeps develop on path).
5+
6+
on:
7+
push:
8+
tags:
9+
- "*.*.*"
10+
pull_request:
11+
branches:
12+
- "release/**"
13+
14+
permissions:
15+
contents: read
16+
17+
jobs:
18+
verify:
19+
runs-on: ubuntu-latest
20+
steps:
21+
- uses: actions/checkout@v4
22+
23+
# Uses Ruby's built-in YAML (psych) - always available on GitHub runners, no extra install.
24+
- name: Ensure webtrit_callkeep is pinned to a git ref
25+
run: |
26+
ruby -ryaml -e '
27+
dep = (YAML.load_file("pubspec.yaml")["dependencies"] || {})["webtrit_callkeep"]
28+
git = dep.is_a?(Hash) ? dep["git"] : nil
29+
ref = git.is_a?(Hash) ? git["ref"].to_s : ""
30+
if ref.empty?
31+
warn "::error::On release branches and tags, webtrit_callkeep must be pinned to a git ref (got path/none). See docs/release_versioning.md."
32+
exit 1
33+
end
34+
puts "OK: webtrit_callkeep is pinned to ref #{ref}."
35+
'

.github/workflows/git-lint.yml

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,25 @@ jobs:
3333
exit 1
3434
fi
3535
36-
REGEX="^(feat|fix|chore|refactor|test|docs|style|ci|perf|build|revert)(\([^)]+\))?: [^A-Z].+$"
36+
# A capitalized first letter in the description is allowed (e.g. "fix: WebRTC ...").
37+
REGEX="^(feat|fix|chore|refactor|test|docs|style|ci|perf|build|revert)(\([^)]+\))?: .+$"
38+
39+
# Legacy commits that predate the current convention and are already merged into
40+
# shared history. Rewriting them would rewrite hundreds of commits across dozens of
41+
# branches, so they are exempted by exact subject instead.
42+
ALLOWLIST=(
43+
"fix fix changing info icon color (#836)"
44+
)
3745
3846
while IFS= read -r SUBJECT; do
3947
if [ -z "$SUBJECT" ]; then continue; fi
40-
48+
49+
for ALLOWED in "${ALLOWLIST[@]}"; do
50+
if [ "$SUBJECT" = "$ALLOWED" ]; then continue 2; fi
51+
done
52+
4153
if [[ ! "$SUBJECT" =~ $REGEX ]]; then
42-
echo "Invalid format or capitalized description: '$SUBJECT'"
54+
echo "Invalid commit message format: '$SUBJECT'"
4355
exit 1
4456
fi
4557
done <<< "$COMMITS_SUBJECTS"

.gitignore

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,3 +59,7 @@ makefile.shared
5959
.claude/settings.local.json
6060
CLAUDE.local.md
6161
**/.claude/
62+
integration_test/patrol_test_bundle.dart
63+
64+
# FVM SDK cache (managed by fvm; version pinned via .fvmrc)
65+
.fvm/

AGENTS.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
11
# AGENTS.md
22

33
WebTrit Phone — Flutter VoIP app, Melos monorepo.
4-
Flutter 3.32.4 (stable), Android SDK 35.0.1.
4+
Flutter 3.44.0 (stable), Android SDK 35.0.1.
5+
6+
## Toolchain
7+
8+
- Flutter version is pinned **only** in `.fvmrc` — single source of truth, read by `fvm` locally and by the `webtrit_phone_builder` CI. When you bump it, update the version mentioned above in the same commit. (Older release branches may still carry the legacy `.github/flutter_version.yaml`; the builder falls back to it there.)
9+
- Use `fvm flutter ...` / `fvm dart ...` so the pinned SDK is used; a bare `flutter` from `PATH` may be a different version. The `.fvm/` SDK cache is gitignored — run `fvm install` once per machine.
510

611
## Build & Test
712

@@ -16,8 +21,8 @@ dart run bin/create_new_schema_dump_and_test_migration.dart # after Drift tabl
1621

1722
## Code Standards
1823

19-
- No Cyrillic anywhere (source, comments, strings, logs, keys).
20-
- No inline comments — DartDoc only for public APIs.
24+
- No Cyrillic in source, comments, logs, strings, or keys, except translation values in localization ARB files (`lib/l10n/arb/*.arb`).
25+
- Comments: no redundant *what* comments that restate the code; comments explain non-obvious *why* (rationale, gotchas, workarounds, links to issues). DartDoc for public APIs.
2126
- No DI frameworks (`get_it`, `injectable`, Service Locator — forbidden).
2227
- Single quotes; 120-char line width.
2328
- Never edit `*.g.dart` / `*.freezed.dart` / `*.gr.dart` — regenerate via `build_runner`.
@@ -49,3 +54,11 @@ packages/ → shared libs (must NOT import from lib/)
4954
- Theme: never raw `Colors.xxx` or `TextStyle` in widgets; `Theme.of(context).extension<T>()`.
5055
- Widgets: `StatelessWidget` always (not helper methods); dumb widgets in `features/*/view/widgets/`.
5156
- Tests: `MockClient`/`mocktail` — no real network calls; DB migrations via `SchemaVerifier`.
57+
- Routing (`auto_route`): the `AppRouter.routes` tree must ALWAYS be complete — never gate route
58+
*declarations* on async/runtime values (server capability, login state, feature flags).
59+
`routeCollection` is `late final`, built once at router construction (before server `system-info`
60+
loads), so any `if (capability) AutoRoute(...)` is frozen with whatever the value was at startup;
61+
later navigation to a route omitted then throws `Failed to navigate to <Route>`. Register every
62+
variant unconditionally and decide which one to *show* at navigation/build time (e.g.
63+
`AutoTabsRouter.routes`, guards, initial-tab resolver). Sibling routes may share a `path`
64+
(`recents`) — `RouteCollection` only requires unique route *names*, and tab matching is name-based.

README.md

Lines changed: 14 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -40,48 +40,24 @@ The application offers extensive customization options:
4040

4141
# Testing
4242

43-
## Test commands
44-
* Run unit and widget tests
43+
### Unit and widget tests
44+
* Run unit and widget tests:
4545
```bash
4646
flutter test
4747
```
48-
* Run integration tests in dev mode
49-
```bash
50-
patrol develop --dart-define-from-file=../dart_define.json --dart-define-from-file=dart_define.integration_test.json --flavor=deeplinkssmsReceiver
51-
```
52-
* Build integration tests
53-
```bash
54-
patrol build android/ios --dart-define-from-file=../dart_define.json --dart-define-from-file=dart_define.integration_test.json --flavor=deeplinkssmsReceiver
55-
```
56-
* To specify a test file, use the `-t` option:
57-
```bash
58-
patrol build -t patrol_test/call_and_recent_test.dart ...
59-
```
60-
* For build and deploy to Firebase Test Lab, use the following command from the `tool/scripts` directory:
61-
```bash
62-
./testlab_assemble_android.sh <testfile(optional)>
63-
./testlab_assemble_ios.sh <testfile(optional)>
64-
```
48+
### Integration tests
49+
- **Framework**: We use Patrol framework for integration testing.
50+
It helps us to automate native iOS and Android behaviors and simplifyes routine tasks during testing.
51+
You can find more information about it here: https://patrol.dev/
52+
Integration tests are located in the `patrol_test` folder.
53+
54+
- **Configuration**: The configuration file `dart_define.integration_test.json` defines all the environment variables (login credentials, contacts, user info, and call settings) for the integration tests. [Integration Test Variables](docs/integration_test_variables.md).
55+
56+
- **Calls test**: For simulating SIP calls during tests, here is [pjsua Companion](packages/pjsua_companion/README.md) — an HTTP server that wraps the `pjsua` CLI to place and answer calls programmatically when app tests is running.
57+
58+
- **Commands**: See [Integration Test Commands](docs/integration_test_commands.md) for all patrol build, run, Firebase Test Lab, and local companion commands.
6559

66-
### Test variables
67-
68-
* `WEBTRIT_APP_TEST_CUSTOM_CORE_URL` (_example core.demo.mycompany.com_)
69-
* `WEBTRIT_APP_TEST_EMAIL_CREDENTIAL` (_example myaccount@mail.com_)
70-
* `WEBTRIT_APP_TEST_EMAIL_VERIFY_CREDENTIAL` (_example 123456_)
71-
* `WEBTRIT_APP_TEST_OTP_CREDENTIAL` (_example +1234566789_)
72-
* `WEBTRIT_APP_TEST_OTP_VERIFY_CREDENTIAL` (_example 123456_)
73-
* `WEBTRIT_APP_TEST_PASSWORD_USER_CREDENTIAL` (_example username_)
74-
* `WEBTRIT_APP_TEST_PASSWORD_PASSWORD_CREDENTIAL` (_example 123456_)
75-
* `WEBTRIT_APP_TEST_DEFAULT_LOGIN_METHOD` (_email_ | _password_ | _otp_)
76-
* `WEBTRIT_APP_TEST_EXT_CONTACT_A` (_example User A_)
77-
* `WEBTRIT_APP_TEST_EXT_CONTACT_A_NUMBER` (_example 00123_)
78-
* `WEBTRIT_APP_TEST_EXT_CONTACT_B` (_example User B_)
79-
* `WEBTRIT_APP_TEST_EXT_CONTACT_B_NUMBER` (_example 00456_)
80-
* `WEBTRIT_APP_TEST_ACCOUNT_NAME` (_example Test Account_)
81-
* `WEBTRIT_APP_TEST_ACCOUNT_MAIN_NUMBER` (_example 1230000_)
82-
* `WEBTRIT_APP_TEST_CALL_NUMBER_A` (_example 1111_)
83-
* `WEBTRIT_APP_TEST_CALL_NUMBER_B` (_example 2222_)
84-
* `WEBTRIT_APP_TEST_CROSS_CALL_SLEEP_SECONDS` (_example 10_)
60+
- **Coverage**: See [Integration Test Coverage](docs/integration_test_coverage.md) for a description of every test file and its steps.
8561

8662
## Contributing
8763

analysis_options.yaml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,24 @@ include: package:flutter_lints/flutter.yaml
33
linter:
44
rules:
55
prefer_single_quotes: true
6+
# TODO: remove this suppression and refactor constructors to use
7+
# initializing formals (`this._x`) across the codebase. Disabled here
8+
# because the Dart 3.12 analyzer started flagging ~59 pre-existing
9+
# explicit-init constructors on the Flutter 3.44 upgrade.
10+
prefer_initializing_formals: false
611

712
analyzer:
813
exclude:
914
- "**/*.g.dart"
1015
- "build/**"
1116
errors:
1217
deprecated_subclass: ignore
18+
# TODO: remove this suppression and refactor lib/extensions/string.dart
19+
# and lib/theme/models/icon_data_converter.dart so the IconData arguments
20+
# become compile-time constants. Disabled here because the warning is
21+
# pre-existing on develop and would block the pre-push analyze hook
22+
# after the Flutter 3.44 upgrade.
23+
non_const_argument_for_const_parameter: ignore
1324

1425
formatter:
1526
page_width: 120

0 commit comments

Comments
 (0)