Guard call permission results and release video targets idempotently
This commit is contained in:
1 parent
c8ecf89424
commit
623344369f
2 files changed
+51
-33
No files matched your search
@@ -61,9 +61,6 @@ import androidx.compose.ui.platform.LocalDensity
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.unit.sp
|
||||
import androidx.core.content.ContextCompat
|
||||
import androidx.core.view.WindowCompat
|
||||
import androidx.core.view.WindowInsetsCompat
|
||||
import androidx.core.view.WindowInsetsControllerCompat
|
||||
import com.utun.chat.data.AudioRoute
|
||||
import com.utun.chat.data.LogManager
|
||||
import com.utun.chat.viewmodel.ChatViewModel
|
||||
@@ -147,6 +144,9 @@ private fun CallVideoButton(video: CallVideoEngine, showLabel: Boolean, onOption
|
||||
)
|
||||
}
|
||||
|
||||
/** Полноэкранный звонок: сигналинг/аудио/видео принадлежат контроллеру и сервису, minimize не завершает звонок.
|
||||
* При ACTIVE proximity блокирует ввод только у разговорного динамика без видео; видеопанели сохраняют TextureView при смене геометрии.
|
||||
* Ориентация окна фиксируется на время экрана и восстанавливается при выходе. */
|
||||
@Composable
|
||||
@OptIn(ExperimentalLayoutApi::class)
|
||||
fun CallScreen(
|
||||
@@ -228,22 +228,30 @@ fun CallScreen(
|
||||
// Геометрию видео меняет TextureView; Activity сохраняет ориентацию окна звонка.
|
||||
DisposableEffect(context) {
|
||||
val activity = context.findActivity(); val previous = activity?.requestedOrientation
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
LogManager.addLog("INFO", "call", "call screen orientation locked; video rotates independently")
|
||||
try {
|
||||
activity?.requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_LOCKED
|
||||
LogManager.addLog("INFO", "call", "call screen orientation locked; video rotates independently")
|
||||
} catch (e: Exception) { LogManager.addLog("ERROR", "call", "Cannot lock call orientation: $e") }
|
||||
onDispose {
|
||||
if (activity != null && previous != null && activity.requestedOrientation != previous) {
|
||||
activity.requestedOrientation = previous
|
||||
LogManager.addLog("INFO", "call", "video screen orientation restored=$previous")
|
||||
try {
|
||||
activity.requestedOrientation = previous
|
||||
LogManager.addLog("INFO", "call", "video screen orientation restored=$previous")
|
||||
} catch (e: Exception) { LogManager.addLog("ERROR", "call", "Cannot restore call orientation=$previous: $e") }
|
||||
}
|
||||
}
|
||||
}
|
||||
val hasBtPermission = Build.VERSION.SDK_INT < Build.VERSION_CODES.S ||
|
||||
ContextCompat.checkSelfPermission(context, Manifest.permission.BLUETOOTH_CONNECT) ==
|
||||
PackageManager.PERMISSION_GRANTED
|
||||
var bluetoothCallId by rememberSaveable { mutableStateOf<Long?>(null) }
|
||||
val btPermissionLauncher = rememberLauncherForActivityResult(
|
||||
ActivityResultContracts.RequestPermission()
|
||||
) { granted ->
|
||||
if (granted) viewModel.setAudioRoute(AudioRoute.HEADSET)
|
||||
val requestedCall = bluetoothCallId; bluetoothCallId = null
|
||||
LogManager.addLog(if (granted) "INFO" else "WARN", "call", "Bluetooth permission=$granted call=$requestedCall")
|
||||
if (granted && requestedCall != null && st?.callId == requestedCall && phase == CallPhase.ACTIVE)
|
||||
viewModel.setAudioRoute(AudioRoute.HEADSET)
|
||||
}
|
||||
|
||||
/* Пока телефон у уха (proximity) — глотаем Back, чтобы не свернуть звонок. */
|
||||
@@ -252,21 +260,7 @@ fun CallScreen(
|
||||
|
||||
/* При блокировке скрываем системные панели (иначе статус-бар светится поверх оверлея). */
|
||||
val hideBars = locked || (viewingVideo && !controlsVisible)
|
||||
DisposableEffect(hideBars) {
|
||||
val act = context.findActivity()
|
||||
val controller = act?.let { WindowCompat.getInsetsController(it.window, it.window.decorView) }
|
||||
if (hideBars) {
|
||||
controller?.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
|
||||
controller?.hide(WindowInsetsCompat.Type.systemBars())
|
||||
LogManager.addLog("DEBUG", "CallScreen", "system bars hidden locked=$locked receiving=$receiving")
|
||||
}
|
||||
onDispose {
|
||||
if (hideBars) {
|
||||
controller?.show(WindowInsetsCompat.Type.systemBars())
|
||||
LogManager.addLog("DEBUG", "CallScreen", "system bars restored")
|
||||
}
|
||||
}
|
||||
}
|
||||
if (hideBars) ViewerFullscreenWindow("call id=${st?.callId}")
|
||||
|
||||
Box(
|
||||
modifier = Modifier.fillMaxSize().background(Color(0xFF0F1B22)).onGloballyPositioned { rootPosition = it.positionInRoot() }
|
||||
@@ -302,7 +296,8 @@ fun CallScreen(
|
||||
if (!hasVideo) Box(Modifier.fillMaxWidth().height(96.dp),
|
||||
contentAlignment = Alignment.Center) {
|
||||
Box(Modifier.size(96.dp).background(Color(0xFF1F6FEB), CircleShape), contentAlignment = Alignment.Center) {
|
||||
Text(name.take(1).uppercase(), fontSize = 40.sp, color = Color.White, fontWeight = FontWeight.Bold)
|
||||
Text(if (name.isEmpty()) "" else String(Character.toChars(name.codePointAt(0))).uppercase(),
|
||||
fontSize = 40.sp, color = Color.White, fontWeight = FontWeight.Bold)
|
||||
}
|
||||
}
|
||||
Spacer(Modifier.height(16.dp))
|
||||
@@ -386,7 +381,12 @@ fun CallScreen(
|
||||
if (hasBtPermission) {
|
||||
viewModel.setAudioRoute(AudioRoute.HEADSET)
|
||||
} else {
|
||||
btPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
bluetoothCallId = st?.callId
|
||||
try { btPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT) } catch (e: Exception) {
|
||||
bluetoothCallId = null
|
||||
LogManager.addLog("ERROR", "call", "Cannot request Bluetooth permission: $e")
|
||||
Toast.makeText(context, R.string.media_audio_failed, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
@@ -429,7 +429,11 @@ fun CallScreen(
|
||||
videoOptionsRequested = true; controlsVisible = true
|
||||
}, requestPermission = {
|
||||
permissionVideo = engine
|
||||
cameraPermissionLauncher.launch(Manifest.permission.CAMERA)
|
||||
try { cameraPermissionLauncher.launch(Manifest.permission.CAMERA) } catch (e: Exception) {
|
||||
permissionVideo = null
|
||||
LogManager.addLog("ERROR", "call", "Cannot request call camera permission: $e")
|
||||
Toast.makeText(context, R.string.camera_unavailable, Toast.LENGTH_LONG).show()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import com.utun.chat.data.CallVideoBitrateRange
|
||||
import com.utun.chat.data.CallVideoEngine
|
||||
import com.utun.chat.data.CallVideoFormat
|
||||
import com.utun.chat.data.StreamVideoCodec
|
||||
import com.utun.chat.data.StreamPreviewTarget
|
||||
import com.utun.chat.R
|
||||
|
||||
internal enum class CallVideoOption { CAMERA, CODEC, BITRATE, RESOLUTION }
|
||||
@@ -185,7 +186,7 @@ internal fun CallVideoStage(video: CallVideoEngine, expanded: CallVideoSource?,
|
||||
"frame=${frameWidth.value}x${frameHeight.value}dp; preview fits available space")
|
||||
}
|
||||
// Обе поверхности живут до конца видеосессии, даже пока аватар скрывает пустой слот.
|
||||
for (source in CallVideoSource.entries) key(source) {
|
||||
for (source in CallVideoSource.entries) key(video, source) {
|
||||
val large = expanded == source
|
||||
val index = if (source == CallVideoSource.REMOTE && localVisible) 1 else 0
|
||||
val x = if (expanded != null) with(density) { maxWidth.toPx() } - widthPx - gapPx
|
||||
@@ -246,6 +247,8 @@ private fun CallVideoPlayback(video: CallVideoEngine, onTap: () -> Unit, modifie
|
||||
|
||||
private class CallVideoTexture(context: Context, private val video: CallVideoEngine) : TextureView(context), ViewTreeObserver.OnPreDrawListener {
|
||||
private var attached: Surface? = null
|
||||
private var attachedTexture: SurfaceTexture? = null
|
||||
private val ownedTextures = java.util.Collections.newSetFromMap(java.util.IdentityHashMap<SurfaceTexture, Boolean>())
|
||||
private var closed = false
|
||||
private var format: CallVideoFormat? = null
|
||||
private var large = false
|
||||
@@ -263,13 +266,24 @@ private class CallVideoTexture(context: Context, private val video: CallVideoEng
|
||||
isOpaque = false
|
||||
surfaceTextureListener = object : SurfaceTextureListener {
|
||||
override fun onSurfaceTextureAvailable(texture: SurfaceTexture, width: Int, height: Int) {
|
||||
if (closed) return
|
||||
val surface = Surface(texture); attached = surface
|
||||
// RX worker освобождает Surface/texture только после остановки MediaCodec.
|
||||
video.surface(surface) { surface.release(); texture.release() }; fit()
|
||||
try {
|
||||
val target = StreamPreviewTarget(texture)
|
||||
ownedTextures.add(texture)
|
||||
if (closed) { target.release(); return }
|
||||
closeOutput()
|
||||
attached = target.surface; attachedTexture = texture
|
||||
// RX worker передаёт Surface/texture в идемпотентный cleanup после остановки decoder.
|
||||
video.surface(target.surface) { target.release() }; fit()
|
||||
} catch (e: Exception) {
|
||||
LogManager.addLog("ERROR", "call", "video UI surface attach failed texture=${System.identityHashCode(texture)}: $e")
|
||||
}
|
||||
}
|
||||
override fun onSurfaceTextureSizeChanged(texture: SurfaceTexture, width: Int, height: Int) { fit() }
|
||||
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean { closeOutput(); return false }
|
||||
override fun onSurfaceTextureDestroyed(texture: SurfaceTexture): Boolean {
|
||||
if (!ownedTextures.remove(texture)) return true
|
||||
if (attachedTexture === texture) closeOutput()
|
||||
return false
|
||||
}
|
||||
override fun onSurfaceTextureUpdated(texture: SurfaceTexture) {
|
||||
updateAt = System.nanoTime(); updates++
|
||||
}
|
||||
@@ -320,7 +334,7 @@ private class CallVideoTexture(context: Context, private val video: CallVideoEng
|
||||
})
|
||||
com.utun.chat.data.LogManager.addLog("DEBUG", "call", "video fit view=${width}x$height frame=${w}x$h expanded=$large")
|
||||
}
|
||||
private fun closeOutput() { attached?.let(video::detachSurface); attached = null }
|
||||
private fun closeOutput() { attached?.let(video::detachSurface); attached = null; attachedTexture = null }
|
||||
fun close() {
|
||||
closed = true
|
||||
closeOutput()
|
||||
|
||||
Reference in new issue
Block a user