Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
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