Files

8.6 KiB

CamSwapper — Project Status

Build Status

  • Both flavors compile: modernDebug and legacyDebug assemble successfully with 0 errors, 0 warnings (minor extractNativeLibs manifest warning from AGP).
  • On-device test: Installed on Pixel 9a (Android 16). FGS crash resolved with specialUse type. App now has Application class, INTERNET + POST_NOTIFICATIONS permissions, and custom notification icon.
  • Min SDK: 29 | Target SDK: 36 | Compile SDK: 36
  • Kotlin: JVM 11, toolchain 17
  • Gradle: 8.13

Recent Changes

  1. Removed Xposed framework dependency — all Xposed APIs (IXposedHookLoadPackage, ReflectionTools, process-based hooks) deleted.
  2. Cleaned up Xposed remnants — removed src/modern/resources/META-INF/xposed/, src/legacy/AndroidManifest.xml (xposed metadata), and src/legacy/assets/ (xposed_init files).
  3. Moved ConfigRepository.kt from src/modern/java/ to src/main/java/ so both product flavors can compile (MediaType/MediaSource enums needed by main source set files).
  4. Rewrote VirtualCameraService.kt (485 corrupted lines → 327 clean lines):
    • Uses reflection for @SystemApi VDM types (VirtualDeviceManager, VirtualDeviceParams, VirtualCameraConfig, VirtualCameraCallback, DeviceFilter.Builder)
    • Uses direct API for public companion types (CompanionDeviceManager, AssociationRequest, AssociationInfo)
    • Single clean VirtualCameraCallback proxy wired to VideoPusher pipeline
    • Proper CDM association flow with fallback to dummy ID
    • Removed 4x duplicated callback blocks, broken braces, dangling code
  5. Fixed foregroundServiceType for SDK 36 — On-device test on Pixel 9a (Android 16) revealed mediaProjection FGS type requires FOREGROUND_SERVICE_MEDIA_PROJECTION + CAPTURE_VIDEO_OUTPUT permissions on target SDK 36. Changed to specialUse with PROPERTY_SPECIAL_USE_FGS_SUBTYPE declaration. APK now installs without crash on boot.
  6. Added CamSwapperApp Application classGlobalState.appContext was lateinit and never initialized, causing potential crash when VideoPusher.start() or SourceManager accessed it. Now properly initialized in Application.onCreate().
  7. Added missing permissionsINTERNET (needed by ExoPlayer/Media3), POST_NOTIFICATIONS (for Android 13+ notification permission compliance).
  8. Replaced notification iconandroid.R.drawable.ic_menu_camera (system resource, may be unavailable) replaced with custom R.drawable.ic_notification.

Architecture Overview

app/src/main/java/com/nothing/camera2magic/
├── MainActivity.kt            # Entry point, permission flow, service bind
├── VirtualCameraService.kt    # Foreground service → VirtualDeviceManager via reflection
├── GlobalState.kt             # Global app context holder
├── viewmodel/
│   ├── ConfigRepository.kt    # SharedPreferences-backed config (MediaType, MediaSource enums)
│   ├── SpotlightViewModel.kt  # Media selection & preview state (MVI + StateFlow)
│   ├── SettingsViewModel.kt   # App settings + HAL mode toggle
│   ├── ViewModelFactory.kt    # ViewModel provider factory
│   └── CompositionLocals.kt   # Compose DI locals
├── view/
│   ├── SpotlightView.kt       # Media source selector, preview grid, module switch
│   └── SettingsView.kt        # Toggle settings + HAL mode config
├── hook/
│   ├── SourceManager.kt       # Media content resolver & fingerprint checking (224 lines)
│   ├── VideoPusher.kt         # ExoPlayer → Surface → VirtualCameraRenderer pipeline
│   ├── VirtualCameraRenderer.kt  # OpenGL ES renderer, SurfaceTexture → EGL → ImageWriter (387 lines)
│   ├── NativeBridge.kt        # JNI bridge for native C++ camera HAL hooks
│   ├── HalConfigManager.kt    # Root-based HAL config writer (/data/local/camera_magic/)
│   ├── BlackHoleMapper.kt     # Empty/dummy camera HAL mapper
│   └── CameraState.kt         # Camera state tracking
├── ui/theme/
│   ├── Color.kt, Theme.kt, Type.kt  # Material3 theme
└── utils/
    └── Dog.kt                 # Logging utility

Product Flavors

Flavor Purpose
legacy Devices without VirtualDeviceManager support (Android 10-13) — currently identical to modern
modern Devices with Android 14+ VirtualDeviceManager API — has additional hardware buffer path available

Pipeline: Media → Virtual Camera

Local Media (ContentResolver) or Network (RTSP)
        │
        ▼
  VideoPusher (ExoPlayer) / SourceManager
        │
        ▼
  VirtualCameraRenderer (OpenGL ES)
        │  SurfaceTexture → EGL → ImageWriter
        ▼
  VirtualCameraService (reflection)
        │  VirtualDeviceManager → VirtualCamera
        ▼
  Other apps see a virtual camera device

Component Details

VirtualCameraService (reflection-based)

  • Status: Compiles , refactored (327 lines, clean architecture)
  • API level gate: Build.VERSION_CODES.UPSIDE_DOWN_CAKE (34)
  • Approach: Reflection for @SystemApi types (VirtualDeviceManager, VirtualDeviceParams, VirtualCameraConfig, VirtualCameraCallback, DeviceFilter.Builder). Direct API for public companion types (CompanionDeviceManager, AssociationRequest, AssociationInfo, DeviceFilter)
  • Callback: VirtualCameraCallback proxy wired — onStreamConfigured() feeds the Surface to VideoPusher.start()
  • CDM: Full CompanionDeviceManager.associate() flow with DeviceFilter.FEATURE_VIRTUAL_CAMERA filter, plus fallback to dummy ID 1 when CDM unavailable or association fails
  • VideoPusher integration: Uses VideoPusher singleton object directly (VideoPusher.start(), VideoPusher.stop())
  • Previously: 485 corrupted lines with 4x duplicated callback blocks, broken braces, dangling code, no callback wiring, VideoPusher.getInstance() dependency that didn't exist

ConfigRepository

  • Status: Clean
  • Backs all settings via SharedPreferences
  • Enums: MediaSource (LOCAL/NETWORK), MediaType (VIDEO/IMAGE)
  • HAL mode settings also stored here

SourceManager

  • Status: Compiles, legacy footprint from Xposed era
  • Global singleton, manages media source selection & fingerprinting
  • Directly accesses ContentResolver for local media
  • Still uses moduleEnabled concept from Xposed module days

VideoPusher + VirtualCameraRenderer

  • Status: Compiles (OpenGL ES + ExoPlayer pipeline)
  • VideoPusher: Singleton object, takes a video ID via start(surfaces, width, height, videoId), creates ExoPlayer, feeds to renderer surfaces
  • VirtualCameraRenderer: 387-line OpenGL ES 2.0 renderer, handles EGL setup, SurfaceTexture → ImageWriter pipeline
  • VideoPusher is wired via VirtualCameraCallback.onStreamConfigured() in VirtualCameraService

HalConfigManager

  • Status: Compiles, requires root
  • Writes config to /data/local/camera_magic/config.txt via su -c
  • Called from SettingsViewModel HAL mode toggle

Remaining Work / Known Issues

Critical

  • (none — all previously identified critical issues resolved)

Medium

  • SourceManager duplicates ConfigRepository — SourceManager has its own SharedPreferences keys and module state, duplicating ConfigRepository. Should be consolidated.
  • Native bridge stubNativeBridge.kt exists but no native .so is loaded (CMake/NDK build disabled). The project uses prebuilt libcamera3.so from jniLibs.
  • legacy flavor — Both flavors currently have identical source sets. The legacy flavor should ideally strip out VirtualDeviceManager-dependent features.

Low / Cleanup

  • Test filesExampleInstrumentedTest.kt and ExampleUnitTest.kt are the default Android Studio stubs.
  • GlobalState.kt — Legacy singleton used by Xposed hooks; may be removable now.
  • Dog utility — Thin wrapper over android.util.Log.

Module Projects (outside app/)

Directory Purpose
root-module/ KernelSU module packaging (HAL-level camera hook)
zygisk-module/ Zygisk module packaging (injection-based)
native/ C++ native code for camera HAL hooks
docs/ Documentation assets
camswapper-kernelsu-v2.zip Prebuilt KernelSU module
camswapper-zygisk-v2.zip Prebuilt Zygisk module

Build Commands

# Build modern flavor (VirtualDeviceManager + system APIs)
./gradlew :app:assembleModernDebug

# Build legacy flavor (no system APIs)
./gradlew :app:assembleLegacyDebug

# Build with native C++ compilation
./gradlew :app:buildNative

# Build release
./gradlew :app:assembleModernRelease