Initial commit

This commit is contained in:
defiQUG
2025-12-26 10:48:33 -08:00
commit 97f75e144f
270 changed files with 35886 additions and 0 deletions

View File

@@ -0,0 +1,58 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.meetings"
compileSdk = AppConfig.compileSdk
defaultConfig {
minSdk = AppConfig.minSdk
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = "17"
}
buildFeatures {
compose = true
}
composeOptions {
kotlinCompilerExtensionVersion = "1.5.4"
}
}
dependencies {
implementation(project(":core:common"))
implementation(project(":core:auth"))
implementation(project(":core:security"))
implementation(project(":modules:communications"))
implementation(platform(Dependencies.composeBom))
implementation(Dependencies.composeUi)
implementation(Dependencies.composeUiGraphics)
implementation(Dependencies.composeMaterial3)
implementation(Dependencies.androidxCoreKtx)
implementation(Dependencies.androidxLifecycleRuntimeKtx)
implementation(Dependencies.hiltAndroid)
kapt(Dependencies.hiltAndroidCompiler)
implementation(Dependencies.retrofit)
implementation(Dependencies.okHttp)
// WebRTC - TODO: Configure WebRTC dependency
// WebRTC library needs to be built from source or obtained separately
// Uncomment when WebRTC is available:
// implementation(Dependencies.webrtc)
}

View File

@@ -0,0 +1,27 @@
package com.smoa.modules.meetings
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.smoa.core.auth.RBACFramework
import com.smoa.modules.meetings.domain.MeetingsService
import com.smoa.modules.meetings.ui.MeetingsScreen
/**
* Meetings module - Encrypted coordination for meetings, briefings, and conferences.
*/
@Composable
fun MeetingsModule(
meetingsService: MeetingsService,
userRole: RBACFramework.Role,
userId: String,
modifier: Modifier = Modifier
) {
MeetingsScreen(
meetingsService = meetingsService,
userRole = userRole,
userId = userId,
modifier = modifier.fillMaxSize()
)
}

View File

@@ -0,0 +1,102 @@
package com.smoa.modules.meetings.domain
import com.smoa.core.auth.RBACFramework
import java.util.Date
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
/**
* Meeting room manager.
* Manages meeting rooms and access control.
*/
@Singleton
class MeetingRoomManager @Inject constructor() {
private val meetings = mutableMapOf<String, MeetingRoom>()
/**
* Get meeting by ID.
*/
fun getMeeting(meetingId: String): MeetingRoom? {
return meetings[meetingId]
}
/**
* Get available meetings for user based on role.
*/
fun getAvailableMeetings(userRole: RBACFramework.Role): List<MeetingRoom> {
return meetings.values.filter { meeting ->
hasAccess(meeting, userRole)
}
}
/**
* Check if user has access to meeting.
*/
fun hasAccess(meeting: MeetingRoom, userRole: RBACFramework.Role): Boolean {
// Admins can access all meetings
if (userRole == RBACFramework.Role.ADMIN) {
return true
}
// Check role authorization
return meeting.allowedRoles.contains(userRole)
}
/**
* Create a new meeting room.
*/
fun createMeeting(
name: String,
description: String?,
hostId: String,
allowedRoles: Set<RBACFramework.Role>
): MeetingRoom {
val meeting = MeetingRoom(
id = UUID.randomUUID().toString(),
name = name,
description = description,
hostId = hostId,
allowedRoles = allowedRoles,
allowScreenSharing = false, // Default: disabled
allowFileTransfer = false, // Default: disabled
allowExternalParticipants = false // Default: disabled per spec
)
meetings[meeting.id] = meeting
return meeting
}
/**
* Remove a meeting room.
*/
fun removeMeeting(meetingId: String) {
meetings.remove(meetingId)
}
}
/**
* Meeting room.
*/
data class MeetingRoom(
val id: String,
val name: String,
val description: String?,
val hostId: String,
val allowedRoles: Set<RBACFramework.Role>,
val allowScreenSharing: Boolean,
val allowFileTransfer: Boolean,
val allowExternalParticipants: Boolean
)
/**
* Meeting participant.
*/
data class Participant(
val userId: String,
val userName: String,
val role: RBACFramework.Role,
val joinedAt: Date,
val isHost: Boolean = false
)

