Skip to content

Conversation

@ucswift
Copy link
Member

@ucswift ucswift commented Feb 3, 2026

Summary by CodeRabbit

Release Notes

  • New Features

    • Added System Audio option to Bluetooth device selection for seamless audio routing.
    • Enhanced Bluetooth device type detection for improved audio handling across specialized and system audio devices.
    • Improved microphone control integration with call management features.
  • Documentation

    • Added comprehensive development guidelines for TypeScript, React Native, and Expo projects.
  • Tests

    • Added microphone delegation test coverage.
    • Removed obsolete test suite for media button functionality.

@coderabbitai
Copy link

coderabbitai bot commented Feb 3, 2026

📝 Walkthrough

Walkthrough

This PR removes MediaButtonService and replaces it with CallKeep-based mute synchronization. It adds system audio device support to Bluetooth connections, introduces device type classification (specialized vs. system), and implements microphone control delegation to the LiveKit store with proper lifecycle management.

Changes

Cohort / File(s) Summary
Documentation & Configuration
.agent/rules/rules.md, .github/workflows/react-native-cicd.yml, src/types/ptt.ts
New development guidelines file; updated CI/CD version string format from 7.${{ github.run_number }}.0 to 7.${{ github.run_number }} across three jq update steps; comment reference updated from media-button.service to headset-button.service.
Bluetooth Device Management
src/components/settings/bluetooth-device-selection-bottom-sheet.tsx, src/services/bluetooth-audio.service.ts, src/stores/app/bluetooth-audio-store.ts
Added System Audio device option with connection via new connectToSystemAudio() method; implemented device type classification (specialized/system) based on advertising data; propagated type field through device models and store; added headset button monitoring flag to store state.
Call Management Services
src/services/callkeep.service.ios.ts, src/services/callkeep.service.android.ts
Introduced endCallCallback registration mechanism and mute listener lifecycle methods (removeMuteListener, restoreMuteListener); iOS implementation adds spam protection logic and per-event timing guards for mute action handling.
MediaButtonService Removal
src/services/media-button.service.ts, src/services/__tests__/media-button.service.test.ts
Removed entire MediaButtonService singleton implementation (491 lines) including platform-specific setup, double-tap timing, PTT handlers, and related test coverage (283 lines).
LiveKit Integration
src/stores/app/livekit-store.ts
Replaced media button service integration with CallKeep-based mute synchronization; added public microphone control methods (setMicrophoneEnabled, toggleMicrophone) with debouncing and audio cues; introduced mute timestamp tracking for synchronization logic.
Test Updates
src/services/__tests__/bluetooth-audio.service.test.ts, src/stores/app/__tests__/livekit-store.test.ts
Added microphone control delegation test suite for Bluetooth audio service; removed media-button.service mock and added setEndCallCallback to CallKeep iOS service mock.

Sequence Diagram

sequenceDiagram
    participant User
    participant UI as Bluetooth Device Sheet
    participant BtAudio as BluetoothAudioService
    participant CK as CallKeepService
    participant LK as LiveKitStore
    participant Audio as Audio System

    User->>UI: Select System Audio
    UI->>BtAudio: connectToSystemAudio()
    BtAudio->>BtAudio: Disconnect current device
    BtAudio->>CK: restoreMuteListener()
    CK->>Audio: Re-register mute listener
    BtAudio->>LK: Revert audio routing
    BtAudio->>LK: Set preferred device (system-audio)
    LK->>Audio: Configure microphone routing
    BtAudio->>UI: Close sheet on success
    
    Note over CK,LK: Mute synchronization via CallKeep
    User->>Audio: Press mute button
    CK->>CK: Guard checks (debounce, spam)
    CK->>LK: Trigger muteStateCallback
    LK->>LK: setMicrophoneEnabled(toggled)
    LK->>Audio: Update microphone state
    LK->>Audio: Play audio cue
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Unit#204 — Modifies the same CI workflow version string format in react-native-cicd.yml (from 7.${{ github.run_number }}.0 to 7.${{ github.run_number }}).
  • Unit#199 — Directly opposed changes: this PR removes MediaButtonService while Unit#199 adds it, affecting the same integration pathways and test mocks.
  • Unit#159 — Related CallKeep integration and app startup modifications, including callback APIs and service lifecycle hooks.

Poem

