transistorsoft/nativescript-background-geolocation-lt

webpack build throwing `Unable to instantiate service com.transistorsoft.backgroundgeolocation.HeadlessJobService`

Closed this issue · 29 comments

Liooo commented

Your Environment

  • Plugin version: 1.9.0-beta.4
  • Android
  • OS version: 7.0
  • Device manufacturer / model: AVD emulator
  • Nativescript version (tns info):
nativescript: 4.2.3
tns-core-modules: 4.2.0
tns-android: 4.2.0
nativescript-dev-webpack: 0.16.0
webpack: 4.6.0
  • Plugin config
    const config = {
      debug: false,
      startOnBoot: false,

      // eternal location tracking
      stopOnTerminate: false,
      enableHeadless: true,

      // accuracy and interval
      desiredAccuracy: 0,
      stationaryRadius: 10,
      distanceFilter: 100,
      activityRecognitionInterval: 10000,

      // http
      url: this.httpService.getUrl('/location/current'),
      autoSync: true,
      method: 'PATCH',
      headers: this.httpService.getHeaders({asObject: true}),
      httpRootProperty: '.',
      locationTemplate: '{ "lat":<%= latitude %>, "lng":<%= longitude %> }',

      // iOS
      preventSuspend: true,

      // android
      foregroundService: true,
      notificationTitle: '...'
    };

Expected Behavior

Headless mode works when built with webpack

Actual Behavior

Raises error below and die when built with webpack
(works fine when I repeat the same step with non-webpack built app)

System.err: java.lang.RuntimeException: Unable to instantiate service com.transistorsoft.backgroundgeolocation.HeadlessJobService: java.lang.ClassNotFoundException: Didn't find class "com.transistorsoft.backgroundgeolocation.HeadlessJobService" on path: DexPathList[[zip file "/data/app/com.myapp/base.apk"],nativeLibraryDirectories=[/data/app/com.myapp/lib/x86, /data/app/com.myapp/base.apk!/lib/x86, /system/lib, /vendor/lib]]
System.err: 	at android.app.ActivityThread.handleCreateService(ActivityThread.java:3147)
System.err: 	at android.app.ActivityThread.-wrap5(ActivityThread.java)
System.err: 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1550)
System.err: 	at android.os.Handler.dispatchMessage(Handler.java:102)
System.err: 	at android.os.Looper.loop(Looper.java:154)
System.err: 	at android.app.ActivityThread.main(ActivityThread.java:6077)
System.err: 	at java.lang.reflect.Method.invoke(Native Method)
System.err: 	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
System.err: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
System.err: Caused by: java.lang.ClassNotFoundException: Didn't find class "com.transistorsoft.backgroundgeolocation.HeadlessJobService" on path: DexPathList[[zip file "/data/app/com.myapp/base.apk"],nativeLibraryDirectories=[/data/app/com.myapp/lib/x86, /data/app/com.myapp/base.apk!/lib/x86, /system/lib, /vendor/lib]]
System.err: 	at dalvik.system.BaseDexClassLoader.findClass(BaseDexClassLoader.java:56)
System.err: 	at java.lang.ClassLoader.loadClass(ClassLoader.java:380)
System.err: 	at java.lang.ClassLoader.loadClass(ClassLoader.java:312)
System.err: 	at android.app.ActivityThread.handleCreateService(ActivityThread.java:3144)
System.err: 	... 8 more

Steps to Reproduce

  1. run app with tns run android --bundle
  2. open the app
  3. kill the app from task list

Debug logs

the gist is the error message above.

Liooo commented

also confirmed the same on a real android device as well

Liooo commented

The minimum gist of the code is like the following:

@Injectable()
export class GeolocationService {
  ...

  onLocation = (location) => {
    console.log('location!!');
  }

  headlessTask(event, completionHandler) {
    switch(event.name) {
      case 'location':
        BackgroundGeolocation.changePace(true);
        this.onLocation(event);
        break;
    }
    completionHandler();
  }

