flutter_background_geolocation icon indicating copy to clipboard operation
flutter_background_geolocation copied to clipboard

onMotionChange is not being called when user have stopped

Open hamza39460 opened this issue 3 years ago • 11 comments

Your Environment

  • Plugin version: ^4.6.2
  • Platform: Android
  • OS version: 9
  • Device manufacturer / model: Samsung Galaxy S8
  • Flutter info (flutter doctor): [✓] Flutter (Channel stable, 2.10.5, on macOS 12.3 21E230 darwin-arm, locale en-PK) [✓] Android toolchain - develop for Android devices (Android SDK version 32.1.0-rc1) [✓] Xcode - develop for iOS and macOS (Xcode 13.3) [✗] Chrome - develop for the web (Cannot find Chrome executable at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome) ! Cannot find Chrome. Try setting CHROME_EXECUTABLE to a Chrome executable. [✓] Android Studio (version 2021.1) [✓] VS Code (version 1.67.0) [!] Connected device ! No devices available [✓] HTTP Host Availability
  • Plugin config:
import 'dart:async';
import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:get/get.dart';
import 'package:intl/intl.dart';
import 'package:location/location.dart';
import 'package:niture/Enums/api_action_type.dart';
import 'package:niture/Globals/endpoints.dart';
import 'package:niture/Globals/utils.dart';
import 'package:niture/Managers/APIManager.dart';
import 'package:niture/Models/location_model.dart';
import 'package:niture/Models/stop_model.dart';
import 'package:niture/Providers/firebase_db_provider.dart';
import 'package:niture/Providers/traveler_provider.dart';
import 'package:provider/provider.dart';
import 'package:permission_handler/permission_handler.dart' as permission;
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart'
    as bg;

class LocationProvider extends ChangeNotifier {
  LocationData? locationData;
  Timer? timer;
  BuildContext? _context;
  String? freq;
  String? travelID;
  List<int> _nearLocationIndexes = [];
  List<LocationData> _nearLocations = [];
  DateTime? startTimeOfStop;
  LocationData? stopLocation;
  int secondsStop = 0;
  bool isMoving = true;
  Location location = Location();
  LocationModel? locationModel;

  _addToFirebase() async {
    if (locationModel == null) return;
    Provider.of<FirebaseDbProvider>(Get.context!, listen: false)
        .setLocation(locationModel!, travelID!, freq!);
    _getNearLocationIndexes();
    await _reportNearLocationIndexes();
  }

  _addStopTimeToFirebase() async {
    secondsStop = DateTime.now().difference(startTimeOfStop!).inSeconds;
    if (secondsStop == 0) return;
    StopModel stopModel = StopModel(
        stopLocation!.latitude!,
        stopLocation!.latitude!,
        dateTimeFormatted(dateTime: startTimeOfStop),
        secondsStop);
    await Provider.of<FirebaseDbProvider>(Get.context!, listen: false)
        .setStopTime(stopModel, travelID!);
    secondsStop = 0;
    stopLocation = null;
    startTimeOfStop = null;
  }

  String dateTimeFormatted({DateTime? dateTime}) {
    dateTime ??= DateTime.now();
    final format = DateFormat('HH:mm:ss dd-MM-yyyy');
    return format.format(dateTime);
  }

  Future<bool> isLocationServiceEnabled() async {
    return await permission.Permission.location.serviceStatus.isEnabled;
  }

  haveLocationPermission() async {
    return await permission.Permission.location.isGranted;
  }

  isPermissionPermanentyDenied() async {
    bool iss = await permission.Permission.location.isPermanentlyDenied;
    log("permanent $iss");
    return iss;
  }

  askForLocationPermission() async {
    Location location = Location();
    PermissionStatus _permissionGranted = await location.hasPermission();
    while (_permissionGranted == PermissionStatus.denied) {
      _permissionGranted = await location.requestPermission();
    }
    return _permissionGranted == PermissionStatus.granted;
  }

  _getNearLocationIndexes() {
    if (locationData == null) return;
    List<LocationModelWithKeys> locations =
        Provider.of<TravelerProvider>(Get.context!, listen: false)
            .travelModel!
            .locations;
    for (int index = 0; index < locations.length; index++) {
      LocationModelWithKeys location = locations[index];
      double distance = Utils.calculateDistance(location.latitude,
          location.longitude, locationData?.latitude, locationData?.longitude);
      if (distance < location.radius) {
        if (!_nearLocationIndexes.contains(index)) {
          _nearLocationIndexes.add(index);
          _nearLocations.add(locationData!);
        }
      }
    }
  }

  _reportNearLocationIndexes() async {
    List<LocationModelWithKeys> locations =
        Provider.of<TravelerProvider>(Get.context!, listen: false)
            .travelModel!
            .locations;
    for (int i = 0; i < _nearLocationIndexes.length; i++) {
      int index = _nearLocationIndexes[i];
      LocationData _locationData = _nearLocations[i];
      LocationModelWithKeys location = locations[index];
      await _reportLocation(location.key, _locationData);
      locations.removeAt(index);
    }
    _nearLocationIndexes.clear();
  }

  _reportLocation(String locationKey, LocationData _locationData) async {
    Map<String, dynamic> data =
        Utils.apiRequestBaseData(ApiActionType.reportLocation.name);
    data.putIfAbsent("travelId", () => travelID);
    data.putIfAbsent("key", () => locationKey);
    data.putIfAbsent("key", () => locationKey);
    data.putIfAbsent("lat", () => _locationData.latitude);
    data.putIfAbsent("lng", () => _locationData.longitude);
    APIManager().getRequest(EndPoints.base, data);
  }

  locationSetupBG(String travelID, String sec, BuildContext context) async {
    FirebaseDbProvider.previousTimeStamp = null;
    _context = context;
    freq = sec;
    this.travelID = travelID;
    isMoving = false;
    location.enableBackgroundMode();
    bg.BackgroundGeolocation.onLocation((bg.Location location) {
      if (isMoving == false && startTimeOfStop != null) {
        _addStopTimeToFirebase();
      }
      isMoving = true;
      if (location.sample) return;
      log('[location] - $location');
      _prepareAndSendToFirebase(location);
    });

    // bg.BackgroundGeolocation.onActivityChange(
    //     (bg.ActivityChangeEvent activity) async {
    //   if (activity.activity == "still") {
    //       startTimeOfStop = DateTime.now();
    //       stopLocation = await location.getLocation();
    //       isMoving = false;
    //   } else {
    //     if (startTimeOfStop != null) {
    //       _addStopTimeToFirebase();
    //     }
    //   }
    // });

    bg.BackgroundGeolocation.onMotionChange((bg.Location location) {
      if (!location.sample) {
        _prepareAndSendToFirebase(location);
      }
      if (!location.isMoving) {
        log("stopped");
        _handleStationary(location);
      } else {
        log("moving");
        if (startTimeOfStop != null) {
          _addStopTimeToFirebase();
        }
      }
    });

    bg.BackgroundGeolocation.onProviderChange((bg.ProviderChangeEvent event) {
      log('[providerchange] - $event');
    });

    bg.BackgroundGeolocation.ready(bg.Config(
            desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
            distanceFilter: 20.0,
            stopOnTerminate: true,
            startOnBoot: true,
            debug: false,
            stopOnStationary: false,
            isMoving: true,
            autoSync: false,
            minimumActivityRecognitionConfidence: 0,
            logLevel: bg.Config.LOG_LEVEL_VERBOSE))
        .then((bg.State state) {
      if (!state.enabled) {
        bg.BackgroundGeolocation.start();
      }
    });
  }

  removeLocationBG() async {
    await bg.BackgroundGeolocation.removeListeners();
    await bg.BackgroundGeolocation.stop();
  }

  _prepareAndSendToFirebase(bg.Location location) {
    double lat = location.coords.latitude;
    double long = location.coords.longitude;
    locationData = LocationData.fromMap({
      "latitude": lat,
      "longitude": long,
    });
    locationModel = LocationModel(locationData!.latitude ?? 0,
        locationData!.longitude ?? 0, dateTimeFormatted(),
        isMoving: location.isMoving);
    _addToFirebase();
  }

