Files
gh-asleep-ai-sleeptrack-ski…/skills/sleeptrack-android/references/complete_viewmodel_implementation.md
2025-11-29 17:58:23 +08:00

18 KiB

Complete ViewModel and Activity Implementation

This reference provides complete, production-ready implementations for Android MVVM architecture with the Asleep SDK.

Complete ViewModel with State Management

@HiltViewModel
class SleepTrackingViewModel @Inject constructor(
    @ApplicationContext private val applicationContext: Application
) : ViewModel() {

    // State management
    private val _trackingState = MutableStateFlow<AsleepState>(AsleepState.STATE_IDLE)
    val trackingState: StateFlow<AsleepState> = _trackingState.asStateFlow()

    private val _userId = MutableLiveData<String?>()
    val userId: LiveData<String?> = _userId

    private val _sessionId = MutableStateFlow<String?>(null)
    val sessionId: StateFlow<String?> = _sessionId.asStateFlow()

    private val _sequence = MutableLiveData<Int>()
    val sequence: LiveData<Int> = _sequence

    private var asleepConfig: AsleepConfig? = null

    // Tracking listener
    private val trackingListener = object : Asleep.AsleepTrackingListener {
        override fun onStart(sessionId: String) {
            _sessionId.value = sessionId
            _trackingState.value = AsleepState.STATE_TRACKING_STARTED
        }

        override fun onPerform(sequence: Int) {
            _sequence.postValue(sequence)

            // Check real-time data every 10 sequences after sequence 10
            if (sequence > 10 && sequence % 10 == 0) {
                getCurrentSleepData()
            }
        }

        override fun onFinish(sessionId: String?) {
            _sessionId.value = sessionId
            _trackingState.value = AsleepState.STATE_IDLE
        }

        override fun onFail(errorCode: Int, detail: String) {
            handleError(AsleepError(errorCode, detail))
        }
    }

    fun initializeSDK(userId: String) {
        if (_trackingState.value != AsleepState.STATE_IDLE) return

        _trackingState.value = AsleepState.STATE_INITIALIZING

        Asleep.initAsleepConfig(
            context = applicationContext,
            apiKey = BuildConfig.ASLEEP_API_KEY,
            userId = userId,
            baseUrl = "https://api.asleep.ai",
            callbackUrl = null,
            service = applicationContext.getString(R.string.app_name),
            asleepConfigListener = object : Asleep.AsleepConfigListener {
                override fun onSuccess(userId: String?, config: AsleepConfig?) {
                    asleepConfig = config
                    _userId.value = userId
                    _trackingState.value = AsleepState.STATE_INITIALIZED
                }

                override fun onFail(errorCode: Int, detail: String) {
                    _trackingState.value = AsleepState.STATE_ERROR(
                        AsleepError(errorCode, detail)
                    )
                }
            }
        )
    }

    fun startTracking() {
        val config = asleepConfig ?: return
        if (_trackingState.value != AsleepState.STATE_INITIALIZED) return

        _trackingState.value = AsleepState.STATE_TRACKING_STARTING

        Asleep.beginSleepTracking(
            asleepConfig = config,
            asleepTrackingListener = trackingListener,
            notificationTitle = "Sleep Tracking",
            notificationText = "Recording your sleep",
            notificationIcon = R.drawable.ic_notification,
            notificationClass = MainActivity::class.java
        )
    }

    fun stopTracking() {
        if (Asleep.isSleepTrackingAlive(applicationContext)) {
            _trackingState.value = AsleepState.STATE_TRACKING_STOPPING
            Asleep.endSleepTracking()
        }
    }

    fun reconnectToTracking() {
        if (Asleep.isSleepTrackingAlive(applicationContext)) {
            Asleep.connectSleepTracking(trackingListener)
            _trackingState.value = AsleepState.STATE_TRACKING_STARTED
        }
    }

    private fun getCurrentSleepData() {
        Asleep.getCurrentSleepData(
            asleepSleepDataListener = object : Asleep.AsleepSleepDataListener {
                override fun onSleepDataReceived(session: Session) {
                    // Process real-time sleep data
                    val currentStage = session.sleepStages?.lastOrNull()
                    Log.d("Sleep", "Current stage: $currentStage")
                }

                override fun onFail(errorCode: Int, detail: String) {
                    Log.e("Sleep", "Failed to get data: $errorCode")
                }
            }
        )
    }

    private fun handleError(error: AsleepError) {
        if (isWarning(error.code)) {
            // Log warning but continue tracking
            Log.w("Sleep", "Warning: ${error.code} - ${error.message}")
        } else {
            // Critical error - stop tracking
            _trackingState.value = AsleepState.STATE_ERROR(error)
        }
    }
}