🐰 Hops around in circles
Media buttons fade, but the mic still flows,
CallKeep takes the helm—now the system knows!
Device types sorted, from tech to au-natural prose,
System audio hops in—a smoother path one chose,
The audio streams dance where CallKeep goes! 🎵

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Title check ⚠️ Warning The title 'RU-T47 Build version number fix' focuses on version numbering changes in the CI/CD workflow, but the changeset includes extensive non-version-related modifications like Bluetooth audio device selection, system audio routing, mute listener management, and removal of media button service functionality. Consider using a more comprehensive title that reflects the major architectural changes, such as 'Refactor media button service and integrate CallKeep mute handling' or 'RU-T47 Replace media button service with CallKeep integration and add system audio support'.
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch develop

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/services/callkeep.service.ios.ts (1)

280-288: ⚠️ Potential issue | 🟡 Minor

Consider invoking endCallCallback only for the current call

The endCallCallback is invoked regardless of whether callUUID matches currentCallUUID. This could cause unintended behavior if there are multiple calls or stale events. Consider guarding the callback invocation.

🛡️ Proposed fix
       if (callUUID === currentCallUUID) {
         currentCallUUID = null;
         this.isCallActive = false;
-      }
 
-      if (this.endCallCallback) {
-        this.endCallCallback();
+        if (this.endCallCallback) {
+          this.endCallCallback();
+        }
       }
