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

17 KiB

name, description
name description
sleeptrack-android This skill helps Android developers integrate the Asleep SDK for sleep tracking functionality. Use this skill when developers ask about Android implementation, MVVM architecture patterns, permission handling, Jetpack Compose UI, Kotlin coroutines integration, or Android-specific sleep tracking features. This skill provides working code examples from the official sample app.

Sleeptrack Android

Overview

This skill provides comprehensive guidance for integrating the Asleep sleep tracking SDK into Android applications. It covers Android-specific implementation details including MVVM architecture, permission handling, state management, UI patterns, and lifecycle management.

Use this skill when developers need to:

  • Set up Asleep SDK in Android projects
  • Implement sleep tracking with proper Android architecture
  • Handle Android-specific permissions (microphone, notifications, battery optimization)
  • Manage tracking lifecycle with state machines
  • Build UI with ViewBinding or Jetpack Compose
  • Handle errors and edge cases in Android environment
  • Implement foreground services for background tracking

Prerequisites: Review the sleeptrack-foundation skill first for core concepts including session lifecycle, error codes, and API fundamentals.

Quick Start

1. Add Dependencies

Add to your app-level build.gradle:

dependencies {
    // Core dependencies
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.appcompat:appcompat:1.6.1'
    implementation 'androidx.lifecycle:lifecycle-service:2.7.0'

    // Required for Asleep SDK
    implementation 'com.squareup.okhttp3:okhttp:4.11.0'
    implementation 'com.google.code.gson:gson:2.10'
    implementation 'ai.asleep:asleepsdk:3.1.4'

    // Optional: Hilt for DI
    implementation "com.google.dagger:hilt-android:2.48"
    kapt "com.google.dagger:hilt-compiler:2.48"
}

Minimum Requirements:

  • minSdk: 24 (Android 7.0)
  • targetSdk: 34
  • Java: 17
  • Kotlin: 1.9.24+

See references/gradle_setup.md for complete Gradle configuration.

2. Configure Permissions

Add to AndroidManifest.xml:

<manifest>
    <!-- Essential permissions -->
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE" />

    <!-- Battery optimization -->
    <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />

    <!-- Notifications (Android 13+) -->
    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
</manifest>

3. Initialize SDK

Asleep.initAsleepConfig(
    context = applicationContext,
    apiKey = "your_api_key_here",
    userId = "unique_user_id",
    baseUrl = "https://api.asleep.ai",
    callbackUrl = null, // Optional webhook URL
    service = "YourAppName",
    asleepConfigListener = object : Asleep.AsleepConfigListener {
        override fun onSuccess(userId: String?, asleepConfig: AsleepConfig?) {
            // SDK initialized successfully
        }

        override fun onFail(errorCode: Int, detail: String) {
            Log.e("Asleep", "Init failed: $errorCode - $detail")
        }
    }
)

4. Start Sleep Tracking

Asleep.beginSleepTracking(
    asleepConfig = asleepConfig,
    asleepTrackingListener = object : Asleep.AsleepTrackingListener {
        override fun onStart(sessionId: String) {
            // Tracking started successfully
        }

        override fun onPerform(sequence: Int) {
            // Called every ~30 seconds (1 sequence)
        }

        override fun onFinish(sessionId: String?) {
            // Tracking completed
        }

        override fun onFail(errorCode: Int, detail: String) {
            // Handle tracking errors
        }
    },
    notificationTitle = "Sleep Tracking Active",
    notificationText = "Tap to return to app",
    notificationIcon = R.drawable.ic_notification,
    notificationClass = MainActivity::class.java
)

5. Stop Tracking

Asleep.endSleepTracking()

Android Architecture Pattern (MVVM + Hilt)

The recommended architecture follows Android best practices with MVVM pattern, Hilt dependency injection, and proper state management.

State Management

Define tracking 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)

Basic ViewModel Pattern

@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

    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 = trackingListener,
                notificationTitle = "Sleep Tracking",
                notificationText = "Active",
                notificationIcon = R.drawable.ic_notification,
                notificationClass = MainActivity::class.java
            )
        }
    }

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

For complete production-ready ViewModel with real-time data, error handling, and lifecycle management, see references/complete_viewmodel_implementation.md.

Basic Activity Pattern

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

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

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

        // 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 ->
                updateUI(state)
            }
        }
    }
}

For complete Activity implementation with permission handling, see references/complete_viewmodel_implementation.md.

Permission Handling

Android requires multiple runtime permissions for sleep tracking:

Required Permissions

  1. RECORD_AUDIO: Microphone access for sleep sound recording
  2. POST_NOTIFICATIONS: Android 13+ notification permission
  3. Battery Optimization: Prevent app from being killed during tracking

Permission Request Flow

// Request permissions sequentially
when {
    !hasMicrophonePermission() -> requestMicrophone()
    !hasNotificationPermission() -> requestNotification()
    !isBatteryOptimizationIgnored() -> requestBatteryOptimization()
    else -> allPermissionsGranted()
}

Best Practices

  1. Request in sequence: Request one permission at a time for better UX
  2. Show rationale: Explain why each permission is needed before requesting
  3. Handle denial: Provide fallback or guide users to settings
  4. Check on resume: Re-check permissions when app resumes
override fun onResume() {
    super.onResume()
    if (!hasRequiredPermissions() && Asleep.isSleepTrackingAlive(applicationContext)) {
        handlePermissionLoss()
    }
}

For complete PermissionManager implementation, see references/complete_viewmodel_implementation.md.

Error Handling

Distinguish between critical errors and warnings:

Error Classification

fun isWarning(errorCode: Int): Boolean {
    return errorCode in setOf(
        AsleepErrorCode.ERR_AUDIO_SILENCED,
        AsleepErrorCode.ERR_UPLOAD_FAILED
    )
}

fun handleError(error: AsleepError) {
    if (isWarning(error.code)) {
        // Show warning, continue tracking
        Toast.makeText(context, getUserFriendlyMessage(error), Toast.LENGTH_SHORT).show()
    } else {
        // Critical error - stop tracking
        stopTracking()
        showErrorDialog(getUserFriendlyMessage(error))
    }
}

UI Patterns

ViewBinding Example

class TrackingFragment : Fragment() {
    private var _binding: FragmentTrackingBinding? = null
    private val binding get() = _binding!!
    private val viewModel: SleepTrackingViewModel by viewModels()

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

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

    override fun onDestroyView() {
        super.onDestroyView()
        _binding = null
    }
}

Jetpack Compose Example

@Composable
fun SleepTrackingScreen(
    viewModel: SleepTrackingViewModel = hiltViewModel()
) {
    val trackingState by viewModel.trackingState.collectAsState()

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        when (trackingState) {
            AsleepState.STATE_TRACKING_STARTED -> {
                Button(onClick = { viewModel.stopTracking() }) {
                    Text("Stop Tracking")
                }
            }
            AsleepState.STATE_INITIALIZED -> {
                Button(onClick = { viewModel.startTracking() }) {
                    Text("Start Sleep Tracking")
                }
            }
            is AsleepState.STATE_ERROR -> {
                ErrorDisplay(error = (trackingState as AsleepState.STATE_ERROR).errorCode)
            }
            else -> {
                CircularProgressIndicator()
            }
        }
    }
}

For complete UI implementations with Material3 components, see references/ui_implementation_guide.md.

Lifecycle Management

Handle App Lifecycle Events

@AndroidEntryPoint
class MainActivity : AppCompatActivity() {

    override fun onStart() {
        super.onStart()
        // Reconnect to existing tracking session
        if (Asleep.isSleepTrackingAlive(applicationContext)) {
            viewModel.reconnectToTracking()
        }
    }

    override fun onStop() {
        super.onStop()
        // Tracking continues in background via foreground service
        // No action needed
    }

    override fun onDestroy() {
        super.onDestroy()
        // Do NOT call endSleepTracking() here
        // Service continues in background
    }
}

Foreground Service

The Asleep SDK automatically manages a foreground service during tracking. The notification keeps the service alive:

Asleep.beginSleepTracking(
    asleepConfig = config,
    asleepTrackingListener = listener,
    notificationTitle = "Sleep Tracking Active",
    notificationText = "Tracking your sleep patterns",
    notificationIcon = R.drawable.ic_sleep,
    notificationClass = MainActivity::class.java  // Tapping notification opens this
)

The service will:

  • Keep the app alive during sleep tracking
  • Show persistent notification
  • Maintain microphone access
  • Continue even if user swipes away the app

Real-Time Sleep Data

Access preliminary sleep data during tracking:

// In ViewModel
override fun onPerform(sequence: Int) {
    // Check after sequence 10, then every 10 sequences
    if (sequence > 10 && sequence % 10 == 0) {
        getCurrentSleepData()
    }
}

private fun getCurrentSleepData() {
    Asleep.getCurrentSleepData(
        asleepSleepDataListener = object : Asleep.AsleepSleepDataListener {
            override fun onSleepDataReceived(session: Session) {
                val currentSleepStage = session.sleepStages?.lastOrNull()
                val currentSnoringStage = session.snoringStages?.lastOrNull()

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

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

Note: Real-time data is preliminary and may differ from final report after processing.

Testing

Unit Testing

class SleepTrackingViewModelTest {
    @get:Rule val mainDispatcherRule = MainDispatcherRule()

    @Test
    fun `startTracking should fail if not initialized`() = runTest {
        viewModel.startTracking()
        assertNotEquals(AsleepState.STATE_TRACKING_STARTED, viewModel.trackingState.value)
    }
}

Integration Testing

@Test
fun trackingFlow_complete() {
    onView(withId(R.id.btn_start_stop)).perform(click())
    onView(withId(R.id.tracking_indicator)).check(matches(isDisplayed()))
}

For complete testing guide with Compose UI tests and test utilities, see references/testing_guide.md.

Common Issues & Solutions

Tracking stops unexpectedly

Causes: Battery optimization, notification dismissed, permission revoked, microphone conflict

Solution: Check battery optimization and permissions on resume:

override fun onResume() {
    super.onResume()
    if (!hasRequiredPermissions() && Asleep.isSleepTrackingAlive(applicationContext)) {
        handlePermissionLoss()
    }
}

No real-time data available

Cause: Checking before sequence 10

Solution: Only call getCurrentSleepData() after sequence 10

ERR_UPLOAD_FORBIDDEN error

Cause: Same user_id tracking on multiple devices

Solution: Use unique user IDs per device or check for active sessions before starting

Resources

This skill includes detailed reference documentation:

  • references/complete_viewmodel_implementation.md: Complete ViewModel, Activity, and PermissionManager implementations
  • references/ui_implementation_guide.md: Complete ViewBinding and Jetpack Compose UI examples
  • references/testing_guide.md: Comprehensive unit, integration, and UI testing guides
  • references/android_architecture_patterns.md: Complete architecture examples from the official sample app
  • references/gradle_setup.md: Comprehensive Gradle configuration including dependencies and ProGuard rules

Official Documentation

Android Resources

Next Steps

After implementing Android sleep tracking:

  1. Test thoroughly: Test on different Android versions (especially 13+ for notifications)
  2. Handle edge cases: Low battery, airplane mode, app updates during tracking
  3. Fetch reports: Use REST API or backend integration to retrieve sleep reports
  4. Build UI: Create compelling visualizations of sleep data
  5. Analytics: Track user engagement and sleep patterns

For backend report fetching and webhook integration, use the sleeptrack-be skill.