transistorsoft/flutter_background_geolocation

Strange sending of smartphone status data dynamically in extras object

Closed this issue · 13 comments

Your Environment

  • Plugin version:4.16.4
  • Platform:Android
  • OS version:14
  • Device manufacturer / model:Samsung galaxy s21 ultra
  • Flutter info (flutter doctor):✓] Flutter (Channel stable, 3.24.2, on Mac OS X 10.15.7 19H2026 darwin-x64, locale es-CO)
    [✓] Android toolchain - develop for Android devices (Android SDK version 33.0.1)
    [!] Xcode - develop for iOS and macOS (Xcode 12.4)
    ✗ Flutter requires Xcode 14 or higher.
    Download the latest version or update via the Mac App Store.
    ! CocoaPods 1.12.1 out of date (1.13.0 is recommended).
    CocoaPods is a package manager for iOS or macOS platform code.
    Without CocoaPods, plugins will not work on iOS or macOS.
    For more info, see https://flutter.dev/to/platform-plugins
    To update CocoaPods, see https://guides.cocoapods.org/using/getting-started.html#updating-cocoapods
    [✓] Chrome - develop for the web
    [✓] Android Studio (version 2020.3)
    [✓] VS Code (version 1.95.3)
    [✓] Connected device (2 available)
    ! Device 192.168.1.7:5555 is offline.
    [✓] Network resources
  • Plugin config:
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:app_movil_protegos2/api.dart';

class LocationService {
  static const platform = MethodChannel('co.itfusion.protegos/screen_state');
  static String _smartphoneBlocked = '1'; // Iniciar como bloqueado por defecto

  static Future<void> initPlatformState(BuildContext context) async {
    try {
      SharedPreferences prefs = await SharedPreferences.getInstance();
      String? userId = prefs.getString("userId") ?? '';
      String? tiposViajes = prefs.getString("tipos_viajes") ?? '';
      String? company = prefs.getString("company") ?? '';
      int? ayuda = prefs.getInt("ayuda");
      String? seguimientoMetros = prefs.getString("seguimiento_metros");
      double? seguimientoMetrosDouble = seguimientoMetros != null ? double.tryParse(seguimientoMetros) : null;

      // Redirigir al login si no hay usuario registrado
      if (userId.isEmpty) {
        Navigator.pushNamed(context, "/login");
        return;
      }

      await checkScreenLocked(); // Sincronizar estado inicial de la pantalla

      _configureBackgroundGeolocation(userId, tiposViajes, company, ayuda, seguimientoMetrosDouble);

      platform.setMethodCallHandler((MethodCall call) async {
        switch (call.method) {
          case 'screenLocked':
            _updateSmartphoneBlockedState('1', 'Teléfono bloqueado');
            break;
          case 'screenUnlocked':
            _updateSmartphoneBlockedState('0', 'Teléfono desbloqueado');
            break;
          default:
            print("Método desconocido: ${call.method}");
        }
      });
    } catch (e) {
      print("Error inicializando el estado de la plataforma: $e");
    }
  }

  static Future<void> checkScreenLocked() async {
    try {
      final bool result = await platform.invokeMethod('isScreenLocked');
      _smartphoneBlocked = result ? '1' : '0';
      print(result ? 'Teléfono bloqueado valor 1 _smartphoneBlocked:' : 'Teléfono desbloqueado valor 0  _smartphoneBlocked:');
      print(_smartphoneBlocked);
      //SharedPreferences prefs = await SharedPreferences.getInstance();
      //prefs.setString("smartphoneBlocked", _smartphoneBlocked);
    } on PlatformException catch (e) {
      print("Error al obtener el estado de pantalla: ${e.message}");
    }
  }