View File

@@ -0,0 +1,185 @@
package com.smoa.modules.meetings.domain
import com.smoa.core.auth.RBACFramework
import com.smoa.core.common.Result
import com.smoa.core.security.AuditLogger
import com.smoa.core.security.AuditEventType
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import java.util.Date
import javax.inject.Inject
import javax.inject.Singleton
/**
* Meetings service for secure audio and video conferencing.
* Supports role-restricted meeting room access and identity-verified participant entry.
*/
@Singleton
class MeetingsService @Inject constructor(
private val meetingRoomManager: MeetingRoomManager,
private val videoTransport: VideoTransport,
private val auditLogger: AuditLogger,
private val rbacFramework: RBACFramework
) {
private val _currentMeeting = MutableStateFlow<MeetingRoom?>(null)
val currentMeeting: StateFlow<MeetingRoom?> = _currentMeeting.asStateFlow()
private val _participants = MutableStateFlow<List<Participant>>(emptyList())
val participants: StateFlow<List<Participant>> = _participants.asStateFlow()
/**
* Join a meeting room.
*/
suspend fun joinMeeting(
meetingId: String,
userRole: RBACFramework.Role,
userId: String,
requireStepUpAuth: Boolean = true
): Result<MeetingRoom> {
val meeting = meetingRoomManager.getMeeting(meetingId) ?: return Result.Error(
IllegalArgumentException("Meeting not found: $meetingId")
)
// Check authorization
if (!meetingRoomManager.hasAccess(meeting, userRole)) {
return Result.Error(SecurityException("Access denied to meeting: $meetingId"))
}
// Step-up authentication required for joining
if (requireStepUpAuth) {
// This would trigger step-up authentication in UI
// For now, we assume it's handled by the caller
}
// Join meeting via video transport
val joinResult = videoTransport.joinMeeting(meetingId, userId)
return when (joinResult) {
is Result.Success -> {
_currentMeeting.value = meeting
val participant = Participant(
userId = userId,
userName = userId, // Would be resolved from user service
role = userRole,
joinedAt = Date(),
isHost = meeting.hostId == userId
)
_participants.value = _participants.value + participant
auditLogger.logEvent(
AuditEventType.MEETING_JOINED,
mapOf(
"meetingId" to meetingId,
"userId" to userId,
"timestamp" to Date().toString()
)
)
Result.Success(meeting)
}
is Result.Error -> joinResult
is Result.Loading -> Result.Error(Exception("Unexpected loading state"))
}
}
/**
* Leave current meeting.
*/
suspend fun leaveMeeting(): Result<Unit> {
val meeting = _currentMeeting.value ?: return Result.Error(
IllegalStateException("Not in any meeting")
)
val result = videoTransport.leaveMeeting(meeting.id)
when (result) {
is Result.Success -> {
_currentMeeting.value = null
_participants.value = emptyList()
auditLogger.logEvent(
AuditEventType.MEETING_LEFT,
mapOf(
"meetingId" to meeting.id,
"timestamp" to Date().toString()
)
)
}
else -> {}
}
return result
}
/**
* Create a new meeting room.
*/
suspend fun createMeeting(
name: String,
description: String?,
hostId: String,
userRole: RBACFramework.Role,
allowedRoles: Set<RBACFramework.Role>? = null
): Result<MeetingRoom> {
// Only operators and admins can create meetings
if (userRole != RBACFramework.Role.OPERATOR && userRole != RBACFramework.Role.ADMIN) {
return Result.Error(SecurityException("Only operators and admins can create meetings"))
}
val meeting = meetingRoomManager.createMeeting(
name = name,
description = description,
hostId = hostId,
allowedRoles = allowedRoles ?: setOf(RBACFramework.Role.OPERATOR, RBACFramework.Role.ADMIN)
)
auditLogger.logEvent(
AuditEventType.MEETING_CREATED,
mapOf(
"meetingId" to meeting.id,
"hostId" to hostId,
"timestamp" to Date().toString()
)
)
return Result.Success(meeting)
}
/**
* Get available meetings for user.
*/
suspend fun getAvailableMeetings(userRole: RBACFramework.Role): List<MeetingRoom> {
return meetingRoomManager.getAvailableMeetings(userRole)
}
/**
* Toggle screen sharing (policy-controlled).
*/
suspend fun toggleScreenSharing(enabled: Boolean): Result<Unit> {
val meeting = _currentMeeting.value ?: return Result.Error(
IllegalStateException("Not in any meeting")
)
// Check if screen sharing is allowed by policy
if (!meeting.allowScreenSharing) {
return Result.Error(SecurityException("Screen sharing not allowed in this meeting"))
}
return videoTransport.toggleScreenSharing(enabled)
}
/**
* Toggle file transfer (policy-controlled).
*/
suspend fun toggleFileTransfer(enabled: Boolean): Result<Unit> {
val meeting = _currentMeeting.value ?: return Result.Error(
IllegalStateException("Not in any meeting")
)
// Check if file transfer is allowed by policy
if (!meeting.allowFileTransfer) {
return Result.Error(SecurityException("File transfer not allowed in this meeting"))
}
return videoTransport.toggleFileTransfer(enabled)
}
}

