Upsert Case — APIExample-Audio
Adding a New Case
Touch exactly 4 files (all paths relative to app/src/main/):
| File |
What to add |
java/.../examples/{basic|advanced|audio}/YourCaseName.java |
Fragment class |
res/layout/fragment_your_case_name.xml |
XML layout |
res/values/strings.xml |
2 strings |
res/navigation/nav_graph.xml |
1 action + 1 destination |
Registration is automatic via reflection — no other files needed.
voice-sdk constraint: Do NOT call enableVideo(), setupLocalVideo(), VideoCanvas, or any video API — the module does not exist and will crash at runtime.
Step 1: Clarify before coding
Before writing a single line, ask:
- What audio API am I demonstrating? — determines which existing case is the closest reference to copy patterns from
- BASIC or ADVANCED group? — BASIC for fundamental join/leave audio patterns; ADVANCED for feature-specific audio APIs
- What's the sort index? — index must be unique within the group. BASIC uses 0–9, ADVANCED starts from 10. Run
query-cases skill first; a collision causes silent ordering bugs at runtime
- Any special permissions beyond
RECORD_AUDIO? — most audio cases only need RECORD_AUDIO; check if the API requires anything else
Step 2: Create the Fragment
MANDATORY — READ ENTIRE FILE before writing any code:
references/fragment-template.java
Do NOT skip — the setParameters, handler.post, getPrivateCloudConfig() null-check, AudioSeatManager wiring, and voice-sdk constraints are only fully shown there and are required in every case.
Do NOT load any other reference files for this task.
Non-obvious points the template highlights:
setParameters(...) for app scenario reporting — required in every case, do not remove
handler.post(RtcEngine::destroy) — NOT RtcEngine.destroy() directly; direct call blocks UI thread (ANR)
getPrivateCloudConfig() null-check before setLocalAccessPoint() — returns null on non-private-cloud builds (NPE)
- All
IRtcEngineEventHandler callbacks run on a background thread — always runOnUIThread() for UI
onActivityCreated → create engine; onDestroy → leaveChannel() then handler.post(RtcEngine::destroy)
ChannelMediaOptions must NOT set publishCameraTrack or autoSubscribeVideo — voice-sdk has no video module
- Use
AudioSeatManager (not VideoReportLayout) to visualize remote participants
Step 3: Create the XML layout
Typical audio layout — channel input + join button + audio controls:
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fitsSystemWindows="true">
<!-- audio status / waveform view goes here -->
<LinearLayout
android:id="@+id/ll_join"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:gravity="center_vertical"
android:orientation="horizontal">
<androidx.appcompat.widget.AppCompatEditText
android:id="@+id/et_channel"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:digits="@string/chanel_support_char"
android:hint="@string/channel_id" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_join"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/join" />
</LinearLayout>
</RelativeLayout>
For waveform visualization, copy the WaveformView pattern from fragment_join_channel_audio.xml.
Step 4: Add nav entries
File: res/navigation/nav_graph.xml
Action — inside <fragment android:id="@+id/Ready"> (NOT mainFragment — mainFragment only has one action, to Ready):
<action
android:id="@+id/action_mainFragment_to_yourCaseName"
app:destination="@id/yourCaseName" />
Destination — at root <navigation> level:
<fragment
android:id="@+id/yourCaseName"
android:name="io.agora.api.example.examples.advanced.YourCaseName"
android:label="@string/item_your_case_name"
tools:layout="@layout/fragment_your_case_name" />
action android:id must exactly match actionId in @Example.
Step 5: Update ARCHITECTURE.md
Add one line to the case list in ARCHITECTURE.md under the correct directory section (basic/, advanced/, or audio/):
├── YourCaseName.java # [index] "Display Name" — key API description
Keep the format consistent with existing entries. This file is the fast-lookup index used by query-cases — keeping it current avoids full directory scans.
Modifying an Existing Case
When modifying an existing case rather than creating a new one, identify which files need changes based on what you are updating:
| What changed |
Files to touch |
| Implementation logic (API calls, event handling) |
java/.../examples/{basic|advanced|audio}/CaseName.java |
| UI layout (views, controls) |
res/layout/fragment_case_name.xml |
| Display name or tips text |
res/values/strings.xml |
| Sort index or group (BASIC ↔ ADVANCED) |
@Example annotation in the Fragment class |
| Navigation label |
res/navigation/nav_graph.xml (fragment label attribute) |
| Class rename or package move |
Fragment class, nav_graph.xml (android:name + destination id), @Example annotation (actionId), layout file name, ARCHITECTURE.md |
After making changes:
- Verify
@Example annotation consistency — ensure index, group, name, actionId, and tipsId still match the actual string resources, nav action ID, and intended group/position. A mismatch causes the case to silently disappear from the list or navigate to the wrong screen.
- Update
res/values/strings.xml if the display name or tips text changed.
- Update
res/navigation/nav_graph.xml if the class name, package, or label changed.
- Update
ARCHITECTURE.md — update the Directory Layout entry and the Case Index table row to reflect any changes to the case name, path, Key APIs, or description.
Verify
./gradlew assembleDebug
When to Use a Spec Instead
If the case meets any of the following criteria, create a Spec rather than using this skill directly:
- Involves coordinated calls across two or more Agora API modules
- Requires a custom UI layout (not one of the standard templates above)
- Manages multiple channels or multiple engine instances
- Requires a foreground Service or background thread coordination
- Involves developing new shared components (widget/utils, etc.)
- Requires optional module integration (e.g. streamEncrypt)
If none apply → use this skill directly; no Spec needed.
Spec Requirements Document Must Include
- List of APIs the case demonstrates (audio APIs only)
- User interaction flow description
- Expected RtcEngine lifecycle behavior
- Required permissions (typically only
RECORD_AUDIO)
Spec Design Document Must Include
- Target project identifier:
APIExample-Audio
- Class/file structure design
- API call sequence (Mermaid sequence diagram recommended)
- State management approach
- UI layout plan
- Integration points with existing shared components
- Case registration info: class name, display name, group (BASIC/ADVANCED), sort index — finalize during design to avoid conflicts
- Generate
@Example annotation parameters, nav_graph.xml action + destination, strings.xml key names (item_ prefix)
- Read
ARCHITECTURE.md or use the query-cases skill to check existing indices
- voice-sdk checks: no video APIs (
enableVideo, setupLocalVideo, setupRemoteVideo, VideoCanvas, startScreenCapture) — violations must be eliminated at design time
- Risk identification and mitigation (API availability, permissions, thread safety, performance)
Spec Task List Integration
- Mark which sub-tasks can be executed with this
upsert-case skill, and provide skill input parameters
- Mark which sub-tasks require manual coding, and provide target file paths and change summaries
- New shared component creation tasks must come before case implementation tasks
NEVER
- NEVER call any video API (
enableVideo, setupLocalVideo, VideoCanvas) — voice-sdk has no video module; crash is immediate.
- NEVER put the nav action inside
<fragment id="mainFragment"> — it belongs in <fragment id="Ready">. mainFragment only routes to Ready; all case actions live in Ready. Wrong placement causes silent navigation failure at runtime.
- NEVER call
RtcEngine.destroy() directly on the main thread — always handler.post(RtcEngine::destroy). Direct call blocks the UI thread and causes ANR.
- NEVER call
setLocalAccessPoint() without null-checking getPrivateCloudConfig() first — it returns null on standard builds, causing NPE.
- NEVER update UI directly inside
IRtcEngineEventHandler callbacks — they run on a background thread. Always wrap with runOnUIThread().
- NEVER omit
setParameters(...) — it's required for Agora backend usage reporting in every case; omitting it causes silent reporting failure even though the app appears to work normally.
1---2name: upsert-case-83description: Add a new audio API example case or modify an existing one in the APIExample-Audio Android demo — creates or updates Fragment class, XML layout, string resources, and nav_graph registration. Use when: adding a new Agora audio API demo screen, modifying an existing case's implementation or registration, implementing a new audio feature example in Java + XML layouts, registering a new case via @Example annotation, subclassing BaseFragment for a new audio demo screen, or updating an existing case's strings, layout, or nav entry. This project uses voice-sdk — no video APIs available. Keywords: add case, modify case, update case, new fragment, nav_graph, @Example, BaseFragment, APIExample-Audio, audio case, voice-sdk, new screen, audio demo, upsert case.4---56# Upsert Case — APIExample-Audio78## Adding a New Case910Touch exactly 4 files (all paths relative to `app/src/main/`):1112| File | What to add |13|---|---|14| `java/.../examples/{basic\|advanced\|audio}/YourCaseName.java` | Fragment class |15| `res/layout/fragment_your_case_name.xml` | XML layout |16| `res/values/strings.xml` | 2 strings |17| `res/navigation/nav_graph.xml` | 1 action + 1 destination |1819Registration is automatic via reflection — no other files needed.2021**voice-sdk constraint**: Do NOT call `enableVideo()`, `setupLocalVideo()`, `VideoCanvas`, or any video API — the module does not exist and will crash at runtime.2223---2425### Step 1: Clarify before coding2627Before writing a single line, ask:28- **What audio API am I demonstrating?** — determines which existing case is the closest reference to copy patterns from29- **BASIC or ADVANCED group?** — BASIC for fundamental join/leave audio patterns; ADVANCED for feature-specific audio APIs30- **What's the sort index?** — index must be unique within the group. BASIC uses 0–9, ADVANCED starts from 10. Run `query-cases` skill first; a collision causes silent ordering bugs at runtime31- **Any special permissions beyond `RECORD_AUDIO`?** — most audio cases only need `RECORD_AUDIO`; check if the API requires anything else3233---3435### Step 2: Create the Fragment3637**MANDATORY — READ ENTIRE FILE before writing any code**:38[`references/fragment-template.java`](references/fragment-template.java)3940Do NOT skip — the `setParameters`, `handler.post`, `getPrivateCloudConfig()` null-check, `AudioSeatManager` wiring, and voice-sdk constraints are only fully shown there and are required in every case.4142**Do NOT load** any other reference files for this task.4344Non-obvious points the template highlights:4546- `setParameters(...)` for app scenario reporting — **required in every case**, do not remove47- `handler.post(RtcEngine::destroy)` — NOT `RtcEngine.destroy()` directly; direct call blocks UI thread (ANR)48- `getPrivateCloudConfig()` null-check before `setLocalAccessPoint()` — returns null on non-private-cloud builds (NPE)49- All `IRtcEngineEventHandler` callbacks run on a **background thread** — always `runOnUIThread()` for UI50- `onActivityCreated` → create engine; `onDestroy` → `leaveChannel()` then `handler.post(RtcEngine::destroy)`51- `ChannelMediaOptions` must NOT set `publishCameraTrack` or `autoSubscribeVideo` — voice-sdk has no video module52- Use `AudioSeatManager` (not `VideoReportLayout`) to visualize remote participants5354---5556### Step 3: Create the XML layout5758Typical audio layout — channel input + join button + audio controls:5960```xml61<?xml version="1.0" encoding="utf-8"?>62<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"63 android:layout_width="match_parent"64 android:layout_height="match_parent"65 android:fitsSystemWindows="true">6667 <!-- audio status / waveform view goes here -->6869 <LinearLayout70 android:id="@+id/ll_join"71 android:layout_width="match_parent"72 android:layout_height="wrap_content"73 android:layout_alignParentBottom="true"74 android:gravity="center_vertical"75 android:orientation="horizontal">7677 <androidx.appcompat.widget.AppCompatEditText78 android:id="@+id/et_channel"79 android:layout_width="0dp"80 android:layout_height="wrap_content"81 android:layout_weight="1"82 android:digits="@string/chanel_support_char"83 android:hint="@string/channel_id" />8485 <androidx.appcompat.widget.AppCompatButton86 android:id="@+id/btn_join"87 android:layout_width="wrap_content"88 android:layout_height="wrap_content"89 android:text="@string/join" />90 </LinearLayout>91</RelativeLayout>92```9394For waveform visualization, copy the `WaveformView` pattern from `fragment_join_channel_audio.xml`.9596---9798### Step 4: Add nav entries99100File: `res/navigation/nav_graph.xml`101102**Action** — inside `<fragment android:id="@+id/Ready">` (NOT mainFragment — mainFragment only has one action, to Ready):103104```xml105<action106 android:id="@+id/action_mainFragment_to_yourCaseName"107 app:destination="@id/yourCaseName" />108```109110**Destination** — at root `<navigation>` level:111112```xml113<fragment114 android:id="@+id/yourCaseName"115 android:name="io.agora.api.example.examples.advanced.YourCaseName"116 android:label="@string/item_your_case_name"117 tools:layout="@layout/fragment_your_case_name" />118```119120`action android:id` must exactly match `actionId` in `@Example`.121122---123124### Step 5: Update ARCHITECTURE.md125126Add one line to the case list in `ARCHITECTURE.md` under the correct directory section (`basic/`, `advanced/`, or `audio/`):127128```129├── YourCaseName.java # [index] "Display Name" — key API description130```131132Keep the format consistent with existing entries. This file is the fast-lookup index used by `query-cases` — keeping it current avoids full directory scans.133134---135136## Modifying an Existing Case137138When modifying an existing case rather than creating a new one, identify which files need changes based on what you are updating:139140| What changed | Files to touch |141|---|---|142| Implementation logic (API calls, event handling) | `java/.../examples/{basic\|advanced\|audio}/CaseName.java` |143| UI layout (views, controls) | `res/layout/fragment_case_name.xml` |144| Display name or tips text | `res/values/strings.xml` |145| Sort index or group (BASIC ↔ ADVANCED) | `@Example` annotation in the Fragment class |146| Navigation label | `res/navigation/nav_graph.xml` (fragment label attribute) |147| Class rename or package move | Fragment class, `nav_graph.xml` (android:name + destination id), `@Example` annotation (actionId), layout file name, `ARCHITECTURE.md` |148149After making changes:1501511. **Verify `@Example` annotation consistency** — ensure `index`, `group`, `name`, `actionId`, and `tipsId` still match the actual string resources, nav action ID, and intended group/position. A mismatch causes the case to silently disappear from the list or navigate to the wrong screen.1522. **Update `res/values/strings.xml`** if the display name or tips text changed.1533. **Update `res/navigation/nav_graph.xml`** if the class name, package, or label changed.1544. **Update `ARCHITECTURE.md`** — update the Directory Layout entry and the Case Index table row to reflect any changes to the case name, path, Key APIs, or description.155156---157158## Verify159160```bash161./gradlew assembleDebug162```163164- [ ] Case appears in correct group at expected sort position165- [ ] Tap navigates to the case screen (silent failure = nav action in wrong fragment)166- [ ] `onJoinChannelSuccess` fires in Logcat167- [ ] After pressing back, check Logcat for `RtcEngine.destroy` within ~2 seconds — if missing, there is a lifecycle bug in `onDestroy`168- [ ] `ARCHITECTURE.md` Case Index table is updated — row added (new case) or row updated (modified case) with correct Case, Path, Key APIs, and Description169- [ ] `@Example` annotation fields (`index`, `group`, `name`, `actionId`, `tipsId`) are consistent with string resources and nav_graph entries170171---172173## When to Use a Spec Instead174175If the case meets any of the following criteria, create a Spec rather than using this skill directly:1761771. Involves coordinated calls across two or more Agora API modules1782. Requires a custom UI layout (not one of the standard templates above)1793. Manages multiple channels or multiple engine instances1804. Requires a foreground Service or background thread coordination1815. Involves developing new shared components (widget/utils, etc.)1826. Requires optional module integration (e.g. streamEncrypt)183184If none apply → use this skill directly; no Spec needed.185186### Spec Requirements Document Must Include187188- List of APIs the case demonstrates (audio APIs only)189- User interaction flow description190- Expected RtcEngine lifecycle behavior191- Required permissions (typically only `RECORD_AUDIO`)192193### Spec Design Document Must Include194195- Target project identifier: `APIExample-Audio`196- Class/file structure design197- API call sequence (Mermaid sequence diagram recommended)198- State management approach199- UI layout plan200- Integration points with existing shared components201- Case registration info: class name, display name, group (BASIC/ADVANCED), sort index — finalize during design to avoid conflicts202- Generate `@Example` annotation parameters, `nav_graph.xml` action + destination, `strings.xml` key names (`item_` prefix)203- Read `ARCHITECTURE.md` or use the `query-cases` skill to check existing indices204- voice-sdk checks: no video APIs (`enableVideo`, `setupLocalVideo`, `setupRemoteVideo`, `VideoCanvas`, `startScreenCapture`) — violations must be eliminated at design time205- Risk identification and mitigation (API availability, permissions, thread safety, performance)206207### Spec Task List Integration208209- Mark which sub-tasks can be executed with this `upsert-case` skill, and provide skill input parameters210- Mark which sub-tasks require manual coding, and provide target file paths and change summaries211- New shared component creation tasks must come before case implementation tasks212213---214215## NEVER216217- **NEVER** call any video API (`enableVideo`, `setupLocalVideo`, `VideoCanvas`) — voice-sdk has no video module; crash is immediate.218- **NEVER** put the nav action inside `<fragment id="mainFragment">` — it belongs in `<fragment id="Ready">`. mainFragment only routes to Ready; all case actions live in Ready. Wrong placement causes silent navigation failure at runtime.219- **NEVER** call `RtcEngine.destroy()` directly on the main thread — always `handler.post(RtcEngine::destroy)`. Direct call blocks the UI thread and causes ANR.220- **NEVER** call `setLocalAccessPoint()` without null-checking `getPrivateCloudConfig()` first — it returns null on standard builds, causing NPE.221- **NEVER** update UI directly inside `IRtcEngineEventHandler` callbacks — they run on a background thread. Always wrap with `runOnUIThread()`.222- **NEVER** omit `setParameters(...)` — it's required for Agora backend usage reporting in every case; omitting it causes silent reporting failure even though the app appears to work normally.