  static void _configureBackgroundGeolocation(
    String userId, String? tiposViajes, String? company, int? ayuda, double? seguimientoMetrosDouble) {
    bg.BackgroundGeolocation.onLocation(_onLocation, _onLocationError);
    bg.BackgroundGeolocation.onMotionChange(_onMotionChange);
    bg.BackgroundGeolocation.onActivityChange(_onActivityChange);
    bg.BackgroundGeolocation.onProviderChange(_onProviderChange);
    bg.BackgroundGeolocation.onConnectivityChange(_onConnectivityChange);
    bg.BackgroundGeolocation.onHttp(_onHttp);

    bg.BackgroundGeolocation.ready(bg.Config(
      reset: true,
      debug: false,
      logLevel: bg.Config.LOG_LEVEL_VERBOSE,
      desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
      distanceFilter: 5,
      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',
      ),
      url: API_URL + "device/locations2",
      extras: {
        "userId": userId,
        "smartphone_blocked": _smartphoneBlocked,
        "tipos_viajes": tiposViajes,
        "company": company,
        "ayuda": ayuda,
      },
      stopOnTerminate: false,
      startOnBoot: true,
      enableHeadless: true,
    )).then((bg.State state) {
      print("[ready] ${state.toMap()}");
    }).catchError((error) {
      print('[ready] ERROR: $error');
    });
  }

  static void _updateSmartphoneBlockedState(String state, String message) {
    _smartphoneBlocked = state;
    print(message);
  }

  static void _onLocation(bg.Location location) async {
    await checkScreenLocked(); // Asegurar sincronización del estado
    print('Estado después de checkScreenLocked: $_smartphoneBlocked');
    print('[location] antes update campo blocked - $location');
    // Actualizar el campo smartphone_blocked en la estructura de location
    //location.extras['smartphone_blocked'] = _smartphoneBlocked;
    String extras_smartphone = _smartphoneBlocked;
    location.extras;

    print('[location] despues update campo blocked- $location');
    String variable = "0";
  }

  static void _onLocationError(bg.LocationError error) {
    print('[location] ERROR - $error');
  }

  static void _onMotionChange(bg.Location location) {
    print('[motionchange] - $location');
  }

  static void _onActivityChange(bg.ActivityChangeEvent event) {
    print('[activitychange] - $event');
  }

  static void _onHttp(bg.HttpEvent event) {
    print('[${bg.Event.HTTP}] - $event');
  }

  static void _onProviderChange(bg.ProviderChangeEvent event) {
    print('[providerchange] - $event');
  }

  static void _onConnectivityChange(bg.ConnectivityChangeEvent event) {
    print('[connectivitychange] - $event');
  }
}

Expected Behavior

That when sending data to the server, it takes the correct value of the smartphone status in the _smartphoneBlocked variable and assigns it correctly to the smartphone_blocked field of the extras object.

Actual Behavior

This variable should send the value 0 if the smartphone is unlocked, or 1 if it is locked: _smartphoneBlocked in the smartphone_blocked field of the extras object. When I debug the code and call the checkScreenLocked function within the onLocation function, it correctly takes the state of the smartphone and assigns it to the variable: _smartphoneBlocked for example if it is unlocked in this onLocation function this variable _smartphoneBlocked prints me a: 0, but when printing in this same onLocation function what the location object has, I see that the variable smartphone_blocked: 1.

Steps to Reproduce

  1. How do I do the code I mentioned before in the "Plugin config" section? First I call this method MethodChannel('co.itfusion.protegos/screen_state'); that I define in the mainActivity.kt file.
  2. Then with this I take in this code the state (0 = unlocked, 1 = unlocked) of the smartphone from this function and that I call from the plugin's onLocation function: static Future checkScreenLocked() async {
    try {
    final bool result = await platform.invokeMethod('isScreenLocked');
    _smartphoneBlocked = result ? '1' : '0';
    print(result ? 'Phone locked value 1 _smartphoneBlocked:' : 'Phone unlocked value 0 _smartphoneBlocked:');
    print(_smartphoneBlocked);
    //SharedPreferences prefs = await SharedPreferences.getInstance();
    //prefs.setString("smartphoneBlocked", _smartphoneBlocked);
    } on PlatformException catch (e) {
    print("Error getting screen state: ${e.message}");
    }
    }

Context

Can I update the data in the smartphone_blocked field of the extras object at the time of sending the location to the server so that it sends this correct value perhaps by adding something like this inside the onLocation function? location.extras['smartphone_blocked'] = _smartphoneBlocked;
How can I do this?