View File

@@ -0,0 +1,135 @@
package com.smoa.modules.meetings.domain
import com.smoa.core.common.Result
import com.smoa.modules.communications.domain.WebRTCPeerConnection
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Singleton
/**
* Video transport for encrypted video conferencing.
* Uses WebRTC for peer-to-peer encrypted audio/video transmission.
*/
@Singleton
class VideoTransport @Inject constructor(
private val webRTCManager: com.smoa.modules.communications.domain.WebRTCManager
) {
private val _connectionState = MutableStateFlow<MeetingConnectionState>(MeetingConnectionState.Disconnected)
val connectionState: StateFlow<MeetingConnectionState> = _connectionState.asStateFlow()
private var currentMeetingId: String? = null
private var isScreenSharing = false
private var isFileTransferEnabled = false
private var peerConnection: WebRTCPeerConnection? = null
/**
* Join a meeting.
*/
suspend fun joinMeeting(meetingId: String, userId: String): Result<Unit> {
return try {
_connectionState.value = MeetingConnectionState.Connecting(meetingId)
// Initialize WebRTC peer connection (audio + video)
val connectionResult = webRTCManager.initializePeerConnection(meetingId, isAudioOnly = false)
when (connectionResult) {
is Result.Success -> {
peerConnection = connectionResult.data
currentMeetingId = meetingId
// Start audio and video transmission
peerConnection?.let { connection ->
webRTCManager.startAudioTransmission(connection)
webRTCManager.startVideoTransmission(connection)
}
_connectionState.value = MeetingConnectionState.Connected(meetingId)
Result.Success(Unit)
}
is Result.Error -> {
_connectionState.value = MeetingConnectionState.Error(
connectionResult.exception.message ?: "Failed to connect"
)
Result.Error(connectionResult.exception)
}
is Result.Loading -> {
_connectionState.value = MeetingConnectionState.Error("Unexpected loading state")
Result.Error(Exception("Unexpected loading state"))
}
}
} catch (e: Exception) {
_connectionState.value = MeetingConnectionState.Error(e.message ?: "Unknown error")
Result.Error(e)
}
}
/**
* Leave current meeting.
*/
suspend fun leaveMeeting(meetingId: String): Result<Unit> {
return try {
if (isScreenSharing) {
toggleScreenSharing(false)
}
// Stop audio and video transmission
peerConnection?.let { connection ->
webRTCManager.stopAudioTransmission(connection)
webRTCManager.stopVideoTransmission(connection)
webRTCManager.closePeerConnection(connection)
}
peerConnection = null
currentMeetingId = null
_connectionState.value = MeetingConnectionState.Disconnected
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Toggle screen sharing.
*/
suspend fun toggleScreenSharing(enabled: Boolean): Result<Unit> {
return try {
// TODO: Implement actual screen sharing
// This would:
// 1. Create screen capture source
// 2. Create video track from screen capture
// 3. Add/replace video track in peer connection
// 4. Start/stop screen capture
isScreenSharing = enabled
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Toggle file transfer.
*/
suspend fun toggleFileTransfer(enabled: Boolean): Result<Unit> {
return try {
// TODO: Implement actual file transfer capability
isFileTransferEnabled = enabled
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
}
/**
* Meeting connection state.
*/
sealed class MeetingConnectionState {
object Disconnected : MeetingConnectionState()
data class Connecting(val meetingId: String) : MeetingConnectionState()
data class Connected(val meetingId: String) : MeetingConnectionState()
data class Error(val message: String) : MeetingConnectionState()
}

View File

@@ -0,0 +1,204 @@
package com.smoa.modules.meetings.ui
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.smoa.core.auth.RBACFramework
import com.smoa.modules.meetings.domain.MeetingRoom
import com.smoa.modules.meetings.domain.MeetingsService
/**
* Meetings screen with meeting list and controls.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MeetingsScreen(
meetingsService: MeetingsService,
userRole: RBACFramework.Role,
userId: String,
modifier: Modifier = Modifier
) {
var meetings by remember { mutableStateOf<List<MeetingRoom>>(emptyList()) }
var currentMeeting by remember { mutableStateOf<MeetingRoom?>(null) }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
// Load available meetings
LaunchedEffect(userRole) {
isLoading = true
errorMessage = null
try {
meetings = meetingsService.getAvailableMeetings(userRole)
} catch (e: Exception) {
errorMessage = e.message
} finally {
isLoading = false
}
}
// Observe current meeting
LaunchedEffect(Unit) {
meetingsService.currentMeeting.collect { meeting ->
currentMeeting = meeting
}
}
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = "Secure Meetings",
style = MaterialTheme.typography.headlineMedium
)
// Create meeting button (for operators/admins)
if (userRole == RBACFramework.Role.OPERATOR || userRole == RBACFramework.Role.ADMIN) {
Button(onClick = {
// Show create meeting dialog
}) {
Text("Create")
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// Current meeting indicator
currentMeeting?.let { meeting ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
),
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "In Meeting: ${meeting.name}",
style = MaterialTheme.typography.titleMedium
)
meeting.description?.let { desc ->
Text(
text = desc,
style = MaterialTheme.typography.bodySmall
)
}
Button(
onClick = {
// Leave meeting
},
modifier = Modifier.padding(top = 8.dp)
) {
Text("Leave Meeting")
}
}
}
Spacer(modifier = Modifier.height(16.dp))
}
// Error message
errorMessage?.let { error ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.errorContainer
),
modifier = Modifier.fillMaxWidth()
) {
Text(
text = error,
color = MaterialTheme.colorScheme.onErrorContainer,
modifier = Modifier.padding(16.dp)
)
}
Spacer(modifier = Modifier.height(8.dp))
}
// Meeting list
if (isLoading) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
} else {
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(meetings) { meeting ->
MeetingCard(
meeting = meeting,
isActive = currentMeeting?.id == meeting.id,
onClick = {
// Join meeting (would require step-up auth)
}
)
}
}
}
}
}
/**
* Meeting card.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun MeetingCard(
meeting: MeetingRoom,
isActive: Boolean,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
Card(
onClick = onClick,
modifier = modifier.fillMaxWidth(),
colors = CardDefaults.cardColors(
containerColor = if (isActive) {
MaterialTheme.colorScheme.primaryContainer
} else {
MaterialTheme.colorScheme.surface
}
)
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = meeting.name,
style = MaterialTheme.typography.titleMedium
)
meeting.description?.let { desc ->
Text(
text = desc,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (isActive) {
Text(
text = "Joined",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}