Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
<uses-permission android:name="android.permission.CAMERA" />

<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
Expand Down
220 changes: 161 additions & 59 deletions app/src/main/java/com/example/intra/CallScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,12 @@ import coil.compose.AsyncImage
import coil.request.ImageRequest
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalContext

import androidx.compose.ui.viewinterop.AndroidView
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material.icons.filled.VideocamOff
import androidx.compose.material.icons.filled.Cameraswitch
import org.webrtc.SurfaceViewRenderer
import org.webrtc.RendererCommon

@Composable
fun CallScreen(
Expand All @@ -42,11 +47,14 @@ fun CallScreen(
onRejectCall: () -> Unit,
onAcceptCall: () -> Unit,
onToggleMute: () -> Unit,
onToggleSpeaker: () -> Unit
onToggleSpeaker: () -> Unit,
onToggleVideo: () -> Unit,
onSwitchCamera: () -> Unit,
webRTCClient: WebRTCClient? = null
) {
// 🔥 TIMER STATE - Call duration track karne ke liye
var callSeconds by remember(state.status) { mutableStateOf(0) }

val context = LocalContext.current

// 🔥 TIMER LOGIC - Connected hone pe start, status change pe auto stop
LaunchedEffect(state.status) {
Expand All @@ -73,6 +81,26 @@ fun CallScreen(
),
contentAlignment = Alignment.Center
) {
// --- VIDEO CALL BACKGROUND ---
if (state.isVideoCall && state.status == CallStatus.CONNECTED) {
AndroidView(
factory = { ctx ->
SurfaceViewRenderer(ctx).apply {
if (webRTCClient != null) {
init(webRTCClient.eglBaseContext, null)
setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
webRTCClient.setupRemoteVideoRenderer(this)
}
}
},
modifier = Modifier.fillMaxSize(),
onRelease = { renderer ->
webRTCClient?.removeRemoteVideoRenderer(renderer)
renderer.release()
}
)
}

Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
Expand All @@ -82,63 +110,119 @@ fun CallScreen(
) {
// --- TOP SECTION: Avatar & Status (Same) ---
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Box(
modifier = Modifier.size(170.dp),
contentAlignment = Alignment.Center
) {
AvatarGlowRing(
isActive = state.status != CallStatus.CONNECTED
)
if (!state.isVideoCall || state.status != CallStatus.CONNECTED) {
AvatarGlowRing(
isActive = state.status != CallStatus.CONNECTED
)

Box(
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
if (state.profilePhotoUrl != null) {
AsyncImage(
model = state.profilePhotoUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
} else {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(60.dp)
)
Box(
modifier = Modifier
.size(120.dp)
.clip(CircleShape)
.background(Color.White.copy(alpha = 0.2f)),
contentAlignment = Alignment.Center
) {
if (state.profilePhotoUrl != null) {
AsyncImage(
model = state.profilePhotoUrl,
contentDescription = null,
modifier = Modifier.fillMaxSize()
)
} else {
Icon(
imageVector = Icons.Default.Person,
contentDescription = null,
tint = Color.White,
modifier = Modifier.size(60.dp)
)
}
}
}
}

Spacer(modifier = Modifier.height(24.dp))
Text(
text = state.targetUser,
color = Color.White,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))

AnimatedContent(
targetState = state.status,
transitionSpec = { fadeIn() togetherWith fadeOut() },
label = "callStatus"
) { status ->
if (!state.isVideoCall || state.status != CallStatus.CONNECTED) {
Spacer(modifier = Modifier.height(24.dp))
Text(
text = when (status) {
CallStatus.OUTGOING -> "Calling..."
CallStatus.INCOMING -> "Incoming Call..."
CallStatus.CONNECTED -> formatTime(callSeconds)
else -> ""
},
color = Color.White.copy(alpha = 0.7f),
fontSize = 18.sp
text = state.targetUser,
color = Color.White,
fontSize = 30.sp,
fontWeight = FontWeight.Bold
)
Spacer(modifier = Modifier.height(8.dp))

AnimatedContent(
targetState = state.status,
transitionSpec = { fadeIn() togetherWith fadeOut() },
label = "callStatus"
) { status ->
Text(
text = when (status) {
CallStatus.OUTGOING -> "Calling..."
CallStatus.INCOMING -> "Incoming Call..."
CallStatus.CONNECTED -> formatTime(callSeconds)
else -> ""
},
color = Color.White.copy(alpha = 0.7f),
fontSize = 18.sp
)
}
} else {
// Transparent overlay texts for Video Call
Column(
modifier = Modifier.fillMaxWidth().padding(16.dp),
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = state.targetUser,
color = Color.White,
fontSize = 24.sp,
fontWeight = FontWeight.Bold
)
Text(
text = formatTime(callSeconds),
color = Color.White,
fontSize = 16.sp
)
}
}
}

// --- LOCAL VIDEO (PiP) ---
if (state.isVideoCall && state.status == CallStatus.CONNECTED && state.isVideoEnabled) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f)
.padding(bottom = 32.dp, end = 16.dp),
contentAlignment = Alignment.BottomEnd
) {
Box(
modifier = Modifier
.width(100.dp)
.height(150.dp)
.clip(androidx.compose.foundation.shape.RoundedCornerShape(12.dp))
.background(Color.Black)
) {
AndroidView(
factory = { ctx ->
SurfaceViewRenderer(ctx).apply {
if (webRTCClient != null) {
init(webRTCClient.eglBaseContext, null)
setScalingType(RendererCommon.ScalingType.SCALE_ASPECT_FILL)
setZOrderMediaOverlay(true)
webRTCClient.setupLocalVideoRenderer(this)
}
}
},
modifier = Modifier.fillMaxSize(),
onRelease = { renderer ->
webRTCClient?.removeLocalVideoRenderer(renderer)
renderer.release()
}
)
}
}
} else {
Spacer(modifier = Modifier.weight(1f))
}

// --- BOTTOM SECTION: Buttons ---
Expand Down Expand Up @@ -176,6 +260,22 @@ fun CallScreen(
onClick = onToggleMute
)

if (state.isVideoCall) {
CallActionButton(
icon = if (state.isVideoEnabled) Icons.Default.Videocam else Icons.Default.VideocamOff,
color = if (state.isVideoEnabled) Color.White else Color.White.copy(alpha = 0.2f),
iconTint = if (state.isVideoEnabled) Color.Black else Color.White,
onClick = onToggleVideo
)

CallActionButton(
icon = Icons.Default.Cameraswitch,
color = Color.White.copy(alpha = 0.2f),
iconTint = Color.White,
onClick = onSwitchCamera
)
}

// End Call Button (Same for Connected/Outgoing)
CallActionButton(
icon = Icons.Default.CallEnd,
Expand All @@ -184,12 +284,14 @@ fun CallScreen(
onClick = onEndCall
)

CallActionButton(
icon = if (state.isSpeakerOn) Icons.Default.VolumeUp else Icons.Default.VolumeOff,
color = if (state.isSpeakerOn) Color.White else Color.White.copy(alpha = 0.2f),
iconTint = if (state.isSpeakerOn) Color.Black else Color.White,
onClick = onToggleSpeaker
)
if (!state.isVideoCall) {
CallActionButton(
icon = if (state.isSpeakerOn) Icons.Default.VolumeUp else Icons.Default.VolumeOff,
color = if (state.isSpeakerOn) Color.White else Color.White.copy(alpha = 0.2f),
iconTint = if (state.isSpeakerOn) Color.Black else Color.White,
onClick = onToggleSpeaker
)
}
}
}
}
Expand Down
5 changes: 4 additions & 1 deletion app/src/main/java/com/example/intra/CallState.kt
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,8 @@ data class CallState(
val targetUser: String = "", // Kisse baat ho rahi hai
val profilePhotoUrl: String? = null, // ✅ ADD THIS
val isMuted: Boolean = false,
val isSpeakerOn: Boolean = true
val isSpeakerOn: Boolean = true,
val isVideoCall: Boolean = false, // Video call indicator
val isVideoEnabled: Boolean = true, // Track if camera is ON/OFF
val isFrontCamera: Boolean = true // Track which camera is active
)
19 changes: 15 additions & 4 deletions app/src/main/java/com/example/intra/CallViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -20,25 +20,28 @@ class CallViewModel : ViewModel() {

// --- Actions ---

fun onIncomingCall(sender: String, profilePhotoUrl: String? = null) {
fun onIncomingCall(sender: String, profilePhotoUrl: String? = null, isVideoCall: Boolean = false) {
if (callActive) return
callActive = true
isRinging.value = true
callState.value = CallState(
status = CallStatus.INCOMING,
targetUser = sender,
profilePhotoUrl = profilePhotoUrl
profilePhotoUrl = profilePhotoUrl,
isVideoCall = isVideoCall,
isSpeakerOn = isVideoCall // Video call mein by default speaker ON hota hai
)
isRinging.value = true // 🔔 Start Ringing
}

fun onStartOutgoingCall(target: String, profilePhotoUrl: String? = null) {
fun onStartOutgoingCall(target: String, profilePhotoUrl: String? = null, isVideoCall: Boolean = false) {
callActive = true
callState.value = CallState(
status = CallStatus.OUTGOING,
targetUser = target,
profilePhotoUrl = profilePhotoUrl,
isSpeakerOn = true
isSpeakerOn = true, // By default keeping it true, WebRTCClient handles it
isVideoCall = isVideoCall
)
// Outgoing me ringtone nahi bajti, tone bajti hai (wo baad me dekhenge)
}
Expand Down Expand Up @@ -66,4 +69,12 @@ class CallViewModel : ViewModel() {
fun updateSpeakerState(isSpeakerOn: Boolean) {
callState.value = callState.value.copy(isSpeakerOn = isSpeakerOn)
}

fun updateVideoState(isVideoEnabled: Boolean) {
callState.value = callState.value.copy(isVideoEnabled = isVideoEnabled)
}

fun switchCamera(isFrontCamera: Boolean) {
callState.value = callState.value.copy(isFrontCamera = isFrontCamera)
}
}
13 changes: 10 additions & 3 deletions app/src/main/java/com/example/intra/ChatScreen.kt
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import androidx.compose.material.icons.filled.Call
import androidx.compose.material.icons.filled.Group
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.SmartToy
import androidx.compose.material.icons.filled.Videocam
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
Expand Down Expand Up @@ -66,7 +67,7 @@ fun ChatScreen(
receiverPhotoUrl: String? = null,
onAttachClick: () -> Unit,
onBackClick: () -> Unit,
onStartCall: () -> Unit,
onStartCall: (isVideo: Boolean) -> Unit,
) {
val listState = rememberLazyListState()
var videoUrlToPlay by remember { mutableStateOf<String?>(null) }
Expand Down Expand Up @@ -213,10 +214,16 @@ fun ChatScreen(
}
},
actions = {
IconButton(onClick = onStartCall) {
IconButton(onClick = { onStartCall(true) }) {
Icon(
imageVector = Icons.Default.Videocam,
contentDescription = "Video Call"
)
}
IconButton(onClick = { onStartCall(false) }) {
Icon(
imageVector = Icons.Default.Call,
contentDescription = "Call"
contentDescription = "Audio Call"
)
}
}
Expand Down
3 changes: 2 additions & 1 deletion app/src/main/java/com/example/intra/ChatViewModel.kt
Original file line number Diff line number Diff line change
Expand Up @@ -339,13 +339,14 @@ class ChatViewModel(
// 📞 CALL REQUEST (Outgoing)
// ===============================

fun sendCallRequest(receiver: String) {
fun sendCallRequest(receiver: String, isVideoCall: Boolean = false) {
val myPhoto = settingsManager.getMyPhoto()
val json = JSONObject().apply {
put("type", "call_request")
put("sender", currentUsername)
put("receiver", receiver)
put("profile_photo", myPhoto)
put("is_video_call", isVideoCall)
}
WsManager.send(json.toString())
}
Expand Down
Loading