transistorsoft / flutter_background_fetch

Periodic callbacks in the background for both IOS and Android. Includes Android Headless mechanism
MIT License
571 stars 167 forks source link

[BUG] Can't use a MethodChannel.invoke method in a headless task #73

Closed Skyost closed 4 years ago

Skyost commented 4 years ago

Your Environment Windows 10 x64.

[√] Android toolchain - develop for Android devices (Android SDK version 28.0.3) [√] Android Studio (version 3.5) [√] Connected device (1 available)

• No issues found!

* Plugin config
```dart
BackgroundFetchConfig(
  minimumFetchInterval: 15, // <- Testing purposes
  stopOnTerminate: false,
  enableHeadless: true,
  requiredNetworkType: BackgroundFetchConfig.NETWORK_TYPE_ANY,
)

To Reproduce Steps to reproduce the behavior:

  1. Open the app so that background_fetch can register a headless task.
  2. Try to use a MethodChannel.invoke method in the task.

Debug logs

2020-01-31 01:20:13.519 6843-6863/fr.skyost.timetable.dev E/flutter: [ERROR:flutter/lib/ui/ui_dart_state.cc(157)] Unhandled Exception: MissingPluginException(No implementation found for method account.get on channel my_channel)
    #0      MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:319:7)
    <asynchronous suspension>
    #1      AndroidUserRepository._read (package:unicaen_timetable/model/user.dart:147:53)
    <asynchronous suspension>
    #2      UserRepository.get (package:unicaen_timetable/model/user.dart:105:27)
    #3      headlessSyncTask (package:unicaen_timetable/main.dart:24:76)
    #4      _headlessCallbackDispatcher.<anonymous closure> (package:background_fetch/background_fetch.dart:437:15)
    #5      MethodChannel._handleAsMethodCall (package:flutter/src/services/platform_channel.dart:402:55)
    #6      MethodChannel.setMethodCallHandler.<anonymous closure> (package:flutter/src/services/platform_channel.dart:370:54)
    #7      _DefaultBinaryMessenger.handlePlatformMessage (package:flutter/src/services/binding.dart:200:33)
    #8      _invoke3.<anonymous closure> (dart:ui/hooks.dart:303:15)
    #9      _rootRun (dart:async/zone.dart:1126:13)
    #10     _CustomZone.run (dart:async/zone.dart:1023:19)
    #11     _CustomZone.runGuarded (dart:async/zone.dart:925:7)
    #12     _invoke3 (dart:ui/hooks.dart:302:10)
    #13     _dispatchPlatformMessage (dart:ui/hooks.dart:162:5)

Additional context:

Here's the code I'm trying to use in a headless task :

MethodChannel('my_channel').invoke('method')

Oh and the method method is registered in the Android side.

christocracy commented 4 years ago

Have you Upgraded your pre 1.12 Android Project?

Skyost commented 4 years ago

In fact I've created this project using flutter v1.12. I'm gonna check your instructions just to be sure.

EDIT : Wow, it seems I forgot to do that step, is it related to my problem ?

christocracy commented 4 years ago

Show me where you’re creating this method channel on the native side

Skyost commented 4 years ago

In my main activity :

override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
  GeneratedPluginRegistrant.registerWith(flutterEngine)
  MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler{
    call, result -> handleMethod(call, result)
  }
}

private fun handleMethod(call: MethodCall, result: MethodChannel.Result) {
  when(call.method) {
    "method" -> {
      // Do things...
      result.success(true)
      return
    }
    else -> result.notImplemented()
  }
}

So I tried to add the part I forgot in my Application class but I got a compile error saying "Application.kt: (16, 48): Type mismatch: inferred type is PluginRegistry but FlutterEngine was expected"

Here's my Application.kt :

class Application : FlutterApplication(), PluginRegistry.PluginRegistrantCallback {
    override fun onCreate() {
        super.onCreate()
        BackgroundFetchPlugin.setPluginRegistrant(this)
    }

    override fun registerWith(registry: PluginRegistry) {
        GeneratedPluginRegistrant.registerWith(registry) // <-- Line 16
    }
}

Oh and I added in my AndroidManifest.xml :

<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools"
    package="fr.skyost.timetable">

    <uses-permission
        android:name="android.permission.AUTHENTICATE_ACCOUNTS"
        android:maxSdkVersion="22" />
    <uses-permission
        android:name="android.permission.GET_ACCOUNTS"
        android:maxSdkVersion="22" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission
        android:name="android.permission.MANAGE_ACCOUNTS"
        android:maxSdkVersion="22" />
    <uses-permission android:name="android.permission.WRITE_SYNC_SETTINGS" />

    <!-- io.flutter.app.FlutterApplication is an android.app.Application that
         calls FlutterMain.startInitialization(this); in its onCreate method.
         In most cases you can leave this as-is, but you if you want to provide
         additional functionality it is fine to subclass or reimplement
         FlutterApplication and put your custom class here. -->
    <application
        tools:replace="android:label"
        android:name=".Application"
        android:label="@string/app_name"
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher">

        <!-- ... -->

        <!-- Don't delete the meta-data below.
        This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
        <meta-data
            android:name="flutterEmbedding"
            android:value="2" />
    </application>
</manifest>
christocracy commented 4 years ago

"headless" means there is no MainActivity. MainActivity IS the "head" of your application.

You must create your method-channel in the Application.kt.

christocracy commented 4 years ago

Another name for "headless" could be "activity-less"

Skyost commented 4 years ago

Oh that's okay thanks for your answer ! I will definitely do that.

And please can you tell me why I have a compile error saying "Application.kt: (16, 48): Type mismatch: inferred type is PluginRegistry but FlutterEngine was expected" ?

christocracy commented 4 years ago

Where did you get that code for your Application.kt, it’s wrong. That’s pre-1.12 code

Skyost commented 4 years ago

Hmm, got it from here. What code should I use now ?

christocracy commented 4 years ago

Yes, well then read that doc again and ask yourself “what version of Flutter sdk am I using?”.

The plugin no longer requires Application extension for anything.

If you want to create a custom Application class, consult with Fluttet sdk docs, not here.

Skyost commented 4 years ago

Alright.

Skyost commented 4 years ago

I closed this issue as this is not a bug but the intended behavior but how can I create my method channel in my Application.kt as I need to have a binary messenger (which is provided by the flutter engine in configureFlutterEngine) ?

EDIT : I think I need to create a FlutterNativeView by myself and use it to communicate with my Dart code.

So I've done it this way in my Application.kt :

class Application : FlutterApplication() {
    companion object {
        const val CHANNEL = "my_channel"

        fun handleMethod(call: MethodCall, result: MethodChannel.Result, context: Context) {
            when(call.method) {
                "method" -> {
                    // Do things...
                    result.success(true)
                }
                else -> result.notImplemented()
            }
        }
    }

    override fun onCreate() {
        super.onCreate()

        FlutterMain.ensureInitializationComplete(this, null)
        val nativeView = FlutterNativeView(this, true)
                MethodChannel(nativeView.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler{
            call, result -> handleMethod(call, result, this)
        }
    }
}

And I've removed my method channel implementation in my MainActivity. Now I have errors in my UI thread.

EDIT 2 :

My second attempt, Application.kt :

class Application : FlutterApplication(), PluginRegistry.PluginRegistrantCallback {
    companion object {
        const val CHANNEL = "my_channel"

        fun handleMethod(call: MethodCall, result: MethodChannel.Result, context: Context) {
            when(call.method) {
                "method" -> {
                    // Do things...
                    result.success(true)
                }
                else -> result.notImplemented()
            }
        }
    }

    override fun onCreate() {
        super.onCreate()
        BackgroundFetchPlugin.setPluginRegistrant(this)
    }

    override fun registerWith(registry: PluginRegistry) {
        FlutterMain.ensureInitializationComplete(applicationContext, null)

        val engine = FlutterEngine(applicationContext)

        val entrypoint: DartEntrypoint = createDefault()
        engine.dartExecutor.executeDartEntrypoint(entrypoint)

        MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            handleMethod(call, result, this)
        }

        GeneratedPluginRegistrant.registerWith(engine)
    }
}

And MainActivity.kt :

class MainActivity : FlutterActivity() {
    override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
        GeneratedPluginRegistrant.registerWith(flutterEngine)
        MethodChannel(flutterEngine.dartExecutor.binaryMessenger, Application.CHANNEL).setMethodCallHandler{
            call, result ->
            Application.handleMethod(call, result, this)
        }
    }
}

Let's see if it works... Still no success : Unhandled Exception: MissingPluginException(No implementation found for method method on channel my_channel).

christocracy commented 4 years ago

You can see an example of a suitable FlutterApplication extension in the /example app.

Where a Context instance is required within FlutterApplication, you can use this within onCreate or getApplicationContext().

Skyost commented 4 years ago

Thank you @christocracy, will check it out soon !

I know for getApplicationContext() or this, I'm currently using getApplicationContext().

Skyost commented 4 years ago

Just checked the example FlutterApplication. So, I see that you're not implementing PluginRegistrantCallback at all so I've removed it. My onCreate method now looks like this :

override fun onCreate() {
  super.onCreate()
  FlutterMain.ensureInitializationComplete(applicationContext, null)

  val engine = FlutterEngine(applicationContext)

  val entrypoint: DartEntrypoint = createDefault()
  engine.dartExecutor.executeDartEntrypoint(entrypoint)

  GeneratedPluginRegistrant.registerWith(engine)
  MethodChannel(engine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
            handleMethod(call, result, this)
}

(Well, the only thing I did was putting everything that was in the registerWith method back in the onCreate method). I get the following error :

══╡ EXCEPTION CAUGHT BY SERVICES LIBRARY ╞══════════════════════════════════════════════════════════
The following MissingPluginException was thrown while activating platform stream on channel
com.transistorsoft/flutter_background_fetch/events:
MissingPluginException(No implementation found for method listen on channel
com.transistorsoft/flutter_background_fetch/events)
When the exception was thrown, this was the stack:
#0      MethodChannel.invokeMethod (package:flutter/src/services/platform_channel.dart:319:7)
<asynchronous suspension>
#1      EventChannel.receiveBroadcastStream.<anonymous closure> (package:flutter/src/services/platform_channel.dart:517:29)
#3      EventChannel.receiveBroadcastStream.<anonymous closure> (package:flutter/src/services/platform_channel.dart:503:64)
#8      BackgroundFetch.configure (package:background_fetch/background_fetch.dart:242:20)

EDIT : Okay, let's try another approach. I will try to use a different channel name for headless tasks. Now headless tasks are using the my_channel.headless channel and non-headless tasks are using the my_channel channel. So the onCreate method in Application.kt looks like :

override fun onCreate() {
  super.onCreate()
  FlutterMain.ensureInitializationComplete(this, null)

  val engine = FlutterEngine(this)
  MethodChannel(engine.dartExecutor.binaryMessenger, "$CHANNEL.headless").setMethodCallHandler { call, result -> handleMethod(call, result, this) }
}

And the MainActivity.kt configureFlutterEngine method looks like :

override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
  GeneratedPluginRegistrant.registerWith(flutterEngine)
  MethodChannel(flutterEngine.dartExecutor.binaryMessenger, Application.CHANNEL).setMethodCallHandler{ call, result -> Application.handleMethod(call, result, this) }
}

Nop : Unhandled Exception: MissingPluginException(No implementation found for method method on channel my_channel)

Skyost commented 4 years ago

I think I'm gonna do all my operations outside my register function as you did here.

christocracy commented 4 years ago

Your code above isn't going to work. It's far more tricky to create a custom MethodChannel in the headless context. It's easy in the foreground case, in the context of the MainActivity.

However, I did some experimenting. I can offer an event-handler on BackgroundFetch's HeadlessTask class to offer you a reference to its FlutterEngine. In the Application#onCreate, you'll do something like this:

NOTE: This onInitialized event handler isn't yet public. I have it working on my local copy only.

HeadlessTask.onInitialized(new HeadlessTask.InitializedCallback() {
            @Override
            public void onStarted(FlutterEngine engine) {
                Log.d("TSBackgroundFetch", "********* engine started: " + engine);
                new MethodChannel(engine.getDartExecutor().getBinaryMessenger(), "channel_foo").setMethodCallHandler(new MethodChannel.MethodCallHandler() {
                    @Override
                    public void onMethodCall(MethodCall call, MethodChannel.Result result) {
                        Log.d("TSBackgroundFetch", "**************** Application method call handler: " + call.method);
                        result.success(true);
                    }
                });
            }
        });

main.dart:

/// Create a custom MethodChannel
const MethodChannel _methodChannel = const MethodChannel("channel_foo");

/// This "Headless Task" is run when app is terminated.
void backgroundFetchHeadlessTask(String taskId) async {
  print("[BackgroundFetch] Headless event received: $taskId");

  // execute a method on custom EventChannel
  _methodChannel.invokeMethod('test');
}
02-01 16:01:18.381 29525 29525 D TSBackgroundFetch: *********************** MainApplication
02-01 16:01:18.392 29525 29525 D TSBackgroundFetch: - Background Fetch event received
02-01 16:01:18.427 29525 29525 D TSBackgroundFetch: - finish: com.transistorsoft.fetch
02-01 16:01:18.427 29525 29525 D TSBackgroundFetch: - jobFinished
02-01 16:01:18.441 29525 29525 D TSBackgroundFetch: HeadlessJobService onStartJob: com.transistorsoft.fetch
02-01 16:01:18.441 29525 29525 D TSBackgroundFetch: 💀 [HeadlessTask com.transistorsoft.fetch]
02-01 16:01:18.720 29525 29525 D TSBackgroundFetch: [HeadlessTask] waiting for client to initialize
02-01 16:01:18.943 29525 29577 I flutter : Observatory listening on http://127.0.0.1:40081/OUS9S3jVjvo=/
02-01 16:01:19.694 29525 29525 I TSBackgroundFetch: $ initialized
02-01 16:01:19.695 29525 29525 D TSBackgroundFetch: ********* engine started: io.flutter.embedding.engine.FlutterEngine@f639884
02-01 16:01:19.753 29525 29563 I flutter : [BackgroundFetch] Headless event received: com.transistorsoft.fetch
02-01 16:01:19.767 29525 29525 D TSBackgroundFetch: **************** Application method call handler: test
christocracy commented 4 years ago

Btw, why do you even need to write a custom MethodChannel? What are you doing on the native side that you can't do via some 3rd-party plugin?

Skyost commented 4 years ago

However, I did some experimenting. I can offer an event-handler on BackgroundFetch's HeadlessTask class to offer you a reference to its FlutterEngine. In the Application#onCreate, you'll do something like this:

Wow great ! I will wait for you to release it then.

Btw, why do you even need to write a custom MethodChannel? What are you doing on the native side that you can't do via some 3rd-party plugin?

In fact I need to access a device account. I need to manage it (create, update and remove it from my app) and with background_fetch, I get the account (username / password) then I synchronize it with my server.
And I think there is no plugin that can do that so far.

Thanks for taking time to help me @christocracy 🎉

pento commented 4 years ago

I've been running into the same problem, and coincidentally I noticed that the fix is in development. In my initial testing, it seems to be working well. 🙂

Skyost commented 4 years ago

@pento Out of curiosity, are you using background_fetch as a mean to synchronize your data at a given interval ? If so, is it working well for you ?

christocracy commented 4 years ago

I have a large refactor coming in branch BGTaskScheduler that’s almost ready for release.

In addition to migrating deprecated iOS fetch api to new iOS 13 BGTaskScheduler, This version introduces a new method #scheduleTask for running arbitrary oneshot/ periodic tasks.

BacgroundGeolocationConfig also introduces new Android-only option #forceAlarmManager to bypass JobScheduler (which is optimized to conserve battery) for more precise minimumFetchInterval.

christocracy commented 4 years ago

@Skyost If you want to try the branch BGTaskScheduler, you can implement this in your MainApplication (I use Java, you'll have to convert to Kotlin), modifying the method-channel name "channel_foo" as desired.

Install from Git

dependencies:
  background_fetch:
    git:
      url: https://github.com/transistorsoft/flutter_background_fetch
      ref: BGTaskScheduler
public class Application  extends FlutterApplication {
    @Override
    public void onCreate() {        
        super.onCreate();

        HeadlessTask.onInitialized(new HeadlessTask.OnInitializedCallback() {
            @Override
            public void onInitialized(FlutterEngine engine) {
                new MethodChannel(engine.getDartExecutor().getBinaryMessenger(), "channel_foo").setMethodCallHandler(new MethodChannel.MethodCallHandler() {
                    @Override
                    public void onMethodCall(@NonNull MethodCall call, @NonNull MethodChannel.Result result) {
                        result.success(true);
                    }
                });
            }
        });
    }
}
christocracy commented 4 years ago

@Skyost If you try BGTaskScheduler branch, you'll also want to read the Breaking Changes in CHANGELOG

Skyost commented 4 years ago

@christocracy Thank you so much ! But as I am not in a hurry, I think I'm gonna wait for a stable release. Your help and your code snippets are greatly appreciated tho.

christocracy commented 4 years ago

@Skyost I'd really appreciate some feedback on implementing this branch. If it works for you, it's pretty much ready to go as-is.

Skyost commented 4 years ago

@christocracy Oh that's okay, no problem. I'm gonna add it in my project and I will give you some feedback at the end of the week (something like thursday or friday). But I can only test on Android, not on iOS.

pento commented 4 years ago

Out of curiosity, are you using background_fetch as a mean to synchronize your data at a given interval ? If so, is it working well for you ?

Yah, my use case is to fetch data from an API every 30 minutes, in a background_fetch headless task. It then has to do a MethodChannel.invokeMethod() call, to update an Android home screen widget. I was getting the same exception as you described earlier when the app is closed.

I'm running into a separate issue with getting the current location in the headless task, it appears to be a known issue in the geolocator package. (It's only a hobby project which may generate revenue some time in the future, so I may just have to live with the bugs until I can replace my hacky workarounds with @christocracy's excellent flutter_background_geolocation package. 🙂)

Skyost commented 4 years ago

@christocracy
Just tested and it works perfectly on Android, thank you ! I still have a little question : is it possible to use third party flutter plugins in a headless task ?

EDIT : Just tested and it works perfectly. Thank you again !

christocracy commented 4 years ago

shared_preferences is a 3rd party plugin I use in the example, right?

Skyost commented 4 years ago

@christocracy You've made a point 😁