-
Notifications
You must be signed in to change notification settings - Fork 56
Improve CallService lifecycle, notification handling, and ringing-to-active transition #1588
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Conversation
Adds more detailed logging to the `CallEventObserver` to provide better insight into the handling of incoming call events.
When a `CallRejectedEvent` is received, the `createdBy` field on the call state is now updated with the user who created the call.
Introduces `LocalCallAcceptedEvent` and `LocalCallRejectedEvent` to represent call state changes originating from the local device. These new local events are now used in `CallEventObserver` instead of the generic `CallAcceptedEvent` and `CallRejectedEvent`, providing a clearer distinction for handling call lifecycle states like accepting a call. Additionally, `CallState` now fires these local events, improving the accuracy of event-driven logic.
# Conflicts: # stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt # stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt
Adds a reason when leaving a call from the `CallLifecycleManager`. This provides more context for why the call was ended, distinguishing between different scenarios such as the task being removed for an ongoing or incoming call.
PR checklist ✅All required conditions are satisfied:
🎉 Great job! This PR is ready for review. |
SDK Size Comparison 📏
|
Renames several components within the `CallService` infrastructure to improve clarity by prefixing them with "CallService". This includes managers and observers. The events `LocalCallAcceptedEvent` and `LocalCallRejectedEvent` are renamed to `LocalCallAcceptedPostEvent` and `LocalCallRejectedPostEvent` respectively to better reflect their purpose. Additionally, this change introduces a comprehensive suite of unit tests for the `CallService` and its related components, covering: - Lifecycle and notification management - Event, ringing state, and notification update observers - Foreground service permission handling - Debouncer and Throttler utilities
Renames `LocalCallAcceptedEvent` and `LocalCallRejectedEvent` to `LocalCallAcceptedPostEvent` and `LocalCallRejectedPostEvent` respectively, to better reflect that they are events sent *after* the action has occurred.
Adds `notificationId` as a parameter to the `updateNotification` function. This allows for updating the notification with a specific ID. The previous `updateNotification` function without the ID has been deprecated.
Adds an overloaded version of the `updateNotification` method that doesn't require a notification ID, simplifying its usage.
The `transitionToAcceptCall` function in `ClientState` is now marked as `internal`, restricting its usage to within the same module. This change also updates the public API definition.
Removes an unnecessary `stopService` call that was being made immediately after `startService` in `ServiceLauncher`.
Adds a TODO comment to `DefaultNotificationDispatcher.kt` to ensure a new fix is verified before merging.
Adds a TODO comment to `DefaultNotificationDispatcher.kt` to ensure a new fix is verified before merging.
Adds the `RobolectricTestRunner` to `ServiceStateTest` to address test failures that occurred when running all tests together. The `unregisterToggleCameraBroadcastReceiver` test has been moved to the end of the file as it was failing for unknown reasons, and a `println` has been added for debugging.
When a call is initiated, it now transitions through the `RingingState.Outgoing` state. This change ensures that a foreground service with an outgoing call notification is started, providing immediate feedback to the user that a call is being placed. A delay is introduced before starting the foreground service for outgoing calls to allow the system to properly transition the call state.
This commit introduces several improvements to the handling of call notifications and the lifecycle of the `CallService`. The `handleNotification` method in `CallService` now returns a `CallServiceHandleNotificationResult` enum. This allows for more granular control over the service's state, enabling it to continue running without re-initialization (`START_NO_CHANGE`) or to be redelivered if an error occurs. Additionally, the logic for handling `LocalCallMissedEvent` has been moved from `CallServiceEventObserver` directly into the `CallState`. This change ensures that incoming call notifications are correctly dismissed when another call is already active, without unnecessarily stopping the foreground service. Other changes include: - Preventing the start of a new `CallService` if one is already running for another call. - Adding a short delay before starting the foreground service when a call is accepted to prevent race conditions. - Enhancing logging for better debugging of service and notification lifecycles.
Removes a leftover development comment in `ServiceLauncher.kt`. Adds a clarifying comment in `ClientState.kt` to explain the logic for transitioning incoming/outgoing calls to an active state within the same service.
Removes temporary "Noob" log messages from `ClientState`, `CallServiceEventObserver`, and `CallService`. Also removes some commented-out code from `CallServiceEventObserver`.
Updates the `isServiceRunning` check to return `false` when the service is not running.
Removes the static `runningServiceClassName` set from `CallService` and updates the `isServiceRunning` check. The `ServiceIntentBuilder.isServiceRunning` method now uses `ActivityManager.getRunningServices()` to determine if a service is active. This provides a more reliable check based on the Android system's state. The method signature for `isServiceRunning` has been updated to require a `Context` parameter.
Extracts the `Throttler` object from `Debouncer.kt` into its own dedicated file, `Throttler.kt`. This change also moves the `debouncer` instance in `CallService` to be a top-level property and adds documentation explaining its purpose.
Throttles the `stopService` requests in `ServiceLauncher` to prevent the service from being stopped and restarted too frequently. The throttling is set to `1000ms` using the new `SERVICE_DESTROY_THROTTLE_TIME_MS` constant defined in `CallService`.
This commit re-enables the `stopServiceGracefully()` call, which was previously commented out. The `source` parameter has also been removed from the `stopServiceGracefully()` method signature.
Removes a `// TODO` comment from `DefaultNotificationDispatcher`.
Retrieves the `notificationId` from the `call.state` when available, instead of calculating it. This ensures a consistent notification ID is used for ongoing, incoming, and outgoing call notifications within the `ServiceNotificationRetriever` and `CallServiceNotificationUpdateObserver`.
Updates the `ServiceNotificationRetrieverTest` to use the call-specific notification ID generated by `callId.getNotificationId()` instead of the call ID's hashcode. This aligns the test with the actual implementation for generating notification IDs.
When cancelling notifications for a call, the `CallServiceNotificationManager` now correctly uses the `notificationId` from the call's state. Previously, it was attempting to cancel a hardcoded `Incoming` notification type ID, which might not be the correct one. This change ensures the notification that was actually shown is the one that gets cancelled.
Adds more detailed logging to the `CallServiceNotificationUpdateObserver` for better insight into notification updates. Also, this change ensures that an existing notification ID from the call state is used before generating a new one when showing call notifications.
This commit refactors the `CallService` to improve clarity and maintainability. Key changes include: - Extracting the notification handling logic into smaller, more focused functions (`handleNullNotification`, `startForegroundForCall`). - Passing the `serviceScope` to `CallServiceEventObserver` to ensure its lifecycle is tied to the service. - Enhancing logging throughout the call service and related managers to provide more detailed information for debugging, including hashcodes to identify specific object instances. - Adding `serviceScope.cancel()` in `onDestroy` to ensure all coroutines are properly cancelled when the service is destroyed.
Refactors the `IncomingCallPresenter` to simplify its logic by extracting functionality into smaller, more descriptive private functions. This improves readability and makes the different paths for handling incoming calls clearer. The `buildStopIntent` method in `ServiceIntentBuilder` is updated to only return an intent if the corresponding service is actually running, preventing unnecessary stop commands. Additionally, this commit: - Removes the handling of `LocalCallMissedEvent` from `CallServiceEventObserver` as it's no longer needed. - Enhances tests for `IncomingCallPresenter` and `ServiceIntentBuilder` to cover the new logic and edge cases.
|
WalkthroughThis pull request refactors the call notification and foreground service lifecycle management in the Android video SDK. It introduces new observer-driven components, permission managers, and state tracking to improve reliability of incoming call notifications and service lifecycle while adding notification ID tracking and enhanced event handling. Changes
Sequence Diagram(s)sequenceDiagram
participant Service as CallService
participant Lifecycle as CallServiceLifecycleManager
participant EventObs as CallServiceEventObserver
participant RingObs as CallServiceRingingStateObserver
participant NotifObs as CallServiceNotificationUpdateObserver
participant CallState as Call/CallState
participant Client as StreamVideoClient
Service->>Service: onCreate()
Service->>Lifecycle: initializeCallAndSocket()
Lifecycle->>Client: get call by ID
Lifecycle->>CallState: connect socket
Lifecycle-->>Service: onError() or success
rect rgb(200, 220, 255)
Note over Service,NotifObs: Incoming Call Ringing Flow
Service->>EventObs: observe(onServiceStop, onRemoveIncoming)
Service->>RingObs: observe(onStopService)
Service->>NotifObs: startForegroundWithServiceType()
RingObs->>CallState: subscribe to ringingState
NotifObs->>CallState: subscribe to ringingState
alt Incoming Ringing
CallState-->>RingObs: Incoming state
RingObs->>CallState: play sound + vibrate
CallState-->>NotifObs: Incoming state
NotifObs->>Service: startForegroundWithServiceType(INCOMING)
end
end
rect rgb(220, 240, 220)
Note over EventObs,Client: Event Handling
CallState-->>EventObs: LocalCallAcceptedPostEvent
alt Accepted on Another Device
EventObs->>Service: onServiceStop()
end
CallState-->>EventObs: CallEndedEvent
EventObs->>Service: onServiceStop()
end
rect rgb(240, 220, 220)
Note over Service,Client: Service Cleanup
alt Graceful Stop
Service->>Service: stopServiceGracefully()
Service->>Lifecycle: endCall()
Lifecycle->>CallState: reject/leave call
end
end
Service->>Service: onDestroy()
Service->>RingObs: cleanup
Service->>EventObs: cleanup
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes This refactoring introduces significant architectural changes to the call service lifecycle and notification handling system. The complexity stems from: (1) multiple interdependent new components (lifecycle managers, observers, permission managers) requiring careful interaction validation; (2) intricate state transitions and event flows across 5+ files in core call/state logic; (3) version-dependent permission handling logic across Android API levels; (4) extensive changes to notification ID tracking and service lifecycle—each requiring separate reasoning. While the changes follow cohesive patterns, the heterogeneity of new abstractions (observers, managers, throttlers) and the density of state management updates across files demand thorough review. Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actionable comments posted: 18
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceLauncher.kt (1)
19-33: Remove duplicate license header.Lines 19-33 contain a duplicate of the license header that already appears at lines 1-15. This duplication should be removed.
🔎 Proposed fix
package io.getstream.video.android.core.notifications.internal.service -/* - * Copyright (c) 2014-2024 Stream.io Inc. All rights reserved. - * - * Licensed under the Stream License; - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://github.com/GetStream/stream-video-android/blob/main/LICENSE - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - import android.annotation.SuppressLintstream-video-android-core/src/main/kotlin/io/getstream/video/android/core/Call.kt (1)
465-477: Remove redundant direct assignment of_ringingCall.valuebeforeaddRingingCall.Lines 470 and 474 both set
_ringingCall.valueto the same call. SinceaddRingingCall()already assigns the value and additionally handles foreground service setup, the direct assignment at line 470 is redundant.The comment claims that
updateFromResponsereads_ringingCall, but inspection of that method shows it only updates call state fields and does not accessringingCallat all. Consolidating to a single call toaddRingingCallwould be clearer.
🧹 Nitpick comments (20)
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceNotificationRetrieverTest.kt (1)
61-64: Consider structured test coroutine patterns for TestScope.The TestScope is created to satisfy
mockStreamVideoClient.scope, but it's not leveraged with structured patterns likerunTestor dispatcher control. If the code under test performs asynchronous work that needs deterministic execution in tests, consider usingStandardTestDispatcherorUnconfinedTestDispatcherwith proper test runners.However, if
ServiceNotificationRetrieveroperates synchronously or the current setup sufficiently covers the test scenarios, the current approach is acceptable.Based on coding guidelines regarding structured coroutines and test best practices.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/models/CallIntentParams.kt (1)
22-27: Consider using a sealed class or enum for thetriggerparameter.The
triggerparameter is currently aString, which allows any value at runtime. Using a type-safe alternative would prevent invalid trigger values and improve compile-time safety.🔎 Example refactor using a sealed class
Define a sealed class for triggers:
internal sealed class CallTrigger { data object IncomingCall : CallTrigger() data object OutgoingCall : CallTrigger() data object OngoingCall : CallTrigger() data object RemoveIncomingCall : CallTrigger() }Then update the data class:
internal data class CallIntentParams( val streamVideo: StreamVideoClient, val callId: StreamCallId, - val trigger: String, + val trigger: CallTrigger, val displayName: String?, )stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/models/ServiceState.kt (2)
50-52: Log swallowed exceptions for diagnostics.Static analysis flagged these silently swallowed exceptions. While failing silently may be intentional for robustness, logging at debug level preserves diagnostic info without impacting release builds.
🔎 Proposed fix to log exceptions
+import io.getstream.log.taggedLogger + internal class ServiceState { + private val logger by taggedLogger("ServiceState") // ... existing fields ... internal fun registerToggleCameraBroadcastReceiver(service: Service, scope: CoroutineScope) { if (!isReceiverRegistered) { try { // ... registration logic ... isReceiverRegistered = true } catch (e: Exception) { - // Silently fail + logger.d(e) { "Failed to register camera broadcast receiver" } } } } internal fun unregisterToggleCameraBroadcastReceiver(service: Service) { if (isReceiverRegistered) { try { toggleCameraBroadcastReceiver?.let { service.unregisterReceiver(it) } isReceiverRegistered = false } catch (e: Exception) { - // Silently fail + logger.d(e) { "Failed to unregister camera broadcast receiver" } } } } }Also applies to: 61-63
28-35: Consider thread safety for mutable state.
ServiceStateholds mutable properties that may be accessed from different threads (e.g., main thread vs. coroutine scope). If concurrent access is expected, consider using@Volatilefor primitive flags orAtomicReferencefor object references.stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/IncomingCallPresenterTest.kt (1)
53-66: Remove unusedcallServiceConfigvariable.The
callServiceConfiginitialized on line 66 withenableTelecom = trueis never used—tests useconfig(line 59) instead.🔎 Proposed fix
private lateinit var context: Context private lateinit var serviceIntentBuilder: ServiceIntentBuilder private lateinit var presenter: IncomingCallPresenter - private lateinit var callServiceConfig: CallServiceConfig private lateinit var notification: Notification private lateinit var streamVideoClient: StreamVideoClient private val callId = StreamCallId("default", "123", "default:123") private val serviceClass = CallService::class.java private val config = CallServiceConfig(serviceClass = serviceClass) @Before fun setup() { MockKAnnotations.init(this, relaxed = true) context = mockk(relaxed = true) serviceIntentBuilder = mockk(relaxed = true) - callServiceConfig = CallServiceConfig(enableTelecom = true) notification = mockk(relaxed = true)stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/observers/CallServiceRingingStateObserver.kt (1)
42-49: Observer usescall.scopeinstead of constructorscope.The observer collects
ringingStateoncall.scope(line 43), but the constructor accepts ascopeparameter. This inconsistency could cause lifecycle issues if the provided scope has different cancellation semantics. Consider using the providedscopeconsistently.🔎 Proposed fix
fun observe(onStopService: () -> Unit) { - call.scope.launch { + scope.launch { call.state.ringingState.collect { state -> logger.i { "Ringing state: $state" } handleRingingState(state, onStopService) } } }stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/observers/CallServiceEventObserver.kt (1)
136-144: Inconsistent scope usage between observers.
observeCallEvents(line 52) uses the constructorscope, butobserveConnectionState(line 137) usescall.scope. Use the providedscopeconsistently for predictable lifecycle management.🔎 Proposed fix
private fun observeConnectionState(onServiceStop: () -> Unit) { - call.scope.launch { + scope.launch { call.state.connection.collectLatest { event -> if (event is RealtimeConnection.Failed) { handleConnectionFailure() } } } }stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/observers/CallServiceNotificationUpdateObserver.kt (1)
183-190: Considerinternalvisibility forstartForegroundWithServiceType.This method is a simple delegation to
onStartService. If it's only called within the module, marking itinternalreduces public API surface.🔎 Proposed fix
- fun startForegroundWithServiceType( + internal fun startForegroundWithServiceType( notificationId: Int, notification: Notification, trigger: String, foregroundServiceType: Int, ) { onStartService(notificationId, notification, trigger, foregroundServiceType) }stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/permissions/AudioCallPermissionManagerTest.kt (1)
33-44: Test coverage is adequate for the current implementation.The test correctly validates that both
PHONE_CALLandMICROPHONEtypes are declared. While additional tests could verify parent class integration (e.g.,androidQServiceType(),noPermissionServiceType()behavior across SDK versions as inLivestreamViewerPermissionManagerTest), the current coverage is sufficient since this class only overridesrequiredForegroundTypes.stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/handlers/StreamDefaultNotificationHandler.kt (1)
215-219: Prefer null-safe casting over broad exception handling.The current try-catch swallows all exceptions silently. Using null-safe casting is more idiomatic in Kotlin and avoids masking unexpected errors.
🔎 Proposed fix
- val createdByUserId = try { - payload["created_by_id"] as String - } catch (ex: Exception) { - "" - } + val createdByUserId = (payload["created_by_id"] as? String).orEmpty()stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/observers/CallServiceEventObserverTest.kt (1)
188-188: Optional: Remove leftover comment.The comment
// nextappears to be a leftover debugging or TODO note.🔎 Proposed cleanup
- @Test // next + @Test fun `call ended stops service`() = runTest {stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/observers/CallServiceNotificationUpdateObserverTest.kt (1)
137-137: Clarify commented-out code.The
advanceUntilIdle()call is commented out without explanation. If this is intentional for testing timing behavior, a comment would help future maintainers understand why. If it's leftover debug code, it should be removed.🔎 Suggested improvement
If intentional:
fun `incoming ringing state starts incoming foreground notification`() = runTest { observer.observe(context) -// advanceUntilIdle() + // Don't advance here to test immediate notification behaviorIf not needed:
fun `incoming ringing state starts incoming foreground notification`() = runTest { observer.observe(context) -// advanceUntilIdle()stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/managers/CallServiceLifecycleManagerTest.kt (1)
129-143: MissingmockkObjectcleanup may cause test pollution.The tests using
mockkObject(StreamVideo.Companion)(lines 133, 150, 165, 179) do not unmock in@After. This can leak mocked state to other tests in the suite.🔎 Proposed fix
Add cleanup in the
tearDownmethod:@After fun tearDown() { Dispatchers.resetMain() + io.mockk.unmockkObject(StreamVideo.Companion) }Alternatively, use
unmockkAll()if other static mocks are also present.stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/Throttler.kt (1)
63-67: Stack trace index 4 is brittle and may break with call site changes.The hardcoded index assumes a specific call depth. Inline functions, lambda wrappers, or compiler optimizations could shift the index. The fallback to
action.hashCode()mitigates this but loses caller context.Consider requiring an explicit key for production callers to avoid relying on stack inspection.
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/managers/CallServiceNotificationManager.kt (1)
56-59: Redundantletblock on non-null parameter.
callIdis already non-null in the function signature, so theletblock adds no value.🔎 Proposed simplification
fun cancelNotifications(service: Service, callId: StreamCallId) { val notificationManager = NotificationManagerCompat.from(service) - callId.let { - logger.d { "[cancelNotifications], notificationId via hashcode: ${it.hashCode()}" } - notificationManager.cancel(it.hashCode()) - } + logger.d { "[cancelNotifications], notificationId via hashcode: ${callId.hashCode()}" } + notificationManager.cancel(callId.hashCode())stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/IncomingCallPresenter.kt (1)
102-126: Unusederrorparameter inshowNotification.The
errorparameter is passed but never used in the function body. Consider removing it or logging it for debugging purposes.🔎 Proposed fix
private fun showNotification( context: Context, notification: Notification?, callId: StreamCallId, - error: Any?, ): ShowIncomingCallResult {Or log the error:
if (!hasNotificationPermission(context) || notification == null) { logger.w { "[showIncomingCall] Cannot show notification - " + "permission=${hasNotificationPermission(context)}, " + - "notification=${notification != null}" + "notification=${notification != null}, " + + "error=$error" }stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/managers/CallServiceLifecycleManager.kt (2)
63-91: InconsistentStreamVideoaccess pattern.
initializeCallAndSocketandupdateRingingCallreceivestreamVideoas a parameter, butendCallaccessesStreamVideo.instanceOrNull()directly. Consider passingstreamVideoas a parameter for consistency and testability.🔎 Proposed signature change
-fun endCall(scope: CoroutineScope, callId: StreamCallId?) { +fun endCall(scope: CoroutineScope, streamVideo: StreamVideo?, callId: StreamCallId?) { callId?.let { id -> - StreamVideo.Companion.instanceOrNull()?.let { streamVideo -> + streamVideo?.let { val call = streamVideo.call(id.type, id.id)
93-106: Magic number2for member count check.The hardcoded
memberCount == 2assumes 1:1 call semantics. Consider extracting to a named constant with documentation explaining the business logic.🔎 Proposed improvement
companion object { /** In a 1:1 call (2 members), rejecting ends the call for both parties */ private const val ONE_TO_ONE_CALL_MEMBER_COUNT = 2 } // Usage: if (memberCount == ONE_TO_ONE_CALL_MEMBER_COUNT) {stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/service/CallService.kt (2)
108-110: Optional: Remove redundant exception string interpolation.The exception is already passed to
logger.e(exception), so including$exceptionin the message string is redundant. The logger will format the stack trace appropriately.🔎 Proposed refinement
val handler = CoroutineExceptionHandler { _, exception -> - logger.e(exception) { "[CallService#Scope] Uncaught exception: $exception" } + logger.e(exception) { "[CallService#Scope] Uncaught exception" } }
614-617: Address TODO: Verify sound ownership before stopping.The TODO comment indicates uncertainty about which call owns the sound. This could cause issues if multiple calls are active. Consider tracking sound ownership explicitly or verifying the current call before stopping sound.
Do you want me to help design a solution for tracking sound ownership across multiple calls?
| public fun <init> (Ljava/lang/String;Ljava/lang/String;)V | ||
| public final fun component1 ()Ljava/lang/String; | ||
| public final fun copy (Ljava/lang/String;)Lio/getstream/android/video/generated/models/LocalCallMissedEvent; | ||
| public static synthetic fun copy$default (Lio/getstream/android/video/generated/models/LocalCallMissedEvent;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/android/video/generated/models/LocalCallMissedEvent; | ||
| public final fun component2 ()Ljava/lang/String; | ||
| public final fun copy (Ljava/lang/String;Ljava/lang/String;)Lio/getstream/android/video/generated/models/LocalCallMissedEvent; | ||
| public static synthetic fun copy$default (Lio/getstream/android/video/generated/models/LocalCallMissedEvent;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/getstream/android/video/generated/models/LocalCallMissedEvent; | ||
| public fun equals (Ljava/lang/Object;)Z | ||
| public fun getCallCID ()Ljava/lang/String; | ||
| public final fun getCallCid ()Ljava/lang/String; | ||
| public final fun getCreatedById ()Ljava/lang/String; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Search for LocalCallMissedEvent constructor usages
rg -n 'LocalCallMissedEvent\s*\(' --type kotlin --type java -C 2Repository: GetStream/stream-video-android
Length of output: 1654
🏁 Script executed:
#!/bin/bash
# Search for LocalCallMissedEvent in source files to understand the implementation
fd -e kt -e java | xargs grep -l 'class LocalCallMissedEvent' 2>/dev/null | head -10Repository: GetStream/stream-video-android
Length of output: 168
🏁 Script executed:
#!/bin/bash
# Check for CHANGELOG or migration documentation
fd -i 'changelog|release|migration|changes' -e md -e txt 2>/dev/null | head -20Repository: GetStream/stream-video-android
Length of output: 86
🏁 Script executed:
#!/bin/bash
# Search for deprecation annotations related to LocalCallMissedEvent
rg -n 'LocalCallMissedEvent' --type kotlin --type java | grep -i 'deprecated\|@Deprecated'Repository: GetStream/stream-video-android
Length of output: 56
🏁 Script executed:
#!/bin/bash
# Check CHANGELOG for LocalCallMissedEvent or related changes
head -100 CHANGELOG.mdRepository: GetStream/stream-video-android
Length of output: 739
🏁 Script executed:
#!/bin/bash
# More comprehensive search for LocalCallMissedEvent usage (including tests)
rg 'LocalCallMissedEvent' --type kotlin --type java -A 1 -B 1Repository: GetStream/stream-video-android
Length of output: 4498
🏁 Script executed:
#!/bin/bash
# Check if there are any example or test files
fd -e kt -e java | xargs grep -l 'LocalCallMissedEvent' 2>/dev/nullRepository: GetStream/stream-video-android
Length of output: 492
Add documentation of the breaking API change to the CHANGELOG.
The LocalCallMissedEvent constructor change from one parameter to two (adding createdById) is confirmed as intentional and properly implemented—all internal usages have been updated. However, the CHANGELOG has not been updated to document this breaking API change in the "Changed" section of stream-video-android before release.
Update CHANGELOG.md to document this change so consumers are aware of the migration needed if they instantiate LocalCallMissedEvent directly.
🤖 Prompt for AI Agents
In stream-video-android-core/api/stream-video-android-core.api around lines 3561
to 3569, the API shows LocalCallMissedEvent's constructor gained a second
parameter (createdById) but the CHANGELOG.md was not updated; add an entry in
stream-video-android/CHANGELOG.md under the "Changed" section documenting the
breaking API change: state that LocalCallMissedEvent's constructor signature
changed from (callCid) to (callCid, createdById), note this is intentional, and
include a short migration note instructing consumers to pass the new createdById
value when instantiating or update usage to the new factory/usage pattern.
| fun setActiveCall(call: Call) { | ||
| this._activeCall.value = call | ||
| removeRingingCall(call) | ||
| call.scope.launch { | ||
| /** | ||
| * Temporary fix: `maybeStartForegroundService` is called just before this code, which can stop the service | ||
| */ | ||
| delay(500L) | ||
| maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) | ||
| /** | ||
| * Transition incoming/outgoing call to active on the same service | ||
| */ | ||
| val ringingState = call.state.ringingState.value | ||
| when (ringingState) { | ||
| is RingingState.Incoming -> { | ||
| call.scope.launch { | ||
| transitionToAcceptCall(call) | ||
| delay(500L) | ||
| maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) | ||
| } | ||
| } | ||
| is RingingState.Outgoing -> { | ||
| call.scope.launch { | ||
| transitionToAcceptCall(call) | ||
| delay(500L) | ||
| maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) | ||
| } | ||
| } | ||
| else -> { | ||
| removeRingingCall(call) | ||
| call.scope.launch { | ||
| /** | ||
| * Temporary fix: `maybeStartForegroundService` is called just before this code, which can stop the service | ||
| */ | ||
| delay(500L) | ||
| maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Replace hard-coded delays with event-driven synchronization.
The 500ms hard-coded delays are labeled as a "Temporary fix" and appear in multiple branches. This approach is fragile and could cause race conditions on slower devices or under load. The timing-based coordination suggests that maybeStartForegroundService might be executing before the service is ready to receive the call.
Consider refactoring to use proper event-driven synchronization (e.g., callbacks, suspendCancellableCoroutine, or a shared state flow) instead of arbitrary delays. If the delay must remain temporarily, extract it to a named constant with documentation explaining why it's needed and what permanent solution is planned.
🔎 Suggested refactor to extract the magic number
+private const val SERVICE_TRANSITION_DELAY_MS = 500L // TODO: Replace with event-driven sync
fun setActiveCall(call: Call) {
this._activeCall.value = call
- /**
- * Transition incoming/outgoing call to active on the same service
- */
val ringingState = call.state.ringingState.value
when (ringingState) {
is RingingState.Incoming -> {
call.scope.launch {
transitionToAcceptCall(call)
- delay(500L)
+ delay(SERVICE_TRANSITION_DELAY_MS)
maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL)
}
}
is RingingState.Outgoing -> {
call.scope.launch {
transitionToAcceptCall(call)
- delay(500L)
+ delay(SERVICE_TRANSITION_DELAY_MS)
maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL)
}
}
else -> {
removeRingingCall(call)
call.scope.launch {
- /**
- * Temporary fix: `maybeStartForegroundService` is called just before this code, which can stop the service
- */
- delay(500L)
+ delay(SERVICE_TRANSITION_DELAY_MS)
maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL)
}
}
}
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun setActiveCall(call: Call) { | |
| this._activeCall.value = call | |
| removeRingingCall(call) | |
| call.scope.launch { | |
| /** | |
| * Temporary fix: `maybeStartForegroundService` is called just before this code, which can stop the service | |
| */ | |
| delay(500L) | |
| maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) | |
| /** | |
| * Transition incoming/outgoing call to active on the same service | |
| */ | |
| val ringingState = call.state.ringingState.value | |
| when (ringingState) { | |
| is RingingState.Incoming -> { | |
| call.scope.launch { | |
| transitionToAcceptCall(call) | |
| delay(500L) | |
| maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) | |
| } | |
| } | |
| is RingingState.Outgoing -> { | |
| call.scope.launch { | |
| transitionToAcceptCall(call) | |
| delay(500L) | |
| maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) | |
| } | |
| } | |
| else -> { | |
| removeRingingCall(call) | |
| call.scope.launch { | |
| /** | |
| * Temporary fix: `maybeStartForegroundService` is called just before this code, which can stop the service | |
| */ | |
| delay(500L) | |
| maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) | |
| } | |
| } | |
| } | |
| private const val SERVICE_TRANSITION_DELAY_MS = 500L // TODO: Replace with event-driven sync | |
| fun setActiveCall(call: Call) { | |
| this._activeCall.value = call | |
| val ringingState = call.state.ringingState.value | |
| when (ringingState) { | |
| is RingingState.Incoming -> { | |
| call.scope.launch { | |
| transitionToAcceptCall(call) | |
| delay(SERVICE_TRANSITION_DELAY_MS) | |
| maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) | |
| } | |
| } | |
| is RingingState.Outgoing -> { | |
| call.scope.launch { | |
| transitionToAcceptCall(call) | |
| delay(SERVICE_TRANSITION_DELAY_MS) | |
| maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) | |
| } | |
| } | |
| else -> { | |
| removeRingingCall(call) | |
| call.scope.launch { | |
| delay(SERVICE_TRANSITION_DELAY_MS) | |
| maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL) | |
| } | |
| } | |
| } | |
| } |
🤖 Prompt for AI Agents
In
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/ClientState.kt
around lines 164-195, the code uses repeated hard-coded 500ms delays to wait for
the call service which is fragile; replace these timing-based waits with
event-driven synchronization: create or use an observable/shared StateFlow or a
suspend function (e.g., suspendCancellableCoroutine) that completes when the
CallService reports it is ready/foregrounded, then await that signal before
calling maybeStartForegroundService(call, CallService.TRIGGER_ONGOING_CALL); if
you must keep a temporary delay, extract the 500L into a well-named constant
(e.g., CALL_SERVICE_STARTUP_WAIT_MS) with a comment explaining it is temporary
and reference the intended permanent solution (state flow / suspend await) so
future maintainers can remove it.
| internal class Debouncer { | ||
|
|
||
| private val handler = Handler(Looper.getMainLooper()) | ||
| private var runnable: Runnable? = null | ||
|
|
||
| fun submit(delayMs: Long, action: () -> Unit) { | ||
| runnable?.let { handler.removeCallbacks(it) } | ||
|
|
||
| runnable = Runnable { action() } | ||
| runnable?.let { | ||
| handler.postDelayed(it, delayMs) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add lifecycle management to prevent memory leaks.
The Debouncer lacks a cleanup mechanism. Since it uses a Handler tied to the main Looper and stores action lambdas that may capture lifecycle-scoped contexts (Activity/Fragment/Service), pending callbacks can execute after the owning component is destroyed, potentially causing memory leaks or crashes.
Consider adding a cancel() or clear() method:
🔎 Suggested lifecycle-safe implementation
internal class Debouncer {
private val handler = Handler(Looper.getMainLooper())
private var runnable: Runnable? = null
fun submit(delayMs: Long, action: () -> Unit) {
runnable?.let { handler.removeCallbacks(it) }
runnable = Runnable { action() }
runnable?.let {
handler.postDelayed(it, delayMs)
}
}
+
+ fun cancel() {
+ runnable?.let { handler.removeCallbacks(it) }
+ runnable = null
+ }
}Alternatively, consider using a coroutine-based approach with structured concurrency for automatic lifecycle management:
internal class Debouncer(private val scope: CoroutineScope) {
private var job: Job? = null
fun submit(delayMs: Long, action: () -> Unit) {
job?.cancel()
job = scope.launch {
delay(delayMs)
action()
}
}
fun cancel() {
job?.cancel()
job = null
}
}🤖 Prompt for AI Agents
In
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/Debouncer.kt
around lines 22 to 35, the Debouncer currently posts Runnables to the main
Handler but lacks lifecycle cleanup, risking leaks or callbacks after a
component is destroyed; add a public cancel() (or clear()) method that calls
runnable?.let { handler.removeCallbacks(it) }, sets runnable = null, and ensure
callers (Activity/Fragment/Service) invoke this in their onDestroy/onCleared;
optionally provide/overload a close() or finalize-like API if used with
lifecycle owners, or replace with a CoroutineScope-backed implementation where
submit cancels the previous Job and cancel() cancels the job and nullls it.
|
|
||
| call.leave("LeaveCallBroadcastReceiver") | ||
| val notificationId = intent.getIntExtra(INTENT_EXTRA_NOTIFICATION_ID, 0) | ||
| logger.d { "[onReceive], notificationId: notificationId" } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix string interpolation in log statement.
The log statement is missing the $ prefix for variable interpolation. It will log the literal string "notificationId" instead of the actual value.
🔎 Proposed fix
- logger.d { "[onReceive], notificationId: notificationId" }
+ logger.d { "[onReceive], notificationId: $notificationId" }📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| logger.d { "[onReceive], notificationId: notificationId" } | |
| logger.d { "[onReceive], notificationId: $notificationId" } |
🤖 Prompt for AI Agents
In
stream-video-android-core/src/main/kotlin/io/getstream/video/android/core/notifications/internal/receivers/LeaveCallBroadcastReceiver.kt
around line 42, the log currently prints the literal "notificationId" instead of
the variable; update the log string to interpolate the variable (e.g., change to
use $notificationId or ${notificationId} inside the string) so the actual
notificationId value is logged.
| val serviceScope: CoroutineScope = | ||
| CoroutineScope(Dispatchers.IO.limitedParallelism(1) + handler + SupervisorJob()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Critical: Shared companion scope cancelled by instance lifecycle.
The serviceScope is a companion object shared across all CallService instances and subclasses, but it's cancelled in onDestroy() (line 559) and internalStopServiceGracefully() (line 618). This will prematurely cancel coroutines for other active service instances.
Each service instance should have its own scope tied to its lifecycle, not a shared static scope.
🔎 Suggested fix
- val serviceScope: CoroutineScope =
- CoroutineScope(Dispatchers.IO.limitedParallelism(1) + handler + SupervisorJob())Move the scope to instance level:
+private lateinit var serviceScope: CoroutineScope
+
override fun onCreate() {
super.onCreate()
+ serviceScope = CoroutineScope(Dispatchers.IO.limitedParallelism(1) + handler + SupervisorJob())
serviceState.startTime = OffsetDateTime.now()
}Committable suggestion skipped: line range outside the PR's diff.
| @Test | ||
| fun `justNotify does nothing when permission is denied`() { | ||
| every { | ||
| ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS) | ||
| } returns PackageManager.PERMISSION_DENIED | ||
|
|
||
| val streamVideo = mockk<StreamVideo>(relaxed = true) | ||
| every { StreamVideo.instanceOrNull() } returns streamVideo | ||
| every { streamVideo.getStreamNotificationDispatcher() } returns notificationDispatcher | ||
|
|
||
| sut.justNotify( | ||
| service = service, | ||
| callId = callId, | ||
| notificationId = 1001, | ||
| notification = notification, | ||
| ) | ||
|
|
||
| verify { notificationDispatcher wasNot Called } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fix incorrect variable reference.
Line 104 references an undefined context variable. The test setup only initializes service. This will cause a compilation or runtime error.
🔎 Proposed fix
@Test
fun `justNotify does nothing when permission is denied`() {
every {
- ContextCompat.checkSelfPermission(context, Manifest.permission.POST_NOTIFICATIONS)
+ ContextCompat.checkSelfPermission(service, Manifest.permission.POST_NOTIFICATIONS)
} returns PackageManager.PERMISSION_DENIED🤖 Prompt for AI Agents
In
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/managers/CallServiceNotificationManagerTest.kt
around lines 101 to 119, the test references an undefined context variable when
stubbing ContextCompat.checkSelfPermission; update the test to use the Service's
context (e.g., service.applicationContext or service as Context) or initialize a
local context val from service before stubbing, and then use that concrete
context in the every { ContextCompat.checkSelfPermission(...) } call so the test
compiles and correctly simulates the permission denial.
| // @Test | ||
| fun `cancelNotifications is safe when callId is null`() { | ||
| // sut.cancelNotifications(service, null) | ||
|
|
||
| // verify { notificationManagerCompat wasNot Called } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove or complete the commented-out test.
This test is entirely commented out. If the scenario is not needed, remove it. If it's needed, complete the implementation.
🤖 Prompt for AI Agents
In
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/managers/CallServiceNotificationManagerTest.kt
around lines 185-190, the test function `cancelNotifications is safe when callId
is null` is fully commented out; either remove the dead test or restore and
implement it: uncomment the test, call sut.cancelNotifications(service, null)
and assert/verify that notificationManagerCompat was not called (consistent with
other tests' mocking/verify style), and ensure the test has the @Test annotation
and necessary mocks/setup; if the scenario is not required, delete the commented
block.
| /** | ||
| * Ignored because of unknown reason of failure, it will fail when running all tests | ||
| */ | ||
| @Test | ||
| fun `unregisterToggleCameraBroadcastReceiver unregisters receiver`() { | ||
| println("[unregisterToggleCameraBroadcastReceiver unregisters receiver]") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
cat -n stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/models/ServiceStateTest.kt | sed -n '100,115p'Repository: GetStream/stream-video-android
Length of output: 712
Remove debug code and properly annotate or fix the flaky test.
The test at line 107 includes a comment indicating it fails intermittently when running all tests, but lacks an @Ignore annotation. Additionally, the println statement at line 108 should be removed.
Either:
- Add
@Ignorewith a reason documenting the known issue, or - Investigate and fix the root cause of the intermittent failure
/**
* Ignored because of unknown reason of failure, it will fail when running all tests
*/
+ @Ignore("Intermittent failure when running all tests - needs investigation")
@Test
fun `unregisterToggleCameraBroadcastReceiver unregisters receiver`() {
- println("[unregisterToggleCameraBroadcastReceiver unregisters receiver]")
val sut = ServiceState()📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| /** | |
| * Ignored because of unknown reason of failure, it will fail when running all tests | |
| */ | |
| @Test | |
| fun `unregisterToggleCameraBroadcastReceiver unregisters receiver`() { | |
| println("[unregisterToggleCameraBroadcastReceiver unregisters receiver]") | |
| /** | |
| * Ignored because of unknown reason of failure, it will fail when running all tests | |
| */ | |
| @Ignore("Intermittent failure when running all tests - needs investigation") | |
| @Test | |
| fun `unregisterToggleCameraBroadcastReceiver unregisters receiver`() { | |
| val sut = ServiceState() |
🤖 Prompt for AI Agents
In
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/models/ServiceStateTest.kt
around lines 103 to 108, remove the debug println and either properly mark the
flaky test with @Ignore including a concise reason for the failure, or
investigate and fix the intermittent failure; the immediate action should be to
delete the println("[unregisterToggleCameraBroadcastReceiver unregisters
receiver]") and add @Ignore("flaky when running full test suite - tracked by
ISSUE-<id> or brief cause") above the test to prevent false CI failures, or
alternatively reproduce and correct the underlying timing/resource issue so the
test can remain enabled.
| fun `buildStopIntent returns intent with stop flag when service is running`() { | ||
| // given | ||
| val builder = spyk(ServiceIntentBuilder()) | ||
|
|
||
| // given | ||
| val serviceClass = CallService::class.java | ||
| val config = CallServiceConfig(serviceClass = serviceClass) | ||
| val param = StopServiceParam( | ||
| callServiceConfiguration = config, | ||
| call = null, | ||
| ) | ||
|
|
||
| // When | ||
| val intent = ServiceIntentBuilder().buildStopIntent( | ||
| context, | ||
| StopServiceParam(callServiceConfiguration = customConfig), | ||
| every { | ||
| builder.isServiceRunning(context, serviceClass) | ||
| } returns true | ||
|
|
||
| // when | ||
| val intent = builder.buildStopIntent(context, param) | ||
|
|
||
| // then | ||
| assertNotNull(intent) | ||
| assertEquals(serviceClass.name, intent!!.component?.className) | ||
| assertTrue(intent.getBooleanExtra(CallService.EXTRA_STOP_SERVICE, false)) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove duplicate "given" comment.
There are two consecutive "given" comments (lines 168 and 171). Remove one for consistency.
🔎 Proposed fix
fun `buildStopIntent returns intent with stop flag when service is running`() {
- // given
val builder = spyk(ServiceIntentBuilder())
// given📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun `buildStopIntent returns intent with stop flag when service is running`() { | |
| // given | |
| val builder = spyk(ServiceIntentBuilder()) | |
| // given | |
| val serviceClass = CallService::class.java | |
| val config = CallServiceConfig(serviceClass = serviceClass) | |
| val param = StopServiceParam( | |
| callServiceConfiguration = config, | |
| call = null, | |
| ) | |
| // When | |
| val intent = ServiceIntentBuilder().buildStopIntent( | |
| context, | |
| StopServiceParam(callServiceConfiguration = customConfig), | |
| every { | |
| builder.isServiceRunning(context, serviceClass) | |
| } returns true | |
| // when | |
| val intent = builder.buildStopIntent(context, param) | |
| // then | |
| assertNotNull(intent) | |
| assertEquals(serviceClass.name, intent!!.component?.className) | |
| assertTrue(intent.getBooleanExtra(CallService.EXTRA_STOP_SERVICE, false)) | |
| } | |
| fun `buildStopIntent returns intent with stop flag when service is running`() { | |
| val builder = spyk(ServiceIntentBuilder()) | |
| // given | |
| val serviceClass = CallService::class.java | |
| val config = CallServiceConfig(serviceClass = serviceClass) | |
| val param = StopServiceParam( | |
| callServiceConfiguration = config, | |
| call = null, | |
| ) | |
| every { | |
| builder.isServiceRunning(context, serviceClass) | |
| } returns true | |
| // when | |
| val intent = builder.buildStopIntent(context, param) | |
| // then | |
| assertNotNull(intent) | |
| assertEquals(serviceClass.name, intent!!.component?.className) | |
| assertTrue(intent.getBooleanExtra(CallService.EXTRA_STOP_SERVICE, false)) | |
| } |
🤖 Prompt for AI Agents
In
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceIntentBuilderTest.kt
around lines 167 to 190, there are two consecutive "given" comments; remove the
duplicate so only one "given" comment remains before the setup block to keep
comments consistent and uncluttered.
| fun `buildStopIntent attaches call cid when call is present`() { | ||
| // given | ||
| val builder = spyk(ServiceIntentBuilder()) | ||
|
|
||
| // given | ||
| val serviceClass = CallService::class.java | ||
| val config = CallServiceConfig(serviceClass = serviceClass) | ||
|
|
||
| val call = mockk<Call> { | ||
| every { type } returns "default" | ||
| every { id } returns "123" | ||
| every { cid } returns "default:123" | ||
| } | ||
|
|
||
| val param = StopServiceParam( | ||
| callServiceConfiguration = config, | ||
| call = call, | ||
| ) | ||
|
|
||
| // Then | ||
| every { | ||
| builder.isServiceRunning(context, serviceClass) | ||
| } returns true | ||
|
|
||
| // when | ||
| val intent = builder.buildStopIntent(context, param) | ||
|
|
||
| // then | ||
| assertNotNull(intent) | ||
| // Note: The actual implementation has some complex logic for running services | ||
| // so we just verify the intent is created | ||
|
|
||
| val streamCallId = | ||
| intent!!.getParcelableExtra<StreamCallId>(INTENT_EXTRA_CALL_CID) | ||
|
|
||
| assertNotNull(streamCallId) | ||
| assertEquals("default", streamCallId!!.type) | ||
| assertEquals("123", streamCallId.id) | ||
| assertEquals("default:123", streamCallId.cid) | ||
|
|
||
| assertTrue(intent.getBooleanExtra(CallService.EXTRA_STOP_SERVICE, false)) | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remove duplicate "given" comment.
There are two consecutive "given" comments (lines 194 and 197). Remove one for consistency.
🔎 Proposed fix
fun `buildStopIntent attaches call cid when call is present`() {
- // given
val builder = spyk(ServiceIntentBuilder())
// given📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| fun `buildStopIntent attaches call cid when call is present`() { | |
| // given | |
| val builder = spyk(ServiceIntentBuilder()) | |
| // given | |
| val serviceClass = CallService::class.java | |
| val config = CallServiceConfig(serviceClass = serviceClass) | |
| val call = mockk<Call> { | |
| every { type } returns "default" | |
| every { id } returns "123" | |
| every { cid } returns "default:123" | |
| } | |
| val param = StopServiceParam( | |
| callServiceConfiguration = config, | |
| call = call, | |
| ) | |
| // Then | |
| every { | |
| builder.isServiceRunning(context, serviceClass) | |
| } returns true | |
| // when | |
| val intent = builder.buildStopIntent(context, param) | |
| // then | |
| assertNotNull(intent) | |
| // Note: The actual implementation has some complex logic for running services | |
| // so we just verify the intent is created | |
| val streamCallId = | |
| intent!!.getParcelableExtra<StreamCallId>(INTENT_EXTRA_CALL_CID) | |
| assertNotNull(streamCallId) | |
| assertEquals("default", streamCallId!!.type) | |
| assertEquals("123", streamCallId.id) | |
| assertEquals("default:123", streamCallId.cid) | |
| assertTrue(intent.getBooleanExtra(CallService.EXTRA_STOP_SERVICE, false)) | |
| } | |
| fun `buildStopIntent attaches call cid when call is present`() { | |
| val builder = spyk(ServiceIntentBuilder()) | |
| // given | |
| val serviceClass = CallService::class.java | |
| val config = CallServiceConfig(serviceClass = serviceClass) | |
| val call = mockk<Call> { | |
| every { type } returns "default" | |
| every { id } returns "123" | |
| every { cid } returns "default:123" | |
| } | |
| val param = StopServiceParam( | |
| callServiceConfiguration = config, | |
| call = call, | |
| ) | |
| every { | |
| builder.isServiceRunning(context, serviceClass) | |
| } returns true | |
| // when | |
| val intent = builder.buildStopIntent(context, param) | |
| // then | |
| assertNotNull(intent) | |
| val streamCallId = | |
| intent!!.getParcelableExtra<StreamCallId>(INTENT_EXTRA_CALL_CID) | |
| assertNotNull(streamCallId) | |
| assertEquals("default", streamCallId!!.type) | |
| assertEquals("123", streamCallId.id) | |
| assertEquals("default:123", streamCallId.cid) | |
| assertTrue(intent.getBooleanExtra(CallService.EXTRA_STOP_SERVICE, false)) | |
| } |
🤖 Prompt for AI Agents
In
stream-video-android-core/src/test/kotlin/io/getstream/video/android/core/notifications/internal/service/ServiceIntentBuilderTest.kt
around lines 193 to 231, remove the duplicate "given" comment so there's only
one "given" heading before the setup block; simply delete the redundant comment
(either the first or second) to keep the test comments consistent and concise
while leaving the rest of the test code unchanged.


Goal
Implementation
Core Changes
Core classes
-Since we will use same service to transition from ringing to active so we need to store a reference of original notification Id
- Process
LocalCallMissedEventhere so that it will work for calls which are not activeService classes refactoring
Util Classes
Testing
Fix FG Service Notification dismiss from Notification Tray
Service lifecycle
Summary by CodeRabbit
New Features
Bug Fixes
✏️ Tip: You can customize this high-level summary in your review settings.