transistorsoft / flutter_background_fetch

Periodic callbacks in the background for both IOS and Android. Includes Android Headless mechanism
MIT License
571 stars 166 forks source link

on background_fetch, save pedometer data to sqlite #102

Closed smallstepman closed 3 years ago

smallstepman commented 4 years ago

Hi, first of all, thank you for your work! This probably isn't about the issue with your package. I'm trying to build an Android Flutter app that will collect pedometer data. I'm at the beginning of my journey with Flutter so I wanted to use as many ready-to-use components as possible, therefore I picked this package https://pub.dev/packages/pedometer for getting data about the steps. Everything works beside one thing: I can't collect data while app is not running. I'm trying to use background_fetch to hopefully catch the amount of steps provided by this package, no matter if app is running or not, but without the luck. I'm also not the first one trying to do it https://github.com/cph-cachet/flutter-plugins/issues/60, other people tried to do it with workmanager and alarm manager but also couldn't make it work. Given your amount of experience with background processing and data from sensors on both flutter and android, I figured you might be the right person to ask for help.

Here are some details if you have time for it: Pedometer package implementation on flutter side: https://github.com/cph-cachet/flutter-plugins/blob/master/packages/pedometer/lib/pedometer.dart and on Native side: https://github.com/cph-cachet/flutter-plugins/blob/master/packages/pedometer/android/src/main/java/cachet/plugins/pedometer/PedometerPlugin.java and my code below. Please note that I'm using Mobx in my app to manage state. Mobx uses concept of Stores. My store that handles state of pedometer is called Steps. For that store Steps, there is a bunch of generated code in steps.g.part file. This code was generated by issuing a command flutter packages pub run build_runner watch. I've tried to provide you with minimal working code, but I couldn't figure out how to make it work without Mobx, as I failed at the stage of making pedometer example app run - I'm unsure what is the issue. If that Mobx code makes it impossible for you to debunk this, please, let me know and I'll do my best to provide you with minimal setup/clonable repo Your Environment

Code

main.dart

import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:background_fetch/background_fetch.dart';

import 'package:app/database.dart' show LocalDatabase;
import 'package:app/model.dart' show Steps;

void backgroundFetchHeadlessTask(String taskId) async {
  // print("[BackgroundFetch] Headless event received: $taskId");
  LocalDatabase().addSteps(40);
  final Steps steps = Steps();
  steps.startListening();
  await new Future.delayed(const Duration(seconds : 5));  
  LocalDatabase().addSteps(steps.steps);
  BackgroundFetch.finish(taskId);
}

void main() {
  runApp(new MyApp());

  BackgroundFetch.registerHeadlessTask(backgroundFetchHeadlessTask);
}

class MyApp extends StatefulWidget {
  @override
  _MyAppState createState() => new _MyAppState();
}

class _MyAppState extends State<MyApp> {
  List dbSteps = [];

  @override
  void initState() {
    super.initState();
    final Steps steps = Steps();
    steps.startListening();

    LocalDatabase().initDB();
    LocalDatabase().addSteps(2);
    LocalDatabase().getSteps().then(
      (v) => print(v)
    );
    LocalDatabase().addSteps(steps.steps);
    print(steps.steps);
    initPlatformState();
  }

  Future<void> initPlatformState() async {
    BackgroundFetch.configure(BackgroundFetchConfig(
        minimumFetchInterval: 15,
        forceAlarmManager: false,
        stopOnTerminate: false,
        startOnBoot: true,
        enableHeadless: true,
        requiresBatteryNotLow: false,
        requiresCharging: false,
        requiresStorageNotLow: false,
        requiresDeviceIdle: false,
        requiredNetworkType: NetworkType.NONE,
    ), _onBackgroundFetch).then((int status) {
      print('[BackgroundFetch] configure success: $status');
      setState(() {
      });

    }).catchError((e) {
      print('[BackgroundFetch] configure ERROR: $e');
      setState(() {
      });
    });
  }
  void _onBackgroundFetch(String taskId) async {
    print("[BackgroundFetch] Event received: $taskId");
    final Steps steps = Steps();
    LocalDatabase().addSteps(20);
    steps.startListening();
    await new Future.delayed(const Duration(seconds : 5));  
    LocalDatabase().addSteps(steps.steps);
    BackgroundFetch.scheduleTask(TaskConfig(
      taskId: "com.transistorsoft.customtask",
      delay: 5000,
      periodic: true,
      forceAlarmManager: false,
      stopOnTerminate: false,
      enableHeadless: true,
      startOnBoot: true
    ));
    BackgroundFetch.finish(taskId);
  }

