6.1 Stress Analysis Manager (Orchestrator)
domain/usecase/StressAnalysisManager.kt
package com.stressless.android.domain.usecase
import android.util.Log
import com.stressless.android.data.repository.LocalStressRepository
import com.stressless.android.domain.model.StressAnalysisResult
import com.stressless.android.ml.audio.AudioPipeline
import com.stressless.android.ml.audio.AudioQuality
import com.stressless.android.ml.models.ECAPAStressEngine
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import java.util.*
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class StressAnalysisManager @Inject constructor(
private val ecapaEngine: ECAPAStressEngine,
private val audioPipeline: AudioPipeline,
private val localRepository: LocalStressRepository
) {
suspend fun performStressAnalysis(
rawAudioData: List<ByteArray>,
context: StressAnalysisContext = StressAnalysisContext()
): StressAnalysisResult = withContext(Dispatchers.Default) {
val analysisStartTime = System.currentTimeMillis()
try {
Log.i("StressAnalysis", "Starting stress analysis with ${rawAudioData.size} audio chunks")
// 1. Audio preprocessing and quality check
val processedAudio = audioPipeline.preprocessAudio(rawAudioData)
val audioQuality = audioPipeline.validateAudioQuality(processedAudio)
if (audioQuality.quality == AudioQuality.Quality.POOR) {
Log.w("StressAnalysis", "Poor audio quality detected: RMS=${audioQuality.rms}, SNR=${audioQuality.snr}")
}
// 2. Feature extraction (MFCC)
val mfccFeatures = audioPipeline.extractMFCCFeatures(processedAudio)
Log.d("StressAnalysis", "MFCC feature extraction completed: ${mfccFeatures.size} features")
// 3. ECAPA-TDNN stress analysis
val embeddings = ecapaEngine.extractStressEmbeddings(mfccFeatures)
val classification = ecapaEngine.classifyStress(embeddings)
// 4. Generate contextual recommendations
val recommendations = generateRecommendations(
classification.level,
classification.confidence,
context,
audioQuality
)
// 5. Create comprehensive result
val totalProcessingTime = System.currentTimeMillis() - analysisStartTime
val result = StressAnalysisResult(
id = UUID.randomUUID().toString(),
stressLevel = classification.level,
confidence = classification.confidence,
recommendations = recommendations,
context = context,
audioQuality = audioQuality,
timestamp = System.currentTimeMillis(),
processingTimeMs = totalProcessingTime,
modelVersion = "ecapa-v1.0"
)
// 6. Save result locally with error handling
try {
localRepository.saveStressAssessment(result)
Log.i("StressAnalysis", "Analysis saved successfully: ${result.id}")
} catch (e: Exception) {
Log.e("StressAnalysis", "Failed to save analysis result", e)
// Don't fail the entire analysis if save fails
}
Log.i("StressAnalysis", "Analysis completed in ${totalProcessingTime}ms with stress level ${classification.level}/10")
return@withContext result
} catch (e: Exception) {
Log.e("StressAnalysis", "Stress analysis failed", e)
throw StressAnalysisException("Analysis failed: ${e.message}", e)
}
}
private fun generateRecommendations(
stressLevel: Int,
confidence: Float,
context: StressAnalysisContext,
audioQuality: AudioQuality
): List<String> {
val recommendations = mutableListOf<String>()
// Add audio quality feedback if needed
if (audioQuality.quality == AudioQuality.Quality.POOR) {
recommendations.add("Try recording in a quieter environment for more accurate results.")
}
// Add confidence-based feedback
if (confidence < 0.7f) {
recommendations.add("Results have lower confidence. Consider recording again in a quiet space.")
}
// Add context-aware stress level recommendations
val stressRecommendations = when {
stressLevel <= 3 -> listOf(
"You seem calm and relaxed. Great job managing your stress!",
"Consider maintaining your current routine and coping strategies."
)
stressLevel <= 5 -> listOf(
"Mild stress detected. This is normal and manageable.",
"Try taking a few deep breaths using the 4-7-8 technique.",
"A short break or brief walk might help you reset."
)
stressLevel <= 7 -> listOf(
"Moderate stress level detected. Time for some stress relief.",
"Try progressive muscle relaxation or meditation.",
"Consider what might be causing stress and how to address it.",
"Take breaks throughout your day to prevent stress buildup."
)
else -> listOf(
"Higher stress level detected. Please prioritize stress management.",
"Try the 4-7-8 breathing technique: inhale for 4, hold for 7, exhale for 8.",
"Consider stepping away from stressful situations if possible.",
"If stress persists, consider speaking with a healthcare professional."
)
}
recommendations.addAll(stressRecommendations)
// Add context-specific recommendations
context.activity?.let { activity ->
when (activity.lowercase()) {
"meeting" -> recommendations.add("Try grounding techniques: notice 5 things you can see, 4 you can hear.")
"commute" -> recommendations.add("Listen to calming music or practice mindful breathing while traveling.")
"work" -> recommendations.add("Take regular breaks and ensure proper ergonomics at your workspace.")
"exercise" -> recommendations.add("Some stress during exercise is normal. Ensure proper warm-up and cool-down.")
}
}
return recommendations.take(4) // Limit to 4 recommendations for better UX
}
suspend fun getRecentTrend(days: Int = 7): StressTrend? = withContext(Dispatchers.IO) {
try {
val startDate = System.currentTimeMillis() - (days * 24 * 60 * 60 * 1000L)
val assessments = localRepository.getAssessmentsByDateRange(startDate, System.currentTimeMillis())
if (assessments.isEmpty()) return@withContext null
val averageStress = assessments.map { it.stressLevel }.average().toFloat()
val trend = if (assessments.size >= 2) {
val recent = assessments.take(assessments.size / 2).map { it.stressLevel }.average()
val earlier = assessments.drop(assessments.size / 2).map { it.stressLevel }.average()
when {
recent > earlier + 1 -> TrendDirection.INCREASING
recent < earlier - 1 -> TrendDirection.DECREASING
else -> TrendDirection.STABLE
}
} else {
TrendDirection.STABLE
}
StressTrend(
averageLevel = averageStress,
direction = trend,
assessmentCount = assessments.size,
periodDays = days
)
} catch (e: Exception) {
Log.e("StressAnalysis", "Failed to get recent trend", e)
null
}
}
}
data class StressAnalysisContext(
val location: String? = null,
val activity: String? = null,
val timeOfDay: String? = null,
val additionalNotes: String? = null
)
data class StressTrend(
val averageLevel: Float,
val direction: TrendDirection,
val assessmentCount: Int,
val periodDays: Int
)
enum class TrendDirection {
INCREASING, DECREASING, STABLE
}
class StressAnalysisException(message: String, cause: Throwable) : Exception(message, cause)
6.2 Local Repository Implementation
data/repository/LocalStressRepository.kt
package com.stressless.android.data.repository
import android.content.Context
import android.util.Log
import com.stressless.android.data.local.dao.StressAssessmentDao
import com.stressless.android.data.local.entities.StressAssessmentEntity
import com.stressless.android.data.security.EncryptionManager
import com.stressless.android.domain.model.StressAnalysisResult
import com.stressless.android.domain.repository.StressRepository
import com.stressless.android.domain.usecase.StressAnalysisContext
import com.stressless.android.ml.audio.AudioQuality
import dagger.hilt.android.qualifiers.ApplicationContext
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import javax.inject.Inject
import javax.inject.Singleton
@Singleton
class LocalStressRepository @Inject constructor(
private val dao: StressAssessmentDao,
private val encryptionManager: EncryptionManager,
@ApplicationContext private val context: Context
) : StressRepository {
private val json = Json {
ignoreUnknownKeys = true
encodeDefaults = true
}
override suspend fun saveStressAssessment(result: StressAnalysisResult) {
try {
// Encrypt sensitive data (recommendations)
val encryptedRecommendations = encryptionManager.encrypt(
json.encodeToString(result.recommendations)
)
val contextJson = json.encodeToString(result.context)
val audioQualityJson = json.encodeToString(result.audioQuality)
val entity = StressAssessmentEntity(
id = result.id,
stressLevel = result.stressLevel,
confidence = result.confidence,
encryptedRecommendations = encryptedRecommendations,
contextTags = contextJson,
timestamp = result.timestamp,
processingTimeMs = result.processingTimeMs,
modelVersion = result.modelVersion,
synced = false
)
dao.insertAssessment(entity)
Log.d("Repository", "Stress assessment saved: ${result.id}")
} catch (e: Exception) {
Log.e("Repository", "Failed to save stress assessment", e)
throw StressRepositoryException("Failed to save assessment", e)
}
}
override fun getAllAssessments(): Flow<List<StressAnalysisResult>> {
return dao.getAllAssessments()
.map { entities -> entities.map { it.toStressAnalysisResult() } }
.catch { e ->
Log.e("Repository", "Failed to get all assessments", e)
emit(emptyList())
}
}
override suspend fun getAssessmentById(id: String): StressAnalysisResult? {
return try {
dao.getAssessmentById(id)?.toStressAnalysisResult()
} catch (e: Exception) {
Log.e("Repository", "Failed to get assessment by id: $id", e)
null
}
}
override suspend fun getAssessmentsByDateRange(
startDate: Long,
endDate: Long
): List<StressAnalysisResult> {
return try {
dao.getAssessmentsByDateRange(startDate, endDate)
.map { it.toStressAnalysisResult() }
} catch (e: Exception) {
Log.e("Repository", "Failed to get assessments by date range", e)
emptyList()
}
}
override suspend fun exportAllData(): String {
return try {
val assessments = dao.getAllAssessments()
// Create exportable format without encrypted data
val exportData = mapOf(
"export_timestamp" to System.currentTimeMillis(),
"app_version" to "1.0.0",
"total_assessments" to assessments.first().size,
"assessments" to assessments.first().map { entity ->
mapOf(
"id" to entity.id,
"stress_level" to entity.stressLevel,
"confidence" to entity.confidence,
"timestamp" to entity.timestamp,
"processing_time_ms" to entity.processingTimeMs,
"model_version" to entity.modelVersion,
// Decrypt recommendations for export
"recommendations" to try {
json.decodeFromString<List<String>>(
encryptionManager.decrypt(entity.encryptedRecommendations)
)
} catch (e: Exception) {
emptyList<String>()
},
"context" to entity.contextTags
)
}
)
json.encodeToString(exportData)
} catch (e: Exception) {
Log.e("Repository", "Failed to export data", e)
throw StressRepositoryException("Failed to export data", e)
}
}
override suspend fun deleteAllData() {
try {
dao.clearAllAssessments()
encryptionManager.clearAllKeys()
Log.i("Repository", "All user data deleted successfully")
} catch (e: Exception) {
Log.e("Repository", "Failed to delete all data", e)
throw StressRepositoryException("Failed to delete data", e)
}
}
override suspend fun getAssessmentCount(): Int {
return try {
dao.getAssessmentCount()
} catch (e: Exception) {
Log.e("Repository", "Failed to get assessment count", e)
0
}
}
override suspend fun getAverageStressLevel(days: Int): Float? {
return try {
val startDate = System.currentTimeMillis() - (days * 24 * 60 * 60 * 1000L)
dao.getAverageStressLevel(startDate)
} catch (e: Exception) {
Log.e("Repository", "Failed to get average stress level", e)
null
}
}
private fun StressAssessmentEntity.toStressAnalysisResult(): StressAnalysisResult {
val decryptedRecommendations = try {
json.decodeFromString<List<String>>(
encryptionManager.decrypt(encryptedRecommendations)
)
} catch (e: Exception) {
Log.w("Repository", "Failed to decrypt recommendations for ${this.id}", e)
listOf("Recommendations unavailable")
}
val contextData = try {
json.decodeFromString<StressAnalysisContext>(contextTags ?: "{}")
} catch (e: Exception) {
StressAnalysisContext()
}
// Default audio quality for legacy records
val audioQuality = AudioQuality(
rms = 0.1f,
snr = 20f,
clippingRate = 0f,
quality = AudioQuality.Quality.GOOD
)
return StressAnalysisResult(
id = id,
stressLevel = stressLevel,
confidence = confidence,
recommendations = decryptedRecommendations,
context = contextData,
audioQuality = audioQuality,
timestamp = timestamp,
processingTimeMs = processingTimeMs,
modelVersion = modelVersion
)
}
}
class StressRepositoryException(message: String, cause: Throwable) : Exception(message, cause)