Skip to content

fix(android): avoid blocking resource loads in connection status#6398

Draft
jeremiah-k wants to merge 6 commits into
meshtastic:mainfrom
jeremiah-k:bugfix/android-compose-resource-anr
Draft

fix(android): avoid blocking resource loads in connection status#6398
jeremiah-k wants to merge 6 commits into
meshtastic:mainfrom
jeremiah-k:bugfix/android-compose-resource-anr

Conversation

@jeremiah-k

@jeremiah-k jeremiah-k commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Overview

This removes blocking Compose resource work from two connection-status paths that can overlap during normal Android lifecycle transitions.

The observed ANR had the Connections screen waiting in Compose resource loading while foreground-service telemetry formatting was resolving Compose resources at the same time. The UI lookup lived inside frequently re-entered animated content, while the service notification path used blocking non-composable resource helpers.

This change resolves connected-device display text outside the animated subtree and moves service-notification formatting onto the existing service coroutine scope.

Review Guide

The commits are intended to be reviewed in order:

  1. Leaf labels — allows signal and battery components to receive caller-resolved text.
  2. Connections composition — resolves connected-device display values in a stable parent composition slot.
  3. Service notifications — renders localized notification state asynchronously with generation and lock safety.

Key Changes

Connection UI

  • Allow Rssi and MaterialBatteryInfo to accept caller-provided labels.
  • Introduce an immutable CurrentlyConnectedText value for localized connected-device text.
  • Resolve RSSI, unknown-battery, disconnect, and firmware-version text outside AnimatedContent.
  • Keep the animated connected-device subtree free of Compose resource lookups.

Service Notifications

  • Use the Android application label as an immediate startForeground fallback.
  • Replace blocking resource helpers with suspend-based formatting.
  • Capture connection and telemetry state into generation-tagged render snapshots.
  • Cancel superseded renders and reject stale results.
  • Preserve the last telemetry message when newer state has no replacement metrics.
  • Update cached state under serviceNotificationLock, then release the lock before posting through NotificationManager.
  • Preserve cancellation and provide a safe fallback when localized rendering fails.

Testing

Added or updated coverage for:

  • caller-provided RSSI and unknown-battery labels;
  • immediate foreground-notification fallback behavior;
  • deferred service-state rendering;
  • cancellation of superseded notification renders;
  • valid oneof telemetry fixtures;
  • notification-channel cleanup and active-notification behavior.

Runtime validation repeatedly backgrounded, foregrounded, stopped, and restarted MainActivity while service telemetry continued to update. The original input-timeout and Compose resource-loading stall did not recur, and the capture contains no notification-render exception, ANR, or process restart.

Migration Notes

  • No user data migration is required.
  • Notification channels and IDs are unchanged.
  • Connection-state and telemetry semantics are unchanged.
  • The constructor change remains internal to the source-built application and its dependency graph.

Summary by CodeRabbit

  • New Features

    • Added customizable labels for RSSI and unknown battery values.
    • Connections screen now standardizes “currently connected” text (including optional firmware version).
    • Foreground service notifications now use safer, faster fallback content while rendering updates.
  • Bug Fixes

    • Prevented older service-state renders from overwriting newer notification content.
    • Improved notification update timing behavior: deferred rendering and proper cancellation on newer updates.
  • Tests

    • Added UI tests for caller-provided labels.
    • Updated notification tests to use render-scoped coroutine timing and cover fallback/deferred/cancellation behavior.

@coderabbitai

coderabbitai Bot commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

The change introduces asynchronous, generation-safe service notification rendering with coroutine-scoped tests. It also allows connected-device UI labels to be resolved by the parent screen and passed into reusable RSSI, battery, and connection components.

Changes

Service notification rendering

Layer / File(s) Summary
Generation-safe notification pipeline
core/service/src/androidMain/kotlin/.../MeshNotificationManagerImpl.kt
Service notifications render asynchronously from captured snapshots, commit only the latest generation, and provide an immediate fallback notification.
Suspend notification formatting
core/service/src/androidMain/kotlin/.../MeshNotificationManagerImpl.kt
Local statistics and device metrics use suspend resource-formatting helpers.
Coroutine lifecycle and rendering tests
core/testing/src/commonMain/kotlin/.../TestScopes.kt, core/service/src/androidHostTest/kotlin/.../MeshNotificationManagerImpl*Test.kt
Tests add scoped coroutine cleanup and verify fallback, deferred rendering, and latest-state notification behavior.

Connected-device UI labels

Layer / File(s) Summary
Caller-provided component labels
core/ui/src/commonMain/kotlin/.../LoraSignalIndicator.kt, core/ui/src/commonMain/kotlin/.../MaterialBatteryInfo.kt, core/ui/src/commonTest/kotlin/.../LoraSignalIndicatorUiTest.kt
RSSI and battery components accept supplied labels, with Compose tests covering custom label rendering.
Precomputed connected-device text
feature/connections/src/commonMain/kotlin/.../ConnectionsScreen.kt, feature/connections/src/commonMain/kotlin/.../components/CurrentlyConnectedInfo.kt
ConnectionsScreen resolves labels and firmware text outside animated content and passes them through CurrentlyConnectedText.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ServiceState
  participant MeshNotificationManagerImpl
  participant ServiceScope
  participant AndroidNotificationManager
  ServiceState->>MeshNotificationManagerImpl: updateServiceStateNotification()
  MeshNotificationManagerImpl->>ServiceScope: launch lazy render job
  ServiceScope->>MeshNotificationManagerImpl: render captured snapshot
  MeshNotificationManagerImpl->>AndroidNotificationManager: commit latest notification
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the main change: fixing Android connection-status paths by avoiding blocking resource loads.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added the bugfix PR tag label Jul 24, 2026
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 24, 2026 01:29

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt (1)

75-75: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use a deterministic test dispatcher here.

updateServiceStateNotification() runs async render work through getStringSuspend(...), but these tests assert immediately after calling it under Dispatchers.Unconfined; if resource resolution resumes off the current thread, the assertions become order-dependent. Move this test to StandardTestDispatcher + advanceUntilIdle() or otherwise advance the renderer before asserting.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt`
at line 75, Update the notificationScope in
MeshNotificationManagerImplConversationTest to use a deterministic
StandardTestDispatcher-backed test scope instead of Dispatchers.Unconfined, and
advance the dispatcher with advanceUntilIdle() after
updateServiceStateNotification() before assertions so asynchronous
getStringSuspend rendering completes deterministically.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt`:
- Around line 139-145: Update the CurrentlyConnectedText preview configuration
to replace the hardcoded “Unknown”, “RSSI”, and “Disconnect” values with
localized core:resources entries resolved through stringResource(Res.string.*),
while leaving the remaining preview setup unchanged.

---

Nitpick comments:
In
`@core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt`:
- Line 75: Update the notificationScope in
MeshNotificationManagerImplConversationTest to use a deterministic
StandardTestDispatcher-backed test scope instead of Dispatchers.Unconfined, and
advance the dispatcher with advanceUntilIdle() after
updateServiceStateNotification() before assertions so asynchronous
getStringSuspend rendering completes deterministically.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8a2cf68b-ca27-4f0a-94c3-84016e17945a

📥 Commits

Reviewing files that changed from the base of the PR and between a82d0e2 and a3544a4.

📒 Files selected for processing (8)
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt
  • core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt
  • core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicatorUiTest.kt
  • feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt
  • feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt

@jeremiah-k
jeremiah-k marked this pull request as draft July 24, 2026 01:39
…ryInfo

Add optional label parameters to Rssi and MaterialBatteryInfo composables,
enabling callers to supply pre-resolved display text without changing
existing invocations. This decouples animated composition slots from the
Compose Multiplatform resource lookup that can block the main thread.
Move Rssi, battery, firmware-version, and disconnect-text resolution out of
the animated connected-device subtree into stable ConnectionsScreen
composition slots. Introduces CurrentlyConnectedText as an immutable value
object to pass pre-computed display strings through the animation scope.
Move service-state title and telemetry formatting onto ServiceScope with getStringSuspend so notification rendering never blocks callers on Compose resource loading.

Capture immutable render snapshots, install lazy jobs before starting them, cancel superseded work, and guard cache publication by generation. Preserve the latest message when telemetry is absent and fall back to the Android application label for immediate foreground startup.

Keep render state and job bookkeeping under the service notification lock, but perform NotificationManager Binder calls only after releasing it so foreground notification reads cannot be delayed by IPC.

