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