diff --git a/Android/APIExample-Audio/.agent/skills/query-cases/SKILL.md b/Android/APIExample-Audio/.agent/skills/query-cases/SKILL.md
deleted file mode 100644
index 4636e2b71..000000000
--- a/Android/APIExample-Audio/.agent/skills/query-cases/SKILL.md
+++ /dev/null
@@ -1,114 +0,0 @@
----
-name: query-cases
-description: >
- Query and browse existing API example cases in the APIExample-Audio Android demo —
- lists cases by group, finds which case demonstrates a specific Agora audio API,
- checks sort index availability, and resolves display names from string resources.
- Use when: someone asks what cases exist, which audio APIs are demonstrated, wants
- to find a case by name or API (e.g. setVoiceBeautifierPreset, enableSpatialAudio),
- needs a free sort index before adding a new case, or wants to know if an audio
- feature is already implemented. This project uses voice-sdk — no video APIs.
- Keywords: list cases, find case, query cases, @Example, sort index, BASIC, ADVANCED,
- available cases, existing cases, which case, is there a case, audio case.
----
-
-# Query Cases — APIExample-Audio
-
-## How cases are registered
-
-Every case is a Fragment under `app/src/main/java/io/agora/api/example/examples/{basic|advanced|audio}/` with an `@Example` annotation:
-
-```java
-@Example(
- index = 10, // unique within the group; BASIC: 0–9, ADVANCED: 10+
- group = ADVANCED,
- name = R.string.item_xxx,
- actionId = R.id.action_mainFragment_to_xxx,
- tipsId = R.string.xxx_tips
-)
-```
-
-A commented-out `@Example` (`//@Example`) means the case is disabled and won't appear in the app.
-
-This project uses `voice-sdk` — all cases are audio-only, no video APIs exist.
-
----
-
-## Query procedure
-
-### Step 1: Decide scope before scanning
-
-Before listing files, ask:
-- **Looking for a specific API?** — scan Javadoc comments for the API name; no need to read all files
-- **Need a free sort index?** — collect all `index` values for the target group, then find the gap
-- **Listing all cases?** — scan all three directories and collect annotations
-
-### Step 2: Read ARCHITECTURE.md first
-
-Read `ARCHITECTURE.md` (the `examples/` section of the Directory Layout). It contains a pre-built index of all cases with group, index, display name, and key API — no file scanning needed for most queries.
-
-Use ARCHITECTURE.md as the primary source. Fall back to scanning the source directories only when:
-- The query requires data not in ARCHITECTURE.md (e.g. full `@Example` field values, `tipsId`)
-- ARCHITECTURE.md appears stale (a case exists in source but not in the doc)
-- The output involves free-index claims, index collisions, or "is index X available?" decisions — these must be validated from source immediately before final output
-
-### Step 3: Scan case directories (fallback only)
-
-| Directory | Group | Contents |
-|-----------|-------|----------|
-| `examples/basic/` | BASIC | Core audio join/leave patterns |
-| `examples/advanced/` | ADVANCED | Feature-specific audio APIs |
-| `examples/audio/` | ADVANCED | Audio visualization (still grouped ADVANCED) |
-
-Each `.java` file is a case. Subdirectories (e.g. `customaudio/`) contain multi-file cases — the main class is the file whose name matches the directory name; if no name match, look for the file containing `@Example`.
-
-### Step 4: Extract `@Example` fields
-
-For each file, read the annotation for `group`, `index`, `name` (string resource ID), and `tipsId`. If the annotation is commented out, the case is disabled.
-
-Resolve display names from `app/src/main/res/values/strings.xml`:
-`R.string.item_voice_effects` → `Voice Effects`
-
-### Step 5: Read class Javadoc for API mapping
-
-The Javadoc above each class lists the key APIs demonstrated:
-
-```java
-/**
- * This demo demonstrates how to apply voice beautifier effects.
- *
- * Key APIs used:
- * - RtcEngine.setVoiceBeautifierPreset()
- */
-```
-
-Use this to answer "which case uses X?" queries without reading the full implementation.
-
-If no Javadoc is present, scan the method body for the API name as a method call. If still not found, note "API mapping unavailable" in the results table.
-
-### Step 6: Present results
-
-Full listing — table format:
-
-| Group | Index | Case Name | File | Key APIs |
-|-------|-------|-----------|------|----------|
-| BASIC | 0 | Join Channel Audio | JoinChannelAudio.java | joinChannel() |
-| ADVANCED | 4 | Voice Effects | VoiceEffects.java | setVoiceBeautifierPreset() |
-
-For a specific query (e.g. "which case uses enableSpatialAudio?"), return only matching rows.
-
-For a free-index query, list all used indices in the target group and identify the next available slot:
-> BASIC range: 0–9. ADVANCED range: 10+.
-> ADVANCED indices in use: 10, 11, 12, 15, 20 → next free: 13
-
-Before returning any free-index/collision result, re-scan source registration points (`@Example` across `basic/`, `advanced/`, `audio/`) and recompute once from source-of-truth data.
-
----
-
-## NEVER
-
-- **NEVER** count a commented-out `@Example` (`//@Example`) as an active case — it is disabled and won't appear in the app.
-- **NEVER** mix index spaces across groups — `audio/` cases use `group=ADVANCED` but share the same index namespace as `advanced/`; always scan both directories together when finding a free index.
-- **NEVER** use filename alone to identify a subdirectory case — the main class is the file whose name matches the directory name; if no match, look for the file with `@Example`.
-- **NEVER** report a free index without scanning all three directories (`basic/`, `advanced/`, `audio/`) for the target group — missing one causes index collisions.
-- **NEVER** suggest video APIs — this project uses voice-sdk only; video APIs do not exist.
diff --git a/Android/APIExample-Audio/.agent/skills/review-case/SKILL.md b/Android/APIExample-Audio/.agent/skills/review-case/SKILL.md
deleted file mode 100644
index ba517c3ea..000000000
--- a/Android/APIExample-Audio/.agent/skills/review-case/SKILL.md
+++ /dev/null
@@ -1,57 +0,0 @@
----
-name: review-case
-description: >
- Review an existing case implementation against project-specific red lines
- and coding standards. Use after implementing or modifying a case.
- Use when: reviewing a case for correctness, checking red-line compliance,
- verifying lifecycle and threading patterns, auditing an existing Fragment.
- Keywords: review, audit, check, red lines, lifecycle, threading, compliance.
----
-
-# Review Case — APIExample-Audio
-
-Run through every item below before considering a case implementation complete.
-Open the case's Fragment source file and verify each point against the actual code.
-
-## Checklist
-
-### Teardown & Lifecycle
-
-- [ ] **leaveChannel before destroy** — `engine.leaveChannel()` is called before `RtcEngine.destroy()` in the teardown path (typically `onDestroy()`). Destroying without leaving first leaks the channel session on the server side.
-
-- [ ] **handler.post for destroy** — `RtcEngine.destroy()` is invoked via `handler.post(RtcEngine::destroy)` and **not** called directly on the main thread. A direct call blocks the UI thread and causes ANR.
-
-### Threading
-
-- [ ] **runOnUIThread for callbacks** — All `IRtcEngineEventHandler` callbacks that update UI are wrapped with `runOnUIThread()`. SDK callbacks arrive on a background thread; touching Views without dispatching to the main thread causes crashes or silent rendering corruption.
-
-### Permissions
-
-- [ ] **Permission check before join** — `checkOrRequestPermission()` is called before `joinChannel()`. Joining without the required permissions (RECORD_AUDIO) causes a silent failure — no error callback, just no audio.
-
-### Backend Reporting
-
-- [ ] **setParameters present** — `setParameters(...)` is called during engine initialisation. This is required for Agora backend usage reporting in every case; omitting it causes silent reporting failure even though the app appears to work normally.
-
-### Private Cloud
-
-- [ ] **getPrivateCloudConfig null-check** — `getPrivateCloudConfig()` is null-checked before `setLocalAccessPoint()` is called. The method returns `null` on standard (non-private-cloud) builds, so calling `setLocalAccessPoint()` without the guard causes a NullPointerException.
-
-### Audio-Only Constraint
-
-- [ ] **No video APIs** — The case must not call `enableVideo()`, `setupLocalVideo()`, or reference `VideoCanvas`. APIExample-Audio uses the voice-SDK which has no video module; calling video APIs causes a compile error or runtime crash.
-
-## If a Check Fails
-
-- Teardown order wrong (`destroy` before `leaveChannel`) — fix teardown to `leaveChannel()` first, then `handler.post(RtcEngine::destroy)`, and re-test back navigation.
-- UI touched in SDK callback without main-thread dispatch — wrap UI updates in `runOnUIThread()` and re-run to verify no thread exceptions.
-- Permission flow missing before `joinChannel()` — add `checkOrRequestPermission()` gate and verify join only after `RECORD_AUDIO` is granted.
-- Any video API appears in code — remove all video API calls/usages immediately and replace with audio-only equivalents.
-- Missing `setParameters(...)` or private-cloud null-check — add both safeguards in engine init and re-run initialization.
-
-## NEVER
-
-- **NEVER** approve a case review if any video API (`enableVideo`, `setupLocalVideo`, `VideoCanvas`) exists in APIExample-Audio.
-- **NEVER** approve a case review with direct `RtcEngine.destroy()` on main thread.
-- **NEVER** approve a case review when `leaveChannel()` is missing before destroy.
-- **NEVER** ignore background-thread UI updates inside `IRtcEngineEventHandler` callbacks.
diff --git a/Android/APIExample-Audio/.agent/skills/upsert-case/SKILL.md b/Android/APIExample-Audio/.agent/skills/upsert-case/SKILL.md
deleted file mode 100644
index 9cb3d65e2..000000000
--- a/Android/APIExample-Audio/.agent/skills/upsert-case/SKILL.md
+++ /dev/null
@@ -1,231 +0,0 @@
----
-name: upsert-case
-description: >
- 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.
----
-
-# 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`](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
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-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 `` (NOT mainFragment — mainFragment only has one action, to Ready):
-
-```xml
-
-```
-
-**Destination** — at root `` level:
-
-```xml
-
-```
-
-`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:
-
-1. **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.
-2. **Update `res/values/strings.xml`** if the display name or tips text changed.
-3. **Update `res/navigation/nav_graph.xml`** if the class name, package, or label changed.
-4. **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
-
-```bash
-./gradlew assembleDebug
-```
-
-- [ ] Case appears in correct group at expected sort position
-- [ ] Tap navigates to the case screen (silent failure = nav action in wrong fragment)
-- [ ] `onJoinChannelSuccess` fires in Logcat
-- [ ] After pressing back, check Logcat for `RtcEngine.destroy` within ~2 seconds — if missing, there is a lifecycle bug in `onDestroy`
-- [ ] `ARCHITECTURE.md` Case Index table is updated — row added (new case) or row updated (modified case) with correct Case, Path, Key APIs, and Description
-- [ ] `@Example` annotation fields (`index`, `group`, `name`, `actionId`, `tipsId`) are consistent with string resources and nav_graph entries
-
----
-
-## When to Use a Spec Instead
-
-If the case meets any of the following criteria, create a Spec rather than using this skill directly:
-
-1. Involves coordinated calls across two or more Agora API modules
-2. Requires a custom UI layout (not one of the standard templates above)
-3. Manages multiple channels or multiple engine instances
-4. Requires a foreground Service or background thread coordination
-5. Involves developing new shared components (widget/utils, etc.)
-6. 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 `` — it belongs in ``. 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.
diff --git a/Android/APIExample-Audio/.agent/skills/upsert-case/references/fragment-template.java b/Android/APIExample-Audio/.agent/skills/upsert-case/references/fragment-template.java
deleted file mode 100644
index b4e9214c7..000000000
--- a/Android/APIExample-Audio/.agent/skills/upsert-case/references/fragment-template.java
+++ /dev/null
@@ -1,209 +0,0 @@
-package io.agora.api.example.examples.advanced;
-
-import static io.agora.api.example.common.model.Examples.ADVANCED;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.EditText;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Random;
-
-import io.agora.api.example.MainApplication;
-import io.agora.api.example.R;
-import io.agora.api.example.annotation.Example;
-import io.agora.api.example.common.BaseFragment;
-import io.agora.api.example.common.widget.AudioSeatManager;
-import io.agora.api.example.utils.PermissonUtils;
-import io.agora.api.example.utils.TokenUtils;
-import io.agora.rtc2.ChannelMediaOptions;
-import io.agora.rtc2.Constants;
-import io.agora.rtc2.IRtcEngineEventHandler;
-import io.agora.rtc2.RtcEngine;
-import io.agora.rtc2.RtcEngineConfig;
-import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
-
-// NOTE: This project uses voice-sdk.
-// Do NOT import or call: enableVideo(), setupLocalVideo(), VideoCanvas, VideoReportLayout,
-// setVideoEncoderConfiguration(), or any other video API — the module does not exist.
-
-/**
- * This demo demonstrates how to use [describe the audio feature here].
- *
- * Key APIs used:
- * - RtcEngine.yourAudioApi()
- */
-@Example(
- index = 10, // unique within the group; BASIC: 0–9, ADVANCED: 10+
- group = ADVANCED, // BASIC or ADVANCED
- name = R.string.item_your_case_name,
- actionId = R.id.action_mainFragment_to_yourCaseName,
- tipsId = R.string.your_case_name_tips
-)
-public class YourCaseName extends BaseFragment implements View.OnClickListener {
- private static final String TAG = YourCaseName.class.getSimpleName();
-
- private Button join;
- private EditText et_channel;
- private RtcEngine engine;
- private int myUid;
- private boolean joined = false;
-
- // AudioSeatManager visualizes remote audio participants — add seat views in your XML layout
- private AudioSeatManager audioSeatManager;
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.fragment_your_case_name, container, false);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- join = view.findViewById(R.id.btn_join);
- et_channel = view.findViewById(R.id.et_channel);
- join.setOnClickListener(this);
- // bind additional feature-specific views here
-
- // Wire AudioSeatManager to seat views defined in your XML layout
- // audioSeatManager = new AudioSeatManager(
- // view.findViewById(R.id.audio_place_01),
- // view.findViewById(R.id.audio_place_02)
- // );
- }
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- Context context = getContext();
- if (context == null) return;
- try {
- RtcEngineConfig config = new RtcEngineConfig();
- config.mContext = context.getApplicationContext();
- config.mAppId = getAgoraAppId();
- config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
- config.mEventHandler = iRtcEngineEventHandler;
- config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT);
- config.mAreaCode = ((MainApplication) getActivity().getApplication())
- .getGlobalSettings().getAreaCode();
- engine = RtcEngine.create(config);
- // REQUIRED in every case — do not remove
- engine.setParameters("{"
- + "\"rtc.report_app_scenario\":"
- + "{"
- + "\"appScenario\":" + 100 + ","
- + "\"serviceType\":" + 11 + ","
- + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
- + "}"
- + "}");
- // null-check is mandatory — returns null on non-private-cloud builds
- LocalAccessPointConfiguration localAccessPointConfiguration =
- ((MainApplication) getActivity().getApplication())
- .getGlobalSettings().getPrivateCloudConfig();
- if (localAccessPointConfiguration != null) {
- engine.setLocalAccessPoint(localAccessPointConfiguration);
- }
- } catch (Exception e) {
- e.printStackTrace();
- getActivity().onBackPressed();
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (engine != null) {
- engine.leaveChannel();
- }
- // MUST use handler.post — do NOT call RtcEngine.destroy() directly on main thread
- handler.post(RtcEngine::destroy);
- engine = null;
- }
-
- @Override
- public void onClick(View v) {
- if (v.getId() == R.id.btn_join) {
- if (!joined) {
- String channelId = et_channel.getText().toString();
- checkOrRequestPermisson(new PermissonUtils.PermissionResultCallback() {
- @Override
- public void onPermissionsResult(boolean allPermissionsGranted,
- String[] permissions, int[] grantResults) {
- if (allPermissionsGranted) {
- joinChannel(channelId);
- }
- }
- });
- } else {
- joined = false;
- engine.leaveChannel();
- join.setText(getString(R.string.join));
- }
- }
- }
-
- private void joinChannel(String channelId) {
- engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
- // --- feature-specific audio setup goes here ---
- // e.g. engine.setAudioProfile(...); engine.setVoiceBeautifierPreset(...);
-
- ChannelMediaOptions options = new ChannelMediaOptions();
- options.autoSubscribeAudio = true;
- options.publishMicrophoneTrack = true;
- // Do NOT set publishCameraTrack or autoSubscribeVideo — voice-sdk has no video module
-
- int uid = new Random().nextInt(1000) + 100000;
- TokenUtils.gen(requireContext(), channelId, uid, token -> {
- int res = engine.joinChannel(token, channelId, uid, options);
- if (res != 0) {
- showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
- return;
- }
- join.setEnabled(false);
- });
- }
-
- private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() {
- @Override
- public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
- Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
- myUid = uid;
- joined = true;
- // ALL UI updates must go through runOnUIThread — callbacks run on background thread
- runOnUIThread(() -> {
- join.setEnabled(true);
- join.setText(getString(R.string.leave));
- });
- }
-
- @Override
- public void onUserJoined(int uid, int elapsed) {
- Log.i(TAG, "onUserJoined -> " + uid);
- runOnUIThread(() -> {
- // audioSeatManager.addUser(uid);
- });
- }
-
- @Override
- public void onUserOffline(int uid, int reason) {
- Log.i(TAG, String.format("user %d offline, reason %d", uid, reason));
- runOnUIThread(() -> {
- // audioSeatManager.removeUser(uid);
- });
- }
-
- @Override
- public void onError(int err) {
- showLongToast("Error code:" + err + ", msg:" + RtcEngine.getErrorDescription(err));
- }
- };
-}
diff --git a/Android/APIExample-Audio/.agents/skills/query-cases/SKILL.md b/Android/APIExample-Audio/.agents/skills/query-cases/SKILL.md
new file mode 100644
index 000000000..796ae664b
--- /dev/null
+++ b/Android/APIExample-Audio/.agents/skills/query-cases/SKILL.md
@@ -0,0 +1,49 @@
+---
+name: query-cases
+description: >
+ Query and browse existing API example cases in the APIExample-Audio Android demo.
+ Use when: someone asks what audio cases exist, which case demonstrates a specific
+ Agora voice API, whether a BASIC or ADVANCED slot is free, or whether a case is
+ fully registered. This project uses voice-sdk only and does not support video APIs.
+ Keywords: list cases, find case, query cases, @Example, sort index, BASIC, ADVANCED,
+ existing cases, available cases, audio case, voice API.
+---
+
+# Query Cases — APIExample-Audio
+
+## What this skill is for
+
+Use this skill when the question is about which audio cases already exist in `APIExample-Audio/`, which case demonstrates a specific voice API, whether a BASIC or ADVANCED slot is free, or whether an audio case is fully registered.
+
+## Source of truth
+
+1. `APIExample-Audio/ARCHITECTURE.md`
+2. `APIExample-Audio/app/src/main/java/io/agora/api/example/examples/**`
+3. `APIExample-Audio/app/src/main/res/navigation/nav_graph.xml`
+4. `APIExample-Audio/app/src/main/res/values/strings.xml`
+
+## Procedure
+
+1. Start with `ARCHITECTURE.md` to answer ordinary "what exists?" or "which case uses API X?" questions quickly.
+2. Read the relevant case source under `examples/basic/`, `examples/advanced/`, or `examples/audio/` when the answer depends on live `@Example` values, method usage, or whether the architecture doc is stale.
+3. For API-to-case mapping, prefer the class Javadoc and then confirm by scanning for the API call in the case implementation.
+4. For registration checks, confirm the case has a live `@Example` annotation, a matching destination and action in `nav_graph.xml`, and corresponding string resources in `strings.xml`.
+5. For free-index or collision checks, scan active `@Example` annotations from source immediately before answering; treat `examples/audio/` as part of the `ADVANCED` index namespace.
+6. Present results with explicit group, index, case name, file path, and key API when applicable, and call out any stale or inconsistent registration artifacts.
+
+## Verify
+
+- Re-scan source before reporting any free BASIC or ADVANCED index
+- Confirm commented-out annotations such as `//@Example` are excluded from active-case results
+- Confirm any claimed registration status matches `@Example`, `nav_graph.xml`, and `strings.xml`
+
+## Out of scope
+
+- Editing or registering cases
+- Recommending video APIs
+- Approving implementation quality
+
+## Never
+
+- Never suggest `enableVideo()`, `setupLocalVideo()`, or `VideoCanvas`
+- Never report a free ADVANCED index without scanning both `advanced/` and `audio/`
diff --git a/Android/APIExample-Audio/.agents/skills/review-case/SKILL.md b/Android/APIExample-Audio/.agents/skills/review-case/SKILL.md
new file mode 100644
index 000000000..3b3e95c23
--- /dev/null
+++ b/Android/APIExample-Audio/.agents/skills/review-case/SKILL.md
@@ -0,0 +1,48 @@
+---
+name: review-case
+description: >
+ Review an APIExample-Audio case after it has been created or modified.
+ Use when: checking lifecycle, permission, registration, and voice-SDK-only
+ constraints before considering a case ready. Always run the minimum build
+ verification command as part of the review.
+ Keywords: review, audit, check, lifecycle, threading, permissions, compliance.
+---
+
+# Review Case — APIExample-Audio
+
+## What this skill is for
+
+Use this skill after a case has been created or modified in `APIExample-Audio/`. It checks lifecycle, permission, registration, and voice-SDK-only constraints, then runs the minimum build verification command.
+
+## Source of truth
+
+1. `APIExample-Audio/AGENTS.md`
+2. `APIExample-Audio/ARCHITECTURE.md`
+3. Target case source under `APIExample-Audio/app/src/main/java/io/agora/api/example/examples/**`
+4. `APIExample-Audio/app/src/main/res/navigation/nav_graph.xml`
+5. `APIExample-Audio/app/src/main/res/values/strings.xml`
+
+## Procedure
+
+1. Open the target case and verify teardown order: `leaveChannel()` before `RtcEngine.destroy()`, aligned with the audio project's documented lifecycle rule.
+2. Verify all UI updates triggered from `IRtcEngineEventHandler` callbacks are dispatched back to the UI thread.
+3. For cases that call `joinChannel()`, verify microphone permission is requested through the project's permission helper before join, and that the case remains audio-only.
+4. Check for required engine-init safeguards used in this project, including backend reporting setup and private-cloud null-guarding where applicable.
+5. Confirm the case registration still aligns across `@Example`, `nav_graph.xml`, `strings.xml`, and `ARCHITECTURE.md`.
+6. Run the minimum build verification command from `APIExample-Audio/` before finalizing the review.
+
+## Verify
+
+- Run `./gradlew assembleDebug` from `APIExample-Audio/`
+- Confirm the target case does not call `enableVideo()`, `setupLocalVideo()`, or `VideoCanvas`
+- Confirm `@Example`, `nav_graph.xml`, `strings.xml`, and `ARCHITECTURE.md` still align
+
+## Out of scope
+
+- Fixing the case during review by default
+- Treating compile success as proof of runtime correctness
+
+## Never
+
+- Never approve any video API usage in the audio-only project
+- Never skip the build command
diff --git a/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md b/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md
new file mode 100644
index 000000000..7048e6920
--- /dev/null
+++ b/Android/APIExample-Audio/.agents/skills/upsert-case/SKILL.md
@@ -0,0 +1,35 @@
+## What this skill is for
+
+Use this skill to add or update a case in `APIExample-Audio/`. It owns the full change closure: case source, XML layout, strings, `nav_graph.xml`, and `ARCHITECTURE.md`, while preserving the voice-SDK-only constraint.
+
+## Source of truth
+
+1. `APIExample-Audio/AGENTS.md`
+2. `APIExample-Audio/ARCHITECTURE.md`
+3. `APIExample-Audio/app/src/main/java/io/agora/api/example/examples/**`
+4. `APIExample-Audio/app/src/main/res/navigation/nav_graph.xml`
+5. `APIExample-Audio/app/src/main/res/values/strings.xml`
+6. `APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java`
+
+## Procedure
+
+1. Run `query-cases` first when the case index, group placement, or nearby examples are unknown.
+2. Create or update the Fragment, layout, strings, nav entries, and `ARCHITECTURE.md` together so the case stays fully registered.
+3. Use `APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java` as the reference for engine lifecycle, usage reporting, private-cloud setup, and voice-only guardrails.
+4. Keep `@Example`, action ID, destination ID, string resources, and audio-only constraints aligned across the implementation.
+
+## Verify
+
+- Run `./gradlew assembleDebug` from `APIExample-Audio/`
+- Confirm the edited case avoids video APIs and keeps `@Example`, layout, strings, nav, and docs aligned
+
+## Out of scope
+
+- Reviewing the finished implementation as an independent reviewer
+- Omitting docs or registration updates because the fragment compiles
+
+## Never
+
+- Never add `publishCameraTrack`
+- Never call `enableVideo()`
+- Never skip `ARCHITECTURE.md`
diff --git a/Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java b/Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java
new file mode 100644
index 000000000..65191135f
--- /dev/null
+++ b/Android/APIExample-Audio/.agents/skills/upsert-case/references/fragment-template.java
@@ -0,0 +1,64 @@
+import android.content.Context;
+import android.os.Bundle;
+
+import androidx.annotation.Nullable;
+
+import io.agora.api.example.MainApplication;
+import io.agora.api.example.common.BaseFragment;
+import io.agora.api.example.common.widget.AudioSeatManager;
+import io.agora.rtc2.ChannelMediaOptions;
+import io.agora.rtc2.IRtcEngineEventHandler;
+import io.agora.rtc2.RtcEngine;
+import io.agora.rtc2.RtcEngineConfig;
+import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
+
+/**
+ * Reference skeleton only. Adapt this to a real case Fragment with its own UI, join flow,
+ * and permission handling before compiling it into the project.
+ */
+public abstract class AudioExampleCaseTemplate extends BaseFragment {
+ protected RtcEngine engine;
+ protected AudioSeatManager audioSeatManager;
+ protected final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() {
+ };
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ Context context = getContext();
+ if (context == null || engine != null) {
+ return;
+ }
+ RtcEngineConfig config = new RtcEngineConfig();
+ config.mContext = context.getApplicationContext();
+ config.mAppId = getAgoraAppId();
+ config.mEventHandler = iRtcEngineEventHandler;
+ config.mAreaCode =
+ ((MainApplication) getActivity().getApplication()).getGlobalSettings().getAreaCode();
+ engine = RtcEngine.create(config);
+ engine.setParameters("{\"rtc.report_app_scenario\":{\"appScenario\":100,\"serviceType\":11,\"appVersion\":\""
+ + RtcEngine.getSdkVersion() + "\"}}");
+ LocalAccessPointConfiguration privateCloud =
+ ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (privateCloud != null) {
+ engine.setLocalAccessPoint(privateCloud);
+ }
+ }
+
+ protected ChannelMediaOptions buildAudioOnlyOptions() {
+ ChannelMediaOptions options = new ChannelMediaOptions();
+ options.autoSubscribeAudio = true;
+ options.publishMicrophoneTrack = true;
+ return options;
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (engine != null) {
+ engine.leaveChannel();
+ RtcEngine.destroy();
+ engine = null;
+ }
+ }
+}
diff --git a/Android/APIExample-Audio/AGENTS.md b/Android/APIExample-Audio/AGENTS.md
index 092f45f59..a7a31663e 100644
--- a/Android/APIExample-Audio/AGENTS.md
+++ b/Android/APIExample-Audio/AGENTS.md
@@ -29,9 +29,9 @@ See [README.md — Obtain an App Id](README.md#obtain-an-app-id).
| Skill | Path | Description |
|-------|------|-------------|
-| upsert-case | `.agent/skills/upsert-case/` | Add a new audio case or modify an existing one |
-| query-cases | `.agent/skills/query-cases/` | Query and browse existing audio cases |
-| review-case | `.agent/skills/review-case/` | Review a case against project red lines |
+| upsert-case | `.agents/skills/upsert-case/` | Add a new audio case or modify an existing one |
+| query-cases | `.agents/skills/query-cases/` | Query and browse existing audio cases |
+| review-case | `.agents/skills/review-case/` | Review a case against project red lines |
## Further Reading
diff --git a/Android/APIExample-Compose/.agent/skills/query-cases/SKILL.md b/Android/APIExample-Compose/.agent/skills/query-cases/SKILL.md
deleted file mode 100644
index e4bb11024..000000000
--- a/Android/APIExample-Compose/.agent/skills/query-cases/SKILL.md
+++ /dev/null
@@ -1,117 +0,0 @@
----
-name: query-cases
-description: >
- Query and browse existing API example cases in the APIExample-Compose Android demo —
- lists cases by group, finds which case demonstrates a specific Agora API, checks list
- position availability, and resolves display names from string resources. Use when:
- someone asks what Compose cases exist, which APIs are demonstrated, wants to find a
- case by name or API (e.g. takeSnapshot, setClientRole), needs to know the current
- list position before adding a new case, or wants to know if a feature is already
- implemented in Compose. Registration is manual via Examples.kt — no @Example annotation.
- Keywords: list cases, find case, query cases, Examples.kt, BasicExampleList,
- AdvanceExampleList, available cases, existing cases, which case, is there a case,
- Compose case, Jetpack Compose.
----
-
-# Query Cases — APIExample-Compose
-
-## How cases are registered
-
-Unlike APIExample, this project does NOT use reflection. Cases are manually registered in:
-
-`app/src/main/java/io/agora/api/example/compose/model/Examples.kt`
-
-Two lists define the groups:
-
-```kotlin
-val BasicExampleList = listOf(
- Example(R.string.example_join_channel_video) { JoinChannelVideo() },
- // …
-)
-
-val AdvanceExampleList = listOf(
- Example(R.string.example_live_streaming) { LiveStreaming() },
- // …
-)
-```
-
-List position is display order — there is no `index` field. String keys use the `example_` prefix.
-
----
-
-## Query procedure
-
-### Step 1: Decide scope before scanning
-
-Before reading files, ask:
-- **Looking for a specific API?** — read Composable KDoc comments for the API name; no need to read all files
-- **Need to know current list positions?** — read Examples.kt only; positions are 1-based list indices
-- **Listing all cases?** — read Examples.kt for the full registry, then resolve names from strings.xml
-
-### Step 2: Read ARCHITECTURE.md first
-
-Read `ARCHITECTURE.md` (the `samples/` section of the Directory Layout). It contains a pre-built index of all cases with group, position, display name, and key API — no file scanning needed for most queries.
-
-Use ARCHITECTURE.md as the primary source. Fall back to reading Examples.kt only when:
-- The query requires data not in ARCHITECTURE.md (e.g. exact list position, `description` field)
-- ARCHITECTURE.md appears stale (a case exists in Examples.kt but not in the doc)
-- The output involves list position availability, duplicate registration checks, or "is this case already registered?" decisions — these must be validated from `Examples.kt` immediately before final output
-
-### Step 3: Read Examples.kt (fallback / position queries)
-
-File: `app/src/main/java/io/agora/api/example/compose/model/Examples.kt`
-
-Parse `BasicExampleList` and `AdvanceExampleList`. Each entry is:
-
-```kotlin
-Example(R.string.example_your_case_name) { YourCaseName() }
-```
-
-Position in the list (1-based) is the display order. There is no `index` field and no disabled/commented-out mechanism equivalent to `//@Example`.
-
-### Step 4: Resolve display names
-
-Resolve `R.string.example_*` from `app/src/main/res/values/strings.xml`:
-`R.string.example_video_snapshot` → `Video Snapshot`
-
-### Step 5: Read Composable KDoc for API mapping
-
-Case implementations are in `app/src/main/java/io/agora/api/example/compose/samples/`. The KDoc above each public Composable lists key APIs:
-
-```kotlin
-/**
- * Demonstrates how to take a snapshot of the local video stream.
- *
- * Key APIs used:
- * - RtcEngine.takeSnapshot()
- */
-@Composable
-fun VideoSnapshot() { … }
-```
-
-Use this to answer "which case uses X?" without reading the full implementation. If no KDoc, scan the function body for the API name.
-
-### Step 6: Present results
-
-Full listing — table format:
-
-| Group | Position | Case Name | File | Key APIs |
-|-------|----------|-----------|------|----------|
-| Basic | 1 | Join Channel Video | JoinChannelVideo.kt | joinChannel(), setupLocalVideo() |
-| Advanced | 3 | Video Snapshot | VideoSnapshot.kt | takeSnapshot() |
-
-For a specific query, return only matching rows.
-
-For a position query, list current entries in the target list and identify the next available slot:
-> AdvanceExampleList has 12 entries → next position: 13 (append at end)
-
-Before returning any position/registration-conflict result, re-read `Examples.kt` and recompute from the current list entries.
-
----
-
-## NEVER
-
-- **NEVER** look for `@Example` annotations — this project uses manual registration in Examples.kt, not reflection.
-- **NEVER** treat list position as a unique ID that must be gap-free — position is just list order; new cases always append at the end of the appropriate list.
-- **NEVER** use the `item_` string prefix — Compose cases use `example_` prefix; `item_` belongs to APIExample.
-- **NEVER** scan `nav_graph.xml` for case registration — Compose navigation is position-based and requires no nav graph changes.
diff --git a/Android/APIExample-Compose/.agent/skills/review-case/SKILL.md b/Android/APIExample-Compose/.agent/skills/review-case/SKILL.md
deleted file mode 100644
index fd0b84bad..000000000
--- a/Android/APIExample-Compose/.agent/skills/review-case/SKILL.md
+++ /dev/null
@@ -1,48 +0,0 @@
----
-name: review-case
-description: >
- Review an existing case implementation against project-specific red lines
- and coding standards. Use after implementing or modifying a case.
- Use when: reviewing a Compose case for correctness, checking red-line compliance,
- verifying lifecycle and state patterns, auditing an existing Composable.
- Keywords: review, audit, check, red lines, lifecycle, state, compliance, Compose.
----
-
-# Review Case — APIExample-Compose
-
-Run through every item below before considering a case implementation complete.
-Open the case's Composable source file and verify each point against the actual code.
-
-## Checklist
-
-### Teardown & Lifecycle
-
-- [ ] **leaveChannel before destroy in onDispose** — `engine.leaveChannel()` is called before `RtcEngine.destroy()` in the `onDispose` block. Destroying without leaving first leaks the channel session on the server side.
-
-- [ ] **DisposableEffect key is lifecycleOwner not Unit** — `DisposableEffect(lifecycleOwner)` not `DisposableEffect(Unit)`. Using `Unit` fires only once and won't clean up on back navigation; the `onDispose` block never re-executes when the lifecycle owner changes.
-
-### State Management
-
-- [ ] **rememberSaveable for channelName/isJoined/uid and remember for RtcEngine** — `channelName`, `isJoined`, `uid` use `rememberSaveable`; `RtcEngine` uses `remember`. `rememberSaveable` survives configuration changes (rotation); `RtcEngine` is not serializable and will crash if placed in `rememberSaveable`.
-
-### Threading
-
-- [ ] **Callbacks dispatch to main thread via coroutineScope.launch(Dispatchers.Main)** — `IRtcEngineEventHandler` callbacks that show `Toast`, `Dialog`, or `AlertDialog` dispatch to the main thread via `coroutineScope.launch(Dispatchers.Main)`. SDK callbacks arrive on a background thread; `Toast` and dialog APIs require the main thread or they throw `CalledFromWrongThreadException`. Note: simple Compose state mutations (e.g. `isJoined = true`) are thread-safe via the snapshot system and do **not** need main-thread dispatch.
-
-### Permissions
-
-- [ ] **Permission check before joinChannel** — Permission launcher (`rememberLauncherForActivityResult`) is called before `joinChannel()`. Joining without the required permissions (`RECORD_AUDIO`, and `CAMERA` for video cases) causes a silent failure — no error callback, just no audio/video.
-
-## If a Check Fails
-
-- `DisposableEffect(Unit)` is used — change key to `lifecycleOwner`, then verify back navigation triggers cleanup.
-- `RtcEngine` stored in `rememberSaveable` or state fields in `remember` only — fix to `RtcEngine -> remember`, UI/session state -> `rememberSaveable`, then verify rotation.
-- Toast/Dialog shown directly in callback — move UI-thread-only calls into `coroutineScope.launch(Dispatchers.Main)`.
-- Permission launcher bypassed before `joinChannel()` — gate join flow behind permission callback and re-test denied/granted paths.
-
-## NEVER
-
-- **NEVER** approve a review when `DisposableEffect` key is `Unit` for case teardown logic.
-- **NEVER** approve a review when `RtcEngine` uses `rememberSaveable`.
-- **NEVER** treat Compose callback state safety as permission to call Toast/Dialog off main thread.
-- **NEVER** skip rotation and back-navigation checks for lifecycle-sensitive Compose cases.
diff --git a/Android/APIExample-Compose/.agent/skills/upsert-case/SKILL.md b/Android/APIExample-Compose/.agent/skills/upsert-case/SKILL.md
deleted file mode 100644
index 3faecf254..000000000
--- a/Android/APIExample-Compose/.agent/skills/upsert-case/SKILL.md
+++ /dev/null
@@ -1,185 +0,0 @@
----
-name: upsert-case
-description: >
- Add a new API example case or modify an existing one in the APIExample-Compose Android demo —
- creates or updates a Kotlin Composable file, registers or updates it in Examples.kt, and manages
- string resources. Use when: adding a new Agora RTC API demo screen in Jetpack Compose, modifying
- an existing case's implementation or registration, porting an existing APIExample case to Compose,
- implementing a new feature example in Kotlin + Compose UI, registering a new entry in
- BasicExampleList or AdvanceExampleList, or updating an existing case's strings or Examples.kt entry.
- Kotlin only — no XML layouts, no Fragments. Keywords: add case, modify case, update case,
- new composable, Examples.kt, BasicExampleList, AdvanceExampleList, APIExample-Compose, Compose case,
- new screen, Jetpack Compose, RTC API example, upsert case.
----
-
-# Upsert Case — APIExample-Compose
-
-## Adding a New Case
-
-Touch exactly 3 files (all paths relative to `app/src/main/`):
-
-| File | What to add |
-|---|---|
-| `java/.../compose/samples/YourCaseName.kt` | Composable file |
-| `java/.../compose/model/Examples.kt` | 1 list entry |
-| `res/values/strings.xml` | 1 string |
-
-No `nav_graph.xml` changes — navigation routes by list position automatically.
-
----
-
-### Step 1: Clarify before coding
-
-Before writing a single line, ask:
-- **What API am I demonstrating?** — determines which existing case is the closest reference (`JoinChannelVideo.kt` for video, `JoinChannelAudio.kt` for audio)
-- **Video or audio-only?** — determines permissions (`CAMERA` + `RECORD_AUDIO` vs `RECORD_AUDIO` only), whether `enableVideo()` and `VideoGrid` are needed
-- **BasicExampleList or AdvanceExampleList?** — Basic for fundamental join/leave patterns; Advance for feature-specific APIs
-- **List position?** — run `query-cases` skill to see current entries; list order is display order
-
----
-
-### Step 2: Create the Composable file
-
-**MANDATORY — READ ENTIRE FILE before writing any code**:
-[`references/composable-template.kt`](references/composable-template.kt)
-
-Do NOT skip — the `SettingPreferences.getArea()`, `DisposableEffect` key, `rememberSaveable` vs `remember` rules, and `@Preview` placement 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:
-
-- `mAreaCode = SettingPreferences.getArea()` — **required**, do not hardcode or omit
-- `DisposableEffect(lifecycleOwner)` — key must be `lifecycleOwner`, not `Unit`; wrong key means cleanup never fires on back navigation
-- `rememberSaveable` for channelName, isJoined, uid, videoIdList — survives rotation
-- `remember` for RtcEngine — must NOT be `rememberSaveable` (engine is not serializable)
-- `IRtcEngineEventHandler` callbacks can mutate Compose state directly — snapshot system is thread-safe, no `runOnUIThread()` needed
-- `Toast`/`Dialog`/`AlertDialog` inside callbacks still need main thread — use `coroutineScope.launch(Dispatchers.Main) { }`
-- `@Preview` goes on the **private** `*View` function only — never on the public stateful entry
-
----
-
-### Step 3: Register in Examples.kt
-
-File: `app/src/main/java/io/agora/api/example/compose/model/Examples.kt`
-
-```kotlin
-val AdvanceExampleList = listOf(
- // … existing entries …
- Example(R.string.example_your_case_name) { YourCaseName() }
-)
-```
-
-List order is display order — position determines where the case appears in the UI.
-
----
-
-### Step 4: Add string resource
-
-File: `app/src/main/res/values/strings.xml`
-
-```xml
-Your Case Name
-```
-
-String key must use the `example_` prefix. No separate tips string needed (unlike APIExample).
-
----
-
-### Step 5: Update ARCHITECTURE.md
-
-Add one line to the case list in `ARCHITECTURE.md` under the correct directory section:
-
-```
-├── YourCaseName.kt # "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, Compose state) | `java/.../compose/samples/CaseName.kt` |
-| Display name | `res/values/strings.xml` |
-| List group (Basic ↔ Advance) or position | `java/.../compose/model/Examples.kt` (move entry between lists or reorder) |
-| Composable function rename | `CaseName.kt` (file + function name), `Examples.kt` (lambda reference), `ARCHITECTURE.md` |
-
-After making changes:
-
-1. **Verify `Examples.kt` entry consistency** — ensure the string resource reference, composable lambda, and list placement (`BasicExampleList` or `AdvanceExampleList`) still match the actual case. A mismatch causes the case to silently disappear from the list or render the wrong screen.
-2. **Update `res/values/strings.xml`** if the display name changed.
-3. **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
-
-```bash
-./gradlew assembleDebug
-```
-
-- [ ] Case appears in the correct group at the expected list position
-- [ ] Tap navigates to the case screen
-- [ ] Channel join succeeds and `isJoined` flips to `true`
-- [ ] Press back — check Logcat for `RtcEngine.destroy` within ~2 seconds; if missing, `DisposableEffect` key is wrong or `onDispose` is incomplete
-- [ ] Rotate screen — `channelName` and `isJoined` survive (`rememberSaveable` working)
-- [ ] `ARCHITECTURE.md` Case Index table is updated — row added (new case) or row updated (modified case) with correct Case, Path, Key APIs, and Description
-- [ ] `Examples.kt` entry is consistent — string resource, composable lambda, and list placement match the actual case
-
----
-
-## When to Use a Spec Instead
-
-If the case meets any of the following criteria, create a Spec rather than using this skill directly:
-
-1. Involves coordinated calls across two or more Agora API modules
-2. Requires a custom Composable layout not covered by the standard template above
-3. Manages multiple channels or multiple engine instances
-4. Requires a foreground Service or background coroutine coordination
-5. Involves developing new shared components (shared Composables / utils)
-6. Requires optional module integration (simpleFilter / streamEncrypt)
-
-If none apply → use this skill directly; no Spec needed.
-
-### Spec Requirements Document Must Include
-
-- List of APIs the case demonstrates
-- User interaction flow description
-- Expected RtcEngine lifecycle behavior
-- Required permissions list
-
-### Spec Design Document Must Include
-
-- Target project identifier: `APIExample-Compose`
-- Composable function structure design
-- API call sequence (Mermaid sequence diagram recommended)
-- State management plan (`remember` vs `rememberSaveable` boundaries)
-- UI layout plan
-- Integration points with existing shared components
-- Case registration info: `Examples.kt` list entry, `strings.xml` key (`example_` prefix) — finalize during design to avoid conflicts
-- Read `ARCHITECTURE.md` or use the `query-cases` skill to check existing entries
-- Compose-specific checks: `DisposableEffect(lifecycleOwner)`, `rememberSaveable` vs `remember`, main-thread dispatch for Toast/Dialog
-- Risk identification and mitigation (API compatibility, performance, permissions, thread safety, rotation/config changes)
-
-### 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
-- Tasks for creating new shared Composables must come before case implementation tasks
-
----
-
-## NEVER
-
-- **NEVER** use XML layouts, `Fragment`, or `ViewBinding` — Compose only.
-- **NEVER** use `remember` for channelName, isJoined, or uid — they must be `rememberSaveable` to survive rotation.
-- **NEVER** use `rememberSaveable` for `RtcEngine` — it is not serializable and will crash on rotation.
-- **NEVER** use `Unit` as the `DisposableEffect` key — it fires only once and won't clean up on back navigation. Always use `lifecycleOwner`.
-- **NEVER** put `@Preview` on the public stateful function — it will crash because `LocalContext` and `LocalLifecycleOwner` are unavailable in preview. Only preview the private `*View` function.
-- **NEVER** call `Toast`/`Dialog`/`AlertDialog` directly inside `IRtcEngineEventHandler` callbacks — they require the main thread. Use `coroutineScope.launch(Dispatchers.Main) { }`.
-- **NEVER** hardcode `mAreaCode` — always use `SettingPreferences.getArea()`.
diff --git a/Android/APIExample-Compose/.agent/skills/upsert-case/references/composable-template.kt b/Android/APIExample-Compose/.agent/skills/upsert-case/references/composable-template.kt
deleted file mode 100644
index 29d8c0da3..000000000
--- a/Android/APIExample-Compose/.agent/skills/upsert-case/references/composable-template.kt
+++ /dev/null
@@ -1,161 +0,0 @@
-package io.agora.api.example.compose.samples
-
-import android.Manifest
-import androidx.activity.compose.rememberLauncherForActivityResult
-import androidx.activity.result.contract.ActivityResultContracts
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.DisposableEffect
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableIntStateOf
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.saveable.rememberSaveable
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.platform.LocalLifecycleOwner
-import androidx.compose.ui.tooling.preview.Preview
-import io.agora.api.example.compose.data.SettingPreferences
-import io.agora.api.example.compose.ui.common.ChannelNameInput
-import io.agora.api.example.compose.utils.AgoraConfig
-import io.agora.api.example.compose.utils.TokenUtils
-import io.agora.rtc2.ChannelMediaOptions
-import io.agora.rtc2.Constants
-import io.agora.rtc2.IRtcEngineEventHandler
-import io.agora.rtc2.RtcEngine
-import io.agora.rtc2.RtcEngineConfig
-
-// For video cases, also import:
-// import io.agora.api.example.compose.ui.common.VideoGrid
-// import io.agora.rtc2.video.VideoCanvas
-// import io.agora.rtc2.video.VideoEncoderConfiguration
-
-/**
- * Demonstrates how to use [describe the feature here].
- *
- * Key APIs used:
- * - RtcEngine.yourApi()
- */
-// PUBLIC stateful entry point — no @Preview here
-@Composable
-fun YourCaseName() {
- val context = LocalContext.current
- val lifecycleOwner = LocalLifecycleOwner.current
-
- // rememberSaveable: survives rotation — use for channelName, isJoined, uid, videoIdList
- var isJoined by rememberSaveable { mutableStateOf(false) }
- var channelName by rememberSaveable { mutableStateOf("") }
- var localUid by rememberSaveable { mutableIntStateOf(0) }
-
- // remember: survives recomposition but NOT rotation — use for RtcEngine, collections
- val rtcEngine = remember {
- RtcEngine.create(RtcEngineConfig().apply {
- mAreaCode = SettingPreferences.getArea() // REQUIRED — do not hardcode
- mContext = context
- mAppId = AgoraConfig.getAppId()
- mEventHandler = object : IRtcEngineEventHandler() {
- // IRtcEngineEventHandler callbacks are safe to mutate Compose state directly —
- // the snapshot system is thread-safe. No runOnUIThread() needed.
- override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
- super.onJoinChannelSuccess(channel, uid, elapsed)
- isJoined = true
- localUid = uid
- }
-
- override fun onLeaveChannel(stats: RtcStats?) {
- super.onLeaveChannel(stats)
- isJoined = false
- }
-
- override fun onUserJoined(uid: Int, elapsed: Int) {
- super.onUserJoined(uid, elapsed)
- // add uid to videoIdList for video cases
- }
-
- override fun onUserOffline(uid: Int, reason: Int) {
- super.onUserOffline(uid, reason)
- // remove uid from videoIdList for video cases
- }
- }
- }).apply {
- // feature-specific engine setup goes here
- // e.g. enableVideo(); setVideoEncoderConfiguration(...)
- }
- }
-
- // MUST use lifecycleOwner as key — ensures cleanup fires when screen leaves composition
- DisposableEffect(lifecycleOwner) {
- onDispose {
- if (isJoined) rtcEngine.leaveChannel()
- RtcEngine.destroy()
- // NOTE: Toast/Dialog/AlertDialog MUST be called on main thread.
- // Inside onDispose this is fine. Inside IRtcEngineEventHandler callbacks,
- // wrap with: coroutineScope.launch(Dispatchers.Main) { ... }
- }
- }
-
- val permissionLauncher = rememberLauncherForActivityResult(
- ActivityResultContracts.RequestMultiplePermissions()
- ) { grantedMap ->
- // Permission callbacks run on main thread — Toast is safe here
- if (grantedMap.values.all { it }) {
- TokenUtils.gen(channelName, 0) { token ->
- val options = ChannelMediaOptions().apply {
- channelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING
- clientRoleType = Constants.CLIENT_ROLE_BROADCASTER
- publishMicrophoneTrack = true
- // publishCameraTrack = true // add for video cases
- }
- rtcEngine.joinChannel(token, channelName, 0, options)
- }
- }
- }
-
- // Delegate all UI to the private stateless View function
- YourCaseNameView(
- channelName = channelName,
- isJoined = isJoined,
- onJoinClick = { name ->
- channelName = name
- permissionLauncher.launch(
- arrayOf(Manifest.permission.RECORD_AUDIO)
- // add Manifest.permission.CAMERA for video cases
- )
- },
- onLeaveClick = { rtcEngine.leaveChannel() }
- )
-}
-
-// @Preview goes here on the PRIVATE stateless function — never on the stateful entry above
-@Preview
-@Composable
-private fun YourCaseNamePreview() {
- YourCaseNameView(
- channelName = "test",
- isJoined = false,
- onJoinClick = {},
- onLeaveClick = {}
- )
-}
-
-// PRIVATE stateless View — receives only plain data and lambdas, no engine/state
-@Composable
-private fun YourCaseNameView(
- channelName: String,
- isJoined: Boolean,
- onJoinClick: (String) -> Unit,
- onLeaveClick: () -> Unit
-) {
- Column(Modifier.fillMaxSize()) {
- // feature-specific UI here
- // For video cases: VideoGrid(videoIdList, setupVideo, ...)
- ChannelNameInput(
- channelName = channelName,
- isJoined = isJoined,
- onJoinClick = onJoinClick,
- onLeaveClick = onLeaveClick
- )
- }
-}
diff --git a/Android/APIExample-Compose/.agents/skills/query-cases/SKILL.md b/Android/APIExample-Compose/.agents/skills/query-cases/SKILL.md
new file mode 100644
index 000000000..391adba55
--- /dev/null
+++ b/Android/APIExample-Compose/.agents/skills/query-cases/SKILL.md
@@ -0,0 +1,50 @@
+---
+name: query-cases
+description: >
+ Query and browse existing API example cases in the APIExample-Compose Android demo —
+ lists cases by group, finds which Composables demonstrate a specific Agora API,
+ checks append position availability, and resolves display names from string
+ resources. Use when: someone asks what Compose cases exist, which APIs are
+ demonstrated, wants to find a case by name or API, needs to know the current list
+ position before adding a new case, or wants to know if a feature is already
+ implemented in Compose. Registration is manual via Examples.kt — no @Example
+ annotation. Keywords: list cases, find case, query cases, Examples.kt,
+ BasicExampleList, AdvanceExampleList, available cases, existing cases, which
+ case, is there a case, Compose case, Jetpack Compose.
+---
+
+## What this skill is for
+
+Use this skill when the question is about which Compose cases already exist in `APIExample-Compose/`, which Composables demonstrate a specific RTC API, where a case is registered in `Examples.kt`, or whether a new list position should be appended.
+
+## Source of truth
+
+1. `APIExample-Compose/ARCHITECTURE.md`
+2. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt`
+3. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/**`
+4. `APIExample-Compose/app/src/main/res/values/strings.xml`
+
+## Procedure
+
+1. Read `ARCHITECTURE.md` first for the fast case index and registration model.
+2. Re-scan live registrations from `Examples.kt` before claiming append position, registration presence, or list order.
+3. Resolve display names from `strings.xml` for any `R.string.example_*` entry referenced by `Examples.kt`.
+4. If the question is API-to-case mapping and `ARCHITECTURE.md` is insufficient, search across `samples/**` and use direct RTC API calls in the Composable source as the primary signal. Return all relevant matching cases, or the best-scoped matches if the API is shared by many samples.
+5. Return direct factual conclusions only. For list position questions, report the current list length and the next append position for the target list (`BasicExampleList` or `AdvanceExampleList`). If the target list is not explicit, infer it from the requested feature or call out that the answer depends on which list the case belongs in.
+
+## Verify
+
+- Re-check any append-position or registration answer against the current contents of `Examples.kt` before answering
+- Confirm any reported case name still matches the `strings.xml` resource referenced by the registered `Example(...)` entry
+- Confirm any API-to-case answer is backed by `ARCHITECTURE.md` or the actual Composable source under `samples/**`
+
+## Out of scope
+
+- Editing or registering cases
+- Looking for `@Example` annotations
+- Claiming `nav_graph.xml` registration exists for Compose cases
+
+## Never
+
+- Never use `item_` string keys for Compose examples
+- Never claim list position is an `@Example` index
diff --git a/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md b/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md
new file mode 100644
index 000000000..f2e2498e3
--- /dev/null
+++ b/Android/APIExample-Compose/.agents/skills/review-case/SKILL.md
@@ -0,0 +1,46 @@
+---
+name: review-case
+description: >
+ Review an existing Compose case implementation against project-specific red
+ lines and coding standards. Use after implementing or modifying a case. Use
+ when: reviewing a Compose case for correctness, checking red-line compliance,
+ verifying lifecycle and state patterns, auditing an existing Composable.
+ Keywords: review, audit, check, red lines, lifecycle, state, compliance,
+ Compose.
+---
+
+## What this skill is for
+
+Use this skill after a Compose case has been created or modified in `APIExample-Compose/`. It checks lifecycle, state, registration, and build verification before the case is treated as review-ready.
+
+## Source of truth
+
+1. `APIExample-Compose/AGENTS.md`
+2. `APIExample-Compose/ARCHITECTURE.md`
+3. The target case source file
+4. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt`
+5. `APIExample-Compose/app/src/main/res/values/strings.xml`
+
+## Procedure
+
+1. Audit lifecycle, permission, state, and threading-sensitive UI rules in the target Composable source.
+2. Check `Examples.kt`, `strings.xml`, and `ARCHITECTURE.md` for registration and documentation closure.
+3. Run the minimum build verification command from the Compose project.
+4. Report findings first, then verification results, then explicit unverified items if any required verification could not be completed.
+
+## Verify
+
+- Run `./gradlew assembleDebug` from `APIExample-Compose/`
+- Confirm `Examples.kt`, `strings.xml`, and `ARCHITECTURE.md` are aligned
+- Confirm engine lifecycle and cleanup are owned by `DisposableEffect(lifecycleOwner)`, and `rememberSaveable` / `remember` boundaries still match current project practice
+
+## Out of scope
+
+- Rewriting the case during review by default
+- Treating snapshot-safe state mutation as permission to show Toast or Dialog off the main thread
+
+## Never
+
+- Never approve `DisposableEffect(Unit)` for case cleanup
+- Never approve `RtcEngine` stored in `rememberSaveable`
+- Never skip the build command
diff --git a/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md b/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md
new file mode 100644
index 000000000..7224860c6
--- /dev/null
+++ b/Android/APIExample-Compose/.agents/skills/upsert-case/SKILL.md
@@ -0,0 +1,50 @@
+---
+name: upsert-case
+description: >
+ Add a new Compose API example case or modify an existing one in the
+ APIExample-Compose Android demo — updates Composable source, Examples.kt
+ registration, string resources, and architecture docs. Use when: adding a new
+ Agora RTC API demo screen in Jetpack Compose, modifying an existing case's
+ implementation or registration, or porting an APIExample case to Compose while
+ preserving Compose-only lifecycle and state rules.
+---
+
+## What this skill is for
+
+Use this skill to add or update a case in `APIExample-Compose/`. It owns the full change closure: Composable source, `Examples.kt` registration, localized `strings.xml` updates, and `ARCHITECTURE.md`.
+
+## Source of truth
+
+1. `APIExample-Compose/AGENTS.md`
+2. `APIExample-Compose/ARCHITECTURE.md`
+3. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/model/Examples.kt`
+4. `APIExample-Compose/app/src/main/java/io/agora/api/example/compose/samples/**`
+5. `APIExample-Compose/app/src/main/res/values/strings.xml`
+6. `APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt`
+
+## Procedure
+
+1. Run `query-cases` first when the target list placement or the closest nearby examples are unclear.
+2. Create or update the Composable source, `Examples.kt`, `strings.xml`, and `ARCHITECTURE.md` together as one change set.
+3. When the case title is user-facing, update both `res/values/strings.xml` and `res/values-zh/strings.xml` to keep locale coverage aligned with current project practice.
+4. Use `APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt` for Compose state, lifecycle, permission, and registration patterns.
+5. Keep the public stateful Composable, the private preview/view split, and the `Examples.kt` plus string registration alignment consistent.
+6. Treat this skill as the current source of truth for case-creation closure even if older sections in `ARCHITECTURE.md` still describe a smaller file set; update `ARCHITECTURE.md` as part of the same change.
+
+## Verify
+
+- Run `./gradlew assembleDebug` from `APIExample-Compose/`
+- Confirm the edited case updates `Examples.kt`, `strings.xml`, `values-zh/strings.xml` when needed, and `ARCHITECTURE.md`
+- Confirm the state model matches current project practice: `rememberSaveable` for UI/session state shown in the canonical Compose samples, and `remember` for `RtcEngine` and other non-serializable objects
+- Confirm the public Composable stays stateful and the preview stays on the private view function
+
+## Out of scope
+
+- Adding XML, Fragment, or ViewBinding code
+- Skipping docs or registration because the Composable compiles
+
+## Never
+
+- Never use `rememberSaveable` for `RtcEngine`
+- Never use `DisposableEffect(Unit)` for teardown
+- Never hardcode `mAreaCode`
diff --git a/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt b/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt
new file mode 100644
index 000000000..613c9c251
--- /dev/null
+++ b/Android/APIExample-Compose/.agents/skills/upsert-case/references/composable-template.kt
@@ -0,0 +1,112 @@
+package io.agora.api.example.compose.samples
+
+import android.Manifest
+import androidx.activity.compose.rememberLauncherForActivityResult
+import androidx.activity.result.contract.ActivityResultContracts
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableIntStateOf
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.saveable.rememberSaveable
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.platform.LocalLifecycleOwner
+import androidx.compose.ui.tooling.preview.Preview
+import io.agora.api.example.compose.BuildConfig
+import io.agora.api.example.compose.data.SettingPreferences
+import io.agora.api.example.compose.utils.TokenUtils
+import io.agora.rtc2.ChannelMediaOptions
+import io.agora.rtc2.IRtcEngineEventHandler
+import io.agora.rtc2.RtcEngine
+import io.agora.rtc2.RtcEngineConfig
+import io.agora.rtc2.RtcStats
+
+@Composable
+fun ExampleCase() {
+ val context = LocalContext.current
+ val lifecycleOwner = LocalLifecycleOwner.current
+ var channelName by rememberSaveable { mutableStateOf("") }
+ var isJoined by rememberSaveable { mutableStateOf(false) }
+ var localUid by rememberSaveable { mutableIntStateOf(0) }
+
+ val rtcEngine = remember {
+ RtcEngine.create(RtcEngineConfig().apply {
+ mAreaCode = SettingPreferences.getArea()
+ mContext = context
+ mAppId = BuildConfig.AGORA_APP_ID
+ mEventHandler = object : IRtcEngineEventHandler() {
+ override fun onJoinChannelSuccess(channel: String?, uid: Int, elapsed: Int) {
+ super.onJoinChannelSuccess(channel, uid, elapsed)
+ isJoined = true
+ localUid = uid
+ }
+
+ override fun onLeaveChannel(stats: RtcStats?) {
+ super.onLeaveChannel(stats)
+ isJoined = false
+ localUid = 0
+ }
+
+ // Add case-specific callbacks here for remote users, stats, or custom media flows.
+ }
+ })
+ }
+
+ DisposableEffect(lifecycleOwner) {
+ onDispose {
+ if (isJoined) {
+ rtcEngine.leaveChannel()
+ }
+ RtcEngine.destroy()
+ }
+ }
+
+ val permissionLauncher = rememberLauncherForActivityResult(
+ ActivityResultContracts.RequestMultiplePermissions()
+ ) { grantedMap ->
+ if (grantedMap.values.all { it }) {
+ TokenUtils.gen(channelName, 0) { token ->
+ rtcEngine.joinChannel(token, channelName, 0, ChannelMediaOptions())
+ }
+ }
+ }
+ val requiredPermissions = arrayOf(
+ Manifest.permission.RECORD_AUDIO,
+ // Add Manifest.permission.CAMERA for video-capable cases.
+ )
+
+ ExampleCaseView(
+ channelName = channelName,
+ isJoined = isJoined,
+ onJoinClick = { newChannelName ->
+ channelName = newChannelName
+ permissionLauncher.launch(requiredPermissions)
+ },
+ onLeaveClick = {
+ rtcEngine.leaveChannel()
+ }
+ )
+}
+
+@Preview
+@Composable
+private fun ExampleCasePreview() {
+ ExampleCaseView(
+ channelName = "Channel Name",
+ isJoined = false,
+ onJoinClick = {},
+ onLeaveClick = {}
+ )
+}
+
+@Composable
+private fun ExampleCaseView(
+ channelName: String,
+ isJoined: Boolean,
+ onJoinClick: (String) -> Unit,
+ onLeaveClick: () -> Unit
+) {
+ // Replace with case-specific UI while keeping preview on this private view function.
+}
diff --git a/Android/APIExample-Compose/AGENTS.md b/Android/APIExample-Compose/AGENTS.md
index 3e63c9283..39b131843 100644
--- a/Android/APIExample-Compose/AGENTS.md
+++ b/Android/APIExample-Compose/AGENTS.md
@@ -29,9 +29,9 @@ See [README.md — Obtain an App Id](README.md#obtain-an-app-id).
| Skill | Path | Description |
|-------|------|-------------|
-| upsert-case | `.agent/skills/upsert-case/` | Add a new Compose case or modify an existing one |
-| query-cases | `.agent/skills/query-cases/` | Query and browse existing Compose cases |
-| review-case | `.agent/skills/review-case/` | Review a case against project red lines |
+| upsert-case | `.agents/skills/upsert-case/` | Add a new Compose case or modify an existing one |
+| query-cases | `.agents/skills/query-cases/` | Query and browse existing Compose cases |
+| review-case | `.agents/skills/review-case/` | Review a case against project red lines |
## Further Reading
diff --git a/Android/APIExample-Compose/ARCHITECTURE.md b/Android/APIExample-Compose/ARCHITECTURE.md
index 3b70c3991..bf757f933 100644
--- a/Android/APIExample-Compose/ARCHITECTURE.md
+++ b/Android/APIExample-Compose/ARCHITECTURE.md
@@ -7,16 +7,16 @@ APIExample-Compose/
├── gradle.properties # rtc_sdk_version
├── AGENTS.md # Agent entry point — build commands, red lines, skill index
├── ARCHITECTURE.md # This file — directory layout, patterns, registration
-├── .kiro/
-│ ├── hooks/
-│ │ └── build-on-task-complete.json # Runs assembleDebug after each spec task completes
-│ ├── skills/
-│ │ ├── add-new-case/SKILL.md # Step-by-step guide for adding a new Compose case
-│ │ └── query-cases/SKILL.md # Query existing cases by API, group, or list position
-│ └── steering/
-│ ├── project-routing.md # Which sub-project to use; hard constraints (always included)
-│ ├── coding-standards.md # RtcEngine lifecycle, Kotlin/Compose rules (always included)
-│ └── complex-case-spec.md # Spec workflow for complex cases (manual inclusion)
+├── .agents/
+│ └── skills/
+│ ├── upsert-case/
+│ │ ├── SKILL.md # Add or update a Compose case
+│ │ └── references/
+│ │ └── composable-template.kt # Compose lifecycle/state reference skeleton
+│ ├── query-cases/
+│ │ └── SKILL.md # Query existing cases by API, group, or list position
+│ └── review-case/
+│ └── SKILL.md # Review a Compose case against project red lines
└── app/src/main/
├── AndroidManifest.xml
├── assets/ # Audio/video sample files
@@ -127,7 +127,7 @@ APIExample-Compose/
Registration is **manual** — no reflection, no annotation scanning.
-**To add a case, edit exactly two files:**
+**To add a case, update at least four project-local artifacts:**
**1. `model/Examples.kt`** — append to `BasicExampleList` or `AdvanceExampleList`:
```kotlin
@@ -143,8 +143,14 @@ val AdvanceExampleList = listOf(
fun MyNewCase() { … }
```
-No `nav_graph.xml`, no `@Example` annotation, no action ID. `NavGraph.kt` routes to cases by their
-index in the list — the order in `Examples.kt` is the display order.
+**3. `res/values/strings.xml`** — add the user-facing example title.
+
+**4. `ARCHITECTURE.md`** — update the case index so discovery tooling stays current.
+
+Update `res/values-zh/strings.xml` too when the case title should remain localized alongside the existing examples.
+
+No `nav_graph.xml`, no `@Example` annotation, and no action ID. `NavGraph.kt` routes to cases by their
+position in the list — the order in `Examples.kt` is the display order inside the target list.
## Composable Case Pattern
diff --git a/Android/APIExample/.agent/skills/query-cases/SKILL.md b/Android/APIExample/.agent/skills/query-cases/SKILL.md
deleted file mode 100644
index ee1944957..000000000
--- a/Android/APIExample/.agent/skills/query-cases/SKILL.md
+++ /dev/null
@@ -1,110 +0,0 @@
----
-name: query-cases
-description: >
- Query and browse existing API example cases in the APIExample Android demo — lists
- cases by group, finds which case demonstrates a specific Agora API, checks sort
- index availability, and resolves display names from string resources. Use when:
- someone asks what cases exist, which APIs are demonstrated, wants to find a case
- by name or API (e.g. takeSnapshot, setClientRole), needs a free sort index before
- adding a new case, or wants to know if a feature is already implemented.
- Keywords: list cases, find case, query cases, @Example, sort index, BASIC, ADVANCED,
- available cases, existing cases, which case, is there a case.
----
-
-# Query Cases — APIExample
-
-## How cases are registered
-
-Every case is a Fragment under `app/src/main/java/io/agora/api/example/examples/{basic|advanced|audio}/` with an `@Example` annotation:
-
-```java
-@Example(
- index = 10, // unique within the group; BASIC: 0–9, ADVANCED: 10+
- group = ADVANCED,
- name = R.string.item_xxx,
- actionId = R.id.action_mainFragment_to_xxx,
- tipsId = R.string.xxx_tips
-)
-```
-
-A commented-out `@Example` (`//@Example`) means the case is disabled and won't appear in the app.
-
----
-
-## Query procedure
-
-### Step 1: Decide scope before scanning
-
-Before listing files, ask:
-- **Looking for a specific API?** — scan Javadoc comments for the API name; no need to read all files
-- **Need a free sort index?** — collect all `index` values for the target group, then find the gap
-- **Listing all cases?** — scan all three directories and collect annotations
-
-### Step 2: Read ARCHITECTURE.md first
-
-Read `ARCHITECTURE.md` (the `examples/` section of the Directory Layout). It contains a pre-built index of all cases with group, index, display name, and key API — no file scanning needed for most queries.
-
-Use ARCHITECTURE.md as the primary source. Fall back to scanning the source directories only when:
-- The query requires data not in ARCHITECTURE.md (e.g. full `@Example` field values, `tipsId`)
-- ARCHITECTURE.md appears stale (a case exists in source but not in the doc)
-- The output involves free-index claims, index collisions, or "is index X available?" decisions — these must be validated from source immediately before final output
-
-### Step 3: Scan case directories (fallback only)
-
-| Directory | Group | Contents |
-|-----------|-------|----------|
-| `examples/basic/` | BASIC | Core join/leave patterns |
-| `examples/advanced/` | ADVANCED | Feature-specific APIs |
-| `examples/audio/` | ADVANCED | Audio-specific cases (still grouped ADVANCED) |
-
-Each `.java` file is a case. Subdirectories (e.g. `customaudio/`) contain multi-file cases — the main class is the file whose name matches the directory name (e.g. `customaudio/CustomAudioSource.java`). If no name match, look for the file containing `@Example`.
-
-### Step 4: Extract `@Example` fields
-
-For each file, read the annotation for `group`, `index`, `name` (string resource ID), and `tipsId`. If the annotation is commented out, the case is disabled.
-
-Resolve display names from `app/src/main/res/values/strings.xml`:
-`R.string.item_video_snapshot` → `Video Snapshot`
-
-### Step 5: Read class Javadoc for API mapping
-
-The Javadoc above each class lists the key APIs demonstrated:
-
-```java
-/**
- * This demo demonstrates how to take a snapshot of the local video stream.
- *
- * Key APIs used:
- * - RtcEngine.takeSnapshot()
- */
-```
-
-Use this to answer "which case uses X?" queries without reading the full implementation.
-
-If no Javadoc is present, scan the method body for the API name as a method call. If still not found, note "API mapping unavailable" in the results table.
-
-### Step 6: Present results
-
-Full listing — table format:
-
-| Group | Index | Case Name | File | Key APIs |
-|-------|-------|-----------|------|----------|
-| BASIC | 0 | Join Channel Video | JoinChannelVideo.java | joinChannel(), setupLocalVideo() |
-| ADVANCED | 10 | Video Snapshot | VideoSnapshot.java | takeSnapshot() |
-
-For a specific query (e.g. "which case uses takeSnapshot?"), return only matching rows.
-
-For a free-index query, list all used indices in the target group and identify the next available slot:
-> BASIC range: 0–9. ADVANCED range: 10+.
-> ADVANCED indices in use: 10, 11, 12, 15, 20 → next free: 13
-
-Before returning any free-index/collision result, re-scan source registration points (`@Example` across `basic/`, `advanced/`, `audio/`) and recompute once from source-of-truth data.
-
----
-
-## NEVER
-
-- **NEVER** count a commented-out `@Example` (`//@Example`) as an active case — it is disabled and won't appear in the app.
-- **NEVER** mix index spaces across groups — `audio/` cases use `group=ADVANCED` but share the same index namespace as `advanced/`; always scan both directories together when finding a free index.
-- **NEVER** use filename alone to identify a subdirectory case — the main class is the file whose name matches the directory name; if no match, look for the file with `@Example`.
-- **NEVER** report a free index without scanning all three directories (`basic/`, `advanced/`, `audio/`) for the target group — missing one causes index collisions.
diff --git a/Android/APIExample/.agent/skills/review-case/SKILL.md b/Android/APIExample/.agent/skills/review-case/SKILL.md
deleted file mode 100644
index 6caabc6ce..000000000
--- a/Android/APIExample/.agent/skills/review-case/SKILL.md
+++ /dev/null
@@ -1,52 +0,0 @@
----
-name: review-case
-description: >
- Review an existing case implementation against project-specific red lines
- and coding standards. Use after implementing or modifying a case.
- Use when: reviewing a case for correctness, checking red-line compliance,
- verifying lifecycle and threading patterns, auditing an existing Fragment.
- Keywords: review, audit, check, red lines, lifecycle, threading, compliance.
----
-
-# Review Case — APIExample
-
-Run through every item below before considering a case implementation complete.
-Open the case's Fragment source file and verify each point against the actual code.
-
-## Checklist
-
-### Teardown & Lifecycle
-
-- [ ] **leaveChannel before destroy** — `engine.leaveChannel()` is called before `RtcEngine.destroy()` in the teardown path (typically `onDestroy()`). Destroying without leaving first leaks the channel session on the server side.
-
-- [ ] **handler.post for destroy** — `RtcEngine.destroy()` is invoked via `handler.post(RtcEngine::destroy)` and **not** called directly on the main thread. A direct call blocks the UI thread and causes ANR.
-
-### Threading
-
-- [ ] **runOnUIThread for callbacks** — All `IRtcEngineEventHandler` callbacks that update UI are wrapped with `runOnUIThread()`. SDK callbacks arrive on a background thread; touching Views without dispatching to the main thread causes crashes or silent rendering corruption.
-
-### Permissions
-
-- [ ] **Permission check before join** — `checkOrRequestPermission()` is called before `joinChannel()`. Joining without the required permissions (RECORD_AUDIO, and CAMERA for video cases) causes a silent failure — no error callback, just no audio/video.
-
-### Backend Reporting
-
-- [ ] **setParameters present** — `setParameters(...)` is called during engine initialisation. This is required for Agora backend usage reporting in every case; omitting it causes silent reporting failure even though the app appears to work normally.
-
-### Private Cloud
-
-- [ ] **getPrivateCloudConfig null-check** — `getPrivateCloudConfig()` is null-checked before `setLocalAccessPoint()` is called. The method returns `null` on standard (non-private-cloud) builds, so calling `setLocalAccessPoint()` without the guard causes a NullPointerException.
-
-## If a Check Fails
-
-- Teardown order wrong (`destroy` before `leaveChannel`) — fix teardown to `leaveChannel()` first, then `handler.post(RtcEngine::destroy)`, and re-test back navigation.
-- UI touched in SDK callback without main-thread dispatch — wrap UI updates in `runOnUIThread()` and re-run the case to verify no thread exceptions.
-- Permission flow missing before `joinChannel()` — add `checkOrRequestPermission()` gate and verify join succeeds only after permission is granted.
-- Missing `setParameters(...)` or private-cloud null-check — add both safeguards in engine init and re-run the init path once.
-
-## NEVER
-
-- **NEVER** approve a case review with direct `RtcEngine.destroy()` on main thread.
-- **NEVER** approve a case review when `leaveChannel()` is missing before destroy.
-- **NEVER** ignore background-thread UI updates inside `IRtcEngineEventHandler` callbacks.
-- **NEVER** assume runtime behavior is correct without at least one back-navigation teardown check in Logcat.
diff --git a/Android/APIExample/.agent/skills/upsert-case/SKILL.md b/Android/APIExample/.agent/skills/upsert-case/SKILL.md
deleted file mode 100644
index 25211787d..000000000
--- a/Android/APIExample/.agent/skills/upsert-case/SKILL.md
+++ /dev/null
@@ -1,342 +0,0 @@
----
-name: upsert-case
-description: >
- Add a new API example case or modify an existing one in the APIExample Android demo —
- creates or updates Fragment class, XML layout, string resources, and nav_graph registration.
- Use when: adding a new Agora RTC API demo screen, modifying an existing case's implementation
- or registration, implementing a new feature example in Java + XML layouts, registering a new
- case via @Example annotation, subclassing BaseFragment for a new demo screen, or updating
- an existing case's strings, layout, or nav entry. Keywords: add case, modify case, update case,
- new fragment, nav_graph, @Example, BaseFragment, APIExample, new screen, demo case, RTC API example.
----
-
-# Upsert Case — APIExample
-
-## 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.
-
----
-
-### Step 1: Clarify before coding
-
-Before writing a single line, ask:
-- **What API am I demonstrating?** — determines which existing case is the closest reference to copy patterns from
-- **Video or audio-only?** — determines permissions (`CAMERA` + `RECORD_AUDIO` vs `RECORD_AUDIO` only), layout complexity, and whether `VideoReportLayout` is needed
-- **BASIC or ADVANCED group?** — BASIC for fundamental channel join/leave patterns; ADVANCED for feature-specific 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
-
----
-
-### Step 2: Create the Fragment
-
-**MANDATORY — READ ENTIRE FILE before writing any code**:
-[`references/fragment-template.java`](references/fragment-template.java)
-
-Do NOT skip — the `setParameters`, `handler.post`, and `getPrivateCloudConfig()` null-check patterns 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)`
-
-For video cases, add `VideoReportLayout` fields and wire `setupRemoteVideo` in `onUserJoined`/`onUserOffline`.
-
----
-
-### Step 3: Create the XML layout
-
-Minimum structure — channel input + join button at bottom:
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-For video cases, use `VideoReportLayout` for each video slot. Pick one of the four standard layouts below — they cover the vast majority of cases.
-
-**General rules (apply to all layouts):**
-- Video containers must sit **above** the bottom control bar. In `RelativeLayout` use `android:layout_above="@id/ll_join"`; in `ConstraintLayout` use `app:layout_constraintBottom_toTopOf="@id/ll_join"`.
-- Each `VideoReportLayout` needs a unique `android:id` (`fl_local`, `fl_remote`, `fl_remote2`, …).
-
----
-
-**Layout A — Single broadcaster (local fullscreen)**
-Use when: broadcaster-only demo, no remote video needed.
-
-```xml
-
-
-```
-
----
-
-**Layout B — 1v1 (local left, remote right, side by side)**
-Use when: two-party call, equal-weight split.
-
-```xml
-
-
-
-
-
-
-
-```
-
----
-
-**Layout C — Audience co-hosting (remote fullscreen background + local PiP top-right)**
-Use when: live streaming where audience co-hosts; remote/host fills screen, local is a small overlay.
-
-```xml
-
-
-
-
-```
-
----
-
-**Layout D — 2×2 grid (up to 4 participants)**
-Use when: multi-party call with up to 4 streams.
-
-```xml
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
----
-
-### Step 4: Add nav entries
-
-File: `res/navigation/nav_graph.xml`
-
-**Action** — inside `` (NOT mainFragment — mainFragment only has one action, to Ready):
-
-```xml
-
-```
-
-**Destination** — at root `` level:
-
-```xml
-
-```
-
-`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, video containers) | `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:
-
-1. **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.
-2. **Update `res/values/strings.xml`** if the display name or tips text changed.
-3. **Update `res/navigation/nav_graph.xml`** if the class name, package, or label changed.
-4. **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
-
-```bash
-./gradlew assembleDebug
-```
-
-- [ ] Case appears in correct group at expected sort position
-- [ ] Tap navigates to the case screen (silent failure = nav action in wrong fragment)
-- [ ] `onJoinChannelSuccess` fires in Logcat
-- [ ] After pressing back, check Logcat for `RtcEngine.destroy` within ~2 seconds — if missing, there is a lifecycle bug in `onDestroy`
-- [ ] `ARCHITECTURE.md` Case Index table is updated — row added (new case) or row updated (modified case) with correct Case, Path, Key APIs, and Description
-- [ ] `@Example` annotation fields (`index`, `group`, `name`, `actionId`, `tipsId`) are consistent with string resources and nav_graph entries
-
----
-
-## When to Use a Spec Instead
-
-If the case meets any of the following criteria, create a Spec rather than using this skill directly:
-
-1. Involves coordinated calls across two or more Agora API modules
-2. Requires a custom UI layout (not one of the standard Layout A/B/C/D templates above)
-3. Involves multi-channel or multi-engine instance management
-4. Requires a foreground Service or background thread coordination
-5. Involves developing new shared components (widgets/utils, etc.)
-6. Requires optional module integration (simpleFilter/streamEncrypt)
-
-If none apply → use this skill directly; no Spec needed.
-
-### Spec Requirements Document Must Include
-
-- List of APIs the case demonstrates
-- User interaction flow description
-- Expected RtcEngine lifecycle behavior
-- Required permissions list
-
-### Spec Design Document Must Include
-
-- Target project identifier: `APIExample`
-- 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
-- Risk identification and mitigation (API compatibility, performance, permissions, thread safety)
-
-### 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
-- Tasks for creating new shared components must come before case implementation tasks
-
----
-
-## NEVER
-
-- **NEVER** put the nav action inside `` — it belongs in ``. 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.
diff --git a/Android/APIExample/.agent/skills/upsert-case/references/fragment-template.java b/Android/APIExample/.agent/skills/upsert-case/references/fragment-template.java
deleted file mode 100644
index a96649654..000000000
--- a/Android/APIExample/.agent/skills/upsert-case/references/fragment-template.java
+++ /dev/null
@@ -1,207 +0,0 @@
-package io.agora.api.example.examples.advanced;
-
-import static io.agora.api.example.common.model.Examples.ADVANCED;
-import static io.agora.rtc2.Constants.RENDER_MODE_HIDDEN;
-
-import android.content.Context;
-import android.os.Bundle;
-import android.util.Log;
-import android.view.LayoutInflater;
-import android.view.SurfaceView;
-import android.view.View;
-import android.view.ViewGroup;
-import android.widget.Button;
-import android.widget.EditText;
-import android.widget.FrameLayout;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.Nullable;
-
-import java.util.Map;
-import java.util.Random;
-import java.util.concurrent.ConcurrentHashMap;
-
-import io.agora.api.example.MainApplication;
-import io.agora.api.example.R;
-import io.agora.api.example.annotation.Example;
-import io.agora.api.example.common.BaseFragment;
-import io.agora.api.example.common.widget.VideoReportLayout;
-import io.agora.api.example.utils.PermissonUtils;
-import io.agora.api.example.utils.TokenUtils;
-import io.agora.rtc2.ChannelMediaOptions;
-import io.agora.rtc2.Constants;
-import io.agora.rtc2.IRtcEngineEventHandler;
-import io.agora.rtc2.RtcEngine;
-import io.agora.rtc2.RtcEngineConfig;
-import io.agora.rtc2.proxy.LocalAccessPointConfiguration;
-import io.agora.rtc2.video.VideoCanvas;
-
-/**
- * This demo demonstrates how to use [describe the feature here].
- *
- * Key APIs used:
- * - RtcEngine.yourApi()
- */
-@Example(
- index = 10, // unique within the group; BASIC: 0–9, ADVANCED: 10+
- group = ADVANCED, // BASIC or ADVANCED
- name = R.string.item_your_case_name,
- actionId = R.id.action_mainFragment_to_yourCaseName,
- tipsId = R.string.your_case_name_tips
-)
-public class YourCaseName extends BaseFragment implements View.OnClickListener {
- private static final String TAG = YourCaseName.class.getSimpleName();
-
- // For video cases: add VideoReportLayout fields here
- // private VideoReportLayout fl_local, fl_remote;
- // private Map remoteViews = new ConcurrentHashMap<>();
-
- private Button join;
- private EditText et_channel;
- private RtcEngine engine;
- private int myUid;
- private boolean joined = false;
-
- @Nullable
- @Override
- public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
- @Nullable Bundle savedInstanceState) {
- return inflater.inflate(R.layout.fragment_your_case_name, container, false);
- }
-
- @Override
- public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
- super.onViewCreated(view, savedInstanceState);
- join = view.findViewById(R.id.btn_join);
- et_channel = view.findViewById(R.id.et_channel);
- join.setOnClickListener(this);
- // bind additional views here
- }
-
- @Override
- public void onActivityCreated(@Nullable Bundle savedInstanceState) {
- super.onActivityCreated(savedInstanceState);
- Context context = getContext();
- if (context == null) return;
- try {
- RtcEngineConfig config = new RtcEngineConfig();
- config.mContext = context.getApplicationContext();
- config.mAppId = getAgoraAppId();
- config.mChannelProfile = Constants.CHANNEL_PROFILE_LIVE_BROADCASTING;
- config.mEventHandler = iRtcEngineEventHandler;
- config.mAudioScenario = Constants.AudioScenario.getValue(Constants.AudioScenario.DEFAULT);
- config.mAreaCode = ((MainApplication) getActivity().getApplication())
- .getGlobalSettings().getAreaCode();
- engine = RtcEngine.create(config);
- // REQUIRED in every case — do not remove
- engine.setParameters("{"
- + "\"rtc.report_app_scenario\":"
- + "{"
- + "\"appScenario\":" + 100 + ","
- + "\"serviceType\":" + 11 + ","
- + "\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\""
- + "}"
- + "}");
- // null-check is mandatory — returns null on non-private-cloud builds
- LocalAccessPointConfiguration localAccessPointConfiguration =
- ((MainApplication) getActivity().getApplication())
- .getGlobalSettings().getPrivateCloudConfig();
- if (localAccessPointConfiguration != null) {
- engine.setLocalAccessPoint(localAccessPointConfiguration);
- }
- } catch (Exception e) {
- e.printStackTrace();
- getActivity().onBackPressed();
- }
- }
-
- @Override
- public void onDestroy() {
- super.onDestroy();
- if (engine != null) {
- engine.leaveChannel();
- }
- // MUST use handler.post — do NOT call RtcEngine.destroy() directly on main thread
- handler.post(RtcEngine::destroy);
- engine = null;
- }
-
- @Override
- public void onClick(View v) {
- if (v.getId() == R.id.btn_join) {
- if (!joined) {
- String channelId = et_channel.getText().toString();
- checkOrRequestPermisson(new PermissonUtils.PermissionResultCallback() {
- @Override
- public void onPermissionsResult(boolean allPermissionsGranted,
- String[] permissions, int[] grantResults) {
- if (allPermissionsGranted) {
- joinChannel(channelId);
- }
- }
- });
- } else {
- joined = false;
- engine.leaveChannel();
- join.setText(getString(R.string.join));
- }
- }
- }
-
- private void joinChannel(String channelId) {
- engine.setClientRole(Constants.CLIENT_ROLE_BROADCASTER);
- // --- feature-specific setup goes here ---
- // e.g. engine.enableVideo(); engine.setVideoEncoderConfiguration(...);
-
- ChannelMediaOptions options = new ChannelMediaOptions();
- options.autoSubscribeAudio = true;
- options.autoSubscribeVideo = true;
- options.publishMicrophoneTrack = true;
- options.publishCameraTrack = true; // remove for audio-only cases
-
- int uid = new Random().nextInt(1000) + 100000;
- TokenUtils.gen(requireContext(), channelId, uid, token -> {
- int res = engine.joinChannel(token, channelId, uid, options);
- if (res != 0) {
- showAlert(RtcEngine.getErrorDescription(Math.abs(res)));
- return;
- }
- join.setEnabled(false);
- });
- }
-
- private final IRtcEngineEventHandler iRtcEngineEventHandler = new IRtcEngineEventHandler() {
- @Override
- public void onJoinChannelSuccess(String channel, int uid, int elapsed) {
- Log.i(TAG, String.format("onJoinChannelSuccess channel %s uid %d", channel, uid));
- myUid = uid;
- joined = true;
- // ALL UI updates must go through runOnUIThread — callbacks run on background thread
- runOnUIThread(() -> {
- join.setEnabled(true);
- join.setText(getString(R.string.leave));
- });
- }
-
- @Override
- public void onUserJoined(int uid, int elapsed) {
- Log.i(TAG, "onUserJoined -> " + uid);
- runOnUIThread(() -> {
- // For video cases: create SurfaceView, call engine.setupRemoteVideo(...)
- });
- }
-
- @Override
- public void onUserOffline(int uid, int reason) {
- Log.i(TAG, String.format("user %d offline, reason %d", uid, reason));
- runOnUIThread(() -> {
- // For video cases: removeAllViews(), call engine.setupRemoteVideo(null, ...)
- });
- }
-
- @Override
- public void onError(int err) {
- showLongToast("Error code:" + err + ", msg:" + RtcEngine.getErrorDescription(err));
- }
- };
-}
diff --git a/Android/APIExample/.agents/skills/query-cases/SKILL.md b/Android/APIExample/.agents/skills/query-cases/SKILL.md
new file mode 100644
index 000000000..76682b221
--- /dev/null
+++ b/Android/APIExample/.agents/skills/query-cases/SKILL.md
@@ -0,0 +1,50 @@
+---
+name: query-cases
+description: >
+ Query and browse existing API example cases in the APIExample Android demo — lists
+ cases by group, finds which case demonstrates a specific Agora API, checks sort
+ index availability, and resolves display names from string resources. Use when:
+ someone asks what cases exist, which APIs are demonstrated, wants to find a case
+ by name or API (e.g. takeSnapshot, setClientRole), needs a free sort index before
+ adding a new case, or wants to know if a feature is already implemented.
+ Keywords: list cases, find case, query cases, @Example, sort index, BASIC, ADVANCED,
+ available cases, existing cases, which case, is there a case.
+---
+
+## What this skill is for
+
+Use this skill when the question is about what cases already exist in `APIExample/`, which case demonstrates a specific RTC API, whether a BASIC or ADVANCED slot is free, or whether a case is fully registered. This skill answers inventory and registration questions only; it does not edit files.
+
+## Source of truth
+
+1. `APIExample/ARCHITECTURE.md`
+2. `APIExample/app/src/main/java/io/agora/api/example/examples/**`
+3. `APIExample/app/src/main/res/navigation/nav_graph.xml`
+4. `APIExample/app/src/main/res/values/strings.xml`
+
+## Procedure
+
+1. Read `ARCHITECTURE.md` first for the fast case index.
+2. Re-scan live `@Example` registrations from source before claiming index availability or whether a case is fully registered.
+3. For any registration answer, cross-check the active `@Example` entry against matching `actionId`, nav action, destination in `nav_graph.xml`, and a valid display-name string resource in `strings.xml`.
+4. If `ARCHITECTURE.md` is stale or insufficient for API-to-case lookup, fall back to class Javadoc or direct API calls under `app/src/main/java/io/agora/api/example/examples/**`.
+5. Resolve display names from `strings.xml`.
+6. Return tables or direct factual conclusions only.
+
+## Verify
+
+- Re-check any free-slot or registration claim against live active `@Example` annotations before answering
+- Confirm any registration answer is backed by matching `@Example`, nav action or destination, and display-name string resource
+- Confirm any reported case name still matches the `strings.xml` resource used by the annotated entry
+
+## Out of scope
+
+- Editing or registering cases
+- Reviewing lifecycle quality
+- Approving implementation correctness
+
+## Never
+
+- Never report a free ADVANCED index without scanning both `advanced/` and `audio/`
+- Never count a commented-out `//@Example` entry as an active registration
+- Never rely on filename alone when a subdirectory case may hide the annotated entry point
diff --git a/Android/APIExample/.agents/skills/review-case/SKILL.md b/Android/APIExample/.agents/skills/review-case/SKILL.md
new file mode 100644
index 000000000..f851a1e74
--- /dev/null
+++ b/Android/APIExample/.agents/skills/review-case/SKILL.md
@@ -0,0 +1,45 @@
+---
+name: review-case
+description: >
+ Review an existing case implementation against project-specific red lines
+ and coding standards. Use after implementing or modifying a case.
+ Use when: reviewing a case for correctness, checking red-line compliance,
+ verifying lifecycle and threading patterns, auditing an existing Fragment.
+ Keywords: review, audit, check, red lines, lifecycle, threading, compliance.
+---
+
+## What this skill is for
+
+Use this skill after a case has been created or modified in `APIExample/`. It checks project red lines, registration closure, and minimum executable verification before the case is treated as review-ready.
+
+## Source of truth
+
+1. `APIExample/AGENTS.md`
+2. `APIExample/ARCHITECTURE.md`
+3. The target case source file
+4. `APIExample/app/src/main/res/navigation/nav_graph.xml`
+5. `APIExample/app/src/main/res/values/strings.xml`
+
+## Procedure
+
+1. Audit lifecycle, permission, threading, and registration rules.
+2. Check `@Example`, `nav_graph.xml`, `strings.xml`, and `ARCHITECTURE.md` for closure.
+3. Run the minimum build verification command.
+4. Report findings first, then verification results, then explicit unverified items.
+
+## Verify
+
+- Run `./gradlew assembleDebug` from `APIExample/`
+- Confirm the target case still appears to satisfy `@Example` + action + destination + string alignment
+
+## Out of scope
+
+- Re-implementing the case from scratch
+- Making silent code fixes during review
+- Claiming runtime behavior was validated if only compile checks ran
+
+## Never
+
+- Never approve direct `RtcEngine.destroy()` on the main thread
+- Never approve missing `leaveChannel()` before destroy
+- Never skip the build command
diff --git a/Android/APIExample/.agents/skills/upsert-case/SKILL.md b/Android/APIExample/.agents/skills/upsert-case/SKILL.md
new file mode 100644
index 000000000..255a6e65b
--- /dev/null
+++ b/Android/APIExample/.agents/skills/upsert-case/SKILL.md
@@ -0,0 +1,51 @@
+---
+name: upsert-case
+description: >
+ Add a new API example case or modify an existing one in the APIExample Android demo —
+ creates or updates Fragment class, XML layout, string resources, nav_graph registration,
+ and architecture docs. Use when: adding a new Agora RTC API demo screen, modifying an
+ existing case's implementation or registration, implementing a new feature example in
+ Java + XML layouts, registering a new case via @Example annotation, subclassing
+ BaseFragment for a new demo screen, or updating an existing case's strings, layout, nav
+ entry, or architecture docs. Keywords: add case, modify case, update case, new fragment,
+ nav_graph, @Example, BaseFragment, APIExample, new screen, demo case, RTC API example.
+---
+
+## What this skill is for
+
+Use this skill to add a new case or update an existing case in `APIExample/`. It owns the full change closure: case source, XML layout, strings, `nav_graph.xml`, and `ARCHITECTURE.md`.
+
+## Source of truth
+
+1. `APIExample/AGENTS.md`
+2. `APIExample/ARCHITECTURE.md`
+3. `APIExample/app/src/main/java/io/agora/api/example/examples/**`
+4. `APIExample/app/src/main/res/navigation/nav_graph.xml`
+5. `APIExample/app/src/main/res/values/strings.xml`
+6. `APIExample/.agents/skills/upsert-case/references/fragment-template.java`
+
+## Procedure
+
+1. Run `query-cases` first when index or placement is unknown.
+2. Create or update the Fragment, layout, strings, navigation entries, and `ARCHITECTURE.md`.
+3. Use the reference template for lifecycle, reporting, and private-cloud guards.
+4. Keep `@Example`, action ID, destination ID, and string resources aligned.
+5. Treat the reference template as a pattern source, not a drop-in class.
+
+## Verify
+
+- Run `./gradlew assembleDebug` from `APIExample/`
+- Re-open the edited file list and confirm the case closure touched every required file category
+
+## Out of scope
+
+- Auditing the final implementation as a reviewer
+- Audio-only cases that belong in `APIExample-Audio/`
+- Compose-based cases that belong in `APIExample-Compose/`
+
+## Never
+
+- Never omit `setParameters(...)`
+- Never skip the `getPrivateCloudConfig()` null-check
+- Never call `RtcEngine.destroy()` directly on the main thread
+- Never stop at “it compiles” without updating registration and `ARCHITECTURE.md`
diff --git a/Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java b/Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java
new file mode 100644
index 000000000..2d029e12d
--- /dev/null
+++ b/Android/APIExample/.agents/skills/upsert-case/references/fragment-template.java
@@ -0,0 +1,46 @@
+// Reference skeleton only.
+// This file demonstrates the required engine lifecycle, reporting, and private-cloud patterns.
+// It is not a drop-in compiled class; each real case must supply its own imports, annotation,
+// event handler, engine config fields, and UI logic.
+public abstract class ExampleCaseTemplate extends BaseFragment {
+ // Supply case-specific handler/config fields in the real Fragment.
+ protected IRtcEngineEventHandler iRtcEngineEventHandler;
+ protected RtcEngine engine;
+
+ @Override
+ public void onActivityCreated(@Nullable Bundle savedInstanceState) {
+ super.onActivityCreated(savedInstanceState);
+ Context context = getContext();
+ if (context == null) {
+ return;
+ }
+ try {
+ RtcEngineConfig config = new RtcEngineConfig();
+ config.mContext = context.getApplicationContext();
+ config.mAppId = getAgoraAppId();
+ config.mEventHandler = iRtcEngineEventHandler;
+ // Add case-specific config here, for example channel profile or area code.
+ engine = RtcEngine.create(config);
+ engine.setParameters("{\"rtc.report_app_scenario\":{\"appScenario\":100,\"serviceType\":11,\"appVersion\":\"" + RtcEngine.getSdkVersion() + "\"}}");
+ LocalAccessPointConfiguration privateCloud =
+ ((MainApplication) getActivity().getApplication()).getGlobalSettings().getPrivateCloudConfig();
+ if (privateCloud != null) {
+ engine.setLocalAccessPoint(privateCloud);
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ // Match the seeded case rollback pattern when engine init fails.
+ getActivity().onBackPressed();
+ }
+ }
+
+ @Override
+ public void onDestroy() {
+ super.onDestroy();
+ if (engine != null) {
+ engine.leaveChannel();
+ }
+ handler.post(RtcEngine::destroy);
+ engine = null;
+ }
+}
diff --git a/Android/APIExample/AGENTS.md b/Android/APIExample/AGENTS.md
index b5c9e8bd7..e142a46a1 100644
--- a/Android/APIExample/AGENTS.md
+++ b/Android/APIExample/AGENTS.md
@@ -40,9 +40,9 @@ Both are `false` by default. Do not enable unless the feature explicitly require
| Skill | Path | Description |
|-------|------|-------------|
-| upsert-case | `.agent/skills/upsert-case/` | Add a new case or modify an existing one |
-| query-cases | `.agent/skills/query-cases/` | Query and browse existing cases |
-| review-case | `.agent/skills/review-case/` | Review a case against project red lines |
+| upsert-case | `.agents/skills/upsert-case/` | Add a new case or modify an existing one |
+| query-cases | `.agents/skills/query-cases/` | Query and browse existing cases |
+| review-case | `.agents/skills/review-case/` | Review a case against project red lines |
## Further Reading
diff --git a/Android/APIExample/app/src/main/res/layout/fragment_beauty_faceunity.xml b/Android/APIExample/app/src/main/res/layout/fragment_beauty_faceunity.xml
index 2aa57b690..424518b90 100644
--- a/Android/APIExample/app/src/main/res/layout/fragment_beauty_faceunity.xml
+++ b/Android/APIExample/app/src/main/res/layout/fragment_beauty_faceunity.xml
@@ -61,17 +61,18 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="#99000000"
+ android:minHeight="56dp"
android:orientation="horizontal"
- android:paddingVertical="12dp"
app:layout_constraintBottom_toBottomOf="parent">
@@ -79,20 +80,22 @@
diff --git a/Android/APIExample/app/src/main/res/values/arrays.xml b/Android/APIExample/app/src/main/res/values/arrays.xml
index 740b8015a..94927aeaf 100644
--- a/Android/APIExample/app/src/main/res/values/arrays.xml
+++ b/Android/APIExample/app/src/main/res/values/arrays.xml
@@ -204,7 +204,6 @@
- VD_960x720
- VD_1280x720
- VD_1920x1080
- - VD_1920x1080
- VD_2540x1440
- VD_3840x2160