icapps / flutter-background-location-tracker

Flutter background location tracker plugin. (Android / iOS)
MIT License
31 stars 37 forks source link

infinite repeat backgroundCallback when First time only install app for any device android #54

Open AhkamKhallaf opened 1 year ago

AhkamKhallaf commented 1 year ago

when BackgroundLocationTrackerManager.handleBackgroundUpdated is called , i call api request to save this new location to server , but this request is repeated many times , how cal fix this bug

@pragma('vm:entry-point')
void backgroundCallback() {
  BackgroundLocationTrackerManager.handleBackgroundUpdated(
      (BackgroundLocationUpdateData data) async {
    await providerContainer.read(currentLocationApiProvider).saveUserLocation(data);
  });
}

final Provider<CurrentLocationApi> currentLocationApiProvider = Provider<CurrentLocationApi>(
        (ProviderRef<CurrentLocationApi> ref) => CurrentLocationApi(ref.read));

class CurrentLocationApi {
  final Reader read;

  CurrentLocationApi(this.read);

  Preferences get preferences => read(preferencesProvider);

  ApiCore get apiCore => read(apiCoreProvider);

  Future<void> saveUserLocation(
      BackgroundLocationUpdateData data,
      ) async {
    await preferences.init();
    apiCore.init();
    try {
      await read(userApiProvider).saveUserLocation(data);
    } catch (onError) {
      debugPrint('$onError,,onError');
    }
  }
}

i use river pod as state management @vanlooverenkoen

vanlooverenkoen commented 1 year ago

What do you exactly mean? The api call will be triggered every time the location updates so this is to be expected

vanlooverenkoen commented 1 year ago

On Android/iOS?

AhkamKhallaf commented 1 year ago

On Android/iOS

android such as OPPO A31 ANDROID , SAMSUNG 172 ANDROID

dev07060 commented 1 year ago

The trackingInterval option actually does not work properly In my emulator which is built with A30, A33 and also on Samsung Galaxy S10(SM G977N) too. if the tracking altitude is not required in your project, geoloactor + flutter_background_service could be a good alternative option it has Stream event and you can adjust it's interval by rxDart throttleTime.

ikbendewilliam commented 1 year ago

@AhkamKhalaaf Do you do WidgetsFlutterBinding.ensureInitialized(); before the await BackgroundLocationTrackerManager.initialize?

AhkamKhallaf commented 1 year ago

@AhkamKhalaaf Do you do WidgetsFlutterBinding.ensureInitialized(); before the await BackgroundLocationTrackerManager.initialize?

yes , i do

ikbendewilliam commented 1 year ago

Have you tried running the example project? Do you also encounter it then?

AhkamKhallaf commented 1 year ago

Have you tried running the example project? Do you also encounter it then?

yes , it's also, there is the same problem

vanlooverenkoen commented 1 year ago

When exactly do you have this issue?

AhkamKhallaf commented 1 year ago
  • With every clean install?

With every clean install

vanlooverenkoen commented 1 year ago

Just for my understanding:

Are these steps correct?

AhkamKhallaf commented 1 year ago
  • With every clean install?

With every clean install

i note , now , it does not work with the example project, and not with my project

 WidgetsFlutterBinding.ensureInitialized();
  /// Background Location initialize
  await BackgroundLocationTrackerManager.initialize(
    backgroundCallback,
    config: const BackgroundLocationTrackerConfig(
      androidConfig: AndroidConfig(
        distanceFilterMeters: 100,
        trackingInterval: Duration(minutes: 5),
        enableCancelTrackingAction: false,
      ),
      iOSConfig: IOSConfig(
        activityType: ActivityType.FITNESS,
        distanceFilterMeters: 100,
        restartAfterKill: true,
      ),
    ),
  );

  // this my config

  FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);
  final ProviderContainer providerContainer = ProviderContainer();
  if (flavorConfig == null) {
    await dotenv.load();
  }

  final Preferences preferences = providerContainer.read(preferencesProvider);
  await preferences.init();
  await Future.wait(<Future<void>>[
    loadImage(splashBackgroundAssets),
    loadImage(splashImageAssets),
  ]);

  await EasyLocalization.ensureInitialized();

  await providerContainer.read(packageInfoHelperProvider).getPackageVersion();

  FlavorConfig.set(
    flavorConfig: preferences.flavorConfig ?? flavorConfig ?? FlavorConfig.production(),
    read: providerContainer.read,
  );

  await Firebase.initializeApp();

  ///Initialize events
  EventsHelper().setEvents(
    events: <Events>[providerContainer.read(firebaseEventsProvider)],
  );

  providerContainer.read(apiCoreProvider).init();

  /// Disable logs in release mode.
  if (kReleaseMode) debugPrint = (String? message, {int? wrapWidth}) {};

  /// disable red screen of death in production.
  if (!isIntegrationTesting) {
    ErrorWidget.builder = (FlutterErrorDetails details) => const SizedBox.shrink();

    ///Enable & initialize crashlytics when we don't run integration testing.
    Crashlytics().enableCrashlytics();
  }

  await SystemChrome.setPreferredOrientations(
    <DeviceOrientation>[DeviceOrientation.portraitUp],
  );
  await providerContainer.read(appStateProvider).fetchAppToken();

  ///Initialize dynamic link
  DynamicLink().getInitialLink();

  final List<Translation> translations = await providerContainer
      .read(translationHelperProvider)
      .getTranslations(forceRefresh: false, keys: <String>["customTranslations"]);

  /// init flutter_bug_logger
  Logger.init(FlavorConfig.isStage());

  runApp(
    showDeviceReview
        ? DevicePreview(
            builder: (BuildContext context) => UncontrolledProviderScope(
              container: providerContainer,
              child: RestartWidget(
                translations: translations,
                showDeviceReview: showDeviceReview,
              ),
            ),
          )
        : UncontrolledProviderScope(
            container: providerContainer,
            child: RestartWidget(
              translations: translations,
              showDeviceReview: showDeviceReview,
            ),
          ),
  );

other code which i run inside main

AhkamKhallaf commented 1 year ago

my background meth when location changed , now i just print , it repeat many times

AhkamKhallaf commented 1 year ago

Just for my understanding:

  • clean install
  • open app
  • start tracking
  • ISSUE HAPPENS
  • quit app
  • stop tracking
  • open app
  • start tracking
  • everything works

Are these steps correct?

yes, but the tracking is enabled always

vanlooverenkoen commented 1 year ago

@dev07060 can you confirm you have exactly the same issue in your project?

And is everything working in our example project?

ikbendewilliam commented 1 year ago

Do you immediately start the tracking or is it after a button press or something (just to make sure you first ask permissions)? For me your config works fine in our example project 🤔

AhkamKhallaf commented 1 year ago

Do you immediately start the tracking or is it after a button press or something (just to make sure you first ask permissions)? For me your config works fine in our example project 🤔

in my project, i depend on silent notification to start track , and i ask user for permissions for location && tracking

vanlooverenkoen commented 1 year ago

Because everything works in the example project, I think in order to help you further we should need a minimal reproducible project. that only contains

Can you provide us with that.

AhkamKhallaf commented 1 year ago

Because everything works in the example project, I think in order to help you further we should need a minimal reproducible project. that only contains

  • the bug
  • minimal screens
  • minimal logic
  • no setup

Can you provide us with that.

i use this package to get updated location with fore&&back ground , there's not any ui for that , i depends on silent notification to start &&end tracking , i use Flutter (Channel stable, 3.10.6) , Dart SDK version: 3.0.6 i'll put copy of my main method

Future main({ FlavorConfig? flavorConfig, bool isIntegrationTesting = false, bool showDeviceReview = false, }) async { WidgetsFlutterBinding.ensureInitialized(); FirebaseMessaging.onBackgroundMessage(_firebaseMessagingBackgroundHandler);

if (flavorConfig == null) { await dotenv.load(); }

final Preferences preferences = providerContainer.read(preferencesProvider); await preferences.init(); await Future.wait(<Future>[ loadImage(splashBackgroundAssets), loadImage(splashImageAssets), ]);

await EasyLocalization.ensureInitialized();

await providerContainer.read(packageInfoHelperProvider).getPackageVersion();

FlavorConfig.set( flavorConfig: preferences.flavorConfig ?? flavorConfig ?? FlavorConfig.production(), read: providerContainer.read, );

await Firebase.initializeApp();

///Initialize events EventsHelper().setEvents( events: [providerContainer.read(firebaseEventsProvider)], );

providerContainer.read(apiCoreProvider).init();

/// Disable logs in release mode. if (kReleaseMode) debugPrint = (String? message, {int? wrapWidth}) {};

/// disable red screen of death in production. if (!isIntegrationTesting) { ErrorWidget.builder = (FlutterErrorDetails details) => const SizedBox.shrink();

///Enable & initialize crashlytics when we don't run integration testing.
Crashlytics().enableCrashlytics();

}

await SystemChrome.setPreferredOrientations(

[DeviceOrientation.portraitUp], ); await providerContainer.read(appStateProvider).fetchAppToken(); ///Initialize dynamic link DynamicLink().getInitialLink(); final List translations = await providerContainer .read(translationHelperProvider) .getTranslations(forceRefresh: false, keys: ["customTranslations"]); /// init flutter_bug_logger Logger.init(FlavorConfig.isStage()); /// Background Location initialize await BackgroundLocationTrackerManager.initialize( backgroundCallback, config: const BackgroundLocationTrackerConfig( androidConfig: AndroidConfig( distanceFilterMeters: 100, trackingInterval: Duration(minutes: 5), enableCancelTrackingAction: false, ), iOSConfig: IOSConfig( activityType: ActivityType.FITNESS, distanceFilterMeters: 100, restartAfterKill: true, ), ), );,, my pubspec.yaml name: ozee_sp description: An eCommerce mobile app for Ozee project, built with Flutter. # The following line prevents the package from being accidentally published to # pub.dev using `pub publish`. This is preferred for private packages. publish_to: 'none' # Remove this line if you wish to publish to pub.dev # The following defines the version and build number for your application. # A version number is three numbers separated by dots, like 1.2.43 # followed by an optional build number separated by a +. # Both the version and the builder number may be overridden in flutter # build by specifying --build-name and --build-number, respectively. # In Android, build-name is used as versionName while build-number used as versionCode. # Read more about Android versioning at https://developer.android.com/studio/publish/versioning # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html version: 1.0.4+22 environment: sdk: '>=2.12.0 <3.0.0' dependencies: auto_size_text: ^3.0.0-nullsafety.0 bot_toast: ^4.0.1 cached_network_image: ^3.0.0 carousel_slider: ^4.0.0 collection: ^1.15.0 cookie_jar: ^3.0.1 dio: ^4.0.4 dio_cookie_manager: ^2.0.0 easy_localization: ^3.0.0 encrypt: ^5.0.0 firebase_analytics: ^10.0.4 flutter: sdk: flutter flutter_bug_logger: ^1.0.0 flutter_cache_manager: ^3.3.0 flutter_dotenv: ^5.0.2 flutter_gen: ^5.1.0+1 flutter_html: ^3.0.0-alpha.5 flutter_localizations: sdk: flutter flutter_rating_bar: ^4.0.0 flutter_riverpod: ^1.0.2 geocoding: ^2.0.0 google_fonts: ^4.0.4 image_picker: ^1.0.2 in_app_review: ^2.0.2 line_awesome_flutter: ^2.0.0 location: ^5.0.1 package_info: ^2.0.2 percent_indicator: ^4.2.2 app_settings: ^5.0.0 background_location_tracker: ^1.4.0 recapet_events: git: url: https://gitlab.zarcony.com/ibrahim.gamal/recapet_events.git ref: 0.0.1 recapet_firebase: git: url: https://gitlab.zarcony.com/ibrahim.gamal/recapet_firebase.git ref: dev recapet_widgets: git: url: https://gitlab.zarcony.com/ibrahim.gamal/recapet_widgets.git ref: dev shared_preferences: ^2.0.6 url_launcher: ^6.1.2 infinite_scroll_pagination: ^3.1.0 connectivity_plus: ^4.0.1 intl: ^0.18.0 dev_dependencies: build_runner: ^2.3.2 device_preview: ^1.1.0 #To get unused files => flutter pub run dart_code_metrics:metrics check-unused-files lib dart_code_metrics: ^5.0.1 flutter_gen_runner: ^5.0.3 #To generate app icons => flutter pub run flutter_launcher_icons flutter_launcher_icons: ^0.13.1 flutter_native_splash: ^2.2.16 flutter_test: sdk: flutter integration_test: sdk: flutter lint: ^2.1.2 # to use flutter gen => flutter packages pub run build_runner build # to delete conflicts => flutter packages pub run build_runner build --delete-conflicting-outputs flutter_gen: output: lib/src/gen/ integrations: flutter_svg: true # For information on the generic Dart part of this file, see the # following page: https://dart.dev/tools/pub/pubspec flutter_native_splash: background_image: 'assets/images/splash_background.png' android: true ios: true web: false android_gravity: center ios_content_mode: center flutter_icons: android: false ios: true image_path: "assets/launcher/ozee_sp_logo.png" adaptive_icon_background: "#000000" # The following section is specific to Flutter. flutter: # The following line ensures that the Material Icons font is # included with your application, so that you can use the icons in # the material Icons class. uses-material-design: true # To add assets to your application, add an assets section, like this: assets: - .env - assets/images/ - assets/images/1.0x/ - assets/images/2.0x/ - assets/images/3.0x/ - assets/images/4.0x/ - assets/launcher/ - assets/i18n/ - lib/src/widgets/timezone-0.9.0/lib/data/ , my build gradle android def localProperties = new Properties() def localPropertiesFile = rootProject.file('local.properties') if (localPropertiesFile.exists()) { localPropertiesFile.withReader('UTF-8') { reader -> localProperties.load(reader) } } def flutterRoot = localProperties.getProperty('flutter.sdk') if (flutterRoot == null) { throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") } def flutterVersionCode = localProperties.getProperty('flutter.versionCode') if (flutterVersionCode == null) { flutterVersionCode = '1' } def flutterVersionName = localProperties.getProperty('flutter.versionName') if (flutterVersionName == null) { flutterVersionName = '1.0' } apply plugin: 'com.android.application' apply plugin: 'kotlin-android' apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" def keyStoreProperties = new Properties() def keyStorePropertiesFile = rootProject.file('key.properties') if (keyStorePropertiesFile.exists()) { keyStoreProperties.load(new FileInputStream(keyStorePropertiesFile)) } def debugKeystoreProperties = new Properties() def debugKeystorePropertiesFile = rootProject.file('debug-key.properties') if (debugKeystorePropertiesFile.exists()) { debugKeystoreProperties.load(new FileInputStream(debugKeystorePropertiesFile)) } android { compileSdkVersion 33 sourceSets { main.java.srcDirs += 'src/main/kotlin' } lintOptions { disable 'InvalidPackage' } defaultConfig { // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). applicationId "sp.ozeesalon.com" minSdkVersion 23 targetSdkVersion 33 versionCode flutterVersionCode.toInteger() versionName flutterVersionName } signingConfigs { debug { keyAlias debugKeystoreProperties['keyAlias'] keyPassword debugKeystoreProperties['keyPassword'] storeFile file(debugKeystoreProperties['storeFile']) storePassword debugKeystoreProperties['storePassword'] } release { keyAlias keyStoreProperties['keyAlias'] keyPassword keyStoreProperties['keyPassword'] storeFile file(keyStoreProperties['storeFile']) storePassword keyStoreProperties['storePassword'] } } buildTypes { release { // TODO: Add your own signing config for the release build. // Signing with the debug keys for now, so `flutter run --release` works. signingConfig signingConfigs.release } } } flutter { source '../..' } dependencies { implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'androidx.appcompat:appcompat:1.5.1' } apply plugin: 'com.google.firebase.crashlytics' apply plugin: 'com.google.gms.google-services' , i hope that be useful
AhkamKhallaf commented 1 year ago

@vanlooverenkoen there is a simple template https://github.com/AhkamKhalaaf/example_location/

AhkamKhallaf commented 1 year ago

Fatal Exception: java.lang.RuntimeException

Unable to create service com.icapps.background_location_tracker.service.LocationUpdatesService: android.app.ForegroundServiceStartNotAllowedException: Service.startForeground() not allowed due to mAllowStartForeground false: service sp.ozeesalon.com/com.icapps.background_location_tracker.service.LocationUpdatesService

Caused by android.app.ForegroundServiceStartNotAllowedException

Service.startForeground() not allowed due to mAllowStartForeground false: service sp.ozeesalon.com/com.icapps.background_location_tracker.service.LocationUpdatesService

android.app.Service.startForeground (Service.java:862)

arrow_right

com.icapps.background_location_tracker.utils.NotificationUtil.startForeground (NotificationUtil.kt:110)

com.icapps.background_location_tracker.service.LocationUpdatesService.startTracking (LocationUpdatesService.kt:176)

com.icapps.background_location_tracker.service.LocationUpdatesService.onCreate (LocationUpdatesService.kt:77)

android.app.ActivityThread.handleCreateService (ActivityThread.java:4651)

AhkamKhallaf commented 1 year ago

The trackingInterval option actually does not work properly In my emulator which is built with A30, A33 and also on Samsung Galaxy S10(SM G977N) too. if the tracking altitude is not required in your project, geoloactor + flutter_background_service could be a good alternative option it has Stream event and you can adjust it's interval by rxDart throttleTime.

