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.atf"
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(platform(Dependencies.composeBom))
implementation(Dependencies.composeUi)
implementation(Dependencies.composeMaterial3)
implementation(Dependencies.androidxCoreKtx)
implementation(Dependencies.hiltAndroid)
kapt(Dependencies.hiltAndroidCompiler)
implementation(Dependencies.roomRuntime)
implementation(Dependencies.roomKtx)
kapt(Dependencies.roomCompiler)
implementation(Dependencies.retrofit)
implementation(Dependencies.okHttp)
implementation(Dependencies.retrofitGson)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.coroutinesAndroid)
}

View File

@@ -0,0 +1,25 @@
package com.smoa.modules.atf
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ATFModule(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "ATF Forms",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -0,0 +1,16 @@
package com.smoa.modules.atf.data
import androidx.room.Database
import androidx.room.RoomDatabase
// TODO: Add entities when implementing storage
// Temporarily commented out to allow build to proceed
// @Database(
// entities = [],
// version = 1,
// exportSchema = false
// )
abstract class ATFFormDatabase : RoomDatabase() {
// DAOs will be added here
}

View File

@@ -0,0 +1,22 @@
package com.smoa.modules.atf.domain
import java.util.Date
/**
* ATF Form 1 - Application to Make and Register a Firearm
*/
data class ATFForm1(
val formId: String,
val applicationDate: Date,
val applicantInfo: PersonInfo,
val firearmType: String,
val firearmDescription: String,
val serialNumber: String?,
val caliber: String,
val barrelLength: String,
val overallLength: String,
val purpose: String,
val signatures: List<DigitalSignature>,
val status: FormStatus
)

View File

@@ -0,0 +1,29 @@
package com.smoa.modules.atf.domain
import java.util.Date
/**
* ATF Form 4 - Application for Tax Paid Transfer and Registration
*/
data class ATFForm4(
val formId: String,
val applicationDate: Date,
val transferorInfo: PersonInfo,
val transfereeInfo: PersonInfo,
val firearmDescription: String,
val serialNumber: String,
val manufacturer: String,
val model: String,
val caliber: String,
val transferType: TransferType,
val signatures: List<DigitalSignature>,
val status: FormStatus
)
enum class TransferType {
INDIVIDUAL,
TRUST,
CORPORATION,
PARTNERSHIP
}

View File

@@ -0,0 +1,55 @@
package com.smoa.modules.atf.domain
import java.util.Date
/**
* ATF Form 4473 - Firearms Transaction Record
*/
data class ATFForm4473(
val formId: String,
val transactionDate: Date,
val firearmManufacturer: String,
val firearmModel: String,
val firearmSerialNumber: String,
val firearmCaliber: String,
val firearmType: FirearmType,
val transfereeInfo: PersonInfo,
val transferorInfo: PersonInfo,
val nicsCheckNumber: String?,
val nicsCheckDate: Date?,
val signatures: List<DigitalSignature>,
val status: FormStatus
)
enum class FirearmType {
HANDGUN,
RIFLE,
SHOTGUN,
OTHER
}
enum class FormStatus {
DRAFT,
SUBMITTED,
APPROVED,
REJECTED,
PENDING
}
data class PersonInfo(
val name: String,
val address: String,
val city: String,
val state: String,
val zipCode: String,
val dateOfBirth: Date,
val socialSecurityNumber: String? // Last 4 digits or full
)
data class DigitalSignature(
val signatureId: String,
val signerId: String,
val signatureDate: Date,
val signatureData: ByteArray
)

View File

@@ -0,0 +1,98 @@
package com.smoa.modules.atf.domain
import com.smoa.core.security.AuditLogger
import com.smoa.core.security.AuditEventType
import java.util.Date
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
/**
* ATF Form service for form validation and submission.
*/
@Singleton
class ATFService @Inject constructor(
private val auditLogger: AuditLogger
) {
/**
* Validate Form 4473 data.
*/
fun validateForm4473(form: ATFForm4473): ValidationResult {
val errors = mutableListOf<String>()
if (form.firearmSerialNumber.isBlank()) {
errors.add("Firearm serial number is required")
}
if (form.transfereeInfo.name.isBlank()) {
errors.add("Transferee name is required")
}
if (form.transfereeInfo.dateOfBirth.after(Date())) {
errors.add("Date of birth cannot be in the future")
}
return if (errors.isEmpty()) {
ValidationResult.Success
} else {
ValidationResult.Failure(errors)
}
}
/**
* Submit Form 4473 to ATF eTrace system.
* Note: Requires federal API approval and OAuth 2.0 authentication.
*/
suspend fun submitForm4473(form: ATFForm4473): Result<SubmissionResult> {
return try {
// Validate form
val validation = validateForm4473(form)
if (validation is ValidationResult.Failure) {
return Result.failure(
IllegalArgumentException("Form validation failed: ${validation.errors.joinToString()}")
)
}
// TODO: Integrate with ATF eTrace API (requires federal approval)
// For now, simulate submission
val submissionResult = SubmissionResult(
submissionId = UUID.randomUUID().toString(),
formId = form.formId,
status = SubmissionStatus.SUBMITTED,
submittedAt = Date()
)
auditLogger.logEvent(
AuditEventType.POLICY_UPDATE,
userId = form.transfereeInfo.name,
module = "atf",
details = "ATF Form 4473 submitted: ${form.formId}"
)
Result.success(submissionResult)
} catch (e: Exception) {
Result.failure(e)
}
}
}
sealed class ValidationResult {
object Success : ValidationResult()
data class Failure(val errors: List<String>) : ValidationResult()
}
data class SubmissionResult(
val submissionId: String,
val formId: String,
val status: SubmissionStatus,
val submittedAt: Date
)
enum class SubmissionStatus {
SUBMITTED,
PENDING,
APPROVED,
REJECTED
}

View File

@@ -0,0 +1,25 @@
package com.smoa.modules.atf.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ATFForm4473Screen(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "ATF Form 4473",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -0,0 +1,55 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.browser"
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(platform(Dependencies.composeBom))
implementation(Dependencies.composeUi)
implementation(Dependencies.composeUiGraphics)
implementation(Dependencies.composeMaterial3)
implementation(Dependencies.androidxCoreKtx)
implementation(Dependencies.androidxLifecycleRuntimeKtx)
implementation(Dependencies.hiltAndroid)
kapt(Dependencies.hiltAndroidCompiler)
// Testing
testImplementation(Dependencies.junit)
testImplementation(Dependencies.mockk)
testImplementation(Dependencies.coroutinesTest)
testImplementation(Dependencies.truth)
}

View File

@@ -0,0 +1,28 @@
package com.smoa.modules.browser
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.smoa.core.security.ScreenProtection
import com.smoa.modules.browser.domain.BrowserService
import com.smoa.modules.browser.domain.URLFilter
import com.smoa.modules.browser.ui.BrowserScreen
/**
* Browser module - Secure access to designated mission or agency web resources.
*/
@Composable
fun BrowserModule(
browserService: BrowserService,
urlFilter: URLFilter,
screenProtection: ScreenProtection,
modifier: Modifier = Modifier
) {
BrowserScreen(
browserService = browserService,
urlFilter = urlFilter,
screenProtection = screenProtection,
modifier = modifier.fillMaxSize()
)
}

View File

@@ -0,0 +1,31 @@
package com.smoa.modules.browser.di
import com.smoa.core.security.ScreenProtection
import com.smoa.core.security.VPNManager
import com.smoa.modules.browser.domain.BrowserService
import com.smoa.modules.browser.domain.URLFilter
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object BrowserModule {
@Provides
@Singleton
fun provideURLFilter(): URLFilter {
return URLFilter()
}
@Provides
@Singleton
fun provideBrowserService(
vpnManager: VPNManager,
urlFilter: URLFilter
): BrowserService {
return BrowserService(vpnManager, urlFilter)
}
}

View File

@@ -0,0 +1,77 @@
package com.smoa.modules.browser.domain
import com.smoa.core.security.VPNManager
import com.smoa.core.security.VPNRequiredException
import javax.inject.Inject
import javax.inject.Singleton
/**
* Browser service for controlled web browsing.
* Enforces VPN requirement and URL allow-list.
*/
@Singleton
class BrowserService @Inject constructor(
private val vpnManager: VPNManager,
private val urlFilter: URLFilter
) {
/**
* Check if URL is allowed.
*/
fun isURLAllowed(url: String): Boolean {
return urlFilter.isAllowed(url)
}
/**
* Navigate to URL with security checks.
*/
suspend fun navigateToURL(url: String): Result<String> {
// Enforce VPN requirement
try {
vpnManager.enforceVPNRequirement()
} catch (e: VPNRequiredException) {
return Result.failure(e)
}
// Check URL allow-list
if (!isURLAllowed(url)) {
return Result.failure(SecurityException("URL not in allow-list: $url"))
}
// Validate URL format
if (!isValidURL(url)) {
return Result.failure(IllegalArgumentException("Invalid URL format: $url"))
}
return Result.success(url)
}
/**
* Validate URL format.
*/
private fun isValidURL(url: String): Boolean {
return try {
java.net.URL(url)
true
} catch (e: Exception) {
false
}
}
/**
* Check if download is allowed.
*/
fun isDownloadAllowed(): Boolean {
// Downloads can be controlled by policy
// For now, downloads are disabled by default
return false
}
/**
* Check if external app sharing is allowed.
*/
fun isExternalSharingAllowed(): Boolean {
// External sharing is disabled by default per spec
return false
}
}

View File

@@ -0,0 +1,115 @@
package com.smoa.modules.browser.domain
import javax.inject.Inject
import javax.inject.Singleton
/**
* URL filter for allow-list management.
* Restricts browser to designated mission or agency web resources.
*/
@Singleton
class URLFilter @Inject constructor() {
private val allowedDomains = mutableSetOf<String>()
private val allowedPaths = mutableMapOf<String, Set<String>>()
init {
// Default allow-list (can be configured via policy)
// Add default mission/agency resources here
}
/**
* Check if URL is allowed.
*/
fun isAllowed(url: String): Boolean {
return try {
val urlObj = java.net.URL(url)
val host = urlObj.host
val path = urlObj.path
// Check if domain is allowed
if (!isDomainAllowed(host)) {
return false
}
// Check if path is allowed for this domain
if (!isPathAllowed(host, path)) {
return false
}
true
} catch (e: Exception) {
false
}
}
/**
* Check if domain is allowed.
*/
private fun isDomainAllowed(host: String): Boolean {
// Check exact match
if (allowedDomains.contains(host)) {
return true
}
// Check subdomain match
return allowedDomains.any { allowedDomain ->
host.endsWith(".$allowedDomain") || host == allowedDomain
}
}
/**
* Check if path is allowed for domain.
*/
private fun isPathAllowed(host: String, path: String): Boolean {
val allowedPathsForDomain = allowedPaths[host]
// If no path restrictions for this domain, allow all paths
if (allowedPathsForDomain == null || allowedPathsForDomain.isEmpty()) {
return true
}
// Check if path matches any allowed path
return allowedPathsForDomain.any { allowedPath ->
path.startsWith(allowedPath)
}
}
/**
* Add allowed domain.
*/
fun addAllowedDomain(domain: String) {
allowedDomains.add(domain)
}
/**
* Remove allowed domain.
*/
fun removeAllowedDomain(domain: String) {
allowedDomains.remove(domain)
allowedPaths.remove(domain)
}
/**
* Add allowed path for domain.
*/
fun addAllowedPath(domain: String, path: String) {
val paths = allowedPaths.getOrPut(domain) { mutableSetOf() } as MutableSet
paths.add(path)
}
/**
* Get all allowed domains.
*/
fun getAllowedDomains(): Set<String> {
return allowedDomains.toSet()
}
/**
* Clear all allowed domains and paths.
*/
fun clear() {
allowedDomains.clear()
allowedPaths.clear()
}
}

View File

@@ -0,0 +1,157 @@
package com.smoa.modules.browser.ui
import android.webkit.WebView
import android.webkit.WebViewClient
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView
import com.smoa.core.security.ScreenProtection
import com.smoa.modules.browser.domain.BrowserService
import com.smoa.modules.browser.domain.URLFilter
/**
* Controlled browser screen with VPN enforcement and URL filtering.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun BrowserScreen(
browserService: BrowserService,
urlFilter: URLFilter,
screenProtection: ScreenProtection,
modifier: Modifier = Modifier
) {
// Enable screen protection
screenProtection.EnableScreenProtection()
var currentURL by remember { mutableStateOf("") }
var urlInput by remember { mutableStateOf("") }
var errorMessage by remember { mutableStateOf<String?>(null) }
var isLoading by remember { mutableStateOf(false) }
val context = LocalContext.current
// WebView state
var webView by remember { mutableStateOf<WebView?>(null) }
// Navigate to URL
suspend fun navigateToURL(url: String) {
isLoading = true
errorMessage = null
browserService.navigateToURL(url)
.onSuccess { allowedURL ->
currentURL = allowedURL
webView?.loadUrl(allowedURL)
}
.onFailure { error ->
errorMessage = error.message
}
.also {
isLoading = false
}
}
Column(
modifier = modifier
.fillMaxSize()
.padding(8.dp)
) {
// URL bar
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(8.dp),
verticalAlignment = Alignment.CenterVertically
) {
OutlinedTextField(
value = urlInput,
onValueChange = { urlInput = it },
label = { Text("URL") },
modifier = Modifier.weight(1f),
singleLine = true,
enabled = !isLoading
)
Button(
onClick = {
if (urlInput.isNotBlank()) {
// Add https:// if no protocol specified
val url = if (!urlInput.startsWith("http://") && !urlInput.startsWith("https://")) {
"https://$urlInput"
} else {
urlInput
}
// Navigate (this would need to be in a coroutine scope)
// For now, just update the URL
currentURL = url
}
},
enabled = !isLoading && urlInput.isNotBlank()
) {
Text("Go")
}
}
Spacer(modifier = Modifier.height(8.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))
}
// Loading indicator
if (isLoading) {
LinearProgressIndicator(modifier = Modifier.fillMaxWidth())
Spacer(modifier = Modifier.height(8.dp))
}
// WebView
AndroidView(
factory = { ctx ->
WebView(ctx).apply {
settings.javaScriptEnabled = true
settings.domStorageEnabled = true
settings.allowFileAccess = false
settings.allowContentAccess = false
settings.setSupportZoom(true)
settings.builtInZoomControls = false
settings.displayZoomControls = false
webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView?, url: String?): Boolean {
// Check if URL is allowed before loading
url?.let {
if (!browserService.isURLAllowed(it)) {
errorMessage = "URL not in allow-list: $it"
return true // Block navigation
}
}
return false // Allow navigation
}
}
webView = this
}
},
modifier = Modifier
.fillMaxWidth()
.weight(1f)
)
}
}

View File

@@ -0,0 +1,98 @@
package com.smoa.modules.browser.domain
import com.smoa.core.security.VPNManager
import com.smoa.core.security.VPNRequiredException
import io.mockk.every
import io.mockk.mockk
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Test
/**
* Unit tests for BrowserService.
*/
class BrowserServiceTest {
private val vpnManager = mockk<VPNManager>(relaxed = true)
private val urlFilter = mockk<URLFilter>(relaxed = true)
private val browserService = BrowserService(vpnManager, urlFilter)
@Test
fun `isURLAllowed should delegate to URLFilter`() {
// Given
val url = "https://example.com"
every { urlFilter.isAllowed(url) } returns true
// When
val result = browserService.isURLAllowed(url)
// Then
assertTrue(result)
}
@Test
fun `navigateToURL should fail when VPN not connected`() = runTest {
// Given
val url = "https://example.com"
every { vpnManager.isVPNRequired() } returns true
every { vpnManager.isVPNConnected() } returns false
every { vpnManager.enforceVPNRequirement() } throws VPNRequiredException("VPN required")
// When
val result = browserService.navigateToURL(url)
// Then
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is VPNRequiredException)
}
@Test
fun `navigateToURL should fail when URL not in allow-list`() = runTest {
// Given
val url = "https://blocked.com"
every { vpnManager.isVPNRequired() } returns true
every { vpnManager.isVPNConnected() } returns true
every { urlFilter.isAllowed(url) } returns false
// When
val result = browserService.navigateToURL(url)
// Then
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is SecurityException)
}
@Test
fun `navigateToURL should succeed for allowed URL with VPN`() = runTest {
// Given
val url = "https://allowed.com"
every { vpnManager.isVPNRequired() } returns true
every { vpnManager.isVPNConnected() } returns true
every { urlFilter.isAllowed(url) } returns true
// When
val result = browserService.navigateToURL(url)
// Then
assertTrue(result.isSuccess)
assertEquals(url, result.getOrNull())
}
@Test
fun `isDownloadAllowed should return false by default`() {
// When
val result = browserService.isDownloadAllowed()
// Then
assertFalse(result)
}
@Test
fun `isExternalSharingAllowed should return false by default`() {
// When
val result = browserService.isExternalSharingAllowed()
// Then
assertFalse(result)
}
}

