Skip to content

Instantly share code, notes, and snippets.

@rubek-joshi
Created January 9, 2026 06:35
Show Gist options
  • Select an option

  • Save rubek-joshi/fae731114a091afa7f95486b2083a041 to your computer and use it in GitHub Desktop.

Select an option

Save rubek-joshi/fae731114a091afa7f95486b2083a041 to your computer and use it in GitHub Desktop.
Meltdown FLutter App Step Count Implementation (Native through Method Channel)
package com.corewalkers.meltdown
import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Build
import android.os.Bundle
import io.flutter.embedding.android.FlutterFragmentActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity : FlutterFragmentActivity(), SensorEventListener {
private val CHANNEL = "com.meltdown/step_counter"
private val PREFS_NAME = "step_counter_prefs"
private val STEP_COUNT_KEY = "last_step_count"
private val TIMESTAMP_KEY = "last_timestamp"
private var sensorManager: SensorManager? = null
private var stepCounterSensor: Sensor? = null
private var currentStepCount: Int = 0
private var methodChannel: MethodChannel? = null
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
methodChannel?.setMethodCallHandler { call, result ->
when (call.method) {
"getStepCount" -> {
val stepCount = getStepCountFromSensor()
if (stepCount != null) {
result.success(stepCount)
} else {
result.error("UNAVAILABLE", "Step counter not available", null)
}
}
"saveStepCount" -> {
val stepCount = call.argument<Int>("stepCount")
val timestamp = call.argument<Long>("timestamp")
if (stepCount != null && timestamp != null) {
saveStepCountToPrefs(stepCount, timestamp)
result.success(true)
} else {
result.error("INVALID_ARGS", "Invalid arguments", null)
}
}
"getLastStepCount" -> {
val data = getLastStepCountFromPrefs()
if (data != null) {
result.success(data)
} else {
result.success(null)
}
}
"syncStepsFromTerminated" -> {
val syncData = syncStepsFromTerminatedState()
result.success(syncData)
}
"getAndroidVersion" -> {
val version = Build.VERSION.SDK_INT
result.success(version)
}
else -> {
result.notImplemented()
}
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initializeSensorManager()
}
private fun initializeSensorManager() {
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
stepCounterSensor = sensorManager?.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
if (stepCounterSensor != null) {
sensorManager?.registerListener(this, stepCounterSensor, SensorManager.SENSOR_DELAY_NORMAL)
android.util.Log.d("StepCounter", "Step counter sensor registered successfully")
} else {
android.util.Log.w("StepCounter", "Step counter sensor not available on this device")
}
}
private fun getStepCountFromSensor(): Int? {
android.util.Log.d("StepCounter", "getStepCountFromSensor called, currentStepCount: $currentStepCount")
// Ensure sensor is registered
if (stepCounterSensor != null && sensorManager != null) {
// Re-register to trigger an immediate sensor event
sensorManager?.unregisterListener(this)
sensorManager?.registerListener(this, stepCounterSensor, SensorManager.SENSOR_DELAY_FASTEST)
android.util.Log.d("StepCounter", "Sensor re-registered to get fresh data")
}
// If we have current data from sensor, return it
if (currentStepCount > 0) {
android.util.Log.d("StepCounter", "Returning current step count: $currentStepCount")
return currentStepCount
}
// If currentStepCount is still 0, wait briefly for sensor callback
// This happens on first app launch
val maxWaitTime = 1500L // Wait up to 1.5 seconds
val startTime = System.currentTimeMillis()
val checkInterval = 50L // Check every 50ms
while (currentStepCount == 0 && (System.currentTimeMillis() - startTime) < maxWaitTime) {
try {
Thread.sleep(checkInterval)
android.util.Log.d("StepCounter", "Waiting for sensor... currentStepCount: $currentStepCount")
} catch (e: InterruptedException) {
android.util.Log.w("StepCounter", "Wait interrupted")
break
}
}
// If we got data from sensor, return it
if (currentStepCount > 0) {
android.util.Log.d("StepCounter", "Got sensor data after waiting: $currentStepCount")
return currentStepCount
}
// If still no sensor data, try to get last known value from prefs
android.util.Log.w("StepCounter", "No sensor data received, checking prefs")
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val savedCount = prefs.getInt(STEP_COUNT_KEY, -1)
if (savedCount > 0) {
android.util.Log.d("StepCounter", "Using saved count from prefs: $savedCount")
return savedCount
}
android.util.Log.e("StepCounter", "No step count available from sensor or prefs")
return null
}
private fun saveStepCountToPrefs(stepCount: Int, timestamp: Long) {
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
prefs.edit().apply {
putInt(STEP_COUNT_KEY, stepCount)
putLong(TIMESTAMP_KEY, timestamp)
apply()
}
android.util.Log.d("StepCounter", "Saved to prefs: stepCount=$stepCount, timestamp=$timestamp")
}
private fun getLastStepCountFromPrefs(): Map<String, Any>? {
val prefs = getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
val stepCount = prefs.getInt(STEP_COUNT_KEY, -1)
val timestamp = prefs.getLong(TIMESTAMP_KEY, -1L)
return if (stepCount > 0 && timestamp > 0) {
mapOf(
"stepCount" to stepCount,
"timestamp" to timestamp
)
} else {
null
}
}
/**
* Sync steps from terminated state
* Returns validated step data that was missed while app was closed
*/
private fun syncStepsFromTerminatedState(): Map<String, Any>? {
try {
android.util.Log.d("StepSync", "=== Starting syncStepsFromTerminatedState ===")
// Get current OS-level step count
val currentStepCount = getStepCountFromSensor()
android.util.Log.d("StepSync", "Current OS step count: $currentStepCount")
if (currentStepCount == null || currentStepCount <= 0) {
android.util.Log.w("StepSync", "No current step count available from sensor")
return null
}
// Get last saved step data
val lastSavedData = getLastStepCountFromPrefs()
if (lastSavedData == null) {
android.util.Log.d("StepSync", "No previous step data found - this is the first run after install")
// First time - save current state and return null (no missed steps)
val now = System.currentTimeMillis()
saveStepCountToPrefs(currentStepCount, now)
android.util.Log.d("StepSync", "Saved baseline: $currentStepCount steps at $now")
return null
}
val lastStepCount = lastSavedData["stepCount"] as Int
val lastTimestamp = lastSavedData["timestamp"] as Long
android.util.Log.d("StepSync", "Last saved step count: $lastStepCount at timestamp: $lastTimestamp")
// Calculate missed steps
val missedSteps = currentStepCount - lastStepCount
val currentTime = System.currentTimeMillis()
val elapsedTimeMs = currentTime - lastTimestamp
val elapsedMinutes = elapsedTimeMs / (1000.0 * 60.0)
android.util.Log.d("StepSync", "Calculated: $missedSteps missed steps over ${elapsedMinutes.toInt()} minutes")
// Validation 1: Check if missed steps is positive
if (missedSteps <= 0) {
android.util.Log.d("StepSync", "No new steps detected (missedSteps: $missedSteps) - possible device reboot")
// Save current state anyway
saveStepCountToPrefs(currentStepCount, currentTime)
return null
}
// Validation 2: Check if elapsed time is reasonable (not negative, not from future)
if (elapsedTimeMs < 0) {
android.util.Log.w("StepSync", "Invalid timestamp - time went backwards (elapsed: $elapsedTimeMs ms)")
saveStepCountToPrefs(currentStepCount, currentTime)
return null
}
// Validation 3: Check if missed steps is reasonable (not more than 50,000)
val MAX_REASONABLE_STEPS = 50000
if (missedSteps > MAX_REASONABLE_STEPS) {
android.util.Log.w("StepSync", "Missed steps ($missedSteps) exceeds reasonable limit ($MAX_REASONABLE_STEPS)")
// Probably a sensor reset - save current state and return null
saveStepCountToPrefs(currentStepCount, currentTime)
return null
}
// Validation 4: Check if step rate is reasonable (max 3 steps per second)
val elapsedSeconds = elapsedTimeMs / 1000.0
if (elapsedSeconds > 0) {
val stepsPerSecond = missedSteps / elapsedSeconds
android.util.Log.d("StepSync", "Step rate: ${"%.3f".format(stepsPerSecond)} steps/second")
if (stepsPerSecond > 3.0) {
android.util.Log.w("StepSync", "Step rate (${"%.3f".format(stepsPerSecond)} steps/sec) is unreasonably high (max: 3.0)")
saveStepCountToPrefs(currentStepCount, currentTime)
return null
}
}
// Validation 5: Check if elapsed time exceeds 24 hours
val MAX_ELAPSED_HOURS = 24
val elapsedHours = elapsedTimeMs / (1000.0 * 60.0 * 60.0)
if (elapsedHours > MAX_ELAPSED_HOURS) {
android.util.Log.w("StepSync", "Elapsed time (${"%.1f".format(elapsedHours)} hours) exceeds $MAX_ELAPSED_HOURS hours")
// Check if the daily average is reasonable (max 30,000 steps per day)
val avgStepsPerDay = missedSteps / (elapsedHours / 24.0)
android.util.Log.d("StepSync", "Average steps per day: ${"%.0f".format(avgStepsPerDay)}")
if (avgStepsPerDay > 100000) {
android.util.Log.w("StepSync", "Unrealistic daily average: ${"%.0f".format(avgStepsPerDay)} steps/day (max: 100,000)")
saveStepCountToPrefs(currentStepCount, currentTime)
return null
}
android.util.Log.d("StepSync", "Daily average is reasonable, allowing multi-day sync")
}
// All validations passed - return the missed steps data
android.util.Log.d("StepSync", "✓ All validations passed!")
android.util.Log.d("StepSync", "Syncing $missedSteps steps from terminated state")
android.util.Log.d("StepSync", "Time range: $lastTimestamp to $currentTime")
// Save current state
saveStepCountToPrefs(currentStepCount, currentTime)
// Return data for Flutter to write to Health Connect
return mapOf(
"missedSteps" to missedSteps,
"startTime" to lastTimestamp,
"endTime" to currentTime
)
} catch (e: Exception) {
android.util.Log.e("StepSync", "Error syncing steps: ${e.message}", e)
return null
}
}
override fun onSensorChanged(event: SensorEvent?) {
if (event?.sensor?.type == Sensor.TYPE_STEP_COUNTER) {
// TYPE_STEP_COUNTER returns the total steps since last reboot
val newCount = event.values[0].toInt()
android.util.Log.d("StepCounter", "onSensorChanged: Sensor reported $newCount steps (previous: $currentStepCount)")
currentStepCount = newCount
}
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
// Not needed for step counter
}
override fun onResume() {
super.onResume()
stepCounterSensor?.also { sensor ->
sensorManager?.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL)
android.util.Log.d("StepCounter", "Sensor re-registered in onResume")
}
}
override fun onPause() {
super.onPause()
// Save current state when app goes to background
if (currentStepCount > 0) {
saveStepCountToPrefs(currentStepCount, System.currentTimeMillis())
}
sensorManager?.unregisterListener(this)
android.util.Log.d("StepCounter", "Sensor unregistered in onPause")
}
override fun onDestroy() {
super.onDestroy()
// Save current state before app is destroyed
if (currentStepCount > 0) {
saveStepCountToPrefs(currentStepCount, System.currentTimeMillis())
}
sensorManager?.unregisterListener(this)
methodChannel?.setMethodCallHandler(null)
android.util.Log.d("StepCounter", "Sensor unregistered in onDestroy")
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment