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