Complete Activity Implementation

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    private lateinit var binding: ActivityMainBinding
    private val viewModel: SleepTrackingViewModel by viewModels()
    private lateinit var permissionManager: PermissionManager

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivityMainBinding.inflate(layoutInflater)
        setContentView(binding.root)

        permissionManager = PermissionManager(this)

        setupUI()
        observeState()
        checkExistingSession()
    }

    private fun setupUI() {
        binding.btnStartStop.setOnClickListener {
            when (viewModel.trackingState.value) {
                AsleepState.STATE_INITIALIZED -> {
                    if (permissionManager.allPermissionsGranted.value == true) {
                        viewModel.startTracking()
                    } else {
                        permissionManager.requestPermissions()
                    }
                }
                AsleepState.STATE_TRACKING_STARTED -> {
                    viewModel.stopTracking()
                }
                else -> {
                    // Button disabled
                }
            }
        }
    }

    private fun observeState() {
        lifecycleScope.launch {
            viewModel.trackingState.collect { state ->
                updateUI(state)
            }
        }

        viewModel.sequence.observe(this) { sequence ->
            binding.tvSequence.text = "Sequence: $sequence"
        }
    }

    private fun updateUI(state: AsleepState) {
        when (state) {
            AsleepState.STATE_IDLE -> {
                binding.btnStartStop.isEnabled = false
                binding.btnStartStop.text = "Initializing..."
            }
            AsleepState.STATE_INITIALIZED -> {
                binding.btnStartStop.isEnabled = true
                binding.btnStartStop.text = "Start Tracking"
            }
            AsleepState.STATE_TRACKING_STARTED -> {
                binding.btnStartStop.isEnabled = true
                binding.btnStartStop.text = "Stop Tracking"
                binding.trackingIndicator.visibility = View.VISIBLE
            }
            AsleepState.STATE_TRACKING_STOPPING -> {
                binding.btnStartStop.isEnabled = false
                binding.btnStartStop.text = "Stopping..."
            }
            is AsleepState.STATE_ERROR -> {
                showErrorDialog(state.errorCode)
            }
            else -> {}
        }
    }

    private fun checkExistingSession() {
        if (Asleep.isSleepTrackingAlive(applicationContext)) {
            viewModel.reconnectToTracking()
        } else {
            viewModel.initializeSDK(getUserId())
        }
    }

    private fun getUserId(): String {
        // Load from SharedPreferences or generate
        val prefs = getSharedPreferences("asleep", MODE_PRIVATE)
        return prefs.getString("user_id", null) ?: run {
            val newId = UUID.randomUUID().toString()
            prefs.edit().putString("user_id", newId).apply()
            newId
        }
    }
}

Complete PermissionManager

class PermissionManager(private val activity: AppCompatActivity) {
    private val context = activity.applicationContext

    // Permission launchers
    private val batteryOptimizationLauncher =
        activity.registerForActivityResult(
            ActivityResultContracts.StartActivityForResult()
        ) {
            checkAndRequestNext()
        }

    private val micPermissionLauncher =
        activity.registerForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { granted ->
            if (granted) checkAndRequestNext()
            else showPermissionDeniedDialog("Microphone")
        }

    private val notificationPermissionLauncher =
        activity.registerForActivityResult(
            ActivityResultContracts.RequestPermission()
        ) { granted ->
            if (granted) checkAndRequestNext()
            else showPermissionDeniedDialog("Notifications")
        }