  _handleStationary(bg.Location location) {
    startTimeOfStop = DateTime.now();
    double lat = location.coords.latitude;
    double long = location.coords.longitude;
    stopLocation = LocationData.fromMap({
      "latitude": lat,
      "longitude": long,
    });
  }
}

Expected Behavior

when the device stops moving onMotionChange should be called with isMoving parameter as false and, my code shall save the timestamp of when onMotionChange is called with false, and when the device starts moving it shall be called again with isMoving parameter as true and save the location at which device was stopped into the firebase. please note device is in a CAR.

Actual Behavior

It is not being called and data is not being saved to firebase. We are already saving the continuous locations to firebase which are being held perfectly but onMotionChange is not behaving expectedly.

Context

The app is to track the trip of Bus Drivers, we are successfully saving the continuous updates to firebase. We are adding one more feature for admin, that tracks the stops of buses (stop record shall include time at which bus was stopped, location of the stop, and approximate time for how much time bus was stopped in seconds), so when the bus is stopped it shall save the location, and time of the stop and when the bus starts moving it shall save the data to firebase. But it is not saving the data to firebase but meanwhile, continuous locations are being saved to firebase when the device is moving

Debug logs

Logs
PASTE_YOUR_LOGS_HERE

hamza39460 avatar May 17 '22 20:05 hamza39460

See api docs Config.stopTimeout

christocracy avatar May 18 '22 01:05 christocracy

I have set stopTimout to 0 but its still not being called when bus is stopped..

hamza39460 avatar May 24 '22 20:05 hamza39460

Are you observing the plugin logs? See wiki Debugging

christocracy avatar May 24 '22 20:05 christocracy

Here are the logs. If you notice the time stamp of 23:01:24.241 when we take a stop of 1 minute. It didn't fire the onMotionChange. event instead it says locationIsInvalid

[05-24 20:16:19.306 INFO LoggerFacade$a .log

hamza39460 avatar May 25 '22 06:05 hamza39460

The stopTimeout timer is initiated when the Motion API reports the device is still. It can take some time, depending on the device and quality of the sensors (accelerometer, gyroscope, magnetometer) to report Motion Transition events. There is nothing to be done for the events but wait with the device perfectly still.

Eg:

05-24 21:00:53.821 INFO [ActivityRecognitionService a] 
╔═════════════════════════════════════════════
║ Motion Transition Result
╠═════════════════════════════════════════════
╟─ 🔴  EXIT: in_vehicle
╟─ 🎾  ENTER: still
╚═════════════════════════════════════════════

christocracy avatar May 25 '22 13:05 christocracy

I also suggest you use debug: true so you can hear the motionchange events soundFX. If you don't know what the SoundFx mean, see the api docs Config.debug.

christocracy avatar May 25 '22 13:05 christocracy

I have set the stopTimeout as 0, but We are not receiving the motion change updates. We made the stop of 1 minute again and the device was perfectly still. Did you check the logs of mentioned time i.e 23:01:24.241

hamza39460 avatar May 25 '22 17:05 hamza39460

Let the device sit still for as long as required. if it takes 15 minutes or an hour, there's nothing that can be done.

Motion API behaviour is better / worse across different device manufacturers.

christocracy avatar May 25 '22 17:05 christocracy

When you see this in the logs, the stopTimeout timer will be initiated. If you've set stopTimeout: 0, the plugin will immediately enter the stationary state and fire the onMotionChange event.

╔═════════════════════════════════════════════
║ Motion Transition Result
╠═════════════════════════════════════════════
╟─ 🎾  ENTER: still
╚═════════════════════════════════════════════

christocracy avatar May 25 '22 17:05 christocracy

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] avatar Sep 21 '22 05:09 stale[bot]

How to achieve int? stopTimeout; to 0 minutes so that when the device changes from Motion state to Stationery state the device still sends lat / long every 1 minute?

As We have set heartbeatInterval = 60 seconds

iloads avatar May 17 '23 10:05 iloads