Full but still broken rework of everything
This commit is contained in:
@@ -15,3 +15,4 @@ local.properties
|
|||||||
app/modern
|
app/modern
|
||||||
app/legacy
|
app/legacy
|
||||||
.sisyphus/
|
.sisyphus/
|
||||||
|
*.zip
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
# 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 class** — `GlobalState.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 permissions** — `INTERNET` (needed by ExoPlayer/Media3), `POST_NOTIFICATIONS` (for Android 13+ notification permission compliance).
|
||||||
|
8. **Replaced notification icon** — `android.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 stub** — `NativeBridge.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 files** — `ExampleInstrumentedTest.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
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 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
|
||||||
|
```
|
||||||
+33
-19
@@ -55,14 +55,13 @@ android {
|
|||||||
versionCode versionCodeOffset + gitCommitCount
|
versionCode versionCodeOffset + gitCommitCount
|
||||||
versionName "${majorVersion}.${minorVersion}.${patchVersion}"
|
versionName "${majorVersion}.${minorVersion}.${patchVersion}"
|
||||||
|
|
||||||
/*
|
// externalNativeBuild disabled - native code not ready yet
|
||||||
externalNativeBuild {
|
// externalNativeBuild {
|
||||||
cmake {
|
// cmake {
|
||||||
cppFlags += ""
|
// cppFlags += ""
|
||||||
arguments "-DANDROID_PLATFORM=android-29"
|
// arguments "-DANDROID_PLATFORM=android-29"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
*/
|
|
||||||
|
|
||||||
ndk {
|
ndk {
|
||||||
abiFilters.add("arm64-v8a")
|
abiFilters.add("arm64-v8a")
|
||||||
@@ -72,10 +71,29 @@ android {
|
|||||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signingConfigs {
|
||||||
|
debug {
|
||||||
|
storeFile file("${System.getProperty('user.home')}/.config/.android/debug.keystore")
|
||||||
|
keyAlias 'androiddebugkey'
|
||||||
|
keyPassword 'android'
|
||||||
|
storePassword 'android'
|
||||||
|
}
|
||||||
|
release {
|
||||||
|
storeFile file("${System.getProperty('user.home')}/.config/.android/debug.keystore")
|
||||||
|
keyAlias 'androiddebugkey'
|
||||||
|
keyPassword 'android'
|
||||||
|
storePassword 'android'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
signingConfig signingConfigs.debug
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
|
signingConfig signingConfigs.release
|
||||||
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -102,14 +120,13 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/*
|
// externalNativeBuild disabled - native code not ready yet
|
||||||
externalNativeBuild {
|
// externalNativeBuild {
|
||||||
cmake {
|
// cmake {
|
||||||
path file("src/main/cpp/CMakeLists.txt")
|
// path file("src/main/cpp/CMakeLists.txt")
|
||||||
version = "3.22.1"
|
// version = "3.22.1"
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
*/
|
|
||||||
|
|
||||||
buildFeatures {
|
buildFeatures {
|
||||||
compose true
|
compose true
|
||||||
@@ -177,9 +194,6 @@ tasks.register("buildNative", Copy) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
legacyCompileOnly "de.robv.android.xposed:api:82"
|
|
||||||
modernCompileOnly 'io.github.libxposed:api:101.0.0'
|
|
||||||
modernImplementation 'io.github.libxposed:service:101.0.0'
|
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-compose:2.10.0"
|
||||||
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0"
|
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.10.0"
|
||||||
implementation "com.google.accompanist:accompanist-permissions:0.37.3"
|
implementation "com.google.accompanist:accompanist-permissions:0.37.3"
|
||||||
|
|||||||
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
|
||||||
xmlns:tools="http://schemas.android.com/tools">
|
|
||||||
<application>
|
|
||||||
<meta-data
|
|
||||||
android:name="xposedmodule"
|
|
||||||
android:value="true" />
|
|
||||||
<meta-data
|
|
||||||
android:name="xposeddescription"
|
|
||||||
android:value="A Virtual Camera, support Android 10+ " />
|
|
||||||
<meta-data
|
|
||||||
android:name="xposedminversion"
|
|
||||||
android:value="93" />
|
|
||||||
</application>
|
|
||||||
</manifest>
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
camera3
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
com.nothing.camera2magic.MagicHook
|
|
||||||
@@ -1,56 +0,0 @@
|
|||||||
package com.nothing.camera2magic
|
|
||||||
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.Toast
|
|
||||||
import com.nothing.camera2magic.hook.SourceManager
|
|
||||||
import de.robv.android.xposed.IXposedHookLoadPackage
|
|
||||||
import de.robv.android.xposed.XC_MethodHook
|
|
||||||
import de.robv.android.xposed.XposedHelpers
|
|
||||||
import de.robv.android.xposed.callbacks.XC_LoadPackage.LoadPackageParam
|
|
||||||
|
|
||||||
class MagicHook : IXposedHookLoadPackage {
|
|
||||||
init {
|
|
||||||
System.loadLibrary("camera3")
|
|
||||||
}
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "[MagicHook]"
|
|
||||||
private const val MODULE_PACKAGE_NAME = "com.nothing.camera2magic"
|
|
||||||
}
|
|
||||||
override fun handleLoadPackage(lpparam: LoadPackageParam) {
|
|
||||||
if (lpparam.packageName == MODULE_PACKAGE_NAME) return
|
|
||||||
GlobalState.packageName = lpparam.packageName
|
|
||||||
|
|
||||||
XposedHelpers.findAndHookMethod(Application::class.java,
|
|
||||||
"onCreate", object : XC_MethodHook() {
|
|
||||||
override fun afterHookedMethod(param: MethodHookParam) {
|
|
||||||
GlobalState.appContext = param.thisObject as Context
|
|
||||||
//TODO:
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
XposedHelpers.findAndHookMethod(Activity::class.java,
|
|
||||||
"onStart", object : XC_MethodHook() {
|
|
||||||
override fun afterHookedMethod(param: MethodHookParam) {
|
|
||||||
val activity = param.thisObject as Activity
|
|
||||||
GlobalState.activityCount ++
|
|
||||||
if (GlobalState.activityCount == 1) {
|
|
||||||
SourceManager.refreshAndDispatch()
|
|
||||||
activity.runOnUiThread {
|
|
||||||
val text = "[✨] " + SourceManager.toastMessage
|
|
||||||
Toast.makeText(activity, text, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
XposedHelpers.findAndHookMethod(Activity::class.java,
|
|
||||||
"onStop", object : XC_MethodHook() {
|
|
||||||
override fun afterHookedMethod(param: MethodHookParam) {
|
|
||||||
GlobalState.activityCount--
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,21 @@
|
|||||||
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
android:name="android.permission.READ_EXTERNAL_STORAGE"
|
||||||
android:maxSdkVersion="32" />
|
android:maxSdkVersion="32" />
|
||||||
|
|
||||||
|
<uses-permission android:name="android.permission.CAMERA" />
|
||||||
|
<uses-permission android:name="android.permission.INTERNET" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE" />
|
||||||
|
<uses-permission android:name="android.permission.CREATE_VIRTUAL_DEVICE" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING" />
|
||||||
|
<uses-permission android:name="android.permission.REQUEST_COMPANION_SELF_MANAGED" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
|
||||||
|
<uses-feature
|
||||||
|
android:name="android.software.companion_device_setup"
|
||||||
|
android:required="false" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
|
android:name=".CamSwapperApp"
|
||||||
android:allowBackup="true"
|
android:allowBackup="true"
|
||||||
android:dataExtractionRules="@xml/data_extraction_rules"
|
android:dataExtractionRules="@xml/data_extraction_rules"
|
||||||
android:fullBackupContent="@xml/backup_rules"
|
android:fullBackupContent="@xml/backup_rules"
|
||||||
@@ -21,15 +35,9 @@
|
|||||||
android:description="@string/app_description"
|
android:description="@string/app_description"
|
||||||
android:extractNativeLibs="false">
|
android:extractNativeLibs="false">
|
||||||
|
|
||||||
<meta-data
|
<property
|
||||||
android:name="xposedmodule"
|
android:name="android.app.role"
|
||||||
android:value="true" />
|
android:value="android.role.app_streaming" />
|
||||||
<meta-data
|
|
||||||
android:name="xposeddescription"
|
|
||||||
android:value="CamSwapper Virtual Camera Module" />
|
|
||||||
<meta-data
|
|
||||||
android:name="xposedminversion"
|
|
||||||
android:value="100" />
|
|
||||||
|
|
||||||
<activity
|
<activity
|
||||||
android:name=".MainActivity"
|
android:name=".MainActivity"
|
||||||
@@ -41,5 +49,14 @@
|
|||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
|
|
||||||
|
<service
|
||||||
|
android:name=".VirtualCameraService"
|
||||||
|
android:exported="false"
|
||||||
|
android:foregroundServiceType="specialUse">
|
||||||
|
<property
|
||||||
|
android:name="android.app.PROPERTY_SPECIAL_USE_FGS_SUBTYPE"
|
||||||
|
android:value="Virtual camera creation and management via VirtualDeviceManager API" />
|
||||||
|
</service>
|
||||||
</application>
|
</application>
|
||||||
</manifest>
|
</manifest>
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
package com.nothing.camera2magic
|
||||||
|
|
||||||
|
import android.app.Application
|
||||||
|
|
||||||
|
class CamSwapperApp : Application() {
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
GlobalState.appContext = applicationContext
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,9 +1,13 @@
|
|||||||
package com.nothing.camera2magic
|
package com.nothing.camera2magic
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.content.ComponentName
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.ServiceConnection
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.IBinder
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import androidx.activity.ComponentActivity
|
import androidx.activity.ComponentActivity
|
||||||
import androidx.activity.compose.setContent
|
import androidx.activity.compose.setContent
|
||||||
@@ -33,6 +37,21 @@ import com.nothing.camera2magic.viewmodel.ViewModelFactory
|
|||||||
|
|
||||||
class MainActivity : ComponentActivity() {
|
class MainActivity : ComponentActivity() {
|
||||||
|
|
||||||
|
private var virtualCameraService: VirtualCameraService? = null
|
||||||
|
private val serviceConnection = object : ServiceConnection {
|
||||||
|
override fun onServiceConnected(name: ComponentName?, service: IBinder?) {
|
||||||
|
val binder = service as VirtualCameraService.LocalBinder
|
||||||
|
virtualCameraService = binder.getService()
|
||||||
|
virtualCameraService?.startVirtualCamera()
|
||||||
|
Log.i("MainActivity", "VirtualCameraService connected")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onServiceDisconnected(name: ComponentName?) {
|
||||||
|
virtualCameraService = null
|
||||||
|
Log.i("MainActivity", "VirtualCameraService disconnected")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@OptIn(ExperimentalPermissionsApi::class)
|
@OptIn(ExperimentalPermissionsApi::class)
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
@@ -72,6 +91,20 @@ class MainActivity : ComponentActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
val intent = Intent(this, VirtualCameraService::class.java)
|
||||||
|
startForegroundService(intent)
|
||||||
|
bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE)
|
||||||
|
Log.i("MainActivity", "VirtualCameraService started and bound")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
super.onStop()
|
||||||
|
unbindService(serviceConnection)
|
||||||
|
Log.i("MainActivity", "VirtualCameraService unbound")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
|||||||
@@ -0,0 +1,419 @@
|
|||||||
|
package com.nothing.camera2magic
|
||||||
|
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.Service
|
||||||
|
import android.companion.AssociationInfo
|
||||||
|
import android.companion.AssociationRequest
|
||||||
|
import android.companion.CompanionDeviceManager
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.content.IntentSender
|
||||||
|
import android.os.Binder
|
||||||
|
import android.os.Build
|
||||||
|
import android.os.Handler
|
||||||
|
import android.os.IBinder
|
||||||
|
import android.os.Looper
|
||||||
|
import android.util.Log
|
||||||
|
import android.view.Surface
|
||||||
|
import androidx.annotation.RequiresApi
|
||||||
|
import com.nothing.camera2magic.hook.VideoPusher
|
||||||
|
import com.nothing.camera2magic.R
|
||||||
|
import java.lang.reflect.InvocationHandler
|
||||||
|
import java.lang.reflect.Proxy
|
||||||
|
import java.util.concurrent.Executor
|
||||||
|
import java.util.concurrent.Executors
|
||||||
|
|
||||||
|
/**
|
||||||
|
* [VirtualCameraService] wraps Android VDM/VirtualCamera @SystemApi behind reflection.
|
||||||
|
*
|
||||||
|
* On this device (Pixel 9a, Android 16) the VDM classes live in
|
||||||
|
* `android.companion.virtual.*` (not `android.companion.*`).
|
||||||
|
* We obtain the service via the public [getSystemService] API, then
|
||||||
|
* derive the correct package prefix from the returned object's class at
|
||||||
|
* runtime so the class names are always correct regardless of device build.
|
||||||
|
*
|
||||||
|
* CompanionDeviceManager, AssociationRequest, AssociationInfo are public API.
|
||||||
|
*/
|
||||||
|
@RequiresApi(Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
|
||||||
|
class VirtualCameraService : Service() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
const val TAG = "VirtualCameraService"
|
||||||
|
const val CHANNEL_ID = "virtual_camera_channel"
|
||||||
|
const val NOTIFICATION_ID = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
private var virtualDevice: Any? = null
|
||||||
|
private var videoPusherActive = false
|
||||||
|
private var callbackExecutor: Executor? = null
|
||||||
|
private var vdmPkg: String = "android.companion.virtual" // default, overridden at runtime
|
||||||
|
|
||||||
|
private val binder = LocalBinder()
|
||||||
|
|
||||||
|
inner class LocalBinder : Binder() {
|
||||||
|
fun getService(): VirtualCameraService = this@VirtualCameraService
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate() {
|
||||||
|
super.onCreate()
|
||||||
|
createNotificationChannel()
|
||||||
|
Log.i(TAG, "Service created")
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onBind(intent: Intent?): IBinder = binder
|
||||||
|
|
||||||
|
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
|
||||||
|
startForeground(NOTIFICATION_ID, buildNotification("Virtual camera service running"))
|
||||||
|
Log.i(TAG, "onStartCommand")
|
||||||
|
return START_STICKY
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroy() {
|
||||||
|
stopVirtualCamera()
|
||||||
|
super.onDestroy()
|
||||||
|
Log.i(TAG, "Service destroyed")
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun vdmClass(shortName: String): Class<*> =
|
||||||
|
Class.forName("$vdmPkg.$shortName")
|
||||||
|
|
||||||
|
fun startVirtualCamera() {
|
||||||
|
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||||
|
Log.e(TAG, "VirtualDeviceManager requires API 34+")
|
||||||
|
updateNotification("Error: API 34+ required")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
val vdm = getSystemService("virtualdevice") ?: run {
|
||||||
|
Log.e(TAG, "VirtualDeviceManager not available")
|
||||||
|
updateNotification("Error: VirtualDeviceManager not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.i(TAG, "VirtualDeviceManager obtained: $vdm")
|
||||||
|
|
||||||
|
// Extract the actual VDM package (e.g. android.companion.virtual)
|
||||||
|
// from the runtime object so class names are correct on any build.
|
||||||
|
val vdmClass = vdm.javaClass
|
||||||
|
val fullName = vdmClass.name
|
||||||
|
vdmPkg = fullName.substringBeforeLast(".")
|
||||||
|
|
||||||
|
val params = buildVirtualDeviceParams()
|
||||||
|
Log.i(TAG, "VirtualDeviceParams created")
|
||||||
|
|
||||||
|
startCdmAssociation(vdm, vdmClass, params)
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to start virtual camera", e)
|
||||||
|
updateNotification("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun stopVirtualCamera() {
|
||||||
|
try {
|
||||||
|
if (videoPusherActive) {
|
||||||
|
VideoPusher.stop()
|
||||||
|
videoPusherActive = false
|
||||||
|
}
|
||||||
|
virtualDevice?.let { device ->
|
||||||
|
device.javaClass.getMethod("close").invoke(device)
|
||||||
|
virtualDevice = null
|
||||||
|
Log.i(TAG, "Virtual camera stopped")
|
||||||
|
updateNotification("Virtual camera stopped")
|
||||||
|
}
|
||||||
|
callbackExecutor?.let { (it as? java.util.concurrent.ExecutorService)?.shutdown() }
|
||||||
|
callbackExecutor = null
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Error stopping virtual camera", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildVirtualDeviceParams(): Any {
|
||||||
|
val paramsClass = vdmClass("VirtualDeviceParams")
|
||||||
|
val builderClass = vdmClass("VirtualDeviceParams\$Builder")
|
||||||
|
val builder = builderClass.getDeclaredConstructor().newInstance()
|
||||||
|
|
||||||
|
builderClass.getMethod("setName", String::class.java)
|
||||||
|
.invoke(builder, "CamSwapper Virtual Camera")
|
||||||
|
|
||||||
|
val policyTypeCamera = paramsClass.getDeclaredField("POLICY_TYPE_CAMERA").get(null) as Int
|
||||||
|
val devicePolicyCustom = paramsClass.getDeclaredField("DEVICE_POLICY_CUSTOM").get(null) as Int
|
||||||
|
builderClass.getMethod("setDevicePolicy", Int::class.javaPrimitiveType, Int::class.javaPrimitiveType)
|
||||||
|
.invoke(builder, policyTypeCamera, devicePolicyCustom)
|
||||||
|
|
||||||
|
return builderClass.getMethod("build").invoke(builder)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun startCdmAssociation(vdm: Any, vdmClass: Class<*>, params: Any) {
|
||||||
|
val cdm = getSystemService(CompanionDeviceManager::class.java)
|
||||||
|
if (cdm == null) {
|
||||||
|
Log.w(TAG, "CompanionDeviceManager not available")
|
||||||
|
updateNotification("Error: CDM not available")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use setDeviceProfile() — public API since API 31 — instead of
|
||||||
|
// DeviceFilter.Builder (which doesn't exist on all SDK builds).
|
||||||
|
// DEVICE_PROFILE_APP_STREAMING grants us a companion association for
|
||||||
|
// virtual display / app streaming use-cases (i.e. virtual camera).
|
||||||
|
val request = try {
|
||||||
|
AssociationRequest.Builder()
|
||||||
|
.setSingleDevice(true)
|
||||||
|
.setDeviceProfile(AssociationRequest.DEVICE_PROFILE_APP_STREAMING)
|
||||||
|
.setSelfManaged(true)
|
||||||
|
.setDisplayName("CamSwapper Virtual Camera")
|
||||||
|
.build()
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to build AssociationRequest", e)
|
||||||
|
updateNotification("Error: Association request failed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
val handler = Handler(Looper.getMainLooper())
|
||||||
|
|
||||||
|
val callback = object : CompanionDeviceManager.Callback() {
|
||||||
|
override fun onAssociationPending(intentSender: IntentSender) {
|
||||||
|
Log.i(TAG, "CDM association pending — requesting user consent")
|
||||||
|
try {
|
||||||
|
startIntentSender(intentSender, null, 0, 0, 0)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
// CompanionAssociationActivity may not exist on some builds.
|
||||||
|
Log.e(TAG, "Failed to show consent dialog", e)
|
||||||
|
// Try creating virtual device without CDM association
|
||||||
|
Log.i(TAG, "Trying direct VDM association bypass...")
|
||||||
|
tryDirectVdm(vdm, vdmClass, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onAssociationCreated(associationInfo: AssociationInfo) {
|
||||||
|
Log.i(TAG, "CDM association created: id=${associationInfo.id}")
|
||||||
|
val method = vdmClass.getMethod(
|
||||||
|
"createVirtualDevice",
|
||||||
|
Int::class.javaPrimitiveType,
|
||||||
|
vdmClass("VirtualDeviceParams")
|
||||||
|
)
|
||||||
|
@Suppress("UNCHECKED_CAST")
|
||||||
|
val device = method.invoke(vdm, associationInfo.id, params) as? Any
|
||||||
|
if (device == null) {
|
||||||
|
Log.e(TAG, "createVirtualDevice returned null")
|
||||||
|
// Fallback: try without CDM
|
||||||
|
tryDirectVdm(vdm, vdmClass, params)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.i(TAG, "VirtualDevice created: $device")
|
||||||
|
virtualDevice = device
|
||||||
|
createVirtualCamera(device)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onFailure(error: CharSequence?) {
|
||||||
|
Log.e(TAG, "CDM association failed: $error")
|
||||||
|
// Fallback: try without CDM
|
||||||
|
tryDirectVdm(vdm, vdmClass, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
cdm.associate(request, callback, handler)
|
||||||
|
Log.i(TAG, "CDM association requested — waiting for user consent")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "cdm.associate threw, trying direct VDM", e)
|
||||||
|
tryDirectVdm(vdm, vdmClass, params)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Attempt to create a VirtualDevice without CDM association by probing
|
||||||
|
* the [VirtualDeviceManager] methods at runtime. On some builds the
|
||||||
|
* device may accept a request without a companion association (e.g.
|
||||||
|
* via reflection on internal methods or a privileged-only overload).
|
||||||
|
*/
|
||||||
|
private fun tryDirectVdm(vdm: Any, vdmClass: Class<*>, params: Any) {
|
||||||
|
try {
|
||||||
|
// Log available methods for debugging
|
||||||
|
Log.i(TAG, "=== VirtualDeviceManager methods ===")
|
||||||
|
for (m in vdmClass.methods.sortedBy { it.name }) {
|
||||||
|
Log.i(TAG, " ${m.name}(${m.parameterTypes.joinToString { it.simpleName }})")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try createVirtualDevice(VirtualDeviceParams) — no association needed
|
||||||
|
val directMethod = vdmClass.methods.firstOrNull { m ->
|
||||||
|
m.name == "createVirtualDevice" &&
|
||||||
|
m.parameterTypes.size == 1
|
||||||
|
}
|
||||||
|
if (directMethod != null) {
|
||||||
|
Log.i(TAG, "Found direct createVirtualDevice, trying...")
|
||||||
|
val device = directMethod.invoke(vdm, params) as? Any
|
||||||
|
if (device != null) {
|
||||||
|
Log.i(TAG, "VirtualDevice created via direct API: $device")
|
||||||
|
virtualDevice = device
|
||||||
|
createVirtualCamera(device)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.w(TAG, "Direct createVirtualDevice returned null")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try createVirtualDevice(0, params) with dummy association ID
|
||||||
|
val intMethod = vdmClass.getMethod(
|
||||||
|
"createVirtualDevice",
|
||||||
|
Int::class.javaPrimitiveType,
|
||||||
|
vdmClass("VirtualDeviceParams")
|
||||||
|
)
|
||||||
|
Log.i(TAG, "Trying createVirtualDevice(0, params)...")
|
||||||
|
val device = intMethod.invoke(vdm, 0, params) as? Any
|
||||||
|
if (device != null) {
|
||||||
|
Log.i(TAG, "VirtualDevice created with id=0: $device")
|
||||||
|
virtualDevice = device
|
||||||
|
createVirtualCamera(device)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
Log.w(TAG, "createVirtualDevice(0, params) returned null")
|
||||||
|
updateNotification("Error: No VDM method worked")
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "All VDM methods failed", e)
|
||||||
|
updateNotification("Error: VDM unavailable")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper to load a class from a sub-package under [vdmPkg].
|
||||||
|
* On Android 16 (API 36) virtual camera classes moved from
|
||||||
|
* `android.companion.virtual.*` to `android.companion.virtual.camera.*`.
|
||||||
|
*/
|
||||||
|
private fun vdmSubClass(subPkg: String, shortName: String): Class<*> =
|
||||||
|
Class.forName("$vdmPkg.$subPkg.$shortName")
|
||||||
|
|
||||||
|
private fun createVirtualCamera(device: Any) {
|
||||||
|
try {
|
||||||
|
val cameraPkg = "camera"
|
||||||
|
|
||||||
|
val callbackClass = vdmSubClass(cameraPkg, "VirtualCameraCallback")
|
||||||
|
val executor = Executors.newSingleThreadExecutor()
|
||||||
|
callbackExecutor = executor
|
||||||
|
|
||||||
|
val callback = Proxy.newProxyInstance(
|
||||||
|
callbackClass.classLoader,
|
||||||
|
arrayOf(callbackClass),
|
||||||
|
InvocationHandler { _, method, args ->
|
||||||
|
when (method.name) {
|
||||||
|
"onStreamConfigured" -> {
|
||||||
|
// Android 16 passes 5 args: streamId, Surface, width, height, format
|
||||||
|
val streamId = args[0] as Int
|
||||||
|
val surface = args[1] as Surface
|
||||||
|
val width = args[2] as Int
|
||||||
|
val height = args[3] as Int
|
||||||
|
Log.i(
|
||||||
|
TAG,
|
||||||
|
"onStreamConfigured: streamId=$streamId, ${width}x${height}"
|
||||||
|
)
|
||||||
|
onStreamConfigured(surface, width, height)
|
||||||
|
}
|
||||||
|
"onStreamClosed" -> {
|
||||||
|
val streamId = args[0] as Int
|
||||||
|
Log.i(TAG, "onStreamClosed: streamId=$streamId")
|
||||||
|
onStreamClosed()
|
||||||
|
}
|
||||||
|
"onProcessCaptureRequest" -> {
|
||||||
|
// Android 16 callback — no-op for now
|
||||||
|
Log.d(TAG, "onProcessCaptureRequest: streamId=${args[0]}")
|
||||||
|
}
|
||||||
|
else -> {
|
||||||
|
Log.w(TAG, "Unknown VirtualCameraCallback: ${method.name}")
|
||||||
|
null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
val configBuilderClass = vdmSubClass(cameraPkg, "VirtualCameraConfig\$Builder")
|
||||||
|
val configClass = vdmSubClass(cameraPkg, "VirtualCameraConfig")
|
||||||
|
|
||||||
|
// Probe available constructors: try single-arg (String) first, fall back to (String, Set)
|
||||||
|
val configBuilder = try {
|
||||||
|
configBuilderClass.getDeclaredConstructor(String::class.java)
|
||||||
|
.newInstance("CamSwapper")
|
||||||
|
} catch (_: NoSuchMethodException) {
|
||||||
|
configBuilderClass.getDeclaredConstructor(
|
||||||
|
String::class.java,
|
||||||
|
Set::class.java
|
||||||
|
).newInstance("CamSwapper", emptySet<Any>())
|
||||||
|
}
|
||||||
|
|
||||||
|
configBuilderClass.getMethod(
|
||||||
|
"addStreamConfig",
|
||||||
|
Int::class.javaPrimitiveType,
|
||||||
|
Int::class.javaPrimitiveType,
|
||||||
|
Int::class.javaPrimitiveType,
|
||||||
|
Int::class.javaPrimitiveType
|
||||||
|
).invoke(configBuilder, 1920, 1080, android.graphics.ImageFormat.YUV_420_888, 30)
|
||||||
|
|
||||||
|
// CameraCharacteristics.LENS_FACING_FRONT = 0 (stable since API 21)
|
||||||
|
// Use literal value to avoid reflection issues on Android 16
|
||||||
|
configBuilderClass.getMethod("setLensFacing", Int::class.javaPrimitiveType)
|
||||||
|
.invoke(configBuilder, 0)
|
||||||
|
|
||||||
|
configBuilderClass.getMethod(
|
||||||
|
"setVirtualCameraCallback",
|
||||||
|
Executor::class.java,
|
||||||
|
callbackClass
|
||||||
|
).invoke(configBuilder, executor, callback)
|
||||||
|
|
||||||
|
val config = configBuilderClass.getMethod("build").invoke(configBuilder)
|
||||||
|
Log.i(TAG, "VirtualCameraConfig built")
|
||||||
|
|
||||||
|
device.javaClass.getMethod("createVirtualCamera", configClass)
|
||||||
|
.invoke(device, config)
|
||||||
|
|
||||||
|
Log.i(TAG, "Virtual camera created and running")
|
||||||
|
updateNotification("Virtual camera running")
|
||||||
|
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to create virtual camera", e)
|
||||||
|
updateNotification("Error: ${e.message}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onStreamConfigured(surface: Surface, width: Int, height: Int) {
|
||||||
|
updateNotification("Stream active: ${width}x${height}")
|
||||||
|
|
||||||
|
val prefs = getSharedPreferences("camera_magic_config", MODE_PRIVATE)
|
||||||
|
val videoId = prefs.getLong("local_video_id", -1L)
|
||||||
|
|
||||||
|
if (videoId > 0) {
|
||||||
|
VideoPusher.start(listOf(surface), width, height, videoId)
|
||||||
|
videoPusherActive = true
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "No video selected (videoId=$videoId)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun onStreamClosed() {
|
||||||
|
if (videoPusherActive) {
|
||||||
|
VideoPusher.stop()
|
||||||
|
videoPusherActive = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun createNotificationChannel() {
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
CHANNEL_ID,
|
||||||
|
"Virtual Camera Service",
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
)
|
||||||
|
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun buildNotification(text: String): Notification {
|
||||||
|
return Notification.Builder(this, CHANNEL_ID)
|
||||||
|
.setContentTitle("CamSwapper")
|
||||||
|
.setContentText(text)
|
||||||
|
.setSmallIcon(R.drawable.ic_notification)
|
||||||
|
.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateNotification(text: String) {
|
||||||
|
val nm = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
nm.notify(NOTIFICATION_ID, buildNotification(text))
|
||||||
|
}
|
||||||
|
}
|
||||||
-52
@@ -2,8 +2,6 @@ package com.nothing.camera2magic.viewmodel
|
|||||||
|
|
||||||
import android.content.SharedPreferences
|
import android.content.SharedPreferences
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
import io.github.libxposed.service.XposedService
|
|
||||||
import io.github.libxposed.service.XposedServiceHelper
|
|
||||||
import androidx.core.content.edit
|
import androidx.core.content.edit
|
||||||
|
|
||||||
private const val TAG = "[VCX][ConfigRepo]"
|
private const val TAG = "[VCX][ConfigRepo]"
|
||||||
@@ -33,21 +31,6 @@ enum class MediaType(val value: Int, val mimeType: String) {
|
|||||||
|
|
||||||
class ConfigRepository(private val prefs: SharedPreferences) {
|
class ConfigRepository(private val prefs: SharedPreferences) {
|
||||||
|
|
||||||
private var xposedService: XposedService? = null
|
|
||||||
|
|
||||||
init {
|
|
||||||
XposedServiceHelper.registerListener(object : XposedServiceHelper.OnServiceListener {
|
|
||||||
override fun onServiceBind(service: XposedService) {
|
|
||||||
Log.i(TAG, "XposedService bound, syncing all to remote")
|
|
||||||
xposedService = service
|
|
||||||
syncAllToRemote()
|
|
||||||
}
|
|
||||||
override fun onServiceDied(service: XposedService) {
|
|
||||||
xposedService = null
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun <T> save(key: String, value: T) {
|
private fun <T> save(key: String, value: T) {
|
||||||
prefs.edit {
|
prefs.edit {
|
||||||
when (value) {
|
when (value) {
|
||||||
@@ -59,41 +42,6 @@ class ConfigRepository(private val prefs: SharedPreferences) {
|
|||||||
else -> throw IllegalArgumentException("Unsupported type")
|
else -> throw IllegalArgumentException("Unsupported type")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
xposedService?.let { service ->
|
|
||||||
try {
|
|
||||||
val remotePrefs = service.getRemotePreferences(GROUP_NAME)
|
|
||||||
remotePrefs.edit {
|
|
||||||
when (value) {
|
|
||||||
is Boolean -> putBoolean(key, value)
|
|
||||||
is Int -> putInt(key, value)
|
|
||||||
is Long -> putLong(key, value)
|
|
||||||
is Float -> putFloat(key, value)
|
|
||||||
is String -> putString(key, value)
|
|
||||||
else -> throw IllegalArgumentException("Unsupported type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Log.e(TAG, "Failed to set_internal_state remote preferences", e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun syncAllToRemote() {
|
|
||||||
xposedService?.let { service ->
|
|
||||||
val remotePrefs = service.getRemotePreferences(GROUP_NAME)
|
|
||||||
remotePrefs.edit {
|
|
||||||
prefs.all.forEach { (key, value) ->
|
|
||||||
when (value) {
|
|
||||||
is Boolean -> putBoolean(key, value)
|
|
||||||
is Int -> putInt(key, value)
|
|
||||||
is Long -> putLong(key, value)
|
|
||||||
is Float -> putFloat(key, value)
|
|
||||||
is String -> putString(key, value)
|
|
||||||
else -> throw IllegalArgumentException("Unsupported type")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var moduleEnabled: Boolean
|
var moduleEnabled: Boolean
|
||||||
Binary file not shown.
@@ -0,0 +1,9 @@
|
|||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
<path
|
||||||
|
android:fillColor="#FFFFFF"
|
||||||
|
android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20,5h-3.17l-1.24,-1.35c-0.37,-0.41 -0.88,-0.65 -1.42,-0.65H9.83c-0.54,0 -1.05,0.24 -1.42,0.65L7.17,5H4c-1.1,0 -2,0.9 -2,2v10c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2V7c0,-1.1 -0.9,-2 -2,-2zM12,18c-3.31,0 -6,-2.69 -6,-6s2.69,-6 6,-6 6,2.69 6,6 -2.69,6 -6,6z" />
|
||||||
|
</vector>
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package com.nothing.camera2magic
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.app.Activity
|
|
||||||
import android.app.Application
|
|
||||||
import android.content.Context
|
|
||||||
import android.widget.Toast
|
|
||||||
import com.nothing.camera2magic.hook.Camera1Hooker
|
|
||||||
import com.nothing.camera2magic.hook.Camera2Hooker
|
|
||||||
import com.nothing.camera2magic.hook.SourceManager
|
|
||||||
import com.nothing.camera2magic.hook.CameraDeviceImplHooker
|
|
||||||
import com.nothing.camera2magic.hook.WebRTCHooker
|
|
||||||
import com.nothing.camera2magic.utils.Dog
|
|
||||||
import io.github.libxposed.api.XposedModule
|
|
||||||
import io.github.libxposed.api.XposedModuleInterface.PackageReadyParam
|
|
||||||
|
|
||||||
class MagicHook : XposedModule() {
|
|
||||||
|
|
||||||
init {
|
|
||||||
System.loadLibrary("camera3")
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
private const val TAG = "[MagicHook]"
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onPackageReady(param: PackageReadyParam) {
|
|
||||||
// Hook for all packages - user will scope to camera apps in LSPosed
|
|
||||||
Dog.i(TAG, "onPackageReady called for package: ${param.packageName}", true)
|
|
||||||
// Do not rely on per-app packageName state for system-wide operation.
|
|
||||||
val remotePrefs = getRemotePreferences("camera_magic_config")
|
|
||||||
SourceManager.skipNativeDispatch = true
|
|
||||||
SourceManager.init(remotePrefs)
|
|
||||||
hookAttach()
|
|
||||||
hookActivity()
|
|
||||||
// Store package name BEFORE hookers (they may access GlobalState.packageName)
|
|
||||||
GlobalState.packageName = param.packageName
|
|
||||||
// App-level hookers (per-app LSPosed scope) - PROVEN WORKING
|
|
||||||
Camera1Hooker.initHooks(this, param)
|
|
||||||
Camera2Hooker.initHooks(this, param)
|
|
||||||
WebRTCHooker.initHooks(this, param)
|
|
||||||
// System-wide CameraDeviceImpl hooker - EXPERIMENTAL, DISABLED BY DEFAULT
|
|
||||||
// Uncomment to test system-wide hooking (scope module to "Android System" in LSPosed)
|
|
||||||
// NOTE: Camera2Hooker and CameraDeviceImplHooker both hook CameraDeviceImpl methods.
|
|
||||||
// Having both active simultaneously causes conflicts. Only enable ONE at a time.
|
|
||||||
// CameraDeviceImplHooker.initHooks(this, param)
|
|
||||||
}
|
|
||||||
|
|
||||||
@SuppressLint("DiscouragedPrivateApi")
|
|
||||||
private fun hookAttach() {
|
|
||||||
val attach = Application::class.java.getDeclaredMethod("attach", Context::class.java)
|
|
||||||
hook(attach).intercept { chain ->
|
|
||||||
GlobalState.appContext = chain.args[0] as Context
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun hookActivity() {
|
|
||||||
val start = Activity::class.java.getDeclaredMethod("onStart")
|
|
||||||
hook(start).intercept { chain ->
|
|
||||||
val result = chain.proceed()
|
|
||||||
val activity = chain.thisObject as Activity
|
|
||||||
GlobalState.activityCount ++
|
|
||||||
SourceManager.refreshAndDispatch()
|
|
||||||
activity.runOnUiThread {
|
|
||||||
val text = "[✨] " + SourceManager.toastMessage
|
|
||||||
Toast.makeText(activity, text, Toast.LENGTH_SHORT).show()
|
|
||||||
}
|
|
||||||
return@intercept result
|
|
||||||
}
|
|
||||||
|
|
||||||
val stop = Activity::class.java.getDeclaredMethod("onStop")
|
|
||||||
hook(stop).intercept { chain ->
|
|
||||||
chain.proceed()
|
|
||||||
GlobalState.activityCount--
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
@file:Suppress("DEPRECATION")
|
|
||||||
|
|
||||||
package com.nothing.camera2magic.hook
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.hardware.Camera
|
|
||||||
import android.view.Surface
|
|
||||||
import android.view.SurfaceHolder
|
|
||||||
import android.graphics.SurfaceTexture
|
|
||||||
import com.nothing.camera2magic.GlobalState
|
|
||||||
import com.nothing.camera2magic.MagicHook
|
|
||||||
import com.nothing.camera2magic.utils.Dog
|
|
||||||
import io.github.libxposed.api.XposedInterface.Chain
|
|
||||||
import io.github.libxposed.api.XposedModuleInterface.PackageReadyParam
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import java.lang.reflect.Proxy
|
|
||||||
import java.util.Collections
|
|
||||||
import java.util.Timer
|
|
||||||
import java.util.WeakHashMap
|
|
||||||
import kotlin.concurrent.schedule
|
|
||||||
|
|
||||||
object Camera1Hooker {
|
|
||||||
private const val TAG = "[CAM1]"
|
|
||||||
|
|
||||||
private val Camera?.shortId : String
|
|
||||||
get() = if (this == null) "null" else "@0x${Integer.toHexString(System.identityHashCode(this))}"
|
|
||||||
|
|
||||||
private var activeCameraRef: WeakReference<Camera>? = null
|
|
||||||
private var cameraState = WeakHashMap<Camera, CameraState>()
|
|
||||||
private var pushMode = false
|
|
||||||
private var blackHole: Any? = null
|
|
||||||
private fun destroyBlackHole() {
|
|
||||||
when (blackHole) {
|
|
||||||
is SurfaceTexture -> {
|
|
||||||
(blackHole as SurfaceTexture).release()
|
|
||||||
}
|
|
||||||
is Surface -> {
|
|
||||||
(blackHole as Surface).release()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
blackHole = null
|
|
||||||
}
|
|
||||||
private fun getCameraState(camera: Camera): CameraState {
|
|
||||||
return synchronized(cameraState) {
|
|
||||||
cameraState.getOrPut(camera) { CameraState() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun isPreviewing(camera: Camera): Boolean {
|
|
||||||
return activeCameraRef?.get() === camera
|
|
||||||
}
|
|
||||||
|
|
||||||
private lateinit var magic: MagicHook
|
|
||||||
private val hookedClasses = Collections.synchronizedSet(
|
|
||||||
Collections.newSetFromMap(WeakHashMap<Class<*>, Boolean>()))
|
|
||||||
|
|
||||||
fun initHooks(module: MagicHook, param: PackageReadyParam) {
|
|
||||||
magic = module
|
|
||||||
Camera::class.java.apply {
|
|
||||||
hookOpenMethod()
|
|
||||||
hookSetParameters()
|
|
||||||
hookSetPreviewTexture()
|
|
||||||
hookSetPreviewDisplay()
|
|
||||||
hookSetDisplayOrientation()
|
|
||||||
hookStartPreview()
|
|
||||||
hookStopPreview()
|
|
||||||
hookRelease()
|
|
||||||
hookSetPreviewCallback()
|
|
||||||
hookAddCallbackBuffer()
|
|
||||||
hookTakePicture()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private val openInterceptor: (Chain) -> Any? = intercept@{ chain ->
|
|
||||||
val camera = chain.proceed() as? Camera ?: return@intercept null
|
|
||||||
activeCameraRef = WeakReference(camera)
|
|
||||||
val cameraId = chain.args.getOrNull(0) as? Int ?: 0
|
|
||||||
val info = Camera.CameraInfo()
|
|
||||||
Camera.getCameraInfo(cameraId, info)
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
|
|
||||||
state.apiLevel = 1
|
|
||||||
state.facingFront = info.facing == Camera.CameraInfo.CAMERA_FACING_FRONT
|
|
||||||
state.sensorOrientation = info.orientation
|
|
||||||
state.packageName = GlobalState.packageName
|
|
||||||
camera
|
|
||||||
}
|
|
||||||
private fun Class<*>.hookOpenMethod() {
|
|
||||||
val open = getDeclaredMethod("open")
|
|
||||||
val openId = getDeclaredMethod("open", Int::class.java)
|
|
||||||
magic.hook(open).intercept(openInterceptor)
|
|
||||||
magic.hook(openId).intercept(openInterceptor)
|
|
||||||
}
|
|
||||||
private fun Class<*>.hookSetParameters() {
|
|
||||||
val setParameters = getDeclaredMethod("setParameters", Camera.Parameters::class.java)
|
|
||||||
magic.hook(setParameters).intercept { chain ->
|
|
||||||
chain.proceed()
|
|
||||||
val camera = chain.thisObject as Camera
|
|
||||||
val params = chain.args[0] as Camera.Parameters
|
|
||||||
val pictureSize = params.pictureSize
|
|
||||||
val previewSize = params.previewSize
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
if (state.pictureWidth != pictureSize.width || state.pictureHeight != pictureSize.height) {
|
|
||||||
state.pictureWidth = pictureSize.width
|
|
||||||
state.pictureHeight = pictureSize.height
|
|
||||||
}
|
|
||||||
if (state.previewWidth != previewSize.width || state.previewHeight != previewSize.height) {
|
|
||||||
state.previewWidth = previewSize.width
|
|
||||||
state.previewHeight = previewSize.height
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun Class<*>.hookSetPreviewTexture() {
|
|
||||||
val setPreviewTexture = getDeclaredMethod("setPreviewTexture",
|
|
||||||
SurfaceTexture::class.java)
|
|
||||||
|
|
||||||
magic.hook(setPreviewTexture).intercept { chain ->
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
val camera = chain.thisObject as Camera
|
|
||||||
val surfaceTexture = chain.args[0] as SurfaceTexture
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
|
|
||||||
val fakeSurfaceTexture = SurfaceTexture(false)
|
|
||||||
.apply { setDefaultBufferSize(1, 1) }
|
|
||||||
val fakeSurface = Surface(fakeSurfaceTexture)
|
|
||||||
state.surfaces.clear()
|
|
||||||
state.surfaces.add(fakeSurface)
|
|
||||||
blackHole = fakeSurfaceTexture.also { chain.proceed(arrayOf(it)) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun Class<*>.hookSetPreviewDisplay() {
|
|
||||||
val setPreviewDisplay = getDeclaredMethod(
|
|
||||||
"setPreviewDisplay",
|
|
||||||
SurfaceHolder::class.java)
|
|
||||||
|
|
||||||
magic.hook(setPreviewDisplay).intercept { chain ->
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
pushMode = true
|
|
||||||
val camera = chain.thisObject as Camera
|
|
||||||
val holder = chain.args[0] as SurfaceHolder
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
@SuppressLint("Recycle")
|
|
||||||
val surfaceTexture = SurfaceTexture(false)
|
|
||||||
.apply { setDefaultBufferSize(1, 1) }
|
|
||||||
val surface = Surface(surfaceTexture).also { blackHole = it }
|
|
||||||
state.surfaces.clear()
|
|
||||||
state.surfaces.add(surface)
|
|
||||||
val surfaceHolderProxy = Proxy.newProxyInstance(holder.javaClass.classLoader,
|
|
||||||
arrayOf(SurfaceHolder::class.java)) { _, method, args ->
|
|
||||||
if (method.name == "getSurface") return@newProxyInstance surface
|
|
||||||
return@newProxyInstance method.invoke(holder, *(args ?: arrayOfNulls<Any>(0)))
|
|
||||||
} as SurfaceHolder
|
|
||||||
chain.proceed(arrayOf(surfaceHolderProxy))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun Class<*>.hookSetDisplayOrientation() {
|
|
||||||
val setDisplayOrientation = getDeclaredMethod(
|
|
||||||
"setDisplayOrientation",
|
|
||||||
Int::class.javaPrimitiveType)
|
|
||||||
|
|
||||||
magic.hook(setDisplayOrientation).intercept { chain ->
|
|
||||||
val camera = chain.thisObject as Camera
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
val displayOrientation = chain.args[0] as Int
|
|
||||||
if (!SourceManager.isReadyForHook() || state.displayOrientation == displayOrientation) return@intercept chain.proceed()
|
|
||||||
state.displayOrientation = displayOrientation
|
|
||||||
if (isPreviewing(camera)) {
|
|
||||||
NativeBridge.setDisplayOrientation(displayOrientation)
|
|
||||||
}
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun Class<*>.hookStartPreview() {
|
|
||||||
val startPreview = getDeclaredMethod("startPreview")
|
|
||||||
magic.hook(startPreview).intercept { chain ->
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
val camera = chain.thisObject as Camera
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
val activeCamera = activeCameraRef?.get()
|
|
||||||
if (activeCamera != null && camera === activeCamera) {
|
|
||||||
NativeBridge.registerSurfaceIfNew(state, true)
|
|
||||||
NativeBridge.needStartRenderer()
|
|
||||||
}
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun Class<*>.hookStopPreview() {
|
|
||||||
val stopPreview = getDeclaredMethod("stopPreview")
|
|
||||||
magic.hook(stopPreview).intercept { chain ->
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
val camera = chain.thisObject as Camera
|
|
||||||
val activeCamera = activeCameraRef?.get()
|
|
||||||
if (activeCamera != null && camera === activeCamera) {
|
|
||||||
NativeBridge.needStopRenderer()
|
|
||||||
}
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun Class<*>.hookRelease() {
|
|
||||||
val release = getDeclaredMethod("release")
|
|
||||||
magic.hook(release).intercept { chain ->
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
val closingCamera = chain.thisObject as Camera
|
|
||||||
val activeCamera = activeCameraRef?.get()
|
|
||||||
|
|
||||||
if (activeCamera != null && closingCamera === activeCamera) {
|
|
||||||
NativeBridge.needStopRenderer()
|
|
||||||
NativeBridge.releaseLastRegisteredSurface()
|
|
||||||
destroyBlackHole()
|
|
||||||
activeCameraRef = null
|
|
||||||
}
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private val previewCallbackInterceptor: (Chain) -> Any? = intercept@ { chain ->
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
val camera = chain.thisObject as Camera
|
|
||||||
val originCallback = chain.args[0] as? Camera.PreviewCallback ?: return@intercept chain.proceed()
|
|
||||||
val clazz = originCallback.javaClass
|
|
||||||
if (hookedClasses.add(clazz)) {
|
|
||||||
val onPreviewFrame = clazz.getDeclaredMethod(
|
|
||||||
"onPreviewFrame",
|
|
||||||
ByteArray::class.java,
|
|
||||||
Camera::class.java)
|
|
||||||
magic.hook(onPreviewFrame).intercept { frame ->
|
|
||||||
val originBuffer = frame.args[0] as ByteArray
|
|
||||||
NativeBridge.overwritePreviewBuffer(originBuffer)
|
|
||||||
frame.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookSetPreviewCallback() {
|
|
||||||
val setPreviewCallback = getDeclaredMethod(
|
|
||||||
"setPreviewCallback",
|
|
||||||
Camera.PreviewCallback::class.java)
|
|
||||||
val setPreviewCallbackWithBuffer = getDeclaredMethod(
|
|
||||||
"setPreviewCallbackWithBuffer",
|
|
||||||
Camera.PreviewCallback::class.java)
|
|
||||||
magic.hook(setPreviewCallback).intercept(previewCallbackInterceptor)
|
|
||||||
magic.hook(setPreviewCallbackWithBuffer).intercept(previewCallbackInterceptor)
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookAddCallbackBuffer() {
|
|
||||||
val addCallbackBuffer = getDeclaredMethod("addCallbackBuffer",
|
|
||||||
ByteArray::class.java)
|
|
||||||
// TODO:
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookTakePicture() {
|
|
||||||
val takePicture = getDeclaredMethod(
|
|
||||||
"takePicture",
|
|
||||||
Camera.ShutterCallback::class.java,
|
|
||||||
Camera.PictureCallback::class.java, // raw
|
|
||||||
Camera.PictureCallback::class.java, // post view
|
|
||||||
Camera.PictureCallback::class.java) // jpeg
|
|
||||||
|
|
||||||
magic.hook(takePicture).intercept { chain ->
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
chain.args[3]?.let { cb ->
|
|
||||||
val clazz = (cb as Camera.PictureCallback).javaClass
|
|
||||||
if (hookedClasses.add(clazz)) {
|
|
||||||
val onPictureTaken = clazz.getDeclaredMethod("onPictureTaken",
|
|
||||||
ByteArray::class.java, Camera::class.java)
|
|
||||||
magic.hook(onPictureTaken).intercept { shot ->
|
|
||||||
val newArgs = shot.args.toTypedArray()
|
|
||||||
newArgs[0] = NativeBridge.overwriteJPEGBytes()
|
|
||||||
shot.proceed(newArgs)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,342 +0,0 @@
|
|||||||
package com.nothing.camera2magic.hook
|
|
||||||
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.content.Context
|
|
||||||
import android.hardware.camera2.CameraCaptureSession
|
|
||||||
import android.hardware.camera2.CameraDevice
|
|
||||||
import android.hardware.camera2.CameraManager
|
|
||||||
import android.hardware.camera2.CameraCharacteristics
|
|
||||||
import android.hardware.camera2.params.OutputConfiguration
|
|
||||||
import android.hardware.camera2.params.SessionConfiguration
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import android.view.Surface
|
|
||||||
import android.view.WindowManager
|
|
||||||
import com.nothing.camera2magic.GlobalState
|
|
||||||
import com.nothing.camera2magic.MagicHook
|
|
||||||
import com.nothing.camera2magic.hook.NativeBridge.needStartRenderer
|
|
||||||
import com.nothing.camera2magic.hook.NativeBridge.needStopRenderer
|
|
||||||
import com.nothing.camera2magic.hook.NativeBridge.registerSurfaceIfNew
|
|
||||||
import com.nothing.camera2magic.hook.NativeBridge.releaseLastRegisteredSurface
|
|
||||||
|
|
||||||
import com.nothing.camera2magic.utils.Dog
|
|
||||||
|
|
||||||
import io.github.libxposed.api.XposedModuleInterface.PackageReadyParam
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import java.util.Collections
|
|
||||||
import java.util.WeakHashMap
|
|
||||||
|
|
||||||
object Camera2Hooker {
|
|
||||||
private const val TAG = "[CAM2]"
|
|
||||||
|
|
||||||
private val CameraDevice?.shortId : String
|
|
||||||
get() = if (this == null) "null" else "@0x${Integer.toHexString(System.identityHashCode(this))}"
|
|
||||||
private lateinit var magic: MagicHook
|
|
||||||
private val hookedClasses = Collections.synchronizedSet(
|
|
||||||
Collections.newSetFromMap(WeakHashMap<Class<*>, Boolean>()))
|
|
||||||
private var activeCameraRef: WeakReference<Any>? = null
|
|
||||||
private var cameraState = WeakHashMap<CameraDevice, CameraState>()
|
|
||||||
private fun getCameraState(camera: CameraDevice): CameraState {
|
|
||||||
return synchronized(cameraState) {
|
|
||||||
cameraState.getOrPut(camera) { CameraState() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
private fun CameraState.saveCameraInfo(camera: CameraDevice) {
|
|
||||||
val cameraIdStr = camera.id
|
|
||||||
val context = GlobalState.appContext
|
|
||||||
val cm = context.getSystemService(Context.CAMERA_SERVICE) as CameraManager
|
|
||||||
val characteristics = cm.getCameraCharacteristics(cameraIdStr)
|
|
||||||
|
|
||||||
val wm = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager
|
|
||||||
@Suppress("DEPRECATION")
|
|
||||||
val rotation = wm.defaultDisplay.rotation
|
|
||||||
|
|
||||||
this.apiLevel = 2
|
|
||||||
this.sensorOrientation = characteristics.get(CameraCharacteristics.SENSOR_ORIENTATION) ?: 90
|
|
||||||
this.facingFront = characteristics.get(CameraCharacteristics.LENS_FACING) == CameraCharacteristics.LENS_FACING_FRONT
|
|
||||||
this.displayOrientation = rotation * 90
|
|
||||||
this.packageName = GlobalState.packageName
|
|
||||||
}
|
|
||||||
private fun CameraState.bindSurface(surface: Surface) {
|
|
||||||
val (width, height, _) = NativeBridge.getSurfaceInfo(surface)
|
|
||||||
// Keep the largest surface as the primary reference for VideoPusher resolution
|
|
||||||
if (width * height > this.previewWidth * this.previewHeight) {
|
|
||||||
this.previewWidth = width
|
|
||||||
this.previewHeight = height
|
|
||||||
this.pictureWidth = width
|
|
||||||
this.pictureHeight = height
|
|
||||||
}
|
|
||||||
this.surfaces.add(surface)
|
|
||||||
}
|
|
||||||
private fun handleStateCallback(callback: CameraCaptureSession.StateCallback) {
|
|
||||||
val clazz = callback.javaClass
|
|
||||||
if (hookedClasses.add(clazz)) {
|
|
||||||
val onConfigured = clazz.getDeclaredMethod("onConfigured",
|
|
||||||
CameraCaptureSession::class.java)
|
|
||||||
|
|
||||||
magic.hook(onConfigured).intercept { chain ->
|
|
||||||
val session = chain.args[0] as CameraCaptureSession
|
|
||||||
val camera = session.device
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
|
|
||||||
SourceManager.refreshAndDispatch()
|
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
|
||||||
val videoId = SourceManager.getVideoId()
|
|
||||||
if (videoId != -1L && state.surfaces.isNotEmpty()) {
|
|
||||||
Dog.i(TAG, "Starting VideoPusher on ${state.surfaces.size} surfaces", true)
|
|
||||||
VideoPusher.start(state.surfaces, state.previewWidth, state.previewHeight, videoId)
|
|
||||||
}
|
|
||||||
}, 100)
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
|
|
||||||
val onConfigureFailed = clazz.getDeclaredMethod("onConfigureFailed",
|
|
||||||
CameraCaptureSession::class.java)
|
|
||||||
|
|
||||||
magic.hook(onConfigureFailed).intercept { chain ->
|
|
||||||
Dog.e(TAG, "CameraCaptureSession.StateCallback: onConfigureFailed.", null, true)
|
|
||||||
BlackHoleMapper.clearAll()
|
|
||||||
activeCameraRef = null
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@SuppressLint("PrivateApi")
|
|
||||||
fun initHooks(module: MagicHook, param: PackageReadyParam) {
|
|
||||||
magic = module
|
|
||||||
val classLoader = param.classLoader
|
|
||||||
val deviceImplClass = classLoader.loadClass("android.hardware.camera2.impl.CameraDeviceImpl")
|
|
||||||
deviceImplClass.apply {
|
|
||||||
hookCreateCaptureSessionWithConfiguration()
|
|
||||||
hookCreateCaptureSessionWithSurfaces()
|
|
||||||
hookCreateCaptureSessionByOutputConfigurations()
|
|
||||||
hookClose()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Android 14+ CameraDeviceSetup support
|
|
||||||
try {
|
|
||||||
val setupClass = classLoader.loadClass("android.hardware.camera2.impl.CameraDeviceSetupImpl")
|
|
||||||
setupClass.apply {
|
|
||||||
hookCreateCaptureSessionWithConfiguration()
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Class might not exist on older versions
|
|
||||||
}
|
|
||||||
|
|
||||||
val builderClass = classLoader.loadClass("android.hardware.camera2.CaptureRequest\$Builder")
|
|
||||||
builderClass.apply {
|
|
||||||
hookAddTarget()
|
|
||||||
hookRemoveTarget()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookCreateCaptureSessionWithConfiguration() {
|
|
||||||
val method = try {
|
|
||||||
getDeclaredMethod("createCaptureSession", SessionConfiguration::class.java)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
method?.let { m ->
|
|
||||||
magic.hook(m).intercept { chain ->
|
|
||||||
Dog.i(TAG, "[CANARY] createCaptureSession(SessionConfiguration) enter", true)
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
|
|
||||||
try {
|
|
||||||
val camera = chain.thisObject as CameraDevice
|
|
||||||
activeCameraRef = WeakReference(camera)
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
state.saveCameraInfo(camera)
|
|
||||||
BlackHoleMapper.clearAll()
|
|
||||||
|
|
||||||
val sessionConfiguration = chain.args[0] as SessionConfiguration
|
|
||||||
Dog.i(TAG, "processing ${sessionConfiguration.outputConfigurations.size} configurations", true)
|
|
||||||
|
|
||||||
@SuppressLint("SoonBlockedPrivateApi")
|
|
||||||
val field = OutputConfiguration::class.java.getDeclaredField("mSurfaces")
|
|
||||||
field.isAccessible = true
|
|
||||||
sessionConfiguration.outputConfigurations.forEach { outputConfiguration ->
|
|
||||||
var modified = false
|
|
||||||
val surfaces = outputConfiguration.surfaces
|
|
||||||
val modifiedSurfaces = surfaces.mapTo(ArrayList<Surface>()) { origin ->
|
|
||||||
val (w, h, f) = NativeBridge.getSurfaceInfo(origin)
|
|
||||||
Dog.i(TAG, "Surface info: origin=$origin, w=$w, h=$h, f=$f", true)
|
|
||||||
|
|
||||||
if (f == 1 || f == 34 || f == 35 || f == 4 || w > 0) {
|
|
||||||
modified = true
|
|
||||||
val blackHoleSurface = BlackHoleMapper.createBlackHole(origin)
|
|
||||||
Dog.i(TAG, "REPLACED surface $origin with $blackHoleSurface (format $f)", true)
|
|
||||||
state.bindSurface(origin)
|
|
||||||
return@mapTo blackHoleSurface
|
|
||||||
}
|
|
||||||
origin
|
|
||||||
}
|
|
||||||
if (modified) field.set(outputConfiguration, modifiedSurfaces)
|
|
||||||
}
|
|
||||||
handleStateCallback(sessionConfiguration.stateCallback)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Error in createCaptureSession(SessionConfiguration) hook", e, true)
|
|
||||||
}
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookCreateCaptureSessionWithSurfaces() {
|
|
||||||
val method = try {
|
|
||||||
getDeclaredMethod(
|
|
||||||
"createCaptureSession",
|
|
||||||
List::class.java,
|
|
||||||
CameraCaptureSession.StateCallback::class.java,
|
|
||||||
Handler::class.java)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
method?.let { m ->
|
|
||||||
magic.hook(m).intercept { chain ->
|
|
||||||
Dog.i(TAG, "[CANARY] createCaptureSession(List, Callback, Handler) enter", true)
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
|
|
||||||
try {
|
|
||||||
val camera = chain.thisObject as CameraDevice
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
activeCameraRef = WeakReference(camera)
|
|
||||||
BlackHoleMapper.clearAll()
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val surfaces = chain.args[0] as List<Surface>
|
|
||||||
Dog.i(TAG, "processing ${surfaces.size} surfaces", true)
|
|
||||||
|
|
||||||
val newList = surfaces.mapTo(ArrayList()) { origin ->
|
|
||||||
val (w, h, f) = NativeBridge.getSurfaceInfo(origin)
|
|
||||||
Dog.i(TAG, "Surface info: origin=$origin, w=$w, h=$h, f=$f", true)
|
|
||||||
|
|
||||||
if (f == 1 || f == 34 || f == 35 || f == 4 || w > 0) {
|
|
||||||
val blackHoleSurface = BlackHoleMapper.createBlackHole(origin)
|
|
||||||
Dog.i(TAG, "REPLACED surface $origin with $blackHoleSurface (format $f)", true)
|
|
||||||
// Bind the ORIGINAL surface to the native renderer
|
|
||||||
state.bindSurface(origin)
|
|
||||||
NativeBridge.registerSurfaceIfNew(state, true)
|
|
||||||
return@mapTo blackHoleSurface
|
|
||||||
}
|
|
||||||
origin
|
|
||||||
}
|
|
||||||
|
|
||||||
val stateCallback = chain.args[1] as CameraCaptureSession.StateCallback
|
|
||||||
handleStateCallback(stateCallback)
|
|
||||||
|
|
||||||
val newArgs = chain.args.toTypedArray()
|
|
||||||
newArgs[0] = newList
|
|
||||||
return@intercept chain.proceed(newArgs)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Error in createCaptureSession(List) hook", e, true)
|
|
||||||
}
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookCreateCaptureSessionByOutputConfigurations() {
|
|
||||||
val method = try {
|
|
||||||
getDeclaredMethod(
|
|
||||||
"createCaptureSessionByOutputConfigurations",
|
|
||||||
List::class.java,
|
|
||||||
CameraCaptureSession.StateCallback::class.java,
|
|
||||||
Handler::class.java)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
null
|
|
||||||
}
|
|
||||||
method?.let { m ->
|
|
||||||
magic.hook(m).intercept { chain ->
|
|
||||||
Dog.i(TAG, "[CANARY] createCaptureSessionByOutputConfigurations enter", true)
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
|
|
||||||
try {
|
|
||||||
val camera = chain.thisObject as CameraDevice
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
activeCameraRef = WeakReference(camera)
|
|
||||||
BlackHoleMapper.clearAll()
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val configs = chain.args[0] as List<OutputConfiguration>
|
|
||||||
Dog.i(TAG, "processing ${configs.size} output configurations", true)
|
|
||||||
|
|
||||||
val field = OutputConfiguration::class.java.getDeclaredField("mSurfaces")
|
|
||||||
field.isAccessible = true
|
|
||||||
|
|
||||||
configs.forEach { outputConfiguration ->
|
|
||||||
var modified = false
|
|
||||||
val surfaces = outputConfiguration.surfaces
|
|
||||||
val modifiedSurfaces = surfaces.mapTo(ArrayList<Surface>()) { origin ->
|
|
||||||
val (w, h, f) = NativeBridge.getSurfaceInfo(origin)
|
|
||||||
Dog.i(TAG, "Surface info: origin=$origin, w=$w, h=$h, f=$f", true)
|
|
||||||
|
|
||||||
if (f == 1 || f == 34 || f == 35 || f == 4 || w > 0) {
|
|
||||||
modified = true
|
|
||||||
val blackHoleSurface = BlackHoleMapper.createBlackHole(origin)
|
|
||||||
Dog.i(TAG, "REPLACED surface $origin with $blackHoleSurface (format $f)", true)
|
|
||||||
// Bind the ORIGINAL surface to the native renderer
|
|
||||||
state.bindSurface(origin)
|
|
||||||
NativeBridge.registerSurfaceIfNew(state, true)
|
|
||||||
return@mapTo blackHoleSurface
|
|
||||||
}
|
|
||||||
origin
|
|
||||||
}
|
|
||||||
if (modified) field.set(outputConfiguration, modifiedSurfaces)
|
|
||||||
}
|
|
||||||
|
|
||||||
val stateCallback = chain.args[1] as CameraCaptureSession.StateCallback
|
|
||||||
handleStateCallback(stateCallback)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Error in createCaptureSessionByOutputConfigurations hook", e, true)
|
|
||||||
}
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookClose() {
|
|
||||||
val close = getMethod("close")
|
|
||||||
magic.hook(close).intercept { chain ->
|
|
||||||
val activeCamera = activeCameraRef?.get() as? CameraDevice
|
|
||||||
val closingCamera = chain.thisObject as CameraDevice
|
|
||||||
|
|
||||||
if (activeCamera != null && closingCamera === activeCamera) {
|
|
||||||
Dog.i(TAG, "camera[${closingCamera.shortId}] close.", true)
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
VideoPusher.stop()
|
|
||||||
}
|
|
||||||
// Don't clear all immediately to avoid switch crash
|
|
||||||
activeCameraRef = null
|
|
||||||
}
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookAddTarget() {
|
|
||||||
val addTarget = getDeclaredMethod("addTarget", Surface::class.java)
|
|
||||||
magic.hook(addTarget).intercept { chain ->
|
|
||||||
val origin = chain.args[0] as Surface
|
|
||||||
val blackHole = BlackHoleMapper.getBlackHole(origin)
|
|
||||||
if (!SourceManager.isReadyForHook() || blackHole == null) {
|
|
||||||
return@intercept chain.proceed()
|
|
||||||
}
|
|
||||||
chain.proceed(arrayOf(blackHole))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookRemoveTarget() {
|
|
||||||
val removeTarget = getDeclaredMethod("removeTarget", Surface::class.java)
|
|
||||||
magic.hook(removeTarget).intercept { chain ->
|
|
||||||
val origin = chain.args[0] as Surface
|
|
||||||
val blackHole = BlackHoleMapper.getBlackHole(origin)
|
|
||||||
if (!SourceManager.isReadyForHook() || blackHole == null) {
|
|
||||||
return@intercept chain.proceed()
|
|
||||||
}
|
|
||||||
chain.proceed(arrayOf(blackHole))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@@ -1,524 +0,0 @@
|
|||||||
package com.nothing.camera2magic.hook
|
|
||||||
|
|
||||||
import android.hardware.camera2.CameraCaptureSession
|
|
||||||
import android.hardware.camera2.CameraDevice
|
|
||||||
import android.hardware.camera2.CameraCaptureSession.StateCallback
|
|
||||||
import android.os.Handler
|
|
||||||
import android.os.Looper
|
|
||||||
import com.nothing.camera2magic.GlobalState
|
|
||||||
import com.nothing.camera2magic.hook.NativeBridge
|
|
||||||
import com.nothing.camera2magic.utils.Dog
|
|
||||||
import com.nothing.camera2magic.hook.SourceManager
|
|
||||||
import com.nothing.camera2magic.hook.VideoPusher
|
|
||||||
import com.nothing.camera2magic.hook.BlackHoleMapper
|
|
||||||
import com.nothing.camera2magic.hook.CameraState
|
|
||||||
import io.github.libxposed.api.XposedModuleInterface.PackageReadyParam
|
|
||||||
import java.lang.ref.WeakReference
|
|
||||||
import java.util.Collections
|
|
||||||
import java.util.WeakHashMap
|
|
||||||
import kotlin.math.min
|
|
||||||
import android.annotation.SuppressLint
|
|
||||||
import android.view.Surface
|
|
||||||
import android.hardware.camera2.params.OutputConfiguration
|
|
||||||
import android.hardware.camera2.params.SessionConfiguration
|
|
||||||
import com.nothing.camera2magic.MagicHook
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Framework-level hooker for CameraDeviceImpl.
|
|
||||||
* Handles system-wide camera session creation interception.
|
|
||||||
*/
|
|
||||||
object CameraDeviceImplHooker {
|
|
||||||
private const val TAG = "[CAMDEV-IMPL-HK]"
|
|
||||||
private lateinit var magic: MagicHook
|
|
||||||
private val hookedClasses = Collections.synchronizedSet(
|
|
||||||
Collections.newSetFromMap(WeakHashMap<Class<*>, Boolean>()))
|
|
||||||
private var activeCameraRef: WeakReference<Any>? = null
|
|
||||||
private val cameraState = WeakHashMap<CameraDevice, CameraState>()
|
|
||||||
|
|
||||||
fun initHooks(module: MagicHook, param: PackageReadyParam) {
|
|
||||||
Dog.i(TAG, "initHooks called", true)
|
|
||||||
magic = module
|
|
||||||
val classLoader = param.classLoader
|
|
||||||
try {
|
|
||||||
// Access the framework class to prepare for hooking
|
|
||||||
Dog.i(TAG, "Loading CameraDeviceImpl class", true)
|
|
||||||
val deviceImplClass = classLoader.loadClass("android.hardware.camera2.impl.CameraDeviceImpl")
|
|
||||||
Dog.i(TAG, "CameraDeviceImpl loaded successfully", true)
|
|
||||||
deviceImplClass.apply {
|
|
||||||
Dog.i(TAG, "Hooking createCaptureSession variants", true)
|
|
||||||
// Register each hook independently to avoid one failing blocking others
|
|
||||||
try {
|
|
||||||
hookCreateCaptureSessionWithSurfaces()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Failed to register hook: createCaptureSessionWithSurfaces", e, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
hookCreateCaptureSessionByOutputConfigurations()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Failed to register hook: createCaptureSessionByOutputConfigurations", e, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
hookCreateCaptureSessionWithConfiguration()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Failed to register hook: createCaptureSessionWithConfiguration", e, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
hookClose()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Failed to register hook: close", e, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
Dog.i(TAG, "Hook registration attempts finished", true)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Best-effort: do not crash module loading if framework class is unavailable
|
|
||||||
Dog.e(TAG, "Failed to access CameraDeviceImpl", e, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Android 14+ CameraDeviceSetup support
|
|
||||||
try {
|
|
||||||
val setupClass = classLoader.loadClass("android.hardware.camera2.impl.CameraDeviceSetupImpl")
|
|
||||||
setupClass.apply {
|
|
||||||
try {
|
|
||||||
hookCreateCaptureSessionWithConfiguration()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Failed to register hook: createCaptureSessionWithConfiguration in CameraDeviceSetupImpl", e, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
// Class might not exist on older versions
|
|
||||||
}
|
|
||||||
|
|
||||||
// Hook CaptureRequest.Builder methods
|
|
||||||
try {
|
|
||||||
val builderClass = classLoader.loadClass("android.hardware.camera2.CaptureRequest\$Builder")
|
|
||||||
builderClass.apply {
|
|
||||||
try {
|
|
||||||
hookAddTarget()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Failed to register hook: addTarget", e, true)
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
hookRemoveTarget()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Failed to register hook: removeTarget", e, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Failed to load CaptureRequest.Builder", e, true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookCreateCaptureSessionWithSurfaces() {
|
|
||||||
val method = try {
|
|
||||||
getDeclaredMethod(
|
|
||||||
"createCaptureSession",
|
|
||||||
List::class.java,
|
|
||||||
CameraCaptureSession.StateCallback::class.java,
|
|
||||||
Handler::class.java)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Could not locate hook method for createCaptureSession(List, StateCallback, Handler)", e, true)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
method?.let { m ->
|
|
||||||
magic.hook(m).intercept { chain ->
|
|
||||||
BlackHoleMapper.clearAll()
|
|
||||||
Dog.i(TAG, "[SYSTEM] createCaptureSession(List, Callback, Handler) enter", true)
|
|
||||||
Dog.i(TAG, "isReadyForHook check", true)
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
|
|
||||||
try {
|
|
||||||
val camera = chain.thisObject as CameraDevice
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
activeCameraRef = WeakReference(camera)
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val surfaces = chain.args[0] as List<Surface>
|
|
||||||
Dog.i(TAG, "Processing ${surfaces.size} surfaces", true)
|
|
||||||
|
|
||||||
val newList = surfaces.mapTo(ArrayList()) { origin ->
|
|
||||||
val (w, h, f) = NativeBridge.getSurfaceInfo(origin)
|
|
||||||
Dog.i(TAG, "Surface info: origin=$origin, w=$w, h=$h, f=$f", true)
|
|
||||||
|
|
||||||
if (f == 1 || f == 34 || f == 35 || f == 4 || w > 0) {
|
|
||||||
val blackHoleSurface = BlackHoleMapper.createBlackHole(origin)
|
|
||||||
Dog.i(TAG, "REPLACED surface $origin with $blackHoleSurface (format $f)", true)
|
|
||||||
state.bindSurface(origin)
|
|
||||||
return@mapTo blackHoleSurface
|
|
||||||
}
|
|
||||||
origin
|
|
||||||
}
|
|
||||||
|
|
||||||
val stateCallback = chain.args[1] as StateCallback
|
|
||||||
Dog.i(TAG, "About to call handleStateCallback", true)
|
|
||||||
try {
|
|
||||||
handleStateCallback(stateCallback)
|
|
||||||
Dog.i(TAG, "handleStateCallback completed", true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "handleStateCallback failed", e, true)
|
|
||||||
}
|
|
||||||
Dog.i(TAG, "About to call checkAndStartVideoPusherFallback", true)
|
|
||||||
try {
|
|
||||||
// Fallback to start VideoPusher if onConfigured doesn't fire
|
|
||||||
checkAndStartVideoPusherFallback(camera, state)
|
|
||||||
Dog.i(TAG, "checkAndStartVideoPusherFallback completed", true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "checkAndStartVideoPusherFallback failed", e, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
val newArgs = chain.args.toTypedArray()
|
|
||||||
newArgs[0] = newList
|
|
||||||
return@intercept chain.proceed(newArgs)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Error in createCaptureSession(List) hook", e, true)
|
|
||||||
return@intercept chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookCreateCaptureSessionByOutputConfigurations() {
|
|
||||||
val method = try {
|
|
||||||
getDeclaredMethod(
|
|
||||||
"createCaptureSessionByOutputConfigurations",
|
|
||||||
List::class.java,
|
|
||||||
CameraCaptureSession.StateCallback::class.java,
|
|
||||||
Handler::class.java)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Could not locate hook method for createCaptureSessionByOutputConfigurations", e, true)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
method?.let { m ->
|
|
||||||
magic.hook(m).intercept { chain ->
|
|
||||||
BlackHoleMapper.clearAll()
|
|
||||||
Dog.i(TAG, "[SYSTEM] createCaptureSessionByOutputConfigurations enter", true)
|
|
||||||
Dog.i(TAG, "isReadyForHook check", true)
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
|
|
||||||
try {
|
|
||||||
val camera = chain.thisObject as CameraDevice
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
activeCameraRef = WeakReference(camera)
|
|
||||||
|
|
||||||
@Suppress("UNCHECKED_CAST")
|
|
||||||
val configs = chain.args[0] as List<OutputConfiguration>
|
|
||||||
Dog.i(TAG, "Processing ${configs.size} output configurations", true)
|
|
||||||
|
|
||||||
// Create new OutputConfiguration objects instead of modifying existing ones
|
|
||||||
val newConfigs = configs.map { oc ->
|
|
||||||
var modified = false
|
|
||||||
val surfaces = oc.surfaces
|
|
||||||
val modifiedSurfaces = surfaces.mapTo(ArrayList<Surface>()) { origin ->
|
|
||||||
val (w, h, f) = NativeBridge.getSurfaceInfo(origin)
|
|
||||||
Dog.i(TAG, "Surface info: origin=$origin, w=$w, h=$h, f=$f", true)
|
|
||||||
|
|
||||||
if (f == 1 || f == 34 || f == 35 || f == 4 || w > 0) {
|
|
||||||
modified = true
|
|
||||||
val blackHoleSurface = BlackHoleMapper.createBlackHole(origin)
|
|
||||||
Dog.i(TAG, "REPLACED surface $origin with $blackHoleSurface (format $f)", true)
|
|
||||||
state.bindSurface(origin)
|
|
||||||
return@mapTo blackHoleSurface
|
|
||||||
}
|
|
||||||
origin
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any surface was modified, create a new OutputConfiguration
|
|
||||||
if (modified) {
|
|
||||||
val newOc = OutputConfiguration(modifiedSurfaces[0])
|
|
||||||
for (i in 1 until modifiedSurfaces.size) {
|
|
||||||
newOc.addSurface(modifiedSurfaces[i])
|
|
||||||
}
|
|
||||||
newOc
|
|
||||||
} else {
|
|
||||||
// No modification needed, keep original
|
|
||||||
oc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val stateCallback = chain.args[1] as StateCallback
|
|
||||||
Dog.i(TAG, "About to call handleStateCallback", true)
|
|
||||||
try {
|
|
||||||
handleStateCallback(stateCallback)
|
|
||||||
Dog.i(TAG, "handleStateCallback completed", true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "handleStateCallback failed", e, true)
|
|
||||||
}
|
|
||||||
Dog.i(TAG, "About to call checkAndStartVideoPusherFallback", true)
|
|
||||||
try {
|
|
||||||
// Fallback to start VideoPusher if onConfigured doesn't fire
|
|
||||||
checkAndStartVideoPusherFallback(camera, state)
|
|
||||||
Dog.i(TAG, "checkAndStartVideoPusherFallback completed", true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "checkAndStartVideoPusherFallback failed", e, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proceed with new configurations
|
|
||||||
val newArgs = chain.args.toTypedArray()
|
|
||||||
newArgs[0] = newConfigs
|
|
||||||
return@intercept chain.proceed(newArgs)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Error in createCaptureSessionByOutputConfigurations hook", e, true)
|
|
||||||
return@intercept chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookCreateCaptureSessionWithConfiguration() {
|
|
||||||
val method = try {
|
|
||||||
getDeclaredMethod("createCaptureSession", SessionConfiguration::class.java)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Could not locate hook method for createCaptureSession(SessionConfiguration)", e, true)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
method?.let { m ->
|
|
||||||
magic.hook(m).intercept { chain ->
|
|
||||||
BlackHoleMapper.clearAll()
|
|
||||||
Dog.i(TAG, "[SYSTEM] createCaptureSession(SessionConfiguration) enter", true)
|
|
||||||
Dog.i(TAG, "isReadyForHook check", true)
|
|
||||||
if (!SourceManager.isReadyForHook()) return@intercept chain.proceed()
|
|
||||||
|
|
||||||
try {
|
|
||||||
val camera = chain.thisObject as CameraDevice
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
activeCameraRef = WeakReference(camera)
|
|
||||||
|
|
||||||
val sessionConfiguration = chain.args[0] as SessionConfiguration
|
|
||||||
Dog.i(TAG, "Processing ${sessionConfiguration.outputConfigurations.size} configurations", true)
|
|
||||||
|
|
||||||
// Create new OutputConfiguration objects instead of modifying existing ones
|
|
||||||
val newConfigs = sessionConfiguration.outputConfigurations.map { oc ->
|
|
||||||
var modified = false
|
|
||||||
val surfaces = oc.surfaces
|
|
||||||
val modifiedSurfaces = surfaces.mapTo(ArrayList<Surface>()) { origin ->
|
|
||||||
val (w, h, f) = NativeBridge.getSurfaceInfo(origin)
|
|
||||||
Dog.i(TAG, "Surface info: origin=$origin, w=$w, h=$h, f=$f", true)
|
|
||||||
|
|
||||||
if (f == 1 || f == 34 || f == 35 || f == 4 || w > 0) {
|
|
||||||
modified = true
|
|
||||||
val blackHoleSurface = BlackHoleMapper.createBlackHole(origin)
|
|
||||||
Dog.i(TAG, "REPLACED surface $origin with $blackHoleSurface (format $f)", true)
|
|
||||||
state.bindSurface(origin)
|
|
||||||
return@mapTo blackHoleSurface
|
|
||||||
}
|
|
||||||
origin
|
|
||||||
}
|
|
||||||
|
|
||||||
// If any surface was modified, create a new OutputConfiguration
|
|
||||||
if (modified) {
|
|
||||||
val newOc = OutputConfiguration(modifiedSurfaces[0])
|
|
||||||
for (i in 1 until modifiedSurfaces.size) {
|
|
||||||
newOc.addSurface(modifiedSurfaces[i])
|
|
||||||
}
|
|
||||||
newOc
|
|
||||||
} else {
|
|
||||||
// No modification needed, keep original
|
|
||||||
oc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create new SessionConfiguration with new OutputConfigurations
|
|
||||||
// Use the original executor from the session configuration
|
|
||||||
val originalExecutor = try {
|
|
||||||
val execField = SessionConfiguration::class.java.getDeclaredField("mExecutor")
|
|
||||||
execField.isAccessible = true
|
|
||||||
execField.get(sessionConfiguration) as? java.util.concurrent.Executor
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.w(TAG, "Could not get original executor from SessionConfiguration", true)
|
|
||||||
null
|
|
||||||
}
|
|
||||||
val newSessionConfig = if (originalExecutor != null) {
|
|
||||||
SessionConfiguration(
|
|
||||||
sessionConfiguration.sessionType,
|
|
||||||
ArrayList<OutputConfiguration>(newConfigs),
|
|
||||||
originalExecutor,
|
|
||||||
sessionConfiguration.stateCallback
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
SessionConfiguration(
|
|
||||||
sessionConfiguration.sessionType,
|
|
||||||
ArrayList<OutputConfiguration>(newConfigs)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Dog.i(TAG, "About to call handleStateCallback", true)
|
|
||||||
try {
|
|
||||||
handleStateCallback(sessionConfiguration.stateCallback)
|
|
||||||
Dog.i(TAG, "handleStateCallback completed", true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "handleStateCallback failed", e, true)
|
|
||||||
}
|
|
||||||
Dog.i(TAG, "About to call checkAndStartVideoPusherFallback", true)
|
|
||||||
try {
|
|
||||||
// Fallback to start VideoPusher if onConfigured doesn't fire
|
|
||||||
checkAndStartVideoPusherFallback(camera, state)
|
|
||||||
Dog.i(TAG, "checkAndStartVideoPusherFallback completed", true)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "checkAndStartVideoPusherFallback failed", e, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Proceed with new session configuration
|
|
||||||
val newArgs = chain.args.toTypedArray()
|
|
||||||
newArgs[0] = newSessionConfig
|
|
||||||
return@intercept chain.proceed(newArgs)
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Error in createCaptureSession(SessionConfiguration) hook", e, true)
|
|
||||||
return@intercept chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun getCameraState(camera: CameraDevice): CameraState {
|
|
||||||
return synchronized(cameraState) {
|
|
||||||
cameraState.getOrPut(camera) { CameraState() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun CameraState.bindSurface(surface: Surface) {
|
|
||||||
val (width, height, _) = NativeBridge.getSurfaceInfo(surface)
|
|
||||||
// Keep the largest surface as the primary reference for VideoPusher resolution
|
|
||||||
if (width * height > this.previewWidth * this.previewHeight) {
|
|
||||||
this.previewWidth = width
|
|
||||||
this.previewHeight = height
|
|
||||||
this.pictureWidth = width
|
|
||||||
this.pictureHeight = height
|
|
||||||
}
|
|
||||||
this.surfaces.add(surface)
|
|
||||||
}
|
|
||||||
|
|
||||||
private var videoPusherStartedRef = WeakHashMap<CameraDevice, Boolean>()
|
|
||||||
|
|
||||||
private fun handleStateCallback(callback: StateCallback) {
|
|
||||||
@Suppress("ConditionAlwaysTrueFalse")
|
|
||||||
if (callback == null) {
|
|
||||||
Dog.w(TAG, "handleStateCallback: callback is null!", true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val clazz = callback.javaClass
|
|
||||||
Dog.i(TAG, "handleStateCallback called for class: ${clazz.simpleName}", true)
|
|
||||||
if (hookedClasses.add(clazz)) {
|
|
||||||
val onConfigured = try {
|
|
||||||
clazz.getDeclaredMethod("onConfigured", CameraCaptureSession::class.java)
|
|
||||||
} catch (e: NoSuchMethodException) {
|
|
||||||
// Try to find inherited method
|
|
||||||
clazz.getMethod("onConfigured", CameraCaptureSession::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
magic.hook(onConfigured).intercept { chain ->
|
|
||||||
try {
|
|
||||||
val session = chain.args[0] as CameraCaptureSession
|
|
||||||
val camera = session.device
|
|
||||||
val state = getCameraState(camera)
|
|
||||||
|
|
||||||
Dog.i(TAG, "onConfigured called - session: $session, camera: $camera, surfaces count: ${state.surfaces.size}", true)
|
|
||||||
|
|
||||||
SourceManager.refreshAndDispatch()
|
|
||||||
|
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
|
||||||
try {
|
|
||||||
val videoId = SourceManager.getVideoId()
|
|
||||||
Dog.i(TAG, "VideoPusher start check - videoId: $videoId, surfaces not empty: ${state.surfaces.isNotEmpty()}", true)
|
|
||||||
if (videoId != -1L && state.surfaces.isNotEmpty()) {
|
|
||||||
Dog.i(TAG, "Starting VideoPusher on ${state.surfaces.size} surfaces", true)
|
|
||||||
// Protect VideoPusher.start() to avoid propagating errors to the framework
|
|
||||||
VideoPusher.start(state.surfaces, state.previewWidth, state.previewHeight, videoId)
|
|
||||||
videoPusherStartedRef[camera] = true
|
|
||||||
} else {
|
|
||||||
Dog.i(TAG, "NOT starting VideoPusher - videoId: $videoId, surfaces empty: ${state.surfaces.isEmpty()}", true)
|
|
||||||
}
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Error while starting VideoPusher", e, true)
|
|
||||||
}
|
|
||||||
}, 100)
|
|
||||||
chain.proceed()
|
|
||||||
} catch (e: Exception) {
|
|
||||||
Dog.e(TAG, "Error in onConfigured hook", e, true)
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
val onConfigureFailed = try {
|
|
||||||
clazz.getDeclaredMethod("onConfigureFailed", CameraCaptureSession::class.java)
|
|
||||||
} catch (e: NoSuchMethodException) {
|
|
||||||
// Try to find inherited method
|
|
||||||
clazz.getMethod("onConfigureFailed", CameraCaptureSession::class.java)
|
|
||||||
}
|
|
||||||
|
|
||||||
magic.hook(onConfigureFailed).intercept { chain ->
|
|
||||||
Dog.e(TAG, "CameraCaptureSession.StateCallback: onConfigureFailed for class: ${clazz.simpleName}", null, true)
|
|
||||||
BlackHoleMapper.clearAll()
|
|
||||||
activeCameraRef = null
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun checkAndStartVideoPusherFallback(camera: CameraDevice, state: CameraState) {
|
|
||||||
@Suppress("ConditionAlwaysTrueFalse")
|
|
||||||
if (camera == null || state == null) {
|
|
||||||
Dog.w(TAG, "FALLBACK: camera or state is null", true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
val alreadyStarted = videoPusherStartedRef[camera] ?: false
|
|
||||||
if (!alreadyStarted) {
|
|
||||||
Dog.i(TAG, "FALLBACK: Checking if we should start VideoPusher for camera: $camera", true)
|
|
||||||
Handler(Looper.getMainLooper()).postDelayed({
|
|
||||||
val videoId = SourceManager.getVideoId()
|
|
||||||
Dog.i(TAG, "FALLBACK: VideoPusher check - videoId: $videoId, surfaces not empty: ${state.surfaces.isNotEmpty()}", true)
|
|
||||||
if (videoId != -1L && state.surfaces.isNotEmpty()) {
|
|
||||||
Dog.i(TAG, "FALLBACK: Starting VideoPusher on ${state.surfaces.size} surfaces (onConfigured didn't fire in time)", true)
|
|
||||||
VideoPusher.start(state.surfaces, state.previewWidth, state.previewHeight, videoId)
|
|
||||||
videoPusherStartedRef[camera] = true
|
|
||||||
} else {
|
|
||||||
Dog.i(TAG, "FALLBACK: Not starting VideoPusher - videoId: $videoId, surfaces empty: ${state.surfaces.isEmpty()}", true)
|
|
||||||
}
|
|
||||||
}, 500) // Reduced from 2000ms to 500ms for faster startup
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookClose() {
|
|
||||||
val close = getMethod("close")
|
|
||||||
magic.hook(close).intercept { chain ->
|
|
||||||
val activeCamera = activeCameraRef?.get() as? CameraDevice
|
|
||||||
val closingCamera = chain.thisObject as CameraDevice
|
|
||||||
|
|
||||||
if (activeCamera != null && closingCamera === activeCamera) {
|
|
||||||
Dog.i(TAG, "camera[${System.identityHashCode(closingCamera)}] close.", true)
|
|
||||||
Handler(Looper.getMainLooper()).post {
|
|
||||||
VideoPusher.stop()
|
|
||||||
}
|
|
||||||
// Don't clear all immediately to avoid switch crash
|
|
||||||
activeCameraRef = null
|
|
||||||
}
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookAddTarget() {
|
|
||||||
val addTarget = getDeclaredMethod("addTarget", Surface::class.java)
|
|
||||||
magic.hook(addTarget).intercept { chain ->
|
|
||||||
val origin = chain.args[0] as Surface
|
|
||||||
val blackHole = BlackHoleMapper.getBlackHole(origin)
|
|
||||||
if (!SourceManager.isReadyForHook() || blackHole == null) {
|
|
||||||
return@intercept chain.proceed()
|
|
||||||
}
|
|
||||||
chain.proceed(arrayOf(blackHole))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun Class<*>.hookRemoveTarget() {
|
|
||||||
val removeTarget = getDeclaredMethod("removeTarget", Surface::class.java)
|
|
||||||
magic.hook(removeTarget).intercept { chain ->
|
|
||||||
val origin = chain.args[0] as Surface
|
|
||||||
val blackHole = BlackHoleMapper.getBlackHole(origin)
|
|
||||||
if (!SourceManager.isReadyForHook() || blackHole == null) {
|
|
||||||
return@intercept chain.proceed()
|
|
||||||
}
|
|
||||||
chain.proceed(arrayOf(blackHole))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
package com.nothing.camera2magic.hook
|
|
||||||
|
|
||||||
import com.nothing.camera2magic.MagicHook
|
|
||||||
import com.nothing.camera2magic.utils.Dog
|
|
||||||
import io.github.libxposed.api.XposedModuleInterface.PackageReadyParam
|
|
||||||
|
|
||||||
object WebRTCHooker {
|
|
||||||
private const val TAG = "[WebRTC]"
|
|
||||||
private val ROTATION_REGEX = Regex("""(\d+)x(\d+).*rotation\s+(\d+)""")
|
|
||||||
private lateinit var magic: MagicHook
|
|
||||||
private var manualRotation = 0
|
|
||||||
fun initHooks(module: MagicHook, param: PackageReadyParam) {
|
|
||||||
magic = module
|
|
||||||
val classLoader = param.classLoader
|
|
||||||
|
|
||||||
classLoader.loadClass("org.webrtc.Logging").apply {
|
|
||||||
val nativeLog = getDeclaredMethod("nativeLog",
|
|
||||||
Int::class.java, String::class.java, String::class.java)
|
|
||||||
magic.hook(nativeLog).intercept { chain ->
|
|
||||||
val tag = chain.args[1] as String
|
|
||||||
val msg = chain.args[2] as String
|
|
||||||
if (msg.contains("rotation", ignoreCase = true)) {
|
|
||||||
handleMessage(msg)
|
|
||||||
}
|
|
||||||
if (tag == "Camera2Session" && msg.contains("Stop Camera2 session", ignoreCase = true)) {
|
|
||||||
manualRotation = 0
|
|
||||||
NativeBridge.updateManualRotation(manualRotation)
|
|
||||||
NativeBridge.needStopRenderer()
|
|
||||||
NativeBridge.releaseLastRegisteredSurface()
|
|
||||||
}
|
|
||||||
|
|
||||||
chain.proceed()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private fun handleMessage(msg: String) {
|
|
||||||
val matchResult = ROTATION_REGEX.find(msg)
|
|
||||||
matchResult?.let {
|
|
||||||
val (_, _, r) = it.destructured
|
|
||||||
val rotation = r.toInt()
|
|
||||||
if (manualRotation != rotation) {
|
|
||||||
Dog.i(TAG, "WebRTC set rotation: $rotation", SourceManager.enableLog)
|
|
||||||
manualRotation = 90
|
|
||||||
NativeBridge.updateManualRotation(manualRotation)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
com.nothing.camera2magic.MagicHook
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
id=com.nothing.camera2magic
|
|
||||||
minApiVersion=101
|
|
||||||
targetApiVersion=101
|
|
||||||
staticScope=false
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
camera3
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
tv.danmaku.bili
|
|
||||||
@@ -0,0 +1,313 @@
|
|||||||
|
# CamSwapper Status - Android 16 (Pixel 9a / tegu)
|
||||||
|
|
||||||
|
**Date**: 2026-05-08
|
||||||
|
**Device**: Pixel 9a (tegu)
|
||||||
|
**Android Version**: 16 (API 36)
|
||||||
|
**Root Solution**: KernelSU with ReZygisk
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
**Goal**: Replace physical camera frames with virtual video/RTSP stream at the system level (HAL layer).
|
||||||
|
|
||||||
|
**Current Status**: ⚠️ **PARTIALLY WORKING** - Hook library built and Zygisk module loads, but injection into camera processes **FAILS** because cameraserver and camera provider are **init services**, not zygote-forked processes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Process Architecture (Pixel 9a)
|
||||||
|
|
||||||
|
### Camera-Related Processes
|
||||||
|
|
||||||
|
| Process | PID | PPID | Type | Can Zygisk Hook? |
|
||||||
|
|---------|-----|------|------|-------------------|
|
||||||
|
| `cameraserver` | ~12018 | 1 (init) | **init service** | ❌ NO |
|
||||||
|
| `camera.provider` | ~12019 | 1 (init) | **init service** | ❌ NO |
|
||||||
|
| `virtual_camera` | ~12361 | 1 (init) | **init service** | ❌ NO |
|
||||||
|
| `zygote64` | 958 | 1 (init) | init service | ✅ YES (but irrelevant) |
|
||||||
|
| `system_server` | varies | zygote64 | zygote-forked | ✅ YES |
|
||||||
|
|
||||||
|
### Key Discovery
|
||||||
|
|
||||||
|
From `zygisk.hpp` line 30-45:
|
||||||
|
> "Please note that modules will only be loaded after zygote has forked the child process."
|
||||||
|
|
||||||
|
**This means Zygisk CANNOT hook init services.** The cameraserver and camera provider are started by init (PPID=1), not forked from zygote.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Works
|
||||||
|
|
||||||
|
### 1. HAL Hook Library (`libcamera_hook.so`)
|
||||||
|
- **Built**: 336KB (stripped), ARM64 aarch64
|
||||||
|
- **Location**: `/data/local/camera_magic/libcamera_hook.so`
|
||||||
|
- **Functionality**:
|
||||||
|
- ✅ Intercepts `dlopen`/`dlsym` to hook Camera HAL symbols
|
||||||
|
- ✅ Implements `CameraProviderHook`, `CameraDeviceHook`, `CameraDeviceSessionHook`
|
||||||
|
- ✅ `inject_video_frame()` implemented (reads from shared memory ring buffer)
|
||||||
|
- ✅ Video decoder (MediaCodec NDK) → YUV420 → shared memory
|
||||||
|
- ✅ RTSP client → RTP receive → shared memory
|
||||||
|
- ✅ Buffer converter (YUV420↔NV21 with NEON)
|
||||||
|
|
||||||
|
### 2. Zygisk Module (`camswapper-zygisk-v2.zip`)
|
||||||
|
- **Properly implemented** using Zygisk API v4
|
||||||
|
- **File**: `/data/adb/modules/camera-hook-zygisk/zygisk/arm64-v8a.so`
|
||||||
|
- **Status**: Loaded in ReZygisk (confirmed in `/data/adb/rezygisk/state.json`)
|
||||||
|
- **Symbols**: `zygisk_module_entry` exported correctly
|
||||||
|
- **Code**: Uses `zygisk::ModuleBase` with `REGISTER_ZYGISK_MODULE(CamSwapperModule)`
|
||||||
|
|
||||||
|
### 3. KernelSU Module (`camswapper-kernelsu-v2.zip`)
|
||||||
|
- **Contains**: `libcamera_hook.so`, `service.sh`, `post-fs-data.sh`
|
||||||
|
- **Injection method**: ptrace dlopen (BROKEN - see below)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What Does NOT Work
|
||||||
|
|
||||||
|
### 1. ❌ Zygisk Injection Fails for Camera Processes
|
||||||
|
|
||||||
|
**Root Cause**: cameraserver and camera.provider are **init services** (PPID=1), not zygote-forked.
|
||||||
|
|
||||||
|
**Evidence**:
|
||||||
|
```bash
|
||||||
|
$ ps -A -o pid,ppid,cmd | grep camera
|
||||||
|
12018 1 cameraserver
|
||||||
|
12019 1 camera.provider
|
||||||
|
12361 1 virtual_camera
|
||||||
|
```
|
||||||
|
|
||||||
|
**Zygisk module loaded in zygote64**:
|
||||||
|
```bash
|
||||||
|
$ cat /proc/zygote64/maps | grep camera-hook
|
||||||
|
70fa88f000-70fa8c2000 r-xp ... /data/adb/modules/camera-hook-zygisk/zygisk/arm64-v8a.so
|
||||||
|
```
|
||||||
|
|
||||||
|
**But NOT in cameraserver**:
|
||||||
|
```bash
|
||||||
|
$ cat /proc/cameraserver/maps | grep camera-hook
|
||||||
|
(empty - not loaded)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Conclusion**: Zygisk `postServerSpecialize()` only runs for `system_server`, not cameraserver. The `is_camera_process()` check in `zygisk_entry.cpp` never triggers because cameraserver isn't zygote-forked.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. ❌ Ptrace dlopen Injection Crashes (SIGSEGV)
|
||||||
|
|
||||||
|
**Method**: Attach to cameraserver via ptrace, call `__loader_dlopen` in linker64 to load `libcamera_hook.so`.
|
||||||
|
|
||||||
|
**Result**: Crashes with SIGSEGV when loading `libcamera_hook.so` (works for `libc.so`).
|
||||||
|
|
||||||
|
**Root Cause**: Linker namespace restrictions on Android 16. The `__loader_dlopen` function requires proper namespace setup that ptrace can't replicate.
|
||||||
|
|
||||||
|
**Evidence** (from previous testing):
|
||||||
|
- dlopen for `libc.so` → returns valid handle (0x2443dbca3ce35c9b)
|
||||||
|
- dlopen for `libcamera_hook.so` → returns NULL or crashes
|
||||||
|
- APEX linker namespace blocks `/data/local/tmp` paths
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. ⚠️ Virtual Camera Service (Potential Solution)
|
||||||
|
|
||||||
|
**Service**: `virtual_camera` (PID ~12361)
|
||||||
|
**Interface**: `android.hardware.camera.provider.ICameraProvider/virtual/0`
|
||||||
|
**Status**: Running but provides **0 camera devices**
|
||||||
|
|
||||||
|
**Evidence**:
|
||||||
|
```bash
|
||||||
|
$ dumpsys android.hardware.camera.provider.ICameraProvider/virtual/0
|
||||||
|
== Camera Provider HAL ... virtual/0-1 (v2.0, remote) static info: 0 devices: ==
|
||||||
|
```
|
||||||
|
|
||||||
|
**Service Definition** (`/system/etc/init/virtual_camera.hal.rc`):
|
||||||
|
```rc
|
||||||
|
service virtual_camera /system/bin/virtual_camera
|
||||||
|
class core
|
||||||
|
user system
|
||||||
|
group system
|
||||||
|
interface aidl virtual_camera
|
||||||
|
interface aidl android.hardware.camera.provider.ICameraProvider/virtual/0
|
||||||
|
oneshot
|
||||||
|
disabled
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key Finding**: The service is "disabled" in RC file but is somehow running. It provides 0 devices, meaning it's not configured with a virtual camera stream.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Attempted Solutions (All Failed)
|
||||||
|
|
||||||
|
### 1. `wrap.cameraserver` Property
|
||||||
|
- **Method**: Set `wrap.cameraserver=/system/bin/cameraserver_wrapper`
|
||||||
|
- **Result**: ❌ Silently ignored on production build (ro.debuggable=0)
|
||||||
|
|
||||||
|
### 2. Bind-Mount Wrapper
|
||||||
|
- **Method**: Bind-mount wrapper binary over `/system/bin/cameraserver`
|
||||||
|
- **Result**: ❌ cameraserver crashes (SELinux context or APEX library resolution)
|
||||||
|
|
||||||
|
### 3. Ptrace dlopen Injection
|
||||||
|
- **Result**: ❌ SIGSEGV (linker namespace issue)
|
||||||
|
|
||||||
|
### 4. Zygisk Module (API v4)
|
||||||
|
- **Result**: ❌ Loads in zygote but can't hook init services
|
||||||
|
|
||||||
|
### 5. Original `zygisk_entry.cpp` (Wrong API)
|
||||||
|
- **Problem**: Exported raw C symbols (`zygisk_module_entry`, `pre_app_specialize`, etc.)
|
||||||
|
- **Fix**: Rewrote to use proper `zygisk::ModuleBase` class
|
||||||
|
- **Result**: Still can't hook init services (fundamental limitation)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Path Forward (Recommended Approaches)
|
||||||
|
|
||||||
|
### Option A: Use `virtual_camera` Service (BEST OPTION)
|
||||||
|
|
||||||
|
**Concept**: Instead of hooking existing camera provider, configure Android's built-in `virtual_camera` service to provide virtual camera devices.
|
||||||
|
|
||||||
|
**Steps**:
|
||||||
|
1. Determine how to configure `virtual_camera` to provide a camera device
|
||||||
|
2. Point it to our video/RTSP stream
|
||||||
|
3. No injection needed - uses Android's native virtual camera support
|
||||||
|
|
||||||
|
**Research Needed**:
|
||||||
|
- Read AOSP source for `virtual_camera` service
|
||||||
|
- Find configuration file or binder interface to add virtual camera streams
|
||||||
|
- Check `IVirtualCameraService` interface (service list shows it exists)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option B: Hook at Binder IPC Layer
|
||||||
|
|
||||||
|
**Concept**: Intercept camera HAL binder calls instead of hooking the process.
|
||||||
|
|
||||||
|
**Method**:
|
||||||
|
1. Use binder hook (via Zygisk or KernelSU)
|
||||||
|
2. Intercept `ICameraDeviceSession::processCaptureRequest`
|
||||||
|
3. Replace buffer contents before they reach the camera HAL
|
||||||
|
|
||||||
|
**Advantage**: Works regardless of which process handles the camera
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option C: Modify init.rc (Require Reboot + Possible Bootloop)
|
||||||
|
|
||||||
|
**Concept**: Add `setenv LD_PRELOAD=/path/to/libcamera_hook.so` to cameraserver service definition.
|
||||||
|
|
||||||
|
**Risk**: High - modifying init.rc can cause bootloops on production builds
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Option D: Use `virtualizationservice` (Discovered Running)
|
||||||
|
|
||||||
|
**Concept**: Pixel 9a runs `virtualizationservice` (PID 6360) which spawns `virtmgr_virtualizationservice` and `virtual_camera`.
|
||||||
|
|
||||||
|
**Possibility**: The virtual camera infrastructure is already running - we just need to configure it properly.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Inventory
|
||||||
|
|
||||||
|
### Built Files
|
||||||
|
| File | Size | Purpose |
|
||||||
|
|------|------|---------|
|
||||||
|
| `native/build/libcamera_hook.so` | 336KB | HAL hook library (stripped) |
|
||||||
|
| `zygisk-module/zygisk/arm64-v8a.so` | 223KB | Zygisk module (proper API v4) |
|
||||||
|
| `camswapper-kernelsu-v2.zip` | 1.9MB | KernelSU module zip |
|
||||||
|
| `camswapper-zygisk-v2.zip` | 79KB | Zygisk module zip |
|
||||||
|
|
||||||
|
### Source Files
|
||||||
|
| File | Lines | Purpose |
|
||||||
|
|------|-------|---------|
|
||||||
|
| `native/src/camera_wrapper.cpp` | 386 | HAL hook with `inject_video_frame()` |
|
||||||
|
| `native/src/video_decoder.cpp` | 401 | MediaCodec decoder → shared memory |
|
||||||
|
| `native/src/rtsp_client.cpp` | 653 | RTSP client → RTP receive |
|
||||||
|
| `native/src/buffer_converter.cpp` | 173 | YUV420↔NV21 conversion |
|
||||||
|
| `zygisk-module/zygisk_entry.cpp` | 88 | Zygisk module (API v4) |
|
||||||
|
| `zygisk-module/jni/zygisk.hpp` | 391 | Zygisk API header |
|
||||||
|
|
||||||
|
### Headers
|
||||||
|
| File | Purpose |
|
||||||
|
|------|---------|
|
||||||
|
| `native/include/video_decoder.h` | Ring buffer shared memory definitions |
|
||||||
|
| `native/include/rtsp_client.h` | RTSP client state structures |
|
||||||
|
| `native/include/camera_hal/*.h` | AIDL camera HAL interface definitions |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Key Learnings for Future AI
|
||||||
|
|
||||||
|
### 1. Always Check Process Parent (PPID)
|
||||||
|
Before attempting Zygisk injection, run:
|
||||||
|
```bash
|
||||||
|
adb shell "ps -A -o pid,ppid,cmd | grep <target_process>"
|
||||||
|
```
|
||||||
|
If PPID=1 (init), Zygisk **cannot** hook it.
|
||||||
|
|
||||||
|
### 2. Zygisk API v4 Correct Usage
|
||||||
|
```cpp
|
||||||
|
#include "zygisk.hpp"
|
||||||
|
class MyModule : public zygisk::ModuleBase {
|
||||||
|
void onLoad(Api *api, JNIEnv *env) override { ... }
|
||||||
|
void postServerSpecialize(const ServerSpecializeArgs *args) override { ... }
|
||||||
|
};
|
||||||
|
REGISTER_ZYGISK_MODULE(MyModule)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. ReZygisk State File
|
||||||
|
Check module loading status:
|
||||||
|
```bash
|
||||||
|
adb shell "su -c 'cat /data/adb/rezygisk/state.json'"
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. Virtual Camera Service Exists
|
||||||
|
On Android 16 (Pixel 9a), `virtual_camera` service is available but disabled by default. It provides 0 devices until configured.
|
||||||
|
|
||||||
|
### 5. Ptrace dlopen Broken on Android 16
|
||||||
|
Linker namespace restrictions prevent ptrace-based dlopen for anything other than system libraries.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Next Session Action Plan
|
||||||
|
|
||||||
|
1. **Research `virtual_camera` configuration**:
|
||||||
|
- Search AOSP source for `virtual_camera` implementation
|
||||||
|
- Find how to add virtual camera streams
|
||||||
|
- Check `IVirtualCameraService` binder interface
|
||||||
|
|
||||||
|
2. **Test Option A first** (most promising):
|
||||||
|
- Enable virtual camera with custom stream
|
||||||
|
- Verify it appears as a camera device
|
||||||
|
- Point it to video/RTSP source
|
||||||
|
|
||||||
|
3. **If Option A fails**, try Option B (binder hook)
|
||||||
|
|
||||||
|
4. **Document findings** in this file for future reference
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Quick Reference Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Check camera processes
|
||||||
|
adb shell "ps -Z | grep -E 'cameraserver|camera.provider|virtual_camera'"
|
||||||
|
|
||||||
|
# Check if Zygisk module loaded in process
|
||||||
|
adb shell "su -c 'cat /proc/\$(pidof zygote64)/maps | grep camera-hook'"
|
||||||
|
|
||||||
|
# Check virtual camera status
|
||||||
|
adb shell "dumpsys android.hardware.camera.provider.ICameraProvider/virtual/0"
|
||||||
|
|
||||||
|
# Check binder services
|
||||||
|
adb shell "service list | grep -i camera"
|
||||||
|
|
||||||
|
# View ReZygisk state
|
||||||
|
adb shell "su -c 'cat /data/adb/rezygisk/state.json'"
|
||||||
|
|
||||||
|
# Check module.prop description (updated by service.sh)
|
||||||
|
adb shell "su -c 'cat /data/adb/modules/camera-hook-zygisk/module.prop'"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated**: 2026-05-08 by Atlas (OhMyOpenCode)
|
||||||
|
**Session ID**: Compressed conversation (b3, b4, b5, b6, b7, b12, b13, b14, b15, b16, b17)
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
cmake_minimum_required(VERSION 3.22.1)
|
cmake_minimum_required(VERSION 3.25)
|
||||||
project(camera_hook LANGUAGES CXX)
|
project(camera_hook LANGUAGES CXX C)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
@@ -51,7 +51,7 @@ set_target_properties(camera_hook PROPERTIES
|
|||||||
|
|
||||||
# Camera provider wrapper (static C binary, bind-mounted over original)
|
# Camera provider wrapper (static C binary, bind-mounted over original)
|
||||||
add_executable(camera_provider_wrapper src/camera_provider_wrapper.c)
|
add_executable(camera_provider_wrapper src/camera_provider_wrapper.c)
|
||||||
target_link_options(camera_provider_wrapper PRIVATE -static)
|
|
||||||
set_target_properties(camera_provider_wrapper PROPERTIES
|
set_target_properties(camera_provider_wrapper PROPERTIES
|
||||||
|
LINKER_LANGUAGE C
|
||||||
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../root-module"
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_SOURCE_DIR}/../root-module"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -54,6 +54,10 @@ int get_rtsp_shmem_fd();
|
|||||||
int get_rtsp_width();
|
int get_rtsp_width();
|
||||||
int get_rtsp_height();
|
int get_rtsp_height();
|
||||||
|
|
||||||
|
// Get the shared memory base pointer (for reading frames)
|
||||||
|
// Returns nullptr if not initialized
|
||||||
|
uint8_t* get_rtsp_shmem_base();
|
||||||
|
|
||||||
} // namespace rtsp_client
|
} // namespace rtsp_client
|
||||||
|
|
||||||
#endif // RTSP_CLIENT_H
|
#endif // RTSP_CLIENT_H
|
||||||
|
|||||||
@@ -87,6 +87,10 @@ int get_decoder_width();
|
|||||||
int get_decoder_height();
|
int get_decoder_height();
|
||||||
bool is_decoder_running();
|
bool is_decoder_running();
|
||||||
|
|
||||||
|
// Get the shared memory base pointer (for reading frames)
|
||||||
|
// Returns nullptr if not initialized
|
||||||
|
uint8_t* get_shmem_base();
|
||||||
|
|
||||||
// Write a YUV420 frame to the shared memory ring buffer
|
// Write a YUV420 frame to the shared memory ring buffer
|
||||||
// Called internally by the decoder thread
|
// Called internally by the decoder thread
|
||||||
// y_data, u_data, v_data: pointers to Y, U, V planes
|
// y_data, u_data, v_data: pointers to Y, U, V planes
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
|
#include <android/hardware_buffer.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
#include <sys/mman.h>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <map>
|
#include <map>
|
||||||
@@ -11,10 +13,20 @@
|
|||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <fstream>
|
#include <fstream>
|
||||||
|
|
||||||
|
// Define native_handle_t (not publicly available in NDK)
|
||||||
|
struct native_handle_t {
|
||||||
|
int version;
|
||||||
|
int numFds;
|
||||||
|
int numInts;
|
||||||
|
int data[0];
|
||||||
|
};
|
||||||
|
|
||||||
#include "camera_hal/types.h"
|
#include "camera_hal/types.h"
|
||||||
#include "camera_hal/ICameraProvider.h"
|
#include "camera_hal/ICameraProvider.h"
|
||||||
#include "camera_hal/ICameraDevice.h"
|
#include "camera_hal/ICameraDevice.h"
|
||||||
#include "camera_hal/ICameraDeviceSession.h"
|
#include "camera_hal/ICameraDeviceSession.h"
|
||||||
|
#include "video_decoder.h"
|
||||||
|
#include "rtsp_client.h"
|
||||||
|
|
||||||
#define LOG_TAG "CameraHook"
|
#define LOG_TAG "CameraHook"
|
||||||
#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
#define ALOGD(...) __android_log_print(ANDROID_LOG_DEBUG, LOG_TAG, __VA_ARGS__)
|
||||||
@@ -105,15 +117,55 @@ static bool check_virtual_camera_enabled() {
|
|||||||
|
|
||||||
static bool inject_video_frame(int32_t stream_id, void* buffer_handle,
|
static bool inject_video_frame(int32_t stream_id, void* buffer_handle,
|
||||||
int32_t width, int32_t height) {
|
int32_t width, int32_t height) {
|
||||||
// Task 7/9 will implement: read from shared memory ring buffer,
|
|
||||||
// lock AHardwareBuffer, copy YUV data, unlock.
|
|
||||||
// Returns true if frame was injected, false if no frame available.
|
|
||||||
(void)stream_id;
|
(void)stream_id;
|
||||||
(void)buffer_handle;
|
|
||||||
(void)width;
|
uint8_t* shmem_base = video_decoder::get_shmem_base();
|
||||||
(void)height;
|
if (!shmem_base) {
|
||||||
|
shmem_base = rtsp_client::get_rtsp_shmem_base();
|
||||||
|
if (!shmem_base) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
video_decoder::RingBufferHeader* header = (video_decoder::RingBufferHeader*)shmem_base;
|
||||||
|
if (header->magic != video_decoder::RING_MAGIC || header->version != video_decoder::RING_VERSION) {
|
||||||
|
ALOGE("Invalid ring buffer magic/version");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!(header->flags & video_decoder::FLAG_DECODER_RUNNING)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t frame_size = header->frame_width * header->frame_height * 3 / 2;
|
||||||
|
uint32_t read_idx = header->write_index;
|
||||||
|
if (read_idx >= header->frame_count) {
|
||||||
|
ALOGE("Invalid write_index: %u", read_idx);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint8_t* frame_data = shmem_base + video_decoder::RING_HEADER_SIZE + (read_idx * frame_size);
|
||||||
|
|
||||||
|
native_handle_t* handle = (native_handle_t*)buffer_handle;
|
||||||
|
if (!handle || handle->numFds < 1) {
|
||||||
|
ALOGE("Invalid native handle");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fd = handle->data[0];
|
||||||
|
size_t buf_size = (size_t)width * height * 3 / 2;
|
||||||
|
void* addr = mmap(nullptr, buf_size, PROT_WRITE, MAP_SHARED, fd, 0);
|
||||||
|
if (addr == MAP_FAILED) {
|
||||||
|
ALOGE("mmap failed: %s", strerror(errno));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
memcpy(addr, frame_data, frame_size);
|
||||||
|
munmap(addr, buf_size);
|
||||||
|
|
||||||
|
ALOGD("Injected frame %u (stream %d, %dx%d)", header->sequence, stream_id, width, height);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static void update_fps_counter() {
|
static void update_fps_counter() {
|
||||||
auto now = std::chrono::steady_clock::now();
|
auto now = std::chrono::steady_clock::now();
|
||||||
|
|||||||
@@ -644,6 +644,10 @@ int rtsp_client::get_rtsp_shmem_fd() {
|
|||||||
return g_rtsp.shmem_fd;
|
return g_rtsp.shmem_fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t* rtsp_client::get_rtsp_shmem_base() {
|
||||||
|
return g_rtsp.shmem_base;
|
||||||
|
}
|
||||||
|
|
||||||
int rtsp_client::get_rtsp_width() {
|
int rtsp_client::get_rtsp_width() {
|
||||||
return g_rtsp.width;
|
return g_rtsp.width;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -387,6 +387,10 @@ int video_decoder::get_shmem_fd() {
|
|||||||
return g_decoder.shmem_fd;
|
return g_decoder.shmem_fd;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
uint8_t* video_decoder::get_shmem_base() {
|
||||||
|
return g_decoder.shmem_base;
|
||||||
|
}
|
||||||
|
|
||||||
int video_decoder::get_decoder_width() {
|
int video_decoder::get_decoder_width() {
|
||||||
return g_decoder.width;
|
return g_decoder.width;
|
||||||
}
|
}
|
||||||
|
|||||||
Executable
BIN
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,33 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
# CamSwapper VDM Privilege Module - Installation customization
|
||||||
|
# Sourced by KernelSU/Magisk installer after files are extracted.
|
||||||
|
|
||||||
|
SKIPUNZIP=0
|
||||||
|
|
||||||
|
# Set permissions
|
||||||
|
set_perm_recursive "$MODPATH/system" 0 0 0755 0644
|
||||||
|
set_perm "$MODPATH/system/priv-app/CamSwapper/CamSwapper.apk" 0 0 0644
|
||||||
|
set_perm "$MODPATH/system/etc/permissions/privapp-permissions-camswapper.xml" 0 0 0644
|
||||||
|
set_perm "$MODPATH/post-fs-data.sh" 0 0 0755
|
||||||
|
set_perm "$MODPATH/service.sh" 0 0 0755
|
||||||
|
set_perm "$MODPATH/uninstall.sh" 0 0 0755
|
||||||
|
|
||||||
|
# Remove user-installed version to avoid package conflict
|
||||||
|
if command -v pm >/dev/null 2>&1; then
|
||||||
|
pm uninstall com.nothing.camera2magic 2>/dev/null && \
|
||||||
|
ui_print "- Removed user-installed CamSwapper"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ui_print ""
|
||||||
|
ui_print " CamSwapper VDM Privilege Module"
|
||||||
|
ui_print " ──────────────────────────────"
|
||||||
|
ui_print " Installed CamSwapper as privileged system app"
|
||||||
|
ui_print " Granted: REQUEST_COMPANION_PROFILE_APP_STREAMING"
|
||||||
|
ui_print " Granted: REQUEST_COMPANION_SELF_MANAGED"
|
||||||
|
ui_print " Granted: CREATE_VIRTUAL_DEVICE"
|
||||||
|
ui_print ""
|
||||||
|
ui_print " After reboot:"
|
||||||
|
ui_print " 1. Open CamSwapper (it's now a system app)"
|
||||||
|
ui_print " 2. Grant CAMERA + POST_NOTIFICATIONS permissions"
|
||||||
|
ui_print " 3. Start virtual camera"
|
||||||
|
ui_print ""
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
id=camswapper-vdm
|
||||||
|
name=CamSwapper VDM Privilege Module
|
||||||
|
version=3.2
|
||||||
|
versionCode=8
|
||||||
|
author=CamSwapper
|
||||||
|
description=Grants CamSwapper privileged system permissions via priv-app overlay, enabling VirtualDeviceManager (VDM) API access for system-level virtual camera creation. Compatible with KernelSU.
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
# Set up overlay mount for CamSwapper priv-app
|
||||||
|
|
||||||
|
MODDIR=/data/adb/modules/camswapper-vdm
|
||||||
|
|
||||||
|
# Mount tmpfs for our overlay upperdir
|
||||||
|
mount -t tmpfs tmpfs /mnt/camswapper_overlay
|
||||||
|
|
||||||
|
# Create directory structure
|
||||||
|
mkdir -p /mnt/camswapper_overlay/upper/system/priv-app/CamSwapper
|
||||||
|
mkdir -p /mnt/camswapper_overlay/work
|
||||||
|
|
||||||
|
# Copy APK from module (it's already there from module packaging)
|
||||||
|
# No, we need to copy it - but the module file is readable by root
|
||||||
|
cp "$MODDIR/system/priv-app/CamSwapper/CamSwapper.apk" /mnt/camswapper_overlay/upper/system/priv-app/CamSwapper/
|
||||||
|
|
||||||
|
# Set proper permissions and SELinux context
|
||||||
|
chmod 755 /mnt/camswapper_overlay/upper/system/priv-app/CamSwapper
|
||||||
|
chmod 644 /mnt/camswapper_overlay/upper/system/priv-app/CamSwapper/CamSwapper.apk
|
||||||
|
chcon -R u:object_r:system_file:s0 /mnt/camswapper_overlay/upper/system/priv-app
|
||||||
|
|
||||||
|
# Mount overlay on /system/priv-app
|
||||||
|
mount -t overlay overlay \
|
||||||
|
-o lowerdir=/system/priv-app,upperdir=/mnt/camswapper_overlay/upper/system/priv-app,workdir=/mnt/camswapper_overlay/work \
|
||||||
|
/system/priv-app
|
||||||
|
|
||||||
|
echo "Overlay mount result: $?"
|
||||||
|
ls /system/priv-app/CamSwapper/ 2>&1
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
MODDIR="${0%/*}"
|
||||||
|
|
||||||
|
OVERLAY_UPPER=/mnt/.cswp_overlay
|
||||||
|
OVERLAY_WORK=/mnt/.cswp_work
|
||||||
|
OVERLAY_WORK_ETC=/mnt/.cswp_work_etc
|
||||||
|
|
||||||
|
# Clean any stale workdir state from previous mounts. OverlayFS workdirs MUST be
|
||||||
|
# pristine — if left dirty after a lazy umount (umount -l), the kernel gets confused.
|
||||||
|
rm -rf "$OVERLAY_WORK" "$OVERLAY_WORK_ETC"
|
||||||
|
mkdir -p "$OVERLAY_WORK" "$OVERLAY_WORK_ETC"
|
||||||
|
mkdir -p "$OVERLAY_UPPER/system/priv-app/CamSwapper"
|
||||||
|
mkdir -p "$OVERLAY_UPPER/system/etc/permissions"
|
||||||
|
|
||||||
|
# Copy APK and privapp permissions XML to overlay upper dir.
|
||||||
|
# Android 16 requires privileged permission allowlisting — without the privapp XML,
|
||||||
|
# system_server FATAL EXCEPTION crashes on boot.
|
||||||
|
cp "$MODDIR/system/priv-app/CamSwapper/CamSwapper.apk" \
|
||||||
|
"$OVERLAY_UPPER/system/priv-app/CamSwapper/"
|
||||||
|
cp "$MODDIR/system/etc/permissions/privapp-permissions-camswapper.xml" \
|
||||||
|
"$OVERLAY_UPPER/system/etc/permissions/"
|
||||||
|
|
||||||
|
# Set permissions and SELinux context
|
||||||
|
chmod 755 "$OVERLAY_UPPER/system/priv-app/CamSwapper"
|
||||||
|
chmod 644 "$OVERLAY_UPPER/system/priv-app/CamSwapper/CamSwapper.apk"
|
||||||
|
chmod 644 "$OVERLAY_UPPER/system/etc/permissions/privapp-permissions-camswapper.xml"
|
||||||
|
chcon -R u:object_r:system_file:s0 "$OVERLAY_UPPER/system"
|
||||||
|
|
||||||
|
# Mount overlay on /system/priv-app so PackageManager sees CamSwapper as priv-app
|
||||||
|
mount -t overlay overlay \
|
||||||
|
-o lowerdir=/system/priv-app,upperdir="$OVERLAY_UPPER/system/priv-app",workdir="$OVERLAY_WORK" \
|
||||||
|
/system/priv-app
|
||||||
|
|
||||||
|
# Mount overlay on /system/etc/permissions with its own workdir so SystemConfig
|
||||||
|
# finds the privapp permissions XML during early boot.
|
||||||
|
mkdir -p "$OVERLAY_WORK_ETC"
|
||||||
|
mount -t overlay overlay \
|
||||||
|
-o lowerdir=/system/etc/permissions,upperdir="$OVERLAY_UPPER/system/etc/permissions",workdir="$OVERLAY_WORK_ETC" \
|
||||||
|
/system/etc/permissions
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Allow privileged app to use VirtualDeviceManager Binder service
|
||||||
|
allow priv_app virtualdevice_service:service_manager find;
|
||||||
|
allow priv_app virtualdevice_service:dir { search };
|
||||||
|
allow priv_app virtualdevice_service:file { read open };
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
MODDIR="${0%/*}"
|
||||||
|
|
||||||
|
# Wait for boot to complete
|
||||||
|
while [ "$(getprop sys.boot_completed)" != 1 ]; do
|
||||||
|
/system/bin/sleep 1
|
||||||
|
done
|
||||||
|
|
||||||
|
# Clean up user-installed version if it's still around
|
||||||
|
INSTALL_PATH=$(pm path com.nothing.camera2magic 2>/dev/null | grep package: | head -1)
|
||||||
|
if echo "$INSTALL_PATH" | grep -q "/data/app/"; then
|
||||||
|
pm uninstall com.nothing.camera2magic 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- Diagnostics ---
|
||||||
|
LOG_TAG="CamSwapperModule"
|
||||||
|
log -t "$LOG_TAG" "=== CamSwapper VDM module service.sh ==="
|
||||||
|
|
||||||
|
# Verify overlay visibility
|
||||||
|
for f in \
|
||||||
|
/system/priv-app/CamSwapper/CamSwapper.apk \
|
||||||
|
/system/etc/permissions/privapp-permissions-camswapper.xml; do
|
||||||
|
if [ -f "$f" ]; then
|
||||||
|
log -t "$LOG_TAG" "OK: $f exists"
|
||||||
|
ls -la "$f" 2>&1 | log -t "$LOG_TAG"
|
||||||
|
else
|
||||||
|
log -t "$LOG_TAG" "MISSING: $f NOT FOUND"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
|
||||||
|
# Show mounts under /system
|
||||||
|
mount | grep " /system" | log -t "$LOG_TAG"
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# CamSwapper VDM Module - system properties (optional)
|
||||||
|
# VirtualDeviceManager is enabled by default on Android 14+
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<permissions>
|
||||||
|
<!-- Grant privileged permissions to CamSwapper for VDM API access.
|
||||||
|
These permissions are signature|privileged level, requiring the app
|
||||||
|
to be installed as a privileged system app (in /system/priv-app/). -->
|
||||||
|
<privapp-permissions package="com.nothing.camera2magic">
|
||||||
|
<permission name="android.permission.REQUEST_COMPANION_PROFILE_APP_STREAMING"/>
|
||||||
|
<permission name="android.permission.REQUEST_COMPANION_SELF_MANAGED"/>
|
||||||
|
<permission name="android.permission.CREATE_VIRTUAL_DEVICE"/>
|
||||||
|
</privapp-permissions>
|
||||||
|
</permissions>
|
||||||
Binary file not shown.
@@ -0,0 +1,8 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
# CamSwapper VDM Privilege Module - Uninstall script
|
||||||
|
# Removes the privileged system app entry from package manager.
|
||||||
|
|
||||||
|
# Remove the system app entry from package manager
|
||||||
|
if command -v pm >/dev/null 2>&1; then
|
||||||
|
pm uninstall com.nothing.camera2magic 2>/dev/null
|
||||||
|
fi
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
LOCAL_PATH := $(call my-dir)
|
||||||
|
|
||||||
|
include $(CLEAR_VARS)
|
||||||
|
LOCAL_MODULE := camswapper
|
||||||
|
LOCAL_SRC_FILES := CamSwapperEntry.cpp
|
||||||
|
LOCAL_LDLIBS := -llog -ldl
|
||||||
|
include $(BUILD_SHARED_LIBRARY)
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
APP_ABI := arm64-v8a
|
||||||
|
APP_CPPFLAGS := -std=c++17 -fno-exceptions -fno-rtti -fvisibility=hidden -fvisibility-inlines-hidden
|
||||||
|
APP_STL := c++_static
|
||||||
|
APP_PLATFORM := android-21
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
#include <dlfcn.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <android/log.h>
|
||||||
|
#include "zygisk.hpp"
|
||||||
|
|
||||||
|
#define LOG_TAG "CamSwapperZygisk"
|
||||||
|
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||||
|
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
||||||
|
#define HOOK_LIB_PATH "/data/local/camera_magic/libcamera_hook.so"
|
||||||
|
|
||||||
|
using zygisk::Api;
|
||||||
|
using zygisk::AppSpecializeArgs;
|
||||||
|
using zygisk::ServerSpecializeArgs;
|
||||||
|
|
||||||
|
static bool is_camera_process(const char *process_name) {
|
||||||
|
if (!process_name) return false;
|
||||||
|
return (strstr(process_name, "cameraserver") != nullptr) ||
|
||||||
|
(strstr(process_name, "camera.provider") != nullptr) ||
|
||||||
|
(strstr(process_name, "android.hardware.camera") != nullptr);
|
||||||
|
}
|
||||||
|
|
||||||
|
class CamSwapperModule : public zygisk::ModuleBase {
|
||||||
|
public:
|
||||||
|
void onLoad(Api *api, JNIEnv *env) override {
|
||||||
|
this->api = api;
|
||||||
|
this->env = env;
|
||||||
|
ALOGI("CamSwapper Zygisk module loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
void preAppSpecialize(AppSpecializeArgs *args) override {
|
||||||
|
if (args && args->nice_name) {
|
||||||
|
const char *process = env->GetStringUTFChars(args->nice_name, nullptr);
|
||||||
|
if (process) {
|
||||||
|
ALOGI("preAppSpecialize: %s", process);
|
||||||
|
env->ReleaseStringUTFChars(args->nice_name, process);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void postAppSpecialize(const AppSpecializeArgs *args) override {
|
||||||
|
if (args && args->nice_name) {
|
||||||
|
const char *process = env->GetStringUTFChars(args->nice_name, nullptr);
|
||||||
|
if (process) {
|
||||||
|
ALOGI("postAppSpecialize: %s", process);
|
||||||
|
if (is_camera_process(process)) {
|
||||||
|
ALOGI("Loading hook in camera app: %s", process);
|
||||||
|
load_hook_lib();
|
||||||
|
}
|
||||||
|
env->ReleaseStringUTFChars(args->nice_name, process);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void preServerSpecialize(ServerSpecializeArgs *args) override {
|
||||||
|
ALOGI("preServerSpecialize: system_server");
|
||||||
|
}
|
||||||
|
|
||||||
|
void postServerSpecialize(const ServerSpecializeArgs *args) override {
|
||||||
|
ALOGI("postServerSpecialize: system_server ready, loading hook");
|
||||||
|
load_hook_lib();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Api *api;
|
||||||
|
JNIEnv *env;
|
||||||
|
|
||||||
|
void load_hook_lib() {
|
||||||
|
struct stat st;
|
||||||
|
if (stat(HOOK_LIB_PATH, &st) != 0) {
|
||||||
|
ALOGE("Hook library not found: %s", HOOK_LIB_PATH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
void *handle = dlopen(HOOK_LIB_PATH, RTLD_NOW | RTLD_LOCAL);
|
||||||
|
if (!handle) {
|
||||||
|
ALOGE("dlopen(%s) failed: %s", HOOK_LIB_PATH, dlerror());
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ALOGI("Hook library loaded: %s (handle=%p)", HOOK_LIB_PATH, handle);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
REGISTER_ZYGISK_MODULE(CamSwapperModule)
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
/* Copyright 2022-2023 John "topjohnwu" Wu
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
* PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This is the public API for Zygisk modules.
|
||||||
|
// DO NOT MODIFY ANY CODE IN THIS HEADER.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#define ZYGISK_API_VERSION 4
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
***************
|
||||||
|
* Introduction
|
||||||
|
***************
|
||||||
|
|
||||||
|
On Android, all app processes are forked from a special daemon called "Zygote".
|
||||||
|
For each new app process, zygote will fork a new process and perform "specialization".
|
||||||
|
This specialization operation enforces the Android security sandbox on the newly forked
|
||||||
|
process to make sure that 3rd party application code is only loaded after it is being
|
||||||
|
restricted within a sandbox.
|
||||||
|
|
||||||
|
On Android, there is also this special process called "system_server". This single
|
||||||
|
process hosts a significant portion of system services, which controls how the
|
||||||
|
Android operating system and apps interact with each other.
|
||||||
|
|
||||||
|
The Zygisk framework provides a way to allow developers to build modules and run custom
|
||||||
|
code before and after system_server and any app processes' specialization.
|
||||||
|
This enable developers to inject code and alter the behavior of system_server and app processes.
|
||||||
|
|
||||||
|
Please note that modules will only be loaded after zygote has forked the child process.
|
||||||
|
THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM_SERVER PROCESS, NOT THE ZYGOTE DAEMON!
|
||||||
|
|
||||||
|
*********************
|
||||||
|
* Development Guide
|
||||||
|
*********************
|
||||||
|
|
||||||
|
Define a class and inherit zygisk::ModuleBase to implement the functionality of your module.
|
||||||
|
Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk.
|
||||||
|
|
||||||
|
Example code:
|
||||||
|
|
||||||
|
static jint (*orig_logger_entry_max)(JNIEnv *env);
|
||||||
|
static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); }
|
||||||
|
|
||||||
|
class ExampleModule : public zygisk::ModuleBase {
|
||||||
|
public:
|
||||||
|
void onLoad(zygisk::Api *api, JNIEnv *env) override {
|
||||||
|
this->api = api;
|
||||||
|
this->env = env;
|
||||||
|
}
|
||||||
|
void preAppSpecialize(zygisk::AppSpecializeArgs *args) override {
|
||||||
|
JNINativeMethod methods[] = {
|
||||||
|
{ "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max },
|
||||||
|
};
|
||||||
|
api->hookJniNativeMethods(env, "android/util/Log", methods, 1);
|
||||||
|
*(void **) &orig_logger_entry_max = methods[0].fnPtr;
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
zygisk::Api *api;
|
||||||
|
JNIEnv *env;
|
||||||
|
};
|
||||||
|
|
||||||
|
REGISTER_ZYGISK_MODULE(ExampleModule)
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Since your module class's code runs with either Zygote's privilege in pre[XXX]Specialize,
|
||||||
|
or runs in the sandbox of the target process in post[XXX]Specialize, the code in your class
|
||||||
|
never runs in a true superuser environment.
|
||||||
|
|
||||||
|
If your module require access to superuser permissions, you can create and register
|
||||||
|
a root companion handler function. This function runs in a separate root companion
|
||||||
|
daemon process, and an Unix domain socket is provided to allow you to perform IPC between
|
||||||
|
your target process and the root companion process.
|
||||||
|
|
||||||
|
Example code:
|
||||||
|
|
||||||
|
static void example_handler(int socket) { ... }
|
||||||
|
|
||||||
|
REGISTER_ZYGISK_COMPANION(example_handler)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace zygisk {
|
||||||
|
|
||||||
|
struct Api;
|
||||||
|
struct AppSpecializeArgs;
|
||||||
|
struct ServerSpecializeArgs;
|
||||||
|
|
||||||
|
class ModuleBase {
|
||||||
|
public:
|
||||||
|
|
||||||
|
// This method is called as soon as the module is loaded into the target process.
|
||||||
|
// A Zygisk API handle will be passed as an argument.
|
||||||
|
virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {}
|
||||||
|
|
||||||
|
// This method is called before the app process is specialized.
|
||||||
|
// At this point, the process just got forked from zygote, but no app specific specialization
|
||||||
|
// is applied. This means that the process does not have any sandbox restrictions and
|
||||||
|
// still runs with the same privilege of zygote.
|
||||||
|
//
|
||||||
|
// All the arguments that will be sent and used for app specialization is passed as a single
|
||||||
|
// AppSpecializeArgs object. You can read and overwrite these arguments to change how the app
|
||||||
|
// process will be specialized.
|
||||||
|
//
|
||||||
|
// If you need to run some operations as superuser, you can call Api::connectCompanion() to
|
||||||
|
// get a socket to do IPC calls with a root companion process.
|
||||||
|
// See Api::connectCompanion() for more info.
|
||||||
|
virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {}
|
||||||
|
|
||||||
|
// This method is called after the app process is specialized.
|
||||||
|
// At this point, the process has all sandbox restrictions enabled for this application.
|
||||||
|
// This means that this method runs with the same privilege of the app's own code.
|
||||||
|
virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {}
|
||||||
|
|
||||||
|
// This method is called before the system server process is specialized.
|
||||||
|
// See preAppSpecialize(args) for more info.
|
||||||
|
virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {}
|
||||||
|
|
||||||
|
// This method is called after the system server process is specialized.
|
||||||
|
// At this point, the process runs with the privilege of system_server.
|
||||||
|
virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AppSpecializeArgs {
|
||||||
|
// Required arguments. These arguments are guaranteed to exist on all Android versions.
|
||||||
|
jint &uid;
|
||||||
|
jint &gid;
|
||||||
|
jintArray &gids;
|
||||||
|
jint &runtime_flags;
|
||||||
|
jobjectArray &rlimits;
|
||||||
|
jint &mount_external;
|
||||||
|
jstring &se_info;
|
||||||
|
jstring &nice_name;
|
||||||
|
jstring &instruction_set;
|
||||||
|
jstring &app_data_dir;
|
||||||
|
|
||||||
|
// Optional arguments. Please check whether the pointer is null before de-referencing
|
||||||
|
jintArray *const fds_to_ignore;
|
||||||
|
jboolean *const is_child_zygote;
|
||||||
|
jboolean *const is_top_app;
|
||||||
|
jobjectArray *const pkg_data_info_list;
|
||||||
|
jobjectArray *const whitelisted_data_info_list;
|
||||||
|
jboolean *const mount_data_dirs;
|
||||||
|
jboolean *const mount_storage_dirs;
|
||||||
|
|
||||||
|
AppSpecializeArgs() = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ServerSpecializeArgs {
|
||||||
|
jint &uid;
|
||||||
|
jint &gid;
|
||||||
|
jintArray &gids;
|
||||||
|
jint &runtime_flags;
|
||||||
|
jlong &permitted_capabilities;
|
||||||
|
jlong &effective_capabilities;
|
||||||
|
|
||||||
|
ServerSpecializeArgs() = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace internal {
|
||||||
|
struct api_table;
|
||||||
|
template <class T> void entry_impl(api_table *, JNIEnv *);
|
||||||
|
}
|
||||||
|
|
||||||
|
// These values are used in Api::setOption(Option)
|
||||||
|
enum Option : int {
|
||||||
|
// Force Magisk's denylist unmount routines to run on this process.
|
||||||
|
//
|
||||||
|
// Setting this option only makes sense in preAppSpecialize.
|
||||||
|
// The actual unmounting happens during app process specialization.
|
||||||
|
//
|
||||||
|
// Set this option to force all Magisk and modules' files to be unmounted from the
|
||||||
|
// mount namespace of the process, regardless of the denylist enforcement status.
|
||||||
|
FORCE_DENYLIST_UNMOUNT = 0,
|
||||||
|
|
||||||
|
// When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize.
|
||||||
|
// Be aware that after dlclose-ing your module, all of your code will be unmapped from memory.
|
||||||
|
// YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS.
|
||||||
|
DLCLOSE_MODULE_LIBRARY = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bit masks of the return value of Api::getFlags()
|
||||||
|
enum StateFlag : uint32_t {
|
||||||
|
// The user has granted root access to the current process
|
||||||
|
PROCESS_GRANTED_ROOT = (1u << 0),
|
||||||
|
|
||||||
|
// The current process was added on the denylist
|
||||||
|
PROCESS_ON_DENYLIST = (1u << 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
// All API methods will stop working after post[XXX]Specialize as Zygisk will be unloaded
|
||||||
|
// from the specialized process afterwards.
|
||||||
|
struct Api {
|
||||||
|
|
||||||
|
// Connect to a root companion process and get a Unix domain socket for IPC.
|
||||||
|
//
|
||||||
|
// This API only works in the pre[XXX]Specialize methods due to SELinux restrictions.
|
||||||
|
//
|
||||||
|
// The pre[XXX]Specialize methods run with the same privilege of zygote.
|
||||||
|
// If you would like to do some operations with superuser permissions, register a handler
|
||||||
|
// function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func).
|
||||||
|
// Another good use case for a companion process is that if you want to share some resources
|
||||||
|
// across multiple processes, hold the resources in the companion process and pass it over.
|
||||||
|
//
|
||||||
|
// The root companion process is ABI aware; that is, when calling this method from a 32-bit
|
||||||
|
// process, you will be connected to a 32-bit companion process, and vice versa for 64-bit.
|
||||||
|
//
|
||||||
|
// Returns a file descriptor to a socket that is connected to the socket passed to your
|
||||||
|
// module's companion request handler. Returns -1 if the connection attempt failed.
|
||||||
|
int connectCompanion();
|
||||||
|
|
||||||
|
// Get the file descriptor of the root folder of the current module.
|
||||||
|
//
|
||||||
|
// This API only works in the pre[XXX]Specialize methods.
|
||||||
|
// Accessing the directory returned is only possible in the pre[XXX]Specialize methods
|
||||||
|
// or in the root companion process (assuming that you sent the fd over the socket).
|
||||||
|
// Both restrictions are due to SELinux and UID.
|
||||||
|
//
|
||||||
|
// Returns -1 if errors occurred.
|
||||||
|
int getModuleDir();
|
||||||
|
|
||||||
|
// Set various options for your module.
|
||||||
|
// Please note that this method accepts one single option at a time.
|
||||||
|
// Check zygisk::Option for the full list of options available.
|
||||||
|
void setOption(Option opt);
|
||||||
|
|
||||||
|
// Get information about the current process.
|
||||||
|
// Returns bitwise-or'd zygisk::StateFlag values.
|
||||||
|
uint32_t getFlags();
|
||||||
|
|
||||||
|
// Exempt the provided file descriptor from being automatically closed.
|
||||||
|
//
|
||||||
|
// This API only make sense in preAppSpecialize; calling this method in any other situation
|
||||||
|
// is either a no-op (returns true) or an error (returns false).
|
||||||
|
//
|
||||||
|
// When false is returned, the provided file descriptor will eventually be closed by zygote.
|
||||||
|
bool exemptFd(int fd);
|
||||||
|
|
||||||
|
// Hook JNI native methods for a class
|
||||||
|
//
|
||||||
|
// Lookup all registered JNI native methods and replace it with your own methods.
|
||||||
|
// The original function pointer will be saved in each JNINativeMethod's fnPtr.
|
||||||
|
// If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr
|
||||||
|
// will be set to nullptr.
|
||||||
|
void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods);
|
||||||
|
|
||||||
|
// Hook functions in the PLT (Procedure Linkage Table) of ELFs loaded in memory.
|
||||||
|
//
|
||||||
|
// Parsing /proc/[PID]/maps will give you the memory map of a process. As an example:
|
||||||
|
//
|
||||||
|
// <address> <perms> <offset> <dev> <inode> <pathname>
|
||||||
|
// 56b4346000-56b4347000 r-xp 00002000 fe:00 235 /system/bin/app_process64
|
||||||
|
// (More details: https://man7.org/linux/man-pages/man5/proc.5.html)
|
||||||
|
//
|
||||||
|
// The `dev` and `inode` pair uniquely identifies a file being mapped into memory.
|
||||||
|
// For matching ELFs loaded in memory, replace function `symbol` with `newFunc`.
|
||||||
|
// If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`.
|
||||||
|
void pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc);
|
||||||
|
|
||||||
|
// Commit all the hooks that was previously registered.
|
||||||
|
// Returns false if an error occurred.
|
||||||
|
bool pltHookCommit();
|
||||||
|
|
||||||
|
private:
|
||||||
|
internal::api_table *tbl;
|
||||||
|
template <class T> friend void internal::entry_impl(internal::api_table *, JNIEnv *);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register a class as a Zygisk module
|
||||||
|
|
||||||
|
#define REGISTER_ZYGISK_MODULE(clazz) \
|
||||||
|
void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \
|
||||||
|
zygisk::internal::entry_impl<clazz>(table, env); \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a root companion request handler function for your module
|
||||||
|
//
|
||||||
|
// The function runs in a superuser daemon process and handles a root companion request from
|
||||||
|
// your module running in a target process. The function has to accept an integer value,
|
||||||
|
// which is a Unix domain socket that is connected to the target process.
|
||||||
|
// See Api::connectCompanion() for more info.
|
||||||
|
//
|
||||||
|
// NOTE: the function can run concurrently on multiple threads.
|
||||||
|
// Be aware of race conditions if you have globally shared resources.
|
||||||
|
|
||||||
|
#define REGISTER_ZYGISK_COMPANION(func) \
|
||||||
|
void zygisk_companion_entry(int client) { func(client); }
|
||||||
|
|
||||||
|
/*********************************************************
|
||||||
|
* The following is internal ABI implementation detail.
|
||||||
|
* You do not have to understand what it is doing.
|
||||||
|
*********************************************************/
|
||||||
|
|
||||||
|
namespace internal {
|
||||||
|
|
||||||
|
struct module_abi {
|
||||||
|
long api_version;
|
||||||
|
ModuleBase *impl;
|
||||||
|
|
||||||
|
void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *);
|
||||||
|
void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *);
|
||||||
|
void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *);
|
||||||
|
void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *);
|
||||||
|
|
||||||
|
module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), impl(module) {
|
||||||
|
preAppSpecialize = [](auto m, auto args) { m->preAppSpecialize(args); };
|
||||||
|
postAppSpecialize = [](auto m, auto args) { m->postAppSpecialize(args); };
|
||||||
|
preServerSpecialize = [](auto m, auto args) { m->preServerSpecialize(args); };
|
||||||
|
postServerSpecialize = [](auto m, auto args) { m->postServerSpecialize(args); };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct api_table {
|
||||||
|
// Base
|
||||||
|
void *impl;
|
||||||
|
bool (*registerModule)(api_table *, module_abi *);
|
||||||
|
|
||||||
|
void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int);
|
||||||
|
void (*pltHookRegister)(dev_t, ino_t, const char *, void *, void **);
|
||||||
|
bool (*exemptFd)(int);
|
||||||
|
bool (*pltHookCommit)();
|
||||||
|
int (*connectCompanion)(void * /* impl */);
|
||||||
|
void (*setOption)(void * /* impl */, Option);
|
||||||
|
int (*getModuleDir)(void * /* impl */);
|
||||||
|
uint32_t (*getFlags)(void * /* impl */);
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void entry_impl(api_table *table, JNIEnv *env) {
|
||||||
|
static Api api;
|
||||||
|
api.tbl = table;
|
||||||
|
static T module;
|
||||||
|
ModuleBase *m = &module;
|
||||||
|
static module_abi abi(m);
|
||||||
|
if (!table->registerModule(table, &abi)) return;
|
||||||
|
m->onLoad(&api, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace internal
|
||||||
|
|
||||||
|
inline int Api::connectCompanion() {
|
||||||
|
return tbl->connectCompanion ? tbl->connectCompanion(tbl->impl) : -1;
|
||||||
|
}
|
||||||
|
inline int Api::getModuleDir() {
|
||||||
|
return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1;
|
||||||
|
}
|
||||||
|
inline void Api::setOption(Option opt) {
|
||||||
|
if (tbl->setOption) tbl->setOption(tbl->impl, opt);
|
||||||
|
}
|
||||||
|
inline uint32_t Api::getFlags() {
|
||||||
|
return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0;
|
||||||
|
}
|
||||||
|
inline bool Api::exemptFd(int fd) {
|
||||||
|
return tbl->exemptFd != nullptr && tbl->exemptFd(fd);
|
||||||
|
}
|
||||||
|
inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) {
|
||||||
|
if (tbl->hookJniNativeMethods) tbl->hookJniNativeMethods(env, className, methods, numMethods);
|
||||||
|
}
|
||||||
|
inline void Api::pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc) {
|
||||||
|
if (tbl->pltHookRegister) tbl->pltHookRegister(dev, inode, symbol, newFunc, oldFunc);
|
||||||
|
}
|
||||||
|
inline bool Api::pltHookCommit() {
|
||||||
|
return tbl->pltHookCommit != nullptr && tbl->pltHookCommit();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace zygisk
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
[[gnu::visibility("default"), maybe_unused]]
|
||||||
|
void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *);
|
||||||
|
|
||||||
|
[[gnu::visibility("default"), maybe_unused]]
|
||||||
|
void zygisk_companion_entry(int);
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
BIN
Binary file not shown.
@@ -1,6 +1,6 @@
|
|||||||
id=camera-hook-zygisk
|
id=camera-hook-zygisk
|
||||||
name=CamSwapper Zygisk Hook
|
name=CamSwapper Zygisk Hook
|
||||||
version=1.0
|
version=2.0
|
||||||
versionCode=1
|
versionCode=2
|
||||||
author=CamSwapper
|
author=CamSwapper
|
||||||
description=Injects libcamera_hook.so into cameraserver via Zygisk
|
description=Injects libcamera_hook.so into cameraserver via Zygisk (v2.0 - proper API)
|
||||||
|
|||||||
BIN
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,11 @@
|
|||||||
|
./obj/local/arm64-v8a/objs/camswapper/CamSwapperEntry.o: \
|
||||||
|
jni/CamSwapperEntry.cpp \
|
||||||
|
/tmp/android-ndk-r25c/sources/cxx-stl/llvm-libc++/include/string.h \
|
||||||
|
/tmp/android-ndk-r25c/sources/cxx-stl/llvm-libc++/include/__config \
|
||||||
|
jni/zygisk.hpp
|
||||||
|
|
||||||
|
/tmp/android-ndk-r25c/sources/cxx-stl/llvm-libc++/include/string.h:
|
||||||
|
|
||||||
|
/tmp/android-ndk-r25c/sources/cxx-stl/llvm-libc++/include/__config:
|
||||||
|
|
||||||
|
jni/zygisk.hpp:
|
||||||
Executable
+25
@@ -0,0 +1,25 @@
|
|||||||
|
#!/system/bin/sh
|
||||||
|
MODDIR="${0%/*}"
|
||||||
|
MODPROP="$MODDIR/module.prop"
|
||||||
|
|
||||||
|
update_description() {
|
||||||
|
if [ -f "$MODPROP" ]; then
|
||||||
|
sed -i "s|^description=.*|description=$1|" "$MODPROP"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
update_description "Checking Zygisk module status..."
|
||||||
|
|
||||||
|
# Check if Zygisk framework is available
|
||||||
|
if [ ! -d "/data/adb/modules" ]; then
|
||||||
|
update_description "Error - /data/adb/modules not found"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if our module is loaded (check for libcamera_hook.so in cameraserver)
|
||||||
|
CS_PID=$(pidof cameraserver 2>/dev/null)
|
||||||
|
if [ -n "$CS_PID" ] && grep -q "libcamera_hook.so" /proc/"$CS_PID"/maps 2>/dev/null; then
|
||||||
|
update_description "Active - hook loaded in cameraserver PID $CS_PID"
|
||||||
|
else
|
||||||
|
update_description "Zygisk module loaded - waiting for cameraserver"
|
||||||
|
fi
|
||||||
@@ -0,0 +1,391 @@
|
|||||||
|
/* Copyright 2022-2023 John "topjohnwu" Wu
|
||||||
|
*
|
||||||
|
* Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
* purpose with or without fee is hereby granted.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
||||||
|
* REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
||||||
|
* AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
||||||
|
* INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
||||||
|
* LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
||||||
|
* OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
||||||
|
* PERFORMANCE OF THIS SOFTWARE.
|
||||||
|
*/
|
||||||
|
|
||||||
|
// This is the public API for Zygisk modules.
|
||||||
|
// DO NOT MODIFY ANY CODE IN THIS HEADER.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <jni.h>
|
||||||
|
|
||||||
|
#define ZYGISK_API_VERSION 4
|
||||||
|
|
||||||
|
/*
|
||||||
|
|
||||||
|
***************
|
||||||
|
* Introduction
|
||||||
|
***************
|
||||||
|
|
||||||
|
On Android, all app processes are forked from a special daemon called "Zygote".
|
||||||
|
For each new app process, zygote will fork a new process and perform "specialization".
|
||||||
|
This specialization operation enforces the Android security sandbox on the newly forked
|
||||||
|
process to make sure that 3rd party application code is only loaded after it is being
|
||||||
|
restricted within a sandbox.
|
||||||
|
|
||||||
|
On Android, there is also this special process called "system_server". This single
|
||||||
|
process hosts a significant portion of system services, which controls how the
|
||||||
|
Android operating system and apps interact with each other.
|
||||||
|
|
||||||
|
The Zygisk framework provides a way to allow developers to build modules and run custom
|
||||||
|
code before and after system_server and any app processes' specialization.
|
||||||
|
This enable developers to inject code and alter the behavior of system_server and app processes.
|
||||||
|
|
||||||
|
Please note that modules will only be loaded after zygote has forked the child process.
|
||||||
|
THIS MEANS ALL OF YOUR CODE RUNS IN THE APP/SYSTEM_SERVER PROCESS, NOT THE ZYGOTE DAEMON!
|
||||||
|
|
||||||
|
*********************
|
||||||
|
* Development Guide
|
||||||
|
*********************
|
||||||
|
|
||||||
|
Define a class and inherit zygisk::ModuleBase to implement the functionality of your module.
|
||||||
|
Use the macro REGISTER_ZYGISK_MODULE(className) to register that class to Zygisk.
|
||||||
|
|
||||||
|
Example code:
|
||||||
|
|
||||||
|
static jint (*orig_logger_entry_max)(JNIEnv *env);
|
||||||
|
static jint my_logger_entry_max(JNIEnv *env) { return orig_logger_entry_max(env); }
|
||||||
|
|
||||||
|
class ExampleModule : public zygisk::ModuleBase {
|
||||||
|
public:
|
||||||
|
void onLoad(zygisk::Api *api, JNIEnv *env) override {
|
||||||
|
this->api = api;
|
||||||
|
this->env = env;
|
||||||
|
}
|
||||||
|
void preAppSpecialize(zygisk::AppSpecializeArgs *args) override {
|
||||||
|
JNINativeMethod methods[] = {
|
||||||
|
{ "logger_entry_max_payload_native", "()I", (void*) my_logger_entry_max },
|
||||||
|
};
|
||||||
|
api->hookJniNativeMethods(env, "android/util/Log", methods, 1);
|
||||||
|
*(void **) &orig_logger_entry_max = methods[0].fnPtr;
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
zygisk::Api *api;
|
||||||
|
JNIEnv *env;
|
||||||
|
};
|
||||||
|
|
||||||
|
REGISTER_ZYGISK_MODULE(ExampleModule)
|
||||||
|
|
||||||
|
-----------------------------------------------------------------------------------------
|
||||||
|
|
||||||
|
Since your module class's code runs with either Zygote's privilege in pre[XXX]Specialize,
|
||||||
|
or runs in the sandbox of the target process in post[XXX]Specialize, the code in your class
|
||||||
|
never runs in a true superuser environment.
|
||||||
|
|
||||||
|
If your module require access to superuser permissions, you can create and register
|
||||||
|
a root companion handler function. This function runs in a separate root companion
|
||||||
|
daemon process, and an Unix domain socket is provided to allow you to perform IPC between
|
||||||
|
your target process and the root companion process.
|
||||||
|
|
||||||
|
Example code:
|
||||||
|
|
||||||
|
static void example_handler(int socket) { ... }
|
||||||
|
|
||||||
|
REGISTER_ZYGISK_COMPANION(example_handler)
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace zygisk {
|
||||||
|
|
||||||
|
struct Api;
|
||||||
|
struct AppSpecializeArgs;
|
||||||
|
struct ServerSpecializeArgs;
|
||||||
|
|
||||||
|
class ModuleBase {
|
||||||
|
public:
|
||||||
|
|
||||||
|
// This method is called as soon as the module is loaded into the target process.
|
||||||
|
// A Zygisk API handle will be passed as an argument.
|
||||||
|
virtual void onLoad([[maybe_unused]] Api *api, [[maybe_unused]] JNIEnv *env) {}
|
||||||
|
|
||||||
|
// This method is called before the app process is specialized.
|
||||||
|
// At this point, the process just got forked from zygote, but no app specific specialization
|
||||||
|
// is applied. This means that the process does not have any sandbox restrictions and
|
||||||
|
// still runs with the same privilege of zygote.
|
||||||
|
//
|
||||||
|
// All the arguments that will be sent and used for app specialization is passed as a single
|
||||||
|
// AppSpecializeArgs object. You can read and overwrite these arguments to change how the app
|
||||||
|
// process will be specialized.
|
||||||
|
//
|
||||||
|
// If you need to run some operations as superuser, you can call Api::connectCompanion() to
|
||||||
|
// get a socket to do IPC calls with a root companion process.
|
||||||
|
// See Api::connectCompanion() for more info.
|
||||||
|
virtual void preAppSpecialize([[maybe_unused]] AppSpecializeArgs *args) {}
|
||||||
|
|
||||||
|
// This method is called after the app process is specialized.
|
||||||
|
// At this point, the process has all sandbox restrictions enabled for this application.
|
||||||
|
// This means that this method runs with the same privilege of the app's own code.
|
||||||
|
virtual void postAppSpecialize([[maybe_unused]] const AppSpecializeArgs *args) {}
|
||||||
|
|
||||||
|
// This method is called before the system server process is specialized.
|
||||||
|
// See preAppSpecialize(args) for more info.
|
||||||
|
virtual void preServerSpecialize([[maybe_unused]] ServerSpecializeArgs *args) {}
|
||||||
|
|
||||||
|
// This method is called after the system server process is specialized.
|
||||||
|
// At this point, the process runs with the privilege of system_server.
|
||||||
|
virtual void postServerSpecialize([[maybe_unused]] const ServerSpecializeArgs *args) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct AppSpecializeArgs {
|
||||||
|
// Required arguments. These arguments are guaranteed to exist on all Android versions.
|
||||||
|
jint &uid;
|
||||||
|
jint &gid;
|
||||||
|
jintArray &gids;
|
||||||
|
jint &runtime_flags;
|
||||||
|
jobjectArray &rlimits;
|
||||||
|
jint &mount_external;
|
||||||
|
jstring &se_info;
|
||||||
|
jstring &nice_name;
|
||||||
|
jstring &instruction_set;
|
||||||
|
jstring &app_data_dir;
|
||||||
|
|
||||||
|
// Optional arguments. Please check whether the pointer is null before de-referencing
|
||||||
|
jintArray *const fds_to_ignore;
|
||||||
|
jboolean *const is_child_zygote;
|
||||||
|
jboolean *const is_top_app;
|
||||||
|
jobjectArray *const pkg_data_info_list;
|
||||||
|
jobjectArray *const whitelisted_data_info_list;
|
||||||
|
jboolean *const mount_data_dirs;
|
||||||
|
jboolean *const mount_storage_dirs;
|
||||||
|
|
||||||
|
AppSpecializeArgs() = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ServerSpecializeArgs {
|
||||||
|
jint &uid;
|
||||||
|
jint &gid;
|
||||||
|
jintArray &gids;
|
||||||
|
jint &runtime_flags;
|
||||||
|
jlong &permitted_capabilities;
|
||||||
|
jlong &effective_capabilities;
|
||||||
|
|
||||||
|
ServerSpecializeArgs() = delete;
|
||||||
|
};
|
||||||
|
|
||||||
|
namespace internal {
|
||||||
|
struct api_table;
|
||||||
|
template <class T> void entry_impl(api_table *, JNIEnv *);
|
||||||
|
}
|
||||||
|
|
||||||
|
// These values are used in Api::setOption(Option)
|
||||||
|
enum Option : int {
|
||||||
|
// Force Magisk's denylist unmount routines to run on this process.
|
||||||
|
//
|
||||||
|
// Setting this option only makes sense in preAppSpecialize.
|
||||||
|
// The actual unmounting happens during app process specialization.
|
||||||
|
//
|
||||||
|
// Set this option to force all Magisk and modules' files to be unmounted from the
|
||||||
|
// mount namespace of the process, regardless of the denylist enforcement status.
|
||||||
|
FORCE_DENYLIST_UNMOUNT = 0,
|
||||||
|
|
||||||
|
// When this option is set, your module's library will be dlclose-ed after post[XXX]Specialize.
|
||||||
|
// Be aware that after dlclose-ing your module, all of your code will be unmapped from memory.
|
||||||
|
// YOU MUST NOT ENABLE THIS OPTION AFTER HOOKING ANY FUNCTIONS IN THE PROCESS.
|
||||||
|
DLCLOSE_MODULE_LIBRARY = 1,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Bit masks of the return value of Api::getFlags()
|
||||||
|
enum StateFlag : uint32_t {
|
||||||
|
// The user has granted root access to the current process
|
||||||
|
PROCESS_GRANTED_ROOT = (1u << 0),
|
||||||
|
|
||||||
|
// The current process was added on the denylist
|
||||||
|
PROCESS_ON_DENYLIST = (1u << 1),
|
||||||
|
};
|
||||||
|
|
||||||
|
// All API methods will stop working after post[XXX]Specialize as Zygisk will be unloaded
|
||||||
|
// from the specialized process afterwards.
|
||||||
|
struct Api {
|
||||||
|
|
||||||
|
// Connect to a root companion process and get a Unix domain socket for IPC.
|
||||||
|
//
|
||||||
|
// This API only works in the pre[XXX]Specialize methods due to SELinux restrictions.
|
||||||
|
//
|
||||||
|
// The pre[XXX]Specialize methods run with the same privilege of zygote.
|
||||||
|
// If you would like to do some operations with superuser permissions, register a handler
|
||||||
|
// function that would be called in the root process with REGISTER_ZYGISK_COMPANION(func).
|
||||||
|
// Another good use case for a companion process is that if you want to share some resources
|
||||||
|
// across multiple processes, hold the resources in the companion process and pass it over.
|
||||||
|
//
|
||||||
|
// The root companion process is ABI aware; that is, when calling this method from a 32-bit
|
||||||
|
// process, you will be connected to a 32-bit companion process, and vice versa for 64-bit.
|
||||||
|
//
|
||||||
|
// Returns a file descriptor to a socket that is connected to the socket passed to your
|
||||||
|
// module's companion request handler. Returns -1 if the connection attempt failed.
|
||||||
|
int connectCompanion();
|
||||||
|
|
||||||
|
// Get the file descriptor of the root folder of the current module.
|
||||||
|
//
|
||||||
|
// This API only works in the pre[XXX]Specialize methods.
|
||||||
|
// Accessing the directory returned is only possible in the pre[XXX]Specialize methods
|
||||||
|
// or in the root companion process (assuming that you sent the fd over the socket).
|
||||||
|
// Both restrictions are due to SELinux and UID.
|
||||||
|
//
|
||||||
|
// Returns -1 if errors occurred.
|
||||||
|
int getModuleDir();
|
||||||
|
|
||||||
|
// Set various options for your module.
|
||||||
|
// Please note that this method accepts one single option at a time.
|
||||||
|
// Check zygisk::Option for the full list of options available.
|
||||||
|
void setOption(Option opt);
|
||||||
|
|
||||||
|
// Get information about the current process.
|
||||||
|
// Returns bitwise-or'd zygisk::StateFlag values.
|
||||||
|
uint32_t getFlags();
|
||||||
|
|
||||||
|
// Exempt the provided file descriptor from being automatically closed.
|
||||||
|
//
|
||||||
|
// This API only make sense in preAppSpecialize; calling this method in any other situation
|
||||||
|
// is either a no-op (returns true) or an error (returns false).
|
||||||
|
//
|
||||||
|
// When false is returned, the provided file descriptor will eventually be closed by zygote.
|
||||||
|
bool exemptFd(int fd);
|
||||||
|
|
||||||
|
// Hook JNI native methods for a class
|
||||||
|
//
|
||||||
|
// Lookup all registered JNI native methods and replace it with your own methods.
|
||||||
|
// The original function pointer will be saved in each JNINativeMethod's fnPtr.
|
||||||
|
// If no matching class, method name, or signature is found, that specific JNINativeMethod.fnPtr
|
||||||
|
// will be set to nullptr.
|
||||||
|
void hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods);
|
||||||
|
|
||||||
|
// Hook functions in the PLT (Procedure Linkage Table) of ELFs loaded in memory.
|
||||||
|
//
|
||||||
|
// Parsing /proc/[PID]/maps will give you the memory map of a process. As an example:
|
||||||
|
//
|
||||||
|
// <address> <perms> <offset> <dev> <inode> <pathname>
|
||||||
|
// 56b4346000-56b4347000 r-xp 00002000 fe:00 235 /system/bin/app_process64
|
||||||
|
// (More details: https://man7.org/linux/man-pages/man5/proc.5.html)
|
||||||
|
//
|
||||||
|
// The `dev` and `inode` pair uniquely identifies a file being mapped into memory.
|
||||||
|
// For matching ELFs loaded in memory, replace function `symbol` with `newFunc`.
|
||||||
|
// If `oldFunc` is not nullptr, the original function pointer will be saved to `oldFunc`.
|
||||||
|
void pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc);
|
||||||
|
|
||||||
|
// Commit all the hooks that was previously registered.
|
||||||
|
// Returns false if an error occurred.
|
||||||
|
bool pltHookCommit();
|
||||||
|
|
||||||
|
private:
|
||||||
|
internal::api_table *tbl;
|
||||||
|
template <class T> friend void internal::entry_impl(internal::api_table *, JNIEnv *);
|
||||||
|
};
|
||||||
|
|
||||||
|
// Register a class as a Zygisk module
|
||||||
|
|
||||||
|
#define REGISTER_ZYGISK_MODULE(clazz) \
|
||||||
|
void zygisk_module_entry(zygisk::internal::api_table *table, JNIEnv *env) { \
|
||||||
|
zygisk::internal::entry_impl<clazz>(table, env); \
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register a root companion request handler function for your module
|
||||||
|
//
|
||||||
|
// The function runs in a superuser daemon process and handles a root companion request from
|
||||||
|
// your module running in a target process. The function has to accept an integer value,
|
||||||
|
// which is a Unix domain socket that is connected to the target process.
|
||||||
|
// See Api::connectCompanion() for more info.
|
||||||
|
//
|
||||||
|
// NOTE: the function can run concurrently on multiple threads.
|
||||||
|
// Be aware of race conditions if you have globally shared resources.
|
||||||
|
|
||||||
|
#define REGISTER_ZYGISK_COMPANION(func) \
|
||||||
|
void zygisk_companion_entry(int client) { func(client); }
|
||||||
|
|
||||||
|
/*********************************************************
|
||||||
|
* The following is internal ABI implementation detail.
|
||||||
|
* You do not have to understand what it is doing.
|
||||||
|
*********************************************************/
|
||||||
|
|
||||||
|
namespace internal {
|
||||||
|
|
||||||
|
struct module_abi {
|
||||||
|
long api_version;
|
||||||
|
ModuleBase *impl;
|
||||||
|
|
||||||
|
void (*preAppSpecialize)(ModuleBase *, AppSpecializeArgs *);
|
||||||
|
void (*postAppSpecialize)(ModuleBase *, const AppSpecializeArgs *);
|
||||||
|
void (*preServerSpecialize)(ModuleBase *, ServerSpecializeArgs *);
|
||||||
|
void (*postServerSpecialize)(ModuleBase *, const ServerSpecializeArgs *);
|
||||||
|
|
||||||
|
module_abi(ModuleBase *module) : api_version(ZYGISK_API_VERSION), impl(module) {
|
||||||
|
preAppSpecialize = [](auto m, auto args) { m->preAppSpecialize(args); };
|
||||||
|
postAppSpecialize = [](auto m, auto args) { m->postAppSpecialize(args); };
|
||||||
|
preServerSpecialize = [](auto m, auto args) { m->preServerSpecialize(args); };
|
||||||
|
postServerSpecialize = [](auto m, auto args) { m->postServerSpecialize(args); };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
struct api_table {
|
||||||
|
// Base
|
||||||
|
void *impl;
|
||||||
|
bool (*registerModule)(api_table *, module_abi *);
|
||||||
|
|
||||||
|
void (*hookJniNativeMethods)(JNIEnv *, const char *, JNINativeMethod *, int);
|
||||||
|
void (*pltHookRegister)(dev_t, ino_t, const char *, void *, void **);
|
||||||
|
bool (*exemptFd)(int);
|
||||||
|
bool (*pltHookCommit)();
|
||||||
|
int (*connectCompanion)(void * /* impl */);
|
||||||
|
void (*setOption)(void * /* impl */, Option);
|
||||||
|
int (*getModuleDir)(void * /* impl */);
|
||||||
|
uint32_t (*getFlags)(void * /* impl */);
|
||||||
|
};
|
||||||
|
|
||||||
|
template <class T>
|
||||||
|
void entry_impl(api_table *table, JNIEnv *env) {
|
||||||
|
static Api api;
|
||||||
|
api.tbl = table;
|
||||||
|
static T module;
|
||||||
|
ModuleBase *m = &module;
|
||||||
|
static module_abi abi(m);
|
||||||
|
if (!table->registerModule(table, &abi)) return;
|
||||||
|
m->onLoad(&api, env);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace internal
|
||||||
|
|
||||||
|
inline int Api::connectCompanion() {
|
||||||
|
return tbl->connectCompanion ? tbl->connectCompanion(tbl->impl) : -1;
|
||||||
|
}
|
||||||
|
inline int Api::getModuleDir() {
|
||||||
|
return tbl->getModuleDir ? tbl->getModuleDir(tbl->impl) : -1;
|
||||||
|
}
|
||||||
|
inline void Api::setOption(Option opt) {
|
||||||
|
if (tbl->setOption) tbl->setOption(tbl->impl, opt);
|
||||||
|
}
|
||||||
|
inline uint32_t Api::getFlags() {
|
||||||
|
return tbl->getFlags ? tbl->getFlags(tbl->impl) : 0;
|
||||||
|
}
|
||||||
|
inline bool Api::exemptFd(int fd) {
|
||||||
|
return tbl->exemptFd != nullptr && tbl->exemptFd(fd);
|
||||||
|
}
|
||||||
|
inline void Api::hookJniNativeMethods(JNIEnv *env, const char *className, JNINativeMethod *methods, int numMethods) {
|
||||||
|
if (tbl->hookJniNativeMethods) tbl->hookJniNativeMethods(env, className, methods, numMethods);
|
||||||
|
}
|
||||||
|
inline void Api::pltHookRegister(dev_t dev, ino_t inode, const char *symbol, void *newFunc, void **oldFunc) {
|
||||||
|
if (tbl->pltHookRegister) tbl->pltHookRegister(dev, inode, symbol, newFunc, oldFunc);
|
||||||
|
}
|
||||||
|
inline bool Api::pltHookCommit() {
|
||||||
|
return tbl->pltHookCommit != nullptr && tbl->pltHookCommit();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace zygisk
|
||||||
|
|
||||||
|
extern "C" {
|
||||||
|
|
||||||
|
[[gnu::visibility("default"), maybe_unused]]
|
||||||
|
void zygisk_module_entry(zygisk::internal::api_table *, JNIEnv *);
|
||||||
|
|
||||||
|
[[gnu::visibility("default"), maybe_unused]]
|
||||||
|
void zygisk_companion_entry(int);
|
||||||
|
|
||||||
|
} // extern "C"
|
||||||
Binary file not shown.
@@ -1,113 +1,88 @@
|
|||||||
#include <cstdlib>
|
|
||||||
#include <cstring>
|
|
||||||
#include <cstdio>
|
|
||||||
#include <dlfcn.h>
|
#include <dlfcn.h>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
#include <fcntl.h>
|
#include <fcntl.h>
|
||||||
#include <pthread.h>
|
#include <cstring>
|
||||||
|
#include <cstdio>
|
||||||
#include <android/log.h>
|
#include <android/log.h>
|
||||||
#include <sys/stat.h>
|
#include "zygisk.hpp"
|
||||||
#include <jni.h>
|
|
||||||
|
|
||||||
#define LOG_TAG "CameraHookZygisk"
|
#define LOG_TAG "CamSwapperZygisk"
|
||||||
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
#define ALOGI(...) __android_log_print(ANDROID_LOG_INFO, LOG_TAG, __VA_ARGS__)
|
||||||
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
#define ALOGE(...) __android_log_print(ANDROID_LOG_ERROR, LOG_TAG, __VA_ARGS__)
|
||||||
|
|
||||||
#define HOOK_LIB_PATH "/data/local/camera_magic/libcamera_hook.so"
|
#define HOOK_LIB_PATH "/data/local/camera_magic/libcamera_hook.so"
|
||||||
|
|
||||||
static void* g_hook_handle = nullptr;
|
using zygisk::Api;
|
||||||
static bool g_hook_loaded = false;
|
using zygisk::AppSpecializeArgs;
|
||||||
static bool g_in_zygote = false;
|
using zygisk::ServerSpecializeArgs;
|
||||||
|
|
||||||
static bool is_camera_process() {
|
static bool is_camera_process(const char *process_name) {
|
||||||
char cmdline[256] = {0};
|
if (!process_name) return false;
|
||||||
int fd = open("/proc/self/cmdline", O_RDONLY);
|
return (strstr(process_name, "cameraserver") != nullptr) ||
|
||||||
if (fd < 0) return false;
|
(strstr(process_name, "camera.provider") != nullptr) ||
|
||||||
ssize_t n = read(fd, cmdline, sizeof(cmdline) - 1);
|
(strstr(process_name, "android.hardware.camera") != nullptr);
|
||||||
close(fd);
|
|
||||||
if (n <= 0) return false;
|
|
||||||
return (strstr(cmdline, "cameraserver") != nullptr ||
|
|
||||||
strstr(cmdline, "camera.provider") != nullptr ||
|
|
||||||
strstr(cmdline, "android.hardware.camera") != nullptr);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static void load_hook() {
|
class CamSwapperModule : public zygisk::ModuleBase {
|
||||||
if (g_hook_loaded) return;
|
public:
|
||||||
g_hook_loaded = true;
|
void onLoad(Api *api, JNIEnv *env) override {
|
||||||
|
this->api = api;
|
||||||
|
this->env = env;
|
||||||
|
ALOGI("CamSwapper Zygisk module loaded");
|
||||||
|
}
|
||||||
|
|
||||||
|
void preAppSpecialize(AppSpecializeArgs *args) override {
|
||||||
|
if (args && args->nice_name) {
|
||||||
|
const char *process = env->GetStringUTFChars(args->nice_name, nullptr);
|
||||||
|
if (process) {
|
||||||
|
ALOGI("preAppSpecialize: %s", process);
|
||||||
|
env->ReleaseStringUTFChars(args->nice_name, process);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void postAppSpecialize(const AppSpecializeArgs *args) override {
|
||||||
|
if (args && args->nice_name) {
|
||||||
|
const char *process = env->GetStringUTFChars(args->nice_name, nullptr);
|
||||||
|
if (process) {
|
||||||
|
ALOGI("postAppSpecialize: %s", process);
|
||||||
|
if (is_camera_process(process)) {
|
||||||
|
ALOGI("Loading hook in camera app: %s", process);
|
||||||
|
load_hook_lib();
|
||||||
|
}
|
||||||
|
env->ReleaseStringUTFChars(args->nice_name, process);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void preServerSpecialize(ServerSpecializeArgs *args) override {
|
||||||
|
ALOGI("preServerSpecialize: system_server");
|
||||||
|
}
|
||||||
|
|
||||||
|
void postServerSpecialize(const ServerSpecializeArgs *args) override {
|
||||||
|
ALOGI("postServerSpecialize: system_server ready, loading hook");
|
||||||
|
load_hook_lib();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Api *api;
|
||||||
|
JNIEnv *env;
|
||||||
|
|
||||||
|
void load_hook_lib() {
|
||||||
struct stat st;
|
struct stat st;
|
||||||
if (stat(HOOK_LIB_PATH, &st) != 0) {
|
if (stat(HOOK_LIB_PATH, &st) != 0) {
|
||||||
ALOGE("Hook library not found: %s", HOOK_LIB_PATH);
|
ALOGE("Hook library not found: %s", HOOK_LIB_PATH);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
g_hook_handle = dlopen(HOOK_LIB_PATH, RTLD_NOW | RTLD_LOCAL);
|
|
||||||
if (!g_hook_handle) {
|
void *handle = dlopen(HOOK_LIB_PATH, RTLD_NOW | RTLD_LOCAL);
|
||||||
|
if (!handle) {
|
||||||
ALOGE("dlopen(%s) failed: %s", HOOK_LIB_PATH, dlerror());
|
ALOGE("dlopen(%s) failed: %s", HOOK_LIB_PATH, dlerror());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
ALOGI("Hook library loaded in PID %d", getpid());
|
|
||||||
}
|
|
||||||
|
|
||||||
static void post_fork_child() {
|
ALOGI("Hook library loaded: %s (handle=%p)", HOOK_LIB_PATH, handle);
|
||||||
if (is_camera_process()) {
|
|
||||||
ALOGI("Camera process detected (PID %d), loading hook", getpid());
|
|
||||||
load_hook();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
};
|
||||||
|
|
||||||
__attribute__((constructor))
|
REGISTER_ZYGISK_MODULE(CamSwapperModule)
|
||||||
static void zygisk_init() {
|
|
||||||
char cmdline[256] = {0};
|
|
||||||
int fd = open("/proc/self/cmdline", O_RDONLY);
|
|
||||||
if (fd >= 0) {
|
|
||||||
read(fd, cmdline, sizeof(cmdline) - 1);
|
|
||||||
close(fd);
|
|
||||||
}
|
|
||||||
if (strstr(cmdline, "zygote") || strstr(cmdline, "zygote64")) {
|
|
||||||
g_in_zygote = true;
|
|
||||||
ALOGI("Loaded in Zygote (PID %d), registering atfork", getpid());
|
|
||||||
pthread_atfork(nullptr, nullptr, post_fork_child);
|
|
||||||
} else if (is_camera_process()) {
|
|
||||||
ALOGI("Loaded directly in camera process (PID %d)", getpid());
|
|
||||||
load_hook();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
extern "C" {
|
|
||||||
|
|
||||||
__attribute__((visibility("default")))
|
|
||||||
void zygisk_module_entry(void* api, JNIEnv* env) {
|
|
||||||
(void)api;
|
|
||||||
(void)env;
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((visibility("default")))
|
|
||||||
void pre_app_specialize(void* module, void* args) {
|
|
||||||
(void)module;
|
|
||||||
(void)args;
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((visibility("default")))
|
|
||||||
void post_app_specialize(void* module, const void* args) {
|
|
||||||
(void)module;
|
|
||||||
(void)args;
|
|
||||||
if (is_camera_process()) {
|
|
||||||
ALOGI("Camera app detected via post_app_specialize (PID %d)", getpid());
|
|
||||||
load_hook();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((visibility("default")))
|
|
||||||
void pre_server_specialize(void* module, void* args) {
|
|
||||||
(void)module;
|
|
||||||
(void)args;
|
|
||||||
}
|
|
||||||
|
|
||||||
__attribute__((visibility("default")))
|
|
||||||
void post_server_specialize(void* module, const void* args) {
|
|
||||||
(void)module;
|
|
||||||
(void)args;
|
|
||||||
ALOGI("Server specialized (PID %d), loading hook", getpid());
|
|
||||||
load_hook();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user