  public startService()
    BackgroundGeolocation.watchPosition(() => { .... })

    BackgroundGeolocation.on('location', this.onLocation);
    BackgroundGeolocation.on('heartbeat', this.onHeartBeat);
    BackgroundGeolocation.on('http', this.onHttp);

    if (isAndroid) {
      BackgroundGeolocation.registerHeadlessTask(this.headlessTask.bind(this));
    }
  }
}

// the root component of the app
@Component({
    selector: 'root',
    moduleId: module.id,
    templateUrl: 'root.component.html'
})
export class RootComponent {
  constructor(private geolocationService: GeolocationService) {}
  
  ngOnInit(){
    const config = {...}
    BackgroundGeolocation.ready(config, ({enabled}) => {
      if (!enabled) {
        BackgroundGeolocation.start(this.onStartDone.bind(this));
      } else {
        this.onStartDone();
      }
    }
  }

  onStartDone() {
    this.geolocationService.startService()
  }
}

@christocracy

please tell me if you need anymore information, thanks

Liooo commented

@christocracy

Please give a look at this, we're stuck here

Liooo commented

@christocracy

Ok made a bare minimum reproducible repo here.
https://github.com/Liooo/geo-loc-test

bootstraped from the template tns-template-blank-ng, with the minimum geolocation headless task. The app dies with the same error when killing the activity.

I understand you're busy, but please at least post a comment or show whatever reaction, this is not some contribution based open source software but we pay not small amount of money for this. Thanks.

The plugin uses Nativescript's Static Binding Generator for the Headless classes

This instructs NativeScript to auto-generate corresponding native Java classes:

  • HeadlessBroadcastReceiver.java
  • HeadlessJobService.java.
$ tns build android

When built with tns build android --bundle, Nativescript fails to auto-generate these classes. I have no experience using Webpack with Nativescript. It's Nativescript's responsibility to auto-generate these classes.

@Liooo as a solution craete manual import to the worker file used in the plugin.

// e.g in your app.component.ts
import "nativescript-background-geolocation-lt/HeadlessJobService";

@transistorsoft to resolve the issue for all users the best approach would be to include explicit imports of the background services files within your source code.

// e.g. in the background-geolocation.ts
import "nativescript-background-geolocation-lt/HeadlessJobService";

Thanks for the tip, @NickIliev

@Liooo Try latest 1.9.0.

Liooo commented

@christocracy Thank you, I'm checking it out.

Liooo commented

@christocracy @NickIliev

The same code now dies like this.

screen shot 2018-12-20 at 8 38 35

and the logcat says someting like

12-20 09:18:06.188 15794 15794 E AndroidRuntime: java.lang.RuntimeException: Unable to instantiate service com.transistorsoft.backgroundgeolocation.HeadlessJobService: java.lang.NullPointerException: Attempt to read from field 'int com.tns.Runtime.currentObjectId' on a null object reference
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at android.app.ActivityThread.handleCreateService(ActivityThread.java:3147)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at android.app.ActivityThread.-wrap5(ActivityThread.java)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at android.app.ActivityThread$H.handleMessage(ActivityThread.java:1550)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at android.os.Handler.dispatchMessage(Handler.java:102)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at android.os.Looper.loop(Looper.java:154)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at android.app.ActivityThread.main(ActivityThread.java:6077)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at java.lang.reflect.Method.invoke(Native Method)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at com.android.internal.os.ZygoteInit$MethodAndArgsCaller.run(ZygoteInit.java:866)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:756)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: Caused by: java.lang.NullPointerException: Attempt to read from field 'int com.tns.Runtime.currentObjectId' on a null object reference
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at com.tns.Runtime.initInstance(Runtime.java:624)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at com.transistorsoft.backgroundgeolocation.HeadlessJobService.<init>(HeadlessJobService.java:12)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at java.lang.Class.newInstance(Native Method)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	at android.app.ActivityThread.handleCreateService(ActivityThread.java:3144)
12-20 09:18:06.188 15794 15794 E AndroidRuntime: 	... 8 more
12-20 09:18:06.189  1627  3010 W ActivityManager:   Force finishing activity engineer.liooo.test/com.tns.ErrorReportActivity