Debug logs

Logs
PASTE_YOUR_LOGS_HERE

Can I update the data in the smartphone_blocked field of the extras object at the time of sending the location to the server so that it sends this correct value perhaps by adding something like this inside the onLocation function?

No. Config.extras (and any Config option) are changed only by using .setConfig (see api docs). The updated extras will be applied only to the next recorded location.

by the time .onLocation has been fired, the http request has likely already been initiated.

you could, of course, force a “next” location to be recorded by calling .getCurrentPosition.

Thank you!
I have read many documents of the great number of functions that the API has with good documentation, but this is something very complex because sometimes I do not find an example of how to call the getCurrentPosition function that you tell me inside the onLocation function. I have also seen the source code examples of the application that you have and I do not see one that calls this getCurrentPosition function inside onLocation. I put this code inside onLocation:
bg.Location location = await bg.BackgroundGeolocation.getCurrentPosition(
extras: {
//"userId": userId,
"smartphone_blocked": _smartphoneBlocked,
//"tipos_viajes": tiposViajes,
//"company": company,
// "ayuda": ayuda,
"band": "1"
});
But it automatically sends locations every second to backend and then blocks the smartphone. Also, after removing this code that I mentioned, and based on what you mentioned and reading the API, I put this code inside the onLocation function to see if it updated the parameter that I mentioned: await bg.BackgroundGeolocation.setConfig(bg.Config(
autoSync: true,
extras: {"smartphone_blocked": _smartphoneBlocked, "band": "1"},
//url: "http://tracker.transistorsoft.com/locations/transistor-flutter-test",
//params: deviceParams
));
And this does nothing. Would you be so kind as to please tell me how I can call this that you say .getCurrentPosition?

When you call .getCurrentPosition, it does not return the first location it receives. It fetches (default) 3 locations, letting the GPS radio "heat up". Each of those locations is fired to the .onLocation event having location.sample set to true. Location "samples" or NOT sent to the HTTP service.

See getCurrentPosition options.samples to control the number of samples recorded.

The options.extras provided to .getCurrentPosition are NOT permanently applied to Config.extras. These are applied only for this .getCurrentPosition request.

await bg.BackgroundGeolocation.setConfig(bg.Config(
  "extras": {
    "foo": "bar"
  }
));

await bg.BackgroundGeolocation.getCurrentPosition({
  "samples": 1
});

I understand what you're saying and I've already looked at the getCurrentPosition documentation again and I understand that I only need to set the samples attribute as you tell me and you gave me the example. I also set the smartphone state variable inside the extras object of the setConfig function to change the correct smartphone state and now the onLocation function looks like this:

static void _onLocation(bg.Location location0) async {
  await checkScreenLocked(); // Ensure state synchronization
  //location.extras['smartphone_blocked'] = _smartphoneBlocked;
  print('[location] before field blocked: $location0');
  
  await bg.BackgroundGeolocation.setConfig(bg.Config(
  //autoSync: true,
  extras: {"smartphone_blocked": _smartphoneBlocked, "band": "1"},
  ));
  
  await bg.BackgroundGeolocation.getCurrentPosition(
  samples: 1,
);

//print('[location] after field blocked: $location');

}

But I don't understand then what the getCurrentPosition function is for in this code. Excuse me for my questions, I have read the API quite a bit, tried many examples but I do not understand this process. My change that you recommend should I make in the onLocation function using these 2 functions that I mentioned? What else should I do?

Do NOT call .getCurrentPosition within .onLocation!! That will create an infinite loop!

So, where can i call getCurrenPosition how you told me: "force a “next” location to be recorded by calling .getCurrentPosition"? Beside of call getCurrentPosition, i suppose, do i need to send this data returned by getCurrentPosition, set my variables in extra field with setConfig, or which are the general steps for "force" a "next" location?

If it were me, I’d initiate a repeating timer to check the value at some frequency. When the value changes, only then would I do my thing with .setConfig / .getCurrentPosition

