Initial commit
This commit is contained in:
46
core/common/build.gradle.kts
Normal file
46
core/common/build.gradle.kts
Normal file
@@ -0,0 +1,46 @@
|
||||
plugins {
|
||||
id("com.android.library")
|
||||
id("org.jetbrains.kotlin.android")
|
||||
id("kotlin-kapt")
|
||||
id("dagger.hilt.android.plugin")
|
||||
}
|
||||
|
||||
android {
|
||||
namespace = "com.smoa.core.common"
|
||||
compileSdk = AppConfig.compileSdk
|
||||
|
||||
defaultConfig {
|
||||
minSdk = AppConfig.minSdk
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_17
|
||||
targetCompatibility = JavaVersion.VERSION_17
|
||||
}
|
||||
|
||||
kotlinOptions {
|
||||
jvmTarget = "17"
|
||||
}
|
||||
}
|
||||
|
||||
java {
|
||||
toolchain {
|
||||
languageVersion.set(JavaLanguageVersion.of(17))
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation(Dependencies.androidxCoreKtx)
|
||||
implementation(Dependencies.androidxLifecycleRuntimeKtx)
|
||||
implementation(Dependencies.coroutinesCore)
|
||||
implementation(Dependencies.coroutinesAndroid)
|
||||
|
||||
implementation(Dependencies.hiltAndroid)
|
||||
kapt(Dependencies.hiltAndroidCompiler)
|
||||
|
||||
// Testing
|
||||
testImplementation(Dependencies.junit)
|
||||
testImplementation(Dependencies.mockk)
|
||||
testImplementation(Dependencies.coroutinesTest)
|
||||
testImplementation(Dependencies.truth)
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Network
|
||||
import android.net.NetworkCapabilities
|
||||
import dagger.hilt.android.qualifiers.ApplicationContext
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Manages connectivity state (online/offline/restricted).
|
||||
*/
|
||||
@Singleton
|
||||
class ConnectivityManager @Inject constructor(
|
||||
@ApplicationContext private val context: Context
|
||||
) {
|
||||
private val systemConnectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as android.net.ConnectivityManager
|
||||
|
||||
private val _connectivityState = MutableStateFlow<ConnectivityState>(ConnectivityState.Unknown)
|
||||
val connectivityState: StateFlow<ConnectivityState> = _connectivityState.asStateFlow()
|
||||
|
||||
private val networkCallback = object : android.net.ConnectivityManager.NetworkCallback() {
|
||||
override fun onAvailable(network: Network) {
|
||||
updateConnectivityState()
|
||||
}
|
||||
|
||||
override fun onLost(network: Network) {
|
||||
updateConnectivityState()
|
||||
}
|
||||
|
||||
override fun onCapabilitiesChanged(
|
||||
network: Network,
|
||||
networkCapabilities: NetworkCapabilities
|
||||
) {
|
||||
updateConnectivityState()
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
registerNetworkCallback()
|
||||
updateConnectivityState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Register network callback to monitor connectivity changes.
|
||||
*/
|
||||
private fun registerNetworkCallback() {
|
||||
val networkRequest = android.net.NetworkRequest.Builder()
|
||||
.addCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
||||
.build()
|
||||
|
||||
systemConnectivityManager.registerNetworkCallback(networkRequest, networkCallback)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update current connectivity state.
|
||||
*/
|
||||
private fun updateConnectivityState() {
|
||||
val activeNetwork = systemConnectivityManager.activeNetwork
|
||||
val capabilities = activeNetwork?.let {
|
||||
systemConnectivityManager.getNetworkCapabilities(it)
|
||||
}
|
||||
|
||||
_connectivityState.value = when {
|
||||
capabilities == null -> ConnectivityState.Offline
|
||||
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) &&
|
||||
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) -> {
|
||||
// Check if connection is restricted (e.g., VPN required but not connected)
|
||||
if (isRestricted(capabilities)) {
|
||||
ConnectivityState.Restricted
|
||||
} else {
|
||||
ConnectivityState.Online
|
||||
}
|
||||
}
|
||||
else -> ConnectivityState.Offline
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connection is restricted (e.g., requires VPN but not connected).
|
||||
*/
|
||||
private fun isRestricted(capabilities: NetworkCapabilities): Boolean {
|
||||
// Implement policy-based restriction checks
|
||||
// For now, return false (can be extended based on policy)
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device is currently online.
|
||||
*/
|
||||
fun isOnline(): Boolean {
|
||||
return _connectivityState.value == ConnectivityState.Online
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device is offline.
|
||||
*/
|
||||
fun isOffline(): Boolean {
|
||||
return _connectivityState.value == ConnectivityState.Offline
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if connection is restricted.
|
||||
*/
|
||||
fun isRestricted(): Boolean {
|
||||
return _connectivityState.value == ConnectivityState.Restricted
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current connectivity state.
|
||||
*/
|
||||
fun getState(): ConnectivityState {
|
||||
return _connectivityState.value
|
||||
}
|
||||
|
||||
enum class ConnectivityState {
|
||||
Online,
|
||||
Offline,
|
||||
Restricted,
|
||||
Unknown
|
||||
}
|
||||
}
|
||||
|
||||
119
core/common/src/main/java/com/smoa/core/common/CountryCodes.kt
Normal file
119
core/common/src/main/java/com/smoa/core/common/CountryCodes.kt
Normal file
@@ -0,0 +1,119 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import java.util.Locale
|
||||
|
||||
/**
|
||||
* ISO 3166 country codes utilities (ISO 3166-1:2020).
|
||||
* Provides country code validation and conversion per ISO 3166 standard.
|
||||
*/
|
||||
object CountryCodes {
|
||||
|
||||
/**
|
||||
* Get ISO 3166-1 alpha-2 country code (2-letter) from country name.
|
||||
*/
|
||||
fun getAlpha2Code(countryName: String): String? {
|
||||
return alpha2Codes[countryName.uppercase(Locale.US)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Get ISO 3166-1 alpha-3 country code (3-letter) from country name.
|
||||
*/
|
||||
fun getAlpha3Code(countryName: String): String? {
|
||||
return alpha3Codes[countryName.uppercase(Locale.US)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert alpha-2 to alpha-3 code.
|
||||
*/
|
||||
fun alpha2ToAlpha3(alpha2: String): String? {
|
||||
return alpha2ToAlpha3Map[alpha2.uppercase(Locale.US)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert alpha-3 to alpha-2 code.
|
||||
*/
|
||||
fun alpha3ToAlpha2(alpha3: String): String? {
|
||||
return alpha3ToAlpha2Map[alpha3.uppercase(Locale.US)]
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate ISO 3166-1 alpha-2 code format and validity.
|
||||
*/
|
||||
fun isValidAlpha2(code: String): Boolean {
|
||||
val upperCode = code.uppercase(Locale.US)
|
||||
return upperCode.length == 2 &&
|
||||
upperCode.all { it.isLetter() } &&
|
||||
alpha2Codes.values.contains(upperCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate ISO 3166-1 alpha-3 code format and validity.
|
||||
*/
|
||||
fun isValidAlpha3(code: String): Boolean {
|
||||
val upperCode = code.uppercase(Locale.US)
|
||||
return upperCode.length == 3 &&
|
||||
upperCode.all { it.isLetter() } &&
|
||||
alpha3Codes.values.contains(upperCode)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get numeric country code (ISO 3166-1 numeric).
|
||||
*/
|
||||
fun getNumericCode(alpha2: String): String? {
|
||||
return numericCodes[alpha2.uppercase(Locale.US)]
|
||||
}
|
||||
|
||||
// Common country codes - in production, use full ISO 3166-1:2020 database
|
||||
private val alpha2Codes = mapOf(
|
||||
"UNITED STATES" to "US",
|
||||
"CANADA" to "CA",
|
||||
"MEXICO" to "MX",
|
||||
"UNITED KINGDOM" to "GB",
|
||||
"FRANCE" to "FR",
|
||||
"GERMANY" to "DE",
|
||||
"ITALY" to "IT",
|
||||
"SPAIN" to "ES",
|
||||
"AUSTRALIA" to "AU",
|
||||
"JAPAN" to "JP",
|
||||
"CHINA" to "CN",
|
||||
"RUSSIA" to "RU"
|
||||
)
|
||||
|
||||
private val alpha3Codes = mapOf(
|
||||
"UNITED STATES" to "USA",
|
||||
"CANADA" to "CAN",
|
||||
"MEXICO" to "MEX",
|
||||
"UNITED KINGDOM" to "GBR",
|
||||
"FRANCE" to "FRA",
|
||||
"GERMANY" to "DEU",
|
||||
"ITALY" to "ITA",
|
||||
"SPAIN" to "ESP",
|
||||
"AUSTRALIA" to "AUS",
|
||||
"JAPAN" to "JPN",
|
||||
"CHINA" to "CHN",
|
||||
"RUSSIA" to "RUS"
|
||||
)
|
||||
|
||||
private val alpha2ToAlpha3Map = mapOf(
|
||||
"US" to "USA",
|
||||
"CA" to "CAN",
|
||||
"MX" to "MEX",
|
||||
"GB" to "GBR",
|
||||
"FR" to "FRA",
|
||||
"DE" to "DEU",
|
||||
"IT" to "ITA",
|
||||
"ES" to "ESP"
|
||||
)
|
||||
|
||||
private val alpha3ToAlpha2Map = alpha2ToAlpha3Map.entries.associate { (k, v) -> v to k }
|
||||
|
||||
private val numericCodes = mapOf(
|
||||
"US" to "840",
|
||||
"CA" to "124",
|
||||
"MX" to "484",
|
||||
"GB" to "826",
|
||||
"FR" to "250",
|
||||
"DE" to "276"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
|
||||
/**
|
||||
* Date formatting utilities for ISO 8601 compliance (ISO 8601:2019).
|
||||
* Ensures full compliance with ISO 8601 standard for date/time representation.
|
||||
*/
|
||||
object DateFormatting {
|
||||
|
||||
private val iso8601Format = SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
private val iso8601Basic = SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
private val iso8601DateOnly = SimpleDateFormat("yyyy-MM-dd", Locale.US)
|
||||
private val iso8601TimeOnly = SimpleDateFormat("HH:mm:ss", Locale.US)
|
||||
|
||||
/**
|
||||
* Format date to ISO 8601 format with timezone (YYYY-MM-DDTHH:mm:ss.sssZ).
|
||||
*/
|
||||
fun toISO8601(date: Date): String {
|
||||
return iso8601Format.format(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date to ISO 8601 basic format (YYYYMMDDTHHmmssZ).
|
||||
*/
|
||||
fun toISO8601Basic(date: Date): String {
|
||||
return iso8601Basic.format(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format date to ISO 8601 date-only format (YYYY-MM-DD).
|
||||
*/
|
||||
fun toISO8601Date(date: Date): String {
|
||||
return iso8601DateOnly.format(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* Format time to ISO 8601 time format (HH:mm:ss).
|
||||
*/
|
||||
fun toISO8601Time(date: Date): String {
|
||||
return iso8601TimeOnly.format(date)
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ISO 8601 date string (extended format).
|
||||
*/
|
||||
fun fromISO8601(dateString: String): Date? {
|
||||
return try {
|
||||
iso8601Format.parse(dateString)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse ISO 8601 basic format date string.
|
||||
*/
|
||||
fun fromISO8601Basic(dateString: String): Date? {
|
||||
return try {
|
||||
iso8601Basic.parse(dateString)
|
||||
} catch (e: Exception) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current time in ISO 8601 format.
|
||||
*/
|
||||
fun nowISO8601(): String {
|
||||
return toISO8601(Date())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import android.content.res.Configuration
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Manages foldable device state (folded/unfolded).
|
||||
*/
|
||||
@Singleton
|
||||
class FoldableStateManager @Inject constructor() {
|
||||
private val _foldState = MutableStateFlow<FoldState>(FoldState.Unknown)
|
||||
val foldState: StateFlow<FoldState> = _foldState.asStateFlow()
|
||||
|
||||
/**
|
||||
* Update fold state based on configuration.
|
||||
*/
|
||||
fun updateFoldState(configuration: Configuration) {
|
||||
val isFolded = configuration.screenWidthDp < 600 // Threshold for tablet/folded detection
|
||||
_foldState.value = if (isFolded) {
|
||||
FoldState.Folded
|
||||
} else {
|
||||
FoldState.Unfolded
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device is currently folded.
|
||||
*/
|
||||
fun isFolded(): Boolean {
|
||||
return _foldState.value == FoldState.Folded
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if device is currently unfolded.
|
||||
*/
|
||||
fun isUnfolded(): Boolean {
|
||||
return _foldState.value == FoldState.Unfolded
|
||||
}
|
||||
|
||||
enum class FoldState {
|
||||
Folded,
|
||||
Unfolded,
|
||||
Unknown
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* ISO/IEC 19794 biometric template support.
|
||||
*/
|
||||
object ISO19794Biometric {
|
||||
|
||||
/**
|
||||
* Biometric template format identifiers per ISO/IEC 19794.
|
||||
*/
|
||||
enum class FormatIdentifier(val code: Int) {
|
||||
FINGERPRINT(0x0010),
|
||||
FACIAL(0x0011),
|
||||
IRIS(0x0012),
|
||||
VOICE(0x0013)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create ISO 19794 compliant biometric header.
|
||||
*/
|
||||
fun createBiometricHeader(
|
||||
formatIdentifier: FormatIdentifier,
|
||||
version: Int = 0x30313000, // Version 1.0
|
||||
length: Int,
|
||||
captureDate: Date
|
||||
): ByteArray {
|
||||
// ISO 19794 header structure
|
||||
val header = mutableListOf<Byte>()
|
||||
|
||||
// Format identifier (4 bytes)
|
||||
header.addAll(intToBytes(formatIdentifier.code, 4))
|
||||
|
||||
// Version (4 bytes)
|
||||
header.addAll(intToBytes(version, 4))
|
||||
|
||||
// Length (4 bytes)
|
||||
header.addAll(intToBytes(length, 4))
|
||||
|
||||
// Capture date/time (14 bytes - YYYYMMDDHHmmss)
|
||||
val dateFormat = java.text.SimpleDateFormat("yyyyMMddHHmmss", java.util.Locale.US)
|
||||
val dateStr = dateFormat.format(captureDate)
|
||||
header.addAll(dateStr.toByteArray(Charsets.ISO_8859_1).toList())
|
||||
|
||||
return header.toByteArray()
|
||||
}
|
||||
|
||||
private fun intToBytes(value: Int, bytes: Int): List<Byte> {
|
||||
val result = mutableListOf<Byte>()
|
||||
for (i in bytes - 1 downTo 0) {
|
||||
result.add(((value shr (i * 8)) and 0xFF).toByte())
|
||||
}
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
102
core/common/src/main/java/com/smoa/core/common/ISO27001ISMS.kt
Normal file
102
core/common/src/main/java/com/smoa/core/common/ISO27001ISMS.kt
Normal file
@@ -0,0 +1,102 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* ISO/IEC 27001 Information Security Management System (ISMS) compliance utilities.
|
||||
*/
|
||||
object ISO27001ISMS {
|
||||
|
||||
/**
|
||||
* Security control categories per ISO 27001.
|
||||
*/
|
||||
enum class ControlCategory {
|
||||
SECURITY_POLICIES,
|
||||
ORGANIZATION_OF_INFORMATION_SECURITY,
|
||||
HUMAN_RESOURCE_SECURITY,
|
||||
ASSET_MANAGEMENT,
|
||||
ACCESS_CONTROL,
|
||||
CRYPTOGRAPHY,
|
||||
PHYSICAL_AND_ENVIRONMENTAL_SECURITY,
|
||||
OPERATIONS_SECURITY,
|
||||
COMMUNICATIONS_SECURITY,
|
||||
SYSTEM_ACQUISITION_DEVELOPMENT_AND_MAINTENANCE,
|
||||
SUPPLIER_RELATIONSHIPS,
|
||||
INFORMATION_SECURITY_INCIDENT_MANAGEMENT,
|
||||
INFORMATION_SECURITY_ASPECTS_OF_BUSINESS_CONTINUITY_MANAGEMENT,
|
||||
COMPLIANCE
|
||||
}
|
||||
|
||||
/**
|
||||
* Security event record for ISMS compliance.
|
||||
*/
|
||||
data class SecurityEvent(
|
||||
val eventId: String,
|
||||
val timestamp: Date,
|
||||
val category: ControlCategory,
|
||||
val description: String,
|
||||
val severity: Severity,
|
||||
val userId: String?,
|
||||
val resource: String?,
|
||||
val outcome: EventOutcome
|
||||
)
|
||||
|
||||
enum class Severity {
|
||||
LOW,
|
||||
MEDIUM,
|
||||
HIGH,
|
||||
CRITICAL
|
||||
}
|
||||
|
||||
enum class EventOutcome {
|
||||
SUCCESS,
|
||||
FAILURE,
|
||||
PARTIAL
|
||||
}
|
||||
|
||||
/**
|
||||
* ISMS documentation structure.
|
||||
*/
|
||||
data class ISMSDocumentation(
|
||||
val policyDocuments: List<PolicyDocument>,
|
||||
val procedures: List<Procedure>,
|
||||
val records: List<SecurityEvent>,
|
||||
val riskAssessments: List<RiskAssessment>,
|
||||
val lastReviewed: Date
|
||||
)
|
||||
|
||||
data class PolicyDocument(
|
||||
val documentId: String,
|
||||
val title: String,
|
||||
val version: String,
|
||||
val effectiveDate: Date,
|
||||
val reviewDate: Date?,
|
||||
val owner: String
|
||||
)
|
||||
|
||||
data class Procedure(
|
||||
val procedureId: String,
|
||||
val title: String,
|
||||
val steps: List<String>,
|
||||
val version: String,
|
||||
val lastUpdated: Date
|
||||
)
|
||||
|
||||
data class RiskAssessment(
|
||||
val assessmentId: String,
|
||||
val asset: String,
|
||||
val threat: String,
|
||||
val vulnerability: String,
|
||||
val riskLevel: RiskLevel,
|
||||
val mitigation: String?,
|
||||
val assessmentDate: Date
|
||||
)
|
||||
|
||||
enum class RiskLevel {
|
||||
LOW,
|
||||
MEDIUM,
|
||||
HIGH,
|
||||
CRITICAL
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
/**
|
||||
* ISO/IEC 7816 smart card integration support.
|
||||
*/
|
||||
object ISO7816SmartCard {
|
||||
|
||||
/**
|
||||
* APDU (Application Protocol Data Unit) command structure per ISO 7816-4.
|
||||
*/
|
||||
data class APDUCommand(
|
||||
val cla: Byte, // Class byte
|
||||
val ins: Byte, // Instruction byte
|
||||
val p1: Byte, // Parameter 1
|
||||
val p2: Byte, // Parameter 2
|
||||
val data: ByteArray? = null,
|
||||
val le: Byte? = null // Expected length
|
||||
) {
|
||||
fun toByteArray(): ByteArray {
|
||||
val result = mutableListOf<Byte>()
|
||||
result.add(cla)
|
||||
result.add(ins)
|
||||
result.add(p1)
|
||||
result.add(p2)
|
||||
|
||||
if (data != null) {
|
||||
result.add(data.size.toByte())
|
||||
result.addAll(data.toList())
|
||||
}
|
||||
|
||||
if (le != null) {
|
||||
result.add(le)
|
||||
}
|
||||
|
||||
return result.toByteArray()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* APDU response structure.
|
||||
*/
|
||||
data class APDUResponse(
|
||||
val data: ByteArray,
|
||||
val sw1: Byte,
|
||||
val sw2: Byte
|
||||
) {
|
||||
val statusWord: Int
|
||||
get() = ((sw1.toInt() and 0xFF) shl 8) or (sw2.toInt() and 0xFF)
|
||||
|
||||
val isSuccess: Boolean
|
||||
get() = statusWord == 0x9000
|
||||
}
|
||||
|
||||
/**
|
||||
* Common APDU instructions per ISO 7816-4.
|
||||
*/
|
||||
object Instructions {
|
||||
const val SELECT_FILE: Byte = 0xA4.toByte()
|
||||
const val READ_BINARY: Byte = 0xB0.toByte()
|
||||
const val UPDATE_BINARY: Byte = 0xD6.toByte()
|
||||
const val READ_RECORD: Byte = 0xB2.toByte()
|
||||
const val GET_RESPONSE: Byte = 0xC0.toByte()
|
||||
const val VERIFY: Byte = 0x20
|
||||
const val CHANGE_REFERENCE_DATA: Byte = 0x24
|
||||
}
|
||||
|
||||
/**
|
||||
* Create SELECT FILE APDU command.
|
||||
*/
|
||||
fun createSelectFileCommand(fileId: ByteArray, p2: Byte = 0x00.toByte()): APDUCommand {
|
||||
return APDUCommand(
|
||||
cla = 0x00,
|
||||
ins = Instructions.SELECT_FILE,
|
||||
p1 = 0x00,
|
||||
p2 = p2,
|
||||
data = fileId
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create READ BINARY APDU command.
|
||||
*/
|
||||
fun createReadBinaryCommand(offset: Int, length: Int): APDUCommand {
|
||||
val offsetBytes = byteArrayOf(
|
||||
((offset shr 8) and 0xFF).toByte(),
|
||||
(offset and 0xFF).toByte()
|
||||
)
|
||||
return APDUCommand(
|
||||
cla = 0x00,
|
||||
ins = Instructions.READ_BINARY,
|
||||
p1 = offsetBytes[0],
|
||||
p2 = offsetBytes[1],
|
||||
le = length.toByte()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import java.util.Date
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Offline policy manager.
|
||||
* Enforces time-bounded offline data caches and automatic purge.
|
||||
*/
|
||||
@Singleton
|
||||
class OfflinePolicyManager @Inject constructor() {
|
||||
companion object {
|
||||
private const val DEFAULT_MAX_OFFLINE_DURATION_MS = 7L * 24 * 60 * 60 * 1000 // 7 days
|
||||
private const val DEFAULT_CREDENTIAL_CACHE_DURATION_MS = 30L * 24 * 60 * 60 * 1000 // 30 days
|
||||
private const val DEFAULT_ORDER_CACHE_DURATION_MS = 14L * 24 * 60 * 60 * 1000 // 14 days
|
||||
private const val DEFAULT_EVIDENCE_CACHE_DURATION_MS = 90L * 24 * 60 * 60 * 1000 // 90 days
|
||||
}
|
||||
|
||||
/**
|
||||
* Get maximum offline duration for a data type.
|
||||
*/
|
||||
fun getMaxOfflineDuration(dataType: OfflineDataType): Long {
|
||||
return when (dataType) {
|
||||
OfflineDataType.Credential -> DEFAULT_CREDENTIAL_CACHE_DURATION_MS
|
||||
OfflineDataType.Order -> DEFAULT_ORDER_CACHE_DURATION_MS
|
||||
OfflineDataType.Evidence -> DEFAULT_EVIDENCE_CACHE_DURATION_MS
|
||||
OfflineDataType.Directory -> DEFAULT_MAX_OFFLINE_DURATION_MS
|
||||
OfflineDataType.Report -> DEFAULT_MAX_OFFLINE_DURATION_MS
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if offline data is still valid.
|
||||
*/
|
||||
fun isOfflineDataValid(lastSyncTime: Date, dataType: OfflineDataType): Boolean {
|
||||
val maxDuration = getMaxOfflineDuration(dataType)
|
||||
val now = Date()
|
||||
val offlineDuration = now.time - lastSyncTime.time
|
||||
return offlineDuration <= maxDuration
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if offline data should be purged.
|
||||
*/
|
||||
fun shouldPurgeOfflineData(lastSyncTime: Date, dataType: OfflineDataType): Boolean {
|
||||
return !isOfflineDataValid(lastSyncTime, dataType)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get time remaining until offline data expires.
|
||||
*/
|
||||
fun getTimeUntilExpiration(lastSyncTime: Date, dataType: OfflineDataType): Long {
|
||||
val maxDuration = getMaxOfflineDuration(dataType)
|
||||
val now = Date()
|
||||
val offlineDuration = now.time - lastSyncTime.time
|
||||
return maxOf(0, maxDuration - offlineDuration)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Offline data types.
|
||||
*/
|
||||
enum class OfflineDataType {
|
||||
Credential,
|
||||
Order,
|
||||
Evidence,
|
||||
Directory,
|
||||
Report
|
||||
}
|
||||
|
||||
8
core/common/src/main/java/com/smoa/core/common/Result.kt
Normal file
8
core/common/src/main/java/com/smoa/core/common/Result.kt
Normal file
@@ -0,0 +1,8 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
sealed class Result<out T> {
|
||||
data class Success<out T>(val data: T) : Result<T>()
|
||||
data class Error(val exception: Throwable) : Result<Nothing>()
|
||||
object Loading : Result<Nothing>()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
||||
/**
|
||||
* Smart card reader interface for ISO 7816 card integration.
|
||||
*
|
||||
* Note: Actual implementation will depend on hardware card reader support.
|
||||
*/
|
||||
@Singleton
|
||||
class SmartCardReader @Inject constructor() {
|
||||
|
||||
/**
|
||||
* Check if smart card is present.
|
||||
*/
|
||||
suspend fun isCardPresent(): Boolean {
|
||||
// TODO: Implement actual card detection
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Connect to smart card.
|
||||
*/
|
||||
suspend fun connect(): Result<SmartCardConnection> {
|
||||
// TODO: Implement actual card connection
|
||||
return Result.Error(NotImplementedError("Smart card connection not yet implemented"))
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnect from smart card.
|
||||
*/
|
||||
suspend fun disconnect() {
|
||||
// TODO: Implement actual card disconnection
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Smart card connection for APDU communication.
|
||||
*/
|
||||
interface SmartCardConnection {
|
||||
/**
|
||||
* Transmit APDU command and receive response.
|
||||
*/
|
||||
suspend fun transmit(command: ISO7816SmartCard.APDUCommand): Result<ISO7816SmartCard.APDUResponse>
|
||||
|
||||
/**
|
||||
* Close connection.
|
||||
*/
|
||||
suspend fun close()
|
||||
}
|
||||
|
||||
107
core/common/src/main/java/com/smoa/core/common/SyncAPI.kt
Normal file
107
core/common/src/main/java/com/smoa/core/common/SyncAPI.kt
Normal file
@@ -0,0 +1,107 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
/**
|
||||
* Sync API interface for backend synchronization.
|
||||
* Defines the contract for syncing data with backend services.
|
||||
*/
|
||||
interface SyncAPI {
|
||||
/**
|
||||
* Sync order to backend.
|
||||
*/
|
||||
suspend fun syncOrder(orderData: ByteArray): Result<SyncResponse>
|
||||
|
||||
/**
|
||||
* Sync evidence to backend.
|
||||
*/
|
||||
suspend fun syncEvidence(evidenceData: ByteArray): Result<SyncResponse>
|
||||
|
||||
/**
|
||||
* Sync credential to backend.
|
||||
*/
|
||||
suspend fun syncCredential(credentialData: ByteArray): Result<SyncResponse>
|
||||
|
||||
/**
|
||||
* Sync directory entry to backend.
|
||||
*/
|
||||
suspend fun syncDirectoryEntry(entryData: ByteArray): Result<SyncResponse>
|
||||
|
||||
/**
|
||||
* Sync report to backend.
|
||||
*/
|
||||
suspend fun syncReport(reportData: ByteArray): Result<SyncResponse>
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync response from backend.
|
||||
*/
|
||||
data class SyncResponse(
|
||||
val success: Boolean,
|
||||
val itemId: String,
|
||||
val serverTimestamp: Long,
|
||||
val conflict: Boolean = false,
|
||||
val remoteData: ByteArray? = null,
|
||||
val message: String? = null
|
||||
)
|
||||
|
||||
/**
|
||||
* Default implementation of SyncAPI.
|
||||
* In production, this would use Retrofit or similar to call actual backend APIs.
|
||||
*/
|
||||
class DefaultSyncAPI : SyncAPI {
|
||||
override suspend fun syncOrder(orderData: ByteArray): Result<SyncResponse> {
|
||||
// TODO: Implement actual API call
|
||||
// This would use Retrofit to POST order data to backend
|
||||
return Result.Success(
|
||||
SyncResponse(
|
||||
success = true,
|
||||
itemId = "order_123",
|
||||
serverTimestamp = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun syncEvidence(evidenceData: ByteArray): Result<SyncResponse> {
|
||||
// TODO: Implement actual API call
|
||||
return Result.Success(
|
||||
SyncResponse(
|
||||
success = true,
|
||||
itemId = "evidence_123",
|
||||
serverTimestamp = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun syncCredential(credentialData: ByteArray): Result<SyncResponse> {
|
||||
// TODO: Implement actual API call
|
||||
return Result.Success(
|
||||
SyncResponse(
|
||||
success = true,
|
||||
itemId = "credential_123",
|
||||
serverTimestamp = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun syncDirectoryEntry(entryData: ByteArray): Result<SyncResponse> {
|
||||
// TODO: Implement actual API call
|
||||
return Result.Success(
|
||||
SyncResponse(
|
||||
success = true,
|
||||
itemId = "directory_123",
|
||||
serverTimestamp = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun syncReport(reportData: ByteArray): Result<SyncResponse> {
|
||||
// TODO: Implement actual API call
|
||||
return Result.Success(
|
||||
SyncResponse(
|
||||
success = true,
|
||||
itemId = "report_123",
|
||||
serverTimestamp = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
400
core/common/src/main/java/com/smoa/core/common/SyncService.kt
Normal file
400
core/common/src/main/java/com/smoa/core/common/SyncService.kt
Normal file
@@ -0,0 +1,400 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import android.content.Context
|
||||
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
|
||||
|
||||
/**
|
||||
* Offline synchronization service.
|
||||
* Handles data synchronization when connectivity is restored.
|
||||
*/
|
||||
@Singleton
|
||||
class SyncService @Inject constructor(
|
||||
private val context: Context,
|
||||
private val connectivityManager: ConnectivityManager,
|
||||
private val syncAPI: SyncAPI = DefaultSyncAPI()
|
||||
) {
|
||||
private val _syncState = MutableStateFlow<SyncState>(SyncState.Idle)
|
||||
val syncState: StateFlow<SyncState> = _syncState.asStateFlow()
|
||||
|
||||
private val syncQueue = mutableListOf<SyncItem>()
|
||||
private val conflictResolver = ConflictResolver()
|
||||
|
||||
/**
|
||||
* Queue an item for synchronization.
|
||||
*/
|
||||
fun queueSync(item: SyncItem) {
|
||||
syncQueue.add(item)
|
||||
if (connectivityManager.isOnline()) {
|
||||
// Note: startSync() is a suspend function, caller should use coroutine scope
|
||||
// This will be handled by the sync service when connectivity is restored
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Start synchronization process.
|
||||
*/
|
||||
suspend fun startSync() {
|
||||
if (!connectivityManager.isOnline()) {
|
||||
_syncState.value = SyncState.WaitingForConnection
|
||||
return
|
||||
}
|
||||
|
||||
if (syncQueue.isEmpty()) {
|
||||
_syncState.value = SyncState.Idle
|
||||
return
|
||||
}
|
||||
|
||||
_syncState.value = SyncState.Syncing(syncQueue.size)
|
||||
|
||||
val itemsToSync = syncQueue.toList()
|
||||
syncQueue.clear()
|
||||
|
||||
for (item in itemsToSync) {
|
||||
try {
|
||||
syncItem(item)
|
||||
} catch (e: ConflictException) {
|
||||
// Handle conflict
|
||||
val resolution = conflictResolver.resolveConflict(item, e)
|
||||
when (resolution) {
|
||||
is ConflictResolution.UseLocal -> {
|
||||
// Keep local version
|
||||
}
|
||||
is ConflictResolution.UseRemote -> {
|
||||
// Use remote version
|
||||
syncItem(item.copy(data = e.remoteData))
|
||||
}
|
||||
is ConflictResolution.Merge -> {
|
||||
// Merge both versions
|
||||
syncItem(item.copy(data = resolution.mergedData))
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
// Re-queue failed items
|
||||
syncQueue.add(item)
|
||||
}
|
||||
}
|
||||
|
||||
_syncState.value = SyncState.Idle
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync a single item.
|
||||
*/
|
||||
private suspend fun syncItem(item: SyncItem) {
|
||||
// Implement sync logic based on item type
|
||||
// In a full implementation, this would call appropriate service methods
|
||||
when (item.type) {
|
||||
SyncItemType.Order -> {
|
||||
syncOrder(item)
|
||||
}
|
||||
SyncItemType.Evidence -> {
|
||||
syncEvidence(item)
|
||||
}
|
||||
SyncItemType.Credential -> {
|
||||
syncCredential(item)
|
||||
}
|
||||
SyncItemType.Directory -> {
|
||||
syncDirectoryEntry(item)
|
||||
}
|
||||
SyncItemType.Report -> {
|
||||
syncReport(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync order item.
|
||||
*/
|
||||
private suspend fun syncOrder(item: SyncItem) {
|
||||
try {
|
||||
// Serialize order data (in production, use proper serialization like JSON)
|
||||
val orderData = serializeOrderData(item.data)
|
||||
|
||||
// Send to backend API
|
||||
val result = syncAPI.syncOrder(orderData)
|
||||
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val response = result.data
|
||||
if (response.conflict && response.remoteData != null) {
|
||||
// Handle conflict
|
||||
throw ConflictException(
|
||||
localData = item.data,
|
||||
remoteData = response.remoteData,
|
||||
message = "Order conflict detected: ${item.id}"
|
||||
)
|
||||
}
|
||||
// Sync successful - item is now synced
|
||||
}
|
||||
is Result.Error -> throw result.exception
|
||||
is Result.Loading -> throw Exception("Unexpected loading state")
|
||||
}
|
||||
} catch (e: ConflictException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to sync order: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync evidence item.
|
||||
*/
|
||||
private suspend fun syncEvidence(item: SyncItem) {
|
||||
try {
|
||||
val evidenceData = serializeEvidenceData(item.data)
|
||||
val result = syncAPI.syncEvidence(evidenceData)
|
||||
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val response = result.data
|
||||
if (response.conflict && response.remoteData != null) {
|
||||
throw ConflictException(
|
||||
localData = item.data,
|
||||
remoteData = response.remoteData,
|
||||
message = "Evidence conflict detected: ${item.id}"
|
||||
)
|
||||
}
|
||||
}
|
||||
is Result.Error -> throw result.exception
|
||||
is Result.Loading -> throw Exception("Unexpected loading state")
|
||||
}
|
||||
} catch (e: ConflictException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to sync evidence: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync credential item.
|
||||
*/
|
||||
private suspend fun syncCredential(item: SyncItem) {
|
||||
try {
|
||||
val credentialData = serializeCredentialData(item.data)
|
||||
val result = syncAPI.syncCredential(credentialData)
|
||||
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val response = result.data
|
||||
if (response.conflict && response.remoteData != null) {
|
||||
throw ConflictException(
|
||||
localData = item.data,
|
||||
remoteData = response.remoteData,
|
||||
message = "Credential conflict detected: ${item.id}"
|
||||
)
|
||||
}
|
||||
}
|
||||
is Result.Error -> throw result.exception
|
||||
is Result.Loading -> throw Exception("Unexpected loading state")
|
||||
}
|
||||
} catch (e: ConflictException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to sync credential: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync directory entry item.
|
||||
*/
|
||||
private suspend fun syncDirectoryEntry(item: SyncItem) {
|
||||
try {
|
||||
val entryData = serializeDirectoryEntryData(item.data)
|
||||
val result = syncAPI.syncDirectoryEntry(entryData)
|
||||
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val response = result.data
|
||||
if (response.conflict && response.remoteData != null) {
|
||||
throw ConflictException(
|
||||
localData = item.data,
|
||||
remoteData = response.remoteData,
|
||||
message = "Directory entry conflict detected: ${item.id}"
|
||||
)
|
||||
}
|
||||
}
|
||||
is Result.Error -> throw result.exception
|
||||
is Result.Loading -> throw Exception("Unexpected loading state")
|
||||
}
|
||||
} catch (e: ConflictException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to sync directory entry: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync report item.
|
||||
*/
|
||||
private suspend fun syncReport(item: SyncItem) {
|
||||
try {
|
||||
val reportData = serializeReportData(item.data)
|
||||
val result = syncAPI.syncReport(reportData)
|
||||
|
||||
when (result) {
|
||||
is Result.Success -> {
|
||||
val response = result.data
|
||||
if (response.conflict && response.remoteData != null) {
|
||||
throw ConflictException(
|
||||
localData = item.data,
|
||||
remoteData = response.remoteData,
|
||||
message = "Report conflict detected: ${item.id}"
|
||||
)
|
||||
}
|
||||
}
|
||||
is Result.Error -> throw result.exception
|
||||
is Result.Loading -> throw Exception("Unexpected loading state")
|
||||
}
|
||||
} catch (e: ConflictException) {
|
||||
throw e
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to sync report: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize order data for transmission.
|
||||
*/
|
||||
private fun serializeOrderData(data: Any): ByteArray {
|
||||
// TODO: Use proper JSON serialization (e.g., Jackson, Gson)
|
||||
// For now, return empty array as placeholder
|
||||
return ByteArray(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize evidence data for transmission.
|
||||
*/
|
||||
private fun serializeEvidenceData(data: Any): ByteArray {
|
||||
// TODO: Use proper JSON serialization
|
||||
return ByteArray(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize credential data for transmission.
|
||||
*/
|
||||
private fun serializeCredentialData(data: Any): ByteArray {
|
||||
// TODO: Use proper JSON serialization
|
||||
return ByteArray(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize directory entry data for transmission.
|
||||
*/
|
||||
private fun serializeDirectoryEntryData(data: Any): ByteArray {
|
||||
// TODO: Use proper JSON serialization
|
||||
return ByteArray(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize report data for transmission.
|
||||
*/
|
||||
private fun serializeReportData(data: Any): ByteArray {
|
||||
// TODO: Use proper JSON serialization
|
||||
return ByteArray(0)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if offline duration threshold has been exceeded.
|
||||
*/
|
||||
fun checkOfflineDuration(lastSyncTime: Date, maxOfflineDurationMs: Long): Boolean {
|
||||
val now = Date()
|
||||
val offlineDuration = now.time - lastSyncTime.time
|
||||
return offlineDuration > maxOfflineDurationMs
|
||||
}
|
||||
|
||||
/**
|
||||
* Purge data that exceeds offline duration threshold.
|
||||
*/
|
||||
suspend fun purgeExpiredOfflineData(maxOfflineDurationMs: Long) {
|
||||
// Purge expired items from sync queue
|
||||
val now = Date()
|
||||
val expiredItems = syncQueue.filter { item ->
|
||||
val itemAge = now.time - item.timestamp.time
|
||||
itemAge > maxOfflineDurationMs
|
||||
}
|
||||
|
||||
syncQueue.removeAll(expiredItems)
|
||||
|
||||
// TODO: Integrate with individual services to purge expired data
|
||||
// This would:
|
||||
// 1. Check each data type's offline duration policy
|
||||
// 2. Remove expired data from local storage
|
||||
// 3. Log purging events
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync item types.
|
||||
*/
|
||||
enum class SyncItemType {
|
||||
Order,
|
||||
Evidence,
|
||||
Credential,
|
||||
Directory,
|
||||
Report
|
||||
}
|
||||
|
||||
/**
|
||||
* Item to be synchronized.
|
||||
*/
|
||||
data class SyncItem(
|
||||
val id: String,
|
||||
val type: SyncItemType,
|
||||
val data: Any,
|
||||
val timestamp: Date = Date(),
|
||||
val operation: SyncOperation = SyncOperation.Update
|
||||
)
|
||||
|
||||
/**
|
||||
* Sync operations.
|
||||
*/
|
||||
enum class SyncOperation {
|
||||
Create,
|
||||
Update,
|
||||
Delete
|
||||
}
|
||||
|
||||
/**
|
||||
* Sync state.
|
||||
*/
|
||||
sealed class SyncState {
|
||||
object Idle : SyncState()
|
||||
object WaitingForConnection : SyncState()
|
||||
data class Syncing(val itemsRemaining: Int) : SyncState()
|
||||
data class Error(val message: String) : SyncState()
|
||||
}
|
||||
|
||||
/**
|
||||
* Conflict exception.
|
||||
*/
|
||||
class ConflictException(
|
||||
val localData: Any,
|
||||
val remoteData: Any,
|
||||
message: String
|
||||
) : Exception(message)
|
||||
|
||||
/**
|
||||
* Conflict resolver.
|
||||
*/
|
||||
class ConflictResolver {
|
||||
fun resolveConflict(item: SyncItem, exception: ConflictException): ConflictResolution {
|
||||
// Default strategy: use remote (server wins)
|
||||
// Can be customized based on item type or policy
|
||||
return ConflictResolution.UseRemote
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Conflict resolution strategies.
|
||||
*/
|
||||
sealed class ConflictResolution {
|
||||
object UseLocal : ConflictResolution()
|
||||
object UseRemote : ConflictResolution()
|
||||
data class Merge(val mergedData: Any) : ConflictResolution()
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package com.smoa.core.common.di
|
||||
|
||||
import android.content.Context
|
||||
import com.smoa.core.common.ConnectivityManager
|
||||
import com.smoa.core.common.FoldableStateManager
|
||||
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 CommonModule {
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideFoldableStateManager(): FoldableStateManager {
|
||||
return FoldableStateManager()
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideConnectivityManager(
|
||||
@ApplicationContext context: Context
|
||||
): ConnectivityManager {
|
||||
return ConnectivityManager(context)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideSyncService(
|
||||
@ApplicationContext context: Context,
|
||||
connectivityManager: ConnectivityManager
|
||||
): com.smoa.core.common.SyncService {
|
||||
return com.smoa.core.common.SyncService(context, connectivityManager)
|
||||
}
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
fun provideOfflinePolicyManager(): com.smoa.core.common.OfflinePolicyManager {
|
||||
return com.smoa.core.common.OfflinePolicyManager()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import io.mockk.MockKMatcherScope
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.coVerify
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.flowOf
|
||||
|
||||
/**
|
||||
* Mock helpers for common test scenarios.
|
||||
*/
|
||||
object MockHelpers {
|
||||
/**
|
||||
* Create a mock that returns a successful Result.
|
||||
*/
|
||||
inline fun <reified T> mockSuccess(value: T): T {
|
||||
return mockk<T> {
|
||||
// Add common mock behaviors here
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a mock that returns a failed Result.
|
||||
*/
|
||||
inline fun <reified T> mockFailure(exception: Exception): T {
|
||||
return mockk<T> {
|
||||
// Add common mock behaviors here
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Flow mock that emits a single value.
|
||||
*/
|
||||
fun <T> mockFlow(value: T): Flow<T> = flowOf(value)
|
||||
|
||||
/**
|
||||
* Create a Flow mock that emits multiple values.
|
||||
*/
|
||||
fun <T> mockFlow(vararg values: T): Flow<T> = flowOf(*values)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function for coEvery with Result.
|
||||
*/
|
||||
fun <T> MockKMatcherScope.coEveryResult(
|
||||
block: suspend MockKMatcherScope.() -> Result<T>
|
||||
): Result<T> {
|
||||
return coEvery { block() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Extension function for coVerify with Result.
|
||||
*/
|
||||
fun <T> MockKMatcherScope.coVerifyResult(
|
||||
verifyBlock: suspend MockKMatcherScope.(Result<T>) -> Unit
|
||||
) {
|
||||
coVerify { verifyBlock(any()) }
|
||||
}
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Unit tests for OfflinePolicyManager.
|
||||
*/
|
||||
class OfflinePolicyManagerTest {
|
||||
private val policyManager = OfflinePolicyManager()
|
||||
|
||||
@Test
|
||||
fun `getMaxOfflineDuration should return correct duration for each type`() {
|
||||
// When
|
||||
val credentialDuration = policyManager.getMaxOfflineDuration(OfflineDataType.Credential)
|
||||
val orderDuration = policyManager.getMaxOfflineDuration(OfflineDataType.Order)
|
||||
val evidenceDuration = policyManager.getMaxOfflineDuration(OfflineDataType.Evidence)
|
||||
|
||||
// Then
|
||||
assertTrue(credentialDuration > 0)
|
||||
assertTrue(orderDuration > 0)
|
||||
assertTrue(evidenceDuration > 0)
|
||||
assertTrue(evidenceDuration > orderDuration) // Evidence has longer retention
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOfflineDataValid should return true for recent data`() {
|
||||
// Given
|
||||
val recentDate = Date(System.currentTimeMillis() - (1 * 24 * 60 * 60 * 1000L)) // 1 day ago
|
||||
|
||||
// When
|
||||
val result = policyManager.isOfflineDataValid(recentDate, OfflineDataType.Credential)
|
||||
|
||||
// Then
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `isOfflineDataValid should return false for expired data`() {
|
||||
// Given
|
||||
val oldDate = Date(System.currentTimeMillis() - (100 * 24 * 60 * 60 * 1000L)) // 100 days ago
|
||||
|
||||
// When
|
||||
val result = policyManager.isOfflineDataValid(oldDate, OfflineDataType.Credential)
|
||||
|
||||
// Then
|
||||
assertFalse(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `shouldPurgeOfflineData should return true for expired data`() {
|
||||
// Given
|
||||
val oldDate = Date(System.currentTimeMillis() - (100 * 24 * 60 * 60 * 1000L))
|
||||
|
||||
// When
|
||||
val result = policyManager.shouldPurgeOfflineData(oldDate, OfflineDataType.Credential)
|
||||
|
||||
// Then
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `getTimeUntilExpiration should return positive value for valid data`() {
|
||||
// Given
|
||||
val recentDate = Date(System.currentTimeMillis() - (1 * 24 * 60 * 60 * 1000L))
|
||||
|
||||
// When
|
||||
val timeRemaining = policyManager.getTimeUntilExpiration(recentDate, OfflineDataType.Credential)
|
||||
|
||||
// Then
|
||||
assertTrue(timeRemaining > 0)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import com.smoa.core.common.SyncAPI
|
||||
import com.smoa.core.common.SyncResponse
|
||||
import io.mockk.coEvery
|
||||
import io.mockk.mockk
|
||||
import kotlinx.coroutines.test.runTest
|
||||
import org.junit.Assert.*
|
||||
import org.junit.Test
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Unit tests for SyncService.
|
||||
*/
|
||||
class SyncServiceTest {
|
||||
private val context = mockk<android.content.Context>(relaxed = true)
|
||||
private val connectivityManager = mockk<ConnectivityManager>(relaxed = true)
|
||||
private val syncAPI = mockk<SyncAPI>(relaxed = true)
|
||||
private val syncService = SyncService(context, connectivityManager, syncAPI)
|
||||
|
||||
@Test
|
||||
fun `queueSync should add item to queue`() = runTest {
|
||||
// Given
|
||||
val item = SyncItem(
|
||||
id = "test1",
|
||||
type = SyncItemType.Order,
|
||||
data = "test data"
|
||||
)
|
||||
every { connectivityManager.isOnline() } returns false
|
||||
|
||||
// When
|
||||
syncService.queueSync(item)
|
||||
|
||||
// Then
|
||||
// Item should be queued (we can't directly verify queue, but sync should work)
|
||||
assertTrue(true) // Placeholder - would verify queue state if exposed
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `startSync should sync items when online`() = runTest {
|
||||
// Given
|
||||
val item = SyncItem(
|
||||
id = "test1",
|
||||
type = SyncItemType.Order,
|
||||
data = "test data"
|
||||
)
|
||||
every { connectivityManager.isOnline() } returns true
|
||||
coEvery { syncAPI.syncOrder(any()) } returns Result.success(
|
||||
SyncResponse(
|
||||
success = true,
|
||||
itemId = "test1",
|
||||
serverTimestamp = System.currentTimeMillis()
|
||||
)
|
||||
)
|
||||
|
||||
// When
|
||||
syncService.queueSync(item)
|
||||
syncService.startSync()
|
||||
|
||||
// Then
|
||||
// Sync should complete successfully
|
||||
assertTrue(true) // Placeholder - would verify sync state
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `checkOfflineDuration should return true when exceeded`() {
|
||||
// Given
|
||||
val lastSyncTime = Date(System.currentTimeMillis() - (8 * 24 * 60 * 60 * 1000L)) // 8 days ago
|
||||
val maxDuration = 7L * 24 * 60 * 60 * 1000L // 7 days
|
||||
|
||||
// When
|
||||
val result = syncService.checkOfflineDuration(lastSyncTime, maxDuration)
|
||||
|
||||
// Then
|
||||
assertTrue(result)
|
||||
}
|
||||
|
||||
@Test
|
||||
fun `checkOfflineDuration should return false when within limit`() {
|
||||
// Given
|
||||
val lastSyncTime = Date(System.currentTimeMillis() - (5 * 24 * 60 * 60 * 1000L)) // 5 days ago
|
||||
val maxDuration = 7L * 24 * 60 * 60 * 1000L // 7 days
|
||||
|
||||
// When
|
||||
val result = syncService.checkOfflineDuration(lastSyncTime, maxDuration)
|
||||
|
||||
// Then
|
||||
assertFalse(result)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.ExperimentalCoroutinesApi
|
||||
import kotlinx.coroutines.test.StandardTestDispatcher
|
||||
import kotlinx.coroutines.test.TestDispatcher
|
||||
import kotlinx.coroutines.test.resetMain
|
||||
import kotlinx.coroutines.test.setMain
|
||||
import org.junit.rules.TestWatcher
|
||||
import org.junit.runner.Description
|
||||
|
||||
/**
|
||||
* JUnit rule for testing coroutines.
|
||||
* Provides a test dispatcher and manages coroutine context.
|
||||
*/
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
class TestCoroutineRule(
|
||||
private val testDispatcher: TestDispatcher = StandardTestDispatcher()
|
||||
) : TestWatcher() {
|
||||
|
||||
override fun starting(description: Description) {
|
||||
super.starting(description)
|
||||
Dispatchers.setMain(testDispatcher)
|
||||
}
|
||||
|
||||
override fun finished(description: Description) {
|
||||
super.finished(description)
|
||||
Dispatchers.resetMain()
|
||||
}
|
||||
|
||||
fun runTest(block: suspend () -> Unit) {
|
||||
testDispatcher.scheduler.advanceUntilIdle()
|
||||
}
|
||||
}
|
||||
|
||||
16
core/common/src/test/java/com/smoa/core/common/TestUtils.kt
Normal file
16
core/common/src/test/java/com/smoa/core/common/TestUtils.kt
Normal file
@@ -0,0 +1,16 @@
|
||||
package com.smoa.core.common
|
||||
|
||||
/**
|
||||
* Test utilities and helpers.
|
||||
*/
|
||||
object TestUtils {
|
||||
/**
|
||||
* Create a test connectivity manager.
|
||||
*/
|
||||
fun createTestConnectivityManager(): ConnectivityManager {
|
||||
// This would be a mock or test implementation
|
||||
// For now, return a placeholder
|
||||
throw NotImplementedError("Test implementation needed")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user