Skip to content

Commit

Permalink
Use unified initializer to workaround crash on startup (#2165)
Browse files Browse the repository at this point in the history
* Use unified initializer to workaround crash on startup (#2116)

* wip

* wip

* wip

* Finalize MapboxInitializer

* update docs

* Rs/extend kdz unified initializer (#2122)

* Avoid repeating code and simplify init logic for optional SDKs

* Do not store AppInitializer in static block

---------

Co-authored-by: Ramon <[email protected]>

* Fixes / improvements

* Clearer delta

* Startup dependency

* PR fixes

* Private api file

* Gather some info when crashing (#2123)

* PR fixes

* minor

* Move exception out of companion

* Descope Nav and Search

* Manifest

* Log time since initializer was called

* Fix issue where initializer exception was overwritten

* Do not reschedule on failure

* Remove code related to Nav and Search and clean up docs

* Upgraded metalava.txt

* Ktlint

* Moved everything to `sdk-base` to be closer to the gl-native and common imports

* Address PR comments

* Make MapboxInitializerException internal

* Added doc for companion object for Dokka to pass. Make init JVM static

* Fix explicit snapshot version, allow override (publish_android_snapshot)

* Update the AWS CLI (publish_android_snapshot)

* Downgrad startup lib to align with common

* changelog

* Downgrade startup lib

---------

Co-authored-by: Ramon <[email protected]>

* rebase

---------

Co-authored-by: Ramon <[email protected]>
  • Loading branch information
kiryldz and jush authored Dec 8, 2023
1 parent a446782 commit 9278967
Show file tree
Hide file tree
Showing 10 changed files with 205 additions and 4 deletions.
8 changes: 5 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,18 @@ Mapbox welcomes participation and contributions from everyone.

# 10.16.3
## Bug fixes 🐞
* Downgrade minimum required `compileSDK` from 31 to 30.
* Fix the `java.lang.UnsatisfiedLinkError` exception happening on the startup.
* Fix widgets flickering due to race condition if they are animated.
* Fix widgets not showing on some zoom levels.
* Fix map being black when using widgets (e.g. when `MapDebugOptions.TILE_BORDERS` option is enabled).
# 10.yy.zz
## Features ✨ and improvements 🏁
* Downgrade minimum required `compileSDK` from 31 to 30.

## Dependencies
* Update Mapbox gestures library to 0.9.1

## Known issues
* The `java.lang.UnsatisfiedLinkError` exception on startup has been fixed when using Mapbox Maps SDK __only__. If other Mapbox products are used (Navigation, Search) - loading navigation / search native libraries might still crash. Mapbox Navigation / Search SDKs fixes will be released separately.

# 10.16.2 November 08, 2023
## Bug fixes 🐞
* Fix a crash because of non-exported runtime-registered broadcasts receivers for apps targeting SDK 34.
Expand Down
2 changes: 2 additions & 0 deletions buildSrc/src/main/kotlin/Project.kt
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ object Dependencies {
const val androidxRecyclerView = "androidx.recyclerview:recyclerview:${Versions.androidxRecyclerView}"
const val androidxCoreKtx = "androidx.core:core-ktx:${Versions.androidxCore}"
const val androidxAnnotations = "androidx.annotation:annotation:${Versions.androidxAnnotation}"
const val androidxStartup = "androidx.startup:startup-runtime:${Versions.androidxStartup}"
const val androidxInterpolators = "androidx.interpolator:interpolator:${Versions.androidxInterpolator}"
const val androidxConstraintLayout = "androidx.constraintlayout:constraintlayout:${Versions.androidxConstraintLayout}"
const val androidxEspresso = "androidx.test.espresso:espresso-core:${Versions.androidxEspresso}"
Expand Down Expand Up @@ -119,6 +120,7 @@ object Versions {
const val androidxCore = "1.6.0" // Latest version that supports compile SDK 30
const val androidxFragmentTesting = "1.3.6" // Latest version that supports compile SDK 30
const val androidxAnnotation = "1.1.0"
const val androidxStartup = "1.1.0"
const val androidxAppcompat = "1.3.0"
const val androidxTest = "1.4.0"
const val androidxArchCoreTest = "2.1.0"
Expand Down
1 change: 1 addition & 0 deletions gradle/sdk-registry.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ afterEvaluate {
dryRun = false
publish = true
snapshot = isSnapshot
override = isSnapshot
publishMessage = "cc @mapbox/maps-android"
publications = [currentComponent.name]
excludeFromRootProject = project.ext.mapboxRegistryExcludeFromRootProject
Expand Down
15 changes: 15 additions & 0 deletions sdk-base/api/PublicRelease/metalava.txt
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,21 @@ package com.mapbox.maps {
@kotlin.RequiresOptIn(level=kotlin.RequiresOptIn.Level, message="This API is experimental. It may be changed in the future without notice.") @kotlin.annotation.Retention(kotlin.annotation.AnnotationRetention) @kotlin.annotation.Target(allowedTargets={kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget, kotlin.annotation.AnnotationTarget}) public @interface MapboxExperimental {
}

@RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) public final class MapboxInitializer implements androidx.startup.Initializer<java.lang.Boolean> {
ctor public MapboxInitializer();
method public Boolean create(android.content.Context context);
method public java.util.List<java.lang.Class<? extends androidx.startup.Initializer<?>>> dependencies();
method @MainThread @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=MapboxInitializerException::class) public static void init(android.content.Context context) throws java.lang.Throwable;
field public static final com.mapbox.maps.MapboxInitializer.Companion Companion;
}

public static final class MapboxInitializer.Companion {
method @MainThread @RestrictTo(androidx.annotation.RestrictTo.Scope.LIBRARY_GROUP_PREFIX) @kotlin.jvm.Throws(exceptionClasses=MapboxInitializerException::class) public void init(android.content.Context context) throws java.lang.Throwable;
}

public final class MapboxInitializerKt {
}

public interface MapboxLifecycleObserver {
method public void onDestroy();
method public void onLowMemory();
Expand Down
1 change: 1 addition & 0 deletions sdk-base/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ dependencies {
api(Dependencies.mapboxGlNative)
api(Dependencies.mapboxCoreCommon)
}
implementation(Dependencies.androidxStartup)

testImplementation(Dependencies.junit)
testImplementation(Dependencies.mockk)
Expand Down
26 changes: 25 additions & 1 deletion sdk-base/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1 +1,25 @@
<manifest package="com.mapbox.maps.base"/>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.mapbox.maps.base">

<application>
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- Disable Common and Maps SDK Initializers -->
<meta-data
android:name="com.mapbox.common.MapboxSDKCommonInitializer"
tools:node="remove" />
<meta-data
android:name="com.mapbox.maps.loader.MapboxMapsInitializer"
tools:node="remove" />

<!-- Introduce the new unified initializer -->
<meta-data
android:name="com.mapbox.maps.MapboxInitializer"
android:value="androidx.startup" />
</provider>
</application>
</manifest>
153 changes: 153 additions & 0 deletions sdk-base/src/main/java/com/mapbox/maps/MapboxInitializer.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
package com.mapbox.maps

import android.content.Context
import android.os.Build
import android.os.Looper
import android.os.SystemClock
import android.util.Log
import androidx.annotation.MainThread
import androidx.annotation.RestrictTo
import androidx.startup.AppInitializer
import androidx.startup.Initializer
import com.mapbox.maps.loader.MapboxMapsInitializer
import java.io.File

/**
* Unified Mapbox SDKs initializer class that catches exceptions to avoid crashing during app
* process start.
*
* Most of the crashes reported are related to [UnsatisfiedLinkError]
* (https://github.com/mapbox/mapbox-maps-android/issues/1109).
*
* This solution is valid only when using Mapbox SDK for Android and no other Mapbox SDK (e.g.
* Navigation, Search,...).
*
* In order to use this solution no other Mapbox SDK initializer should run (i.e.
* [MapboxMapsInitializer] or [com.mapbox.common.MapboxSDKCommonInitializer]) during process start.
* See the `sdk/src/main/AndroidManifest.xml` file.
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
class MapboxInitializer : Initializer<Boolean> {

/**
* This code is run exactly one time on process startup.
*/
override fun create(context: Context): Boolean {
initializerCalledElapsedTime = SystemClock.elapsedRealtime()
// try-catch to avoid terminating the whole process
try {
init(context)
} catch (e: Throwable) {
// Catch the exception, store it and log instead of propagating it. The app process will be
// able to continue its start. The Mapbox SDK is not loaded and can't be used until `init`
// function in the companion object is called. `MapView`, `MapSurface` and `Snapshotter` will
// call the init by themselves.
initializerFailure = e
Log.w(TAG, "Exception occurred when initializing Mapbox: ${e.message}")
}
return true
}

/**
* We do not need any dependencies here.
*/
override fun dependencies(): MutableList<Class<out Initializer<*>>> {
return mutableListOf()
}

/**
* Companion object for [MapboxInitializer] that holds some static state to keep track of
* initialization state and also provides [init] that does the SDK native stack initialization.
*/
companion object {
private const val TAG = "MapboxInitializer"
private var successfulInit = false
private var currentAttempt = 0

/**
* Elapsed time since boot when [MapboxInitializer.create] was called or null if it was not
* called.
*/
internal var initializerCalledElapsedTime: Long? = null
private set
internal var initializerFailure: Throwable? = null
private set

/**
* This function initializes Maps SDK native stack if it has not yet been done successfully.
*
* It can be called multiple times. If the native stack was already initialized successfully
* then this is a no-op.
*
* If initialization process throws an exception we catch it and enhanced it with system
* information (see [MapboxInitializerException]).
*/
@RestrictTo(RestrictTo.Scope.LIBRARY_GROUP_PREFIX)
@MainThread
@JvmStatic
@Throws(MapboxInitializerException::class)
public fun init(context: Context) {
if (successfulInit) {
return
}
if (Looper.myLooper() != Looper.getMainLooper()) {
throw RuntimeException("Mapbox must be called from main thread only!")
}
// we operate with application context to avoid memory leaks with Activity / View context
val applicationContext = context.applicationContext
Log.i(TAG, "MapboxInitializer started initialization, attempt ${++currentAttempt}")
runCatchingEnhanced(applicationContext) {
// it is enough to call only MapboxMapsInitializer as it has dependency on MapboxSDKCommonInitializer
AppInitializer.getInstance(applicationContext)
.initializeComponent(MapboxMapsInitializer::class.java)
Log.i(TAG, "MapboxInitializer initialized Maps successfully")
}
successfulInit = true
}

/**
* Runs the given [function]. If [function] throws a [Throwable] then a
* [MapboxInitializerException] is thrown which contains extra information in it.
*/
@Throws(MapboxInitializerException::class)
private inline fun runCatchingEnhanced(context: Context, function: () -> Unit) {
try {
function()
} catch (t: Throwable) {
// if we got to this point there we are most likely hitting UnsatisfiedLinkError, re-throw an exception
throw MapboxInitializerException(currentAttempt, context, t)
}
}
}
}

internal class MapboxInitializerException(attempt: Int, context: Context, t: Throwable) :
Throwable(gatherSystemInfo(attempt, context, t), t)

private fun gatherSystemInfo(attempt: Int, context: Context, t: Throwable): String {
val isInstantApp = runCatching {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
context.packageManager?.isInstantApp
} else {
null
}
}
val nativeLibs = runCatching {
context.packageManager?.getApplicationInfo(context.packageName, 0)?.let { ai ->
File(ai.nativeLibraryDir).list()?.joinToString() ?: ""
}
}
val initializerCalledMsg = MapboxInitializer.initializerCalledElapsedTime?.let {
// Log how long since the first time we tried to initialize during app process start. Or "null" if initializer was never called
"initializer called ${SystemClock.elapsedRealtime() - it }ms ago"
} ?: "initializer not called"

return "Failed to initialize: Attempt=$attempt," +
" exception=[${t.javaClass.simpleName}]," +
" $initializerCalledMsg," +
" initializerFailure=[${MapboxInitializer.initializerFailure?.javaClass?.simpleName}]," +
// Most likely initializerFailure is MapboxInitializerException so try to find its cause
" initializerFailure.cause=[${MapboxInitializer.initializerFailure?.cause?.javaClass?.simpleName}]," +
" extractedNativeLibs=[${nativeLibs.getOrNull()}]," +
" isInstantApp=[${isInstantApp.getOrNull()}],"
}
1 change: 1 addition & 0 deletions sdk/src/main/java/com/mapbox/maps/MapSurface.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ class MapSurface : MapPluginProviderDelegate, MapControllable {
surface: Surface,
mapInitOptions: MapInitOptions = MapInitOptions(context) // could use strong ref here as MapInitOptions have strong ref in any case
) {
MapboxInitializer.init(context)
this.context = context
this.surface = surface
this.mapInitOptions = mapInitOptions
Expand Down
1 change: 1 addition & 0 deletions sdk/src/main/java/com/mapbox/maps/MapView.kt
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ open class MapView : FrameLayout, MapPluginProviderDelegate, MapControllable {
defStyleRes: Int,
initOptions: MapInitOptions?,
) : super(context, attrs, defStyleAttr, defStyleRes) {
MapboxInitializer.init(context)
val resolvedMapInitOptions = if (attrs != null) {
parseTypedArray(context, attrs)
} else {
Expand Down
1 change: 1 addition & 0 deletions sdk/src/main/java/com/mapbox/maps/Snapshotter.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ open class Snapshotter {
options: MapSnapshotOptions,
overlayOptions: SnapshotOverlayOptions = SnapshotOverlayOptions()
) {
MapboxInitializer.init(context)
this.context = WeakReference(context)
mapSnapshotOptions = options
snapshotOverlayOptions = overlayOptions
Expand Down

0 comments on commit 9278967

Please sign in to comment.