View File

@@ -0,0 +1,57 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.communications"
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(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.communications
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.communications.domain.CommunicationsService
import com.smoa.modules.communications.ui.CommunicationsScreen
/**
* Communications module - Mission voice communications using channelized, unit-based access.
*/
@Composable
fun CommunicationsModule(
communicationsService: CommunicationsService,
userRole: RBACFramework.Role,
userUnit: String?,
modifier: Modifier = Modifier
) {
CommunicationsScreen(
communicationsService = communicationsService,
userRole = userRole,
userUnit = userUnit,
modifier = modifier.fillMaxSize()
)
}

View File

@@ -0,0 +1,57 @@
package com.smoa.modules.communications.di
import android.content.Context
import com.smoa.core.security.AuditLogger
import com.smoa.modules.communications.domain.ChannelManager
import com.smoa.modules.communications.domain.CommunicationsService
import com.smoa.modules.communications.domain.VoiceTransport
import com.smoa.modules.communications.domain.WebRTCManager
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object CommunicationsModule {
@Provides
@Singleton
fun provideWebRTCManager(
@ApplicationContext context: Context
): WebRTCManager {
return WebRTCManager(context)
}
@Provides
@Singleton
fun provideChannelManager(): ChannelManager {
return ChannelManager()
}
@Provides
@Singleton
fun provideVoiceTransport(
webRTCManager: WebRTCManager
): VoiceTransport {
return VoiceTransport(webRTCManager)
}
@Provides
@Singleton
fun provideCommunicationsService(
channelManager: ChannelManager,
voiceTransport: VoiceTransport,
auditLogger: AuditLogger,
rbacFramework: com.smoa.core.auth.RBACFramework
): CommunicationsService {
return CommunicationsService(
channelManager,
voiceTransport,
auditLogger,
rbacFramework
)
}
}

View File

@@ -0,0 +1,103 @@
package com.smoa.modules.communications.domain
import com.smoa.core.auth.RBACFramework
import javax.inject.Inject
import javax.inject.Singleton
/**
* Channel manager for communication channels.
* Manages channel access based on role and unit authorization.
*/
@Singleton
class ChannelManager @Inject constructor() {
private val channels = mutableMapOf<String, Channel>()
init {
// Initialize default channels (can be loaded from policy/config)
// Example channels would be added here
}
/**
* Get channel by ID.
*/
fun getChannel(channelId: String): Channel? {
return channels[channelId]
}
/**
* Get available channels for user based on role and unit.
*/
fun getAvailableChannels(
userRole: RBACFramework.Role,
userUnit: String?
): List<Channel> {
return channels.values.filter { channel ->
hasAccess(channel, userRole, userUnit)
}
}
/**
* Check if user has access to channel.
*/
fun hasAccess(
channel: Channel,
userRole: RBACFramework.Role,
userUnit: String?
): Boolean {
// Admins can access all channels
if (userRole == RBACFramework.Role.ADMIN) {
return true
}
// Check unit authorization
if (channel.unitRestricted && userUnit != null && channel.allowedUnits.contains(userUnit)) {
return true
}
// Check role authorization
if (channel.allowedRoles.contains(userRole)) {
return true
}
return false
}
/**
* Register a channel.
*/
fun registerChannel(channel: Channel) {
channels[channel.id] = channel
}
/**
* Remove a channel.
*/
fun removeChannel(channelId: String) {
channels.remove(channelId)
}
}
/**
* Communication channel.
*/
data class Channel(
val id: String,
val name: String,
val description: String?,
val unitRestricted: Boolean,
val allowedUnits: Set<String>,
val allowedRoles: Set<RBACFramework.Role>,
val priority: ChannelPriority = ChannelPriority.NORMAL,
val encrypted: Boolean = true
)
/**
* Channel priority levels.
*/
enum class ChannelPriority {
LOW,
NORMAL,
HIGH,
ALERT
}

View File

@@ -0,0 +1,154 @@
package com.smoa.modules.communications.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
/**
* Communications service for radio-style voice communications.
* Supports multi-channel push-to-talk (PTT) with encrypted voice transport.
*/
@Singleton
class CommunicationsService @Inject constructor(
private val channelManager: ChannelManager,
private val voiceTransport: VoiceTransport,
private val auditLogger: AuditLogger,
private val rbacFramework: RBACFramework
) {
private val _currentChannel = MutableStateFlow<Channel?>(null)
val currentChannel: StateFlow<Channel?> = _currentChannel.asStateFlow()
private val _isPTTActive = MutableStateFlow(false)
val isPTTActive: StateFlow<Boolean> = _isPTTActive.asStateFlow()
/**
* Join a communication channel.
*/
suspend fun joinChannel(
channelId: String,
userRole: RBACFramework.Role,
userUnit: String?
): Result<Channel> {
val channel = channelManager.getChannel(channelId) ?: return Result.Error(
IllegalArgumentException("Channel not found: $channelId")
)
// Check authorization
if (!channelManager.hasAccess(channel, userRole, userUnit)) {
return Result.Error(SecurityException("Access denied to channel: $channelId"))
}
// Leave current channel if any
_currentChannel.value?.let { leaveChannel(it.id) }
// Join new channel
val joinResult = voiceTransport.joinChannel(channelId)
return when (joinResult) {
is Result.Success -> {
_currentChannel.value = channel
auditLogger.logEvent(
AuditEventType.CHANNEL_JOINED,
mapOf(
"channelId" to channelId,
"channelName" to channel.name,
"timestamp" to Date().toString()
)
)
Result.Success(channel)
}
is Result.Error -> joinResult
is Result.Loading -> Result.Error(Exception("Unexpected loading state"))
}
}
/**
* Leave current channel.
*/
suspend fun leaveChannel(channelId: String): Result<Unit> {
val result = voiceTransport.leaveChannel(channelId)
when (result) {
is Result.Success -> {
_currentChannel.value = null
auditLogger.logEvent(
AuditEventType.CHANNEL_LEFT,
mapOf(
"channelId" to channelId,
"timestamp" to Date().toString()
)
)
}
else -> {}
}
return result
}
/**
* Start push-to-talk (PTT).
*/
suspend fun startPTT(): Result<Unit> {
val channel = _currentChannel.value ?: return Result.Error(
IllegalStateException("Not connected to any channel")
)
val result = voiceTransport.startTransmission(channel.id)
when (result) {
is Result.Success -> {
_isPTTActive.value = true
auditLogger.logEvent(
AuditEventType.PTT_STARTED,
mapOf(
"channelId" to channel.id,
"timestamp" to Date().toString()
)
)
}
else -> {}
}
return result
}
/**
* Stop push-to-talk (PTT).
*/
suspend fun stopPTT(): Result<Unit> {
val channel = _currentChannel.value ?: return Result.Error(
IllegalStateException("Not connected to any channel")
)
val result = voiceTransport.stopTransmission(channel.id)
when (result) {
is Result.Success -> {
_isPTTActive.value = false
auditLogger.logEvent(
AuditEventType.PTT_STOPPED,
mapOf(
"channelId" to channel.id,
"timestamp" to Date().toString()
)
)
}
else -> {}
}
return result
}
/**
* Get available channels for user.
*/
suspend fun getAvailableChannels(
userRole: RBACFramework.Role,
userUnit: String?
): List<Channel> {
return channelManager.getAvailableChannels(userRole, userUnit)
}
}

View File

@@ -0,0 +1,140 @@
package com.smoa.modules.communications.domain
import com.smoa.core.common.Result
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Singleton
/**
* Voice transport for encrypted voice communication.
* Uses WebRTC for peer-to-peer encrypted voice transmission.
*/
@Singleton
class VoiceTransport @Inject constructor(
private val webRTCManager: WebRTCManager
) {
private val _connectionState = MutableStateFlow<ConnectionState>(ConnectionState.Disconnected)
val connectionState: StateFlow<ConnectionState> = _connectionState.asStateFlow()
private var currentChannelId: String? = null
private var isTransmitting = false
private var peerConnection: WebRTCPeerConnection? = null
/**
* Join a communication channel.
*/
suspend fun joinChannel(channelId: String): Result<Unit> {
return try {
_connectionState.value = ConnectionState.Connecting(channelId)
// Initialize WebRTC peer connection (audio only for voice)
val connectionResult = webRTCManager.initializePeerConnection(channelId, isAudioOnly = true)
when (connectionResult) {
is Result.Success -> {
peerConnection = connectionResult.data
currentChannelId = channelId
_connectionState.value = ConnectionState.Connected(channelId)
Result.Success(Unit)
}
is Result.Error -> {
_connectionState.value = ConnectionState.Error(connectionResult.exception.message ?: "Failed to connect")
Result.Error(connectionResult.exception)
}
is Result.Loading -> {
_connectionState.value = ConnectionState.Error("Unexpected loading state")
Result.Error(Exception("Unexpected loading state"))
}
}
} catch (e: Exception) {
_connectionState.value = ConnectionState.Error(e.message ?: "Unknown error")
Result.Error(e)
}
}
/**
* Leave current channel.
*/
suspend fun leaveChannel(channelId: String): Result<Unit> {
return try {
if (isTransmitting) {
stopTransmission(channelId)
}
// Close WebRTC peer connection
peerConnection?.let { connection ->
webRTCManager.closePeerConnection(connection)
}
peerConnection = null
currentChannelId = null
_connectionState.value = ConnectionState.Disconnected
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Start voice transmission (PTT).
*/
suspend fun startTransmission(channelId: String): Result<Unit> {
return try {
if (currentChannelId != channelId) {
return Result.Error(IllegalStateException("Not connected to channel: $channelId"))
}
val connection = peerConnection ?: return Result.Error(
IllegalStateException("No active peer connection")
)
// Start audio transmission via WebRTC
val result = webRTCManager.startAudioTransmission(connection)
when (result) {
is Result.Success -> {
isTransmitting = true
}
else -> {}
}
result
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Stop voice transmission (PTT release).
*/
suspend fun stopTransmission(channelId: String): Result<Unit> {
return try {
val connection = peerConnection ?: return Result.Error(
IllegalStateException("No active peer connection")
)
// Stop audio transmission via WebRTC
val result = webRTCManager.stopAudioTransmission(connection)
when (result) {
is Result.Success -> {
isTransmitting = false
}
else -> {}
}
result
} catch (e: Exception) {
Result.Error(e)
}
}
}
/**
* Connection state.
*/
sealed class ConnectionState {
object Disconnected : ConnectionState()
data class Connecting(val channelId: String) : ConnectionState()
data class Connected(val channelId: String) : ConnectionState()
data class Error(val message: String) : ConnectionState()
}

View File

@@ -0,0 +1,46 @@
package com.smoa.modules.communications.domain
/**
* WebRTC configuration for STUN/TURN servers and signaling.
*/
data class WebRTCConfig(
val stunServers: List<StunServer>,
val turnServers: List<TurnServer>,
val signalingServerUrl: String,
val iceCandidatePoolSize: Int = 10
) {
companion object {
/**
* Default configuration with public STUN servers.
* In production, use organization-specific STUN/TURN servers.
*/
fun default(): WebRTCConfig {
return WebRTCConfig(
stunServers = listOf(
StunServer("stun:stun.l.google.com:19302"),
StunServer("stun:stun1.l.google.com:19302")
),
turnServers = emptyList(), // TURN servers should be configured per deployment
signalingServerUrl = "", // Should be configured per deployment
iceCandidatePoolSize = 10
)
}
}
}
/**
* STUN server configuration.
*/
data class StunServer(
val url: String
)
/**
* TURN server configuration.
*/
data class TurnServer(
val url: String,
val username: String? = null,
val credential: String? = null
)

View File

@@ -0,0 +1,238 @@
package com.smoa.modules.communications.domain
import android.content.Context
import com.smoa.core.common.Result
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import javax.inject.Inject
import javax.inject.Singleton
/**
* WebRTC Manager for voice and video communication.
* Provides WebRTC peer connection management for Communications and Meetings modules.
*/
@Singleton
class WebRTCManager @Inject constructor(
private val context: Context
) {
private val config = WebRTCConfig.default()
private val peerConnections = mutableMapOf<String, WebRTCPeerConnection>()
private val _connectionState = MutableStateFlow<WebRTCConnectionState>(WebRTCConnectionState.Disconnected)
val connectionState: StateFlow<WebRTCConnectionState> = _connectionState.asStateFlow()
/**
* Initialize WebRTC peer connection.
*/
suspend fun initializePeerConnection(
channelId: String,
isAudioOnly: Boolean = false
): Result<WebRTCPeerConnection> {
return try {
_connectionState.value = WebRTCConnectionState.Connecting(channelId)
// Create peer connection configuration
val rtcConfig = createRTCConfiguration()
// TODO: Initialize actual WebRTC PeerConnection when library is fully integrated
// This would:
// 1. Initialize PeerConnectionFactory with options
// 2. Create PeerConnection with rtcConfig
// 3. Set up audio/video tracks based on isAudioOnly
// 4. Configure ICE candidates
// 5. Set up signaling channel
val peerConnection = WebRTCPeerConnection(
channelId = channelId,
isAudioOnly = isAudioOnly,
config = rtcConfig
)
peerConnections[channelId] = peerConnection
_connectionState.value = WebRTCConnectionState.Connected(channelId)
Result.Success(peerConnection)
} catch (e: Exception) {
_connectionState.value = WebRTCConnectionState.Error(e.message ?: "Unknown error")
Result.Error(e)
}
}
/**
* Create RTC configuration with STUN/TURN servers.
*/
private fun createRTCConfiguration(): RTCConfiguration {
val iceServers = mutableListOf<IceServer>()
// Add STUN servers
config.stunServers.forEach { stunServer ->
iceServers.add(IceServer(stunServer.url))
}
// Add TURN servers
config.turnServers.forEach { turnServer ->
iceServers.add(
IceServer(
url = turnServer.url,
username = turnServer.username,
credential = turnServer.credential
)
)
}
return RTCConfiguration(
iceServers = iceServers,
iceCandidatePoolSize = config.iceCandidatePoolSize
)
}
/**
* Close peer connection.
*/
suspend fun closePeerConnection(peerConnection: WebRTCPeerConnection): Result<Unit> {
return try {
// Stop all tracks
if (peerConnection.isAudioActive) {
stopAudioTransmission(peerConnection)
}
if (peerConnection.isVideoActive) {
stopVideoTransmission(peerConnection)
}
// TODO: Close actual WebRTC PeerConnection when library is fully integrated
// This would:
// 1. Close peer connection
// 2. Release all tracks
// 3. Dispose of resources
peerConnections.remove(peerConnection.channelId)
_connectionState.value = WebRTCConnectionState.Disconnected
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Start audio capture and transmission.
*/
suspend fun startAudioTransmission(peerConnection: WebRTCPeerConnection): Result<Unit> {
return try {
// TODO: Start audio capture when WebRTC library is fully integrated
// This would:
// 1. Create AudioSource with constraints
// 2. Create AudioTrack from source
// 3. Add track to peer connection's sender
// 4. Enable track
// 5. Start audio capture
peerConnection.isAudioActive = true
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Stop audio transmission.
*/
suspend fun stopAudioTransmission(peerConnection: WebRTCPeerConnection): Result<Unit> {
return try {
// TODO: Stop audio capture when WebRTC library is fully integrated
// This would:
// 1. Disable audio track
// 2. Remove track from peer connection sender
// 3. Stop track
// 4. Release audio source
peerConnection.isAudioActive = false
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Start video capture and transmission.
*/
suspend fun startVideoTransmission(peerConnection: WebRTCPeerConnection): Result<Unit> {
return try {
if (peerConnection.isAudioOnly) {
return Result.Error(IllegalStateException("Video not supported for audio-only connection"))
}
// TODO: Start video capture when WebRTC library is fully integrated
// This would:
// 1. Create VideoSource with camera constraints
// 2. Create VideoTrack from source
// 3. Add track to peer connection's sender
// 4. Enable track
// 5. Start camera capture
peerConnection.isVideoActive = true
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Stop video transmission.
*/
suspend fun stopVideoTransmission(peerConnection: WebRTCPeerConnection): Result<Unit> {
return try {
// TODO: Stop video capture when WebRTC library is fully integrated
// This would:
// 1. Disable video track
// 2. Remove track from peer connection sender
// 3. Stop track
// 4. Release video source and camera
peerConnection.isVideoActive = false
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
}
/**
* WebRTC peer connection wrapper.
*/
data class WebRTCPeerConnection(
val channelId: String,
val isAudioOnly: Boolean = false,
val config: RTCConfiguration,
var isAudioActive: Boolean = false,
var isVideoActive: Boolean = false
// TODO: Add actual WebRTC PeerConnection instance when library is integrated
// private val peerConnection: PeerConnection
)
/**
* RTC configuration for peer connections.
*/
data class RTCConfiguration(
val iceServers: List<IceServer>,
val iceCandidatePoolSize: Int = 10
)
/**
* ICE server configuration.
*/
data class IceServer(
val url: String,
val username: String? = null,
val credential: String? = null
)
/**
* WebRTC connection state.
*/
sealed class WebRTCConnectionState {
object Disconnected : WebRTCConnectionState()
data class Connecting(val channelId: String) : WebRTCConnectionState()
data class Connected(val channelId: String) : WebRTCConnectionState()
data class Error(val message: String) : WebRTCConnectionState()
}

View File

@@ -0,0 +1,213 @@
package com.smoa.modules.communications.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.communications.domain.Channel
import com.smoa.modules.communications.domain.CommunicationsService
/**
* Communications screen with channel list and PTT controls.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun CommunicationsScreen(
communicationsService: CommunicationsService,
userRole: RBACFramework.Role,
userUnit: String?,
modifier: Modifier = Modifier
) {
var channels by remember { mutableStateOf<List<Channel>>(emptyList()) }
var currentChannel by remember { mutableStateOf<Channel?>(null) }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
val isPTTActive by communicationsService.isPTTActive.collectAsState()
// Load available channels
LaunchedEffect(userRole, userUnit) {
isLoading = true
errorMessage = null
try {
channels = communicationsService.getAvailableChannels(userRole, userUnit)
} catch (e: Exception) {
errorMessage = e.message
} finally {
isLoading = false
}
}
// Observe current channel
LaunchedEffect(Unit) {
communicationsService.currentChannel.collect { channel ->
currentChannel = channel
}
}
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Unit Communications",
style = MaterialTheme.typography.headlineMedium
)
Spacer(modifier = Modifier.height(16.dp))
// Current channel indicator
currentChannel?.let { channel ->
Card(
colors = CardDefaults.cardColors(
containerColor = MaterialTheme.colorScheme.primaryContainer
),
modifier = Modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = "Current Channel: ${channel.name}",
style = MaterialTheme.typography.titleMedium
)
channel.description?.let { desc ->
Text(
text = desc,
style = MaterialTheme.typography.bodySmall
)
}
}
}
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))
}
// Channel 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(channels) { channel ->
ChannelCard(
channel = channel,
isActive = currentChannel?.id == channel.id,
onClick = {
// Join channel
// This would need to be in a coroutine scope
}
)
}
}
}
Spacer(modifier = Modifier.height(16.dp))
// PTT Button
Button(
onClick = {
if (isPTTActive) {
// Stop PTT
} else {
// Start PTT
}
},
modifier = Modifier
.fillMaxWidth()
.height(64.dp),
colors = ButtonDefaults.buttonColors(
containerColor = if (isPTTActive) {
MaterialTheme.colorScheme.error
} else {
MaterialTheme.colorScheme.primary
}
),
enabled = currentChannel != null
) {
Text(
text = if (isPTTActive) "RELEASE" else "PUSH TO TALK",
style = MaterialTheme.typography.titleLarge
)
}
}
}
/**
* Channel card.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ChannelCard(
channel: Channel,
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 = channel.name,
style = MaterialTheme.typography.titleMedium
)
channel.description?.let { desc ->
Text(
text = desc,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
if (isActive) {
Text(
text = "Connected",
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.primary
)
}
}
}
}

View File

@@ -0,0 +1,60 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.credentials"
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(":core:barcode"))
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.roomRuntime)
implementation(Dependencies.roomKtx)
kapt(Dependencies.roomCompiler)
// Barcode generation
implementation(Dependencies.zxingCore)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.coroutinesAndroid)
}

View File

@@ -0,0 +1,36 @@
package com.smoa.modules.credentials
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Credentials module - Secure presentation of government-issued credentials.
*/
@Composable
fun CredentialsModule(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Issued Credentials",
style = MaterialTheme.typography.headlineMedium
)
Text(
text = "Barcode generation and credential display functionality available",
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(top = 8.dp)
)
// Credential display UI with barcode integration will be implemented here
}
}

View File

@@ -0,0 +1,41 @@
package com.smoa.modules.credentials
import com.smoa.core.barcode.BarcodeEncoder
import com.smoa.core.barcode.formats.AAMVACredential
import com.smoa.core.barcode.formats.ICAO9303Credential
import com.smoa.core.barcode.formats.MILSTD129Credential
import com.smoa.core.common.Result
import com.google.zxing.common.BitMatrix
import javax.inject.Inject
import javax.inject.Singleton
/**
* Repository for credentials management with barcode generation.
*/
@Singleton
class CredentialsRepository @Inject constructor(
private val barcodeEncoder: BarcodeEncoder
) {
/**
* Generate AAMVA credential barcode.
*/
suspend fun generateAAMVABarcode(credential: AAMVACredential): Result<BitMatrix> {
return barcodeEncoder.encodeAAMVA(credential)
}
/**
* Generate ICAO 9303 credential barcode.
*/
suspend fun generateICAO9303Barcode(credential: ICAO9303Credential): Result<BitMatrix> {
return barcodeEncoder.encodeICAO9303(credential)
}
/**
* Generate MIL-STD-129 credential barcode.
*/
suspend fun generateMILSTD129Barcode(credential: MILSTD129Credential): Result<BitMatrix> {
return barcodeEncoder.encodeMILSTD129(credential)
}
}

View File

@@ -0,0 +1,40 @@
package com.smoa.modules.credentials.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.smoa.core.barcode.ui.BarcodeDisplay
import com.google.zxing.common.BitMatrix
/**
* Display credential with PDF417 barcode.
*/
@Composable
fun BarcodeCredentialDisplay(
bitMatrix: BitMatrix,
credentialTitle: String,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxWidth()
.padding(16.dp)
) {
Text(
text = credentialTitle,
style = MaterialTheme.typography.headlineSmall,
modifier = Modifier.padding(bottom = 16.dp)
)
BarcodeDisplay(
bitMatrix = bitMatrix,
modifier = Modifier.fillMaxWidth()
)
}
}

View File

@@ -0,0 +1,62 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.directory"
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(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.roomRuntime)
implementation(Dependencies.roomKtx)
kapt(Dependencies.roomCompiler)
// Database Encryption
implementation(Dependencies.sqlcipher)
// Testing
testImplementation(Dependencies.junit)
testImplementation(Dependencies.mockk)
testImplementation(Dependencies.coroutinesTest)
testImplementation(Dependencies.truth)
}

View File

@@ -0,0 +1,30 @@
package com.smoa.modules.directory
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.directory.domain.DirectoryEntry
import com.smoa.modules.directory.domain.DirectoryService
import com.smoa.modules.directory.ui.DirectoryListScreen
/**
* Directory module - Controlled access to internal routing and contact information.
*/
@Composable
fun DirectoryModule(
directoryService: DirectoryService,
userRole: RBACFramework.Role,
userUnit: String?,
onEntryClick: (DirectoryEntry) -> Unit = {},
modifier: Modifier = Modifier
) {
DirectoryListScreen(
directoryService = directoryService,
userRole = userRole,
userUnit = userUnit,
onEntryClick = onEntryClick,
modifier = modifier.fillMaxSize()
)
}

View File

@@ -0,0 +1,63 @@
package com.smoa.modules.directory.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
/**
* Data Access Object for directory entries.
*/
@Dao
interface DirectoryDao {
/**
* Get all directory entries.
*/
@Query("SELECT * FROM directory_entries ORDER BY name ASC")
fun observeAllEntries(): Flow<List<DirectoryEntity>>
/**
* Get directory entry by ID.
*/
@Query("SELECT * FROM directory_entries WHERE id = :entryId")
suspend fun getEntryById(entryId: String): DirectoryEntity?
/**
* Search directory entries by name, title, or unit.
*/
@Query("""
SELECT * FROM directory_entries
WHERE name LIKE :query
OR title LIKE :query
OR unit LIKE :query
ORDER BY name ASC
""")
suspend fun searchDirectory(query: String): List<DirectoryEntity>
/**
* Get directory entries by unit.
*/
@Query("SELECT * FROM directory_entries WHERE unit = :unit ORDER BY name ASC")
suspend fun getEntriesByUnit(unit: String): List<DirectoryEntity>
/**
* Insert or update directory entry.
*/
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun upsertEntry(entry: DirectoryEntity)
/**
* Update directory entry.
*/
@Update
suspend fun updateEntry(entry: DirectoryEntity)
/**
* Delete directory entry.
*/
@Query("DELETE FROM directory_entries WHERE id = :entryId")
suspend fun deleteEntry(entryId: String)
}

View File

@@ -0,0 +1,17 @@
package com.smoa.modules.directory.data
import androidx.room.Database
import androidx.room.RoomDatabase
/**
* Directory database.
*/
@Database(
entities = [DirectoryEntity::class],
version = 1,
exportSchema = false
)
abstract class DirectoryDatabase : RoomDatabase() {
abstract fun directoryDao(): DirectoryDao
}

View File

@@ -0,0 +1,38 @@
package com.smoa.modules.directory.data
import android.content.Context
import androidx.room.Room
import com.smoa.core.security.EncryptedDatabaseHelper
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DirectoryDatabaseModule {
@Provides
@Singleton
fun provideDirectoryDatabase(
@ApplicationContext context: Context,
encryptedDatabaseHelper: EncryptedDatabaseHelper
): DirectoryDatabase {
val factory = encryptedDatabaseHelper.createOpenHelperFactory("directory_database")
return Room.databaseBuilder(
context,
DirectoryDatabase::class.java,
"directory_database"
)
.openHelperFactory(factory)
.build()
}
@Provides
fun provideDirectoryDao(database: DirectoryDatabase): DirectoryDao {
return database.directoryDao()
}
}

View File

@@ -0,0 +1,63 @@
package com.smoa.modules.directory.data
import androidx.room.Entity
import androidx.room.PrimaryKey
import com.smoa.modules.directory.domain.DirectoryEntry
/**
* Directory entity for Room database.
*/
@Entity(tableName = "directory_entries")
data class DirectoryEntity(
@PrimaryKey
val id: String,
val name: String,
val title: String?,
val unit: String,
val phoneNumber: String?,
val extension: String?,
val email: String?,
val secureRoutingId: String?,
val role: String?,
val clearanceLevel: String?,
val lastUpdated: Long = System.currentTimeMillis()
)
/**
* Convert entity to domain model.
*/
fun DirectoryEntity.toDomain(): com.smoa.modules.directory.domain.DirectoryEntry {
return com.smoa.modules.directory.domain.DirectoryEntry(
id = id,
name = name,
title = title,
unit = unit,
phoneNumber = phoneNumber,
extension = extension,
email = email,
secureRoutingId = secureRoutingId,
role = role,
clearanceLevel = clearanceLevel,
lastUpdated = lastUpdated
)
}
/**
* Convert domain model to entity.
*/
fun com.smoa.modules.directory.domain.DirectoryEntry.toEntity(): DirectoryEntity {
return DirectoryEntity(
id = id,
name = name,
title = title,
unit = unit,
phoneNumber = phoneNumber,
extension = extension,
email = email,
secureRoutingId = secureRoutingId,
role = role,
clearanceLevel = clearanceLevel,
lastUpdated = lastUpdated
)
}

View File

@@ -0,0 +1,24 @@
package com.smoa.modules.directory.di
import com.smoa.core.auth.RBACFramework
import com.smoa.modules.directory.data.DirectoryDao
import com.smoa.modules.directory.domain.DirectoryService
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object DirectoryModule {
@Provides
@Singleton
fun provideDirectoryService(
directoryDao: DirectoryDao,
rbacFramework: RBACFramework
): DirectoryService {
return DirectoryService(directoryDao, rbacFramework)
}
}

View File

@@ -0,0 +1,170 @@
package com.smoa.modules.directory.domain
import com.smoa.core.auth.RBACFramework
import com.smoa.core.common.Result
import com.smoa.modules.directory.data.DirectoryDao
import com.smoa.modules.directory.data.DirectoryEntity
import com.smoa.modules.directory.data.toDomain
import com.smoa.modules.directory.data.toEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import javax.inject.Inject
import javax.inject.Singleton
/**
* Directory service for managing internal directory and contact information.
* Enforces unit-scoped and role-scoped directory views.
*/
@Singleton
class DirectoryService @Inject constructor(
private val directoryDao: DirectoryDao,
private val rbacFramework: RBACFramework
) {
/**
* Search directory entries.
* Results are filtered by user's role and unit scope.
*/
suspend fun searchDirectory(
query: String,
userRole: RBACFramework.Role,
userUnit: String?
): List<DirectoryEntry> {
val entries = directoryDao.searchDirectory("%$query%")
// Filter by role and unit scope
val filtered = entries.filter { entry ->
// Check if user has permission to view this entry
hasAccessToEntry(entry, userRole, userUnit)
}
return filtered.map { entry: DirectoryEntity -> entry.toDomain() }
}
/**
* Get directory entry by ID.
*/
suspend fun getDirectoryEntry(
entryId: String,
userRole: RBACFramework.Role,
userUnit: String?
): DirectoryEntry? {
val entity = directoryDao.getEntryById(entryId) ?: return null
// Check access
if (!hasAccessToEntry(entity, userRole, userUnit)) {
return null
}
return entity.toDomain()
}
/**
* Get all directory entries for a unit.
*/
suspend fun getDirectoryEntriesByUnit(
unit: String,
userRole: RBACFramework.Role,
userUnit: String?
): List<DirectoryEntry> {
// Check if user has access to this unit
if (userUnit != null && userUnit != unit && userRole != RBACFramework.Role.ADMIN) {
return emptyList()
}
return directoryDao.getEntriesByUnit(unit).map { it.toDomain() }
}
/**
* Get directory entries observable (for UI).
*/
fun observeDirectoryEntries(
userRole: RBACFramework.Role,
userUnit: String?
): Flow<List<DirectoryEntry>> {
return directoryDao.observeAllEntries()
.map { entities ->
entities
.filter { hasAccessToEntry(it, userRole, userUnit) }
.map { it.toDomain() }
}
}
/**
* Check if user has access to a directory entry.
*/
private fun hasAccessToEntry(
entry: DirectoryEntity,
userRole: RBACFramework.Role,
userUnit: String?
): Boolean {
// Admins can see all entries
if (userRole == RBACFramework.Role.ADMIN) {
return true
}
// Check unit scope
if (userUnit != null && entry.unit != userUnit) {
// User can only see entries from their unit
return false
}
// Check role permissions
return rbacFramework.hasPermission(userRole, RBACFramework.Permission.VIEW_DIRECTORY)
}
/**
* Add or update directory entry (admin only).
*/
suspend fun upsertDirectoryEntry(
entry: DirectoryEntry,
userRole: RBACFramework.Role
): Result<DirectoryEntry> {
if (userRole != RBACFramework.Role.ADMIN) {
return Result.Error(SecurityException("Only administrators can modify directory entries"))
}
return try {
val entity = entry.toEntity()
directoryDao.upsertEntry(entity)
Result.Success(entry)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Delete directory entry (admin only).
*/
suspend fun deleteDirectoryEntry(
entryId: String,
userRole: RBACFramework.Role
): Result<Unit> {
if (userRole != RBACFramework.Role.ADMIN) {
return Result.Error(SecurityException("Only administrators can delete directory entries"))
}
return try {
directoryDao.deleteEntry(entryId)
Result.Success(Unit)
} catch (e: Exception) {
Result.Error(e)
}
}
}
/**
* Directory entry domain model.
*/
data class DirectoryEntry(
val id: String,
val name: String,
val title: String?,
val unit: String,
val phoneNumber: String?,
val extension: String?,
val email: String?,
val secureRoutingId: String?,
val role: String?,
val clearanceLevel: String?,
val lastUpdated: Long = System.currentTimeMillis()
)

View File

@@ -0,0 +1,183 @@
package com.smoa.modules.directory.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.modules.directory.domain.DirectoryEntry
import com.smoa.modules.directory.domain.DirectoryService
/**
* Directory list screen with search functionality.
*/
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun DirectoryListScreen(
directoryService: DirectoryService,
userRole: com.smoa.core.auth.RBACFramework.Role,
userUnit: String?,
onEntryClick: (DirectoryEntry) -> Unit,
modifier: Modifier = Modifier
) {
var searchQuery by remember { mutableStateOf("") }
var directoryEntries by remember { mutableStateOf<List<DirectoryEntry>>(emptyList()) }
var isLoading by remember { mutableStateOf(false) }
var errorMessage by remember { mutableStateOf<String?>(null) }
// Observe directory entries
LaunchedEffect(userRole, userUnit) {
directoryService.observeDirectoryEntries(userRole, userUnit)
.collect { entries ->
directoryEntries = entries
}
}
// Search functionality
LaunchedEffect(searchQuery) {
if (searchQuery.isBlank()) {
// Show all entries when search is empty
directoryService.observeDirectoryEntries(userRole, userUnit)
.collect { entries ->
directoryEntries = entries
}
} else {
isLoading = true
errorMessage = null
try {
directoryEntries = directoryService.searchDirectory(searchQuery, userRole, userUnit)
} catch (e: Exception) {
errorMessage = e.message
} finally {
isLoading = false
}
}
}
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
// Search bar
OutlinedTextField(
value = searchQuery,
onValueChange = { searchQuery = it },
label = { Text("Search directory") },
placeholder = { Text("Name, title, or unit") },
modifier = Modifier.fillMaxWidth(),
singleLine = true
)
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))
}
// Loading indicator
if (isLoading) {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center
) {
CircularProgressIndicator()
}
}
// Directory entries list
if (directoryEntries.isEmpty() && !isLoading) {
Box(
modifier = Modifier
.fillMaxWidth()
.weight(1f),
contentAlignment = Alignment.Center
) {
Text(
text = if (searchQuery.isBlank()) "No directory entries" else "No results found",
style = MaterialTheme.typography.bodyLarge
)
}
} else {
LazyColumn(
modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(directoryEntries) { entry ->
DirectoryEntryCard(
entry = entry,
onClick = { onEntryClick(entry) }
)
}
}
}
}
}
/**
* Directory entry card.
*/
@Composable
fun DirectoryEntryCard(
entry: DirectoryEntry,
onClick: () -> Unit,
modifier: Modifier = Modifier
) {
@OptIn(ExperimentalMaterial3Api::class)
Card(
onClick = onClick,
modifier = modifier.fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp)
) {
Text(
text = entry.name,
style = MaterialTheme.typography.titleMedium
)
entry.title?.let { title ->
Text(
text = title,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = entry.unit,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
entry.phoneNumber?.let { phone ->
Text(
text = phone,
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
}

View File

@@ -0,0 +1,149 @@
package com.smoa.modules.directory.domain
import com.smoa.core.auth.RBACFramework
import com.smoa.modules.directory.data.DirectoryDao
import com.smoa.modules.directory.data.DirectoryEntity
import io.mockk.coEvery
import io.mockk.every
import io.mockk.mockk
import io.mockk.verify
import kotlinx.coroutines.test.runTest
import org.junit.Assert.*
import org.junit.Test
/**
* Unit tests for DirectoryService.
*/
class DirectoryServiceTest {
private val directoryDao = mockk<DirectoryDao>(relaxed = true)
private val rbacFramework = mockk<RBACFramework>(relaxed = true)
private val directoryService = DirectoryService(directoryDao, rbacFramework)
@Test
fun `searchDirectory should filter by role and unit`() = runTest {
// Given
val query = "test"
val userRole = RBACFramework.Role.OPERATOR
val userUnit = "Unit1"
val entity1 = DirectoryEntity(
id = "1",
name = "Test User 1",
title = "Officer",
unit = "Unit1",
phoneNumber = "123-456-7890",
extension = null,
email = null,
secureRoutingId = null,
role = null,
clearanceLevel = null
)
val entity2 = DirectoryEntity(
id = "2",
name = "Test User 2",
title = "Officer",
unit = "Unit2",
phoneNumber = "123-456-7891",
extension = null,
email = null,
secureRoutingId = null,
role = null,
clearanceLevel = null
)
coEvery { directoryDao.searchDirectory("%$query%") } returns listOf(entity1, entity2)
every { rbacFramework.hasPermission(userRole, RBACFramework.Permission.VIEW_DIRECTORY) } returns true
// When
val result = directoryService.searchDirectory(query, userRole, userUnit)
// Then
assertEquals(1, result.size)
assertEquals("Test User 1", result[0].name)
assertEquals("Unit1", result[0].unit)
}
@Test
fun `getDirectoryEntry should return null for unauthorized access`() = runTest {
// Given
val entryId = "1"
val userRole = RBACFramework.Role.VIEWER
val userUnit = "Unit1"
val entity = DirectoryEntity(
id = entryId,
name = "Test User",
title = "Officer",
unit = "Unit2", // Different unit
phoneNumber = null,
extension = null,
email = null,
secureRoutingId = null,
role = null,
clearanceLevel = null
)
coEvery { directoryDao.getEntryById(entryId) } returns entity
every { rbacFramework.hasPermission(userRole, RBACFramework.Permission.VIEW_DIRECTORY) } returns true
// When
val result = directoryService.getDirectoryEntry(entryId, userRole, userUnit)
// Then
assertNull(result)
}
@Test
fun `upsertDirectoryEntry should fail for non-admin users`() = runTest {
// Given
val entry = DirectoryEntry(
id = "1",
name = "Test User",
title = "Officer",
unit = "Unit1",
phoneNumber = null,
extension = null,
email = null,
secureRoutingId = null,
role = null,
clearanceLevel = null
)
val userRole = RBACFramework.Role.OPERATOR
// When
val result = directoryService.upsertDirectoryEntry(entry, userRole)
// Then
assertTrue(result.isFailure)
assertTrue(result.exceptionOrNull() is SecurityException)
}
@Test
fun `upsertDirectoryEntry should succeed for admin users`() = runTest {
// Given
val entry = DirectoryEntry(
id = "1",
name = "Test User",
title = "Officer",
unit = "Unit1",
phoneNumber = null,
extension = null,
email = null,
secureRoutingId = null,
role = null,
clearanceLevel = null
)
val userRole = RBACFramework.Role.ADMIN
coEvery { directoryDao.upsertEntry(any()) } returns Unit
// When
val result = directoryService.upsertDirectoryEntry(entry, userRole)
// Then
assertTrue(result.isSuccess)
verify { directoryDao.upsertEntry(any()) }
}
}

View File

@@ -0,0 +1,59 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.evidence"
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(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.roomRuntime)
implementation(Dependencies.roomKtx)
kapt(Dependencies.roomCompiler)
// Database Encryption
implementation(Dependencies.sqlcipher)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.coroutinesAndroid)
}

View File

@@ -0,0 +1,33 @@
package com.smoa.modules.evidence.data
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import java.util.Date
@Entity(
tableName = "custody_transfers",
foreignKeys = [
ForeignKey(
entity = EvidenceEntity::class,
parentColumns = ["evidenceId"],
childColumns = ["evidenceId"],
onDelete = ForeignKey.CASCADE
)
]
)
@TypeConverters(EvidenceConverters::class)
data class CustodyTransferEntity(
@PrimaryKey
val transferId: String,
val evidenceId: String,
val timestamp: Date,
val fromCustodian: String,
val toCustodian: String,
val reason: String,
val evidenceCondition: String,
val signatureData: ByteArray?,
val notes: String?
)

View File

@@ -0,0 +1,20 @@
package com.smoa.modules.evidence.data
import androidx.room.TypeConverter
import com.smoa.modules.evidence.domain.EvidenceType
import java.util.Date
class EvidenceConverters {
@TypeConverter
fun fromEvidenceType(value: EvidenceType): String = value.name
@TypeConverter
fun toEvidenceType(value: String): EvidenceType = EvidenceType.valueOf(value)
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
}

View File

@@ -0,0 +1,62 @@
package com.smoa.modules.evidence.data
import androidx.room.Dao
import androidx.room.Embedded
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Relation
import androidx.room.Transaction
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface EvidenceDao {
@Query("SELECT * FROM evidence ORDER BY collectionDate DESC")
fun getAllEvidence(): Flow<List<EvidenceEntity>>
@Query("SELECT * FROM evidence WHERE evidenceId = :evidenceId")
suspend fun getEvidenceById(evidenceId: String): EvidenceEntity?
@Query("SELECT * FROM evidence WHERE caseNumber = :caseNumber ORDER BY collectionDate DESC")
fun getEvidenceByCase(caseNumber: String): Flow<List<EvidenceEntity>>
@Query("SELECT * FROM evidence WHERE currentCustodian = :custodian ORDER BY collectionDate DESC")
fun getEvidenceByCustodian(custodian: String): Flow<List<EvidenceEntity>>
@Query("SELECT * FROM evidence WHERE description LIKE :query OR evidenceId LIKE :query ORDER BY collectionDate DESC")
fun searchEvidence(query: String): Flow<List<EvidenceEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertEvidence(evidence: EvidenceEntity)
@Update
suspend fun updateEvidence(evidence: EvidenceEntity)
@Query("DELETE FROM evidence WHERE evidenceId = :evidenceId")
suspend fun deleteEvidence(evidenceId: String)
}
@Dao
interface CustodyTransferDao {
@Query("SELECT * FROM custody_transfers WHERE evidenceId = :evidenceId ORDER BY timestamp ASC")
fun getChainOfCustody(evidenceId: String): Flow<List<CustodyTransferEntity>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertTransfer(transfer: CustodyTransferEntity)
@Transaction
@Query("SELECT * FROM evidence WHERE evidenceId = :evidenceId")
suspend fun getEvidenceWithChain(evidenceId: String): EvidenceWithChain?
}
data class EvidenceWithChain(
@Embedded
val evidence: EvidenceEntity,
@Relation(
parentColumn = "evidenceId",
entityColumn = "evidenceId"
)
val transfers: List<CustodyTransferEntity>
)

View File

@@ -0,0 +1,17 @@
package com.smoa.modules.evidence.data
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@Database(
entities = [EvidenceEntity::class, CustodyTransferEntity::class],
version = 1,
exportSchema = false
)
@TypeConverters(EvidenceConverters::class)
abstract class EvidenceDatabase : RoomDatabase() {
abstract fun evidenceDao(): EvidenceDao
abstract fun custodyTransferDao(): CustodyTransferDao
}

View File

@@ -0,0 +1,42 @@
package com.smoa.modules.evidence.data
import android.content.Context
import androidx.room.Room
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object EvidenceDatabaseModule {
@Provides
@Singleton
fun provideEvidenceDatabase(
@ApplicationContext context: Context,
encryptedDatabaseHelper: com.smoa.core.security.EncryptedDatabaseHelper
): EvidenceDatabase {
val factory = encryptedDatabaseHelper.createOpenHelperFactory("evidence_database")
return Room.databaseBuilder(
context,
EvidenceDatabase::class.java,
"evidence_database"
)
.openHelperFactory(factory)
.build()
}
@Provides
fun provideEvidenceDao(database: EvidenceDatabase): EvidenceDao {
return database.evidenceDao()
}
@Provides
fun provideCustodyTransferDao(database: EvidenceDatabase): CustodyTransferDao {
return database.custodyTransferDao()
}
}

View File

@@ -0,0 +1,26 @@
package com.smoa.modules.evidence.data
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.smoa.modules.evidence.domain.EvidenceType
import java.util.Date
@Entity(tableName = "evidence")
@TypeConverters(EvidenceConverters::class)
data class EvidenceEntity(
@PrimaryKey
val evidenceId: String,
val caseNumber: String,
val description: String,
val evidenceType: EvidenceType,
val collectionDate: Date,
val collectionLocation: String,
val collectionMethod: String,
val collectedBy: String,
val currentCustodian: String,
val storageLocation: String?,
val createdAt: Date,
val updatedAt: Date
)

View File

@@ -0,0 +1,56 @@
package com.smoa.modules.evidence.domain
import java.util.Date
/**
* Evidence data model per NIST SP 800-88.
*/
data class Evidence(
val evidenceId: String,
val caseNumber: String,
val description: String,
val evidenceType: EvidenceType,
val collectionDate: Date,
val collectionLocation: String,
val collectionMethod: String,
val collectedBy: String,
val currentCustodian: String,
val storageLocation: String?,
val chainOfCustody: List<CustodyTransfer>,
val metadata: EvidenceMetadata
)
enum class EvidenceType {
PHYSICAL,
DIGITAL,
BIOLOGICAL,
CHEMICAL,
FIREARM,
DOCUMENT
}
data class CustodyTransfer(
val transferId: String,
val timestamp: Date,
val fromCustodian: String,
val toCustodian: String,
val reason: String,
val evidenceCondition: String,
val signature: DigitalSignature,
val notes: String?
)
data class DigitalSignature(
val signatureId: String,
val signerId: String,
val signerName: String,
val signatureDate: Date,
val signatureData: ByteArray
)
data class EvidenceMetadata(
val tags: List<String> = emptyList(),
val photos: List<String> = emptyList(),
val documents: List<String> = emptyList()
)

View File

@@ -0,0 +1,120 @@
package com.smoa.modules.evidence.domain
import com.smoa.modules.evidence.data.CustodyTransferDao
import com.smoa.modules.evidence.data.EvidenceDao
import com.smoa.modules.evidence.data.EvidenceEntity
import com.smoa.modules.evidence.data.CustodyTransferEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.Date
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class EvidenceRepository @Inject constructor(
private val evidenceDao: EvidenceDao,
private val custodyTransferDao: CustodyTransferDao
) {
fun getAllEvidence(): Flow<List<Evidence>> {
return evidenceDao.getAllEvidence().map { entities ->
entities.map { it.toDomain(emptyList()) }
}
}
suspend fun getEvidenceById(evidenceId: String): Evidence? {
val entity = evidenceDao.getEvidenceById(evidenceId) ?: return null
val transfers = custodyTransferDao.getChainOfCustody(evidenceId)
// Convert Flow to List (simplified - in production use proper async handling)
return entity.toDomain(emptyList()) // Will need to load transfers separately
}
fun getEvidenceByCase(caseNumber: String): Flow<List<Evidence>> {
return evidenceDao.getEvidenceByCase(caseNumber).map { entities ->
entities.map { it.toDomain(emptyList()) }
}
}
suspend fun insertEvidence(evidence: Evidence) {
evidenceDao.insertEvidence(evidence.toEntity())
}
suspend fun addCustodyTransfer(transfer: CustodyTransfer) {
custodyTransferDao.insertTransfer(transfer.toEntity())
}
fun getChainOfCustody(evidenceId: String): Flow<List<CustodyTransfer>> {
return custodyTransferDao.getChainOfCustody(evidenceId).map { entities ->
entities.map { it.toDomain() }
}
}
}
private fun EvidenceEntity.toDomain(transfers: List<CustodyTransfer>): Evidence {
return Evidence(
evidenceId = evidenceId,
caseNumber = caseNumber,
description = description,
evidenceType = evidenceType,
collectionDate = collectionDate,
collectionLocation = collectionLocation,
collectionMethod = collectionMethod,
collectedBy = collectedBy,
currentCustodian = currentCustodian,
storageLocation = storageLocation,
chainOfCustody = transfers,
metadata = EvidenceMetadata()
)
}
private fun Evidence.toEntity(): EvidenceEntity {
return EvidenceEntity(
evidenceId = evidenceId,
caseNumber = caseNumber,
description = description,
evidenceType = evidenceType,
collectionDate = collectionDate,
collectionLocation = collectionLocation,
collectionMethod = collectionMethod,
collectedBy = collectedBy,
currentCustodian = currentCustodian,
storageLocation = storageLocation,
createdAt = Date(),
updatedAt = Date()
)
}
private fun CustodyTransferEntity.toDomain(): CustodyTransfer {
return CustodyTransfer(
transferId = transferId,
timestamp = timestamp,
fromCustodian = fromCustodian,
toCustodian = toCustodian,
reason = reason,
evidenceCondition = evidenceCondition,
signature = DigitalSignature(
signatureId = transferId,
signerId = fromCustodian,
signerName = fromCustodian,
signatureDate = timestamp,
signatureData = signatureData ?: ByteArray(0)
),
notes = notes
)
}
private fun CustodyTransfer.toEntity(): CustodyTransferEntity {
return CustodyTransferEntity(
transferId = transferId,
evidenceId = "", // Should be set by caller
timestamp = timestamp,
fromCustodian = fromCustodian,
toCustodian = toCustodian,
reason = reason,
evidenceCondition = evidenceCondition,
signatureData = signature.signatureData,
notes = notes
)
}

View File

@@ -0,0 +1,96 @@
package com.smoa.modules.evidence.domain
import com.smoa.core.security.AuditLogger
import com.smoa.core.security.AuditEventType
import kotlinx.coroutines.flow.Flow
import java.util.Date
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class EvidenceService @Inject constructor(
private val repository: EvidenceRepository,
private val auditLogger: AuditLogger
) {
suspend fun createEvidence(
caseNumber: String,
description: String,
evidenceType: EvidenceType,
collectionLocation: String,
collectionMethod: String,
collectedBy: String,
metadata: EvidenceMetadata = EvidenceMetadata()
): Result<Evidence> {
return try {
val evidence = Evidence(
evidenceId = UUID.randomUUID().toString(),
caseNumber = caseNumber,
description = description,
evidenceType = evidenceType,
collectionDate = Date(),
collectionLocation = collectionLocation,
collectionMethod = collectionMethod,
collectedBy = collectedBy,
currentCustodian = collectedBy,
storageLocation = null,
chainOfCustody = emptyList(),
metadata = metadata
)
repository.insertEvidence(evidence)
auditLogger.logEvent(
AuditEventType.CREDENTIAL_ACCESS,
userId = collectedBy,
module = "evidence",
details = "Evidence created: ${evidence.evidenceId}"
)
Result.success(evidence)
} catch (e: Exception) {
Result.failure(e)
}
}
suspend fun transferCustody(
evidenceId: String,
fromCustodian: String,
toCustodian: String,
reason: String,
evidenceCondition: String,
signature: DigitalSignature,
notes: String?
): Result<CustodyTransfer> {
return try {
val transfer = CustodyTransfer(
transferId = UUID.randomUUID().toString(),
timestamp = Date(),
fromCustodian = fromCustodian,
toCustodian = toCustodian,
reason = reason,
evidenceCondition = evidenceCondition,
signature = signature,
notes = notes
)
// In production, update evidence currentCustodian
repository.addCustodyTransfer(transfer)
auditLogger.logEvent(
AuditEventType.CREDENTIAL_ACCESS,
userId = fromCustodian,
module = "evidence",
details = "Custody transferred: $evidenceId from $fromCustodian to $toCustodian"
)
Result.success(transfer)
} catch (e: Exception) {
Result.failure(e)
}
}
fun getAllEvidence(): Flow<List<Evidence>> = repository.getAllEvidence()
fun getEvidenceByCase(caseNumber: String): Flow<List<Evidence>> = repository.getEvidenceByCase(caseNumber)
fun getChainOfCustody(evidenceId: String): Flow<List<CustodyTransfer>> = repository.getChainOfCustody(evidenceId)
}

View File

@@ -0,0 +1,28 @@
package com.smoa.modules.evidence.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun ChainOfCustodyScreen(
evidenceId: String,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Chain of Custody",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -0,0 +1,25 @@
package com.smoa.modules.evidence.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun EvidenceListScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Evidence",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -0,0 +1,25 @@
package com.smoa.modules.evidence.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun EvidenceModule(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Evidence Chain of Custody",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -0,0 +1,50 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.intelligence"
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(platform(Dependencies.composeBom))
implementation(Dependencies.composeUi)
implementation(Dependencies.composeMaterial3)
implementation(Dependencies.androidxCoreKtx)
implementation(Dependencies.hiltAndroid)
kapt(Dependencies.hiltAndroidCompiler)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.coroutinesAndroid)
}

View File

@@ -0,0 +1,25 @@
package com.smoa.modules.intelligence
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun IntelligenceModule(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Intelligence Operations",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -0,0 +1,34 @@
package com.smoa.modules.intelligence.domain
/**
* Compartmented access control framework for intelligence operations.
*/
data class Compartment(
val compartmentId: String,
val name: String,
val description: String,
val accessLevel: AccessLevel,
val controllingAgency: String,
val authorizedPersonnel: List<String>
)
enum class AccessLevel {
UNCLASSIFIED,
CONFIDENTIAL,
SECRET,
TOP_SECRET,
TS_SCI // Top Secret - Sensitive Compartmented Information
}
/**
* Need-to-know enforcement.
*/
data class NeedToKnow(
val compartmentId: String,
val userId: String,
val justification: String,
val authorizedBy: String,
val authorizationDate: java.util.Date,
val expirationDate: java.util.Date?
)

View File

@@ -0,0 +1,59 @@
package com.smoa.modules.intelligence.domain
import javax.inject.Inject
import javax.inject.Singleton
/**
* Compartment manager for multi-level security (MLS) system.
*/
@Singleton
class CompartmentManager @Inject constructor() {
private val compartments = mutableMapOf<String, Compartment>()
private val userCompartments = mutableMapOf<String, Set<String>>()
private val needToKnowRecords = mutableMapOf<String, List<NeedToKnow>>()
/**
* Register a compartment.
*/
fun registerCompartment(compartment: Compartment) {
compartments[compartment.compartmentId] = compartment
}
/**
* Check if user has access to compartment.
*/
fun hasAccess(userId: String, compartmentId: String): Boolean {
val userComps = userCompartments[userId] ?: return false
return userComps.contains(compartmentId)
}
/**
* Check need-to-know for compartment access.
*/
fun hasNeedToKnow(userId: String, compartmentId: String): Boolean {
val records = needToKnowRecords[userId] ?: return false
val now = java.util.Date()
return records.any {
it.compartmentId == compartmentId &&
it.expirationDate?.after(now) != false
}
}
/**
* Grant compartment access to user.
*/
fun grantAccess(userId: String, compartmentId: String) {
val current = userCompartments[userId] ?: emptySet()
userCompartments[userId] = current + compartmentId
}
/**
* Add need-to-know authorization.
*/
fun addNeedToKnow(needToKnow: NeedToKnow) {
val current = needToKnowRecords[needToKnow.userId] ?: emptyList()
needToKnowRecords[needToKnow.userId] = current + needToKnow
}
}

View File

@@ -0,0 +1,75 @@
package com.smoa.modules.intelligence.domain
import com.smoa.core.security.AuditLogger
import com.smoa.core.security.AuditEventType
import java.util.Date
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
/**
* Intelligence operations service.
*/
@Singleton
class IntelligenceService @Inject constructor(
private val compartmentManager: CompartmentManager,
private val auditLogger: AuditLogger
) {
/**
* Register compartment.
*/
suspend fun registerCompartment(compartment: Compartment) {
compartmentManager.registerCompartment(compartment)
auditLogger.logEvent(
AuditEventType.POLICY_UPDATE,
userId = compartment.controllingAgency,
module = "intelligence",
details = "Compartment registered: ${compartment.compartmentId}"
)
}
/**
* Check compartment access.
*/
fun checkCompartmentAccess(userId: String, compartmentId: String): Boolean {
return compartmentManager.hasAccess(userId, compartmentId) &&
compartmentManager.hasNeedToKnow(userId, compartmentId)
}
/**
* Create protected source.
*/
suspend fun createProtectedSource(
sourceType: SourceType,
codename: String?,
description: String,
protectionLevel: ProtectionLevel,
authorizedHandlers: List<String>
): Result<ProtectedSource> {
return try {
val source = ProtectedSource(
sourceId = UUID.randomUUID().toString(),
sourceType = sourceType,
codename = codename,
description = description,
protectionLevel = protectionLevel,
authorizedHandlers = authorizedHandlers,
creationDate = Date(),
lastAccessDate = null
)
auditLogger.logEvent(
AuditEventType.CREDENTIAL_ACCESS,
userId = authorizedHandlers.firstOrNull() ?: "system",
module = "intelligence",
details = "Protected source created: ${source.sourceId}"
)
Result.success(source)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -0,0 +1,50 @@
package com.smoa.modules.intelligence.domain
import java.util.Date
/**
* Source protection framework for intelligence operations.
*/
data class ProtectedSource(
val sourceId: String,
val sourceType: SourceType,
val codename: String?,
val description: String,
val protectionLevel: ProtectionLevel,
val authorizedHandlers: List<String>,
val creationDate: Date,
val lastAccessDate: Date?
)
enum class SourceType {
HUMAN_INTELLIGENCE,
SIGNALS_INTELLIGENCE,
IMAGERY_INTELLIGENCE,
OPEN_SOURCE,
OTHER
}
enum class ProtectionLevel {
ROUTINE,
SENSITIVE,
HIGHLY_SENSITIVE,
CRITICAL
}
data class SourceHandlingRecord(
val recordId: String,
val sourceId: String,
val handlerId: String,
val action: HandlingAction,
val timestamp: Date,
val notes: String?
)
enum class HandlingAction {
ACCESSED,
MODIFIED,
SHARED,
ARCHIVED,
DESTROYED
}

View File

@@ -0,0 +1,52 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.judicial"
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:orders"))
implementation(project(":modules:evidence"))
implementation(platform(Dependencies.composeBom))
implementation(Dependencies.composeUi)
implementation(Dependencies.composeMaterial3)
implementation(Dependencies.androidxCoreKtx)
implementation(Dependencies.hiltAndroid)
kapt(Dependencies.hiltAndroidCompiler)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.coroutinesAndroid)
}

View File

@@ -0,0 +1,25 @@
package com.smoa.modules.judicial
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun JudicialModule(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Judicial Operations",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -0,0 +1,88 @@
package com.smoa.modules.judicial.domain
import java.util.Date
/**
* Case file data model for judicial case management.
*/
data class CaseFile(
val caseId: String,
val caseNumber: String,
val courtName: String,
val caseType: CaseType,
val title: String,
val description: String,
val filingDate: Date,
val status: CaseStatus,
val parties: List<Party>,
val documents: List<CaseDocument>,
val orders: List<String>, // Order IDs
val docketEntries: List<DocketEntry>
)
enum class CaseType {
CRIMINAL,
CIVIL,
FAMILY,
PROBATE,
TRAFFIC
}
enum class CaseStatus {
OPEN,
CLOSED,
APPEALED,
PENDING
}
data class Party(
val partyId: String,
val name: String,
val role: PartyRole,
val contactInfo: ContactInfo
)
enum class PartyRole {
PLAINTIFF,
DEFENDANT,
WITNESS,
ATTORNEY
}
data class ContactInfo(
val address: String?,
val phone: String?,
val email: String?
)
data class CaseDocument(
val documentId: String,
val fileName: String,
val documentType: DocumentType,
val uploadDate: Date,
val uploadedBy: String
)
enum class DocumentType {
COMPLAINT,
MOTION,
BRIEF,
EVIDENCE,
ORDER,
OTHER
}
data class DocketEntry(
val entryId: String,
val entryDate: Date,
val description: String,
val entryType: DocketEntryType
)
enum class DocketEntryType {
FILING,
HEARING,
ORDER,
JUDGMENT
}

View File

@@ -0,0 +1,54 @@
package com.smoa.modules.judicial.domain
import java.util.Date
/**
* Court order data model.
*/
data class CourtOrder(
val orderId: String,
val courtName: String,
val caseNumber: String,
val orderType: CourtOrderType,
val title: String,
val content: String,
val judgeName: String,
val issuedDate: Date,
val effectiveDate: Date,
val expirationDate: Date?,
val status: OrderStatus,
val executionStatus: ExecutionStatus,
val signatures: List<DigitalSignature>
)
enum class CourtOrderType {
SEARCH_WARRANT,
ARREST_WARRANT,
SUBPOENA,
RESTRAINING_ORDER,
COURT_ORDER
}
enum class OrderStatus {
DRAFT,
SIGNED,
ISSUED,
EXECUTED,
EXPIRED,
REVOKED
}
enum class ExecutionStatus {
NOT_EXECUTED,
EXECUTING,
EXECUTED,
FAILED
}
data class DigitalSignature(
val signatureId: String,
val signerId: String,
val signatureDate: Date,
val signatureData: ByteArray
)

View File

@@ -0,0 +1,130 @@
package com.smoa.modules.judicial.domain
import com.smoa.core.security.AuditLogger
import com.smoa.core.security.AuditEventType
import java.util.Date
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
/**
* Judicial operations service.
*/
@Singleton
class JudicialService @Inject constructor(
private val auditLogger: AuditLogger
) {
/**
* Create court order.
*/
suspend fun createCourtOrder(
courtName: String,
caseNumber: String,
orderType: CourtOrderType,
title: String,
content: String,
judgeName: String,
effectiveDate: Date,
expirationDate: Date?
): Result<CourtOrder> {
return try {
val order = CourtOrder(
orderId = UUID.randomUUID().toString(),
courtName = courtName,
caseNumber = caseNumber,
orderType = orderType,
title = title,
content = content,
judgeName = judgeName,
issuedDate = Date(),
effectiveDate = effectiveDate,
expirationDate = expirationDate,
status = OrderStatus.DRAFT,
executionStatus = ExecutionStatus.NOT_EXECUTED,
signatures = emptyList()
)
auditLogger.logEvent(
AuditEventType.POLICY_UPDATE,
userId = judgeName,
module = "judicial",
details = "Court order created: ${order.orderId}"
)
Result.success(order)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Create case file.
*/
suspend fun createCaseFile(
caseNumber: String,
courtName: String,
caseType: CaseType,
title: String,
description: String
): Result<CaseFile> {
return try {
val caseFile = CaseFile(
caseId = UUID.randomUUID().toString(),
caseNumber = caseNumber,
courtName = courtName,
caseType = caseType,
title = title,
description = description,
filingDate = Date(),
status = CaseStatus.OPEN,
parties = emptyList(),
documents = emptyList(),
orders = emptyList(),
docketEntries = emptyList()
)
Result.success(caseFile)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Create subpoena.
*/
suspend fun createSubpoena(
caseNumber: String,
courtName: String,
subpoenaType: SubpoenaType,
recipientName: String,
recipientAddress: String,
description: String,
requestedDate: Date,
issuedBy: String
): Result<Subpoena> {
return try {
val subpoena = Subpoena(
subpoenaId = UUID.randomUUID().toString(),
caseNumber = caseNumber,
courtName = courtName,
subpoenaType = subpoenaType,
recipientName = recipientName,
recipientAddress = recipientAddress,
recipientContact = "",
description = description,
requestedDate = requestedDate,
serviceDate = null,
status = SubpoenaStatus.DRAFT,
servedBy = null,
issuedBy = issuedBy,
issueDate = Date()
)
Result.success(subpoena)
} catch (e: Exception) {
Result.failure(e)
}
}
}

View File

@@ -0,0 +1,38 @@
package com.smoa.modules.judicial.domain
import java.util.Date
/**
* Subpoena data model.
*/
data class Subpoena(
val subpoenaId: String,
val caseNumber: String,
val courtName: String,
val subpoenaType: SubpoenaType,
val recipientName: String,
val recipientAddress: String,
val recipientContact: String,
val description: String,
val requestedDate: Date,
val serviceDate: Date?,
val status: SubpoenaStatus,
val servedBy: String?,
val issuedBy: String,
val issueDate: Date
)
enum class SubpoenaType {
DUCES_TECUM, // Produce documents
AD_TESTIFICANDUM, // Testify
DEPOSITION
}
enum class SubpoenaStatus {
DRAFT,
ISSUED,
SERVED,
COMPLIED,
QUASHED
}

View File

@@ -0,0 +1,25 @@
package com.smoa.modules.judicial.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun CourtOrderScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Court Order",
style = MaterialTheme.typography.headlineMedium
)
}
}

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
)
}
}
}
}

View File

@@ -0,0 +1,51 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.military"
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(":core:barcode"))
implementation(platform(Dependencies.composeBom))
implementation(Dependencies.composeUi)
implementation(Dependencies.composeMaterial3)
implementation(Dependencies.androidxCoreKtx)
implementation(Dependencies.hiltAndroid)
kapt(Dependencies.hiltAndroidCompiler)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.coroutinesAndroid)
}

View File

@@ -0,0 +1,25 @@
package com.smoa.modules.military
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun MilitaryModule(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Military Operations",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -0,0 +1,48 @@
package com.smoa.modules.military.domain
import javax.inject.Inject
import javax.inject.Singleton
/**
* Document classification marking manager per DOD standards.
*/
@Singleton
class ClassificationManager @Inject constructor() {
/**
* Get classification banner text.
*/
fun getClassificationBanner(level: ClassificationLevel): String {
return when (level) {
ClassificationLevel.UNCLASSIFIED -> "UNCLASSIFIED"
ClassificationLevel.CONFIDENTIAL -> "CONFIDENTIAL"
ClassificationLevel.SECRET -> "SECRET"
ClassificationLevel.TOP_SECRET -> "TOP SECRET"
}
}
/**
* Check if user has clearance for classification level.
*/
fun hasClearance(userClearance: ClassificationLevel, documentLevel: ClassificationLevel): Boolean {
val clearanceHierarchy = mapOf(
ClassificationLevel.UNCLASSIFIED to 0,
ClassificationLevel.CONFIDENTIAL to 1,
ClassificationLevel.SECRET to 2,
ClassificationLevel.TOP_SECRET to 3
)
val userLevel = clearanceHierarchy[userClearance] ?: -1
val docLevel = clearanceHierarchy[documentLevel] ?: Int.MAX_VALUE
return userLevel >= docLevel
}
}
enum class ClassificationLevel {
UNCLASSIFIED,
CONFIDENTIAL,
SECRET,
TOP_SECRET
}

View File

@@ -0,0 +1,39 @@
package com.smoa.modules.military.domain
import java.util.Date
/**
* Military credential data model per MIL-STD-129.
*/
data class MilitaryCredential(
val credentialId: String,
val serviceCode: String, // Service branch code
val rank: String?,
val lastName: String,
val firstName: String,
val middleInitial: String?,
val socialSecurityNumber: String,
val dateOfBirth: Date,
val expirationDate: Date,
val issueDate: Date,
val cardNumber: String,
val unit: String?,
val clearanceLevel: ClearanceLevel?
)
enum class ClearanceLevel {
CONFIDENTIAL,
SECRET,
TOP_SECRET,
TS_SCI // Top Secret - Sensitive Compartmented Information
}
enum class ServiceBranch {
ARMY,
NAVY,
AIR_FORCE,
MARINES,
COAST_GUARD,
SPACE_FORCE
}

View File

@@ -0,0 +1,75 @@
package com.smoa.modules.military.domain
import com.smoa.core.security.AuditLogger
import com.smoa.core.security.AuditEventType
import java.util.Date
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
/**
* Military operations service.
*/
@Singleton
class MilitaryService @Inject constructor(
private val classificationManager: ClassificationManager,
private val auditLogger: AuditLogger
) {
/**
* Create military credential.
*/
suspend fun createMilitaryCredential(
serviceCode: String,
rank: String?,
lastName: String,
firstName: String,
socialSecurityNumber: String,
dateOfBirth: Date,
expirationDate: Date,
unit: String?,
clearanceLevel: ClearanceLevel?
): Result<MilitaryCredential> {
return try {
val credential = MilitaryCredential(
credentialId = UUID.randomUUID().toString(),
serviceCode = serviceCode,
rank = rank,
lastName = lastName,
firstName = firstName,
middleInitial = null,
socialSecurityNumber = socialSecurityNumber,
dateOfBirth = dateOfBirth,
expirationDate = expirationDate,
issueDate = Date(),
cardNumber = UUID.randomUUID().toString(),
unit = unit,
clearanceLevel = clearanceLevel
)
auditLogger.logEvent(
AuditEventType.CREDENTIAL_ACCESS,
userId = firstName,
module = "military",
details = "Military credential created: ${credential.credentialId}"
)
Result.success(credential)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Check classification access.
*/
fun checkClassificationAccess(userClearance: ClearanceLevel, documentLevel: ClassificationLevel): Boolean {
val userLevel = when (userClearance) {
ClearanceLevel.CONFIDENTIAL -> ClassificationLevel.CONFIDENTIAL
ClearanceLevel.SECRET -> ClassificationLevel.SECRET
ClearanceLevel.TOP_SECRET, ClearanceLevel.TS_SCI -> ClassificationLevel.TOP_SECRET
}
return classificationManager.hasClearance(userLevel, documentLevel)
}
}

View File

@@ -0,0 +1,25 @@
package com.smoa.modules.military.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun MilitaryCredentialScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Military Credential",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -0,0 +1,54 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.ncic"
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(platform(Dependencies.composeBom))
implementation(Dependencies.composeUi)
implementation(Dependencies.composeMaterial3)
implementation(Dependencies.androidxCoreKtx)
implementation(Dependencies.hiltAndroid)
kapt(Dependencies.hiltAndroidCompiler)
implementation(Dependencies.retrofit)
implementation(Dependencies.okHttp)
implementation(Dependencies.retrofitGson)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.coroutinesAndroid)
}

View File

@@ -0,0 +1,25 @@
package com.smoa.modules.ncic
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun NCICModule(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "NCIC/III Integration",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -0,0 +1,17 @@
package com.smoa.modules.ncic.data
// import androidx.room.Database
// import androidx.room.RoomDatabase
// TODO: Add entities when implementing storage
// Temporarily commented out to allow build to proceed
// @Database(
// entities = [],
// version = 1,
// exportSchema = false
// )
// Temporarily commented out - will be re-enabled when entities are added
// abstract class NCICQueryDatabase : RoomDatabase() {
// // DAOs will be added here
// }

View File

@@ -0,0 +1,48 @@
package com.smoa.modules.ncic.domain
import java.util.Date
import java.util.UUID
/**
* NCIC query models for National Crime Information Center database queries.
*/
data class NCICQuery(
val queryId: String = UUID.randomUUID().toString(),
val ori: String, // Originating Agency Identifier
val ucn: String, // Unique Control Number
val queryType: NCICQueryType,
val searchCriteria: Map<String, String>,
val timestamp: Date = Date(),
val operatorId: String
)
enum class NCICQueryType {
PERSON,
VEHICLE,
ARTICLE,
BOAT,
GUN,
LICENSE_PLATE
}
data class NCICResponse(
val queryId: String,
val responseCode: NCICResponseCode,
val records: List<NCICRecord>?,
val timestamp: Date,
val message: String?
)
enum class NCICResponseCode {
HIT,
NO_HIT,
ERROR,
RESTRICTED
}
data class NCICRecord(
val recordType: String,
val data: Map<String, String>,
val flags: List<String>
)

View File

@@ -0,0 +1,69 @@
package com.smoa.modules.ncic.domain
import com.smoa.core.security.AuditLogger
import com.smoa.core.security.AuditEventType
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flowOf
import java.util.Date
import javax.inject.Inject
import javax.inject.Singleton
/**
* NCIC query service.
* Note: Actual NCIC API integration requires CJIS approval.
*/
@Singleton
class NCICService @Inject constructor(
private val oriManager: ORIManager,
private val ucnGenerator: UCNGenerator,
private val auditLogger: AuditLogger
) {
/**
* Execute NCIC query.
* Note: Requires CJIS Security Policy compliance and API access.
*/
suspend fun executeQuery(query: NCICQuery): Result<NCICResponse> {
return try {
// Validate ORI
if (!oriManager.validateORIFormat(query.ori)) {
return Result.failure(IllegalArgumentException("Invalid ORI format"))
}
// Validate UCN
if (!ucnGenerator.validateUCN(query.ucn)) {
return Result.failure(IllegalArgumentException("Invalid UCN format"))
}
// TODO: Integrate with NCIC API (requires CJIS approval)
// For now, simulate response
val response = NCICResponse(
queryId = query.queryId,
responseCode = NCICResponseCode.NO_HIT,
records = null,
timestamp = Date(),
message = "Query executed (simulated - API integration pending)"
)
auditLogger.logEvent(
AuditEventType.CREDENTIAL_ACCESS,
userId = query.operatorId,
module = "ncic",
details = "NCIC query executed: ${query.queryId}, type: ${query.queryType}"
)
Result.success(response)
} catch (e: Exception) {
Result.failure(e)
}
}
/**
* Execute III (Interstate Identification Index) query.
*/
suspend fun executeIIIQuery(query: NCICQuery): Result<NCICResponse> {
// III queries follow similar pattern to NCIC
return executeQuery(query)
}
}

View File

@@ -0,0 +1,43 @@
package com.smoa.modules.ncic.domain
import javax.inject.Inject
import javax.inject.Singleton
/**
* ORI (Originating Agency Identifier) management.
*/
@Singleton
class ORIManager @Inject constructor() {
private val registeredORIs = mutableMapOf<String, ORIInfo>()
/**
* Register an ORI for an agency.
*/
fun registerORI(ori: String, info: ORIInfo) {
registeredORIs[ori] = info
}
/**
* Get ORI information.
*/
fun getORIInfo(ori: String): ORIInfo? {
return registeredORIs[ori]
}
/**
* Validate ORI format.
*/
fun validateORIFormat(ori: String): Boolean {
// ORI format: 9 characters (3 letters, 3 numbers, 3 letters/numbers)
return ori.length == 9 && ori.matches(Regex("[A-Z]{3}[0-9]{3}[A-Z0-9]{3}"))
}
}
data class ORIInfo(
val ori: String,
val agencyName: String,
val state: String,
val jurisdiction: String
)

View File

@@ -0,0 +1,43 @@
package com.smoa.modules.ncic.domain
import java.util.Date
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
/**
* UCN (Unique Control Number) generator and validator.
*/
@Singleton
class UCNGenerator @Inject constructor() {
/**
* Generate a UCN for a query.
*/
fun generateUCN(ori: String, timestamp: Date = Date()): String {
// UCN format: ORI + Date + Sequence
val dateStr = java.text.SimpleDateFormat("yyMMdd", java.util.Locale.US).format(timestamp)
val sequence = UUID.randomUUID().toString().take(6).uppercase()
return "$ori$dateStr$sequence"
}
/**
* Validate UCN format.
*/
fun validateUCN(ucn: String): Boolean {
// UCN should be at least 15 characters
return ucn.length >= 15 && ucn.isNotBlank()
}
/**
* Extract ORI from UCN.
*/
fun extractORI(ucn: String): String? {
return if (ucn.length >= 9) {
ucn.substring(0, 9)
} else {
null
}
}
}

View File

@@ -0,0 +1,25 @@
package com.smoa.modules.ncic.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@Composable
fun NCICQueryScreen(modifier: Modifier = Modifier) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "NCIC Query",
style = MaterialTheme.typography.headlineMedium
)
}
}

View File

@@ -0,0 +1,59 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.orders"
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(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.roomRuntime)
implementation(Dependencies.roomKtx)
kapt(Dependencies.roomCompiler)
// Database Encryption
implementation(Dependencies.sqlcipher)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.coroutinesAndroid)
}

View File

@@ -0,0 +1,27 @@
package com.smoa.modules.orders.data
import androidx.room.TypeConverter
import com.smoa.modules.orders.domain.OrderStatus
import com.smoa.modules.orders.domain.OrderType
import java.util.Date
class OrderConverters {
@TypeConverter
fun fromOrderType(value: OrderType): String = value.name
@TypeConverter
fun toOrderType(value: String): OrderType = OrderType.valueOf(value)
@TypeConverter
fun fromOrderStatus(value: OrderStatus): String = value.name
@TypeConverter
fun toOrderStatus(value: String): OrderStatus = OrderStatus.valueOf(value)
@TypeConverter
fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) }
@TypeConverter
fun dateToTimestamp(date: Date?): Long? = date?.time
}

View File

@@ -0,0 +1,42 @@
package com.smoa.modules.orders.data
import androidx.room.Dao
import androidx.room.Insert
import androidx.room.OnConflictStrategy
import androidx.room.Query
import androidx.room.Update
import kotlinx.coroutines.flow.Flow
@Dao
interface OrderDao {
@Query("SELECT * FROM orders ORDER BY issueDate DESC")
fun getAllOrders(): Flow<List<OrderEntity>>
@Query("SELECT * FROM orders WHERE orderId = :orderId")
suspend fun getOrderById(orderId: String): OrderEntity?
@Query("SELECT * FROM orders WHERE status = :status ORDER BY issueDate DESC")
fun getOrdersByStatus(status: String): Flow<List<OrderEntity>>
@Query("SELECT * FROM orders WHERE orderType = :orderType ORDER BY issueDate DESC")
fun getOrdersByType(orderType: String): Flow<List<OrderEntity>>
@Query("SELECT * FROM orders WHERE issuedTo = :userId ORDER BY issueDate DESC")
fun getOrdersForUser(userId: String): Flow<List<OrderEntity>>
@Query("SELECT * FROM orders WHERE title LIKE :query OR content LIKE :query ORDER BY issueDate DESC")
fun searchOrders(query: String): Flow<List<OrderEntity>>
@Query("SELECT * FROM orders WHERE expirationDate IS NOT NULL AND expirationDate < :nowMillis AND status NOT IN ('EXPIRED', 'REVOKED')")
suspend fun getExpiredOrders(nowMillis: Long): List<OrderEntity>
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insertOrder(order: OrderEntity)
@Update
suspend fun updateOrder(order: OrderEntity)
@Query("DELETE FROM orders WHERE orderId = :orderId")
suspend fun deleteOrder(orderId: String)
}

View File

@@ -0,0 +1,16 @@
package com.smoa.modules.orders.data
import androidx.room.Database
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
@Database(
entities = [OrderEntity::class],
version = 1,
exportSchema = false
)
@TypeConverters(OrderConverters::class)
abstract class OrderDatabase : RoomDatabase() {
abstract fun orderDao(): OrderDao
}

View File

@@ -0,0 +1,38 @@
package com.smoa.modules.orders.data
import android.content.Context
import androidx.room.Room
import com.smoa.core.security.EncryptedDatabaseHelper
import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.android.qualifiers.ApplicationContext
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton
@Module
@InstallIn(SingletonComponent::class)
object OrderDatabaseModule {
@Provides
@Singleton
fun provideOrderDatabase(
@ApplicationContext context: Context,
encryptedDatabaseHelper: EncryptedDatabaseHelper
): OrderDatabase {
val passphrase = encryptedDatabaseHelper.getDatabasePassphrase("orders_database")
val factory = encryptedDatabaseHelper.createOpenHelperFactory("orders_database")
return Room.databaseBuilder(
context,
OrderDatabase::class.java,
"orders_database"
)
.openHelperFactory(factory)
.build()
}
@Provides
fun provideOrderDao(database: OrderDatabase): OrderDao {
return database.orderDao()
}
}

View File

@@ -0,0 +1,30 @@
package com.smoa.modules.orders.data
import androidx.room.Entity
import androidx.room.PrimaryKey
import androidx.room.TypeConverters
import com.smoa.modules.orders.domain.OrderStatus
import com.smoa.modules.orders.domain.OrderType
import java.util.Date
@Entity(tableName = "orders")
@TypeConverters(OrderConverters::class)
data class OrderEntity(
@PrimaryKey
val orderId: String,
val orderType: OrderType,
val title: String,
val content: String,
val issuedBy: String,
val issuedTo: String?,
val issueDate: Date,
val effectiveDate: Date,
val expirationDate: Date?,
val status: OrderStatus,
val classification: String?,
val jurisdiction: String,
val caseNumber: String?,
val createdAt: Date,
val updatedAt: Date
)

View File

@@ -0,0 +1,91 @@
package com.smoa.modules.orders.domain
import java.util.Date
/**
* Order data model for digital orders management.
*/
data class Order(
val orderId: String,
val orderType: OrderType,
val title: String,
val content: String,
val issuedBy: String, // Authority/author
val issuedTo: String?,
val issueDate: Date,
val effectiveDate: Date,
val expirationDate: Date?,
val status: OrderStatus,
val attachments: List<OrderAttachment> = emptyList(),
val signatures: List<DigitalSignature> = emptyList(),
val metadata: OrderMetadata
)
enum class OrderType {
AUTHORIZATION,
ASSIGNMENT,
SEARCH_WARRANT,
ARREST_WARRANT,
COURT_ORDER,
ADMINISTRATIVE
}
enum class OrderStatus {
DRAFT,
PENDING_APPROVAL,
APPROVED,
ISSUED,
EXECUTED,
EXPIRED,
REVOKED
}
data class OrderAttachment(
val attachmentId: String,
val fileName: String,
val mimeType: String,
val size: Long,
val content: ByteArray,
val uploadedDate: Date
)
data class DigitalSignature(
val signatureId: String,
val signerId: String,
val signerName: String,
val signatureDate: Date,
val signatureData: ByteArray,
val certificate: String? // X.509 certificate
)
data class OrderMetadata(
val classification: ClassificationLevel?,
val jurisdiction: String,
val caseNumber: String?,
val relatedOrders: List<String> = emptyList(),
val keywords: List<String> = emptyList()
)
enum class ClassificationLevel {
UNCLASSIFIED,
CONFIDENTIAL,
SECRET,
TOP_SECRET
}
data class OrderCopy(
val originalOrderId: String,
val copyId: String,
val generatedDate: Date,
val generatedBy: String,
val copyType: CopyType,
val authenticationCode: String, // HMAC-based for verification
val orderContent: ByteArray // Encrypted/signed
)
enum class CopyType {
CERTIFIED_TRUE_COPY,
INFORMATIONAL_COPY,
REDACTED_COPY
}

View File

@@ -0,0 +1,111 @@
package com.smoa.modules.orders.domain
import com.smoa.modules.orders.data.OrderDao
import com.smoa.modules.orders.data.OrderEntity
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
import java.util.Date
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class OrderRepository @Inject constructor(
private val orderDao: OrderDao
) {
fun getAllOrders(): Flow<List<Order>> {
return orderDao.getAllOrders().map { entities ->
entities.map { it.toDomain() }
}
}
suspend fun getOrderById(orderId: String): Order? {
return orderDao.getOrderById(orderId)?.toDomain()
}
fun getOrdersByStatus(status: OrderStatus): Flow<List<Order>> {
return orderDao.getOrdersByStatus(status.name).map { entities ->
entities.map { it.toDomain() }
}
}
fun getOrdersByType(orderType: OrderType): Flow<List<Order>> {
return orderDao.getOrdersByType(orderType.name).map { entities ->
entities.map { it.toDomain() }
}
}
fun getOrdersForUser(userId: String): Flow<List<Order>> {
return orderDao.getOrdersForUser(userId).map { entities ->
entities.map { it.toDomain() }
}
}
fun searchOrders(query: String): Flow<List<Order>> {
return orderDao.searchOrders("%$query%").map { entities ->
entities.map { it.toDomain() }
}
}
suspend fun insertOrder(order: Order) {
orderDao.insertOrder(order.toEntity())
}
suspend fun updateOrder(order: Order) {
orderDao.updateOrder(order.toEntity())
}
suspend fun deleteOrder(orderId: String) {
orderDao.deleteOrder(orderId)
}
suspend fun getExpiredOrders(): List<Order> {
return orderDao.getExpiredOrders(Date().time).map { it.toDomain() }
}
}
private fun OrderEntity.toDomain(): Order {
return Order(
orderId = orderId,
orderType = orderType,
title = title,
content = content,
issuedBy = issuedBy,
issuedTo = issuedTo,
issueDate = issueDate,
effectiveDate = effectiveDate,
expirationDate = expirationDate,
status = status,
attachments = emptyList(), // Load separately if needed
signatures = emptyList(), // Load separately if needed
metadata = OrderMetadata(
classification = classification?.let {
ClassificationLevel.valueOf(it)
},
jurisdiction = jurisdiction,
caseNumber = caseNumber,
relatedOrders = emptyList(),
keywords = emptyList()
)
)
}
private fun Order.toEntity(): OrderEntity {
return OrderEntity(
orderId = orderId,
orderType = orderType,
title = title,
content = content,
issuedBy = issuedBy,
issuedTo = issuedTo,
issueDate = issueDate,
effectiveDate = effectiveDate,
expirationDate = expirationDate,
status = status,
classification = metadata.classification?.name,
jurisdiction = metadata.jurisdiction,
caseNumber = metadata.caseNumber,
createdAt = Date(),
updatedAt = Date()
)
}

View File

@@ -0,0 +1,161 @@
package com.smoa.modules.orders.domain
import com.smoa.core.common.Result
import com.smoa.core.security.AuditLogger
import com.smoa.core.security.AuditEventType
import kotlinx.coroutines.flow.Flow
import java.util.Date
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class OrderService @Inject constructor(
private val repository: OrderRepository,
private val auditLogger: AuditLogger
) {
/**
* Create a new order.
*/
suspend fun createOrder(
orderType: OrderType,
title: String,
content: String,
issuedBy: String,
issuedTo: String?,
effectiveDate: Date,
expirationDate: Date?,
metadata: OrderMetadata
): Result<Order> {
return try {
val order = Order(
orderId = UUID.randomUUID().toString(),
orderType = orderType,
title = title,
content = content,
issuedBy = issuedBy,
issuedTo = issuedTo,
issueDate = Date(),
effectiveDate = effectiveDate,
expirationDate = expirationDate,
status = OrderStatus.DRAFT,
attachments = emptyList(),
signatures = emptyList(),
metadata = metadata
)
repository.insertOrder(order)
auditLogger.logEvent(
AuditEventType.POLICY_UPDATE,
userId = issuedBy,
module = "orders",
details = "Order created: ${order.orderId}"
)
Result.Success(order)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Update order status through workflow.
*/
suspend fun updateOrderStatus(
orderId: String,
newStatus: OrderStatus,
userId: String
): Result<Order> {
return try {
val order = repository.getOrderById(orderId)
?: return Result.Error(IllegalArgumentException("Order not found"))
val updatedOrder = order.copy(
status = newStatus,
metadata = order.metadata.copy()
)
repository.updateOrder(updatedOrder)
auditLogger.logEvent(
AuditEventType.POLICY_UPDATE,
userId = userId,
module = "orders",
details = "Order status updated: $orderId -> $newStatus"
)
Result.Success(updatedOrder)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Check and expire orders that have passed expiration date.
*/
suspend fun checkAndExpireOrders() {
val expiredOrders = repository.getExpiredOrders()
expiredOrders.forEach { order ->
if (order.status != OrderStatus.EXPIRED && order.status != OrderStatus.REVOKED) {
updateOrderStatus(order.orderId, OrderStatus.EXPIRED, "system")
}
}
}
/**
* Generate authenticated copy of order.
*/
suspend fun generateOrderCopy(
orderId: String,
copyType: CopyType,
generatedBy: String
): Result<OrderCopy> {
return try {
val order = repository.getOrderById(orderId)
?: return Result.Error(IllegalArgumentException("Order not found"))
// Generate HMAC-based authentication code
val authCode = generateAuthenticationCode(order, copyType, generatedBy)
val copy = OrderCopy(
originalOrderId = orderId,
copyId = UUID.randomUUID().toString(),
generatedDate = Date(),
generatedBy = generatedBy,
copyType = copyType,
authenticationCode = authCode,
orderContent = order.content.toByteArray() // In production, encrypt this
)
auditLogger.logEvent(
AuditEventType.CREDENTIAL_ACCESS,
userId = generatedBy,
module = "orders",
details = "Order copy generated: $orderId, type: $copyType"
)
Result.Success(copy)
} catch (e: Exception) {
Result.Error(e)
}
}
/**
* Generate HMAC-based authentication code for order copy.
*/
private fun generateAuthenticationCode(
order: Order,
copyType: CopyType,
generatedBy: String
): String {
// Simplified - in production, use proper HMAC with secret key
val data = "${order.orderId}|${copyType.name}|${generatedBy}|${Date().time}"
return data.hashCode().toString()
}
fun getAllOrders(): Flow<List<Order>> = repository.getAllOrders()
fun getOrdersByStatus(status: OrderStatus): Flow<List<Order>> = repository.getOrdersByStatus(status)
fun getOrdersByType(orderType: OrderType): Flow<List<Order>> = repository.getOrdersByType(orderType)
fun searchOrders(query: String): Flow<List<Order>> = repository.searchOrders(query)
}

View File

@@ -0,0 +1,33 @@
package com.smoa.modules.orders.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Order detail screen for viewing individual order.
*/
@Composable
fun OrderDetailScreen(
orderId: String,
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Order Details",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
// Order detail UI will be implemented here
}
}

View File

@@ -0,0 +1,32 @@
package com.smoa.modules.orders.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Order list screen displaying all orders.
*/
@Composable
fun OrderListScreen(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Orders",
style = MaterialTheme.typography.headlineMedium,
modifier = Modifier.padding(bottom = 16.dp)
)
// Order list UI will be implemented here
}
}

View File

@@ -0,0 +1,31 @@
package com.smoa.modules.orders.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
/**
* Orders module - Digital orders management system.
*/
@Composable
fun OrdersModule(
modifier: Modifier = Modifier
) {
Column(
modifier = modifier
.fillMaxSize()
.padding(16.dp)
) {
Text(
text = "Orders Management",
style = MaterialTheme.typography.headlineMedium
)
// Orders management UI will be implemented here
}
}

View File

@@ -0,0 +1,65 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
id("kotlin-kapt")
id("dagger.hilt.android.plugin")
}
android {
namespace = "com.smoa.modules.reports"
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:security"))
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)
// Report generation libraries
// TODO: PDFBox Android - may need alternative version or repository
// Temporarily commented - uncomment when dependency is available:
// implementation(Dependencies.pdfbox)
// TODO: POI requires minSdk 26, but project uses minSdk 24
// Temporarily commented - uncomment when minSdk is increased or alternative is found:
// implementation(Dependencies.poi)
// implementation(Dependencies.poiOoxml)
implementation(Dependencies.jacksonCore)
implementation(Dependencies.jacksonDatabind)
implementation(Dependencies.jacksonKotlin)
implementation(Dependencies.jaxbApi)
implementation(Dependencies.jaxbRuntime)
implementation(Dependencies.coroutinesCore)
implementation(Dependencies.coroutinesAndroid)
}

