I am attempting to track down a nasty crash bug in the NativeScript Mapbox plugin. On android any app built with that plugin crashes onResume.
To rule out a bug in the Mapbox GL Native Android library, I followed the install steps to create a very simple example app in Java that loads and displays a map.
That sample app does not crash regardless how many times I pause and resume.
I notice that the NativeScript Mapbox plugin does not seem to call the recommended Mapbox lifecycle hooks and that there are a number of crashes reported in the Mapbox native issues list where the answer is "follow the lifecycle hook guidelines".
So my next thought was to see if I can translate the Java code directly into NativeScript following the recommended life cycle hooks (exactly as it's done in the sample app). This way I would be able to determine if the crash is because of the lifecycle hooks not being called correctly or because of some more esoteric NativeScript issue.
The working Java activity is:
package com.amapboxtest.mapboxtest;
import android.support.annotation.NonNull;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
import com.mapbox.mapboxsdk.Mapbox;
import com.mapbox.mapboxsdk.maps.MapView;
import com.mapbox.mapboxsdk.maps.MapboxMap;
import com.mapbox.mapboxsdk.maps.OnMapReadyCallback;
import com.mapbox.mapboxsdk.maps.Style;
import android.support.design.widget.FloatingActionButton;
import android.support.design.widget.Snackbar;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.Menu;
import android.view.MenuItem;
import android.util.Log;
public class MainActivity extends AppCompatActivity {
private MapView mapView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Log.d( "test","onCreate()");
Mapbox.getInstance(this, "MAPBOX_ACCESS_TOKEN_HERE");
setContentView(R.layout.activity_main);
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
FloatingActionButton fab = findViewById(R.id.fab);
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG)
.setAction("Action", null).show();
}
});
mapView = findViewById(R.id.mapView);
mapView.onCreate(savedInstanceState);
mapView.getMapAsync(new OnMapReadyCallback() {
@Override
public void onMapReady(@NonNull MapboxMap mapboxMap) {
Log.d( "test","onMapReady()");
mapboxMap.setStyle(Style.MAPBOX_STREETS, new Style.OnStyleLoaded() {
@Override
public void onStyleLoaded(@NonNull Style style) {
Log.d( "test", "onStyleLoaded()");
// Map is set up and the style has loaded. Now you can add data or make other map adjustments
}
});
}
});
}
@Override
public void onStart() {
Log.d( "test", "onStart()");
super.onStart();
mapView.onStart();
}
@Override
public void onResume() {
Log.d( "test", "onResume");
super.onResume();
mapView.onResume();
}
@Override
public void onPause() {
Log.d( "test", "onPause");
super.onPause();
mapView.onPause();
}
@Override
public void onStop() {
Log.d( "test", "onStop");
super.onStop();
mapView.onStop();
}
@Override
public void onLowMemory() {
Log.d( "test", "onLowMemory()");
super.onLowMemory();
mapView.onLowMemory();
}
@Override
protected void onDestroy() {
Log.d( "test","onDestroy");
super.onDestroy();
mapView.onDestroy();
}
@Override
protected void onSaveInstanceState(Bundle outState) {
Log.d( "test", "onSaveInstanceState()");
super.onSaveInstanceState(outState);
mapView.onSaveInstanceState(outState);
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
getMenuInflater().inflate(R.menu.menu_main, menu);
return true;
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
//noinspection SimplifiableIfStatement
if (id == R.id.action_settings) {
return true;
}
return super.onOptionsItemSelected(item);
}
}
My initial attempt at a NativeScript translation:
/**
*
* @link https://github.com/NativeScript/android-runtime/issues/981
*/
import {setActivityCallbacks, AndroidActivityCallbacks} from "tns-core-modules/ui/frame";
import * as application from "tns-core-modules/application";
declare const com, java, org;
@JavaProxy("com.amapboxtest.MainActivity")
class Activity extends android.support.v7.app.AppCompatActivity {
public isNativeScriptActivity;
private _callbacks: AndroidActivityCallbacks;
private mapView: any;
public onCreate(savedInstanceState: android.os.Bundle): void {
console.log( "Activity::onCreate()" );
this.isNativeScriptActivity = true;
if (!this._callbacks) {
setActivityCallbacks(this);
}
this._callbacks.onCreate(this, savedInstanceState, super.onCreate );
console.log( "Activity::onCreate(): after _callbacks.onCreate()" );
let layout;
let resourceId;
console.log( "Activity::onCreate(): before getting layout" );
// this fails.
try {
layout = this.getResources().getIdentifier( "activity_main", "layout", this.getPackageName() );
} catch( e ) {
console.error( "Unable to get layout:", e );
throw e;
}
this.setContentView(layout);
console.log( "Activity::onCreate(): before getting resourceId" );
try {
resourceId = this.getResources().getIdentifier( "mapView", "id", this.getPackageName() );
} catch( e ) {
console.error( "Unable to get resourceId:", e );
throw e;
}
this.mapView = this.findViewById( resourceId );
console.log( "Activity::onCreate(): after findViewById()" );
this.mapView.onCreate( savedInstanceState );
console.log( "Activity::onCreate(): after this.mapView.onCreate( savedInstanceState" );
com.mapbox.mapboxsdk.Mapbox.getInstance( application.android.context, 'SET_ACCESS_TOKEN_HERE' );
console.log( "Activity::onCreate(): after getInstance()" );
// modelled after mapbox.android.ts in the Nativescript-Mapbox plugin.
this.mapView.getMapAsync(
new com.mapbox.mapboxsdk.maps.OnMapReadyCallback({
onMapReady: mapboxMap => {
console.log( "onMapReady()");
this.mapView.addOnDidFinishLoadingStyleListener(
new com.mapbox.mapboxsdk.maps.MapView.OnDidFinishLoadingStyleListener({
onDidFinishLoadingStyle : style => {
console.log( "style loaded" );
}
})
);
let builder = new com.mapbox.mapboxsdk.maps.Style.Builder();
const Style = com.mapbox.mapboxsdk.constants.Style;
mapboxMap.setStyle(
builder.fromUrl( Style.LIGHT )
);
}
})
);
} // end of onCreate()
// -------------------------------------------------------
public onSaveInstanceState(outState: android.os.Bundle): void {
console.log( "Activity::onSaveInstanceState()" );
this._callbacks.onSaveInstanceState(this, outState, super.onSaveInstanceState);
this.mapView.onSaveInstanceState( outState );
}
// -------------------------------------------------------
public onStart(): void {
console.log( "Activity::onStart()" );
this._callbacks.onStart(this, super.onStart);
this.mapView.onStart();
}
// -------------------------------------------------------
public onStop(): void {
console.log( "Activity::onStop()" );
this._callbacks.onStop(this, super.onStop);
this.mapView.onStop();
}
// -------------------------------------------------------
public onDestroy(): void {
console.log( "Activity::onDestroy()" );
this._callbacks.onDestroy(this, super.onDestroy);
this.mapView.onDestroy();
}
// -------------------------------------------------------
public onBackPressed(): void {
console.log( "Activity::onBackPressed()" );
this._callbacks.onBackPressed(this, super.onBackPressed);
}
// -------------------------------------------------------
public onRequestPermissionsResult(requestCode: number, permissions: Array<string>, grantResults: Array<number>): void {
console.log( "Activity::onCRequestPermissionResult()" );
this._callbacks.onRequestPermissionsResult(this, requestCode, permissions, grantResults, undefined /*TODO: Enable if needed*/);
}
// -------------------------------------------------------
public onActivityResult(requestCode: number, resultCode: number, data: android.content.Intent): void {
console.log( "Activity::onActivityResult()" );
this._callbacks.onActivityResult(this, requestCode, resultCode, data, super.onActivityResult);
}
}
// END
I copied over the gradle dependencies from the Java app and added them to App_Resources/Android/app.gradle.
I also copied over the app/src/main/res/layout/activity_main.xml and content_main.xml files.
Initially, it would error out on content_main.xml because apparently android.support.constraint.ConstraintLayout is not supported by Nativescript. So, basing off what I see in the NativeScript Mapbox plugin I changed it to android.widget.FrameLayout. I tested this in the java app and it seems to work.
However, now I am stuck at a runtime exception that I do not understand:
System.err: java.lang.RuntimeException: Unable to start activity ComponentInfo{com.amapboxtest.nsmapboxtest/com.amapboxtest.MainActivity}: com.tns.NativeScriptException:
System.err: Calling js method onCreate failed
System.err:
System.err: Error: android.view.InflateException: Binary XML file line #9: Binary XML file line #12: Error inflating class com.mapbox.mapboxsdk.maps.MapView
System.err: Caused by: android.view.InflateException: Binary XML file line #12: Error inflating class com.mapbox.mapboxsdk.maps.MapView
System.err: Caused by: java.lang.reflect.InvocationTargetException
System.err: java.lang.reflect.Constructor.newInstance0(Native Method)
System.err: java.lang.reflect.Constructor.newInstance(Constructor.java:334)
System.err: android.view.LayoutInflater.createView(LayoutInflater.java:647)
It's dying at this line in the Activity:
layout = this.getResources().getIdentifier( "activity_main", "layout", this.getPackageName() );
NativeScript apparently doesn't expose R.id hence the getResources() calls.
I am very new to NativeScript and native Android development and am having to dive further into the weeds than I would like, but I need to get this crash issue resolved and getting this simple translation to work is the next critical step.
Any guidance on what I am doing wrong in translating this example into NativeScript would be greatly appreciated. I have to imagine there's something simple that I am missing.
I have put both the working Java example and the broken NativeScript example up on Github.