  @override
  Widget build(BuildContext context) {

    LocalDatabase().getSteps().then((v) => setState(() => dbSteps = v));

    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(
          title: const Text('BackgroundFetch with Pedometer', style: TextStyle(color: Colors.black)),
          backgroundColor: Colors.amberAccent,
          brightness: Brightness.light,
        ),
        body: dbSteps.isEmpty ? Container() : Container(
          child: ListView.builder(
              itemCount: dbSteps.length,
              itemBuilder: (BuildContext context, int index) {
                return Text(dbSteps[index].toString());
              }
          ),
        ),
      ),
    );
  }
}

database.dart

import 'package:path/path.dart';
import 'package:sqflite/sqflite.dart';

import 'dart:async';

class LocalDatabase {
  static final LocalDatabase _instance = LocalDatabase._internal();

  factory LocalDatabase() => _instance;

  static Database _db;

  Future<Database> get db async {
    if (_db != null) {
      return _db;
    } 
    _db = await initDB();
    return _db;
  }

  LocalDatabase._internal();

  Future<Database> initDB() async {
    String dbDirectory = await getDatabasesPath();
    String dbPath = join(dbDirectory, 'main.db');
    var theDb = await openDatabase(dbPath, version: 1, onCreate: _onCreate);
    return theDb;
  }

  void _onCreate(Database db, int version) async {
    await db.execute('''
      CREATE TABLE Steps
      (id STRING PRIMARY KEY, 
        datetime DATETIME,
        steps INT 
      )
      '''
    );
  }

  Future<int> addSteps(int steps) async {
    var dbClient = await db;
    try {
      int res = await dbClient.insert("Steps", 
        { "datetime":DateTime.now().toString(), "steps": steps }
      );
      // print("steps added $res");
      // dbClient.query('Steps').then((v) => print('last is: ${v.last}')); 
      return res;
    } catch (e) {
      print(e);
    }
  }

  Future<List<Map>> getSteps() async {
    var dbClient = await db;
    List<Map> res = await dbClient.query("Steps");
    if (res.length == 0) return null;
    return res;
  }

  Future closeDb() async {
    var dbClient = await db;
    dbClient.close();
  }
}

steps.dart

import 'dart:async';
import 'package:app/database.dart';
import 'package:pedometer/pedometer.dart';
import 'package:mobx/mobx.dart';

part 'steps.g.dart';

class Steps = StepsBase with _$Steps;

abstract class StepsBase with Store {

  @observable
  int steps;

  Pedometer _pedometer;
  StreamSubscription<int> _subscription;
  Function _onError, _onDone;

  @action
  void startListening() {
    _pedometer = Pedometer();
    _subscription = _pedometer.pedometerStream.listen(
      _onData,
      onError: _onError, 
      onDone: _onDone, 
      cancelOnError: true
    );
  }

  @action
  void stopListening() {
    _subscription.cancel();
  }

  void _onData(int stepCountValue) async {
    LocalDatabase().addSteps(steps);
    steps = stepCountValue;
  }

}

steps.g.dart

// GENERATED CODE - DO NOT MODIFY BY HAND

part of 'steps.dart';

// **************************************************************************
// StoreGenerator
// **************************************************************************

// ignore_for_file: non_constant_identifier_names, unnecessary_lambdas, prefer_expression_function_bodies, lines_longer_than_80_chars, avoid_as, avoid_annotating_with_dynamic

mixin _$Steps on StepsBase, Store {
  final _$stepsAtom = Atom(name: 'StepsBase.steps');

  @override
  int get steps {
    _$stepsAtom.context.enforceReadPolicy(_$stepsAtom);
    _$stepsAtom.reportObserved();
    return super.steps;
  }

  @override
  set steps(int value) {
    _$stepsAtom.context.conditionallyRunInAction(() {
      super.steps = value;
      _$stepsAtom.reportChanged();
    }, _$stepsAtom, name: '${_$stepsAtom.name}_set');
  }

  final _$StepsBaseActionController = ActionController(name: 'StepsBase');

  @override
  void startListening() {
    final _$actionInfo = _$StepsBaseActionController.startAction();
    try {
      return super.startListening();
    } finally {
      _$StepsBaseActionController.endAction(_$actionInfo);
    }
  }

  @override
  void stopListening() {
    final _$actionInfo = _$StepsBaseActionController.startAction();
    try {
      return super.stopListening();
    } finally {
      _$StepsBaseActionController.endAction(_$actionInfo);
    }
  }

  @override
  String toString() {
    final string = 'steps: ${steps.toString()}';
    return '{$string}';
  }
}