View File

@@ -0,0 +1,63 @@
package com.smoa.modules.reports.domain
import java.util.Date
data class Report(
val reportId: String,
val reportType: ReportType,
val title: String,
val template: ReportTemplate?,
val format: ReportFormat,
val content: ByteArray,
val generatedDate: Date,
val generatedBy: String,
val signature: DigitalSignature?,
val metadata: ReportMetadata
)
enum class ReportType {
OPERATIONAL,
COMPLIANCE,
AUDIT,
EVIDENCE,
ACTIVITY,
REGULATORY
}
enum class ReportFormat {
PDF,
XML,
JSON,
CSV,
EXCEL
}
data class ReportTemplate(
val templateId: String,
val name: String,
val reportType: ReportType,
val format: ReportFormat,
val templateContent: String
)
data class DigitalSignature(
val signatureId: String,
val signerId: String,
val signatureDate: Date,
val signatureData: ByteArray
)
data class ReportMetadata(
val scheduled: Boolean = false,
val scheduleFrequency: ScheduleFrequency? = null,
val distributionList: List<String> = emptyList()
)
enum class ScheduleFrequency {
DAILY,
WEEKLY,
MONTHLY,
QUARTERLY,
YEARLY
}

View File

@@ -0,0 +1,66 @@
package com.smoa.modules.reports.domain
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.KotlinModule
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.io.ByteArrayOutputStream
import javax.inject.Inject
import javax.inject.Singleton
/**
* Report generator service for multi-format report generation.
*/
@Singleton
class ReportGenerator @Inject constructor() {
private val objectMapper = ObjectMapper().registerModule(KotlinModule.Builder().build())
/**
* Generate report in specified format.
*/
suspend fun generateReport(
report: Report,
template: ReportTemplate?
): Result<ByteArray> {
return withContext(Dispatchers.IO) {
try {
val content = when (report.format) {
ReportFormat.JSON -> generateJSON(report)
ReportFormat.XML -> generateXML(report)
ReportFormat.CSV -> generateCSV(report)
ReportFormat.PDF -> generatePDF(report, template)
ReportFormat.EXCEL -> generateExcel(report)
}
Result.success(content)
} catch (e: Exception) {
Result.failure(e)
}
}
}
private fun generateJSON(report: Report): ByteArray {
return objectMapper.writeValueAsBytes(report)
}
private fun generateXML(report: Report): ByteArray {
// Placeholder - will use JAXB in full implementation
return report.content
}
private fun generateCSV(report: Report): ByteArray {
// Placeholder - will use Apache POI in full implementation
return report.content
}
private fun generatePDF(report: Report, template: ReportTemplate?): ByteArray {
// Placeholder - will use PDFBox in full implementation
return report.content
}
private fun generateExcel(report: Report): ByteArray {
// Placeholder - will use Apache POI in full implementation
return report.content
}
}

View File

@@ -0,0 +1,61 @@
package com.smoa.modules.reports.domain
import com.smoa.core.security.AuditLogger
import com.smoa.core.security.AuditEventType
import java.util.Date
import java.util.UUID
import javax.inject.Inject
import javax.inject.Singleton
/**
* Report service for generating and managing reports.
*/
@Singleton
class ReportService @Inject constructor(
private val reportGenerator: ReportGenerator,
private val auditLogger: AuditLogger
) {
/**
* Generate report.
*/
suspend fun generateReport(
reportType: ReportType,
format: ReportFormat,
title: String,
content: ByteArray,
generatedBy: String,
template: ReportTemplate?
): Result<Report> {
return try {
val report = Report(
reportId = UUID.randomUUID().toString(),
reportType = reportType,
title = title,
template = template,
format = format,
content = content,
generatedDate = Date(),
generatedBy = generatedBy,
signature = null, // TODO: Add digital signature
metadata = ReportMetadata()
)
// Generate report in specified format
val generatedContent = reportGenerator.generateReport(report, template)
val finalReport = report.copy(content = generatedContent.getOrElse { content })
auditLogger.logEvent(
AuditEventType.POLICY_UPDATE,
userId = generatedBy,
module = "reports",
details = "Report generated: ${finalReport.reportId}, type: $reportType"
)
Result.success(finalReport)
} catch (e: Exception) {
Result.failure(e)
}
}
}

Some files were not shown because too many files have changed in this diff Show More