    private val _allPermissionsGranted = MutableLiveData(false)
    val allPermissionsGranted: LiveData<Boolean> = _allPermissionsGranted

    fun requestPermissions() {
        checkAndRequestNext()
    }

    private fun checkAndRequestNext() {
        when {
            !isBatteryOptimizationIgnored() -> requestBatteryOptimization()
            !hasMicrophonePermission() -> requestMicrophone()
            !hasNotificationPermission() -> requestNotification()
            else -> {
                _allPermissionsGranted.value = true
            }
        }
    }

    private fun requestBatteryOptimization() {
        val intent = Intent().apply {
            action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS
            data = Uri.parse("package:${context.packageName}")
        }
        batteryOptimizationLauncher.launch(intent)
    }

    private fun requestMicrophone() {
        if (ActivityCompat.shouldShowRequestPermissionRationale(
                activity,
                Manifest.permission.RECORD_AUDIO
            )) {
            showPermissionRationale(
                "Microphone access is required to record sleep sounds"
            )
        } else {
            micPermissionLauncher.launch(Manifest.permission.RECORD_AUDIO)
        }
    }

    private fun requestNotification() {
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            notificationPermissionLauncher.launch(
                Manifest.permission.POST_NOTIFICATIONS
            )
        } else {
            checkAndRequestNext()
        }
    }

    private fun isBatteryOptimizationIgnored(): Boolean {
        val pm = context.getSystemService(Context.POWER_SERVICE) as PowerManager
        return pm.isIgnoringBatteryOptimizations(context.packageName)
    }

    private fun hasMicrophonePermission(): Boolean {
        return ContextCompat.checkSelfPermission(
            context,
            Manifest.permission.RECORD_AUDIO
        ) == PackageManager.PERMISSION_GRANTED
    }

    private fun hasNotificationPermission(): Boolean {
        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
            ContextCompat.checkSelfPermission(
                context,
                Manifest.permission.POST_NOTIFICATIONS
            ) == PackageManager.PERMISSION_GRANTED
        } else {
            NotificationManagerCompat.from(context).areNotificationsEnabled()
        }
    }

    private fun showPermissionRationale(message: String) {
        AlertDialog.Builder(activity)
            .setTitle("Permission Required")
            .setMessage(message)
            .setPositiveButton("OK") { _, _ ->
                checkAndRequestNext()
            }
            .show()
    }

    private fun showPermissionDeniedDialog(permissionName: String) {
        AlertDialog.Builder(activity)
            .setTitle("Permission Denied")
            .setMessage("$permissionName permission is required for sleep tracking. Please grant it in app settings.")
            .setPositiveButton("Settings") { _, _ ->
                val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
                    data = Uri.parse("package:${context.packageName}")
                }
                activity.startActivity(intent)
            }
            .setNegativeButton("Cancel", null)
            .show()
    }
}

Minimal Production Templates

Minimal ViewModel

@HiltViewModel
class SleepTrackingViewModel @Inject constructor(
    @ApplicationContext private val app: Application
) : ViewModel() {

    private val _trackingState = MutableStateFlow<AsleepState>(AsleepState.STATE_IDLE)
    val trackingState: StateFlow<AsleepState> = _trackingState

    private var config: AsleepConfig? = null

    private val listener = object : Asleep.AsleepTrackingListener {
        override fun onStart(sessionId: String) {
            _trackingState.value = AsleepState.STATE_TRACKING_STARTED
        }
        override fun onPerform(sequence: Int) {}
        override fun onFinish(sessionId: String?) {
            _trackingState.value = AsleepState.STATE_IDLE
        }
        override fun onFail(errorCode: Int, detail: String) {
            _trackingState.value = AsleepState.STATE_ERROR(AsleepError(errorCode, detail))
        }
    }

    fun initializeSDK(userId: String) {
        Asleep.initAsleepConfig(
            context = app,
            apiKey = BuildConfig.ASLEEP_API_KEY,
            userId = userId,
            baseUrl = "https://api.asleep.ai",
            callbackUrl = null,
            service = "MyApp",
            asleepConfigListener = object : Asleep.AsleepConfigListener {
                override fun onSuccess(userId: String?, asleepConfig: AsleepConfig?) {
                    config = asleepConfig
                    _trackingState.value = AsleepState.STATE_INITIALIZED
                }
                override fun onFail(errorCode: Int, detail: String) {
                    _trackingState.value = AsleepState.STATE_ERROR(AsleepError(errorCode, detail))
                }
            }
        )
    }

    fun startTracking() {
        config?.let {
            Asleep.beginSleepTracking(
                asleepConfig = it,
                asleepTrackingListener = listener,
                notificationTitle = "Sleep Tracking",
                notificationText = "Active",
                notificationIcon = R.drawable.ic_notification,
                notificationClass = SleepActivity::class.java
            )
        }
    }

    fun stopTracking() {
        Asleep.endSleepTracking()
    }
}

Minimal Activity

@AndroidEntryPoint
class SleepActivity : AppCompatActivity() {

    private lateinit var binding: ActivitySleepBinding
    private val viewModel: SleepTrackingViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        binding = ActivitySleepBinding.inflate(layoutInflater)
        setContentView(binding.root)

        // Initialize SDK
        viewModel.initializeSDK(loadUserId())

        // Setup button
        binding.btnTrack.setOnClickListener {
            when (viewModel.trackingState.value) {
                AsleepState.STATE_INITIALIZED -> viewModel.startTracking()
                AsleepState.STATE_TRACKING_STARTED -> viewModel.stopTracking()
                else -> {}
            }
        }

        // Observe state
        lifecycleScope.launch {
            viewModel.trackingState.collect { state ->
                binding.btnTrack.text = when (state) {
                    AsleepState.STATE_INITIALIZED -> "Start"
                    AsleepState.STATE_TRACKING_STARTED -> "Stop"
                    else -> "Loading..."
                }
            }
        }
    }

    private fun loadUserId(): String {
        val prefs = getSharedPreferences("app", MODE_PRIVATE)
        return prefs.getString("user_id", null) ?: UUID.randomUUID().toString().also {
            prefs.edit().putString("user_id", it).apply()
        }
    }
}

State Management Pattern

Define states using sealed classes:

sealed class AsleepState {
    data object STATE_IDLE: AsleepState()
    data object STATE_INITIALIZING : AsleepState()
    data object STATE_INITIALIZED : AsleepState()
    data object STATE_TRACKING_STARTING : AsleepState()
    data object STATE_TRACKING_STARTED : AsleepState()
    data object STATE_TRACKING_STOPPING : AsleepState()
    data class STATE_ERROR(val errorCode: AsleepError) : AsleepState()
}

data class AsleepError(val code: Int, val message: String)

Real-Time Data Access

// In ViewModel
private var lastCheckedSequence = 0

private val trackingListener = object : Asleep.AsleepTrackingListener {
    override fun onPerform(sequence: Int) {
        _sequence.postValue(sequence)

        // Check after sequence 10, then every 10 sequences
        if (sequence > 10 && sequence - lastCheckedSequence >= 10) {
            getCurrentSleepData(sequence)
        }
    }
    // ... other callbacks
}

private fun getCurrentSleepData(sequence: Int) {
    Asleep.getCurrentSleepData(
        asleepSleepDataListener = object : Asleep.AsleepSleepDataListener {
            override fun onSleepDataReceived(session: Session) {
                lastCheckedSequence = sequence

                // Extract current data
                val currentSleepStage = session.sleepStages?.lastOrNull()
                val currentSnoringStage = session.snoringStages?.lastOrNull()

                _currentSleepStage.postValue(currentSleepStage)

                Log.d("Sleep", "Current stage: $currentSleepStage")
                Log.d("Sleep", "Snoring: $currentSnoringStage")
            }

            override fun onFail(errorCode: Int, detail: String) {
                Log.e("Sleep", "Failed to get current data: $errorCode")
            }
        }
    )
}