Use valid Wire oneof telemetry fixtures and a deterministic node-repository fallback, and centralize render-scope cleanup across the Android host regressions.
Resolve "Unknown", "RSSI", and "Disconnect" through stringResource() in the
CurrentlyConnectedInfo preview so it reflects the production UI and follows
the user-facing-strings convention.
Replace the class-level Dispatchers.Unconfined notificationScope with a
per-test StandardTestDispatcher-backed scope managed by runWithRenderScope,
matching the sibling MeshNotificationManagerImplTest pattern.

advanceUntilIdle() after updateMessageNotification / updateServiceStateNotification
ensures getStringSuspend render jobs complete before the assertions
inspect active notifications, removing the order-dependence that let the
test pass under Unconfined but fail when resource resolution resumed off
the current thread.
@jeremiah-k
jeremiah-k force-pushed the bugfix/android-compose-resource-anr branch from a3544a4 to ae22f8b Compare July 24, 2026 02:41
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 24, 2026 11:06

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt`:
- Around line 100-107: Move the duplicated runWithRenderScope helper from
MeshNotificationManagerImplConversationTest and MeshNotificationManagerImplTest
into the core:testing module, preserving its TestScope, CoroutineScope,
dispatcher, SupervisorJob, and cancellation behavior. Expose the shared utility
and import it from both tests, removing their local helper definitions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 85355b3f-69d7-4e4e-9997-405967f2a33b

📥 Commits

Reviewing files that changed from the base of the PR and between a3544a4 and ae22f8b.

📒 Files selected for processing (8)
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt
  • core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt
  • core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicatorUiTest.kt
  • feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt
  • feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt
🚧 Files skipped from review as they are similar to previous changes (7)
  • core/ui/src/commonTest/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicatorUiTest.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/MaterialBatteryInfo.kt
  • core/ui/src/commonMain/kotlin/org/meshtastic/core/ui/component/LoraSignalIndicator.kt
  • feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/ConnectionsScreen.kt
  • feature/connections/src/commonMain/kotlin/org/meshtastic/feature/connections/ui/components/CurrentlyConnectedInfo.kt
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt
  • core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt

Comment on lines +100 to +107
private fun runWithRenderScope(block: suspend TestScope.(CoroutineScope) -> Unit) = runTest {
val scope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob())
try {
block(scope)
} finally {
scope.cancel()
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Centralize the duplicated render-scope helper.

runWithRenderScope is duplicated in MeshNotificationManagerImplTest.kt; move it into core:testing and import it from both tests. As per coding guidelines, “Place reusable test fakes, doubles, and utilities in core:testing.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt`
around lines 100 - 107, Move the duplicated runWithRenderScope helper from
MeshNotificationManagerImplConversationTest and MeshNotificationManagerImplTest
into the core:testing module, preserving its TestScope, CoroutineScope,
dispatcher, SupervisorJob, and cancellation behavior. Expose the shared utility
and import it from both tests, removing their local helper definitions.

Source: Coding guidelines

@jeremiah-k
jeremiah-k marked this pull request as draft July 24, 2026 11:11
Guard notificationManager.notify() in the render-catch path with safeCatching so a failure while posting the degraded fallback cannot escape the render handler and cancel sibling render jobs running under the SupervisorJob.

Preserve the primary rendered-notify path unchanged. Centralize the deterministic supervised render-scope fixture in core:testing so both notification suites share the same test scheduler, lifecycle, and guaranteed cancellation behavior.
@jeremiah-k
jeremiah-k force-pushed the bugfix/android-compose-resource-anr branch from ae22f8b to 3e939c2 Compare July 24, 2026 13:19
@jeremiah-k
jeremiah-k marked this pull request as ready for review July 24, 2026 15:48

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In
`@core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestScopes.kt`:
- Around line 27-32: Update runWithRenderScope so the renderScope job is
parented to the runTest coroutine context instead of creating an independent
SupervisorJob, or otherwise explicitly await its rendered work before the
finally block cancels it. Ensure render-scope coroutine failures remain
observable and runTest waits for all launched work.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 85be81a0-6d01-4ac4-bcc9-90954d664bbb

📥 Commits

Reviewing files that changed from the base of the PR and between ae22f8b and 3e939c2.

📒 Files selected for processing (4)
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt
  • core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt
  • core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestScopes.kt
🚧 Files skipped from review as they are similar to previous changes (3)
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplTest.kt
  • core/service/src/androidHostTest/kotlin/org/meshtastic/core/service/MeshNotificationManagerImplConversationTest.kt
  • core/service/src/androidMain/kotlin/org/meshtastic/core/service/MeshNotificationManagerImpl.kt

Comment on lines +27 to +32
fun runWithRenderScope(block: suspend TestScope.(CoroutineScope) -> Unit) = runTest {
val renderScope = CoroutineScope(StandardTestDispatcher(testScheduler) + SupervisorJob())
try {
block(renderScope)
} finally {
renderScope.cancel()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n -C3 'runWithRenderScope|SupervisorJob\(' .
rg -n -C2 'kotlinx-coroutines-test|coroutines' gradle/libs.versions.toml

Repository: meshtastic/Meshtastic-Android

Length of output: 50385


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== TestScopes.kt =="
sed -n '1,80p' core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestScopes.kt

echo
echo "== runWithRenderScope calls (first 80 matches) =="
rg -n 'runWithRenderScope|renderScope|launch|asStateFlow|StateFlow' core/service/src/androidHostTest/core/service/src/commonTest 2>/dev/null | head -n 120 || true

echo
echo "== core/testing build.gradle variants =="
for f in core/testing/build.gradle* core/testing/build.gradle.kts; do
  [ -f "$f" ] && { echo "--- $f"; sed -n '1,160p' "$f"; }
done

echo
echo "== coroutines version aliases =="
sed -n '1,220p' gradle/libs.versions.toml | rg -n -C2 'coroutines|kotlinx|compose-multiplatform' || true

Repository: meshtastic/Meshtastic-Android

Length of output: 8169


🌐 Web query:

kotlinx.coroutines.test runTest cancels any coroutines launched in the scope Job parented by parent job

💡 Result:

In kotlinx.coroutines.test, the runTest function does not automatically cancel all coroutines simply because they are parented by a job passed to it. Understanding the behavior: 1. Test Completion Requirement: By design, runTest requires all coroutines launched within the test body to complete before it returns [1][2]. If active, uncompleted coroutines remain after the test body finishes, runTest will fail the test (typically by throwing an AssertionError) [1][3]. This behavior is intended to prevent resource leaks and ensure predictable test outcomes [4]. 2. Job Parentage: When you provide a parent Job to runTest, the test scope becomes a child of that job [5][6]. However, runTest does not implicitly cancel that parent job or its other children upon test completion [5]. It only tracks and waits for the completion of the coroutines launched within its own test scope [1][7]. 3. Handling Never-Ending Coroutines: If your test involves coroutines that are intentionally long-running or never-ending (e.g., background observers), you should use TestScope.backgroundScope [1][2]. Coroutines launched in backgroundScope are automatically cancelled when the test finishes [3][8]. This is the recommended approach for tasks that outlive the tested code, rather than relying on the parent job's lifecycle [7][4]. 4. Timeout and Failure: runTest includes a default timeout (60 seconds) [2][8]. If the test takes longer, it will cancel the test coroutine to prevent hanging [1][2]. If the test body itself fails, child coroutines are cancelled as part of standard structured concurrency behavior [5]. In summary, runTest enforces completion of launched coroutines but does not perform cleanup of external or parent-provided jobs [1][5]. You must manage the lifecycle of such jobs explicitly or utilize backgroundScope for background tasks [7][3].

Citations:


Parent the render scope to runTest or make failures observable.

SupervisorJob() gives renderScope its own root job, so coroutines launched in it are not part of runTest’s wait-for-completion path, and render exceptions can be swallowed if callers don’t handle them. Parent the job to the test scope, or wait for/rendered work to complete before the finally { renderScope.cancel() }.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@core/testing/src/commonMain/kotlin/org/meshtastic/core/testing/TestScopes.kt`
around lines 27 - 32, Update runWithRenderScope so the renderScope job is
parented to the runTest coroutine context instead of creating an independent
SupervisorJob, or otherwise explicitly await its rendered work before the
finally block cancels it. Ensure render-scope coroutine failures remain
observable and runTest waits for all launched work.

@jeremiah-k
jeremiah-k marked this pull request as draft July 24, 2026 15:52
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix PR tag

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant