# Android Architecture Patterns for Asleep SDK This document contains key architecture patterns from the Asleep Android sample app. ## 1. Application Setup with Hilt ```kotlin @HiltAndroidApp class SampleApplication : Application() { companion object { private lateinit var instance: SampleApplication val ACTION_AUTO_TRACKING: String by lazy { instance.packageName + ".ACTION_AUTO_TRACKING" } } override fun onCreate() { super.onCreate() instance = this } } ``` ## 2. State Management with Sealed Classes ```kotlin 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() } ``` ## 3. ViewModel with Asleep Integration ```kotlin @HiltViewModel class AsleepViewModel @Inject constructor( @ApplicationContext private val applicationContext: Application ) : ViewModel() { // State management private var _asleepState = MutableStateFlow(AsleepState.STATE_IDLE) val asleepState: StateFlow get() = _asleepState // User and session data private var _asleepUserId = MutableLiveData(null) val asleepUserId: LiveData get() = _asleepUserId private var _sessionId = MutableStateFlow(null) val sessionId: StateFlow get() = _sessionId // Initialize SDK fun initAsleepConfig() { if (_asleepState.value != AsleepState.STATE_IDLE) return _asleepState.value = AsleepState.STATE_INITIALIZING val storedUserId = PreferenceHelper.getAsleepUserId(applicationContext) Asleep.initAsleepConfig( context = applicationContext, apiKey = Constants.ASLEEP_API_KEY, userId = storedUserId, baseUrl = Constants.BASE_URL, callbackUrl = Constants.CALLBACK_URL, service = Constants.SERVICE_NAME, asleepConfigListener = object : Asleep.AsleepConfigListener { override fun onFail(errorCode: Int, detail: String) { _asleepErrorCode.value = AsleepError(errorCode, detail) _asleepState.value = AsleepState.STATE_ERROR(AsleepError(errorCode, detail)) } override fun onSuccess(userId: String?, asleepConfig: AsleepConfig?) { _asleepConfig.value = asleepConfig _asleepUserId.value = userId userId?.let { PreferenceHelper.putAsleepUserId(applicationContext, it) } _asleepState.value = AsleepState.STATE_INITIALIZED } } ) } // Tracking lifecycle private val asleepTrackingListener = object: Asleep.AsleepTrackingListener { override fun onStart(sessionId: String) { _asleepState.value = AsleepState.STATE_TRACKING_STARTED } override fun onPerform(sequence: Int) { _sequence.postValue(sequence) if (sequence > 10 && (sequence % 10 == 1 || sequence - (_analyzedSeq ?: 0) > 10)) { getCurrentSleepData(sequence) } } override fun onFinish(sessionId: String?) { _sessionId.value = sessionId if (asleepState.value is AsleepState.STATE_ERROR) { // Exit due to Error } else { // Successful Finish _asleepState.value = AsleepState.STATE_IDLE } } override fun onFail(errorCode: Int, detail: String) { handleErrorOrWarning(AsleepError(errorCode, detail)) } } fun beginSleepTracking() { if (_asleepState.value == AsleepState.STATE_INITIALIZED) { _asleepState.value = AsleepState.STATE_TRACKING_STARTING _asleepConfig.value?.let { Asleep.beginSleepTracking( asleepConfig = it, asleepTrackingListener = asleepTrackingListener, notificationTitle = applicationContext.getString(R.string.app_name), notificationText = "", notificationIcon = R.mipmap.ic_app, notificationClass = MainActivity::class.java ) } PreferenceHelper.saveStartTrackingTime(applicationContext, System.currentTimeMillis()) } } fun endSleepTracking() { if (Asleep.isSleepTrackingAlive(applicationContext)) { _asleepState.value = AsleepState.STATE_TRACKING_STOPPING Asleep.endSleepTracking() } } fun connectSleepTracking() { Asleep.connectSleepTracking(asleepTrackingListener) _asleepUserId.value = PreferenceHelper.getAsleepUserId(applicationContext) _asleepState.value = AsleepState.STATE_TRACKING_STARTED } // Error handling fun handleErrorOrWarning(asleepError: AsleepError) { val code = asleepError.code val message = asleepError.message if (isWarning(code)) { // Log warning, continue tracking _warningMessage.postValue("$existingMessage\n${getCurrentTime()} $code - $message") } else { // Critical error, stop tracking _asleepErrorCode.postValue(asleepError) _asleepState.value = AsleepState.STATE_ERROR(asleepError) } } } ``` ## 4. Activity State Handling ```kotlin @AndroidEntryPoint class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding private lateinit var permissionManager: PermissionManager private val asleepViewModel: AsleepViewModel by viewModels() override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityMainBinding.inflate(layoutInflater) setContentView(binding.root) permissionManager = PermissionManager(this) setPermissionObserver() permissionManager.checkAllPermissions() // State management with coroutines lifecycleScope.launch { asleepViewModel.asleepState.collect { state -> when (state) { AsleepState.STATE_IDLE -> { checkRunningService() } AsleepState.STATE_INITIALIZING -> { binding.btnControlTracking.text = "No user id" binding.btnControlTracking.isEnabled = false } AsleepState.STATE_INITIALIZED -> { binding.btnControlTracking.apply { isEnabled = true text = getString(R.string.button_text_start_tracking) setOnClickListener { if (permissionManager.allPermissionsGranted.value == true) { asleepViewModel.beginSleepTracking() } else { permissionManager.checkAndRequestPermissions() } } } } AsleepState.STATE_TRACKING_STARTED -> { binding.btnControlTracking.apply { isEnabled = true text = getString(R.string.button_text_stop_tracking) setOnClickListener { if (asleepViewModel.isEnoughTrackingTime()) { asleepViewModel.endSleepTracking() } else { showInsufficientTimeDialog() } } } } is AsleepState.STATE_ERROR -> { binding.btnControlTracking.isEnabled = false showErrorDialog(supportFragmentManager) } } } } } private fun checkRunningService() { val isRunningService = Asleep.isSleepTrackingAlive(applicationContext) if (isRunningService) { asleepViewModel.connectSleepTracking() } else { asleepViewModel.initAsleepConfig() } } } ``` ## 5. Permission Management ```kotlin class PermissionManager(private val activity: AppCompatActivity) { private val context = activity.applicationContext private val batteryOptimizationLauncher = activity.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { checkAndRequestPermissions() } private val micPermissionLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { checkAndRequestPermissions() } private val notificationPermissionLauncher = activity.registerForActivityResult(ActivityResultContracts.RequestPermission()) { checkAndRequestPermissions() } private val _allPermissionsGranted = MutableLiveData(false) val allPermissionsGranted: LiveData = _allPermissionsGranted fun checkAndRequestPermissions() { when { !isBatteryOptimizationIgnored() -> { val intent = Intent().apply { action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS data = Uri.parse("package:${context.packageName}") } batteryOptimizationLauncher.launch(intent) } context.checkSelfPermission(android.Manifest.permission.RECORD_AUDIO) != android.content.pm.PackageManager.PERMISSION_GRANTED -> { if (shouldShowRequestPermissionRationale(activity, android.Manifest.permission.RECORD_AUDIO)) { showPermissionDialog() } else { micPermissionLauncher.launch(android.Manifest.permission.RECORD_AUDIO) } } hasNotificationPermission().not() -> { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU || shouldShowRequestPermissionRationale(activity, android.Manifest.permission.POST_NOTIFICATIONS)) { showPermissionDialog() } else { notificationPermissionLauncher.launch( android.Manifest.permission.POST_NOTIFICATIONS) } } else -> { checkAllPermissions() } } } fun checkAllPermissions() { _batteryOptimized.value = isBatteryOptimizationIgnored() _micPermission.value = context.checkSelfPermission( android.Manifest.permission.RECORD_AUDIO ) == android.content.pm.PackageManager.PERMISSION_GRANTED _notificationPermission.value = hasNotificationPermission() _allPermissionsGranted.value = (_batteryOptimized.value == true) && (_micPermission.value == true) && (_notificationPermission.value == true) } private fun isBatteryOptimizationIgnored(): Boolean { val powerManager = context.getSystemService(Context.POWER_SERVICE) as PowerManager val packageName = context.packageName return powerManager.isIgnoringBatteryOptimizations(packageName) } private fun hasNotificationPermission(): Boolean { return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { context.checkSelfPermission(android.Manifest.permission.POST_NOTIFICATIONS) == android.content.pm.PackageManager.PERMISSION_GRANTED } else { NotificationManagerCompat.from(context).areNotificationsEnabled() } } } ``` ## 6. Error Handling Utilities ```kotlin data class AsleepError(val code: Int, val message: String) internal fun isWarning(errorCode: Int): Boolean { return errorCode in setOf( AsleepErrorCode.ERR_AUDIO_SILENCED, AsleepErrorCode.ERR_AUDIO_UNSILENCED, AsleepErrorCode.ERR_UPLOAD_FAILED, ) } internal fun getDebugMessage(errorCode: AsleepError): String { if (isNetworkError(errorCode.message)) { return "Please check your network connection." } if (isMethodFormatInvalid(errorCode.message)) { return "Please check the method format, including argument values and types." } return when (errorCode.code) { AsleepErrorCode.ERR_MIC_PERMISSION -> "The app does not have microphone access permission." AsleepErrorCode.ERR_AUDIO -> "Another app is using the microphone, or there is an issue with the microphone settings." AsleepErrorCode.ERR_INVALID_URL -> "Please check the URL format." AsleepErrorCode.ERR_COMMON_EXPIRED -> "The API rate limit has been exceeded, or the plan has expired." AsleepErrorCode.ERR_UPLOAD_FORBIDDEN -> "initAsleepConfig() was performed elsewhere with the same ID during the tracking." AsleepErrorCode.ERR_UPLOAD_NOT_FOUND, AsleepErrorCode.ERR_CLOSE_NOT_FOUND -> "The session has already ended." else -> "" } } ```