Created
January 9, 2026 06:35
-
-
Save rubek-joshi/fae731114a091afa7f95486b2083a041 to your computer and use it in GitHub Desktop.
Meltdown FLutter App Step Count Implementation (Native through Method Channel)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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