package com.example.avtex.engine import android.Manifest import android.app.Notification import android.app.PendingIntent import android.app.Service import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import android.content.IntentFilter import android.content.pm.PackageManager import android.os.Build import android.os.IBinder import android.os.PowerManager import android.util.Log import androidx.core.app.NotificationCompat import androidx.core.content.ContextCompat import com.example.avtex.AvtexApplication import com.example.avtex.MainActivity import com.example.avtex.hac.HacClient import com.google.android.gms.location.ActivityRecognition import com.google.android.gms.location.SleepSegmentRequest import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.delay import kotlinx.coroutines.launch import java.text.SimpleDateFormat import java.util.Date import java.util.Locale class SleepTrackingService : Service() { private val serviceScope = CoroutineScope(SupervisorJob() + Dispatchers.Default) private lateinit var significantMotionTrigger: SignificantMotionTrigger private lateinit var hacClient: HacClient private var screenReceiver: BroadcastReceiver? = null private var inactivityJob: Job? = null private var sleepApiPendingIntent: PendingIntent? = null private var currentState: SleepState = SleepState.AWAKE private var stateChangeTimestamp: Long = System.currentTimeMillis() private val settingsRepository get() = AvtexApplication.instance.settingsRepository override fun onCreate() { super.onCreate() Log.d(TAG, "SleepTrackingService onCreate") hacClient = HacClient(this) significantMotionTrigger = SignificantMotionTrigger(this) { handleWakeupDetected(source = "significant_motion") } registerScreenReceiver() } override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { val action = intent?.action ?: ACTION_START_TRACKING Log.d(TAG, "onStartCommand action: $action") when (action) { ACTION_START_TRACKING -> { startForegroundServiceWithNotification() initializeTracking() } ACTION_STOP_TRACKING -> { stopTracking() stopForeground(STOP_FOREGROUND_REMOVE) stopSelf() } ACTION_MANUAL_TOGGLE -> { val newState = if (currentState == SleepState.AWAKE) SleepState.ASLEEP else SleepState.AWAKE transitionToState(newState, source = "manual_toggle", confidence = 100) } ACTION_TEST_HAC -> { serviceScope.launch { val settings = settingsRepository.getSettings() val isAsleep = currentState == SleepState.ASLEEP settingsRepository.addLog("HAC_TEST", "Отправка тестового запроса в Home Assistant...") val result = hacClient.sendSleepEvent(settings, isAsleep, source = "manual_test") result.onSuccess { msg -> settingsRepository.addLog("HAC_TEST", "Тест успешен: $msg", isImportant = true) settingsRepository.updateHacStatus("Тест успешен: $msg") }.onFailure { err -> settingsRepository.addLog("HAC_TEST", "Ошибка теста: ${err.localizedMessage}", isImportant = true) settingsRepository.updateHacStatus("Ошибка: ${err.localizedMessage}") } } } } return START_STICKY } private fun startForegroundServiceWithNotification() { val notification = buildNotification( title = "Avtex: Мониторинг активности", text = "Статус: ${if (currentState == SleepState.ASLEEP) "Спит 🌙" else "Бодрствует ☀️"}" ) startForeground(NOTIFICATION_ID, notification) } private fun initializeTracking() { serviceScope.launch { val settings = settingsRepository.getSettings() currentState = if (settings.currentState == "ASLEEP") SleepState.ASLEEP else SleepState.AWAKE stateChangeTimestamp = settings.lastStateChangeMillis settingsRepository.updateTrackingActive(true) settingsRepository.addLog("Engine", "Служба мониторинга сна запущена", isImportant = true) updateNotification() subscribeToSleepApi() if (currentState == SleepState.ASLEEP) { significantMotionTrigger.arm() } } } private fun stopTracking() { serviceScope.launch { settingsRepository.updateTrackingActive(false) settingsRepository.addLog("Engine", "Служба мониторинга остановлена", isImportant = true) significantMotionTrigger.disarm() unsubscribeFromSleepApi() inactivityJob?.cancel() } } private fun registerScreenReceiver() { screenReceiver = object : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { when (intent.action) { Intent.ACTION_SCREEN_OFF -> { Log.d(TAG, "Screen OFF event received") handleScreenOff() } Intent.ACTION_SCREEN_ON -> { Log.d(TAG, "Screen ON event received") } Intent.ACTION_USER_PRESENT -> { Log.d(TAG, "User unlocked device") handleWakeupDetected(source = "screen_unlocked") } } } } val filter = IntentFilter().apply { addAction(Intent.ACTION_SCREEN_OFF) addAction(Intent.ACTION_SCREEN_ON) addAction(Intent.ACTION_USER_PRESENT) } registerReceiver(screenReceiver, filter) } private fun handleScreenOff() { if (currentState == SleepState.AWAKE) { inactivityJob?.cancel() inactivityJob = serviceScope.launch { val settings = settingsRepository.getSettings() val minutes = settings.inactivityMinutes Log.d(TAG, "Starting inactivity countdown: $minutes minutes") settingsRepository.addLog("Engine", "Экран выключен. Ожидание сна ($minutes мин)...") delay(minutes * 60 * 1000L) // If still awake and not interrupted, assume sleep if (currentState == SleepState.AWAKE) { settingsRepository.addLog("Engine", "Время покоя истекло ($minutes мин). Переход в режим сна.", isImportant = true) transitionToState(SleepState.ASLEEP, source = "inactivity_timer", confidence = 90) } } } } private fun handleWakeupDetected(source: String) { inactivityJob?.cancel() if (currentState == SleepState.ASLEEP) { Log.d(TAG, "Wakeup detected from source: $source") transitionToState(SleepState.AWAKE, source = source, confidence = 100) } } private fun transitionToState(newState: SleepState, source: String, confidence: Int) { if (currentState == newState) return currentState = newState stateChangeTimestamp = System.currentTimeMillis() serviceScope.launch { val isAsleep = (newState == SleepState.ASLEEP) val stateName = if (isAsleep) "ASLEEP" else "AWAKE" settingsRepository.updateCurrentState(stateName, stateChangeTimestamp) if (isAsleep) { // When falling asleep, arm significant motion trigger for instant wake detection val armed = significantMotionTrigger.arm() settingsRepository.addLog( "Engine", "Зафиксировано засыпание ($source). Датчик движения взведен: $armed", isImportant = true ) } else { // When waking up, disarm trigger significantMotionTrigger.disarm() settingsRepository.addLog( "Engine", "Зафиксировано пробуждение ($source).", isImportant = true ) } updateNotification() // Acquire temporary WakeLock to ensure HTTP request reaches HAC on OnePlus val powerManager = getSystemService(Context.POWER_SERVICE) as PowerManager val wakeLock = powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "Avtex:HacTransmissionWakeLock") wakeLock.acquire(15000L) // max 15 seconds try { val settings = settingsRepository.getSettings() val result = hacClient.sendSleepEvent(settings, isAsleep, source, confidence) result.onSuccess { msg -> Log.d(TAG, "HAC send success: $msg") settingsRepository.addLog("HAC", "Отправлено в Home Assistant: $msg") settingsRepository.updateHacStatus("Отправлено в ${formatTime(stateChangeTimestamp)}: $msg") }.onFailure { err -> Log.e(TAG, "HAC send error", err) settingsRepository.addLog("HAC", "Ошибка отправки в Home Assistant: ${err.localizedMessage}", isImportant = true) settingsRepository.updateHacStatus("Ошибка: ${err.localizedMessage}") } } finally { if (wakeLock.isHeld) { wakeLock.release() } } } } private fun subscribeToSleepApi() { if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACTIVITY_RECOGNITION) != PackageManager.PERMISSION_GRANTED) { Log.w(TAG, "ACTIVITY_RECOGNITION permission not granted for Sleep API") settingsRepository.addLog("SleepAPI", "Нет разрешения на активность. Используются аппаратные датчики движения.") return } try { val intent = Intent(this, SleepReceiver::class.java).apply { action = SleepReceiver.ACTION_SLEEP_API_EVENT } val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE } else { PendingIntent.FLAG_UPDATE_CURRENT } sleepApiPendingIntent = PendingIntent.getBroadcast(this, REQUEST_CODE_SLEEP_API, intent, flags) val request = SleepSegmentRequest.getDefaultSleepSegmentRequest() val task = ActivityRecognition.getClient(this).requestSleepSegmentUpdates(sleepApiPendingIntent!!, request) task.addOnSuccessListener { Log.d(TAG, "Successfully subscribed to Google Sleep API") settingsRepository.addLog("SleepAPI", "Google Sleep API успешно подключен") } task.addOnFailureListener { e -> Log.e(TAG, "Failed to subscribe to Google Sleep API", e) settingsRepository.addLog("SleepAPI", "Не удалось подключить Google Sleep API: ${e.localizedMessage}") } } catch (e: Exception) { Log.e(TAG, "Exception subscribing to Sleep API", e) } } private fun unsubscribeFromSleepApi() { sleepApiPendingIntent?.let { pi -> try { ActivityRecognition.getClient(this).removeSleepSegmentUpdates(pi) Log.d(TAG, "Unsubscribed from Sleep API") } catch (e: Exception) { Log.e(TAG, "Error unsubscribing from Sleep API", e) } } } private fun buildNotification(title: String, text: String): Notification { val openAppIntent = Intent(this, MainActivity::class.java).apply { flags = Intent.FLAG_ACTIVITY_SINGLE_TOP } val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE } else { PendingIntent.FLAG_UPDATE_CURRENT } val pendingIntent = PendingIntent.getActivity(this, 0, openAppIntent, flags) return NotificationCompat.Builder(this, AvtexApplication.CHANNEL_ID_TRACKING) .setContentTitle(title) .setContentText(text) .setSmallIcon(android.R.drawable.ic_lock_idle_charging) .setContentIntent(pendingIntent) .setOngoing(true) .setPriority(NotificationCompat.PRIORITY_LOW) .setCategory(NotificationCompat.CATEGORY_SERVICE) .build() } private fun updateNotification() { val stateText = if (currentState == SleepState.ASLEEP) "Спит 🌙" else "Бодрствует ☀️" val timeFormatted = formatTime(stateChangeTimestamp) val notification = buildNotification( title = "Avtex: $stateText", text = "Состояние с $timeFormatted" ) val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as android.app.NotificationManager notificationManager.notify(NOTIFICATION_ID, notification) } private fun formatTime(millis: Long): String { return SimpleDateFormat("HH:mm", Locale.getDefault()).format(Date(millis)) } override fun onDestroy() { Log.d(TAG, "SleepTrackingService onDestroy") screenReceiver?.let { unregisterReceiver(it) screenReceiver = null } significantMotionTrigger.disarm() unsubscribeFromSleepApi() serviceScope.cancel() super.onDestroy() } override fun onBind(intent: Intent?): IBinder? = null companion object { private const val TAG = "SleepTrackingService" private const val NOTIFICATION_ID = 1001 private const val REQUEST_CODE_SLEEP_API = 2001 const val ACTION_START_TRACKING = "com.example.avtex.ACTION_START_TRACKING" const val ACTION_STOP_TRACKING = "com.example.avtex.ACTION_STOP_TRACKING" const val ACTION_MANUAL_TOGGLE = "com.example.avtex.ACTION_MANUAL_TOGGLE" const val ACTION_TEST_HAC = "com.example.avtex.ACTION_TEST_HAC" @Volatile private var instance: SleepTrackingService? = null fun onSleepClassifyReceived(context: Context, confidence: Int, motion: Int, light: Int) { // If confidence is high and device is in stationary motion val repo = AvtexApplication.instance.settingsRepository CoroutineScope(Dispatchers.Default).launch { val settings = repo.getSettings() if (settings.isTrackingActive && settings.currentState == "AWAKE") { if (confidence >= settings.confidenceThreshold && motion <= 2) { Log.d(TAG, "SleepClassify indicates sleep: confidence=$confidence, motion=$motion") // Send intent to service to transition state val intent = Intent(context, SleepTrackingService::class.java).apply { action = ACTION_MANUAL_TOGGLE // or specific intent } // Only trigger if actually confident } } } } fun onSleepSegmentReceived(context: Context, start: Long, end: Long, status: Int) { Log.d(TAG, "SleepSegment completed: start=$start, end=$end, status=$status") } } }