Skip to content

Conversation

georgezy-amzn
Copy link
Collaborator

@georgezy-amzn georgezy-amzn commented Aug 26, 2025

ℹ️ Description

  • Publish appStateChanged/appMemoryLow events
  • Publish appState event attribute

Issue #, if available

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update
    • README update
    • CHANGELOG update
    • guides update
  • This change requires a dependency update
    • Amazon Chime SDK Media
    • Other (update corresponding legal documents)

🧪 How Has This Been Tested?

  • Tested with demo on real device, verifying if app enter foreground/background events are published
  • Unit tests added for the change

Unit test coverage

  • Class coverage:
  • Line coverage:

Additional Manual Test

  • Pause and resume remote video
  • Switch local camera
  • Rotate screen back and forth

📱 Screenshots, if available

provide screenshots/video record if there's a UI change in demo app

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

GlobalScope.launch {
audioClient?.stopSession()
appLifecycleObserver.stopObserving()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this seem like an odd place to put this? No higher level observer?

Copy link
Collaborator Author

@georgezy-amzn georgezy-amzn Aug 26, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this is the highest level we can get, an alternative is to monitor the state change of DefaultAudioClientController.state, but I don't like it.

@@ -570,8 +572,10 @@ class DefaultAudioClientObserver(

private fun handleAudioClientStop(statusCode: MeetingSessionStatusCode?) {
if (audioClient != null) {
// TODO: assess if only notifyAudioClientObserver should be in GlobalScope.launch
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about this, I think GlobalScope is a reasonable default, though it would be nice to allow configuration.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm OK with GlobalScope, what I mean is we should only have the notifyAudioClientObserver logic in the GlobalScope, the rest of the logic should stay on the same thread as where handleAudioClientStop is called, so that we can reduce the async overheads, but I need to think about it before making the change.

The overall code should look like this:

private fun handleAudioClientStop(statusCode: MeetingSessionStatusCode?) {
        if (audioClient != null) {
            audioClient?.stopSession()
            appLifecycleObserver.stopObserving()
            DefaultAudioClientController.audioClientState = AudioClientState.STOPPED
            GlobalScope.launch {
                notifyAudioClientObserver { observer ->
                    observer.onAudioSessionStopped(MeetingSessionStatus(statusCode))
                }
            }
        } else {
            logger.error(TAG, "Failed to stop audio session since audioClient is null")
        }
    }

import com.amazonaws.services.chime.sdk.meetings.analytics.MeetingHistoryEventName
import com.amazonaws.services.chime.sdk.meetings.utils.logger.Logger

class DefaultAppLifecycleObserver(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this be Monitor to differentiate from observer pattern?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, I don't have a preference.

class DefaultAppLifecycleObserver(
private val eventAnalyticsController: EventAnalyticsController,
private val logger: Logger
) : AppLifecycleObserver, DefaultLifecycleObserver {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we subscribe to other observers as well? https://developer.android.com/reference/androidx/lifecycle/DefaultLifecycleObserver

We could technically make this just one event type + attribute as well

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure

@georgezy-amzn georgezy-amzn changed the title Publish app enter foreground/background events Publish appStateChanged/appMemoryLow events Sep 2, 2025
private var eventAnalyticsObservers = ConcurrentSet.createConcurrentSet<EventAnalyticsObserver>()

override fun publishEvent(name: EventName, attributes: EventAttributes?) {
override fun publishEvent(name: EventName, attributes: EventAttributes?, notifyObservers: Boolean) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious, why is this optional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you mean attributes:EventAttributes?, my understanding is because not every event comes with attributes. Alternatively we can also use non-optional EventAttributes and set to empty map for default, I'm not sure why we didn't go this approach.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant notifyObservers

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Currently there are publishEvent() and pushEvent() in DefaultEventAnalyticsController, the only diff between them is that pushEvent() does not notify event observers, the rest of the logic is the same, so i'm tying to remove pushEvent(), and use publishEvent() for all events, adding notifyObservers in order to make publishEvent() have the same pattern as pushEvent().

@@ -16,7 +16,7 @@ interface EventAnalyticsController {
* @param name: [EventName] - Name of event to publish
* @param attributes: [EventAttributes] - Attributes of event to pass to builders.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs update, and if so probably explanation of why this may be false.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice catch!

package com.amazonaws.services.chime.sdk.meetings.ingestion

interface AppLifecycleObserver {
fun startObserving()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Lets make these start() and stop(). Having the word Observing in the API name sounds redundant.

eventReporter?.report(SDKEvent(name, eventAttributes))

ObserverUtils.notifyObserverOnMainThread(eventAnalyticsObservers) {
it.onEventReceived(name, eventAttributes)
if (notifyObservers) {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the reason for making notification optional?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the first step to replace pushHistory() with publishEvent() - the only different between those 2 functions is publishHistory() does not notify event observers, so we should merge those 2 functions(at least merge the logic if we still want to keep the signature). Having notification as optional will make the logic same(or closer) to pushHistory().

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe I missed the discussion, do we deprecate pushHistory? Why do we want to merge them?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no discussion, this is just a change I'm planning to make.

Why do we want to merge them?
Because those functions are doing almost the same thing except notifying the event observers, it's causing confusions and not necessary to have duplicate logics.

@@ -15,8 +15,9 @@ interface EventAnalyticsController {
*
* @param name: [EventName] - Name of event to publish
* @param attributes: [EventAttributes] - Attributes of event to pass to builders.
* @param notifyObservers: [Boolean] - Whether to notify `EventAnalyticsObserver` of the events.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll prefer the explanation be more use case oriented, i.e. - when am I recommended to notify EventAnalyticsObserver and when not.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do


private var handler: AppStateHandler? = null

// App states should be posted only when the meeting session is running
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: meeting session isn't the hard requirement right? builder still can post events when use this monitor alone

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a privacy concern, IMO it makes sense to monitor the states only when the session is running, if we monitor the app states all the time regardless if the session is running, it could lead to customer concerns. Builders can do whatever they want since they own the app.

appStateMonitor.start()
appStateMonitor.start()

verify(exactly = 2) { mockLogger.info(any(), "Started monitoring app state and memory") }
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These idempotent test cases are not how I exactly imagine - I think it should verify something only happen once even you call the API multiple time.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And wondering if any alternative for verification method instead of matching texts in log which is relatively less stable.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These idempotent test cases are not how I exactly imagine - I think it should verify something only happen once even you call the API multiple time.

  • I think idempotent means it will have the effect as it performed once when performed multiple times, it doesn't have to happen once, as long as the result is the same. In the start() logic, we are resetting the observers before subscribing, so the logger will be called twice.

And wondering if any alternative for verification method instead of matching texts in log which is relatively less stable.

  • I would say it's a more accurate verification by matching the texts, instead of less stable.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree "I think idempotent means it will have the effect as it performed once when performed multiple times", and that's why I feel the name is bit misleading to me, the effect is now 2 logs instead of 1 log. It is idempotent to me if only 1 log for multiple calls that prevent additional resources to be created. I'm good with naming like "start should resetting observers".

Stable here I meant if I update the log text, the test would fail unnecessarily, essentially the logic here isn't relying on the logging output but the API invocation. Ideally we could verify memoryHandler.post get called.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Make sense, will update!

|`contentShareSignalingDropped` |The content share WebSocket failed or closed with an error.
|`videoClientSignalingDropped` |The video client signaling websocket failed or closed with an error.
|`contentShareSignalingDropped` |The content share client signaling websocket failed or closed with an error.
|`appStateChanged` |The application state is changed.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to update. for detail attributes for the states?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We added appState attribute.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I meant do we need to list what are the available states?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will do

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants