Skip to content

[Help Wanted]: [Android] Reliable way to execute task at specific time (e.g. 11:30 PM) and stop BackgroundGeolocation #1670

@chikki200027-glitch

Description

@chikki200027-glitch

Required Reading

  • Confirmed

Plugin Version

flutter_background_geolocation: "4.16.9", background_fetch: "1.5.0"

Mobile operating-system(s)

  • iOS
  • Android

Device Manufacturer(s) and Model(s)

Nokia T20 Tab - TA-1397

Device operating-systems(s)

Android 13

What do you require assistance about?

I am using flutter_background_geolocation for continuous tracking and I have a business requirement:

At ~11:30 PM, I need to:
Call a “End Day” API
Stop background tracking (BackgroundGeolocation.stop())

Problem:
I am unable to reliably execute this logic at the scheduled time.

However:
onHeartbeat is not firing consistently (sometimes not at all)
Timers are not reliable in background (expected due to Android limitations)

Observation:
On some devices (e.g. Xiaomi), location updates continue overnight
But onHeartbeat does not fire, so my logic is never executed
On other devices (Nokia/Motorola), background tracking itself stops after some time

So currently I don’t have a reliable trigger to:
run code at a specific time
and then stop the service

Questions:

  • Is there any reliable way within this plugin to execute a task at a specific time (e.g. 11:30 PM)?
  • What is the recommended approach to:
    run a one-time task at night
    and then stop tracking reliably?

Goal:
I need a reliable way to ensure:
API is called once at night
tracking is stopped afterward
even under Android background restrictions.

Environment:
Flutter version: [3.41.6]
Plugin version: flutter_background_geolocation: "4.16.9", background_fetch: "1.5.0"
Android versions tested:11, 12, 13
Devices: Xiaomi, Nokia, Motorola, Samsung

[Optional] Plugin Code and/or Config

**Current Setup:**

// Receive events from BackgroundGeolocation in Headless state.ksdnsknd
@pragma('vm:entry-point')
void backgroundGeolocationHeadlessTask(bg.HeadlessEvent headlessEvent) async {
  switch (headlessEvent.name) {
    case bg.Event.BOOT:
      final now = DateTime.now();
      if (now.hour >= 23 && now.minute >= 30) {
        // Device rebooted at night, don't let services start
        await bg.BackgroundGeolocation.stop();
        await bf.BackgroundFetch.stop();
      }
      bg.State state = await bg.BackgroundGeolocation.state;
      customPrint('📬 didDeviceReboot: ${state.didDeviceReboot}');
      break;
    case bg.Event.TERMINATE:
      try {
        bg.Location location = await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, extras: {'event': 'terminate', 'headless': true});
        customPrint('[getCurrentPosition] Headless: $location');
      } catch (e) {
        return;
      }
      break;
    // TODO: RECHECK
    case bg.Event.HEARTBEAT:
      final now = DateTime.now();
      if (now.hour == 23 && now.minute >= 30) {
        // await forceStopAllServices();
        // try {
        //   onSyncStart.syncAllDataFinal();
        // } catch (e) {}
        await finishYourDayInBg();
        await forceStopAllServices();
      }
      break;
    case bg.Event.LOCATION:
      bg.Location location = headlessEvent.event;
      customPrint(location);
      break;
    case bg.Event.MOTIONCHANGE:
      bg.Location location = headlessEvent.event;
      customPrint(location);
      break;
    case bg.Event.GEOFENCE:
      bg.GeofenceEvent geofenceEvent = headlessEvent.event;
      customPrint(geofenceEvent);
      break;
    case bg.Event.GEOFENCESCHANGE:
      bg.GeofencesChangeEvent event = headlessEvent.event;
      customPrint(event);
      break;
    case bg.Event.SCHEDULE:
      bg.State state = headlessEvent.event;
      customPrint(state);
      break;
    case bg.Event.ACTIVITYCHANGE:
      bg.ActivityChangeEvent event = headlessEvent.event;
      customPrint(event);
      break;
    case bg.Event.HTTP:
      bg.HttpEvent event = headlessEvent.event;
      customPrint(event);
      break;
    case bg.Event.POWERSAVECHANGE:
      bool enabled = headlessEvent.event;
      customPrint(enabled);
      break;
    case bg.Event.CONNECTIVITYCHANGE:
      bg.ConnectivityChangeEvent event = headlessEvent.event;
      customPrint(event);
      break;
    case bg.Event.ENABLEDCHANGE:
      bool enabled = headlessEvent.event;
      customPrint(enabled);
      break;
    // TODO: RECHECK
    case bg.Event.AUTHORIZATION:
      bg.AuthorizationEvent event = headlessEvent.event;
      customPrint(event);
      bg.BackgroundGeolocation.setConfig(bg.Config(url: '${baseUrl2}AddbackgroundLocation', headers: {'Authorization': "Bearer ${prefs!.getString('token')}"}));
      break;
  }
}



int _delayUntil(TimeOfDay time) {
  final now = DateTime.now();

  DateTime target = DateTime(now.year, now.month, now.day, time.hour, time.minute);

  if (target.isBefore(now)) {
    target = target.add(const Duration(days: 1));
  }

  return target.difference(now).inMilliseconds;
}

Future<void> scheduleDailyStop() async {
  await bf.BackgroundFetch.scheduleTask(
    bf.TaskConfig(
      taskId: "night_stop_tracking",
      delay: _delayUntil(const TimeOfDay(hour: 23, minute: 30)),
      periodic: false,
      stopOnTerminate: false,
      enableHeadless: true,
      startOnBoot: true,
      forceAlarmManager: true,
    ),
  );
}

void onFetch(String taskId) async {
  if (taskId == "night_stop_tracking") {
    await bg.BackgroundGeolocation.stop();
    await forceStopAllServices();

    // schedule tomorrow's stop
    await scheduleDailyStop();
  }

  bf.BackgroundFetch.finish(taskId);
}

/// Receive events from BackgroundFetch in Headless state.
@pragma('vm:entry-point')
void backgroundFetchHeadlessTask(bf.HeadlessTask task) async {
  String taskId = task.taskId;

  if (task.timeout) {
    bf.BackgroundFetch.finish(taskId);
    return;
  }

  if (taskId == "night_stop_tracking") {
    await finishYourDayInBg(); // ← now does exactly what you want
    await scheduleDailyStop(); // for tomorrow
    bf.BackgroundFetch.finish(taskId);
    return;
  }

  // Normal periodic task
  log('Background Fetch: normal connectivity trigger');
  await storeConnectivityInfoLocally();
  bf.BackgroundFetch.finish(taskId);
}


API REQUEST - TO END DAY [ I executed this manually, it's working - no issue here ]

Future<void> finishYourDayInBg() async {
  final prefs = await SharedPreferences.getInstance();
  await prefs.reload(); // ensure latest values

  // ←←← IDEMPOTENCY GUARD (prevents double execution)
  if (prefs.getString('dayType') == Constants.finishDay) {
    customPrint('🌙 Night finish already done → skipping');
    await forceStopAllServices();
    return;
  }

  // final bool isLocService = await Permission.locationWhenInUse.serviceStatus.isEnabled;
  bool internetAvailable = false;

  try {
    internetAvailable = await checkIfInternetWorking();
  } catch (_) {}

  customPrint('🌙 NIGHT FINISH TRIGGERED | Internet: $internetAvailable | Loc: ');

  // ====================== DATA FIRST (your requirement) ======================
  if (internetAvailable) {
    customPrint('📤 Internet ONFull Isar + connectivity sync');

    // 2. Final connectivity log + sync
    try {
      await storeConnectivityInfoLocally();
      customPrint('Connectivity synced');
    } catch (e) {
      customPrint('⚠️ Connectivity sync failed: $e');
    }

    // 1. Most important: Isar sync
    try {
      await onSyncStart.syncAllDataFinal();
      customPrint('Isar sync completed');
    } catch (e) {
      customPrint('⚠️ Isar sync failed (continuing): $e');
    }

    // 3. Finishday API
    // if (isLocService) {
    try {
      final value = await bg.BackgroundGeolocation.getCurrentPosition(samples: 1, timeout: 30);

      String finalAddress = '-';
      try {
        final placemarks = await placemarkFromCoordinates(value.coords.latitude, value.coords.longitude);
        finalAddress =
            '${placemarks[0].name}, ${placemarks[0].subLocality}, '
            '${placemarks[0].thoroughfare}, ${placemarks[0].locality}, '
            '${placemarks[0].postalCode}, ${placemarks[0].administrativeArea}, '
            '${placemarks[0].country}';
      } catch (_) {}

      final pp = {
        'loginid': prefs.getString('loginID') ?? '',
        'latitude': value.coords.latitude,
        'longitude': value.coords.longitude,
        'location': finalAddress,
        'batterylevel': (value.battery.level * 100).toInt(),
        'endtime': DateTime.now().toIso8601String(),
        'endgpsenbled': true,
        'endlocationaccuracy': value.coords.accuracy.toPrecision(2),
        'endmocklocationenabled': value.mock,
      };

      final temp = await fRetryOptions.retry(
        () => http.post(
          Uri.parse('${baseUrl}Finishday'),
          body: jsonEncode(pp),
          headers: {'Accept': 'application/json', 'Content-Type': 'application/json', 'Authorization': 'Bearer ${prefs.getString('token')}'},
        ),
        retryIf: (e) => e is SocketException || e is TimeoutException || e is HttpException || e is http.ClientException || e is FormatException,
      );

      customPrint('Finishday API success: ${temp.statusCode}');
    } catch (e) {
      customPrint('⚠️ Finishday API failed (data already synced): $e');
    }
    // }
  } else {
    customPrint('📴 No internet → Skipping sync & API, only local cleanup');
  }

  // ====================== ALWAYS CLEAN UP ======================
  try {
    await prefs.setString('dayType', Constants.finishDay);
    await prefs.setBool('isDayFinished', true);
    await prefs.remove('beatID');
    await prefs.remove('distributorID');

    await forceStopAllServices();
    customPrint('🛑 All background services stopped (battery saved)');
  } catch (e) {
    customPrint('Cleanup error: $e');
  }
}


 
Future<void> initPlatformState() async {
    // Configure BackgroundFetch.
    await bf.BackgroundFetch.configure(
      bf.BackgroundFetchConfig(
        forceAlarmManager: true,
        minimumFetchInterval: 15,
        stopOnTerminate: false,
        enableHeadless: true,
        startOnBoot: true,
        requiresBatteryNotLow: false,
        requiresCharging: false,
        requiresStorageNotLow: false,
        requiresDeviceIdle: false,
        requiredNetworkType: bf.NetworkType.NONE,
      ),
      (String taskId) async {
        bg.BackgroundGeolocation.getCurrentPosition(extras: {'event': 'terminate', 'headless': true});
        customPrint('[BackgroundFetch] Event received $taskId');
        bf.BackgroundFetch.finish(taskId);
      },
      (String taskId) async {
        bf.BackgroundFetch.finish('taskId');
      },
    );
    if (!mounted) return;
  }


 
@override
  void initState() {
    super.initState();
    WidgetsBinding.instance.addObserver(this);;
    bg.BackgroundGeolocation.onConnectivityChange((bg.ConnectivityChangeEvent event) async {
      if (event.connected == true) {
        await onSyncStart.syncAllDataFinal();
      }
    });
    // 2.  Configure the plugin
    if (prefs!.getString('dayType') != Constants.noStartDayRequired) {
      bg.BackgroundGeolocation.ready(
        bg.Config(
          enableHeadless: true,
          heartbeatInterval: 60,
          locationAuthorizationRequest: 'Always',
          url: 'https://example.com/AddbackgroundLocation',
          headers: {'Authorization': "Bearer ${prefs!.getString('token')}"},
          desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
          distanceFilter: 50,
          // scheduleUseAlarmManager: true,
          // schedule: ['1-7 21:19-21:20'],
          stopOnTerminate: false,
          disableElasticity: false,
          elasticityMultiplier: 1,
          desiredOdometerAccuracy: 100,
          // stationaryRadius: ,
          stationaryRadius: 25,
          maxDaysToPersist: 3,
          locationUpdateInterval: 8000,
          stopTimeout: 5,
          disableMotionActivityUpdates: false,
          // useSignificantChangesOnly: true,
          disableStopDetection: false,
          motionTriggerDelay: 0,
          autoSync: true,
          disableAutoSyncOnCellular: false,
          persistMode: 1,
          preventSuspend: true,
          notificationPriority: 1,
          startOnBoot: true,
          debug: false,
          logLevel: bg.Config.LOG_LEVEL_VERBOSE,
          backgroundPermissionRationale: bg.PermissionRationale(
            title: "Allow {applicationName} to access this device's location even when the app is closed or not in use.",
            message: 'This app collects location data to enable recording your trips to work and calculate distance-travelled.',
            positiveAction: 'Change to "{backgroundPermissionOptionLabel}"',
            negativeAction: 'Cancel',
          ),
          notificationLargeIcon: 'drawable/ic_stat_Expamplelogo',
          notificationSmallIcon: 'drawable/ic_stat_Expamplelogo',
          foregroundService: true,
          notification: bg.Notification(
            title: 'Day Started - Expample',
            text: 'Example started',
            priority: bg.Config.NOTIFICATION_PRIORITY_HIGH,
            sticky: true,
            smallIcon: 'drawable/ic_stat_Expamplelogo', // <-- defaults to app icon
            largeIcon: 'drawable/ic_stat_Expamplelogo',
            channelId: 'my_channel_id',
            actions: [],
          ),
        ),
      );
 
    }
    initPlatformState();
  }

[Optional] Relevant log output

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions