mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
refactor(analytics)!: modularize analytics - remove Logging (#3256)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
9aa0cf9335
commit
cad88d277b
72 changed files with 1219 additions and 1426 deletions
|
|
@ -1,32 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh
|
||||
|
||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltAndroidApp
|
||||
class MeshUtilApplication : GeeksvilleApplication() {
|
||||
@Inject override lateinit var analyticsPrefs: AnalyticsPrefs
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
}
|
||||
}
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.analytics
|
||||
|
||||
import android.os.Bundle
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.analytics.FirebaseAnalytics
|
||||
import com.google.firebase.analytics.analytics
|
||||
import com.google.firebase.analytics.logEvent
|
||||
|
||||
class DataPair(val name: String, valueIn: Any?) {
|
||||
val value = valueIn ?: "null"
|
||||
|
||||
// / An accumulating firebase event - only one allowed per event
|
||||
constructor(d: Double) : this(FirebaseAnalytics.Param.VALUE, d)
|
||||
|
||||
constructor(d: Int) : this(FirebaseAnalytics.Param.VALUE, d)
|
||||
}
|
||||
|
||||
/** Implement our analytics API using Firebase Analytics */
|
||||
class FirebaseAnalytics(installId: String) :
|
||||
AnalyticsProvider,
|
||||
Logging {
|
||||
|
||||
val t = Firebase.analytics.apply { setUserId(installId) }
|
||||
|
||||
override fun setEnabled(on: Boolean) {
|
||||
t.setAnalyticsCollectionEnabled(on)
|
||||
}
|
||||
|
||||
override fun endSession() {
|
||||
track("End Session")
|
||||
// Mint.flush() // Send results now
|
||||
}
|
||||
|
||||
override fun trackLowValue(event: String, vararg properties: DataPair) {
|
||||
track(event, *properties)
|
||||
}
|
||||
|
||||
override fun track(event: String, vararg properties: DataPair) {
|
||||
debug("Analytics: track $event")
|
||||
|
||||
val bundle = Bundle()
|
||||
properties.forEach {
|
||||
when (it.value) {
|
||||
is Double -> bundle.putDouble(it.name, it.value)
|
||||
is Int -> bundle.putLong(it.name, it.value.toLong())
|
||||
is Long -> bundle.putLong(it.name, it.value)
|
||||
is Float -> bundle.putDouble(it.name, it.value.toDouble())
|
||||
else -> bundle.putString(it.name, it.value.toString())
|
||||
}
|
||||
}
|
||||
t.logEvent(event, bundle)
|
||||
}
|
||||
|
||||
override fun startSession() {
|
||||
debug("Analytics: start session")
|
||||
// automatic with firebase
|
||||
}
|
||||
|
||||
override fun setUserInfo(vararg p: DataPair) {
|
||||
p.forEach { t.setUserProperty(it.name, it.value.toString()) }
|
||||
}
|
||||
|
||||
override fun increment(name: String, amount: Double) {
|
||||
// Mint.logEvent("$name increment")
|
||||
}
|
||||
|
||||
/** Send a google analytics screen view event */
|
||||
override fun sendScreenView(name: String) {
|
||||
debug("Analytics: start screen $name")
|
||||
t.logEvent(FirebaseAnalytics.Event.SCREEN_VIEW) {
|
||||
param(FirebaseAnalytics.Param.SCREEN_NAME, name)
|
||||
param(FirebaseAnalytics.Param.SCREEN_CLASS, "MainActivity")
|
||||
}
|
||||
}
|
||||
|
||||
override fun endScreenView() {
|
||||
// debug("Analytics: end screen")
|
||||
}
|
||||
}
|
||||
|
|
@ -1,267 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program 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.
|
||||
*
|
||||
* This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.android
|
||||
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.provider.Settings
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.navigation.NavHostController
|
||||
import com.datadog.android.Datadog
|
||||
import com.datadog.android.DatadogSite
|
||||
import com.datadog.android.compose.ExperimentalTrackingApi
|
||||
import com.datadog.android.compose.NavigationViewTrackingEffect
|
||||
import com.datadog.android.compose.enableComposeActionTracking
|
||||
import com.datadog.android.core.configuration.Configuration
|
||||
import com.datadog.android.log.Logger
|
||||
import com.datadog.android.log.Logs
|
||||
import com.datadog.android.log.LogsConfiguration
|
||||
import com.datadog.android.privacy.TrackingConsent
|
||||
import com.datadog.android.rum.GlobalRumMonitor
|
||||
import com.datadog.android.rum.Rum
|
||||
import com.datadog.android.rum.RumConfiguration
|
||||
import com.datadog.android.rum.tracking.AcceptAllNavDestinations
|
||||
import com.datadog.android.sessionreplay.SessionReplay
|
||||
import com.datadog.android.sessionreplay.SessionReplayConfiguration
|
||||
import com.datadog.android.sessionreplay.compose.ComposeExtensionSupport
|
||||
import com.datadog.android.timber.DatadogTree
|
||||
import com.datadog.android.trace.Trace
|
||||
import com.datadog.android.trace.TraceConfiguration
|
||||
import com.datadog.android.trace.opentelemetry.DatadogOpenTelemetry
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.analytics.AnalyticsProvider
|
||||
import com.geeksville.mesh.analytics.FirebaseAnalytics
|
||||
import com.geeksville.mesh.util.exceptionReporter
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailabilityLight
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.analytics.analytics
|
||||
import com.google.firebase.crashlytics.crashlytics
|
||||
import com.google.firebase.crashlytics.setCustomKeys
|
||||
import com.google.firebase.initialize
|
||||
import com.suddenh4x.ratingdialog.AppRating
|
||||
import io.opentelemetry.api.GlobalOpenTelemetry
|
||||
import org.meshtastic.core.model.DeviceHardware
|
||||
import org.meshtastic.core.prefs.analytics.AnalyticsPrefs
|
||||
import timber.log.Timber
|
||||
|
||||
abstract class GeeksvilleApplication :
|
||||
Application(),
|
||||
Logging {
|
||||
|
||||
companion object {
|
||||
lateinit var analytics: AnalyticsProvider
|
||||
}
|
||||
|
||||
// / Are we running inside the testlab?
|
||||
val isInTestLab: Boolean
|
||||
get() {
|
||||
val testLabSetting = Settings.System.getString(contentResolver, "firebase.test.lab")
|
||||
if (testLabSetting != null) {
|
||||
info("Testlab is $testLabSetting")
|
||||
}
|
||||
return "true" == testLabSetting
|
||||
}
|
||||
|
||||
abstract val analyticsPrefs: AnalyticsPrefs
|
||||
|
||||
private val minimumLaunchTimes: Int = 10
|
||||
private val minimumDays: Int = 10
|
||||
private val minimumLaunchTimesToShowAgain: Int = 5
|
||||
private val minimumDaysToShowAgain: Int = 14
|
||||
|
||||
/** Ask user to rate in play store */
|
||||
@Suppress("MagicNumber")
|
||||
fun askToRate(activity: AppCompatActivity) {
|
||||
if (!isGooglePlayAvailable) return
|
||||
|
||||
@Suppress("MaxLineLength")
|
||||
exceptionReporter {
|
||||
// we don't want to crash our app because of bugs in this optional feature
|
||||
AppRating.Builder(activity)
|
||||
.setMinimumLaunchTimes(minimumLaunchTimes) // default is 5, 3 means app is launched 3 or more times
|
||||
.setMinimumDays(
|
||||
minimumDays,
|
||||
) // default is 5, 0 means install day, 10 means app is launched 10 or more days
|
||||
// later than installation
|
||||
.setMinimumLaunchTimesToShowAgain(
|
||||
minimumLaunchTimesToShowAgain,
|
||||
) // default is 5, 1 means app is launched 1 or more times after neutral button
|
||||
// clicked
|
||||
.setMinimumDaysToShowAgain(
|
||||
minimumDaysToShowAgain,
|
||||
) // default is 14, 1 means app is launched 1 or more days after neutral button
|
||||
// clicked
|
||||
.showIfMeetsConditions()
|
||||
}
|
||||
}
|
||||
|
||||
lateinit var analyticsPrefsChangedListener: SharedPreferences.OnSharedPreferenceChangeListener
|
||||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
initDatadog()
|
||||
initCrashlytics()
|
||||
updateAnalyticsConsent()
|
||||
// listen for changes to analytics prefs
|
||||
analyticsPrefsChangedListener =
|
||||
SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
|
||||
if (key == "allowed") {
|
||||
updateAnalyticsConsent()
|
||||
}
|
||||
}
|
||||
getSharedPreferences("analytics-prefs", MODE_PRIVATE)
|
||||
.registerOnSharedPreferenceChangeListener(analyticsPrefsChangedListener)
|
||||
}
|
||||
|
||||
private val sampleRate = 100f
|
||||
|
||||
private fun initCrashlytics() {
|
||||
analytics = FirebaseAnalytics(analyticsPrefs.installId)
|
||||
Firebase.initialize(this)
|
||||
Firebase.crashlytics.setUserId(analyticsPrefs.installId)
|
||||
Timber.plant(CrashlyticsTree())
|
||||
}
|
||||
|
||||
private fun updateAnalyticsConsent() {
|
||||
if (!isAnalyticsAvailable || isInTestLab) {
|
||||
info("Analytics not available")
|
||||
return
|
||||
}
|
||||
val isAnalyticsAllowed = analyticsPrefs.analyticsAllowed
|
||||
info(if (isAnalyticsAllowed) "Analytics enabled" else "Analytics disabled")
|
||||
Datadog.setTrackingConsent(if (isAnalyticsAllowed) TrackingConsent.GRANTED else TrackingConsent.NOT_GRANTED)
|
||||
|
||||
analytics.setEnabled(isAnalyticsAllowed)
|
||||
Firebase.crashlytics.isCrashlyticsCollectionEnabled = isAnalyticsAllowed
|
||||
Firebase.analytics.setAnalyticsCollectionEnabled(isAnalyticsAllowed)
|
||||
Firebase.crashlytics.sendUnsentReports()
|
||||
}
|
||||
|
||||
private class CrashlyticsTree : Timber.Tree() {
|
||||
|
||||
companion object {
|
||||
private const val KEY_PRIORITY = "priority"
|
||||
private const val KEY_TAG = "tag"
|
||||
private const val KEY_MESSAGE = "message"
|
||||
}
|
||||
|
||||
override fun log(priority: Int, tag: String?, message: String, t: Throwable?) {
|
||||
Firebase.crashlytics.setCustomKeys {
|
||||
key(KEY_PRIORITY, priority)
|
||||
key(KEY_TAG, tag ?: "No Tag")
|
||||
key(KEY_MESSAGE, message)
|
||||
}
|
||||
|
||||
if (t == null) {
|
||||
Firebase.crashlytics.recordException(Exception(message))
|
||||
} else {
|
||||
Firebase.crashlytics.recordException(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun initDatadog() {
|
||||
val logger =
|
||||
Logger.Builder()
|
||||
.setNetworkInfoEnabled(true)
|
||||
.setRemoteSampleRate(sampleRate)
|
||||
.setBundleWithTraceEnabled(true)
|
||||
.setBundleWithRumEnabled(true)
|
||||
.build()
|
||||
val configuration =
|
||||
Configuration.Builder(
|
||||
clientToken = BuildConfig.datadogClientToken,
|
||||
env = if (BuildConfig.DEBUG) "debug" else "release",
|
||||
variant = BuildConfig.FLAVOR,
|
||||
)
|
||||
.useSite(DatadogSite.US5)
|
||||
.setCrashReportsEnabled(true)
|
||||
.setUseDeveloperModeWhenDebuggable(true)
|
||||
.build()
|
||||
val consent = TrackingConsent.PENDING
|
||||
Datadog.initialize(this, configuration, consent)
|
||||
Datadog.setUserInfo(analyticsPrefs.installId)
|
||||
|
||||
val rumConfiguration =
|
||||
RumConfiguration.Builder(BuildConfig.datadogApplicationId)
|
||||
.trackAnonymousUser(true)
|
||||
.trackBackgroundEvents(true)
|
||||
.trackFrustrations(true)
|
||||
.trackLongTasks()
|
||||
.trackNonFatalAnrs(true)
|
||||
.trackUserInteractions()
|
||||
.enableComposeActionTracking()
|
||||
.build()
|
||||
Rum.enable(rumConfiguration)
|
||||
|
||||
val logsConfig = LogsConfiguration.Builder().build()
|
||||
Logs.enable(logsConfig)
|
||||
|
||||
val traceConfig = TraceConfiguration.Builder().build()
|
||||
Trace.enable(traceConfig)
|
||||
|
||||
GlobalOpenTelemetry.set(DatadogOpenTelemetry(BuildConfig.APPLICATION_ID))
|
||||
|
||||
val sessionReplayConfig =
|
||||
SessionReplayConfiguration.Builder(sampleRate = 20.0f)
|
||||
// in case you need Jetpack Compose support
|
||||
.addExtensionSupport(ComposeExtensionSupport())
|
||||
.build()
|
||||
|
||||
SessionReplay.enable(sessionReplayConfig)
|
||||
|
||||
Timber.plant(Timber.DebugTree(), DatadogTree(logger))
|
||||
}
|
||||
}
|
||||
|
||||
fun setAttributes(firmwareVersion: String, deviceHardware: DeviceHardware) {
|
||||
GlobalRumMonitor.get().addAttribute("firmware_version", firmwareVersion.extractSemanticVersion())
|
||||
GlobalRumMonitor.get().addAttribute("device_hardware", deviceHardware.hwModelSlug)
|
||||
}
|
||||
|
||||
private val Context.isGooglePlayAvailable: Boolean
|
||||
get() =
|
||||
GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(this).let {
|
||||
it != ConnectionResult.SERVICE_MISSING && it != ConnectionResult.SERVICE_INVALID
|
||||
}
|
||||
|
||||
private val isDatadogAvailable: Boolean = Datadog.isInitialized()
|
||||
|
||||
val Context.isAnalyticsAvailable: Boolean
|
||||
get() = isDatadogAvailable && isGooglePlayAvailable
|
||||
|
||||
@OptIn(ExperimentalTrackingApi::class)
|
||||
@Composable
|
||||
fun AddNavigationTracking(navController: NavHostController) {
|
||||
NavigationViewTrackingEffect(
|
||||
navController = navController,
|
||||
trackArguments = true,
|
||||
destinationPredicate = AcceptAllNavDestinations(),
|
||||
)
|
||||
}
|
||||
|
||||
fun String.extractSemanticVersion(): String {
|
||||
// Regex to capture up to three numeric parts separated by dots
|
||||
val regex = """^(\d+)(?:\.(\d+))?(?:\.(\d+))?""".toRegex()
|
||||
val matchResult = regex.find(this)
|
||||
return matchResult?.groupValues?.drop(1)?.filter { it.isNotEmpty() }?.joinToString(".")
|
||||
?: this // Fallback to original if no match
|
||||
}
|
||||
|
|
@ -32,12 +32,12 @@ import androidx.compose.runtime.remember
|
|||
import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.platform.LocalContext
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.google.android.gms.common.api.ResolvableApiException
|
||||
import com.google.android.gms.location.LocationRequest
|
||||
import com.google.android.gms.location.LocationServices
|
||||
import com.google.android.gms.location.LocationSettingsRequest
|
||||
import com.google.android.gms.location.Priority
|
||||
import timber.log.Timber
|
||||
|
||||
private const val INTERVAL_MILLIS = 10000L
|
||||
|
||||
|
|
@ -66,11 +66,11 @@ fun LocationPermissionsHandler(onPermissionResult: (Boolean) -> Unit) {
|
|||
val locationSettingsLauncher =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.StartIntentSenderForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
debug("Location settings changed by user.")
|
||||
Timber.d("Location settings changed by user.")
|
||||
// User has enabled location services or improved accuracy.
|
||||
onPermissionResult(true) // Settings are now adequate, and permission was already granted.
|
||||
} else {
|
||||
debug("Location settings change cancelled by user.")
|
||||
Timber.d("Location settings change cancelled by user.")
|
||||
// User chose not to change settings. The permission itself is still granted,
|
||||
// but the experience might be degraded. For the purpose of enabling map features,
|
||||
// we consider this as success if the core permission is there.
|
||||
|
|
@ -111,7 +111,7 @@ fun LocationPermissionsHandler(onPermissionResult: (Boolean) -> Unit) {
|
|||
val task = client.checkLocationSettings(builder.build())
|
||||
|
||||
task.addOnSuccessListener {
|
||||
debug("Location settings are satisfied.")
|
||||
Timber.d("Location settings are satisfied.")
|
||||
onPermissionResult(true) // Permission granted and settings are good
|
||||
}
|
||||
|
||||
|
|
@ -122,11 +122,11 @@ fun LocationPermissionsHandler(onPermissionResult: (Boolean) -> Unit) {
|
|||
locationSettingsLauncher.launch(intentSenderRequest)
|
||||
// Result of this launch will be handled by locationSettingsLauncher's callback
|
||||
} catch (sendEx: ActivityNotFoundException) {
|
||||
debug("Error launching location settings resolution ${sendEx.message}.")
|
||||
Timber.d("Error launching location settings resolution ${sendEx.message}.")
|
||||
onPermissionResult(true) // Permission is granted, but settings dialog failed. Proceed.
|
||||
}
|
||||
} else {
|
||||
debug("Location settings are not satisfiable.${exception.message}")
|
||||
Timber.d("Location settings are not satisfiable.${exception.message}")
|
||||
onPermissionResult(true) // Permission is granted, but settings not ideal. Proceed.
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,8 +66,6 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
|||
import com.geeksville.mesh.ConfigProtos.Config.DisplayConfig.DisplayUnits
|
||||
import com.geeksville.mesh.MeshProtos.Position
|
||||
import com.geeksville.mesh.MeshProtos.Waypoint
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.BuildUtils.warn
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.ui.map.components.ClusterItemsListDialog
|
||||
import com.geeksville.mesh.ui.map.components.CustomMapLayersSheet
|
||||
|
|
@ -205,7 +203,7 @@ fun MapView(
|
|||
try {
|
||||
cameraPositionState.animate(cameraUpdate)
|
||||
} catch (e: IllegalStateException) {
|
||||
debug("Error animating camera to location: ${e.message}")
|
||||
Timber.d("Error animating camera to location: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -224,14 +222,14 @@ fun MapView(
|
|||
|
||||
try {
|
||||
fusedLocationClient.requestLocationUpdates(locationRequest, locationCallback, null)
|
||||
debug("Started location tracking")
|
||||
Timber.d("Started location tracking")
|
||||
} catch (e: SecurityException) {
|
||||
debug("Location permission not available: ${e.message}")
|
||||
Timber.d("Location permission not available: ${e.message}")
|
||||
isLocationTrackingEnabled = false
|
||||
}
|
||||
} else {
|
||||
fusedLocationClient.removeLocationUpdates(locationCallback)
|
||||
debug("Stopped location tracking")
|
||||
Timber.d("Stopped location tracking")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -374,7 +372,7 @@ fun MapView(
|
|||
cameraPositionState.animate(CameraUpdateFactory.newLatLngBounds(bounds, padding))
|
||||
}
|
||||
} catch (e: IllegalStateException) {
|
||||
warn("MapView Could not animate to bounds: ${e.message}")
|
||||
Timber.w("MapView Could not animate to bounds: ${e.message}")
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
@ -462,7 +460,7 @@ fun MapView(
|
|||
CameraUpdateFactory.newLatLngBounds(bounds.build(), 100),
|
||||
)
|
||||
}
|
||||
debug("Cluster clicked! $cluster")
|
||||
Timber.d("Cluster clicked! $cluster")
|
||||
}
|
||||
true
|
||||
},
|
||||
|
|
@ -574,9 +572,9 @@ fun MapView(
|
|||
val currentPosition = cameraPositionState.position
|
||||
val newCameraPosition = CameraPosition.Builder(currentPosition).bearing(0f).build()
|
||||
cameraPositionState.animate(CameraUpdateFactory.newCameraPosition(newCameraPosition))
|
||||
debug("Oriented map to north")
|
||||
Timber.d("Oriented map to north")
|
||||
} catch (e: IllegalStateException) {
|
||||
debug("Error orienting map to north: ${e.message}")
|
||||
Timber.d("Error orienting map to north: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import android.net.Uri
|
|||
import androidx.core.net.toFile
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.geeksville.mesh.ConfigProtos
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.google.android.gms.maps.GoogleMap
|
||||
import com.google.android.gms.maps.model.TileProvider
|
||||
import com.google.android.gms.maps.model.UrlTileProvider
|
||||
|
|
@ -442,7 +441,7 @@ constructor(
|
|||
try {
|
||||
application.contentResolver.openInputStream(uriToLoad)
|
||||
} catch (_: Exception) {
|
||||
debug("MapViewModel: Error opening InputStream from URI: $uriToLoad")
|
||||
Timber.d("MapViewModel: Error opening InputStream from URI: $uriToLoad")
|
||||
null
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue