Files
Avtex/app/src/main/java/com/example/avtex/engine/SleepTrackingService.kt
T

374 lines
16 KiB
Kotlin
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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")
}
}
}