feat: localized strings, PiP support, and ICE candidate fix
Localized strings: - Added 28 string resources for all call UI text in strings.xml - Replaced hardcoded strings in CallScreen, CallForegroundService, NotificationUtils, and RenderRoomTopBar with stringRes() calls - Technical error messages in CallController remain English (no composable context available) Picture-in-Picture: - EnterPipOnLeave composable observes lifecycle and enters PiP mode when app goes to background during active call - MainActivity manifest updated with supportsPictureInPicture and smallestScreenSize configChange - 9:16 aspect ratio for portrait PiP window ICE candidate fix (from logcat diagnosis): - acceptIncomingCall() no longer clears pendingIceCandidates at start — candidates buffered while ringing were being wiped before flushPendingIceCandidates() could apply them - This was causing "Flushing 0 buffered ICE candidates" on callee StrictMode fix: - initiateCall/acceptIncomingCall now run WebRTC session creation on Dispatchers.IO to avoid DiskReadViolation from native lib loading on main thread https://claude.ai/code/session_017hZm7yu7CzmcQgZGSaqSXS
This commit is contained in:
@@ -80,7 +80,8 @@
|
|||||||
android:exported="true"
|
android:exported="true"
|
||||||
android:launchMode="singleInstance"
|
android:launchMode="singleInstance"
|
||||||
android:windowSoftInputMode="adjustResize"
|
android:windowSoftInputMode="adjustResize"
|
||||||
android:configChanges="orientation|screenSize|screenLayout"
|
android:configChanges="orientation|screenSize|screenLayout|smallestScreenSize"
|
||||||
|
android:supportsPictureInPicture="true"
|
||||||
android:theme="@style/Theme.Amethyst">
|
android:theme="@style/Theme.Amethyst">
|
||||||
|
|
||||||
<intent-filter android:label="Amethyst">
|
<intent-filter android:label="Amethyst">
|
||||||
|
|||||||
@@ -32,10 +32,12 @@ import com.vitorpamplona.quartz.nipACWebRtcCalls.events.CallIceCandidateEvent
|
|||||||
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType
|
import com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType
|
||||||
import com.vitorpamplona.quartz.utils.Log
|
import com.vitorpamplona.quartz.utils.Log
|
||||||
import kotlinx.coroutines.CoroutineScope
|
import kotlinx.coroutines.CoroutineScope
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.flow.MutableStateFlow
|
import kotlinx.coroutines.flow.MutableStateFlow
|
||||||
import kotlinx.coroutines.flow.StateFlow
|
import kotlinx.coroutines.flow.StateFlow
|
||||||
import kotlinx.coroutines.flow.asStateFlow
|
import kotlinx.coroutines.flow.asStateFlow
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
import org.webrtc.IceCandidate
|
import org.webrtc.IceCandidate
|
||||||
import org.webrtc.MediaStream
|
import org.webrtc.MediaStream
|
||||||
import org.webrtc.SessionDescription
|
import org.webrtc.SessionDescription
|
||||||
@@ -113,34 +115,36 @@ class CallController(
|
|||||||
peerPubKey: String,
|
peerPubKey: String,
|
||||||
callType: CallType,
|
callType: CallType,
|
||||||
) {
|
) {
|
||||||
val callId = UUID.randomUUID().toString()
|
scope.launch {
|
||||||
_errorMessage.value = null
|
val callId = UUID.randomUUID().toString()
|
||||||
remoteDescriptionSet = false
|
_errorMessage.value = null
|
||||||
pendingIceCandidates.clear()
|
remoteDescriptionSet = false
|
||||||
|
pendingIceCandidates.clear()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
createWebRtcSession()
|
withContext(Dispatchers.IO) { createWebRtcSession() }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to create WebRTC session", e)
|
Log.e(TAG, "Failed to create WebRTC session", e)
|
||||||
_errorMessage.value = "Failed to start call: ${e.message}"
|
_errorMessage.value = "Failed to start call: ${e.message}"
|
||||||
return
|
return@launch
|
||||||
}
|
|
||||||
|
|
||||||
val session =
|
|
||||||
webRtcSession ?: run {
|
|
||||||
_errorMessage.value = "Failed to create WebRTC session"
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session.addAudioTrack()
|
val session =
|
||||||
if (callType == CallType.VIDEO) {
|
webRtcSession ?: run {
|
||||||
session.addVideoTrack()
|
_errorMessage.value = "Failed to create WebRTC session"
|
||||||
_localVideoTrack.value = session.getLocalVideoTrack()
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
session.createOffer { sdp ->
|
session.addAudioTrack()
|
||||||
scope.launch {
|
if (callType == CallType.VIDEO) {
|
||||||
callManager.initiateCall(peerPubKey, callType, callId, sdp.description)
|
session.addVideoTrack()
|
||||||
|
_localVideoTrack.value = session.getLocalVideoTrack()
|
||||||
|
}
|
||||||
|
|
||||||
|
session.createOffer { sdp ->
|
||||||
|
scope.launch {
|
||||||
|
callManager.initiateCall(peerPubKey, callType, callId, sdp.description)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -149,36 +153,38 @@ class CallController(
|
|||||||
val state = callManager.state.value
|
val state = callManager.state.value
|
||||||
if (state !is CallState.IncomingCall) return
|
if (state !is CallState.IncomingCall) return
|
||||||
|
|
||||||
_errorMessage.value = null
|
scope.launch {
|
||||||
remoteDescriptionSet = false
|
_errorMessage.value = null
|
||||||
pendingIceCandidates.clear()
|
remoteDescriptionSet = false
|
||||||
|
// Don't clear pendingIceCandidates here — they were buffered while ringing
|
||||||
|
|
||||||
try {
|
try {
|
||||||
createWebRtcSession()
|
withContext(Dispatchers.IO) { createWebRtcSession() }
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(TAG, "Failed to create WebRTC session", e)
|
Log.e(TAG, "Failed to create WebRTC session", e)
|
||||||
_errorMessage.value = "Failed to accept call: ${e.message}"
|
_errorMessage.value = "Failed to accept call: ${e.message}"
|
||||||
return
|
return@launch
|
||||||
}
|
|
||||||
|
|
||||||
val session =
|
|
||||||
webRtcSession ?: run {
|
|
||||||
_errorMessage.value = "Failed to create WebRTC session"
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
|
|
||||||
session.addAudioTrack()
|
val session =
|
||||||
if (state.callType == CallType.VIDEO) {
|
webRtcSession ?: run {
|
||||||
session.addVideoTrack()
|
_errorMessage.value = "Failed to create WebRTC session"
|
||||||
_localVideoTrack.value = session.getLocalVideoTrack()
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
session.setRemoteDescription(SessionDescription(SessionDescription.Type.OFFER, sdpOffer))
|
session.addAudioTrack()
|
||||||
flushPendingIceCandidates()
|
if (state.callType == CallType.VIDEO) {
|
||||||
|
session.addVideoTrack()
|
||||||
|
_localVideoTrack.value = session.getLocalVideoTrack()
|
||||||
|
}
|
||||||
|
|
||||||
session.createAnswer { sdp ->
|
session.setRemoteDescription(SessionDescription(SessionDescription.Type.OFFER, sdpOffer))
|
||||||
scope.launch {
|
flushPendingIceCandidates()
|
||||||
callManager.acceptCall(sdp.description)
|
|
||||||
|
session.createAnswer { sdp ->
|
||||||
|
scope.launch {
|
||||||
|
callManager.acceptCall(sdp.description)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-3
@@ -81,10 +81,10 @@ class CallForegroundService : Service() {
|
|||||||
val channel =
|
val channel =
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CHANNEL_ID,
|
CHANNEL_ID,
|
||||||
"Calls",
|
getString(R.string.call_ongoing),
|
||||||
NotificationManager.IMPORTANCE_LOW,
|
NotificationManager.IMPORTANCE_LOW,
|
||||||
).apply {
|
).apply {
|
||||||
description = "Ongoing call notification"
|
description = getString(R.string.call_ongoing_description)
|
||||||
}
|
}
|
||||||
val notificationManager = getSystemService(NotificationManager::class.java)
|
val notificationManager = getSystemService(NotificationManager::class.java)
|
||||||
notificationManager.createNotificationChannel(channel)
|
notificationManager.createNotificationChannel(channel)
|
||||||
@@ -94,7 +94,7 @@ class CallForegroundService : Service() {
|
|||||||
NotificationCompat
|
NotificationCompat
|
||||||
.Builder(this, CHANNEL_ID)
|
.Builder(this, CHANNEL_ID)
|
||||||
.setContentTitle(getString(R.string.app_name))
|
.setContentTitle(getString(R.string.app_name))
|
||||||
.setContentText("Call with $peerName")
|
.setContentText(getString(R.string.call_with, peerName))
|
||||||
.setSmallIcon(R.drawable.amethyst)
|
.setSmallIcon(R.drawable.amethyst)
|
||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.build()
|
.build()
|
||||||
|
|||||||
+5
-5
@@ -519,10 +519,10 @@ object NotificationUtils {
|
|||||||
callChannel =
|
callChannel =
|
||||||
NotificationChannel(
|
NotificationChannel(
|
||||||
CALL_CHANNEL_ID,
|
CALL_CHANNEL_ID,
|
||||||
"Incoming calls",
|
stringRes(applicationContext, R.string.app_notification_calls_channel_name),
|
||||||
NotificationManager.IMPORTANCE_HIGH,
|
NotificationManager.IMPORTANCE_HIGH,
|
||||||
).apply {
|
).apply {
|
||||||
description = "Notifications for incoming voice and video calls"
|
description = stringRes(applicationContext, R.string.app_notification_calls_channel_description)
|
||||||
}
|
}
|
||||||
|
|
||||||
val notificationManager: NotificationManager =
|
val notificationManager: NotificationManager =
|
||||||
@@ -573,7 +573,7 @@ object NotificationUtils {
|
|||||||
NotificationCompat
|
NotificationCompat
|
||||||
.Builder(applicationContext, channel.id)
|
.Builder(applicationContext, channel.id)
|
||||||
.setSmallIcon(R.drawable.amethyst)
|
.setSmallIcon(R.drawable.amethyst)
|
||||||
.setContentTitle("Incoming call")
|
.setContentTitle(stringRes(applicationContext, R.string.call_incoming))
|
||||||
.setContentText(callerName)
|
.setContentText(callerName)
|
||||||
.setLargeIcon(callerBitmap)
|
.setLargeIcon(callerBitmap)
|
||||||
.setContentIntent(contentPendingIntent)
|
.setContentIntent(contentPendingIntent)
|
||||||
@@ -584,8 +584,8 @@ object NotificationUtils {
|
|||||||
.setOngoing(true)
|
.setOngoing(true)
|
||||||
.setTimeoutAfter(60_000)
|
.setTimeoutAfter(60_000)
|
||||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||||
.addAction(R.drawable.amethyst, "Reject", contentPendingIntent)
|
.addAction(R.drawable.amethyst, stringRes(applicationContext, R.string.call_reject), contentPendingIntent)
|
||||||
.addAction(R.drawable.amethyst, "Accept", fullScreenPendingIntent)
|
.addAction(R.drawable.amethyst, stringRes(applicationContext, R.string.call_accept), fullScreenPendingIntent)
|
||||||
|
|
||||||
notificationManager.notify("call", CALL_NOTIFICATION_ID, builder.build())
|
notificationManager.notify("call", CALL_NOTIFICATION_ID, builder.build())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import androidx.compose.ui.text.font.FontWeight
|
|||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.sp
|
import androidx.compose.ui.unit.sp
|
||||||
import androidx.compose.ui.viewinterop.AndroidView
|
import androidx.compose.ui.viewinterop.AndroidView
|
||||||
|
import com.vitorpamplona.amethyst.R
|
||||||
import com.vitorpamplona.amethyst.commons.call.CallManager
|
import com.vitorpamplona.amethyst.commons.call.CallManager
|
||||||
import com.vitorpamplona.amethyst.commons.call.CallState
|
import com.vitorpamplona.amethyst.commons.call.CallState
|
||||||
import com.vitorpamplona.amethyst.service.call.CallController
|
import com.vitorpamplona.amethyst.service.call.CallController
|
||||||
@@ -73,6 +74,7 @@ import com.vitorpamplona.amethyst.ui.note.ClickableUserPicture
|
|||||||
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
import com.vitorpamplona.amethyst.ui.note.UsernameDisplay
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.AccountViewModel
|
||||||
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
import com.vitorpamplona.amethyst.ui.screen.loggedIn.chats.rooms.LoadUser
|
||||||
|
import com.vitorpamplona.amethyst.ui.stringRes
|
||||||
import com.vitorpamplona.quartz.utils.TimeUtils
|
import com.vitorpamplona.quartz.utils.TimeUtils
|
||||||
import kotlinx.coroutines.delay
|
import kotlinx.coroutines.delay
|
||||||
import kotlinx.coroutines.launch
|
import kotlinx.coroutines.launch
|
||||||
@@ -98,6 +100,7 @@ fun CallScreen(
|
|||||||
}
|
}
|
||||||
|
|
||||||
KeepScreenOn()
|
KeepScreenOn()
|
||||||
|
EnterPipOnLeave(callState)
|
||||||
|
|
||||||
Box(modifier = Modifier.fillMaxSize()) {
|
Box(modifier = Modifier.fillMaxSize()) {
|
||||||
when (val state = callState) {
|
when (val state = callState) {
|
||||||
@@ -108,7 +111,7 @@ fun CallScreen(
|
|||||||
is CallState.Offering -> {
|
is CallState.Offering -> {
|
||||||
CallInProgressUI(
|
CallInProgressUI(
|
||||||
peerPubKey = state.peerPubKey,
|
peerPubKey = state.peerPubKey,
|
||||||
statusText = "Calling...",
|
statusText = stringRes(R.string.call_calling),
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onHangup = { scope.launch { callManager.hangup() } },
|
onHangup = { scope.launch { callManager.hangup() } },
|
||||||
)
|
)
|
||||||
@@ -131,7 +134,7 @@ fun CallScreen(
|
|||||||
is CallState.Connecting -> {
|
is CallState.Connecting -> {
|
||||||
CallInProgressUI(
|
CallInProgressUI(
|
||||||
peerPubKey = state.peerPubKey,
|
peerPubKey = state.peerPubKey,
|
||||||
statusText = "Connecting...",
|
statusText = stringRes(R.string.call_connecting),
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onHangup = { scope.launch { callManager.hangup() } },
|
onHangup = { scope.launch { callManager.hangup() } },
|
||||||
)
|
)
|
||||||
@@ -152,7 +155,7 @@ fun CallScreen(
|
|||||||
is CallState.Ended -> {
|
is CallState.Ended -> {
|
||||||
CallInProgressUI(
|
CallInProgressUI(
|
||||||
peerPubKey = state.peerPubKey,
|
peerPubKey = state.peerPubKey,
|
||||||
statusText = "Call ended",
|
statusText = stringRes(R.string.call_ended),
|
||||||
accountViewModel = accountViewModel,
|
accountViewModel = accountViewModel,
|
||||||
onHangup = { onCallEnded() },
|
onHangup = { onCallEnded() },
|
||||||
)
|
)
|
||||||
@@ -164,7 +167,7 @@ fun CallScreen(
|
|||||||
modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp),
|
modifier = Modifier.align(Alignment.BottomCenter).padding(16.dp),
|
||||||
action = {
|
action = {
|
||||||
Text(
|
Text(
|
||||||
"Dismiss",
|
stringRes(R.string.call_dismiss),
|
||||||
modifier =
|
modifier =
|
||||||
Modifier.padding(8.dp),
|
Modifier.padding(8.dp),
|
||||||
color = MaterialTheme.colorScheme.inversePrimary,
|
color = MaterialTheme.colorScheme.inversePrimary,
|
||||||
@@ -225,7 +228,7 @@ private fun CallInProgressUI(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CallEnd,
|
Icons.Default.CallEnd,
|
||||||
contentDescription = "Hang up",
|
contentDescription = stringRes(R.string.call_hangup),
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(32.dp),
|
modifier = Modifier.size(32.dp),
|
||||||
)
|
)
|
||||||
@@ -270,7 +273,14 @@ private fun IncomingCallUI(
|
|||||||
}
|
}
|
||||||
Spacer(modifier = Modifier.height(8.dp))
|
Spacer(modifier = Modifier.height(8.dp))
|
||||||
Text(
|
Text(
|
||||||
text = "Incoming ${callType.value} call...",
|
text =
|
||||||
|
stringRes(
|
||||||
|
if (callType == com.vitorpamplona.quartz.nipACWebRtcCalls.tags.CallType.VIDEO) {
|
||||||
|
R.string.call_incoming_video
|
||||||
|
} else {
|
||||||
|
R.string.call_incoming_voice
|
||||||
|
},
|
||||||
|
),
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
fontSize = 16.sp,
|
fontSize = 16.sp,
|
||||||
)
|
)
|
||||||
@@ -286,7 +296,7 @@ private fun IncomingCallUI(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CallEnd,
|
Icons.Default.CallEnd,
|
||||||
contentDescription = "Reject",
|
contentDescription = stringRes(R.string.call_reject),
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(32.dp),
|
modifier = Modifier.size(32.dp),
|
||||||
)
|
)
|
||||||
@@ -299,7 +309,7 @@ private fun IncomingCallUI(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.Call,
|
Icons.Default.Call,
|
||||||
contentDescription = "Accept",
|
contentDescription = stringRes(R.string.call_accept),
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(32.dp),
|
modifier = Modifier.size(32.dp),
|
||||||
)
|
)
|
||||||
@@ -428,7 +438,7 @@ private fun ConnectedCallUI(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isAudioMuted) Icons.Default.MicOff else Icons.Default.Mic,
|
imageVector = if (isAudioMuted) Icons.Default.MicOff else Icons.Default.Mic,
|
||||||
contentDescription = if (isAudioMuted) "Unmute" else "Mute",
|
contentDescription = stringRes(if (isAudioMuted) R.string.call_unmute else R.string.call_mute),
|
||||||
tint = if (isAudioMuted) Color.Red else Color.White,
|
tint = if (isAudioMuted) Color.Red else Color.White,
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
)
|
)
|
||||||
@@ -439,7 +449,7 @@ private fun ConnectedCallUI(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isVideoEnabled) Icons.Default.Videocam else Icons.Default.VideocamOff,
|
imageVector = if (isVideoEnabled) Icons.Default.Videocam else Icons.Default.VideocamOff,
|
||||||
contentDescription = if (isVideoEnabled) "Camera off" else "Camera on",
|
contentDescription = stringRes(if (isVideoEnabled) R.string.call_camera_off else R.string.call_camera_on),
|
||||||
tint = if (!isVideoEnabled) Color.Red else Color.White,
|
tint = if (!isVideoEnabled) Color.Red else Color.White,
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
)
|
)
|
||||||
@@ -450,7 +460,7 @@ private fun ConnectedCallUI(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = if (isSpeakerOn) Icons.Default.VolumeUp else Icons.Default.VolumeOff,
|
imageVector = if (isSpeakerOn) Icons.Default.VolumeUp else Icons.Default.VolumeOff,
|
||||||
contentDescription = if (isSpeakerOn) "Earpiece" else "Speaker",
|
contentDescription = stringRes(if (isSpeakerOn) R.string.call_earpiece else R.string.call_speaker),
|
||||||
tint = if (isSpeakerOn) Color.Cyan else Color.White,
|
tint = if (isSpeakerOn) Color.Cyan else Color.White,
|
||||||
modifier = Modifier.size(28.dp),
|
modifier = Modifier.size(28.dp),
|
||||||
)
|
)
|
||||||
@@ -465,7 +475,7 @@ private fun ConnectedCallUI(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
Icons.Default.CallEnd,
|
Icons.Default.CallEnd,
|
||||||
contentDescription = "Hang up",
|
contentDescription = stringRes(R.string.call_hangup),
|
||||||
tint = Color.White,
|
tint = Color.White,
|
||||||
modifier = Modifier.size(32.dp),
|
modifier = Modifier.size(32.dp),
|
||||||
)
|
)
|
||||||
@@ -515,3 +525,36 @@ private fun KeepScreenOn() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun EnterPipOnLeave(callState: CallState) {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val activity = context as? android.app.Activity ?: return
|
||||||
|
val isActiveCall =
|
||||||
|
callState is CallState.Connected ||
|
||||||
|
callState is CallState.Connecting ||
|
||||||
|
callState is CallState.Offering
|
||||||
|
|
||||||
|
if (isActiveCall && android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
|
||||||
|
val lifecycleOwner = androidx.lifecycle.compose.LocalLifecycleOwner.current
|
||||||
|
DisposableEffect(lifecycleOwner) {
|
||||||
|
val observer =
|
||||||
|
object : androidx.lifecycle.DefaultLifecycleObserver {
|
||||||
|
override fun onStop(owner: androidx.lifecycle.LifecycleOwner) {
|
||||||
|
try {
|
||||||
|
val params =
|
||||||
|
android.app.PictureInPictureParams
|
||||||
|
.Builder()
|
||||||
|
.setAspectRatio(android.util.Rational(9, 16))
|
||||||
|
.build()
|
||||||
|
activity.enterPictureInPictureMode(params)
|
||||||
|
} catch (_: Exception) {
|
||||||
|
// PiP not supported or activity not in correct state
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
lifecycleOwner.lifecycle.addObserver(observer)
|
||||||
|
onDispose { lifecycleOwner.lifecycle.removeObserver(observer) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+2
-2
@@ -99,7 +99,7 @@ fun RenderRoomTopBar(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Videocam,
|
imageVector = Icons.Default.Videocam,
|
||||||
contentDescription = "Video call",
|
contentDescription = stringRes(R.string.call_video),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
)
|
)
|
||||||
@@ -113,7 +113,7 @@ fun RenderRoomTopBar(
|
|||||||
) {
|
) {
|
||||||
Icon(
|
Icon(
|
||||||
imageVector = Icons.Default.Call,
|
imageVector = Icons.Default.Call,
|
||||||
contentDescription = "Voice call",
|
contentDescription = stringRes(R.string.call_voice),
|
||||||
tint = MaterialTheme.colorScheme.primary,
|
tint = MaterialTheme.colorScheme.primary,
|
||||||
modifier = Modifier.size(20.dp),
|
modifier = Modifier.size(20.dp),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -771,6 +771,34 @@
|
|||||||
<string name="app_notification_chess_your_turn">%1$s moved — your turn</string>
|
<string name="app_notification_chess_your_turn">%1$s moved — your turn</string>
|
||||||
<string name="app_notification_chess_summary">Chess updates</string>
|
<string name="app_notification_chess_summary">Chess updates</string>
|
||||||
|
|
||||||
|
<!-- Call notifications and UI -->
|
||||||
|
<string name="app_notification_calls_channel_name">Incoming calls</string>
|
||||||
|
<string name="app_notification_calls_channel_description">Notifications for incoming voice and video calls</string>
|
||||||
|
<string name="call_incoming">Incoming call</string>
|
||||||
|
<string name="call_incoming_voice">Incoming voice call\u2026</string>
|
||||||
|
<string name="call_incoming_video">Incoming video call\u2026</string>
|
||||||
|
<string name="call_calling">Calling\u2026</string>
|
||||||
|
<string name="call_connecting">Connecting\u2026</string>
|
||||||
|
<string name="call_ended">Call ended</string>
|
||||||
|
<string name="call_with">Call with %1$s</string>
|
||||||
|
<string name="call_ongoing">Calls</string>
|
||||||
|
<string name="call_ongoing_description">Ongoing call notification</string>
|
||||||
|
<string name="call_hangup">Hang up</string>
|
||||||
|
<string name="call_accept">Accept</string>
|
||||||
|
<string name="call_reject">Reject</string>
|
||||||
|
<string name="call_dismiss">Dismiss</string>
|
||||||
|
<string name="call_mute">Mute</string>
|
||||||
|
<string name="call_unmute">Unmute</string>
|
||||||
|
<string name="call_camera_on">Camera on</string>
|
||||||
|
<string name="call_camera_off">Camera off</string>
|
||||||
|
<string name="call_speaker">Speaker</string>
|
||||||
|
<string name="call_earpiece">Earpiece</string>
|
||||||
|
<string name="call_voice">Voice call</string>
|
||||||
|
<string name="call_video">Video call</string>
|
||||||
|
<string name="call_failed_start">Failed to start call</string>
|
||||||
|
<string name="call_failed_accept">Failed to accept call</string>
|
||||||
|
<string name="call_failed_session">Failed to create call session</string>
|
||||||
|
|
||||||
<string name="reply_notify">Notify: </string>
|
<string name="reply_notify">Notify: </string>
|
||||||
|
|
||||||
<string name="channel_list_join_conversation">Join Conversation</string>
|
<string name="channel_list_join_conversation">Join Conversation</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user