I hate NativeScript.

Liooo commented

lol, don't say that we all just trying to build something for somebody

@NickIliev Do you think you can figure out how to fix?

@Liooo, @NickIliev has provided all that's required in pointing out the problem. You need not cc him on this anymore.

Liooo commented

Okay then will be waiting for your work, thanks man

In the meantime, show me your tns info

Liooo commented

@christocracy

It says my transistor soft subscription expires in 5days or something, so can you finish the fix by then or please at least give me the bug fixed version if it takes longer.

your test app work for me.

$ tns info
✔ Getting NativeScript components versions information...
✔ Component nativescript has 5.1.0 version and is up to date.
✔ Component tns-core-modules has 5.1.1 version and is up to date.
✔ Component tns-android has 5.1.0 version and is up to date.
✔ Component tns-ios has 5.1.0 version and is up to date.

$ tns run android --bundle

Refreshing application on device FA6970312056...
Successfully synced application engineer.liooo.test on device FA6970312056.
JS: Angular is running in the development mode. Call enableProdMode() to enable the production mode.
JS: [BackgroundGeolocation] onActivityDestroyedEvent - com.intentfilter.androidpermissions.PermissionsActivity@6edbabb
JS: location
JS: location
JS: location
Liooo commented

@christocracy

You need to kill the app from the task list and get the headless mode to kick in.

Liooo commented

we should see console.log("headless") if it's working

Liooo commented

@christocracy

any updates?

Liooo commented

@christocracy
Happy new year.
How's this thing going.

Your latest issue is actually in your application code here.

You cannot registerHeadlessTask in your application components. The Headless Mode instructions explicitly tell you to install this in the root app.ts file. Since you're using Angular, I moved it to main.ts and it works. Application components are not rendered in the headless application instance.

// this import should be first in order to load some required settings (like globals and reflect-metadata)
import { platformNativeScriptDynamic } from "nativescript-angular/platform";
+import { BackgroundGeolocation } from "nativescript-background-geolocation-lt";

import { AppModule } from "./app/app.module";

+BackgroundGeolocation.registerHeadlessTask((event, completionHandler) => {
+	console.log('[HeadlessTask] ', event.name, event);
+	completionHandler();
+});

platformNativeScriptDynamic().bootstrapModule(AppModule);

However, there is one minor bug fix I added to the plugin, adding a missing return statement when the headlessTask failed to be registered (due to the method being placed in the wrong file, as in your test app). Published as 1.9.1.

Liooo commented

@christocracy

Oh thanks for that, I'll try!

You cannot registerHeadlessTask in your application components.

Then what if the library users want to call Angular service inside headlessTask which probably is often the case?

Then what if the library users want to call Angular service inside headlessTask which probably is often the case?

If you have some methods stored in an external file that you wish to execute in the headless context, simply import them. How you bring some methods into the context where the headless-task is registered is not the plugin's concern.

Liooo commented

Angular's got all these ecosystem with DI and service instantiation and stuff so it doesn't work just by importing the class. But as long as location tracking headless part is working I can find some other workaround so it's ok. Thanks!

If there's a method in some Service that you can't manage to make accessible to your app in the headless context, simply extract some particular method into a simple external library file and import that into both the Service and Headless contexts. Javascript 101.

Liooo commented

yea that's what I'll do thanks.

But if the user has whatever large angular codebase it's normal to call a service from another service for example, and it's not easy nor good for the code maintainability to extract only headless mode accessed logic out of them. So definitely better if it work in angular ecosystem

that's between you and angular. You'll have to explore and see what parts of an Angular app are booted within the Headless context.