Upload to Firestore in background with Flutter - android

I am using christocracy's flutter_background_geolocation package to build a crowdsensing app. This app relies on the geofencing function of the aforementioned package quite heavily. In the main function, I have implemented a callback function that is as follows (partial code):
void _onGeofence(bg.GeofenceEvent event) async {
await showGeofenceNotification(flutterLocalNotificationsPlugin,
title: "Geofence", body: "$event", id: notification_id);
if (action == "ENTER") {
// update certain variables
BarometerService.startBarometerService();
BarometerService.floorChange.listen((floorChanges) {
// update floor
updateDatabase();
});
}
else if (action == "EXIT") {
// update certain variables
BarometerService.stopBarometerService();
}
updateDatabase();
setState(() {
// update UI
});
}
The code works perfectly when the app is open and in focus. However, when in background, the barometer service stops. The updateDatabase() function is also not carried out as my Firestore console doesn't get updated.
Here is the code for updating the database:
Future updateUserState(String matric, bool inLWN, bool inVaughan, String activity, int confidence, int floor) async {
return await userCollection.document(uid).setData({
'matric': matric,
'inLWN': inLWN,
'inVaughan': inVaughan,
'activity': activity,
'confidence': confidence,
'floor': floor,
});
}
And here is the code for BarometerService (which uses Flutter sensors plugin):
import 'package:sensors/sensors.dart';
static startBarometerService() {
Stream<BarometerEvent> barometer10Events = barometerEvents.throttle(Duration(seconds:PERIOD_SECONDS));
subscription = barometer10Events.listen(onBarometer);
streamController = new StreamController();
}
How do I make my services run even when app is closed or terminated? I have implemented the same code in my headless callback functions (except updating UI), but nothing besides updating my (local) variables and showing local flutter notifications is working.
Headless task for reference:
void headlessTask(bg.HeadlessEvent headlessEvent) async {
print('[BackgroundGeolocation HeadlessTask]: $headlessEvent');
switch(headlessEvent.name) {
case bg.Event.GEOFENCE:
bg.GeofenceEvent geofenceEvent = headlessEvent.event;
onHeadlessGeofence(geofenceEvent);
print('- [Headless] GeofenceEvent: $geofenceEvent');
break;
case bg.Event.ACTIVITYCHANGE:
bg.ActivityChangeEvent event = headlessEvent.event;
onHeadlessActivityChange(event);
print('- [Headless] ActivityChangeEvent: $event');
break;
}
}
onHeadlessGeofence is almost identical to the callback _onGeofence, besides the setState().
The full code can be found here

Related

flutter: Can't detect subscription renewal (in_app_purchase)

The bounty expires in 6 days. Answers to this question are eligible for a +50 reputation bounty.
mr.Penguin wants to draw more attention to this question.
I'm using a tool called in_app_purchase (version: ^3.1.4 ) to manage subscriptions in my app. It's working well, and I'm able to give users access to the product they're subscribing to.
Right now, my app is in test mode. I get emails from Google to let me know if a subscription has been canceled, renewed, or if it's new.
When a subscription is renewed, I want to know about it so I can update the user's account. To do that, I use a method called restorePurchases() to check past purchases. I then compare the date of the latest purchase with the one I have stored in my FireStore database. If the new purchase is more recent than the last one, I update the user's account and give them access to the product they're subscribed to.
Bellow is how I implemented that logic:
initState method:
#override
void initState() {
super.initState();
final Stream purchaseUpdated = _inAppPurchase.purchaseStream;
_subscription = purchaseUpdated.listen(
(purchaseDetailsList) {
loggy.info("I AM LISTENING....");
_listenToPurchaseUpdated(purchaseDetailsList);
},
onDone: () {
_subscription.cancel();
},
onError: (error) {
// handle error here.
},
);
_inAppPurchase.restorePurchases();//here I trigger the purchaseStream each time initState is called
}
_listenToPurchaseUpdated method:
Future<void> _listenToPurchaseUpdated(purchaseDetailsList) async {
for (final PurchaseDetails purchaseDetails in purchaseDetailsList) {
if (purchaseDetails.status == PurchaseStatus.pending) {
showPendingUI();
} else {
if (purchaseDetails.status == PurchaseStatus.error) {
hendleError(purchaseDetails);
} else if (purchaseDetails.status == PurchaseStatus.purchased ||
purchaseDetails.status == PurchaseStatus.restored) {
if (!mounted) return;
final lastTransactionDate =
await SubscriptionService().getLastTransactionDate();
if (lastTransactionDate.isNotEmpty) {
loggy.info(
"last transaction date: $lastTransactionDate",
);
}
loggy.info(
"current transaction date: ${purchaseDetails.transactionDate}",
);
if (lastTransactionDate.isEmpty ||
lastTransactionDate != purchaseDetails.transactionDate) {
final bool isRecentRenewal = DateTime.fromMillisecondsSinceEpoch(
int.parse(purchaseDetails.transactionDate!),
).isAfter(
DateTime.fromMillisecondsSinceEpoch(
int.parse(lastTransactionDate),
),
);
if (isRecentRenewal) {
loggy.info("delivering product in progress....");
await deliverProduct(purchaseDetails);
}
}
}
if (purchaseDetails.pendingCompletePurchase) {
await _inAppPurchase.completePurchase(purchaseDetails);
}
}
}
}
In the above method, I'm just getting the last transactionDate that was stored in FireStore and comparing it with the current transactionDate, this field can be found on:
purchaseDetails.transactionDate
Unfortunately, when the subscription is renewed, I can't detect it in the app, and the transactionDate remains the same:

Flutter workmanager with home_widget working in debug and profile, but not in Android build APK

I have a todo app built in Flutter and intended only for Android. I built a home screen widget for it (using the home_widget package in Flutter) to allow users to see a list of tasks and check them off directly from the widget.
At midnight, the tasks should reset with the new tasks for the day (I used the workmanager package to accomplish this, although I also tried the android_alarm_manager_plus package, with the same results). All of this functionality is working perfectly in debug mode, and even in profile mode (I can't test it in release mode because, according to my understanding, that would remove services and thus the home_widget would not work; however, when I do the build, that doesn't seem to be the problem because the home widget still shows up). BUT! When I build the release APK and submit it to Google Play for internal testing, then download it onto my Pixel 7 (with no power saving modes on, as far as I'm aware), the midnight function does not run. :(
Here's the relevant code:
main_prod.dart
void main() async {
return mainGeneric('Prod Name', ProdFirebaseOptions.currentPlatform, Environment.prod);
}
main_generic.dart
/// Used for Background Updates using Workmanager Plugin
#pragma('vm:entry-point')
void workmanagerCallbackDispatcher() {
Workmanager().executeTask((taskName, inputData) {
if (taskName == 'widgetBackgroundUpdate') {
try {
return Future.wait<void>([
// This is a static Future<void> function from a helper class that resets
// the tasks; it seems to be working when I test it by itself, as well as
// in debug or profile mode.
MidnightService.resetTasks(),
]).then((value) {
return Future.value(true);
});
} catch(err) {
print(err.toString());
throw Exception(err);
}
}
return Future.value(true);
});
}
void _startBackgroundUpdate() async {
if (await MidnightService.shouldUpdateWorkManagerTasks()) {
(await SharedPreferences.getInstance()).setInt('midnight_tasks_update_version', Constants.MIDNIGHT_TASKS_UPDATE_VERSION);
await Workmanager().cancelAll();
DateTime now = DateTime.now();
int nowMillis = now.millisecondsSinceEpoch;
int midnightTonightMillis = DateTime(now?.year ?? 0, now?.month ?? 0, (now?.day ?? 0) + 1).millisecondsSinceEpoch;
int millisUntilMidnight = midnightTonightMillis - nowMillis;
await Workmanager().registerPeriodicTask('midnightUpdate', 'widgetBackgroundUpdate', initialDelay: Duration(milliseconds: millisUntilMidnight), frequency: Duration(days: 1));
}
}
void mainGeneric(String appName, FirebaseOptions firebaseOptions, Environment environment) async {
// Avoid errors caused by flutter upgrade.
WidgetsFlutterBinding.ensureInitialized();
Workmanager().initialize(workmanagerCallbackDispatcher, isInDebugMode: kDebugMode).then((_) => _startBackgroundUpdate());
...
// If this is the first time opening the app with widget functionality.
HomeWidget.getWidgetData<String>('todays_tasks_string', defaultValue: '').then((todaysTasksString) async {
if (todaysTasksString == '') {
List<Task> todaysTasks = await Repositories().taskRepository.getFocusedTasks();
await HomeWidgetUtils.setTodaysTasks(todaysTasks);
return true;
}
return false;
});
Firebase.initializeApp(
name: appName,
options: firebaseOptions,
).then((_) async {
...
});
HomeWidget.registerBackgroundCallback(homeWidgetBackgroundCallback);
runApp(AppConfig(
child: MyApp(),
environment: environment,
appTitle: appName,
));
}
// Called when doing background work initiated from home screen widget
#pragma('vm:entry-point')
Future<void> homeWidgetBackgroundCallback(Uri uri) async {
if (uri.host.startsWith('completetask_')) {
String todaysTasksString = await HomeWidgetUtils.updateTaskById(uri.host.split('_')[1], true);
await HomeWidget.saveWidgetData<String>('todays_tasks_string', todaysTasksString);
await HomeWidget.updateWidget(androidName: 'TodaysTasksWidgetProvider');
}
}
midnight_service.dart
class MidnightService {
...
static Future<bool> shouldUpdateWorkManagerTasks() async {
try {
final prefs = await SharedPreferences.getInstance();
int midnightTasksUpdateVersion = prefs.getInt('midnight_tasks_update_version');
return Constants.MIDNIGHT_TASKS_UPDATE_VERSION > midnightTasksUpdateVersion;
}
catch (e) { print(e); }
return true;
}
}
It might also be valuable to note that, when a user checks off a task from the home screen widget, sometimes the task takes a while to actually be checked off (and sometimes requires the app to be opened before it will execute). However, I figured this is just a slowness issue or something controlled by the OS that I can't do much about.
With all of that, my question is then, why is the workmanager not executing its midnight task?
I've been smashing my head against this for days, so any help and/or advice is greatly appreciated!!

Right way to use useEffect in react native

Here is my sample React component:
const OwnerView = () => {
const [monthlyCharge, setMonthlyCharge] = useState(0)
useEffect(() => {
getPerMonthCharges(ownerPhoneNumber, vehicles.length)
}, [])
async function getPerMonthCharges(ownerPhoneNumber, noOfCars) {
console.log(`inside getPerMonthCharges`);
try {
const serviceProviderChargesDoc = await firestore().collection(`${serviceProviderId}_charges`).doc(`${ownerPhoneNumber}`).get()
if (serviceProviderChargesDoc?.data()?.chargesPerMonth > 0) {
setMonthlyCharge(serviceProviderChargesDoc?.data()?.chargesPerMonth)
return
}
} catch (error) {
console.log(`Error while fetching monthly charge ${error}`);
}
setMonthlyCharge(noOfCars * perMonthGeneralCharge)
console.log(`done with getPerMonthCharges`);
}
}
There is a possibility that OwnerView gets unmounted even before getPerMonthCharges() completes its execution. Therefore in case OwnerView gets unmounted I receive a warning that am doing state update on an unmounted component and this is a non-op. Can someone please highlight what is your observation and right way to write this piece of code?
There are many ways to address this
You can check if the component is still Mounted, a bit ugly approach I agree, but quite a standard one (I would just use something like useAsync from react-use, which essentially does the same, but hides the ugliness)
Move loading logic outside of UI and make part of the global state (Redux, MobX, Apollo, or any other state management library), it would be in lines of separation of concerns and should make your code more readable.
The worst would be to prevent your user from any actions, while content is loading - making your app seem clunky, but React would not complain anymore.
The closest to the right way would be 2, but this can sparkle religious debates and some witch-burning, which I'm not a fan of.
You can refer to this: https://reactjs.org/docs/hooks-effect.html#effects-with-cleanup
You can have a variable to keep track whether your component has unmount, let isMounted = true inside useEffect and set it to false as soon as the component is unmounted.
The code will be:
useEffect(() => {
let isMounted = true;
async function getPerMonthCharges(ownerPhoneNumber, noOfCars) {
console.log(`inside getPerMonthCharges`);
try {
const serviceProviderChargesDoc = await firestore().collection(`${serviceProviderId}_charges`).doc(`${ownerPhoneNumber}`).get()
if (serviceProviderChargesDoc?.data()?.chargesPerMonth > 0 && isMounted) { // add conditional check
setMonthlyCharge(serviceProviderChargesDoc?.data()?.chargesPerMonth)
return
}
} catch (error) {
console.log(`Error while fetching monthly charge ${error}`);
}
if (isMounted) setMonthlyCharge(noOfCars * perMonthGeneralCharge) // add conditional check
console.log(`done with getPerMonthCharges`);
}
getPerMonthCharges(ownerPhoneNumber, vehicles.length)
return () => { isMounted = false }; // cleanup toggles value, if unmounted
}, []);

How can I update a stream from within a StreamBuilder?

Thank you very much in advance for your help! Please let me know if the question is not clear i would be happy to add more details if needed.
I have a Finite State Machine that handles some audio recognition. This FSM is wrapped by a "manager" whose job is to handle the state transitions (processState, nextState). The FSM manager exposes a stream which is updated every time nextState is called
FSM/Manager layout
class FSM_Manager{
StreamController<RecognitionState> _stateStream =
StreamController<RecognitionState>();
Sink<RecognitionState> get _inState => _stateStream.sink;
Stream<RecognitionState> get outState => _stateStream.stream;
RecognitionState _currentState, _previousState;
void setState(RecognitionState state) {
_previousState = _currentState;
_currentState = state;
_addCurrentStateToStream();
}
void _addCurrentStateToStream() {
_inState.add(_currentState);
}
Future nextState() async {
_currentState.nextState(this);
}
Future processState(itemToRecognize) async {
await _currentState.processState(itemToRecognize);
}
}
abstract class BaseState {
RecognitionStateID get stateID; //enum with each state's ID
Future processState(itemToRecognize);
Future nextState(FSM_Manager manager);
}
class FSM_State1 implements BaseState{
bool isSuccess = false;
void processState(itemToRecognize) async {
isSuccess = await performRecognition(itemToRecognize);
}
void nextState (FSM_Manager fsmManager) {
if(isSuccess){
// go to next State
fsmManager.setState(NEXT_STATE);
} else {
//go to some other state
fsmManager.setState(SOME_OTHER_STATE);
}
}
}
class FSM_State2 implements BaseState{
bool isSuccess = false;
void processState(itemToRecognize) async {
isSuccess = await performRecognition(itemToRecognize);
}
void nextState () {
if(isSuccess){
// go to next State
} else {
// go to another State
}
}
}
I have a screen (Stateful Widget) which uses a StreamBuidler to listen to the "outState" stream in order to rebuild the screen with the information in the new State.
Stateful Widget
class _RecognitionScreenState extends State<RecognitionScreen> {
ItemToRecognize item;
var currStateiD;
FSM_Manager _fsmManager;
RecognitionScreenState(
ItemToRecognize item, FSM_Manager fsmManager) {
this.item = item;
this._fsmManager = fsmManager;
}
#override
Widget build(BuildContext context) {
String outString = '';
return StreamBuilder<RecognitionState>(
stream: _stateContext.outState,
builder: (BuildContext context,
AsyncSnapshot<RecognitionState> snapshot) {
if (snapshot.hasData) {
outString = snapshot.data.stateID.toString();
return Text(outString);
} else {
return Text('');
}
});
}
}
Now, I do not know where/how to call processState and nextState from, I cant do it from the build method so i Tried to use initState() and didUpdateWidget in the StatefulWidget so that the states are processed in the beginning and after every build respectively. This approach didnt work, the nextState method was never called. I feel like im missing something trivial but i just dont see where to call those functions from outside of the Stateful Widget in order to trigger a rebuild only after the state has changed
Thanks again for your help
EDIT
I apologize for the confusion,
I added the BaseState definition (just an abstract class with some method so that i dont forget to implement them)
the nextState method takes an FSM_Manager as a parameter and calls setState on success or failure and sets the next state
the States "implement" the BaseState class, they dont "extend" it
It's difficult for me to tell from your code because I don't know what your BaseState does but inside your builder - presumably from some event or callback - you would do:
inState.add(<--Some RecognitionState-->);
This would trigger the StreamBuilder to rebuild.
If everything else in your code is put together properly.

Run flutter code when android application class starts

I'm making a plugin for Flutter to handle fcm messages using an android native library.
As we know when a message is received by FCM, it starts the app (It's application class) and runs the codes within Application#onCreate block, so we can run native code when app starts by fcm in the background.
My question is, is it possible to run flutter code at that time when application starts?
For instance if the message was received:
Application class:
public class Application extends FlutterApplication {
#Override
public void onCreate() {
super.onCreate();
// Start flutter engine
// Invoke a dart code in the Plugin using methodChannel or etc.
}
}
Short answer, Yes
You can call a Dart method in background using it's handle key.
1. Register your plugin in the background
Implement a custom application class (override FlutterApplication)
public class MyApp extends FlutterApplication implements PluginRegistry.PluginRegistrantCallback {
#Override
public void registerWith(io.flutter.plugin.common.PluginRegistry registry) {
// For apps using FlutterEmbedding v1
GeneratedPluginRegistrant.registerWith(registry);
// App with V2 will initialize plugins automatically, you might need to register your own however
}
}
Remember to register the class in the AndroidManifest by adding android:name=".MyApp" to <application> attributes.
What is embedding v2?
2. Create a setup function as top level function in your flutter code
/// Define this TopLevel or static
void _setup() async {
MethodChannel backgroundChannel = const MethodChannel('flutter_background');
// Setup Flutter state needed for MethodChannels.
WidgetsFlutterBinding.ensureInitialized();
// This is where the magic happens and we handle background events from the
// native portion of the plugin.
backgroundChannel.setMethodCallHandler((MethodCall call) async {
if (call.method == 'handleBackgroundMessage') {
final CallbackHandle handle =
CallbackHandle.fromRawHandle(call.arguments['handle']);
final Function handlerFunction =
PluginUtilities.getCallbackFromHandle(handle);
try {
var dataArg = call.arguments['message'];
if (dataArg == null) {
print('Data received from callback is null');
return;
}
await handlerFunction(dataArg);
} catch (e) {
print('Unable to handle incoming background message.\n$e');
}
}
return Future.value();
});
3. Create a top level callback that will get the background message and calls it
_bgFunction(dynamic message) {
// Message received in background
// Remember, this will be a different isolate. So, no widgets
}
4. Get the handle key of the background function and setup and send it to native via MethodChannel
// dart:ui needed
CallbackHandle setup PluginUtilities.getCallbackHandle(_setup);
CallbackHandle handle PluginUtilities.getCallbackHandle(_bgFunction);
_channel.invokeMethod<bool>(
'handleFunction',
<String, dynamic>{
'handle': handle.toRawHandle(),
'setup': setup.toRawHandle()
},
);
5. Save them into SharedPref in the native side
public void onMethodCall(MethodCall call, MethodChannel.Result result) {
String methodName = call.method
if (methodName == "handleFunction") {
long handle = call.argument("handle");
long setup = call.argument("setup");
// save them
}
}
6. When background is awaken, start a background isolate
FlutterMain.ensureInitializationComplete(context, null)
val appBundlePath = FlutterMain.findAppBundlePath()
val flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(setupHandleYouHadSaved)
FlutterNativeView backgroundFlutterView = FlutterNativeView(context, true)
val args = FlutterRunArguments()
args.bundlePath = appBundlePath
args.entrypoint = flutterCallback.callbackName
args.libraryPath = flutterCallback.callbackLibraryPath
backgroundFlutterView?.runFromBundle(args)
// Initialize your registrant in the app class
pluginRegistrantCallback?.registerWith(backgroundFlutterView?.pluginRegistry)
7. When your plugin is registered, create a background channel and pass it to
val backgroundChannel = MethodChannel(messenger, "pushe_flutter_background")
8. Call the setup method that would call and give the message to you callback
private fun sendBackgroundMessageToExecute(context: Context, message: String) {
if (backgroundChannel == null) {
return
}
val args: MutableMap<String, Any?> = HashMap()
if (backgroundMessageHandle == null) {
backgroundMessageHandle = getMessageHandle(context)
}
args["handle"] = backgroundMessageHandle
args["message"] = message
// The created background channel at step 7
backgroundChannel?.invokeMethod("handleBackgroundMessage", args, null)
}
The sendBackgroundMessageToExecute will execute the dart _setup function and pass the message and callback handle. In the step 2, callback will be called.
Note: There are still certain corner cases you may want to consider (for instance thread waiting and ...). Checkout the samples and see the source code.
There are several projects which support background execution when app is started in the background.
FirebaseMessaging
Pushe
WorkManager
I did it a different, simpler way compared to Mahdi's answer. I avoided defining an additional entrypoint/ callback, using PluginUtilities, callback handles, saving handles in SharedPreferences, passing messages with handles between dart and platform, or implementing a FlutterApplication.
I was working on a flutter plugin (so you don't have to worry about this if you use my library for push notifications 😂), so I implement FlutterPlugin. If I want to do background processing and the Flutter app isn't running, I just launch the Flutter app without an Activity or View. This is only necessary on Android, since the FlutterEngine/ main dart function runs already runs when a background message is received in an iOS app. The benefit is that this is the same behaviour as iOS: a Flutter app is always running when the app is launched, even if there is no app shown to the user.
I launch the application by using:
flutterEngine = new FlutterEngine(context, null);
DartExecutor executor = flutterEngine.getDartExecutor();
backgroundMethodChannel = new MethodChannel(executor, "com.example.package.background");
backgroundMethodChannel.setMethodCallHandler(this);
// Get and launch the users app isolate manually:
executor.executeDartEntrypoint(DartExecutor.DartEntrypoint.createDefault());
I did this to implement background push notification handling in a library, ably_flutter. It seems to work well. The FlutterEngine/ application is launched only when the application is not already running. I do this by keeping track of the activity (using ActivityAware):
if (isApplicationInForeground) {
// Send message to Dart side app already running
Intent onMessageReceivedIntent = new Intent(PUSH_ON_MESSAGE_RECEIVED);
onMessageReceivedIntent.putExtras(intent.getExtras());
LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
} else if (AblyFlutterPlugin.isActivityRunning) {
// Flutter is already running, just send a background message to it.
Intent onMessageReceivedIntent = new Intent(PUSH_ON_BACKGROUND_MESSAGE_RECEIVED);
onMessageReceivedIntent.putExtras(intent.getExtras());
LocalBroadcastManager.getInstance(context).sendBroadcast(onMessageReceivedIntent);
} else {
// No existing Flutter Activity is running, create a FlutterEngine and pass it the RemoteMessage
new PushBackgroundIsolateRunner(context, asyncCompletionHandlerPendingResult, message);
}
Then, I just use a separate MethodChannel to pass the messages back to the dart side. There's more to this parallel processing (like telling the Java side that the App is running/ ready. Search for call.method.equals(pushSetOnBackgroundMessage) in the codebase.). You can see more about the implementation PushBackgroundIsolateRunner.java at ably_flutter. I also used goAsync inside the broadcast receiver to extend the execution time from 10s to 30s, to be consistent with iOS 30s wall clock time.
You can use a headless Runner to run dart code from an Application class (or service, broadcast receiver etc).
There's a good in depth article on how to implement this: https://medium.com/flutter/executing-dart-in-the-background-with-flutter-plugins-and-geofencing-2b3e40a1a124
According to my knowledge we have to call a class GeneratedPluginRegistrant.registerWith(this); at oncreate method where flutter code has to run.
If you mean you want to run some arbitrary Dart code in the background you can use the this plugin we created which really facilitates the use of background work.
You can register a background job that should be executed at a given point in time and it will call back in to your Dart code where you can run some code in the background.
//Provide a top level function or static function.
//This function will be called by Android and will return the value you provided when you registered the task.
//See below
void callbackDispatcher() {
Workmanager.defaultCallbackDispatcher((echoValue) {
print("Native echoed: $echoValue");
return Future.value(true);
});
}
Workmanager.initialize(callbackDispatcher)
Then you can schedule them.
Workmanager.registerOneOffTask(
"1",
"simpleTask"
);
The String simpleTask will be returned in the callbackDispatcher function once it starts running in the background.
This allows for you to schedule multiple background jobs and identify them by this id.

Categories

Resources