diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 83e2cdb..e233a44 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -187,7 +187,6 @@ dependencies { // Mapbox implementation(libs.mapbox.maps) -// implementation(libs.mapbox.maps.compose) implementation(libs.mapbox.search) implementation(libs.mapbox.navigation) diff --git a/app/src/main/java/illyan/jay/MainActivity.kt b/app/src/main/java/illyan/jay/MainActivity.kt index 29294f2..f1d77a2 100644 --- a/app/src/main/java/illyan/jay/MainActivity.kt +++ b/app/src/main/java/illyan/jay/MainActivity.kt @@ -26,6 +26,13 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner @@ -40,6 +47,7 @@ import illyan.jay.domain.interactor.AuthInteractor import illyan.jay.ui.NavGraphs import illyan.jay.ui.components.PreviewAccessibility import illyan.jay.ui.theme.JayThemeWithViewModel +import illyan.jay.util.MapboxExceptionHandler import javax.inject.Inject @AndroidEntryPoint @@ -78,14 +86,27 @@ class MainActivity : AppCompatActivity() { } } + val mapboxExceptionHandler = MapboxExceptionHandler() + Thread.setDefaultUncaughtExceptionHandler(mapboxExceptionHandler) + setContent { + var mapboxMapViewNotSupported by rememberSaveable { mutableStateOf(false) } + LaunchedEffect(Unit) { + mapboxExceptionHandler.openGlNotSupportedCallback = { + mapboxMapViewNotSupported = true + } + } JayThemeWithViewModel { - MainScreen(modifier = Modifier.fillMaxSize()) + CompositionLocalProvider(LocalMapboxNotSupported provides mapboxMapViewNotSupported) { + MainScreen(modifier = Modifier.fillMaxSize()) + } } } } } +val LocalMapboxNotSupported = compositionLocalOf { false } + @PreviewAccessibility @Composable fun MainScreen( diff --git a/app/src/main/java/illyan/jay/ui/map/Map.kt b/app/src/main/java/illyan/jay/ui/map/Map.kt index 7908cd9..34c6319 100644 --- a/app/src/main/java/illyan/jay/ui/map/Map.kt +++ b/app/src/main/java/illyan/jay/ui/map/Map.kt @@ -19,9 +19,24 @@ package illyan.jay.ui.map import android.content.Context +import androidx.compose.animation.Crossfade +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.statusBars +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.ArrowDownward +import androidx.compose.material.icons.rounded.BrokenImage +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.surfaceColorAtElevation import androidx.compose.runtime.Composable import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect @@ -30,22 +45,27 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.LayoutDirection +import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView +import com.mapbox.common.Cancelable import com.mapbox.geojson.Point import com.mapbox.maps.CameraOptions import com.mapbox.maps.CameraState import com.mapbox.maps.EdgeInsets import com.mapbox.maps.ImageHolder import com.mapbox.maps.MapInitOptions +import com.mapbox.maps.MapLoaded import com.mapbox.maps.MapOptions import com.mapbox.maps.MapView import com.mapbox.maps.Style -import com.mapbox.maps.extension.observable.eventdata.MapLoadedEventData import com.mapbox.maps.extension.style.expressions.dsl.generated.interpolate import com.mapbox.maps.plugin.LocationPuck2D import com.mapbox.maps.plugin.attribution.attribution @@ -54,7 +74,9 @@ import com.mapbox.maps.plugin.gestures.gestures import com.mapbox.maps.plugin.locationcomponent.LocationComponentPlugin import com.mapbox.maps.plugin.logo.logo import com.mapbox.maps.plugin.scalebar.scalebar +import illyan.jay.LocalMapboxNotSupported import illyan.jay.R +import illyan.jay.ui.components.PreviewAll import illyan.jay.ui.poi.model.Place val BmeK = Place( @@ -122,7 +144,7 @@ fun MapboxMap( } LaunchedEffect(initialStyleUri) { - map.getMapboxMap().loadStyleUri(initialStyleUri) + map.mapboxMap.loadStyleUri(initialStyleUri) } MapboxMapContainer( @@ -147,29 +169,101 @@ private fun MapboxMapContainer( onCameraChanged: (CameraState) -> Unit = {} ) { DisposableEffect(Unit) { - val onMapLoadedListener = { _: MapLoadedEventData -> onMapFullyLoaded(map) } - map.getMapboxMap().addOnMapLoadedListener(onMapLoadedListener) - map.getMapboxMap().addOnCameraChangeListener { onCameraChanged(map.getMapboxMap().cameraState) } - onDispose { map.getMapboxMap().removeOnMapLoadedListener(onMapLoadedListener) } + val onMapLoadedListener = { _: MapLoaded -> onMapFullyLoaded(map) } + val cancelable = mutableListOf() + map.mapboxMap.subscribeMapLoaded(onMapLoadedListener) + map.mapboxMap.subscribeCameraChanged { onCameraChanged(map.mapboxMap.cameraState) } + onDispose { cancelable.forEach { it.cancel() } } } val statusBarHeight = LocalDensity.current.run { WindowInsets.statusBars.getTop(this) } - val fixedStatusBarHeight = rememberSaveable { statusBarHeight } - AndroidView( + val fixedStatusBarHeight = rememberSaveable(statusBarHeight) { statusBarHeight } + val isMapNotSupported = LocalMapboxNotSupported.current + Crossfade( modifier = modifier, - factory = { map } + targetState = isMapNotSupported, + label = "MapboxMap" + ) { notSupported -> + if (notSupported) { + LaunchedEffect(Unit) { onMapFullyLoaded(map) } + MapsNotSupportedCard(modifier = modifier) + } else { + AndroidView( + modifier = modifier, + factory = { map } + ) { + it.logo.position = 0 + it.logo.marginTop = fixedStatusBarHeight.toFloat() + it.attribution.position = 0 + it.attribution.marginTop = fixedStatusBarHeight.toFloat() + it.attribution.marginLeft = 240f + it.compass.marginTop = fixedStatusBarHeight.toFloat() + it.gestures.scrollEnabled = true + it.scalebar.isMetricUnits = true // TODO: set this in settings or based on location, etc. + it.scalebar.enabled = false // TODO: enable it later if needed (though pay attention to ugly design) + } + } + } +} + +@Composable +fun MapsNotSupportedCard( + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center ) { - it.logo.position = 0 - it.logo.marginTop = fixedStatusBarHeight.toFloat() - it.attribution.position = 0 - it.attribution.marginTop = fixedStatusBarHeight.toFloat() - it.attribution.marginLeft = 240f - it.compass.marginTop = fixedStatusBarHeight.toFloat() - it.gestures.scrollEnabled = true - it.scalebar.isMetricUnits = true // TODO: set this in settings or based on location, etc. - it.scalebar.enabled = false // TODO: enable it later if needed (though pay attention to ugly design) + Card( + modifier = Modifier.padding(horizontal = 16.dp), + colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceColorAtElevation(1.dp)) + ) { + Column( + modifier = Modifier.padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Icon( + modifier = Modifier.size(64.dp), + imageVector = Icons.Rounded.BrokenImage, + contentDescription = "Mapbox Map Error Icon", + tint = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.mapbox_map_initialization_problem), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + Icon( + modifier = Modifier.size(32.dp), + imageVector = Icons.Rounded.ArrowDownward, + contentDescription = "Mapbox Map Error Icon", + tint = MaterialTheme.colorScheme.onSurface + ) + Text( + text = stringResource(R.string.mapbox_map_unsupported_opengl), + style = MaterialTheme.typography.headlineSmall, + color = MaterialTheme.colorScheme.onSurface, + textAlign = TextAlign.Center + ) + Text( + modifier = Modifier.padding(top = 8.dp), + text = stringResource(R.string.mapbox_map_problem_not_affecting_jay), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurface + ) + } + } } } +@PreviewAll +@Composable +fun PreviewMapsNotSupportedCard() { + MapsNotSupportedCard() +} + fun LocationComponentPlugin.turnOnWithDefaultPuck() { if (!enabled) { enabled = true diff --git a/app/src/main/java/illyan/jay/util/MapboxExceptionHandler.kt b/app/src/main/java/illyan/jay/util/MapboxExceptionHandler.kt new file mode 100644 index 0000000..2badcce --- /dev/null +++ b/app/src/main/java/illyan/jay/util/MapboxExceptionHandler.kt @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 Balázs Püspök-Kiss (Illyan) + * + * Jay is a driver behaviour analytics app. + * + * This file is part of Jay. + * + * Jay is free software: you can redistribute it and/or modify it under the + * terms of the GNU General Public License as published by the Free Software + * Foundation, either version 3 of the License, or (at your option) any later version. + * Jay is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; + * without even the implied warranty of MERCHANTABILITY or FITNESS FOR + * A PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with Jay. + * If not, see . + */ + +package illyan.jay.util + +import timber.log.Timber + +class MapboxExceptionHandler : Thread.UncaughtExceptionHandler { + + var openGlNotSupportedCallback: () -> Unit = {} + + override fun uncaughtException(t: Thread, e: Throwable) { + if (e is IllegalStateException && + e.message?.contains("OpenGL ES 3.0 context could not be created") == true) { + // Older emulated Android devices may not support OpenGL ES 3.0 properly + openGlNotSupportedCallback() + } + Timber.e(e) + } +} \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2201902..ddaf26f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -154,6 +154,9 @@ Download Refresh No Available Models Found + Problem occurred during Mapbox Map\'s initialization + OpenGL not supported + Jay\'s location tracking and data analytics are not affected. Delete All Restart Required Machine Learning functionality is in Beta. Restart is required after each action to take effect.