oh, it's so important for me , now track interval does not work well for any android device, handleBackgroundUpdated is called for less than one second , when first time run app, but when kill it and run again, it work s fine , any help about this issue @dev07060

AhkamKhallaf commented 1 year ago

Do you immediately start the tracking or is it after a button press or something (just to make sure you first ask permissions)? For me your config works fine in our example project 🤔

yes, i immediately start tracking , inside initState homeScreen , there is a problem with this @ikbendewilliam

AhkamKhallaf commented 1 year ago

Because everything works in the example project, I think in order to help you further we should need a minimal reproducible project. that only contains

  • the bug
  • minimal screens
  • minimal logic
  • no setup

Can you provide us with that.

i try example project but not work with my environment , can you tell me your com.google.gms:google-services or any settings can effect @vanlooverenkoen

ikbendewilliam commented 1 year ago

@AhkamKhalaaf I've checked out your example, thank you for providing this to us. First I have to say that I don't encounter your issue that the backgroundCallback is repeated. It's only called after the set time interval. That being said, there are a few issues I see in your code.

In the handleBackgroundUpdated you do (data) async => { This means that you actually return a Set from the method and not execute the content of your method.

More importantly, you don't await the Repo().update(data). Meaning that when you do an async call inside this, the background will be disposed as it won't wait for the process to complete. This is the main issue I think in your code.

You start tracking before you have permission, this is not good. You should first ask the user for permission and then start tracking. I would update your init and checkLocation as follows:

  void initState() {
    super.initState();
    checkLocation();
  }
...
  Future<void> checkLocation() async {
...
    _permissionGranted = await location.hasPermission();
    if (_permissionGranted == PermissionStatus.denied) {
      _permissionGranted = await location.requestPermission();
      if (_permissionGranted != PermissionStatus.granted) {
        return;
      }
    }
    await startTRack();
    _getTrackingStatus();
    _startLocationsUpdatesStream();
    debugPrint('${await BackgroundLocationTrackerManager.isTracking()},,isTrackingisTracking');

With these fixes, it works fine for me on my Android device. Can you fix these issues and see if this resolves your issue?

AhkamKhallaf commented 1 year ago

> @AhkamKhalaaf I've checked out your example, thank you for providing this to us. First I have to say that I don't encounter your issue that the backgroundCallback is repeated. It's only called after the set time interval. That being said, there are a few issues I see in your code.

In the handleBackgroundUpdated you do (data) async => { This means that you actually return a Set from the method and not execute the content of your method.

More importantly, you don't await the Repo().update(data). Meaning that when you do an async call inside this, the background will be disposed as it won't wait for the process to complete. This is the main issue I think in your code.

You start tracking before you have permission, this is not good. You should first ask the user for permission and then start tracking. I would update your init and checkLocation as follows:

  void initState() {
    super.initState();
    checkLocation();
  }
...
  Future<void> checkLocation() async {
...
    _permissionGranted = await location.hasPermission();
    if (_permissionGranted == PermissionStatus.denied) {
      _permissionGranted = await location.requestPermission();
      if (_permissionGranted != PermissionStatus.granted) {
        return;
      }
    }
    await startTRack();
    _getTrackingStatus();
    _startLocationsUpdatesStream();
    debugPrint('${await BackgroundLocationTrackerManager.isTracking()},,isTrackingisTracking');

With these fixes, it works fine for me on my Android device. Can you fix these issues and see if this resolves your issue?

@ikbendewilliam thank you very much for your response,

i follow all your notes and push them to repo , but the issue is still , handleBackgroundUpdated is repeated every second , as you show the location is the same

![Uploading Screenshot_20230914_113907.png…]()

AhkamKhallaf commented 1 year ago

i use in android/build.gradle classpath 'com.android.tools.build:gradle:7.1.2' in android/gradle/wrapper/gradle-wrapper.properties distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip what about you ?@ikbendewilliam

ikbendewilliam commented 1 year ago

Hi @AhkamKhalaaf the gradle version depends on your local installation and shouldn't matter unless you encounter issues with this. I don't fully know what the issue can be in your case as it is working on our end. Have you tried different devices? And does it work on iOS?

AhkamKhallaf commented 1 year ago

Hi @AhkamKhalaaf the gradle version depends on your local installation and shouldn't matter unless you encounter issues with this. I don't fully know what the issue can be in your case as it is working on our end. Have you tried different devices? And does it work on iOS?

yes , it works with ios platform , yes i try with diffirent emulators and read device