🤖 Fix all issues with AI agents
In @.agent/rules/rules.md:
- Line 44: Fix the spelling mistake in the rule text: replace the word
"dictonary" with "dictionary" in the sentence that mentions storing translation
files in src/translations (the line containing "The app is multi-lingual, so
ensure all text is wrapped in `t()` from `react-i18next` for translations with
the dictonary files stored in `src/translations`.").

In `@src/components/settings/bluetooth-device-selection-bottom-sheet.tsx`:
- Around line 276-279: In the BluetoothDeviceSelectionBottomSheet component,
replace the hardcoded user-facing strings in the two <Text> elements ("System
Audio" and "AirPods, Car, Wired Headset") with calls to the i18n translator
(t()) from react-i18next (ensure t is imported/available in the component), e.g.
t('settings.systemAudio') and t('settings.systemAudioExamples'), and
update/create corresponding keys in the translations files; also ensure the JSX
formatting uses single-line expressions or proper multilines to satisfy the
static analyzer (keep each <Text> content as a single JS expression calling
t()).
🧹 Nitpick comments (11)
src/services/callkeep.service.android.ts (1)

224-236: No-op stubs for interface compatibility.

These methods provide API parity with the iOS implementation. Consider adding a brief inline comment explaining they are Android no-ops for cross-platform compatibility.

src/stores/app/bluetooth-audio-store.ts (1)

33-37: Inconsistent indentation in interface definition.

The BluetoothSystemAudioDevice interface uses 5-space indentation instead of the 2-space convention used elsewhere in the file.

🔧 Proposed fix
 export interface BluetoothSystemAudioDevice {
-     id: string;
-     name: string;
-     type: 'system';
+  id: string;
+  name: string;
+  type: 'system';
 }
src/services/__tests__/bluetooth-audio.service.test.ts (1)

174-197: Test coverage for microphone control delegation looks good.

The tests verify that the bluetooth audio service correctly delegates microphone control to the LiveKit store. Consider extracting the mock retrieval to a constant outside the test body for cleaner access:

💡 Alternative mock access pattern
// At the top of the describe block or test file
const getLiveKitStoreMock = () => 
  require('@/stores/app/livekit-store').useLiveKitStore.getState();

// Then in tests:
expect(getLiveKitStoreMock().toggleMicrophone).toHaveBeenCalled();
src/stores/app/__tests__/livekit-store.test.ts (1)

46-46: Minor: Trailing empty line from removed mock.

This empty line appears to be a remnant from removing the media-button service mock. Consider removing it for cleaner formatting.

.agent/rules/rules.md (1)

42-43: Minor grammar improvement suggestion

The sentence structure could be cleaner.

-- Create and use Jest to test to validate all generated components
-- Generate tests for all components, services and logic generated. Ensure tests run without errors and fix any issues.
+- Use Jest to validate all generated components
+- Generate tests for all components, services, and logic. Ensure tests run without errors and resolve any issues.
src/stores/app/livekit-store.ts (3)

378-390: Dead code: onLocalTrackMuted is defined but never used

The onLocalTrackMuted function is declared but never attached to any listener or invoked. Either remove it or implement the intended functionality.

-      const onLocalTrackMuted = () => {
-         // Update CallKeep state if needed?
-         // For now, let's just trust CallKeep's own state management or system UI.
-      };
-      
-      // We attach these listeners to the local participant if needed for other UI sync

342-367: Simplify inline comments in mute sync callback

The verbose inline comments explaining the logic can be reduced. The condition check and action are correct, but the explanation is overly detailed for production code.

♻️ Suggested simplification
       // Setup CallKeep mute sync
       callKeepService.setMuteStateCallback(async (muted) => {
         logger.info({
            message: 'Syncing mute state from CallKeep',
            context: { muted }
         });
         
-        if (room.localParticipant.isMicrophoneEnabled === muted) {
-             // If CallKeep says "muted" (true), and Mic is enabled (true), we need to disable mic.
-             // If CallKeep says "unmuted" (false), and Mic is disabled (false), we need to enable mic.
-             // Wait, logic check:
-             // isMicrophoneEnabled = true means NOT MUTED.
-             // muted = true means MUTED.
-             // So if isMicrophoneEnabled (true) and muted (true) -> mismatch, we must mute.
-             // if isMicrophoneEnabled (false) and muted (false) -> mismatch, we must unmute.
-             
-             // Actually effectively: setMicrophoneEnabled(!muted)
-             await room.localParticipant.setMicrophoneEnabled(!muted);
+        // Sync mic state: isMicrophoneEnabled should be the inverse of muted
+        const shouldBeEnabled = !muted;
+        if (room.localParticipant.isMicrophoneEnabled !== shouldBeEnabled) {
+             await room.localParticipant.setMicrophoneEnabled(shouldBeEnabled);
              
              // Play sound
              if (!muted) {
                  await audioService.playStartTransmittingSound();
              } else {
                  await audioService.playStopTransmittingSound();
              }
         }
       });

494-498: Cleanup looks correct, minor formatting issue

The callback cleanup is appropriate. There's an extra blank line at 498 that could be removed.

       // Cleanup CallKeep Mute Callback
       callKeepService.setMuteStateCallback(null);
       callKeepService.setEndCallCallback(null);
-
-
+
       set({
src/services/bluetooth-audio.service.ts (3)

135-141: Fix formatting: inconsistent indentation

Static analysis flagged extra spaces on lines 136-140. Apply consistent indentation.

♻️ Proposed fix
         if (preferredDevice.id === 'system-audio') {
-            logger.info({
-                message: 'Preferred device is System Audio, ensuring no specialized device is connected',
-            });
-            // We are already in system audio mode by default if no device is connected
-            return;
+          logger.info({
+            message: 'Preferred device is System Audio, ensuring no specialized device is connected',
+          });
+          // We are already in system audio mode by default if no device is connected
+          return;
         }

591-615: Fix formatting in getDeviceType method

Static analysis flagged inconsistent indentation. Also consider extracting the specialized UUIDs to a constant for reusability.

♻️ Proposed fix for formatting
   private getDeviceType(device: Device): 'specialized' | 'system' {
     const advertisingData = device.advertising;
     const serviceUUIDs = advertisingData?.serviceUUIDs || [];
-    
+
     // Check for specialized PTT service UUIDs
     const isSpecialized = serviceUUIDs.some((uuid: string) => {
-      return [
-        AINA_HEADSET_SERVICE, 
-        B01INRICO_HEADSET_SERVICE, 
-        HYS_HEADSET_SERVICE
-      ].some(specialized => this.areUuidsEqual(uuid, specialized));
+      return [AINA_HEADSET_SERVICE, B01INRICO_HEADSET_SERVICE, HYS_HEADSET_SERVICE].some((specialized) =>
+        this.areUuidsEqual(uuid, specialized)
+      );
     });
 
     if (isSpecialized) {
       return 'specialized';
     }
 
     // Check by name for known specialized devices if UUID check fails
     const name = device.name?.toLowerCase() || '';
     if (name.includes('aina') || name.includes('inrico') || name.includes('hys')) {
-        return 'specialized';
+      return 'specialized';
     }
 
     return 'system';
   }

1019-1027: Remove or address verbose TODO comments

The inline comments about storage persistence are implementation notes that should be addressed or removed before merging. If persistence is needed, implement it; otherwise, remove the commentary.

-        // Save to storage (implied by previous code loading it from storage, but we need to save it too? 
-        // usage of usePreferredBluetoothDevice hook elsewhere handles saving, 
-        // but here we are in service. The UI calls logic that saves it eventually 
-        // or we should do it here if we want persistence.)
-        // The service reads from storage using require('@/lib/storage'), so we should probably save it too if we want it to persist.
-        // However, the UI calls setPreferredDevice from the hook which likely saves it.
-        // We will let the UI handle the persistence call or add it here if needed.
-        // For now, updating the store is enough for the session.
+        // Note: UI handles persistence via usePreferredBluetoothDevice hook

- Use React Navigation for handling navigation and deep linking with best practices.
- Create and use Jest to test to validate all generated components
- Generate tests for all components, services and logic generated. Ensure tests run without errors and fix any issues.
- The app is multi-lingual, so ensure all text is wrapped in `t()` from `react-i18next` for translations with the dictonary files stored in `src/translations`.
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Typo: "dictonary" should be "dictionary"

-- The app is multi-lingual, so ensure all text is wrapped in `t()` from `react-i18next` for translations with the dictonary files stored in `src/translations`.
+- The app is multi-lingual, so ensure all text is wrapped in `t()` from `react-i18next` for translations with the dictionary files stored in `src/translations`.
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- The app is multi-lingual, so ensure all text is wrapped in `t()` from `react-i18next` for translations with the dictonary files stored in `src/translations`.
- The app is multi-lingual, so ensure all text is wrapped in `t()` from `react-i18next` for translations with the dictionary files stored in `src/translations`.
🤖 Prompt for AI Agents
In @.agent/rules/rules.md at line 44, Fix the spelling mistake in the rule text:
replace the word "dictonary" with "dictionary" in the sentence that mentions
storing translation files in src/translations (the line containing "The app is
multi-lingual, so ensure all text is wrapped in `t()` from `react-i18next` for
translations with the dictonary files stored in `src/translations`.").

Comment on lines +276 to +279
<Text className={`font-medium ${preferredDevice?.id === 'system-audio' ? 'text-primary-700 dark:text-primary-300' : 'text-neutral-900 dark:text-neutral-100'}`}>
System Audio
</Text>
<Text className="text-xs text-neutral-500">AirPods, Car, Wired Headset</Text>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

User-facing text missing i18n wrapping.

"System Audio" and "AirPods, Car, Wired Headset" are hardcoded strings. Per coding guidelines, all user-facing text should be wrapped in t().

🌐 Proposed fix for i18n
-                  <Text className={`font-medium ${preferredDevice?.id === 'system-audio' ? 'text-primary-700 dark:text-primary-300' : 'text-neutral-900 dark:text-neutral-100'}`}>
-                    System Audio
-                  </Text>
-                  <Text className="text-xs text-neutral-500">AirPods, Car, Wired Headset</Text>
+                  <Text className={`font-medium ${preferredDevice?.id === 'system-audio' ? 'text-primary-700 dark:text-primary-300' : 'text-neutral-900 dark:text-neutral-100'}`}>
+                    {t('bluetooth.system_audio')}
+                  </Text>
+                  <Text className="text-xs text-neutral-500">{t('bluetooth.system_audio_description')}</Text>

Also addresses the static analysis hint about multiline formatting.

As per coding guidelines: "Ensure all text is wrapped in t() from react-i18next for translations with dictionary files stored in src/translations"

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
<Text className={`font-medium ${preferredDevice?.id === 'system-audio' ? 'text-primary-700 dark:text-primary-300' : 'text-neutral-900 dark:text-neutral-100'}`}>
System Audio
</Text>
<Text className="text-xs text-neutral-500">AirPods, Car, Wired Headset</Text>
<Text className={`font-medium ${preferredDevice?.id === 'system-audio' ? 'text-primary-700 dark:text-primary-300' : 'text-neutral-900 dark:text-neutral-100'}`}>
{t('bluetooth.system_audio')}
</Text>
<Text className="text-xs text-neutral-500">{t('bluetooth.system_audio_description')}</Text>
🧰 Tools
🪛 GitHub Check: test

[warning] 276-276:
Replace ⏎····················System·Audio⏎·················· with System·Audio

🤖 Prompt for AI Agents
In `@src/components/settings/bluetooth-device-selection-bottom-sheet.tsx` around
lines 276 - 279, In the BluetoothDeviceSelectionBottomSheet component, replace
the hardcoded user-facing strings in the two <Text> elements ("System Audio" and
"AirPods, Car, Wired Headset") with calls to the i18n translator (t()) from
react-i18next (ensure t is imported/available in the component), e.g.
t('settings.systemAudio') and t('settings.systemAudioExamples'), and
update/create corresponding keys in the translations files; also ensure the JSX
formatting uses single-line expressions or proper multilines to satisfy the
static analyzer (keep each <Text> content as a single JS expression calling
t()).

@ucswift
Copy link
Member Author

ucswift commented Feb 3, 2026

Approve

Copy link

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

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

This PR is approved.

@ucswift ucswift merged commit 43dcd16 into master Feb 3, 2026
17 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants