flutterandroid-sensorssensormanagerpedometerflutter-method-channel

Flutter: how can I permanently register a sensor (and never unregister it?)


TL;DR how can I have an Android sensor permanently running/active/registered for my app, even if I close it?

Objective:
I'm making a Flutter application that counts your steps using the pedometer package,
which uses the built-in sensor TYPE_STEP_COUNTER of Android,
which returns the # of steps taken since last boot (iOS). On Android, any steps taken before installing the app are not counted.

How I implemented it:

Important:
The TYPE_STEP_COUNTER sensor must be permanently running/stay registered in the background, even after I lock my phone, go to the home-screen, or close the app...

Observations:

Conclusion:
I need to find a way to register the TYPE_STEP_COUNTER sensor from my Flutter app, and keep it registered even after I close the app.

2 Attempted (but unsuccessful) Solutions:

1st Attempt:
Calling Native Android Code from my Flutter Code to register the sensor
This is my main.dart file (with the unimportant parts left out for simplicity):

void main() {
  WidgetsFlutterBinding.ensureInitialized();
  runApp(App());
}

class App extends StatefulWidget {
  @override
  _AppState createState() => _AppState();
}

class _AppState extends State<App> with WidgetsBindingObserver {

  @override
  void initState() {
    super.initState();

    if (Platform.isAndroid) {
      _activateStepCounterSensor();
    } else if (Platform.isIOS) {
      //TODO check if anything is needed to to here
    }
  }

  void _activateStepCounterSensor() async {
    MethodChannel _stepCounterChannel = MethodChannel('com.cedricds.wanderapp/stepCounter'); //convention
    dynamic result = await _stepCounterChannel.invokeMethod('activateStepCounterSensor');
    switch (result) {
      case "success":
        //The following line gets printed when I run the flutter app on my Samsung Galaxy S7:
        print('_activateStepCounterSensor(): successfully registered step counter sensor for android');
        break;
      case "error":
        print('_activateStepCounterSensor(): failed to register step counter sensor (not available) for android');
        //TODO display errorpage (because app is completely useless in this case)
        break;
      default:
        print('_activateStepCounterSensor(): unknown result: $result');
        break;
    }
  }

  //build() and other lifecycle-methods and helper methods: not important for this question
}

This is my MainActivity.kt file:

package com.cedricds.wanderapp

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEvent
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.util.Log
import android.widget.Toast
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

class MainActivity2: FlutterActivity(), SensorEventListener {

    private val STEP_COUNTER_CHANNEL = "com.cedricds.wanderapp/stepCounter";
    private lateinit var channel: MethodChannel

    override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
        super.configureFlutterEngine(flutterEngine)

        channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, STEP_COUNTER_CHANNEL)
        channel.setMethodCallHandler { call, result ->
            when(call.method){ //this is like switch-case statement in Java
                "activateStepCounterSensor" -> {
                    activateStepCounterSensor(result)
                }
            }
        }
    }

    private var sensorManager : SensorManager?=null
    private var sensor: Sensor ?= null

    private fun activateStepCounterSensor(result: MethodChannel.Result) {
        //This line gets printed when I run the flutter app, so the method gets called successfully:
        Log.d("Android", "Native Android: activateStepCounterSensor()")
        
        sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
        sensor = sensorManager?.getDefaultSensor(Sensor.TYPE_STEP_COUNTER)
        if (sensor == null) {
            Toast.makeText(this, "missing hardware.", Toast.LENGTH_LONG).show()
            result.error("error", "error", "error")
        } else {
            sensorManager?.registerListener(this, sensor, SensorManager.SENSOR_DELAY_NORMAL)
            //This line gets printed:
            Log.d("Android", "Native Android: registered TYPE_STEP_COUNTER")
            //and never unregister that listener
            result.success("success")
        }
    }

    override fun onSensorChanged(p0: SensorEvent?) {}
    override fun onAccuracyChanged(p0: Sensor?, p1: Int) {}
}

Despite the few print(...) and Log.d(...) being printed in the console as expected, the app doesn't work how I expected it to work. When I exit the app, walk for example 50 steps, then open the app again, those 50 steps are missing. It seems the sensor is being unregistered somewhere.

2nd Attempt:
Modifying the pedometer package's code by removing unregisterListener(...): The only changes I did to the file were 2 Log.d(...) statements and more importantly, commenting out a specific line of code.

modified SensorStreamHandler.kt from the pedometer package:

package com.example.pedometer

import android.content.Context
import android.hardware.Sensor
import android.hardware.SensorEventListener
import android.hardware.SensorManager
import android.os.Looper
import io.flutter.embedding.engine.plugins.FlutterPlugin
import io.flutter.plugin.common.BinaryMessenger
import io.flutter.plugin.common.EventChannel
import android.os.Handler
import android.util.Log

class SensorStreamHandler() : EventChannel.StreamHandler {

    private var sensorEventListener: SensorEventListener? = null
    private var sensorManager: SensorManager? = null
    private var sensor: Sensor? = null
    private lateinit var context: Context
    private lateinit var sensorName: String
    private lateinit var flutterPluginBinding: FlutterPlugin.FlutterPluginBinding

    constructor(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding, sensorType: Int) : this() {
        this.context = flutterPluginBinding.applicationContext
        this.sensorName = if (sensorType == Sensor.TYPE_STEP_COUNTER) "StepCount" else "StepDetection"
        sensorManager = context.getSystemService(Context.SENSOR_SERVICE) as SensorManager
        sensor = sensorManager!!.getDefaultSensor(sensorType)
        this.flutterPluginBinding = flutterPluginBinding
    }

    override fun onListen(arguments: Any?, events: EventChannel.EventSink?) {
        Log.d("Pedometer", "Native Android: onListen()")
        if (sensor == null) {
            events!!.error("1", "$sensorName not available",
                    "$sensorName is not available on this device");
        } else {
            sensorEventListener = sensorEventListener(events!!);
            sensorManager!!.registerListener(sensorEventListener, sensor, SensorManager.SENSOR_DELAY_NORMAL);
        }
    }

    override fun onCancel(arguments: Any?) {
        Log.d("Pedometer", "Native Android: onCancel()")
        
        //The only change I did: commenting out the following line:
//        sensorManager!!.unregisterListener(sensorEventListener);
    }

}

This also did not solve my problem. So if someone knows how I can permanently register the TYPE_STEP_COUNTER sensor in my flutter app, please let me know.


Solution

  • Update: I've contacted one of the developers of the pedometer package, and he suggested me to use flutter_foreground_service (which is developed by the same team/company as pedometer). It works.

    But I would still find it interesting, if there is another way (maybe similar to my 2 failed attempts).


    I have so many people asking me for a more detailed description of how I solved my problem. Unfortunately I can't share my whole code with you, just this small snipped which always gets run at the beginning (right after main()):

        if (Platform.isAndroid && !SharedPrefsHelper().isForegroundServiceStarted()) {
          ForegroundService().start();
          // ForegroundServiceNotification.setPriority(AndroidNotificationPriority.LOW);
          SharedPrefsHelper().setIsForegroundServiceStarted(true);
        }
    

    Don't worry about SharedPrefsHelper, that's just a class that I created to make it easier to work with shared_preferences.