Much easier to understand with this explanation of yours my friend! So finally, inside the timer I imagine with a timer that runs every minute "won't this consume important battery of the smartphone?", I would call the getCurrentPosition function, and with the data of this position I compare against the previous position and with that if the state of the smartphone changes, then I use setConfig to change the value of the state of the smartphone? if so I thank you immensely for your help and I will not bother you anymore.

And only run the timer while your app is in the foreground. Actually, why aren’t you just setting up an app lifecycle listener to do this when the app goes to background?

Hi,
I did this what you told me:"setting up an app lifecycle listener to do this when the app goes to background", and this seems update good and this seems to update the state of the _smartphoneBlocked variable fine, but this value never changes when sent by your plugin to my server, it always sends me a :0. For example this was my listener:

@override
void didChangeAppLifecycleState(AppLifecycleState state) {
isStateSynced = false; // Force new synchronization on lifecycle changes
if (state == AppLifecycleState.paused || state == AppLifecycleState.resumed) {
checkScreenLocked();
}
}

The code I'm trying for this service is the following:

import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_background_geolocation/flutter_background_geolocation.dart' as bg;
import 'package:flutter/services.dart';
import 'package:flutter/material.dart';
import 'package:app_movil_protegos2/api.dart';

class LocationService with WidgetsBindingObserver {
  static const platform = MethodChannel('co.itfusion.protegos/screen_state');
  static String _smartphoneBlocked = '1'; // Iniciar como bloqueado por defecto
  static bool isObserverAdded = false; // Bandera para evitar múltiples observadores
  static bool isStateSynced = false; // Bandera para sincronizar estado de pantalla

  /// Inicializar el servicio y configurar los listeners
  static Future<void> initPlatformState(BuildContext context) async {
    try {
      if (!isObserverAdded) {
        WidgetsBinding.instance.addObserver(LocationService());
        isObserverAdded = true;
      }

      SharedPreferences prefs = await SharedPreferences.getInstance();
      String? userId = prefs.getString(SharedPrefsKeys.userId) ?? '';
      String? tiposViajes = prefs.getString(SharedPrefsKeys.tiposViajes) ?? '';
      String? company = prefs.getString(SharedPrefsKeys.company) ?? '';
      int? ayuda = prefs.getInt(SharedPrefsKeys.ayuda);
      String? seguimientoMetros = prefs.getString(SharedPrefsKeys.seguimientoMetros);
      double? seguimientoMetrosDouble =
          seguimientoMetros != null ? double.tryParse(seguimientoMetros) : null;

      if (userId.isEmpty) {
        Navigator.pushNamed(context, "/login");
        return;
      }

      if (seguimientoMetrosDouble == null || seguimientoMetrosDouble <= 0) {
        print("Error: 'seguimiento_metros' no válido. Usando valor predeterminado.");
        seguimientoMetrosDouble = 10.0;
      }

      await checkScreenLocked();

      _configureBackgroundGeolocation(
        userId, tiposViajes, company, ayuda, seguimientoMetrosDouble,
      );

      platform.setMethodCallHandler((MethodCall call) async {
        switch (call.method) {
          case 'screenLocked':
            _updateSmartphoneBlockedState('1', 'Teléfono bloqueado');
            break;
          case 'screenUnlocked':
            _updateSmartphoneBlockedState('0', 'Teléfono desbloqueado');
            break;
          default:
            print("Método desconocido: ${call.method}");
        }
      });
    } catch (e) {
      print("Error inicializando el estado de la plataforma: $e");
    }
  }

  /// Listener del ciclo de vida de la app
  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    isStateSynced = false; // Forzar nueva sincronización en cambios de ciclo de vida
    if (state == AppLifecycleState.paused || state == AppLifecycleState.resumed) {
      checkScreenLocked();
    }
  }

  /// Verifica el estado inicial de la pantalla
  static Future<void> checkScreenLocked() async {
    if (isStateSynced) return;
    try {
      final bool result = await platform.invokeMethod('isScreenLocked');
      _smartphoneBlocked = result ? '1' : '0';
      isStateSynced = true;
      print(result
          ? 'Teléfono bloqueado valor 1 _smartphoneBlocked: $_smartphoneBlocked'
          : 'Teléfono desbloqueado valor 0 _smartphoneBlocked: $_smartphoneBlocked');
    } on PlatformException catch (e) {
      print("Error al obtener el estado de pantalla: ${e.message}");
      _smartphoneBlocked = '0'; // Fallback en caso de error
    }
  }

  /// Configuración del plugin BackgroundGeolocation
  static void _configureBackgroundGeolocation(
    String userId,
    String? tiposViajes,
    String? company,
    int? ayuda,
    double? seguimientoMetrosDouble,
  ) {
    bg.BackgroundGeolocation.onLocation(_onLocation, _onLocationError);
    bg.BackgroundGeolocation.onMotionChange(_onMotionChange);
    bg.BackgroundGeolocation.onActivityChange(_onActivityChange);
    bg.BackgroundGeolocation.onProviderChange(_onProviderChange);
    bg.BackgroundGeolocation.onConnectivityChange(_onConnectivityChange);
    bg.BackgroundGeolocation.onHttp(_onHttp);

    bg.BackgroundGeolocation.ready(bg.Config(
      reset: true,
      debug: true,
      logLevel: bg.Config.LOG_LEVEL_VERBOSE,
      desiredAccuracy: bg.Config.DESIRED_ACCURACY_HIGH,
      distanceFilter: 10,
      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',
      ),
      url: API_URL + "device/locations2",
      extras: {
        "userId": userId,
        "smartphone_blocked": _smartphoneBlocked,
        "tipos_viajes": tiposViajes,
        "company": company,
        "ayuda": ayuda,
        "band": "0"
      },
      stopOnTerminate: false,
      startOnBoot: true,
      enableHeadless: true,
    )).then((bg.State state) {
      print("[ready] ${state.toMap()}");
    }).catchError((error) {
      print('[ready] ERROR: $error');
    });
  }

  /// Actualiza el estado del bloqueo de pantalla
  static void _updateSmartphoneBlockedState(String state, String message) {
    _smartphoneBlocked = state;
    print('Actualización estado automático mensaje: $message Estado: $_smartphoneBlocked');
  }

  /// Eventos del plugin BackgroundGeolocation
  static void _onLocation(bg.Location location) async {
    await checkScreenLocked(); // Asegurar sincronización del estado
    print('[location] before field blocked: $location');
  }

  static void _onLocationError(bg.LocationError error) {
    print('[location] ERROR - $error');
  }

  static void _onMotionChange(bg.Location location) {
    print('[motionchange] - $location');

  }

  static void _onActivityChange(bg.ActivityChangeEvent event) {
    print('[activitychange] - $event');
  }

  static void _onHttp(bg.HttpEvent event) {
    print('[${bg.Event.HTTP}] - $event');
  }

  static void _onProviderChange(bg.ProviderChangeEvent event) {
    print('[providerchange] - $event');
  }

  static void _onConnectivityChange(bg.ConnectivityChangeEvent event) {
    print('[connectivitychange] - $event');
  }
}

/// Constantes para claves de SharedPreferences
class SharedPrefsKeys {
  static const String userId = "userId";
  static const String tiposViajes = "tipos_viajes";
  static const String company = "company";
  static const String ayuda = "ayuda";
  static const String seguimientoMetros = "seguimiento_metros";
}

Bbut I don't know how to update the _smartphoneBlocked field inside your plugin, maybe I should use the config something like this inside an event like onMotionChange? something maybe like this?

bg.BackgroundGeolocation.setConfig(bg.Config(
extras: {
"userId": userId,
"smartphone_blocked": _smartphoneBlocked,
"tipos_viajes": tiposViajes,
"company": company,
"ayuda": ayuda,
"band": "0",
},
));

But I don't know how to update the _smartphoneBlocked

use .setConfig to change ANY config option, including Config.extras.

Every location recorded AFTER .setConfig completes, will have those new extras stamped upon it. It's really simple.

you should be calling .setConfig in your checkScreenLocked method (and only when the value actually changes).

thanks a lot Chris!!! awesome plugin and help from you!