Initial commit
This commit is contained in:
59
modules/evidence/build.gradle.kts
Normal file
59
modules/evidence/build.gradle.kts
Normal 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)
|
||||
}
|
||||
|
||||
@@ -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?
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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()
|
||||
)
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user