pubspec.yaml

name: app
description: A new Flutter project.
version: 1.0.0+1

environment:
  sdk: ">=2.6.0 <3.0.0"

dependencies:
  flutter:
    sdk: flutter

  flutter_localizations:
    sdk: flutter

  mobx: ^1.0.2
  flutter_mobx: ^1.0.1
  cupertino_icons: ^0.1.2
  pedometer: ^1.0.0
  sqflite:
  background_fetch: ^0.5.1

dev_dependencies:
  flutter_test:
    sdk: flutter
  build_runner: ^1.7.4
  mobx_codegen: ^1.0.2

flutter:
  uses-material-design: true

assets:

Debug logs Here is the screenshot of the app. You can differentiate records into 4 groups

  1. values continuously increment - this data is generated while app was running in foreground or background and I was shaking by phone to generate 'steps'
  2. value=2 and next value=null - generated while opening the app in _MyAppState.initState()
  3. value=20 and next value=null - called from _onBackgroundFetch
  4. value=40 and next value=null - called from backgroundFetchHeadlessTask

    Above shows that your plugin is working perfectly, but I'm unable to get my data. I'd love to know how to get some steps data in backgroundFetch and HeadlessTask, instead of null. Is there something wrong with the way I'm trying to do this? Or the implementation of pedometer package makes it unable to work without application context, and if so, how would one approach fixing it?

stale[bot] commented 3 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. You may also mark this issue as a "discussion" and I will leave this open.

Igitt-Igitt commented 3 years ago

I have the same problem. Is there a workaround?

yamachaso commented 3 years ago

I also have the same problem... need help!!

Igitt-Igitt commented 3 years ago

You can use a foreground service. This works. But on some devices, for https://pub.dev/packages/pedometer an additional app such as Health or Google Fit is required :-(

Is there a solution for this?

FirdousNath commented 3 years ago

Did someone find any solution ?

mquadrant commented 3 years ago

I am facing this issue too. Is there any solution for this?

FirdousNath commented 3 years ago

@christocracy attention needed!

christocracy commented 3 years ago

Provide a simple test-case App that demonstrates your problem, in a public github repo.

darshansatra1 commented 3 years ago

You guys can use flutter_foreground_service_plugin for getting the pedometer steps. I was stuck in this problem for days but then I tried foreground service and it worked.

FirdousNath commented 3 years ago

You guys can use flutter_foreground_service_plugin for getting the pedometer steps. I was stuck in this problem for days but then I tried foreground service and it worked.

can you share your solution ?

stale[bot] commented 3 years ago

This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions. You may also mark this issue as a "discussion" and I will leave this open.

stale[bot] commented 3 years ago

Closing this issue after a prolonged period of inactivity. Fell free to reopen this issue, if this still affecting you.

yasirugrak commented 2 years ago

I need help with the same issue. I want my pedometer app to run in the background. It should continue to count even when the application is covered.

christocracy commented 2 years ago

I want my pedometer app to run in the background.

That's not what background_fetch does. It runs a periodic callback (fastest is once every 15 min). When the callback runs, it has 30s of background time before your app is suspended until the next event.

Anyway, pedometer api is always recording. You can query the api with a start/end datetime to receive the pedometer data the OS stored during that time.

yasirugrak commented 2 years ago

I want my pedometer app to run in the background.

That's not what background_fetch does. It runs a periodic callback (fastest is once every 15 min). When the callback runs, it has 30s of background time before your app is suspended until the next event.

Anyway, pedometer api is always recording. You can query the api with a start/end datetime to receive the pedometer data the OS stored during that time.

Thank you for your answer. I want to show the steps taken with the non-deletable notification to the user. How can I do it.

christocracy commented 2 years ago

How can I do it.

I have no idea (I've never used pedometer api) but background_fetch is not going to help with that.

Igitt-Igitt commented 2 years ago

I want my pedometer app to run in the background.

That's not what background_fetch does. It runs a periodic callback (fastest is once every 15 min). When the callback runs, it has 30s of background time before your app is suspended until the next event.

Anyway, pedometer api is always recording. You can query the api with a start/end datetime to receive the pedometer data the OS stored during that time.

Where can I find the API where I can query the pedometer data with a start/end date? The pedometer package only provides events as far as I know.

christocracy commented 2 years ago

Where can I find the API where I can query the pedometer data

I don't know about Android, but the iOS CMPedometer API has a method queryPedometerData(from:to)

I have never used this API, I only know that it exists. You will have to consult the documentation of your pedometer package to learn more.