Initial commit
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
package com.nothing.camera2magic
|
||||
|
||||
import android.content.Context
|
||||
|
||||
object GlobalState {
|
||||
@Volatile
|
||||
lateinit var appContext: Context
|
||||
@Volatile
|
||||
var packageName: String = ""
|
||||
@Volatile
|
||||
var activityCount = 0
|
||||
}
|
||||
+153
@@ -0,0 +1,153 @@
|
||||
package com.nothing.camera2magic
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.util.Log
|
||||
import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.activity.enableEdgeToEdge
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.verticalScroll
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import com.nothing.camera2magic.ui.theme.VirtualCameraXTheme
|
||||
import com.nothing.camera2magic.view.SettingsView
|
||||
import com.nothing.camera2magic.view.SpotlightView
|
||||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import com.nothing.camera2magic.viewmodel.ConfigRepository
|
||||
import com.nothing.camera2magic.viewmodel.LocalViewModelFactory
|
||||
import com.nothing.camera2magic.viewmodel.ViewModelFactory
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
|
||||
@OptIn(ExperimentalPermissionsApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
val prefs = getSharedPreferences("camera_magic_config", MODE_PRIVATE)
|
||||
|
||||
enableEdgeToEdge()
|
||||
setContent {
|
||||
val repository = remember { ConfigRepository(prefs) }
|
||||
val factory = remember { ViewModelFactory(application, repository) }
|
||||
VirtualCameraXTheme(dynamicColor = true) {
|
||||
CompositionLocalProvider(LocalViewModelFactory provides factory) {
|
||||
val permissions = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
||||
listOf(
|
||||
Manifest.permission.READ_MEDIA_IMAGES,
|
||||
Manifest.permission.READ_MEDIA_VIDEO
|
||||
)
|
||||
} else {
|
||||
listOf(Manifest.permission.READ_EXTERNAL_STORAGE)
|
||||
}
|
||||
|
||||
// 2. Create and remember permission state
|
||||
val permissionState = rememberMultiplePermissionsState(permissions)
|
||||
|
||||
Surface(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
color = MaterialTheme.colorScheme.background
|
||||
) {
|
||||
if(permissionState.allPermissionsGranted) {
|
||||
MainScreen()
|
||||
} else {
|
||||
PermissionRationaleScreen(
|
||||
onGrantPermissionClick = { permissionState.launchMultiplePermissionRequest() }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun PermissionRationaleScreen(onGrantPermissionClick: () -> Unit) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(16.dp),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Column(
|
||||
horizontalAlignment = Alignment.CenterHorizontally,
|
||||
verticalArrangement = Arrangement.Center
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.permission_rationale_title),
|
||||
style = MaterialTheme.typography.headlineSmall,
|
||||
fontWeight = FontWeight.Bold,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
Text(
|
||||
text = stringResource(R.string.permission_rationale_description),
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
textAlign = TextAlign.Center
|
||||
)
|
||||
Spacer(modifier = Modifier.height(24.dp))
|
||||
Button(onClick = onGrantPermissionClick) {
|
||||
Text(text = stringResource(R.string.grant_permission_button_name))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
private fun AppTopBar() {
|
||||
// Use CenterAlignedTopAppBar to ensure title content is centered
|
||||
CenterAlignedTopAppBar(
|
||||
title = {
|
||||
// Row layout is used to display icon and text side-by-side
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Image(
|
||||
painter = painterResource(R.drawable.icon),
|
||||
contentDescription = "App Logo",
|
||||
modifier = Modifier.size(24.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(8.dp))
|
||||
Text(
|
||||
text = "Cam2 Magic",
|
||||
fontSize = 20.sp,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
@Composable
|
||||
private fun MainScreen() {
|
||||
val scrollState = rememberScrollState()
|
||||
|
||||
Scaffold(topBar = { AppTopBar() },
|
||||
containerColor = MaterialTheme.colorScheme.background,
|
||||
modifier = Modifier.statusBarsPadding()
|
||||
) { innerPadding ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(innerPadding)
|
||||
.padding(horizontal = 16.dp)
|
||||
.verticalScroll(scrollState),
|
||||
verticalArrangement = Arrangement.spacedBy(20.dp) // Use Arrangement for uniform spacing
|
||||
) {
|
||||
SpotlightView()
|
||||
SettingsView()
|
||||
Spacer(modifier = Modifier.height(0.dp)) // Optional bottom spacing
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package com.nothing.camera2magic.hook
|
||||
|
||||
import android.graphics.ImageFormat
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.media.ImageReader
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.view.Surface
|
||||
import com.nothing.camera2magic.utils.Dog
|
||||
import java.util.WeakHashMap
|
||||
|
||||
data class BlackHole(
|
||||
val identityId: Int,
|
||||
val surface: Surface,
|
||||
val texture: SurfaceTexture
|
||||
)
|
||||
private const val TAG = "[BlackHole]"
|
||||
object BlackHoleMapper {
|
||||
private val oabMap = WeakHashMap<Surface, BlackHole>()
|
||||
fun createBlackHole(origin: Surface): Surface {
|
||||
return oabMap.getOrPut(origin) {
|
||||
val id = 20 + oabMap.size
|
||||
val (w, h, f) = NativeBridge.getSurfaceInfo(origin)
|
||||
val targetW = if (w > 0) w else 1280
|
||||
val targetH = if (h > 0) h else 720
|
||||
|
||||
Dog.i(TAG, "Creating proxy SurfaceTexture for $targetW x $targetH", true)
|
||||
val texture = SurfaceTexture(0).apply {
|
||||
detachFromGLContext()
|
||||
setDefaultBufferSize(targetW, targetH)
|
||||
}
|
||||
val proxySurface = Surface(texture)
|
||||
BlackHole(id, proxySurface, texture)
|
||||
}.surface
|
||||
}
|
||||
|
||||
fun getBlackHole(origin: Surface): Surface? {
|
||||
return oabMap[origin]?.surface
|
||||
}
|
||||
|
||||
fun clearAll() {
|
||||
oabMap.values.forEach {
|
||||
it.surface.release()
|
||||
it.texture.release()
|
||||
}
|
||||
oabMap.clear()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
package com.nothing.camera2magic.hook
|
||||
import android.view.Surface
|
||||
import android.hardware.camera2.CameraDevice
|
||||
|
||||
data class CameraState(
|
||||
var packageName: String = "",
|
||||
var apiLevel: Int = 0,
|
||||
var facingFront: Boolean = false,
|
||||
var pictureWidth: Int = 0,
|
||||
var pictureHeight: Int = 0,
|
||||
var previewWidth: Int = 0,
|
||||
var previewHeight: Int = 0,
|
||||
var sensorOrientation: Int = 90,
|
||||
var displayOrientation: Int = 0,
|
||||
val surfaces: MutableSet<Surface> = mutableSetOf()
|
||||
)
|
||||
@@ -0,0 +1,117 @@
|
||||
package com.nothing.camera2magic.hook
|
||||
|
||||
import android.util.Log
|
||||
import java.io.File
|
||||
|
||||
private const val TAG = "HalConfigManager"
|
||||
private const val CONFIG_DIR = "/data/local/camera_magic"
|
||||
private const val CONFIG_FILE = "$CONFIG_DIR/config.txt"
|
||||
|
||||
object HalConfigManager {
|
||||
|
||||
fun isRootAvailable(): Boolean {
|
||||
return try {
|
||||
val process = Runtime.getRuntime().exec("su -c id")
|
||||
val exitCode = process.waitFor()
|
||||
exitCode == 0
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Root check failed", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun writeConfig(
|
||||
enabled: Boolean,
|
||||
sourceMode: String,
|
||||
videoPath: String,
|
||||
rtspUrl: String
|
||||
): Boolean {
|
||||
if (!isRootAvailable()) {
|
||||
Log.e(TAG, "Root access not available, cannot write HAL config")
|
||||
return false
|
||||
}
|
||||
|
||||
val configContent = buildConfigContent(enabled, sourceMode, videoPath, rtspUrl)
|
||||
|
||||
return try {
|
||||
val mkdirProcess = Runtime.getRuntime().exec("su -c mkdir -p $CONFIG_DIR")
|
||||
mkdirProcess.waitFor()
|
||||
|
||||
val tempFile = File.createTempFile("camera_magic_config", ".txt")
|
||||
tempFile.writeText(configContent)
|
||||
|
||||
val copyProcess = Runtime.getRuntime().exec(
|
||||
"su -c cp ${tempFile.absolutePath} $CONFIG_FILE"
|
||||
)
|
||||
val copyExit = copyProcess.waitFor()
|
||||
tempFile.delete()
|
||||
|
||||
if (copyExit == 0) {
|
||||
val chmodProcess = Runtime.getRuntime().exec("su -c chmod 644 $CONFIG_FILE")
|
||||
chmodProcess.waitFor()
|
||||
Log.i(TAG, "HAL config written successfully")
|
||||
true
|
||||
} else {
|
||||
Log.e(TAG, "Failed to copy config file, exit code: $copyExit")
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to write HAL config", e)
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fun readConfig(): HalConfig {
|
||||
return try {
|
||||
val process = Runtime.getRuntime().exec("su -c cat $CONFIG_FILE")
|
||||
val exitCode = process.waitFor()
|
||||
if (exitCode == 0) {
|
||||
val content = process.inputStream.bufferedReader().readText()
|
||||
parseConfigContent(content)
|
||||
} else {
|
||||
HalConfig()
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.w(TAG, "Failed to read HAL config", e)
|
||||
HalConfig()
|
||||
}
|
||||
}
|
||||
|
||||
private fun buildConfigContent(
|
||||
enabled: Boolean,
|
||||
sourceMode: String,
|
||||
videoPath: String,
|
||||
rtspUrl: String
|
||||
): String {
|
||||
return buildString {
|
||||
appendLine("enabled=${if (enabled) "1" else "0"}")
|
||||
appendLine("source_mode=$sourceMode")
|
||||
appendLine("video_path=$videoPath")
|
||||
appendLine("rtsp_url=$rtspUrl")
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseConfigContent(content: String): HalConfig {
|
||||
val config = HalConfig()
|
||||
for (line in content.lineSequence()) {
|
||||
val trimmed = line.trim()
|
||||
if (trimmed.isEmpty() || trimmed.startsWith('#')) continue
|
||||
val parts = trimmed.split('=', limit = 2)
|
||||
if (parts.size != 2) continue
|
||||
when (parts[0]) {
|
||||
"enabled" -> config.enabled = parts[1] == "1"
|
||||
"source_mode" -> config.sourceMode = parts[1]
|
||||
"video_path" -> config.videoPath = parts[1]
|
||||
"rtsp_url" -> config.rtspUrl = parts[1]
|
||||
}
|
||||
}
|
||||
return config
|
||||
}
|
||||
|
||||
data class HalConfig(
|
||||
var enabled: Boolean = false,
|
||||
var sourceMode: String = "file",
|
||||
var videoPath: String = "/data/local/camera_magic/video.mp4",
|
||||
var rtspUrl: String = ""
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
@file:Suppress("DEPRECATION")
|
||||
|
||||
package com.nothing.camera2magic.hook
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import android.hardware.Camera
|
||||
import android.view.Surface
|
||||
import com.nothing.camera2magic.utils.Dog
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.WeakHashMap
|
||||
|
||||
object NativeBridge {
|
||||
private const val TAG = "[Bridge]"
|
||||
@Volatile
|
||||
private var lastRegisteredSurface: WeakReference<Surface>? = null
|
||||
|
||||
private val surfaceLock = Any()
|
||||
@Volatile
|
||||
private var cachedBuffer: ByteArray? = null
|
||||
|
||||
@Volatile
|
||||
var currentCamera: WeakReference<Camera>? = null
|
||||
@Volatile
|
||||
var previewCallback: WeakReference<Camera.PreviewCallback>? = null
|
||||
|
||||
@JvmStatic
|
||||
fun ensureBuffer(size: Int): ByteArray {
|
||||
if (cachedBuffer != null && cachedBuffer!!.size == size) {
|
||||
return cachedBuffer!!
|
||||
}
|
||||
return ByteArray(size).also { cachedBuffer = it }
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun frameUpdated(width: Int, height: Int) {
|
||||
val buffer = cachedBuffer ?: return
|
||||
val expectedSize = width * height * 3 / 2
|
||||
if (buffer.size < expectedSize) return
|
||||
runCatching {
|
||||
val camera = currentCamera?.get() ?: return
|
||||
val callback = previewCallback?.get() ?: return
|
||||
callback.onPreviewFrame(buffer, camera)
|
||||
}
|
||||
}
|
||||
@JvmStatic
|
||||
external fun updateGlobalConfig(playSound: Boolean, enableLog: Boolean)
|
||||
@JvmStatic
|
||||
external fun registerSurface(cameraState: CameraState)
|
||||
@JvmStatic
|
||||
external fun updateManualRotation(rotation: Int)
|
||||
@JvmStatic
|
||||
external fun setDisplayOrientation(orientation: Int)
|
||||
@JvmStatic
|
||||
external fun getSurfaceInfo(surface: Surface): IntArray
|
||||
@JvmStatic
|
||||
external fun resetMediaSource()
|
||||
@JvmStatic
|
||||
external fun processVideo(fd: Int, offset: Long, length: Long): Boolean
|
||||
@JvmStatic
|
||||
external fun processBitmap(bitmap: Bitmap): Boolean
|
||||
@JvmStatic
|
||||
external fun needStopRenderer()
|
||||
@JvmStatic
|
||||
external fun needStartRenderer()
|
||||
@JvmStatic
|
||||
external fun overwritePreviewBuffer(originBuffer: ByteArray)
|
||||
@JvmStatic
|
||||
external fun overwriteJPEGBytes(quality: Int = 90): ByteArray
|
||||
|
||||
fun registerSurfaceIfNew(state: CameraState, forceRefresh: Boolean = false) {
|
||||
synchronized(surfaceLock) {
|
||||
val lastSurface = lastRegisteredSurface?.get()
|
||||
val currentSurface = state.surfaces.firstOrNull()
|
||||
currentSurface?.let { surface ->
|
||||
if (forceRefresh || surface != lastSurface) {
|
||||
registerSurface(cameraState = state)
|
||||
lastRegisteredSurface = WeakReference(surface)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun releaseLastRegisteredSurface() {
|
||||
synchronized(surfaceLock) {
|
||||
lastRegisteredSurface = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,224 @@
|
||||
package com.nothing.camera2magic.hook
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.BitmapFactory
|
||||
import android.provider.MediaStore
|
||||
import com.nothing.camera2magic.GlobalState
|
||||
import com.nothing.camera2magic.utils.Dog
|
||||
import java.io.FileNotFoundException
|
||||
|
||||
|
||||
object SourceManager {
|
||||
private const val TAG = "[MediaSource]"
|
||||
private const val LOCAL_MEDIA_TYPE_VIDEO = 0x0000
|
||||
private const val LOCAL_MEDIA_TYPE_IMAGE = 0x0001
|
||||
private const val NETWORK_MEDIA_TYPE_RTSP = 0x0100
|
||||
private const val KEY_MODULE_ENABLED = "main_module_enabled"
|
||||
private const val KEY_PLAY_SOUND = "main_play_sound"
|
||||
private const val KEY_ENABLE_LOG = "main_enable_log"
|
||||
private const val KEY_MEDIA_SOURCE = "media_source" // 0: local, 1: network
|
||||
private const val KEY_LOCAL_MEDIA_TYPE = "local_media_type" // 0: video, 1: image
|
||||
private const val KEY_LOCAL_VIDEO_ID = "local_video_id"
|
||||
private const val KEY_LOCAL_IMAGE_ID = "local_image_id"
|
||||
private const val KEY_NETWORK_RTSP_URI = "network_rtsp_uri"
|
||||
|
||||
private lateinit var prefs: SharedPreferences
|
||||
|
||||
private var lastMediaFingerprint: String = ""
|
||||
@Volatile
|
||||
var moduleEnabled: Boolean = true
|
||||
private set
|
||||
@Volatile
|
||||
private var playSound: Boolean = false
|
||||
@Volatile
|
||||
var enableLog: Boolean = false
|
||||
private set
|
||||
@Volatile
|
||||
private var mediaSource: Int = 0
|
||||
@Volatile
|
||||
private var mediaType: Int = 0
|
||||
@Volatile
|
||||
private var selectedMedia: Int = 0x0000
|
||||
@Volatile
|
||||
var toastMessage: String? = null
|
||||
@Volatile
|
||||
private var videoId: Long = -1L
|
||||
@Volatile
|
||||
private var imageId: Long = -1L
|
||||
@Volatile
|
||||
private var rtspUri: String = ""
|
||||
|
||||
@Volatile
|
||||
var mediaIsReady: Boolean = false
|
||||
private set
|
||||
|
||||
@Volatile
|
||||
var skipNativeDispatch: Boolean = false
|
||||
|
||||
fun init(remotePrefs: SharedPreferences) {
|
||||
this.prefs = remotePrefs
|
||||
refreshPrefs()
|
||||
}
|
||||
|
||||
fun refreshAndDispatch() {
|
||||
refreshPrefs()
|
||||
if (!moduleEnabled) {
|
||||
updateState(false, "Module disabled")
|
||||
return
|
||||
}
|
||||
val fingerprint = getMediaFingerprint()
|
||||
if (fingerprint != lastMediaFingerprint || !mediaIsReady) {
|
||||
Dog.i(TAG, "dispatching media: $fingerprint (ready=$mediaIsReady)", true)
|
||||
if (skipNativeDispatch) {
|
||||
mediaIsReady = true
|
||||
lastMediaFingerprint = fingerprint
|
||||
updateState(true, "Video ready (skipNativeDispatch)")
|
||||
} else {
|
||||
dispatchMediaSourceToNative()
|
||||
lastMediaFingerprint = fingerprint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun refreshPrefs() {
|
||||
try {
|
||||
if (!::prefs.isInitialized) return
|
||||
moduleEnabled = prefs.getBoolean(KEY_MODULE_ENABLED, true)
|
||||
playSound = prefs.getBoolean(KEY_PLAY_SOUND, false)
|
||||
enableLog = prefs.getBoolean(KEY_ENABLE_LOG, false)
|
||||
|
||||
mediaSource = prefs.getInt(KEY_MEDIA_SOURCE, 0)
|
||||
mediaType = prefs.getInt(KEY_LOCAL_MEDIA_TYPE, 0)
|
||||
selectedMedia = (mediaSource shl 8) or mediaType
|
||||
|
||||
videoId = prefs.getLong(KEY_LOCAL_VIDEO_ID, -1L)
|
||||
imageId = prefs.getLong(KEY_LOCAL_IMAGE_ID, -1L)
|
||||
rtspUri = prefs.getString(KEY_NETWORK_RTSP_URI, "") ?: ""
|
||||
|
||||
Dog.i(TAG, "refreshPrefs: enabled=$moduleEnabled, log=$enableLog, source=$mediaSource, type=$mediaType, selected=$selectedMedia, vId=$videoId", true)
|
||||
NativeBridge.updateGlobalConfig(playSound, enableLog)
|
||||
|
||||
} catch (e: Exception) {
|
||||
Dog.e(TAG, "refreshPrefs failed", e, true)
|
||||
}
|
||||
}
|
||||
|
||||
fun isReadyForHook(): Boolean = true // Force true for debugging
|
||||
|
||||
fun getVideoId(): Long = videoId
|
||||
|
||||
private fun dispatchMediaSourceToNative() {
|
||||
mediaIsReady = false
|
||||
when (selectedMedia) {
|
||||
LOCAL_MEDIA_TYPE_VIDEO -> { updateVideoSource() }
|
||||
LOCAL_MEDIA_TYPE_IMAGE -> { updateImageSource() }
|
||||
NETWORK_MEDIA_TYPE_RTSP -> { }
|
||||
}
|
||||
}
|
||||
|
||||
private fun getMediaFingerprint(): String {
|
||||
return when (selectedMedia) {
|
||||
0x0000 -> "$selectedMedia:$videoId"
|
||||
0x0001 -> "$selectedMedia:$imageId"
|
||||
0x0100 -> "$selectedMedia:$rtspUri"
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateVideoSource() {
|
||||
if (videoId == -1L) {
|
||||
NativeBridge.resetMediaSource()
|
||||
updateState(false, "Video not set")
|
||||
return
|
||||
}
|
||||
val contentResolver = GlobalState.appContext.contentResolver
|
||||
val uri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, videoId)
|
||||
|
||||
val result = runCatching {
|
||||
val afd = contentResolver.openAssetFileDescriptor(uri, "r")
|
||||
?: throw FileNotFoundException("Unable to open video: $uri")
|
||||
afd.use { it ->
|
||||
NativeBridge.processVideo(
|
||||
it.parcelFileDescriptor.fd,
|
||||
it.startOffset,
|
||||
it.length
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
result.onSuccess { success ->
|
||||
val msg = if (success) "Video ready" else "Video receiving error (Native)"
|
||||
updateState(success, msg)
|
||||
}.onFailure { e ->
|
||||
val msg = when(e) {
|
||||
is SecurityException -> "No permission to read video"
|
||||
else -> "Image transmission error (Java IO)"
|
||||
}
|
||||
updateState(false, msg)
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateImageSource() {
|
||||
if (imageId == -1L) {
|
||||
NativeBridge.resetMediaSource()
|
||||
updateState(false, "Image not set")
|
||||
return
|
||||
}
|
||||
|
||||
val uri = ContentUris.withAppendedId(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, imageId)
|
||||
val contentResolver = GlobalState.appContext.contentResolver
|
||||
val result = runCatching {
|
||||
val options = BitmapFactory.Options().apply {
|
||||
inJustDecodeBounds = true
|
||||
contentResolver.openInputStream(uri)?.use {
|
||||
BitmapFactory.decodeStream(it, null, this)
|
||||
}
|
||||
}
|
||||
|
||||
options.inJustDecodeBounds = false
|
||||
options.inPreferredConfig = Bitmap.Config.ARGB_8888
|
||||
options.inSampleSize = calculateInSampleSize(options, 1080, 1920)
|
||||
|
||||
val bitmap = contentResolver.openInputStream(uri)?.use { stream ->
|
||||
BitmapFactory.decodeStream(stream, null, options)
|
||||
} ?: throw IllegalStateException("Unable to decode image")
|
||||
|
||||
try {
|
||||
NativeBridge.processBitmap(bitmap)
|
||||
} finally {
|
||||
bitmap.recycle()
|
||||
}
|
||||
}
|
||||
|
||||
result.onSuccess { success ->
|
||||
val msg = if (success) "Image ready" else "Image receiving failed (Native)"
|
||||
updateState(success, msg)
|
||||
}.onFailure { e ->
|
||||
val msg = when (e) {
|
||||
is SecurityException -> "No permission to read image"
|
||||
else -> "Image transmission error (Java IO)"
|
||||
}
|
||||
updateState(false, msg)
|
||||
}
|
||||
}
|
||||
|
||||
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int, reqHeight: Int): Int {
|
||||
val (height: Int, width: Int) = options.outHeight to options.outWidth
|
||||
var inSampleSize = 1
|
||||
if (height > reqHeight || width > reqWidth) {
|
||||
val halfHeight = height / 2
|
||||
val halfWidth = width / 2
|
||||
while (halfHeight / inSampleSize >= reqHeight && halfWidth / inSampleSize >= reqWidth) {
|
||||
inSampleSize *= 2
|
||||
}
|
||||
}
|
||||
return inSampleSize
|
||||
}
|
||||
|
||||
private fun updateState(ready: Boolean, message: String) {
|
||||
mediaIsReady = ready
|
||||
toastMessage = message
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package com.nothing.camera2magic.hook
|
||||
|
||||
import android.content.ContentUris
|
||||
import android.provider.MediaStore
|
||||
import androidx.media3.common.MediaItem
|
||||
import androidx.media3.common.Player
|
||||
import androidx.media3.exoplayer.ExoPlayer
|
||||
import com.nothing.camera2magic.GlobalState
|
||||
import com.nothing.camera2magic.utils.Dog
|
||||
import android.view.Surface
|
||||
|
||||
object VideoPusher {
|
||||
private const val TAG = "VideoPusher"
|
||||
private var exoPlayer: ExoPlayer? = null
|
||||
private var renderer: VirtualCameraRenderer? = null
|
||||
|
||||
fun start(targetSurfaces: Collection<Surface>, width: Int, height: Int, videoId: Long) {
|
||||
Dog.i(TAG, "Starting VideoPusher for videoId: $videoId on ${targetSurfaces.size} surfaces", true)
|
||||
|
||||
val context = GlobalState.appContext
|
||||
val videoUri = ContentUris.withAppendedId(MediaStore.Video.Media.EXTERNAL_CONTENT_URI, videoId)
|
||||
|
||||
// Dynamically detect video resolution
|
||||
val retriever = android.media.MediaMetadataRetriever()
|
||||
var vWidth = 0
|
||||
var vHeight = 0
|
||||
try {
|
||||
retriever.setDataSource(context, videoUri)
|
||||
vWidth = retriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_WIDTH)?.toInt() ?: 0
|
||||
vHeight = retriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_HEIGHT)?.toInt() ?: 0
|
||||
val rotation = retriever.extractMetadata(android.media.MediaMetadataRetriever.METADATA_KEY_VIDEO_ROTATION)?.toInt() ?: 0
|
||||
if (rotation == 90 || rotation == 270) {
|
||||
val temp = vWidth
|
||||
vWidth = vHeight
|
||||
vHeight = temp
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Dog.e(TAG, "Failed to extract video metadata: ${e.message}", e, true)
|
||||
} finally {
|
||||
retriever.release()
|
||||
}
|
||||
|
||||
if (renderer == null) {
|
||||
renderer = VirtualCameraRenderer()
|
||||
}
|
||||
|
||||
renderer?.setTargetSurfaces(targetSurfaces, width, height)
|
||||
if (vWidth > 0 && vHeight > 0) {
|
||||
renderer?.setVideoSize(vWidth, vHeight)
|
||||
}
|
||||
|
||||
if (exoPlayer == null) {
|
||||
exoPlayer = ExoPlayer.Builder(context).build().apply {
|
||||
repeatMode = Player.REPEAT_MODE_ALL
|
||||
playWhenReady = true
|
||||
}
|
||||
}
|
||||
|
||||
exoPlayer?.apply {
|
||||
setMediaItem(MediaItem.fromUri(videoUri))
|
||||
setVideoSurface(renderer?.inputSurface)
|
||||
addListener(object : Player.Listener {
|
||||
override fun onPlayerError(error: androidx.media3.common.PlaybackException) {
|
||||
Dog.e(TAG, "CRITICAL: ExoPlayer failed to play video. Error: ${error.message}", error, true)
|
||||
}
|
||||
override fun onPlaybackStateChanged(state: Int) {
|
||||
val stateStr = when(state) {
|
||||
Player.STATE_IDLE -> "IDLE"
|
||||
Player.STATE_BUFFERING -> "BUFFERING"
|
||||
Player.STATE_READY -> "READY"
|
||||
Player.STATE_ENDED -> "ENDED"
|
||||
else -> "UNKNOWN"
|
||||
}
|
||||
Dog.i(TAG, "ExoPlayer State: $stateStr", true)
|
||||
}
|
||||
})
|
||||
prepare()
|
||||
play()
|
||||
}
|
||||
}
|
||||
|
||||
fun stop() {
|
||||
Dog.i(TAG, "Stopping VideoPusher")
|
||||
exoPlayer?.stop()
|
||||
renderer?.setTargetSurfaces(emptyList(), 0, 0)
|
||||
}
|
||||
|
||||
fun release() {
|
||||
exoPlayer?.release()
|
||||
exoPlayer = null
|
||||
renderer?.release()
|
||||
renderer = null
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,387 @@
|
||||
package com.nothing.camera2magic.hook
|
||||
|
||||
import android.graphics.SurfaceTexture
|
||||
import android.opengl.*
|
||||
import android.opengl.EGL14.*
|
||||
import android.opengl.GLES11Ext.GL_TEXTURE_EXTERNAL_OES
|
||||
import android.opengl.GLES20.*
|
||||
import android.os.Handler
|
||||
import android.os.HandlerThread
|
||||
import android.view.Surface
|
||||
import com.nothing.camera2magic.utils.Dog
|
||||
import java.nio.ByteBuffer
|
||||
import java.nio.ByteOrder
|
||||
import java.nio.FloatBuffer
|
||||
|
||||
class VirtualCameraRenderer : SurfaceTexture.OnFrameAvailableListener {
|
||||
companion object {
|
||||
private const val TAG = "VCR"
|
||||
}
|
||||
|
||||
private val renderThread = HandlerThread("CameraRenderThread").apply { start() }
|
||||
private val renderHandler = Handler(renderThread.looper)
|
||||
|
||||
private var eglDisplay: EGLDisplay = EGL_NO_DISPLAY
|
||||
private var eglContext: EGLContext = EGL_NO_CONTEXT
|
||||
private var eglSurface: EGLSurface = EGL_NO_SURFACE
|
||||
private var eglConfig: EGLConfig? = null
|
||||
|
||||
private var surfaceTexture: SurfaceTexture? = null
|
||||
private var textureId: Int = -1
|
||||
private var programId: Int = -1
|
||||
private var uMVPMatrixHandle: Int = -1
|
||||
private var uSTMatrixHandle: Int = -1
|
||||
private var aPositionHandle: Int = -1
|
||||
private var aTextureCoordHandle: Int = -1
|
||||
|
||||
private val mvpMatrix = FloatArray(16)
|
||||
private val stMatrix = FloatArray(16)
|
||||
|
||||
@Volatile
|
||||
var inputSurface: Surface? = null
|
||||
private set
|
||||
|
||||
private val targetSurfaces = mutableListOf<EGLSurface>()
|
||||
private val surfaceSizes = mutableMapOf<EGLSurface, Pair<Int, Int>>()
|
||||
private val surfaceFormats = mutableMapOf<EGLSurface, Int>()
|
||||
private val imageWriters = mutableMapOf<Surface, android.media.ImageWriter>()
|
||||
private var primaryWidth = 0
|
||||
private var primaryHeight = 0
|
||||
private var videoWidth = 0
|
||||
private var videoHeight = 0
|
||||
|
||||
private val vertexShaderCode = """
|
||||
uniform mat4 uMVPMatrix;
|
||||
uniform mat4 uSTMatrix;
|
||||
attribute vec4 aPosition;
|
||||
attribute vec4 aTextureCoord;
|
||||
varying vec2 vTextureCoord;
|
||||
void main() {
|
||||
gl_Position = uMVPMatrix * aPosition;
|
||||
vTextureCoord = (uSTMatrix * aTextureCoord).xy;
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
private val fragmentShaderCode = """
|
||||
#extension GL_OES_EGL_image_external : require
|
||||
precision mediump float;
|
||||
varying vec2 vTextureCoord;
|
||||
uniform samplerExternalOES sTexture;
|
||||
void main() {
|
||||
gl_FragColor = texture2D(sTexture, vTextureCoord);
|
||||
}
|
||||
""".trimIndent()
|
||||
|
||||
private val vertexBuffer: FloatBuffer = ByteBuffer.allocateDirect(16 * 4)
|
||||
.order(ByteOrder.nativeOrder())
|
||||
.asFloatBuffer()
|
||||
.apply {
|
||||
put(floatArrayOf(
|
||||
-1.0f, -1.0f, 0.0f, 1.0f,
|
||||
1.0f, -1.0f, 0.0f, 1.0f,
|
||||
-1.0f, 1.0f, 0.0f, 1.0f,
|
||||
1.0f, 1.0f, 0.0f, 1.0f
|
||||
))
|
||||
position(0)
|
||||
}
|
||||
|
||||
private val textureBuffer: FloatBuffer = ByteBuffer.allocateDirect(16 * 4)
|
||||
.order(ByteOrder.nativeOrder())
|
||||
.asFloatBuffer()
|
||||
.apply {
|
||||
put(floatArrayOf(
|
||||
0.0f, 0.0f, 0.0f, 1.0f,
|
||||
1.0f, 0.0f, 0.0f, 1.0f,
|
||||
0.0f, 1.0f, 0.0f, 1.0f,
|
||||
1.0f, 1.0f, 0.0f, 1.0f
|
||||
))
|
||||
position(0)
|
||||
}
|
||||
|
||||
private var pbufferSurface: EGLSurface = EGL_NO_SURFACE
|
||||
private var pixelBuffer: ByteBuffer? = null
|
||||
|
||||
init {
|
||||
Matrix.setIdentityM(mvpMatrix, 0)
|
||||
Matrix.setIdentityM(stMatrix, 0)
|
||||
renderHandler.post { setupEGL() }
|
||||
}
|
||||
|
||||
fun setTargetSurfaces(surfaces: Collection<Surface>, width: Int, height: Int) {
|
||||
renderHandler.post {
|
||||
Dog.i(TAG, "setTargetSurfaces: count=${surfaces.size}, ref_w=$width, ref_h=$height", true)
|
||||
|
||||
// Clean up existing surfaces
|
||||
targetSurfaces.forEach { eglDestroySurface(eglDisplay, it) }
|
||||
targetSurfaces.clear()
|
||||
surfaceSizes.clear()
|
||||
surfaceFormats.clear()
|
||||
|
||||
primaryWidth = width
|
||||
primaryHeight = height
|
||||
updateMVPMatrix()
|
||||
|
||||
surfaces.forEach { surface ->
|
||||
if (surface.isValid) {
|
||||
val (w, h, f) = NativeBridge.getSurfaceInfo(surface)
|
||||
|
||||
// If it's a YUV/JPEG capture surface, we use ImageWriter instead of EGL
|
||||
if (f == 34 || f == 35 || f == 33) {
|
||||
Dog.i(TAG, "Creating ImageWriter for YUV/JPEG surface (format $f)", true)
|
||||
try {
|
||||
val writer = android.media.ImageWriter.newInstance(surface, 2)
|
||||
imageWriters[surface] = writer
|
||||
} catch (e: Exception) {
|
||||
Dog.e(TAG, "Failed to create ImageWriter for format $f", e, true)
|
||||
}
|
||||
return@forEach
|
||||
}
|
||||
|
||||
val surfaceAttribs = intArrayOf(EGL_NONE)
|
||||
val eglSurface = eglCreateWindowSurface(eglDisplay, eglConfig, surface, surfaceAttribs, 0)
|
||||
if (eglSurface != EGL_NO_SURFACE) {
|
||||
targetSurfaces.add(eglSurface)
|
||||
surfaceSizes[eglSurface] = Pair(w, h)
|
||||
surfaceFormats[eglSurface] = f
|
||||
Dog.i(TAG, "Added EGLSurface for $surface ($w x $h, format $f)", true)
|
||||
} else {
|
||||
val error = eglGetError()
|
||||
Dog.e(TAG, "eglCreateWindowSurface failed for $surface: ${GLUtils.getEGLErrorString(error)}", null, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateMVPMatrix() {
|
||||
if (primaryWidth <= 0 || primaryHeight <= 0 || videoWidth <= 0 || videoHeight <= 0) {
|
||||
Matrix.setIdentityM(mvpMatrix, 0)
|
||||
return
|
||||
}
|
||||
val surfaceRatio = primaryWidth.toFloat() / primaryHeight
|
||||
val videoRatio = videoWidth.toFloat() / videoHeight
|
||||
|
||||
Matrix.setIdentityM(mvpMatrix, 0)
|
||||
if (videoRatio > surfaceRatio) {
|
||||
// Video is wider than surface - letterbox top/bottom
|
||||
Matrix.scaleM(mvpMatrix, 0, 1.0f, surfaceRatio / videoRatio, 1.0f)
|
||||
} else {
|
||||
// Video is narrower than surface - letterbox sides
|
||||
Matrix.scaleM(mvpMatrix, 0, videoRatio / surfaceRatio, 1.0f, 1.0f)
|
||||
}
|
||||
Dog.i(TAG, "MVP updated: sRatio=$surfaceRatio, vRatio=$videoRatio", true)
|
||||
}
|
||||
|
||||
private fun setupEGL() {
|
||||
eglDisplay = eglGetDisplay(EGL_DEFAULT_DISPLAY)
|
||||
val version = IntArray(2)
|
||||
eglInitialize(eglDisplay, version, 0, version, 1)
|
||||
|
||||
val attribList = intArrayOf(
|
||||
EGL_RED_SIZE, 8,
|
||||
EGL_GREEN_SIZE, 8,
|
||||
EGL_BLUE_SIZE, 8,
|
||||
EGL_RENDERABLE_TYPE, EGL_OPENGL_ES2_BIT,
|
||||
EGL_NONE
|
||||
)
|
||||
val configs = arrayOfNulls<EGLConfig>(1)
|
||||
val numConfigs = IntArray(1)
|
||||
eglChooseConfig(eglDisplay, attribList, 0, configs, 0, configs.size, numConfigs, 0)
|
||||
eglConfig = configs[0]
|
||||
|
||||
val contextAttribs = intArrayOf(EGL_CONTEXT_CLIENT_VERSION, 2, EGL_NONE)
|
||||
eglContext = eglCreateContext(eglDisplay, eglConfig, EGL_NO_CONTEXT, contextAttribs, 0)
|
||||
|
||||
val pbufferAttribs = intArrayOf(EGL_WIDTH, 1, EGL_HEIGHT, 1, EGL_NONE)
|
||||
pbufferSurface = eglCreatePbufferSurface(eglDisplay, eglConfig, pbufferAttribs, 0)
|
||||
eglMakeCurrent(eglDisplay, pbufferSurface, pbufferSurface, eglContext)
|
||||
|
||||
setupShaders()
|
||||
setupTexture()
|
||||
|
||||
surfaceTexture = SurfaceTexture(textureId).apply {
|
||||
setOnFrameAvailableListener(this@VirtualCameraRenderer, renderHandler)
|
||||
}
|
||||
inputSurface = Surface(surfaceTexture)
|
||||
Dog.i(TAG, "EGL Setup complete. inputSurface ready.", true)
|
||||
}
|
||||
|
||||
private fun setupShaders() {
|
||||
val vertexShader = loadShader(GL_VERTEX_SHADER, vertexShaderCode)
|
||||
val fragmentShader = loadShader(GL_FRAGMENT_SHADER, fragmentShaderCode)
|
||||
programId = glCreateProgram().also {
|
||||
glAttachShader(it, vertexShader)
|
||||
glAttachShader(it, fragmentShader)
|
||||
glLinkProgram(it)
|
||||
}
|
||||
|
||||
uMVPMatrixHandle = glGetUniformLocation(programId, "uMVPMatrix")
|
||||
uSTMatrixHandle = glGetUniformLocation(programId, "uSTMatrix")
|
||||
aPositionHandle = glGetAttribLocation(programId, "aPosition")
|
||||
aTextureCoordHandle = glGetAttribLocation(programId, "aTextureCoord")
|
||||
}
|
||||
|
||||
private fun setupTexture() {
|
||||
val textures = IntArray(1)
|
||||
glGenTextures(1, textures, 0)
|
||||
textureId = textures[0]
|
||||
glBindTexture(GL_TEXTURE_EXTERNAL_OES, textureId)
|
||||
glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MIN_FILTER, GL_LINEAR.toFloat())
|
||||
glTexParameterf(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_MAG_FILTER, GL_LINEAR.toFloat())
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE)
|
||||
glTexParameteri(GL_TEXTURE_EXTERNAL_OES, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE)
|
||||
}
|
||||
|
||||
private fun loadShader(type: Int, shaderCode: String): Int {
|
||||
return glCreateShader(type).also {
|
||||
glShaderSource(it, shaderCode)
|
||||
glCompileShader(it)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onFrameAvailable(st: SurfaceTexture) {
|
||||
renderHandler.post {
|
||||
if (targetSurfaces.isEmpty()) return@post
|
||||
|
||||
st.updateTexImage()
|
||||
st.getTransformMatrix(stMatrix)
|
||||
val timestamp = st.timestamp
|
||||
|
||||
val it = targetSurfaces.iterator()
|
||||
while (it.hasNext()) {
|
||||
val eglSurface = it.next()
|
||||
if (!eglMakeCurrent(eglDisplay, eglSurface, eglSurface, eglContext)) {
|
||||
val error = eglGetError()
|
||||
Dog.e(TAG, "eglMakeCurrent failed for surface, removing: ${GLUtils.getEGLErrorString(error)}", null, true)
|
||||
eglDestroySurface(eglDisplay, eglSurface)
|
||||
it.remove()
|
||||
continue
|
||||
}
|
||||
|
||||
val size = surfaceSizes[eglSurface] ?: Pair(primaryWidth, primaryHeight)
|
||||
glViewport(0, 0, size.first, size.second)
|
||||
|
||||
glClearColor(0.0f, 0.0f, 0.0f, 1.0f)
|
||||
glClear(GL_COLOR_BUFFER_BIT)
|
||||
|
||||
glUseProgram(programId)
|
||||
glUniformMatrix4fv(uMVPMatrixHandle, 1, false, mvpMatrix, 0)
|
||||
glUniformMatrix4fv(uSTMatrixHandle, 1, false, stMatrix, 0)
|
||||
|
||||
glEnableVertexAttribArray(aPositionHandle)
|
||||
glVertexAttribPointer(aPositionHandle, 4, GL_FLOAT, false, 16, vertexBuffer)
|
||||
|
||||
glEnableVertexAttribArray(aTextureCoordHandle)
|
||||
glVertexAttribPointer(aTextureCoordHandle, 4, GL_FLOAT, false, 16, textureBuffer)
|
||||
|
||||
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4)
|
||||
|
||||
glDisableVertexAttribArray(aPositionHandle)
|
||||
glDisableVertexAttribArray(aTextureCoordHandle)
|
||||
|
||||
if (timestamp > 0) {
|
||||
EGLExt.eglPresentationTimeANDROID(eglDisplay, eglSurface, timestamp)
|
||||
}
|
||||
eglSwapBuffers(eglDisplay, eglSurface)
|
||||
}
|
||||
|
||||
// Feed YUV ImageWriters
|
||||
if (imageWriters.isNotEmpty()) {
|
||||
feedImageWriters(st)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun feedImageWriters(st: SurfaceTexture) {
|
||||
val width = primaryWidth
|
||||
val height = primaryHeight
|
||||
if (width <= 0 || height <= 0) return
|
||||
|
||||
// 1. Draw to offscreen pbuffer for reading
|
||||
eglMakeCurrent(eglDisplay, pbufferSurface, pbufferSurface, eglContext)
|
||||
// Need to resize pbuffer or use an FBO for correct sizing.
|
||||
// For now, we assume the pbuffer is used as a state-holder and we use an FBO if needed.
|
||||
// Simplified: Read from the last drawn EGL surface if available.
|
||||
if (targetSurfaces.isNotEmpty()) {
|
||||
eglMakeCurrent(eglDisplay, targetSurfaces[0], targetSurfaces[0], eglContext)
|
||||
}
|
||||
|
||||
val size = width * height * 4
|
||||
if (pixelBuffer == null || pixelBuffer!!.capacity() < size) {
|
||||
pixelBuffer = ByteBuffer.allocateDirect(size).order(ByteOrder.nativeOrder())
|
||||
}
|
||||
pixelBuffer!!.rewind()
|
||||
glReadPixels(0, 0, width, height, GL_RGBA, GL_UNSIGNED_BYTE, pixelBuffer)
|
||||
|
||||
imageWriters.forEach { (surface, writer) ->
|
||||
try {
|
||||
val image = writer.dequeueInputImage()
|
||||
val planes = image.planes
|
||||
|
||||
// Fast RGB to YUV conversion (simplified I420)
|
||||
val yBuffer = planes[0].buffer
|
||||
val uBuffer = planes[1].buffer
|
||||
val vBuffer = planes[2].buffer
|
||||
|
||||
val yStride = planes[0].rowStride
|
||||
val uvStride = planes[1].rowStride
|
||||
val uvPixelStride = planes[1].pixelStride
|
||||
|
||||
pixelBuffer!!.rewind()
|
||||
for (y in 0 until height) {
|
||||
for (x in 0 until width) {
|
||||
val r = pixelBuffer!!.get().toInt() and 0xFF
|
||||
val g = pixelBuffer!!.get().toInt() and 0xFF
|
||||
val b = pixelBuffer!!.get().toInt() and 0xFF
|
||||
pixelBuffer!!.get() // skip A
|
||||
|
||||
// Y = 0.299R + 0.587G + 0.114B
|
||||
val yVal = ((66 * r + 129 * g + 25 * b + 128) shr 8) + 16
|
||||
yBuffer.put(y * yStride + x, yVal.toByte())
|
||||
|
||||
if (y % 2 == 0 && x % 2 == 0) {
|
||||
// U = -0.169R - 0.331G + 0.500B
|
||||
val uVal = ((-38 * r - 74 * g + 112 * b + 128) shr 8) + 128
|
||||
// V = 0.500R - 0.419G - 0.081B
|
||||
val vVal = ((112 * r - 94 * g - 18 * b + 128) shr 8) + 128
|
||||
|
||||
uBuffer.put((y / 2) * uvStride + (x / 2) * uvPixelStride, uVal.toByte())
|
||||
vBuffer.put((y / 2) * uvStride + (x / 2) * uvPixelStride, vVal.toByte())
|
||||
}
|
||||
}
|
||||
}
|
||||
writer.queueInputImage(image)
|
||||
} catch (e: Exception) {
|
||||
// Ignore if writer is full or surface is gone
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setVideoSize(width: Int, height: Int) {
|
||||
renderHandler.post {
|
||||
Dog.i(TAG, "setVideoSize: $width x $height", true)
|
||||
videoWidth = width
|
||||
videoHeight = height
|
||||
updateMVPMatrix()
|
||||
}
|
||||
}
|
||||
|
||||
fun release() {
|
||||
renderHandler.post {
|
||||
targetSurfaces.forEach { eglDestroySurface(eglDisplay, it) }
|
||||
targetSurfaces.clear()
|
||||
surfaceSizes.clear()
|
||||
if (eglContext != EGL_NO_CONTEXT) {
|
||||
eglDestroyContext(eglDisplay, eglContext)
|
||||
eglContext = EGL_NO_CONTEXT
|
||||
}
|
||||
if (eglDisplay != EGL_NO_DISPLAY) {
|
||||
eglTerminate(eglDisplay)
|
||||
eglDisplay = EGL_NO_DISPLAY
|
||||
}
|
||||
inputSurface?.release()
|
||||
surfaceTexture?.release()
|
||||
renderThread.quit()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
package com.nothing.camera2magic.ui.theme
|
||||
|
||||
import androidx.compose.ui.graphics.Color
|
||||
|
||||
val primaryLight = Color(0xFF65558F)
|
||||
val onPrimaryLight = Color(0xFFFFFFFF)
|
||||
val primaryContainerLight = Color(0xFFEADDFF)
|
||||
val onPrimaryContainerLight = Color(0xFF4F378B)
|
||||
val secondaryLight = Color(0xFF625B71)
|
||||
val onSecondaryLight = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLight = Color(0xFFE8DEF8)
|
||||
val onSecondaryContainerLight = Color(0xFF4A4458)
|
||||
val tertiaryLight = Color(0xFF7D5260)
|
||||
val onTertiaryLight = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLight = Color(0xFFFFD8E4)
|
||||
val onTertiaryContainerLight = Color(0xFF633B48)
|
||||
val errorLight = Color(0xFFB3261E)
|
||||
val onErrorLight = Color(0xFFFFFFFF)
|
||||
val errorContainerLight = Color(0xFFF9DEDC)
|
||||
val onErrorContainerLight = Color(0xFF8C1D18)
|
||||
val backgroundLight = Color(0xFFFEF7FF)
|
||||
val onBackgroundLight = Color(0xFF1D1B20)
|
||||
val surfaceLight = Color(0xFFFEF7FF)
|
||||
val onSurfaceLight = Color(0xFF1D1B20)
|
||||
val surfaceVariantLight = Color(0xFFE7E0EC)
|
||||
val onSurfaceVariantLight = Color(0xFF49454F)
|
||||
val outlineLight = Color(0xFF79747E)
|
||||
val outlineVariantLight = Color(0xFFCAC4D0)
|
||||
val scrimLight = Color(0xFF000000)
|
||||
val inverseSurfaceLight = Color(0xFF322F35)
|
||||
val inverseOnSurfaceLight = Color(0xFFF5EFF7)
|
||||
val inversePrimaryLight = Color(0xFFD0BCFF)
|
||||
val surfaceDimLight = Color(0xFFDED8E1)
|
||||
val surfaceBrightLight = Color(0xFFFEF7FF)
|
||||
val surfaceContainerLowestLight = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLight = Color(0xFFF7F2FA)
|
||||
val surfaceContainerLight = Color(0xFFF3EDF7)
|
||||
val surfaceContainerHighLight = Color(0xFFECE6F0)
|
||||
val surfaceContainerHighestLight = Color(0xFFE6E0E9)
|
||||
|
||||
val primaryLightMediumContrast = Color(0xFF3C2D63)
|
||||
val onPrimaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val primaryContainerLightMediumContrast = Color(0xFF74649F)
|
||||
val onPrimaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val secondaryLightMediumContrast = Color(0xFF3C2D63)
|
||||
val onSecondaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLightMediumContrast = Color(0xFF74649F)
|
||||
val onSecondaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryLightMediumContrast = Color(0xFF5B2238)
|
||||
val onTertiaryLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLightMediumContrast = Color(0xFF9C5870)
|
||||
val onTertiaryContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val errorLightMediumContrast = Color(0xFF5E2320)
|
||||
val onErrorLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val errorContainerLightMediumContrast = Color(0xFFA15853)
|
||||
val onErrorContainerLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val backgroundLightMediumContrast = Color(0xFFFDF7FF)
|
||||
val onBackgroundLightMediumContrast = Color(0xFF1D1B20)
|
||||
val surfaceLightMediumContrast = Color(0xFFFDF7FF)
|
||||
val onSurfaceLightMediumContrast = Color(0xFF121016)
|
||||
val surfaceVariantLightMediumContrast = Color(0xFFE6E0EC)
|
||||
val onSurfaceVariantLightMediumContrast = Color(0xFF37353E)
|
||||
val outlineLightMediumContrast = Color(0xFF54515A)
|
||||
val outlineVariantLightMediumContrast = Color(0xFF6F6B75)
|
||||
val scrimLightMediumContrast = Color(0xFF000000)
|
||||
val inverseSurfaceLightMediumContrast = Color(0xFF322F35)
|
||||
val inverseOnSurfaceLightMediumContrast = Color(0xFFF5EFF7)
|
||||
val inversePrimaryLightMediumContrast = Color(0xFFCFBDFE)
|
||||
val surfaceDimLightMediumContrast = Color(0xFFCAC5CD)
|
||||
val surfaceBrightLightMediumContrast = Color(0xFFFDF7FF)
|
||||
val surfaceContainerLowestLightMediumContrast = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLightMediumContrast = Color(0xFFF7F2FA)
|
||||
val surfaceContainerLightMediumContrast = Color(0xFFECE6EE)
|
||||
val surfaceContainerHighLightMediumContrast = Color(0xFFE0DBE3)
|
||||
val surfaceContainerHighestLightMediumContrast = Color(0xFFD5D0D8)
|
||||
|
||||
val primaryLightHighContrast = Color(0xFF312259)
|
||||
val onPrimaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val primaryContainerLightHighContrast = Color(0xFF4F4078)
|
||||
val onPrimaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val secondaryLightHighContrast = Color(0xFF312259)
|
||||
val onSecondaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val secondaryContainerLightHighContrast = Color(0xFF4F4078)
|
||||
val onSecondaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryLightHighContrast = Color(0xFF4F182E)
|
||||
val onTertiaryLightHighContrast = Color(0xFFFFFFFF)
|
||||
val tertiaryContainerLightHighContrast = Color(0xFF72354C)
|
||||
val onTertiaryContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val errorLightHighContrast = Color(0xFF511917)
|
||||
val onErrorLightHighContrast = Color(0xFFFFFFFF)
|
||||
val errorContainerLightHighContrast = Color(0xFF763632)
|
||||
val onErrorContainerLightHighContrast = Color(0xFFFFFFFF)
|
||||
val backgroundLightHighContrast = Color(0xFFFDF7FF)
|
||||
val onBackgroundLightHighContrast = Color(0xFF1D1B20)
|
||||
val surfaceLightHighContrast = Color(0xFFFDF7FF)
|
||||
val onSurfaceLightHighContrast = Color(0xFF000000)
|
||||
val surfaceVariantLightHighContrast = Color(0xFFE6E0EC)
|
||||
val onSurfaceVariantLightHighContrast = Color(0xFF000000)
|
||||
val outlineLightHighContrast = Color(0xFF2D2B33)
|
||||
val outlineVariantLightHighContrast = Color(0xFF4A4851)
|
||||
val scrimLightHighContrast = Color(0xFF000000)
|
||||
val inverseSurfaceLightHighContrast = Color(0xFF322F35)
|
||||
val inverseOnSurfaceLightHighContrast = Color(0xFFFFFFFF)
|
||||
val inversePrimaryLightHighContrast = Color(0xFFCFBDFE)
|
||||
val surfaceDimLightHighContrast = Color(0xFFBCB7BF)
|
||||
val surfaceBrightLightHighContrast = Color(0xFFFDF7FF)
|
||||
val surfaceContainerLowestLightHighContrast = Color(0xFFFFFFFF)
|
||||
val surfaceContainerLowLightHighContrast = Color(0xFFF5EFF7)
|
||||
val surfaceContainerLightHighContrast = Color(0xFFE6E1E9)
|
||||
val surfaceContainerHighLightHighContrast = Color(0xFFD8D3DA)
|
||||
val surfaceContainerHighestLightHighContrast = Color(0xFFCAC5CD)
|
||||
|
||||
val primaryDark = Color(0xFFD0BCFE)
|
||||
val onPrimaryDark = Color(0xFF381E72)
|
||||
val primaryContainerDark = Color(0xFF4F378B)
|
||||
val onPrimaryContainerDark = Color(0xFFEADDFF)
|
||||
val secondaryDark = Color(0xFFCCC2DC)
|
||||
val onSecondaryDark = Color(0xFF332D41)
|
||||
val secondaryContainerDark = Color(0xFF4A4458)
|
||||
val onSecondaryContainerDark = Color(0xFFE8DEF8)
|
||||
val tertiaryDark = Color(0xFFEFB8C8)
|
||||
val onTertiaryDark = Color(0xFF492532)
|
||||
val tertiaryContainerDark = Color(0xFF633B48)
|
||||
val onTertiaryContainerDark = Color(0xFFFFD8E4)
|
||||
val errorDark = Color(0xFFF2B8B5)
|
||||
val onErrorDark = Color(0xFF601410)
|
||||
val errorContainerDark = Color(0xFF8C1D18)
|
||||
val onErrorContainerDark = Color(0xFFF9DEDC)
|
||||
val backgroundDark = Color(0xFF141218)
|
||||
val onBackgroundDark = Color(0xFFE6E0E9)
|
||||
val surfaceDark = Color(0xFF141218)
|
||||
val onSurfaceDark = Color(0xFFE6E0E9)
|
||||
val surfaceVariantDark = Color(0xFF49454F)
|
||||
val onSurfaceVariantDark = Color(0xFFCAC4D0)
|
||||
val outlineDark = Color(0xFF938F99)
|
||||
val outlineVariantDark = Color(0xFF49454F)
|
||||
val scrimDark = Color(0xFF000000)
|
||||
val inverseSurfaceDark = Color(0xFFE6E0E9)
|
||||
val inverseOnSurfaceDark = Color(0xFF322F35)
|
||||
val inversePrimaryDark = Color(0xFF6750A4)
|
||||
val surfaceDimDark = Color(0xFF141218)
|
||||
val surfaceBrightDark = Color(0xFF3B383E)
|
||||
val surfaceContainerLowestDark = Color(0xFF0F0D13)
|
||||
val surfaceContainerLowDark = Color(0xFF1D1B20)
|
||||
val surfaceContainerDark = Color(0xFF211F26)
|
||||
val surfaceContainerHighDark = Color(0xFF2B2930)
|
||||
val surfaceContainerHighestDark = Color(0xFF36343B)
|
||||
|
||||
val primaryDarkMediumContrast = Color(0xFFE3D6FF)
|
||||
val onPrimaryDarkMediumContrast = Color(0xFF2B1B52)
|
||||
val primaryContainerDarkMediumContrast = Color(0xFF9887C5)
|
||||
val onPrimaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val secondaryDarkMediumContrast = Color(0xFFE3D6FF)
|
||||
val onSecondaryDarkMediumContrast = Color(0xFF2B1B52)
|
||||
val secondaryContainerDarkMediumContrast = Color(0xFF9887C5)
|
||||
val onSecondaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val tertiaryDarkMediumContrast = Color(0xFFFFD0DD)
|
||||
val onTertiaryDarkMediumContrast = Color(0xFF471228)
|
||||
val tertiaryContainerDarkMediumContrast = Color(0xFFC57B93)
|
||||
val onTertiaryContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val errorDarkMediumContrast = Color(0xFFFFD2CE)
|
||||
val onErrorDarkMediumContrast = Color(0xFF481311)
|
||||
val errorContainerDarkMediumContrast = Color(0xFFCC7B74)
|
||||
val onErrorContainerDarkMediumContrast = Color(0xFF000000)
|
||||
val backgroundDarkMediumContrast = Color(0xFF141218)
|
||||
val onBackgroundDarkMediumContrast = Color(0xFFE6E0E9)
|
||||
val surfaceDarkMediumContrast = Color(0xFF141318)
|
||||
val onSurfaceDarkMediumContrast = Color(0xFFFFFFFF)
|
||||
val surfaceVariantDarkMediumContrast = Color(0xFF48454E)
|
||||
val onSurfaceVariantDarkMediumContrast = Color(0xFFE0DAE5)
|
||||
val outlineDarkMediumContrast = Color(0xFFB5B0BB)
|
||||
val outlineVariantDarkMediumContrast = Color(0xFF938F99)
|
||||
val scrimDarkMediumContrast = Color(0xFF000000)
|
||||
val inverseSurfaceDarkMediumContrast = Color(0xFFE6E1E9)
|
||||
val inverseOnSurfaceDarkMediumContrast = Color(0xFF2B292F)
|
||||
val inversePrimaryDarkMediumContrast = Color(0xFF4E3F77)
|
||||
val surfaceDimDarkMediumContrast = Color(0xFF141318)
|
||||
val surfaceBrightDarkMediumContrast = Color(0xFF46434A)
|
||||
val surfaceContainerLowestDarkMediumContrast = Color(0xFF08070B)
|
||||
val surfaceContainerLowDarkMediumContrast = Color(0xFF1E1D22)
|
||||
val surfaceContainerDarkMediumContrast = Color(0xFF29272D)
|
||||
val surfaceContainerHighDarkMediumContrast = Color(0xFF343238)
|
||||
val surfaceContainerHighestDarkMediumContrast = Color(0xFF3F3D43)
|
||||
|
||||
val primaryDarkHighContrast = Color(0xFFF5EDFF)
|
||||
val onPrimaryDarkHighContrast = Color(0xFF000000)
|
||||
val primaryContainerDarkHighContrast = Color(0xFFCBB9FA)
|
||||
val onPrimaryContainerDarkHighContrast = Color(0xFF0F0033)
|
||||
val secondaryDarkHighContrast = Color(0xFFF5EDFF)
|
||||
val onSecondaryDarkHighContrast = Color(0xFF000000)
|
||||
val secondaryContainerDarkHighContrast = Color(0xFFCBB9FA)
|
||||
val onSecondaryContainerDarkHighContrast = Color(0xFF0F0033)
|
||||
val tertiaryDarkHighContrast = Color(0xFFFFEBEF)
|
||||
val onTertiaryDarkHighContrast = Color(0xFF000000)
|
||||
val tertiaryContainerDarkHighContrast = Color(0xFFFEABC5)
|
||||
val onTertiaryContainerDarkHighContrast = Color(0xFF20000D)
|
||||
val errorDarkHighContrast = Color(0xFFFFECEA)
|
||||
val onErrorDarkHighContrast = Color(0xFF000000)
|
||||
val errorContainerDarkHighContrast = Color(0xFFFFAEA7)
|
||||
val onErrorContainerDarkHighContrast = Color(0xFF220001)
|
||||
val backgroundDarkHighContrast = Color(0xFF141218)
|
||||
val onBackgroundDarkHighContrast = Color(0xFFE6E0E9)
|
||||
val surfaceDarkHighContrast = Color(0xFF141318)
|
||||
val onSurfaceDarkHighContrast = Color(0xFFFFFFFF)
|
||||
val surfaceVariantDarkHighContrast = Color(0xFF48454E)
|
||||
val onSurfaceVariantDarkHighContrast = Color(0xFFFFFFFF)
|
||||
val outlineDarkHighContrast = Color(0xFFF4EEF9)
|
||||
val outlineVariantDarkHighContrast = Color(0xFFC6C1CC)
|
||||
val scrimDarkHighContrast = Color(0xFF000000)
|
||||
val inverseSurfaceDarkHighContrast = Color(0xFFE6E1E9)
|
||||
val inverseOnSurfaceDarkHighContrast = Color(0xFF000000)
|
||||
val inversePrimaryDarkHighContrast = Color(0xFF4E3F77)
|
||||
val surfaceDimDarkHighContrast = Color(0xFF141318)
|
||||
val surfaceBrightDarkHighContrast = Color(0xFF524F55)
|
||||
val surfaceContainerLowestDarkHighContrast = Color(0xFF000000)
|
||||
val surfaceContainerLowDarkHighContrast = Color(0xFF211F24)
|
||||
val surfaceContainerDarkHighContrast = Color(0xFF322F35)
|
||||
val surfaceContainerHighDarkHighContrast = Color(0xFF3D3A41)
|
||||
val surfaceContainerHighestDarkHighContrast = Color(0xFF48464C)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
package com.nothing.camera2magic.ui.theme
|
||||
import android.app.Activity
|
||||
import android.os.Build
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.lightColorScheme
|
||||
import androidx.compose.material3.darkColorScheme
|
||||
import androidx.compose.material3.dynamicDarkColorScheme
|
||||
import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.Immutable
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.toArgb
|
||||
import androidx.compose.runtime.SideEffect
|
||||
import androidx.compose.ui.platform.LocalView
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
|
||||
private val lightScheme = lightColorScheme(
|
||||
primary = primaryLight,
|
||||
onPrimary = onPrimaryLight,
|
||||
primaryContainer = primaryContainerLight,
|
||||
onPrimaryContainer = onPrimaryContainerLight,
|
||||
secondary = secondaryLight,
|
||||
onSecondary = onSecondaryLight,
|
||||
secondaryContainer = secondaryContainerLight,
|
||||
onSecondaryContainer = onSecondaryContainerLight,
|
||||
tertiary = tertiaryLight,
|
||||
onTertiary = onTertiaryLight,
|
||||
tertiaryContainer = tertiaryContainerLight,
|
||||
onTertiaryContainer = onTertiaryContainerLight,
|
||||
error = errorLight,
|
||||
onError = onErrorLight,
|
||||
errorContainer = errorContainerLight,
|
||||
onErrorContainer = onErrorContainerLight,
|
||||
background = backgroundLight,
|
||||
onBackground = onBackgroundLight,
|
||||
surface = surfaceLight,
|
||||
onSurface = onSurfaceLight,
|
||||
surfaceVariant = surfaceVariantLight,
|
||||
onSurfaceVariant = onSurfaceVariantLight,
|
||||
outline = outlineLight,
|
||||
outlineVariant = outlineVariantLight,
|
||||
scrim = scrimLight,
|
||||
inverseSurface = inverseSurfaceLight,
|
||||
inverseOnSurface = inverseOnSurfaceLight,
|
||||
inversePrimary = inversePrimaryLight,
|
||||
surfaceDim = surfaceDimLight,
|
||||
surfaceBright = surfaceBrightLight,
|
||||
surfaceContainerLowest = surfaceContainerLowestLight,
|
||||
surfaceContainerLow = surfaceContainerLowLight,
|
||||
surfaceContainer = surfaceContainerLight,
|
||||
surfaceContainerHigh = surfaceContainerHighLight,
|
||||
surfaceContainerHighest = surfaceContainerHighestLight,
|
||||
)
|
||||
|
||||
private val darkScheme = darkColorScheme(
|
||||
primary = primaryDark,
|
||||
onPrimary = onPrimaryDark,
|
||||
primaryContainer = primaryContainerDark,
|
||||
onPrimaryContainer = onPrimaryContainerDark,
|
||||
secondary = secondaryDark,
|
||||
onSecondary = onSecondaryDark,
|
||||
secondaryContainer = secondaryContainerDark,
|
||||
onSecondaryContainer = onSecondaryContainerDark,
|
||||
tertiary = tertiaryDark,
|
||||
onTertiary = onTertiaryDark,
|
||||
tertiaryContainer = tertiaryContainerDark,
|
||||
onTertiaryContainer = onTertiaryContainerDark,
|
||||
error = errorDark,
|
||||
onError = onErrorDark,
|
||||
errorContainer = errorContainerDark,
|
||||
onErrorContainer = onErrorContainerDark,
|
||||
background = backgroundDark,
|
||||
onBackground = onBackgroundDark,
|
||||
surface = surfaceDark,
|
||||
onSurface = onSurfaceDark,
|
||||
surfaceVariant = surfaceVariantDark,
|
||||
onSurfaceVariant = onSurfaceVariantDark,
|
||||
outline = outlineDark,
|
||||
outlineVariant = outlineVariantDark,
|
||||
scrim = scrimDark,
|
||||
inverseSurface = inverseSurfaceDark,
|
||||
inverseOnSurface = inverseOnSurfaceDark,
|
||||
inversePrimary = inversePrimaryDark,
|
||||
surfaceDim = surfaceDimDark,
|
||||
surfaceBright = surfaceBrightDark,
|
||||
surfaceContainerLowest = surfaceContainerLowestDark,
|
||||
surfaceContainerLow = surfaceContainerLowDark,
|
||||
surfaceContainer = surfaceContainerDark,
|
||||
surfaceContainerHigh = surfaceContainerHighDark,
|
||||
surfaceContainerHighest = surfaceContainerHighestDark,
|
||||
)
|
||||
|
||||
private val mediumContrastLightColorScheme = lightColorScheme(
|
||||
primary = primaryLightMediumContrast,
|
||||
onPrimary = onPrimaryLightMediumContrast,
|
||||
primaryContainer = primaryContainerLightMediumContrast,
|
||||
onPrimaryContainer = onPrimaryContainerLightMediumContrast,
|
||||
secondary = secondaryLightMediumContrast,
|
||||
onSecondary = onSecondaryLightMediumContrast,
|
||||
secondaryContainer = secondaryContainerLightMediumContrast,
|
||||
onSecondaryContainer = onSecondaryContainerLightMediumContrast,
|
||||
tertiary = tertiaryLightMediumContrast,
|
||||
onTertiary = onTertiaryLightMediumContrast,
|
||||
tertiaryContainer = tertiaryContainerLightMediumContrast,
|
||||
onTertiaryContainer = onTertiaryContainerLightMediumContrast,
|
||||
error = errorLightMediumContrast,
|
||||
onError = onErrorLightMediumContrast,
|
||||
errorContainer = errorContainerLightMediumContrast,
|
||||
onErrorContainer = onErrorContainerLightMediumContrast,
|
||||
background = backgroundLightMediumContrast,
|
||||
onBackground = onBackgroundLightMediumContrast,
|
||||
surface = surfaceLightMediumContrast,
|
||||
onSurface = onSurfaceLightMediumContrast,
|
||||
surfaceVariant = surfaceVariantLightMediumContrast,
|
||||
onSurfaceVariant = onSurfaceVariantLightMediumContrast,
|
||||
outline = outlineLightMediumContrast,
|
||||
outlineVariant = outlineVariantLightMediumContrast,
|
||||
scrim = scrimLightMediumContrast,
|
||||
inverseSurface = inverseSurfaceLightMediumContrast,
|
||||
inverseOnSurface = inverseOnSurfaceLightMediumContrast,
|
||||
inversePrimary = inversePrimaryLightMediumContrast,
|
||||
surfaceDim = surfaceDimLightMediumContrast,
|
||||
surfaceBright = surfaceBrightLightMediumContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestLightMediumContrast,
|
||||
surfaceContainerLow = surfaceContainerLowLightMediumContrast,
|
||||
surfaceContainer = surfaceContainerLightMediumContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighLightMediumContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestLightMediumContrast,
|
||||
)
|
||||
|
||||
private val highContrastLightColorScheme = lightColorScheme(
|
||||
primary = primaryLightHighContrast,
|
||||
onPrimary = onPrimaryLightHighContrast,
|
||||
primaryContainer = primaryContainerLightHighContrast,
|
||||
onPrimaryContainer = onPrimaryContainerLightHighContrast,
|
||||
secondary = secondaryLightHighContrast,
|
||||
onSecondary = onSecondaryLightHighContrast,
|
||||
secondaryContainer = secondaryContainerLightHighContrast,
|
||||
onSecondaryContainer = onSecondaryContainerLightHighContrast,
|
||||
tertiary = tertiaryLightHighContrast,
|
||||
onTertiary = onTertiaryLightHighContrast,
|
||||
tertiaryContainer = tertiaryContainerLightHighContrast,
|
||||
onTertiaryContainer = onTertiaryContainerLightHighContrast,
|
||||
error = errorLightHighContrast,
|
||||
onError = onErrorLightHighContrast,
|
||||
errorContainer = errorContainerLightHighContrast,
|
||||
onErrorContainer = onErrorContainerLightHighContrast,
|
||||
background = backgroundLightHighContrast,
|
||||
onBackground = onBackgroundLightHighContrast,
|
||||
surface = surfaceLightHighContrast,
|
||||
onSurface = onSurfaceLightHighContrast,
|
||||
surfaceVariant = surfaceVariantLightHighContrast,
|
||||
onSurfaceVariant = onSurfaceVariantLightHighContrast,
|
||||
outline = outlineLightHighContrast,
|
||||
outlineVariant = outlineVariantLightHighContrast,
|
||||
scrim = scrimLightHighContrast,
|
||||
inverseSurface = inverseSurfaceLightHighContrast,
|
||||
inverseOnSurface = inverseOnSurfaceLightHighContrast,
|
||||
inversePrimary = inversePrimaryLightHighContrast,
|
||||
surfaceDim = surfaceDimLightHighContrast,
|
||||
surfaceBright = surfaceBrightLightHighContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestLightHighContrast,
|
||||
surfaceContainerLow = surfaceContainerLowLightHighContrast,
|
||||
surfaceContainer = surfaceContainerLightHighContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighLightHighContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestLightHighContrast,
|
||||
)
|
||||
|
||||
private val mediumContrastDarkColorScheme = darkColorScheme(
|
||||
primary = primaryDarkMediumContrast,
|
||||
onPrimary = onPrimaryDarkMediumContrast,
|
||||
primaryContainer = primaryContainerDarkMediumContrast,
|
||||
onPrimaryContainer = onPrimaryContainerDarkMediumContrast,
|
||||
secondary = secondaryDarkMediumContrast,
|
||||
onSecondary = onSecondaryDarkMediumContrast,
|
||||
secondaryContainer = secondaryContainerDarkMediumContrast,
|
||||
onSecondaryContainer = onSecondaryContainerDarkMediumContrast,
|
||||
tertiary = tertiaryDarkMediumContrast,
|
||||
onTertiary = onTertiaryDarkMediumContrast,
|
||||
tertiaryContainer = tertiaryContainerDarkMediumContrast,
|
||||
onTertiaryContainer = onTertiaryContainerDarkMediumContrast,
|
||||
error = errorDarkMediumContrast,
|
||||
onError = onErrorDarkMediumContrast,
|
||||
errorContainer = errorContainerDarkMediumContrast,
|
||||
onErrorContainer = onErrorContainerDarkMediumContrast,
|
||||
background = backgroundDarkMediumContrast,
|
||||
onBackground = onBackgroundDarkMediumContrast,
|
||||
surface = surfaceDarkMediumContrast,
|
||||
onSurface = onSurfaceDarkMediumContrast,
|
||||
surfaceVariant = surfaceVariantDarkMediumContrast,
|
||||
onSurfaceVariant = onSurfaceVariantDarkMediumContrast,
|
||||
outline = outlineDarkMediumContrast,
|
||||
outlineVariant = outlineVariantDarkMediumContrast,
|
||||
scrim = scrimDarkMediumContrast,
|
||||
inverseSurface = inverseSurfaceDarkMediumContrast,
|
||||
inverseOnSurface = inverseOnSurfaceDarkMediumContrast,
|
||||
inversePrimary = inversePrimaryDarkMediumContrast,
|
||||
surfaceDim = surfaceDimDarkMediumContrast,
|
||||
surfaceBright = surfaceBrightDarkMediumContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestDarkMediumContrast,
|
||||
surfaceContainerLow = surfaceContainerLowDarkMediumContrast,
|
||||
surfaceContainer = surfaceContainerDarkMediumContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighDarkMediumContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestDarkMediumContrast,
|
||||
)
|
||||
|
||||
private val highContrastDarkColorScheme = darkColorScheme(
|
||||
primary = primaryDarkHighContrast,
|
||||
onPrimary = onPrimaryDarkHighContrast,
|
||||
primaryContainer = primaryContainerDarkHighContrast,
|
||||
onPrimaryContainer = onPrimaryContainerDarkHighContrast,
|
||||
secondary = secondaryDarkHighContrast,
|
||||
onSecondary = onSecondaryDarkHighContrast,
|
||||
secondaryContainer = secondaryContainerDarkHighContrast,
|
||||
onSecondaryContainer = onSecondaryContainerDarkHighContrast,
|
||||
tertiary = tertiaryDarkHighContrast,
|
||||
onTertiary = onTertiaryDarkHighContrast,
|
||||
tertiaryContainer = tertiaryContainerDarkHighContrast,
|
||||
onTertiaryContainer = onTertiaryContainerDarkHighContrast,
|
||||
error = errorDarkHighContrast,
|
||||
onError = onErrorDarkHighContrast,
|
||||
errorContainer = errorContainerDarkHighContrast,
|
||||
onErrorContainer = onErrorContainerDarkHighContrast,
|
||||
background = backgroundDarkHighContrast,
|
||||
onBackground = onBackgroundDarkHighContrast,
|
||||
surface = surfaceDarkHighContrast,
|
||||
onSurface = onSurfaceDarkHighContrast,
|
||||
surfaceVariant = surfaceVariantDarkHighContrast,
|
||||
onSurfaceVariant = onSurfaceVariantDarkHighContrast,
|
||||
outline = outlineDarkHighContrast,
|
||||
outlineVariant = outlineVariantDarkHighContrast,
|
||||
scrim = scrimDarkHighContrast,
|
||||
inverseSurface = inverseSurfaceDarkHighContrast,
|
||||
inverseOnSurface = inverseOnSurfaceDarkHighContrast,
|
||||
inversePrimary = inversePrimaryDarkHighContrast,
|
||||
surfaceDim = surfaceDimDarkHighContrast,
|
||||
surfaceBright = surfaceBrightDarkHighContrast,
|
||||
surfaceContainerLowest = surfaceContainerLowestDarkHighContrast,
|
||||
surfaceContainerLow = surfaceContainerLowDarkHighContrast,
|
||||
surfaceContainer = surfaceContainerDarkHighContrast,
|
||||
surfaceContainerHigh = surfaceContainerHighDarkHighContrast,
|
||||
surfaceContainerHighest = surfaceContainerHighestDarkHighContrast,
|
||||
)
|
||||
|
||||
@Immutable
|
||||
data class ColorFamily(
|
||||
val color: Color,
|
||||
val onColor: Color,
|
||||
val colorContainer: Color,
|
||||
val onColorContainer: Color
|
||||
)
|
||||
|
||||
val unspecified_scheme = ColorFamily(
|
||||
Color.Unspecified, Color.Unspecified, Color.Unspecified, Color.Unspecified
|
||||
)
|
||||
|
||||
@Composable
|
||||
fun VirtualCameraXTheme(
|
||||
darkTheme: Boolean = isSystemInDarkTheme(),
|
||||
// Dynamic color is available on Android 12+
|
||||
dynamicColor: Boolean = true,
|
||||
content: @Composable() () -> Unit
|
||||
) {
|
||||
val colorScheme = when {
|
||||
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
|
||||
val context = LocalContext.current
|
||||
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
|
||||
}
|
||||
|
||||
darkTheme -> darkScheme
|
||||
else -> lightScheme
|
||||
}
|
||||
val view = LocalView.current
|
||||
if (!view.isInEditMode) {
|
||||
SideEffect {
|
||||
val window = (view.context as Activity).window
|
||||
|
||||
// 1. Set the status bar background color to transparent to blend it with the application background
|
||||
window.statusBarColor = Color.Transparent.toArgb()
|
||||
|
||||
// 2. Set the status bar text color based on whether the theme is light or dark
|
||||
// isAppearanceLightStatusBars = true -> Status bar text and icons become dark
|
||||
// isAppearanceLightStatusBars = false -> Status bar text and icons become light (default)
|
||||
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = !darkTheme
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
MaterialTheme(
|
||||
colorScheme = colorScheme,
|
||||
typography = AppTypography,
|
||||
content = content
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,5 @@
|
||||
package com.nothing.camera2magic.ui.theme
|
||||
|
||||
import androidx.compose.material3.Typography
|
||||
|
||||
val AppTypography = Typography()
|
||||
@@ -0,0 +1,23 @@
|
||||
package com.nothing.camera2magic.utils
|
||||
import android.util.Log
|
||||
import android.view.Surface
|
||||
|
||||
val Surface?.shortId : String
|
||||
get() = if (this == null) "null" else "@0x${Integer.toHexString(System.identityHashCode(this))}"
|
||||
|
||||
object Dog {
|
||||
private const val PREFIX = "[VCX]"
|
||||
|
||||
fun i(tag: String? = null, message: String, enabled: Boolean = true) {
|
||||
Log.i("$PREFIX$tag", message)
|
||||
}
|
||||
|
||||
fun w(tag: String? = null, message: String, enabled: Boolean = true) {
|
||||
Log.w("$PREFIX$tag", message)
|
||||
}
|
||||
|
||||
fun e(tag: String? = null, message: String, throwable: Throwable? = null, enabled: Boolean = true) {
|
||||
if (throwable != null) Log.e("$PREFIX$tag", message, throwable)
|
||||
else Log.e("$PREFIX$tag", message)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
package com.nothing.camera2magic.view
|
||||
|
||||
import android.app.Application
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.ExperimentalLayoutApi
|
||||
import androidx.compose.foundation.layout.FlowRow
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Spacer
|
||||
import androidx.compose.foundation.layout.defaultMinSize
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
||||
import androidx.compose.material3.Button
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.MaterialTheme
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.collectAsState
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.nothing.camera2magic.viewmodel.SettingsViewModel
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import com.nothing.camera2magic.R
|
||||
import com.nothing.camera2magic.viewmodel.LocalViewModelFactory
|
||||
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
@Composable
|
||||
fun SettingsView() {
|
||||
val factory = LocalViewModelFactory.current
|
||||
val viewModel: SettingsViewModel = viewModel(factory = factory)
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
FlowRow(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
maxItemsInEachRow = 2,
|
||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(16.dp)
|
||||
) {
|
||||
// "Play sound" button
|
||||
SettingsToggleButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.play_sound_button_name),
|
||||
icon = ImageVector.vectorResource(R.drawable.volume_up_24px),
|
||||
isChecked = uiState.playSound,
|
||||
onClick = { viewModel.onPlaySoundToggled() }
|
||||
)
|
||||
|
||||
// "Enable Log" button
|
||||
SettingsToggleButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.enable_log_button_name),
|
||||
icon = ImageVector.vectorResource(R.drawable.breaking_news_24px),
|
||||
isChecked = uiState.enableLog,
|
||||
onClick = { viewModel.onEnableLogToggled() }
|
||||
)
|
||||
|
||||
// "HAL Mode" button (system-wide camera hook)
|
||||
SettingsToggleButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.hal_mode_button_name),
|
||||
icon = ImageVector.vectorResource(R.drawable.developer_board_24px),
|
||||
isChecked = uiState.halModeEnabled,
|
||||
onClick = { viewModel.onHalModeToggled() }
|
||||
)
|
||||
|
||||
// "Inject Control" buttons
|
||||
/*
|
||||
SettingsToggleButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.inject_control_button_name),
|
||||
icon = ImageVector.vectorResource(R.drawable.control_camera_24px),
|
||||
isChecked = uiState.injectMenu,
|
||||
onClick = { viewModel.onInjectMenuToggled() }
|
||||
)
|
||||
SettingsToggleButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
text = stringResource(R.string.manually_rotate_button_name),
|
||||
icon = ImageVector.vectorResource(R.drawable.rotate_90_degrees_cw_24px),
|
||||
isChecked = uiState.manuallyRotate,
|
||||
onClick = { viewModel.onManuallyRotateToggled() }
|
||||
)
|
||||
|
||||
*/
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SettingsToggleButton(
|
||||
modifier: Modifier = Modifier,
|
||||
text: String,
|
||||
icon: ImageVector,
|
||||
isChecked: Boolean,
|
||||
onClick: () -> Unit
|
||||
) {
|
||||
val containerColor = if (isChecked) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.surfaceContainer
|
||||
}
|
||||
|
||||
val contentColor = if (isChecked) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onSurfaceVariant
|
||||
}
|
||||
|
||||
Button(
|
||||
onClick = onClick,
|
||||
modifier = modifier
|
||||
.defaultMinSize(minHeight = 58.dp)
|
||||
.fillMaxWidth(),
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = containerColor,
|
||||
contentColor = contentColor
|
||||
),
|
||||
elevation = ButtonDefaults.buttonElevation(defaultElevation = 0.dp),
|
||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 20.dp)
|
||||
) {
|
||||
Column(
|
||||
verticalArrangement = Arrangement.Center,
|
||||
horizontalAlignment = Alignment.CenterHorizontally
|
||||
) {
|
||||
Icon(
|
||||
imageVector = icon,
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(16.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.height(8.dp))
|
||||
Text(
|
||||
text = text,
|
||||
fontWeight = FontWeight.Medium,
|
||||
maxLines = 1,
|
||||
softWrap = false
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,328 @@
|
||||
package com.nothing.camera2magic.view
|
||||
|
||||
import android.graphics.Bitmap
|
||||
import androidx.activity.compose.rememberLauncherForActivityResult
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
import androidx.compose.animation.fadeIn
|
||||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.Image
|
||||
import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.combinedClickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.draw.scale
|
||||
import androidx.compose.ui.graphics.Brush
|
||||
import androidx.compose.ui.graphics.Color
|
||||
import androidx.compose.ui.graphics.asImageBitmap
|
||||
import androidx.compose.ui.graphics.vector.ImageVector
|
||||
import androidx.compose.ui.layout.ContentScale
|
||||
import androidx.compose.ui.res.stringArrayResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.res.vectorResource
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.LifecycleEventObserver
|
||||
import androidx.lifecycle.compose.LocalLifecycleOwner
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.nothing.camera2magic.viewmodel.SpotlightViewModel
|
||||
import com.nothing.camera2magic.R
|
||||
import com.nothing.camera2magic.viewmodel.LocalViewModelFactory
|
||||
import com.nothing.camera2magic.viewmodel.MediaSource
|
||||
import com.nothing.camera2magic.viewmodel.MediaType
|
||||
import kotlin.enums.EnumEntries
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun SpotlightView() {
|
||||
val mediaSources = MediaSource.entries
|
||||
val mediaSourceLabels = stringArrayResource(R.array.media_source)
|
||||
val mediaTypes = MediaType.entries
|
||||
|
||||
val factory = LocalViewModelFactory.current
|
||||
val viewModel: SpotlightViewModel = viewModel(factory = factory)
|
||||
|
||||
val mediaThumbnails by viewModel.thumbnails.collectAsState()
|
||||
|
||||
val uiState by viewModel.uiState.collectAsState()
|
||||
|
||||
var pendingType by remember { mutableStateOf<MediaType?>(null) }
|
||||
|
||||
val launcher = rememberLauncherForActivityResult(ActivityResultContracts.GetContent()) {
|
||||
pendingType?.let { type ->
|
||||
viewModel.onMediaSelected(type, it)
|
||||
}
|
||||
}
|
||||
|
||||
val pickMedia = { type: MediaType ->
|
||||
pendingType = type
|
||||
launcher.launch(type.mimeType)
|
||||
}
|
||||
|
||||
Card(
|
||||
shape = RoundedCornerShape(24.dp),
|
||||
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
|
||||
modifier = Modifier.fillMaxWidth()
|
||||
) {
|
||||
Column(modifier = Modifier.padding(16.dp)) {
|
||||
MediaSourceSelector(
|
||||
sources = mediaSources,
|
||||
labels = mediaSourceLabels,
|
||||
selectedIndex = uiState.selectedMediaSource.value,
|
||||
onSourceSelected = { index -> viewModel.selectedMediaSourceFrom(index) }
|
||||
)
|
||||
Spacer(modifier = Modifier.height(16.dp))
|
||||
MediaPreviewGrid(
|
||||
mediaTypes = mediaTypes,
|
||||
thumbnails = mediaThumbnails,
|
||||
currentType = uiState.currentType,
|
||||
onPickMedia = { type -> pickMedia(type) },
|
||||
onClearMedia = { type -> viewModel.clearMediaBy(type)},
|
||||
onTypeSelected = { type -> viewModel.setCurrentMediaType(type) }
|
||||
|
||||
)
|
||||
ModuleSwitch(
|
||||
text = stringResource(R.string.module_switch_name),
|
||||
isEnabled = uiState.moduleEnabled,
|
||||
onToggle = { viewModel.onModuleToggled() }
|
||||
)
|
||||
}
|
||||
}
|
||||
OnLifecycleEvent { event ->
|
||||
if (event == Lifecycle.Event.ON_RESUME) {
|
||||
viewModel.performHealthCheckAndRefresh()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediaSourceSelector(
|
||||
sources: EnumEntries<MediaSource>,
|
||||
labels: Array<String>,
|
||||
selectedIndex: Int,
|
||||
onSourceSelected: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
){
|
||||
SingleChoiceSegmentedButtonRow(modifier = modifier.fillMaxWidth()) {
|
||||
sources.forEachIndexed { index, source ->
|
||||
SegmentedButton(
|
||||
shape = SegmentedButtonDefaults.itemShape(index = index, count = sources.size),
|
||||
onClick = { onSourceSelected(index) },
|
||||
selected = index == selectedIndex,
|
||||
enabled = source != MediaSource.NETWORK
|
||||
) {
|
||||
Text(labels[index])
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediaPreviewGrid(
|
||||
mediaTypes: EnumEntries<MediaType>,
|
||||
thumbnails: Map<MediaType, Bitmap?>,
|
||||
currentType: MediaType,
|
||||
onPickMedia: (MediaType) -> Unit,
|
||||
onClearMedia: (MediaType) -> Unit,
|
||||
onTypeSelected: (MediaType) -> Unit,
|
||||
modifier: Modifier = Modifier
|
||||
) {
|
||||
Row(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
mediaTypes.forEach { type ->
|
||||
Column(modifier = Modifier.weight(1f)) {
|
||||
MediaThumbnailCard(
|
||||
thumbnail = thumbnails[type],
|
||||
mediaType = type,
|
||||
onClick = { onPickMedia(type) },
|
||||
onClear = { onClearMedia(type) }
|
||||
)
|
||||
RadioButtonRow(
|
||||
selected = currentType == type,
|
||||
onClick = { onTypeSelected(type) },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun MediaThumbnailCard(
|
||||
modifier: Modifier = Modifier,
|
||||
mediaType: MediaType,
|
||||
thumbnail: Bitmap?,
|
||||
onClick: () -> Unit,
|
||||
onClear: () -> Unit
|
||||
) {
|
||||
var isInDeleteMode by remember { mutableStateOf(false) }
|
||||
|
||||
fun handleOnClick() {
|
||||
if (isInDeleteMode) {
|
||||
isInDeleteMode = false
|
||||
} else {
|
||||
onClick()
|
||||
}
|
||||
}
|
||||
|
||||
fun handleOnLongClick() {
|
||||
if (thumbnail != null) {
|
||||
isInDeleteMode = true
|
||||
}
|
||||
}
|
||||
|
||||
fun handleOnClear() {
|
||||
onClear()
|
||||
isInDeleteMode = false
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = modifier
|
||||
.aspectRatio(9f / 16f).clip(RoundedCornerShape(12.dp))
|
||||
.background(MaterialTheme.colorScheme.surface)
|
||||
.combinedClickable(
|
||||
onClick = ::handleOnClick,
|
||||
onLongClick = ::handleOnLongClick
|
||||
),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
ThumbnailContent(thumbnail, mediaType)
|
||||
DeleteModeOverlay(isInDeleteMode, ::handleOnClear)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ThumbnailContent(thumbnail: Bitmap?, mediaType: MediaType) {
|
||||
if (thumbnail != null) {
|
||||
Image (
|
||||
bitmap = thumbnail.asImageBitmap(),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
contentScale = ContentScale.Crop
|
||||
)
|
||||
} else {
|
||||
val iconResource = if (mediaType == MediaType.VIDEO) {
|
||||
R.drawable.video_file_24px
|
||||
} else {
|
||||
R.drawable.image_24px
|
||||
}
|
||||
Image(
|
||||
imageVector = ImageVector.vectorResource(iconResource),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.scale(1.5f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun DeleteModeOverlay(visible: Boolean, onClear: () -> Unit) {
|
||||
AnimatedVisibility(visible = visible, enter = fadeIn(), exit = fadeOut()) {
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().background(
|
||||
Brush.verticalGradient(
|
||||
colors = listOf(
|
||||
Color.Black.copy(alpha = 0.6f),
|
||||
Color.Transparent,
|
||||
Color.Transparent
|
||||
)
|
||||
)
|
||||
)
|
||||
){
|
||||
IconButton(
|
||||
onClick = onClear,
|
||||
modifier = Modifier.align(Alignment.Center).padding(8.dp).size(28.dp)
|
||||
.clip(CircleShape).background(Color.Black.copy(alpha = 0.4f))
|
||||
) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.close_24px),
|
||||
contentDescription = "Clear thumbnail",
|
||||
tint = Color.White,
|
||||
modifier = Modifier.size(18.dp)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun RadioButtonRow(selected: Boolean, onClick: () -> Unit) {
|
||||
Row(modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
|
||||
horizontalArrangement = Arrangement.Center,
|
||||
verticalAlignment = Alignment.CenterVertically) {
|
||||
RadioButton(
|
||||
selected = selected,
|
||||
onClick = onClick,
|
||||
colors = RadioButtonDefaults.colors(selectedColor = MaterialTheme.colorScheme.primary)
|
||||
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ModuleSwitch(
|
||||
text: String,
|
||||
isEnabled: Boolean,
|
||||
onToggle: () -> Unit
|
||||
) {
|
||||
Column(modifier = Modifier.fillMaxWidth()) {
|
||||
HorizontalDivider(
|
||||
color = MaterialTheme.colorScheme.onSurfaceVariant.copy(alpha = 0.1f),
|
||||
modifier = Modifier.padding(bottom = 8.dp)
|
||||
)
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalAlignment = Alignment.CenterVertically,
|
||||
horizontalArrangement = Arrangement.SpaceBetween
|
||||
) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Icon(
|
||||
imageVector = ImageVector.vectorResource(R.drawable.developer_board_24px),
|
||||
contentDescription = null,
|
||||
modifier = Modifier.size(20.dp)
|
||||
)
|
||||
Spacer(modifier = Modifier.width(12.dp))
|
||||
Text(text = text, fontSize = 16.sp, fontWeight = FontWeight.Medium)
|
||||
}
|
||||
Switch(
|
||||
checked = isEnabled,
|
||||
onCheckedChange = { onToggle() },
|
||||
colors = SwitchDefaults.colors(
|
||||
checkedThumbColor = MaterialTheme.colorScheme.onPrimary,
|
||||
checkedTrackColor = MaterialTheme.colorScheme.primary,
|
||||
uncheckedThumbColor = MaterialTheme.colorScheme.outline,
|
||||
uncheckedTrackColor = MaterialTheme.colorScheme.surfaceContainerHighest,
|
||||
uncheckedBorderColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.5f)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun OnLifecycleEvent(onEvent: (event: Lifecycle.Event) -> Unit) {
|
||||
val eventHandler by rememberUpdatedState(onEvent)
|
||||
val lifecycleOwner by rememberUpdatedState(LocalLifecycleOwner.current)
|
||||
|
||||
DisposableEffect(lifecycleOwner) {
|
||||
val observer = LifecycleEventObserver { _, event ->
|
||||
eventHandler(event)
|
||||
}
|
||||
lifecycleOwner.lifecycle.addObserver(observer)
|
||||
onDispose {
|
||||
lifecycleOwner.lifecycle.removeObserver(observer)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package com.nothing.camera2magic.viewmodel
|
||||
|
||||
import androidx.compose.runtime.staticCompositionLocalOf
|
||||
|
||||
val LocalConfigRepository = staticCompositionLocalOf<ConfigRepository> {
|
||||
error("No ConfigRepository provided")
|
||||
}
|
||||
val LocalViewModelFactory = staticCompositionLocalOf<ViewModelFactory> {
|
||||
error("No ViewModelFactory provided")
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package com.nothing.camera2magic.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.nothing.camera2magic.hook.HalConfigManager
|
||||
|
||||
private const val TAG = "SettingsViewModel"
|
||||
|
||||
class SettingsViewModel(private val repository: ConfigRepository) : ViewModel() {
|
||||
|
||||
private val _uiState = MutableStateFlow(SettingsUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadInitialSettings()
|
||||
}
|
||||
|
||||
private fun loadInitialSettings() {
|
||||
_uiState.value = SettingsUiState(
|
||||
playSound = repository.playSound,
|
||||
enableLog = repository.enableLog,
|
||||
injectMenu = repository.injectMenu,
|
||||
manuallyRotate = repository.manuallyRotate,
|
||||
halModeEnabled = repository.halModeEnabled
|
||||
)
|
||||
}
|
||||
fun onPlaySoundToggled() {
|
||||
_uiState.update { currentState ->
|
||||
val newState = !currentState.playSound
|
||||
repository.playSound = newState
|
||||
currentState.copy(playSound = newState)
|
||||
}
|
||||
}
|
||||
|
||||
fun onEnableLogToggled() {
|
||||
_uiState.update { currentState ->
|
||||
val newState = !currentState.enableLog
|
||||
repository.enableLog = newState
|
||||
currentState.copy(enableLog = newState)
|
||||
}
|
||||
}
|
||||
|
||||
fun onInjectMenuToggled() {
|
||||
_uiState.update { currentState ->
|
||||
val newState = !currentState.injectMenu
|
||||
repository.injectMenu = newState
|
||||
currentState.copy(injectMenu = newState)
|
||||
}
|
||||
}
|
||||
fun onManuallyRotateToggled() {
|
||||
_uiState.update { currentState ->
|
||||
val newState = !currentState.manuallyRotate
|
||||
repository.manuallyRotate = newState
|
||||
currentState.copy(manuallyRotate = newState)
|
||||
}
|
||||
}
|
||||
|
||||
fun onHalModeToggled() {
|
||||
_uiState.update { currentState ->
|
||||
val newState = !currentState.halModeEnabled
|
||||
repository.halModeEnabled = newState
|
||||
|
||||
val success = HalConfigManager.writeConfig(
|
||||
enabled = newState,
|
||||
sourceMode = repository.halSourceMode,
|
||||
videoPath = repository.halVideoPath,
|
||||
rtspUrl = repository.halRtspUrl
|
||||
)
|
||||
if (success) {
|
||||
Log.i(TAG, "HAL mode ${if (newState) "enabled" else "disabled"}, config written")
|
||||
} else {
|
||||
Log.w(TAG, "HAL mode toggled but config write failed (root required)")
|
||||
}
|
||||
|
||||
currentState.copy(halModeEnabled = newState)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
data class SettingsUiState(
|
||||
val playSound: Boolean = false,
|
||||
val enableLog: Boolean = false,
|
||||
val injectMenu: Boolean = false,
|
||||
val manuallyRotate: Boolean = false,
|
||||
val halModeEnabled: Boolean = false
|
||||
)
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
package com.nothing.camera2magic.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import android.content.ContentUris
|
||||
import android.graphics.Bitmap
|
||||
import android.net.Uri
|
||||
import android.provider.MediaStore
|
||||
import android.util.Size
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import java.lang.Exception
|
||||
|
||||
data class SpotlightUiState(
|
||||
val moduleEnabled: Boolean = true,
|
||||
val selectedMediaSource: MediaSource = MediaSource.LOCAL,
|
||||
val currentType: MediaType = MediaType.VIDEO,
|
||||
)
|
||||
|
||||
class SpotlightViewModel(
|
||||
private val app: Application,
|
||||
private val repository: ConfigRepository
|
||||
) : ViewModel() {
|
||||
|
||||
private val _thumbnails = MutableStateFlow<Map<MediaType, Bitmap?>>(emptyMap())
|
||||
val thumbnails = _thumbnails.asStateFlow()
|
||||
|
||||
private val _uiState = MutableStateFlow(SpotlightUiState())
|
||||
val uiState = _uiState.asStateFlow()
|
||||
|
||||
init {
|
||||
loadInitialSettings()
|
||||
performHealthCheckAndRefresh()
|
||||
}
|
||||
|
||||
fun onModuleToggled() {
|
||||
_uiState.update { currentState ->
|
||||
val newState = !currentState.moduleEnabled
|
||||
repository.moduleEnabled = newState
|
||||
currentState.copy(moduleEnabled = newState)
|
||||
}
|
||||
}
|
||||
fun selectedMediaSourceFrom(value: Int) {
|
||||
val source = MediaSource.fromValue(value)
|
||||
_uiState.update { currentState ->
|
||||
repository.mediaSource = value
|
||||
currentState.copy(selectedMediaSource = source)
|
||||
}
|
||||
}
|
||||
|
||||
fun setCurrentMediaType(type: MediaType) {
|
||||
_uiState.update { currentState ->
|
||||
repository.localMediaType = type.value
|
||||
currentState.copy(currentType = type)
|
||||
}
|
||||
}
|
||||
|
||||
fun onMediaSelected(type: MediaType, uri: Uri?) {
|
||||
if (uri == null) return
|
||||
val mediaId = try {
|
||||
uri.lastPathSegment?.toLongOrNull()
|
||||
} catch (_: kotlin.Exception) { null }
|
||||
if (mediaId != null) {
|
||||
saveMediaId(type, mediaId)
|
||||
loadAndVerifyMedia(type, mediaId)
|
||||
}
|
||||
}
|
||||
fun clearMediaBy(type: MediaType) {
|
||||
when (type) {
|
||||
MediaType.VIDEO -> repository.videoId = -1L
|
||||
MediaType.IMAGE -> repository.imageId = -1L
|
||||
}
|
||||
updateThumbnailState(type, null)
|
||||
}
|
||||
fun performHealthCheckAndRefresh() {
|
||||
MediaType.entries.forEach { type ->
|
||||
loadAndVerifyMedia(type)
|
||||
}
|
||||
}
|
||||
|
||||
private fun saveMediaId(type: MediaType, id: Long) {
|
||||
when (type) {
|
||||
MediaType.VIDEO -> repository.videoId = id
|
||||
MediaType.IMAGE -> repository.imageId = id
|
||||
}
|
||||
}
|
||||
private fun loadInitialSettings() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
moduleEnabled = repository.moduleEnabled,
|
||||
selectedMediaSource = MediaSource.fromValue(repository.mediaSource),
|
||||
currentType = MediaType.fromValue(repository.localMediaType)
|
||||
)
|
||||
}
|
||||
}
|
||||
private fun getMediaId(type: MediaType): Long {
|
||||
return when (type) {
|
||||
MediaType.VIDEO -> repository.videoId
|
||||
MediaType.IMAGE -> repository.imageId
|
||||
}
|
||||
}
|
||||
private fun loadAndVerifyMedia(type: MediaType, mediaIdOverride: Long? = null) {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val mediaId = mediaIdOverride ?: getMediaId(type)
|
||||
if (mediaId == -1L) {
|
||||
updateThumbnailState(type, null)
|
||||
return@launch
|
||||
}
|
||||
|
||||
var thumbnail: Bitmap? = null
|
||||
var isMediaValid = false
|
||||
|
||||
try {
|
||||
val contentUri = when (type) {
|
||||
MediaType.VIDEO -> MediaStore.Video.Media.EXTERNAL_CONTENT_URI
|
||||
MediaType.IMAGE -> MediaStore.Images.Media.EXTERNAL_CONTENT_URI
|
||||
}
|
||||
|
||||
val uri = ContentUris.withAppendedId(contentUri, mediaId)
|
||||
app.contentResolver.openFileDescriptor(uri, "r")?.use {
|
||||
isMediaValid = true
|
||||
thumbnail = app.contentResolver.loadThumbnail(uri, Size(720, 1280), null)
|
||||
}
|
||||
} catch (_: Exception) {
|
||||
isMediaValid = false
|
||||
}
|
||||
if (isMediaValid) {
|
||||
updateThumbnailState(type, thumbnail)
|
||||
} else {
|
||||
updateThumbnailState(type, null)
|
||||
if (mediaIdOverride == null) {
|
||||
saveMediaId(type, -1L)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
private fun updateThumbnailState(type: MediaType, thumbnail: Bitmap?) {
|
||||
_thumbnails.update { currentMap ->
|
||||
currentMap + (type to thumbnail)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
package com.nothing.camera2magic.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
|
||||
|
||||
class ViewModelFactory(
|
||||
private val app: Application,
|
||||
private val repository: ConfigRepository) : ViewModelProvider.Factory {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||
return when {
|
||||
modelClass.isAssignableFrom(SpotlightViewModel::class.java) -> {
|
||||
SpotlightViewModel(app, repository) as T
|
||||
}
|
||||
modelClass.isAssignableFrom(SettingsViewModel::class.java) -> {
|
||||
SettingsViewModel(repository) as T
|
||||
}
|
||||
else -> throw IllegalArgumentException("Unknown ViewModel class: ${modelClass.name}")
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user