mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
feat(analytics): Integrate Datadog for RUM and Logging (#2578)
Signed-off-by: James Rich <2199651+jamesarich@users.noreply.github.com>
This commit is contained in:
parent
f5478b42c3
commit
ab22a655c4
18 changed files with 348 additions and 194 deletions
2
.github/workflows/merge-queue.yml
vendored
2
.github/workflows/merge-queue.yml
vendored
|
|
@ -16,6 +16,8 @@ jobs:
|
|||
upload_artifacts: false
|
||||
secrets:
|
||||
GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
|
||||
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
|
||||
|
||||
androidTest:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
|
|
|
|||
13
.github/workflows/pull-request.yml
vendored
13
.github/workflows/pull-request.yml
vendored
|
|
@ -12,12 +12,19 @@ concurrency:
|
|||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
test_secrets:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
TEST_SECRET: ${{ secrets.TEST_SECRET }}
|
||||
steps:
|
||||
- name: Test Secrets
|
||||
run: |
|
||||
echo "$TEST_SECRET"
|
||||
|
||||
build_and_detekt:
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android' && github.head_ref != 'scheduled-updates'
|
||||
uses: ./.github/workflows/reusable-android-build.yml
|
||||
secrets:
|
||||
GRADLE_ENCRYPTION_KEY: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
|
||||
# inputs.upload_artifacts defaults to true, so no need to specify for PRs
|
||||
secrets: inherit
|
||||
|
||||
androidTest:
|
||||
# Assuming androidTest should also only run for the main repository
|
||||
|
|
|
|||
7
.github/workflows/release.yml
vendored
7
.github/workflows/release.yml
vendored
|
|
@ -99,6 +99,9 @@ jobs:
|
|||
needs: prepare-release-info # Depends on version info
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'meshtastic/Meshtastic-Android'
|
||||
env:
|
||||
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
|
||||
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
|
||||
outputs:
|
||||
aab_path: app/build/outputs/bundle/googleRelease/app-google-release.aab
|
||||
aab_name: googleRelease-${{ needs.prepare-release-info.outputs.versionNameBase }}-${{ needs.prepare-release-info.outputs.versionCode }}.aab
|
||||
|
|
@ -118,11 +121,15 @@ jobs:
|
|||
echo $GSERVICES > ./app/google-services.json
|
||||
echo $KEYSTORE | base64 -di > ./app/$KEYSTORE_FILENAME
|
||||
echo "$KEYSTORE_PROPERTIES" > ./keystore.properties
|
||||
echo "datadogApplicationId=$DATADOG_APPLICATION_ID" >> ./secrets.properties
|
||||
echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" >> ./secrets.properties
|
||||
env:
|
||||
GSERVICES: ${{ secrets.GSERVICES }}
|
||||
KEYSTORE: ${{ secrets.KEYSTORE }}
|
||||
KEYSTORE_FILENAME: ${{ secrets.KEYSTORE_FILENAME }}
|
||||
KEYSTORE_PROPERTIES: ${{ secrets.KEYSTORE_PROPERTIES }}
|
||||
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
|
||||
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
|
||||
|
||||
- name: Set up JDK 21
|
||||
uses: actions/setup-java@v4
|
||||
|
|
|
|||
26
.github/workflows/reusable-android-build.yml
vendored
26
.github/workflows/reusable-android-build.yml
vendored
|
|
@ -2,21 +2,36 @@ name: Reusable Android Build and Detekt
|
|||
|
||||
on:
|
||||
workflow_call:
|
||||
secrets:
|
||||
GRADLE_ENCRYPTION_KEY:
|
||||
required: false
|
||||
DATADOG_APPLICATION_ID:
|
||||
required: false
|
||||
DATADOG_CLIENT_TOKEN:
|
||||
required: false
|
||||
TEST_SECRET:
|
||||
required: false
|
||||
inputs:
|
||||
upload_artifacts:
|
||||
description: 'Whether to upload build and Detekt artifacts'
|
||||
required: false
|
||||
type: boolean
|
||||
default: true
|
||||
secrets:
|
||||
GRADLE_ENCRYPTION_KEY:
|
||||
required: false
|
||||
|
||||
jobs:
|
||||
build_and_detekt:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 35
|
||||
env:
|
||||
DATADOG_APPLICATION_ID: ${{ secrets.DATADOG_APPLICATION_ID }}
|
||||
DATADOG_CLIENT_TOKEN: ${{ secrets.DATADOG_CLIENT_TOKEN }}
|
||||
|
||||
steps:
|
||||
- name: Test Secrets
|
||||
env:
|
||||
TEST_SECRET: ${{ secrets.TEST_SECRET }}
|
||||
run: echo "$TEST_SECRET"
|
||||
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
|
|
@ -47,6 +62,11 @@ jobs:
|
|||
- name: Expose Version Code as Environment Variable
|
||||
run: echo "VERSION_CODE=${{ steps.calculate_version_code.outputs.versionCode }}" >> $GITHUB_ENV
|
||||
|
||||
- name: Load secrets
|
||||
if: env.DATADOG_APPLICATION_ID != '' && env.DATADOG_CLIENT_TOKEN != ''
|
||||
run: |
|
||||
echo "datadogApplicationId=$DATADOG_APPLICATION_ID" >> ./secrets.properties
|
||||
echo "datadogClientToken=$DATADOG_CLIENT_TOKEN" >> ./secrets.properties
|
||||
- name: Run Spotless, Detekt, Build, Lint, and Local Tests
|
||||
run: ./gradlew :app:spotlessCheck :app:detekt :app:lintFdroidDebug :app:lintGoogleDebug :app:assembleDebug :app:testFdroidDebug :app:testGoogleDebug --configuration-cache --scan
|
||||
env:
|
||||
|
|
|
|||
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -29,3 +29,4 @@ keystore.properties
|
|||
|
||||
# VS code
|
||||
.vscode/settings.json
|
||||
/secrets.properties
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ plugins {
|
|||
alias(libs.plugins.protobuf)
|
||||
alias(libs.plugins.devtools.ksp)
|
||||
alias(libs.plugins.detekt)
|
||||
alias(libs.plugins.datadog)
|
||||
alias(libs.plugins.secrets.gradle.plugin)
|
||||
alias(libs.plugins.spotless)
|
||||
}
|
||||
|
||||
|
|
@ -163,6 +165,17 @@ kotlin {
|
|||
}
|
||||
}
|
||||
|
||||
secrets {
|
||||
defaultPropertiesFileName = "secrets.defaults.properties"
|
||||
propertiesFileName = "secrets.properties"
|
||||
}
|
||||
|
||||
datadog {
|
||||
// compose instrumentation is broken for kotlin 2.2.x - see:
|
||||
// https://github.com/DataDog/dd-sdk-android-gradle-plugin/issues/407
|
||||
// composeInstrumentation = InstrumentationMode.AUTO
|
||||
}
|
||||
|
||||
// per protobuf-gradle-plugin docs, this is recommended for android
|
||||
protobuf {
|
||||
protoc { artifact = libs.protobuf.protoc.get().toString() }
|
||||
|
|
@ -180,8 +193,18 @@ protobuf {
|
|||
androidComponents {
|
||||
onVariants(selector().all()) { variant ->
|
||||
project.afterEvaluate {
|
||||
val capName = variant.name.replaceFirstChar { it.uppercase() }
|
||||
tasks.named("ksp${capName}Kotlin") { dependsOn("generate${capName}Proto") }
|
||||
val variantNameCapped = variant.name.replaceFirstChar { it.uppercase() }
|
||||
tasks.named("ksp${variantNameCapped}Kotlin") { dependsOn("generate${variantNameCapped}Proto") }
|
||||
}
|
||||
}
|
||||
onVariants(selector().withBuildType("release")) { variant ->
|
||||
if (variant.flavorName == "google") {
|
||||
val variantNameCapped = variant.name.replaceFirstChar { it.uppercase() }
|
||||
val minifyTaskName = "minify${variantNameCapped}WithR8"
|
||||
val uploadTaskName = "uploadMapping$variantNameCapped"
|
||||
if (project.tasks.findByName(uploadTaskName) != null && project.tasks.findByName(minifyTaskName) != null) {
|
||||
tasks.named(minifyTaskName).configure { finalizedBy(uploadTaskName) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -225,6 +248,7 @@ dependencies {
|
|||
implementation(libs.work.runtime.ktx)
|
||||
implementation(libs.core.location.altitude)
|
||||
implementation(libs.accompanist.permissions)
|
||||
implementation(libs.timber)
|
||||
|
||||
// Compose BOM
|
||||
implementation(platform(libs.compose.bom))
|
||||
|
|
@ -233,6 +257,7 @@ dependencies {
|
|||
// Firebase BOM
|
||||
"googleImplementation"(platform(libs.firebase.bom))
|
||||
"googleImplementation"(libs.bundles.firebase)
|
||||
"googleImplementation"(libs.bundles.datadog)
|
||||
|
||||
// ksp
|
||||
ksp(libs.room.compiler)
|
||||
|
|
@ -262,7 +287,7 @@ detekt {
|
|||
baseline = file("../config/detekt/detekt-baseline.xml")
|
||||
}
|
||||
|
||||
val googleServiceKeywords = listOf("crashlytics", "google")
|
||||
val googleServiceKeywords = listOf("crashlytics", "google", "datadog")
|
||||
|
||||
tasks.configureEach {
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -18,7 +18,6 @@
|
|||
package com.geeksville.mesh
|
||||
|
||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
|
||||
@HiltAndroidApp
|
||||
|
|
@ -26,8 +25,5 @@ class MeshUtilApplication : GeeksvilleApplication() {
|
|||
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
Logging.showLogs = BuildConfig.DEBUG
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,21 +23,24 @@ import android.content.SharedPreferences
|
|||
import android.provider.Settings
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.edit
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.analytics.AnalyticsProvider
|
||||
import com.geeksville.mesh.model.DeviceHardware
|
||||
import timber.log.Timber
|
||||
|
||||
open class GeeksvilleApplication : Application(), Logging {
|
||||
open class GeeksvilleApplication :
|
||||
Application(),
|
||||
Logging {
|
||||
|
||||
companion object {
|
||||
lateinit var analytics: AnalyticsProvider
|
||||
}
|
||||
|
||||
/// Are we running inside the testlab?
|
||||
// / Are we running inside the testlab?
|
||||
val isInTestLab: Boolean
|
||||
get() {
|
||||
val testLabSetting =
|
||||
Settings.System.getString(contentResolver, "firebase.test.lab") ?: null
|
||||
if(testLabSetting != null)
|
||||
info("Testlab is $testLabSetting")
|
||||
val testLabSetting = Settings.System.getString(contentResolver, "firebase.test.lab") ?: null
|
||||
if (testLabSetting != null) info("Testlab is $testLabSetting")
|
||||
return "true" == testLabSetting
|
||||
}
|
||||
|
||||
|
|
@ -48,9 +51,7 @@ open class GeeksvilleApplication : Application(), Logging {
|
|||
var isAnalyticsAllowed: Boolean
|
||||
get() = analyticsPrefs.getBoolean("allowed", true)
|
||||
set(value) {
|
||||
analyticsPrefs.edit {
|
||||
putBoolean("allowed", value)
|
||||
}
|
||||
analyticsPrefs.edit { putBoolean("allowed", value) }
|
||||
|
||||
// Change the flag with the providers
|
||||
analytics.setEnabled(value && !isInTestLab) // Never do analytics in the test lab
|
||||
|
|
@ -64,10 +65,18 @@ open class GeeksvilleApplication : Application(), Logging {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
if (BuildConfig.DEBUG) {
|
||||
Timber.plant(Timber.DebugTree())
|
||||
}
|
||||
|
||||
val nopAnalytics = com.geeksville.mesh.analytics.NopAnalytics(this)
|
||||
analytics = nopAnalytics
|
||||
isAnalyticsAllowed = false
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.isGooglePlayAvailable(): Boolean = false
|
||||
fun Context.isGooglePlayAvailable(): Boolean = false
|
||||
|
||||
fun setAttributes(deviceVersion: String, deviceHardware: DeviceHardware) {
|
||||
// No-op for F-Droid version
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,11 +21,11 @@ import android.os.Debug
|
|||
import com.geeksville.mesh.android.AppPrefs
|
||||
import com.geeksville.mesh.android.BuildUtils.isEmulator
|
||||
import com.geeksville.mesh.android.GeeksvilleApplication
|
||||
import com.geeksville.mesh.android.Logging
|
||||
import com.geeksville.mesh.util.Exceptions
|
||||
import com.google.firebase.crashlytics.crashlytics
|
||||
import com.google.firebase.Firebase
|
||||
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
||||
import com.google.firebase.crashlytics.setCustomKeys
|
||||
import dagger.hilt.android.HiltAndroidApp
|
||||
import timber.log.Timber
|
||||
|
||||
@HiltAndroidApp
|
||||
class MeshUtilApplication : GeeksvilleApplication() {
|
||||
|
|
@ -33,30 +33,15 @@ class MeshUtilApplication : GeeksvilleApplication() {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
Logging.showLogs = BuildConfig.DEBUG
|
||||
|
||||
// We default to off in the manifest - we turn on here if the user approves
|
||||
// leave off when running in the debugger
|
||||
if (!isEmulator && (!BuildConfig.DEBUG || !Debug.isDebuggerConnected())) {
|
||||
val crashlytics = Firebase.crashlytics
|
||||
crashlytics.setCrashlyticsCollectionEnabled(isAnalyticsAllowed)
|
||||
crashlytics.setCustomKey("debug_build", BuildConfig.DEBUG)
|
||||
|
||||
val crashlytics = FirebaseCrashlytics.getInstance()
|
||||
val pref = AppPrefs(this)
|
||||
crashlytics.setUserId(pref.getInstallId()) // be able to group all bugs per anonymous user
|
||||
|
||||
// We always send our log messages to the crashlytics lib, but they only get sent to the server if we report an exception
|
||||
// This makes log messages work properly if someone turns on analytics just before they click report bug.
|
||||
// send all log messages through crashyltics, so if we do crash we'll have those in the report
|
||||
val standardLogger = Logging.printlog
|
||||
Logging.printlog = { level, tag, message ->
|
||||
crashlytics.log("$tag: $message")
|
||||
standardLogger(level, tag, message)
|
||||
}
|
||||
|
||||
fun sendCrashReports() {
|
||||
if (isAnalyticsAllowed)
|
||||
crashlytics.sendUnsentReports()
|
||||
if (isAnalyticsAllowed) crashlytics.sendUnsentReports()
|
||||
}
|
||||
|
||||
// Send any old reports if user approves
|
||||
|
|
@ -67,6 +52,30 @@ class MeshUtilApplication : GeeksvilleApplication() {
|
|||
crashlytics.recordException(exception)
|
||||
sendCrashReports() // Send the new report
|
||||
}
|
||||
Timber.plant(CrashlyticsTree())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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?) {
|
||||
FirebaseCrashlytics.getInstance().setCustomKeys {
|
||||
key(KEY_PRIORITY, priority)
|
||||
key(KEY_TAG, tag ?: "No Tag")
|
||||
key(KEY_MESSAGE, message)
|
||||
}
|
||||
|
||||
if (t == null) {
|
||||
FirebaseCrashlytics.getInstance().recordException(Exception(message))
|
||||
} else {
|
||||
FirebaseCrashlytics.getInstance().recordException(t)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,44 +21,54 @@ import android.app.Application
|
|||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.provider.Settings
|
||||
import android.util.Log
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.core.content.edit
|
||||
import com.datadog.android.Datadog
|
||||
import com.datadog.android.DatadogSite
|
||||
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.timber.DatadogTree
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.analytics.AnalyticsProvider
|
||||
import com.geeksville.mesh.analytics.FirebaseAnalytics
|
||||
import com.geeksville.mesh.model.DeviceHardware
|
||||
import com.geeksville.mesh.util.exceptionReporter
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailabilityLight
|
||||
import com.suddenh4x.ratingdialog.AppRating
|
||||
import timber.log.Timber
|
||||
|
||||
/**
|
||||
* Created by kevinh on 1/4/15.
|
||||
*/
|
||||
|
||||
open class GeeksvilleApplication : Application(), Logging {
|
||||
/** Created by kevinh on 1/4/15. */
|
||||
open class GeeksvilleApplication :
|
||||
Application(),
|
||||
Logging {
|
||||
|
||||
companion object {
|
||||
lateinit var analytics: AnalyticsProvider
|
||||
}
|
||||
|
||||
/// Are we running inside the testlab?
|
||||
// / Are we running inside the testlab?
|
||||
val isInTestLab: Boolean
|
||||
get() {
|
||||
val testLabSetting =
|
||||
Settings.System.getString(contentResolver, "firebase.test.lab") ?: null
|
||||
if(testLabSetting != null)
|
||||
info("Testlab is $testLabSetting")
|
||||
val testLabSetting = Settings.System.getString(contentResolver, "firebase.test.lab") ?: null
|
||||
if (testLabSetting != null) info("Testlab is $testLabSetting")
|
||||
return "true" == testLabSetting
|
||||
}
|
||||
|
||||
private val analyticsPrefs: SharedPreferences by lazy {
|
||||
getSharedPreferences("analytics-prefs", Context.MODE_PRIVATE)
|
||||
}
|
||||
private val analyticsPrefs: SharedPreferences by lazy { getSharedPreferences("analytics-prefs", MODE_PRIVATE) }
|
||||
|
||||
var isAnalyticsAllowed: Boolean
|
||||
get() = analyticsPrefs.getBoolean("allowed", true)
|
||||
set(value) {
|
||||
analyticsPrefs.edit {
|
||||
putBoolean("allowed", value)
|
||||
}
|
||||
analyticsPrefs.edit { putBoolean("allowed", value) }
|
||||
|
||||
// Change the flag with the providers
|
||||
analytics.setEnabled(value && !isInTestLab) // Never do analytics in the test lab
|
||||
|
|
@ -68,12 +78,20 @@ open class GeeksvilleApplication : Application(), Logging {
|
|||
fun askToRate(activity: AppCompatActivity) {
|
||||
if (!isGooglePlayAvailable()) return
|
||||
|
||||
exceptionReporter { // we don't want to crash our app because of bugs in this optional feature
|
||||
exceptionReporter {
|
||||
// we don't want to crash our app because of bugs in this optional feature
|
||||
AppRating.Builder(activity)
|
||||
.setMinimumLaunchTimes(10) // default is 5, 3 means app is launched 3 or more times
|
||||
.setMinimumDays(10) // default is 5, 0 means install day, 10 means app is launched 10 or more days later than installation
|
||||
.setMinimumLaunchTimesToShowAgain(5) // default is 5, 1 means app is launched 1 or more times after neutral button clicked
|
||||
.setMinimumDaysToShowAgain(14) // default is 14, 1 means app is launched 1 or more days after neutral button clicked
|
||||
.setMinimumDays(10) // default is 5, 0 means install day, 10 means app is launched 10 or more days
|
||||
// later than installation
|
||||
.setMinimumLaunchTimesToShowAgain(
|
||||
5,
|
||||
) // default is 5, 1 means app is launched 1 or more times after neutral button
|
||||
// clicked
|
||||
.setMinimumDaysToShowAgain(
|
||||
14,
|
||||
) // default is 14, 1 means app is launched 1 or more days after neutral button
|
||||
// clicked
|
||||
.showIfMeetsConditions()
|
||||
}
|
||||
}
|
||||
|
|
@ -81,19 +99,64 @@ open class GeeksvilleApplication : Application(), Logging {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
val firebaseAnalytics = com.geeksville.mesh.analytics.FirebaseAnalytics(this)
|
||||
val logger =
|
||||
Logger.Builder()
|
||||
.setNetworkInfoEnabled(true)
|
||||
.setLogcatLogsEnabled(true)
|
||||
.setRemoteSampleRate(100f)
|
||||
.setBundleWithTraceEnabled(true)
|
||||
.setName("TimberLogger")
|
||||
.build()
|
||||
|
||||
val firebaseAnalytics = FirebaseAnalytics(this)
|
||||
analytics = firebaseAnalytics
|
||||
|
||||
// Set analytics per prefs
|
||||
isAnalyticsAllowed = isAnalyticsAllowed
|
||||
if (isAnalyticsAllowed || BuildConfig.DEBUG) {
|
||||
// datadog analytics
|
||||
val configuration =
|
||||
Configuration.Builder(
|
||||
clientToken = BuildConfig.datadogClientToken,
|
||||
env = if (BuildConfig.DEBUG || true) "debug" else "release",
|
||||
variant = BuildConfig.FLAVOR,
|
||||
)
|
||||
.useSite(DatadogSite.US5)
|
||||
.setCrashReportsEnabled(true)
|
||||
.setUseDeveloperModeWhenDebuggable(true)
|
||||
.build()
|
||||
val consent =
|
||||
if (isAnalyticsAllowed) {
|
||||
TrackingConsent.GRANTED
|
||||
} else {
|
||||
TrackingConsent.NOT_GRANTED
|
||||
}
|
||||
Datadog.initialize(this, configuration, consent)
|
||||
Datadog.setVerbosity(Log.VERBOSE)
|
||||
|
||||
val rumConfiguration =
|
||||
RumConfiguration.Builder(BuildConfig.datadogApplicationId)
|
||||
.trackUserInteractions()
|
||||
.trackLongTasks()
|
||||
.trackBackgroundEvents(true)
|
||||
.enableComposeActionTracking()
|
||||
.build()
|
||||
Rum.enable(rumConfiguration)
|
||||
|
||||
val logsConfig = LogsConfiguration.Builder().build()
|
||||
Logs.enable(logsConfig)
|
||||
|
||||
Timber.plant(Timber.DebugTree(), DatadogTree(logger))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun Context.isGooglePlayAvailable(): Boolean {
|
||||
return GoogleApiAvailabilityLight.getInstance()
|
||||
.isGooglePlayServicesAvailable(this)
|
||||
.let {
|
||||
it != ConnectionResult.SERVICE_MISSING &&
|
||||
it != ConnectionResult.SERVICE_INVALID
|
||||
}
|
||||
}
|
||||
fun setAttributes(firmwareVersion: String, deviceHardware: DeviceHardware) {
|
||||
GlobalRumMonitor.get().addAttribute("firmware_version", firmwareVersion)
|
||||
GlobalRumMonitor.get().addAttribute("device_hardware", deviceHardware.hwModelSlug)
|
||||
}
|
||||
|
||||
fun Context.isGooglePlayAvailable(): Boolean =
|
||||
GoogleApiAvailabilityLight.getInstance().isGooglePlayServicesAvailable(this).let {
|
||||
it != ConnectionResult.SERVICE_MISSING && it != ConnectionResult.SERVICE_INVALID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,73 +17,32 @@
|
|||
|
||||
package com.geeksville.mesh.android
|
||||
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.util.Exceptions
|
||||
|
||||
/**
|
||||
* Created by kevinh on 12/24/14.
|
||||
*/
|
||||
|
||||
typealias LogPrinter = (Int, String, String) -> Unit
|
||||
import timber.log.Timber
|
||||
|
||||
interface Logging {
|
||||
|
||||
companion object {
|
||||
/** Some vendors strip log messages unless the severity is super high.
|
||||
*
|
||||
* alps == Soyes
|
||||
* HMD Global == mfg of the Nokia 7.2
|
||||
*/
|
||||
private val badVendors = setOf("OnePlus", "alps", "HMD Global", "Sony")
|
||||
private fun tag(): String = this.javaClass.name
|
||||
|
||||
/// if false NO logs will be shown, set this in the application based on BuildConfig.DEBUG
|
||||
var showLogs = true
|
||||
fun info(msg: String) = Timber.tag(tag()).i(msg)
|
||||
|
||||
/** if true, all logs will be printed at error level. Sometimes necessary for buggy ROMs
|
||||
* that filter logcat output below this level.
|
||||
*
|
||||
* Since there are so many bad vendors, we just always lie if we are a release build
|
||||
*/
|
||||
var forceErrorLevel = !BuildConfig.DEBUG || badVendors.contains(Build.MANUFACTURER)
|
||||
fun debug(msg: String) = Timber.tag(tag()).d(msg)
|
||||
|
||||
/// If false debug logs will not be shown (but others might)
|
||||
var showDebug = true
|
||||
fun warn(msg: String) = Timber.tag(tag()).w(msg)
|
||||
|
||||
/**
|
||||
* By default all logs are printed using the standard android Log class. But clients
|
||||
* can change printlog to a different implementation (for logging to files or via
|
||||
* google crashlytics)
|
||||
*/
|
||||
var printlog: LogPrinter = { level, tag, message ->
|
||||
if (showLogs) {
|
||||
if (showDebug || level > Log.DEBUG) {
|
||||
Log.println(if (forceErrorLevel) Log.ERROR else level, tag, message)
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Log an error message, note - we call this errormsg rather than error because error() is a stdlib function in
|
||||
* kotlin in the global namespace and we don't want users to accidentally call that.
|
||||
*/
|
||||
fun errormsg(msg: String, ex: Throwable? = null) {
|
||||
if (ex?.message != null) {
|
||||
Timber.tag(tag()).e(ex, msg)
|
||||
} else {
|
||||
Timber.tag(tag()).e(msg)
|
||||
}
|
||||
}
|
||||
|
||||
private fun tag(): String = this.javaClass.getName()
|
||||
|
||||
fun info(msg: String) = printlog(Log.INFO, tag(), msg)
|
||||
fun verbose(msg: String) = printlog(Log.VERBOSE, tag(), msg)
|
||||
fun debug(msg: String) = printlog(Log.DEBUG, tag(), msg)
|
||||
fun warn(msg: String) = printlog(Log.WARN, tag(), msg)
|
||||
|
||||
/**
|
||||
* Log an error message, note - we call this errormsg rather than error because error() is
|
||||
* a stdlib function in kotlin in the global namespace and we don't want users to accidentally call that.
|
||||
*/
|
||||
fun errormsg(msg: String, ex: Throwable? = null) {
|
||||
if (ex?.message != null)
|
||||
printlog(Log.ERROR, tag(), "$msg (exception ${ex.message})")
|
||||
else
|
||||
printlog(Log.ERROR, tag(), "$msg")
|
||||
}
|
||||
|
||||
/// Kotlin assertions are disabled on android, so instead we use this assert helper
|
||||
// / Kotlin assertions are disabled on android, so instead we use this assert helper
|
||||
fun logAssert(f: Boolean) {
|
||||
if (!f) {
|
||||
val ex = AssertionError("Assertion failed")
|
||||
|
|
@ -93,8 +52,8 @@ interface Logging {
|
|||
}
|
||||
}
|
||||
|
||||
/// Report an error (including messaging our crash reporter service if allowed
|
||||
// / Report an error (including messaging our crash reporter service if allowed
|
||||
fun reportError(s: String) {
|
||||
Exceptions.report(Exception("logging reportError: $s"), s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,6 +56,7 @@ import com.geeksville.mesh.database.entity.MyNodeEntity
|
|||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.database.entity.QuickChatAction
|
||||
import com.geeksville.mesh.database.entity.asDeviceVersion
|
||||
import com.geeksville.mesh.repository.api.DeviceHardwareRepository
|
||||
import com.geeksville.mesh.repository.api.FirmwareReleaseRepository
|
||||
import com.geeksville.mesh.repository.datastore.RadioConfigRepository
|
||||
import com.geeksville.mesh.repository.location.LocationRepository
|
||||
|
|
@ -192,6 +193,7 @@ constructor(
|
|||
private val radioConfigRepository: RadioConfigRepository,
|
||||
private val radioInterfaceService: RadioInterfaceService,
|
||||
private val meshLogRepository: MeshLogRepository,
|
||||
private val deviceHardwareRepository: DeviceHardwareRepository,
|
||||
private val packetRepository: PacketRepository,
|
||||
private val quickChatActionRepository: QuickChatActionRepository,
|
||||
private val locationRepository: LocationRepository,
|
||||
|
|
@ -219,8 +221,17 @@ constructor(
|
|||
viewModelScope.launch { _excludedModulesUnlocked.value = true }
|
||||
}
|
||||
|
||||
val firmwareVersion = myNodeInfo.mapNotNull { nodeInfo -> nodeInfo?.firmwareVersion }
|
||||
|
||||
val firmwareEdition = meshLogRepository.getMyNodeInfo().map { nodeInfo -> nodeInfo?.firmwareEdition }
|
||||
|
||||
val deviceHardware: StateFlow<DeviceHardware?> =
|
||||
ourNodeInfo
|
||||
.mapNotNull { nodeInfo ->
|
||||
nodeInfo?.user?.hwModel?.let { deviceHardwareRepository.getDeviceHardwareByModel(it.number) }
|
||||
}
|
||||
.stateIn(scope = viewModelScope, started = SharingStarted.WhileSubscribed(5_000), initialValue = null)
|
||||
|
||||
val clientNotification: StateFlow<MeshProtos.ClientNotification?> = radioConfigRepository.clientNotification
|
||||
|
||||
fun clearClientNotification(notification: MeshProtos.ClientNotification) {
|
||||
|
|
|
|||
|
|
@ -27,7 +27,9 @@ import kotlinx.coroutines.withContext
|
|||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class DeviceHardwareRepository @Inject constructor(
|
||||
class DeviceHardwareRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val apiDataSource: DeviceHardwareRemoteDataSource,
|
||||
private val localDataSource: DeviceHardwareLocalDataSource,
|
||||
private val jsonDataSource: DeviceHardwareJsonDataSource,
|
||||
|
|
@ -45,15 +47,13 @@ class DeviceHardwareRepository @Inject constructor(
|
|||
} else {
|
||||
val cachedHardware = localDataSource.getByHwModel(hwModel)
|
||||
if (cachedHardware != null && !isCacheExpired(cachedHardware.lastUpdated)) {
|
||||
debug("Using recent cached device hardware")
|
||||
val externalModel = cachedHardware.asExternalModel()
|
||||
return@withContext externalModel
|
||||
}
|
||||
}
|
||||
try {
|
||||
debug("Fetching device hardware from server")
|
||||
val deviceHardware = apiDataSource.getAllDeviceHardware()
|
||||
?: throw IOException("empty response from server")
|
||||
val deviceHardware =
|
||||
apiDataSource.getAllDeviceHardware() ?: throw IOException("empty response from server")
|
||||
localDataSource.insertAllDeviceHardware(deviceHardware)
|
||||
val cachedHardware = localDataSource.getByHwModel(hwModel)
|
||||
val externalModel = cachedHardware?.asExternalModel()
|
||||
|
|
@ -65,7 +65,6 @@ class DeviceHardwareRepository @Inject constructor(
|
|||
debug("Using stale cached device hardware")
|
||||
return@withContext cachedHardware.asExternalModel()
|
||||
}
|
||||
debug("Loading and caching device hardware from local JSON asset")
|
||||
localDataSource.insertAllDeviceHardware(jsonDataSource.loadDeviceHardwareFromJsonAsset())
|
||||
cachedHardware = localDataSource.getByHwModel(hwModel)
|
||||
val externalModel = cachedHardware?.asExternalModel()
|
||||
|
|
@ -78,10 +77,7 @@ class DeviceHardwareRepository @Inject constructor(
|
|||
localDataSource.deleteAllDeviceHardware()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cache is expired
|
||||
*/
|
||||
private fun isCacheExpired(lastUpdated: Long): Boolean {
|
||||
return System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
|
||||
}
|
||||
/** Check if the cache is expired */
|
||||
private fun isCacheExpired(lastUpdated: Long): Boolean =
|
||||
System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
|
||||
package com.geeksville.mesh.repository.api
|
||||
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.BuildUtils.warn
|
||||
import com.geeksville.mesh.database.entity.FirmwareRelease
|
||||
import com.geeksville.mesh.database.entity.FirmwareReleaseType
|
||||
|
|
@ -28,7 +27,9 @@ import kotlinx.coroutines.flow.flow
|
|||
import java.io.IOException
|
||||
import javax.inject.Inject
|
||||
|
||||
class FirmwareReleaseRepository @Inject constructor(
|
||||
class FirmwareReleaseRepository
|
||||
@Inject
|
||||
constructor(
|
||||
private val apiDataSource: FirmwareReleaseRemoteDataSource,
|
||||
private val localDataSource: FirmwareReleaseLocalDataSource,
|
||||
private val jsonDataSource: FirmwareReleaseJsonDataSource,
|
||||
|
|
@ -43,61 +44,50 @@ class FirmwareReleaseRepository @Inject constructor(
|
|||
|
||||
val alphaRelease: Flow<FirmwareRelease?> = getLatestFirmware(FirmwareReleaseType.ALPHA)
|
||||
|
||||
private fun getLatestFirmware(
|
||||
releaseType: FirmwareReleaseType,
|
||||
refresh: Boolean = false
|
||||
): Flow<FirmwareRelease?> = flow {
|
||||
if (refresh) {
|
||||
invalidateCache()
|
||||
} else {
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
if (cachedRelease != null && !isCacheExpired(cachedRelease.lastUpdated)) {
|
||||
debug("Using recent cached firmware release")
|
||||
val externalModel = cachedRelease.asExternalModel()
|
||||
private fun getLatestFirmware(releaseType: FirmwareReleaseType, refresh: Boolean = false): Flow<FirmwareRelease?> =
|
||||
flow {
|
||||
if (refresh) {
|
||||
invalidateCache()
|
||||
} else {
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
if (cachedRelease != null && !isCacheExpired(cachedRelease.lastUpdated)) {
|
||||
val externalModel = cachedRelease.asExternalModel()
|
||||
emit(externalModel)
|
||||
return@flow
|
||||
}
|
||||
}
|
||||
try {
|
||||
val networkFirmwareReleases =
|
||||
apiDataSource.getFirmwareReleases() ?: throw IOException("empty response from server")
|
||||
val releases =
|
||||
when (releaseType) {
|
||||
FirmwareReleaseType.STABLE -> networkFirmwareReleases.releases.stable
|
||||
FirmwareReleaseType.ALPHA -> networkFirmwareReleases.releases.alpha
|
||||
}
|
||||
localDataSource.insertFirmwareReleases(releases, releaseType)
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
val externalModel = cachedRelease?.asExternalModel()
|
||||
emit(externalModel)
|
||||
} catch (e: IOException) {
|
||||
warn("Failed to fetch firmware releases from server: ${e.message}")
|
||||
val jsonFirmwareReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
|
||||
val releases =
|
||||
when (releaseType) {
|
||||
FirmwareReleaseType.STABLE -> jsonFirmwareReleases.releases.stable
|
||||
FirmwareReleaseType.ALPHA -> jsonFirmwareReleases.releases.alpha
|
||||
}
|
||||
localDataSource.insertFirmwareReleases(releases, releaseType)
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
val externalModel = cachedRelease?.asExternalModel()
|
||||
emit(externalModel)
|
||||
return@flow
|
||||
}
|
||||
}
|
||||
try {
|
||||
debug("Fetching firmware releases from server")
|
||||
val networkFirmwareReleases = apiDataSource.getFirmwareReleases()
|
||||
?: throw IOException("empty response from server")
|
||||
val releases = when (releaseType) {
|
||||
FirmwareReleaseType.STABLE -> networkFirmwareReleases.releases.stable
|
||||
FirmwareReleaseType.ALPHA -> networkFirmwareReleases.releases.alpha
|
||||
}
|
||||
localDataSource.insertFirmwareReleases(
|
||||
releases,
|
||||
releaseType
|
||||
)
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
val externalModel = cachedRelease?.asExternalModel()
|
||||
emit(externalModel)
|
||||
} catch (e: IOException) {
|
||||
warn("Failed to fetch firmware releases from server: ${e.message}")
|
||||
val jsonFirmwareReleases = jsonDataSource.loadFirmwareReleaseFromJsonAsset()
|
||||
val releases = when (releaseType) {
|
||||
FirmwareReleaseType.STABLE -> jsonFirmwareReleases.releases.stable
|
||||
FirmwareReleaseType.ALPHA -> jsonFirmwareReleases.releases.alpha
|
||||
}
|
||||
localDataSource.insertFirmwareReleases(
|
||||
releases,
|
||||
releaseType
|
||||
)
|
||||
val cachedRelease = localDataSource.getLatestRelease(releaseType)
|
||||
val externalModel = cachedRelease?.asExternalModel()
|
||||
emit(externalModel)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun invalidateCache() {
|
||||
localDataSource.deleteAllFirmwareReleases()
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the cache is expired
|
||||
*/
|
||||
private fun isCacheExpired(lastUpdated: Long): Boolean {
|
||||
return System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
|
||||
}
|
||||
/** Check if the cache is expired */
|
||||
private fun isCacheExpired(lastUpdated: Long): Boolean =
|
||||
System.currentTimeMillis() - lastUpdated > CACHE_EXPIRATION_TIME_MS
|
||||
}
|
||||
|
|
|
|||
|
|
@ -83,6 +83,7 @@ import com.geeksville.mesh.BuildConfig
|
|||
import com.geeksville.mesh.MeshProtos
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.android.BuildUtils.debug
|
||||
import com.geeksville.mesh.android.setAttributes
|
||||
import com.geeksville.mesh.model.BluetoothViewModel
|
||||
import com.geeksville.mesh.model.DeviceVersion
|
||||
import com.geeksville.mesh.model.Node
|
||||
|
|
@ -306,6 +307,7 @@ fun MainScreen(
|
|||
}
|
||||
|
||||
@Composable
|
||||
@Suppress("LongMethod", "CyclomaticComplexMethod")
|
||||
private fun VersionChecks(viewModel: UIViewModel) {
|
||||
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
|
||||
val myNodeInfo by viewModel.myNodeInfo.collectAsStateWithLifecycle()
|
||||
|
|
@ -313,6 +315,10 @@ private fun VersionChecks(viewModel: UIViewModel) {
|
|||
|
||||
val firmwareEdition by viewModel.firmwareEdition.collectAsStateWithLifecycle(null)
|
||||
|
||||
val currentFirmwareVersion by viewModel.firmwareVersion.collectAsStateWithLifecycle(null)
|
||||
|
||||
val currentDeviceHardware by viewModel.deviceHardware.collectAsStateWithLifecycle(null)
|
||||
|
||||
val latestStableFirmwareRelease by viewModel.latestStableFirmwareRelease.collectAsState(DeviceVersion("2.6.4"))
|
||||
LaunchedEffect(connectionState, firmwareEdition) {
|
||||
if (connectionState == MeshService.ConnectionState.CONNECTED) {
|
||||
|
|
@ -330,6 +336,15 @@ private fun VersionChecks(viewModel: UIViewModel) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
LaunchedEffect(connectionState, currentFirmwareVersion, currentDeviceHardware) {
|
||||
if (connectionState == MeshService.ConnectionState.CONNECTED) {
|
||||
if (currentDeviceHardware != null && currentFirmwareVersion != null) {
|
||||
setAttributes(currentFirmwareVersion!!, currentDeviceHardware!!)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the device is running an old app version or firmware version
|
||||
LaunchedEffect(connectionState, myNodeInfo) {
|
||||
if (connectionState == MeshService.ConnectionState.CONNECTED) {
|
||||
|
|
|
|||
|
|
@ -28,6 +28,8 @@ buildscript {
|
|||
classpath(libs.firebase.crashlytics.gradle)
|
||||
classpath(libs.protobuf.gradle.plugin)
|
||||
classpath(libs.hilt.android.gradle.plugin)
|
||||
classpath(libs.secrets.gradle.plugin)
|
||||
classpath(libs.dd.sdk.android.gradle.plugin)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
#[allow(unused)]
|
||||
[versions]
|
||||
accompanistPermissions = "0.37.3"
|
||||
adaptive = "1.2.0-alpha10"
|
||||
|
|
@ -13,6 +14,8 @@ core-location-altitude = "1.0.0-alpha03"
|
|||
core-splashscreen = "1.0.1"
|
||||
crashlytics = "3.0.5"
|
||||
datastore = "1.1.7"
|
||||
dd-sdk-android = "2.25.0"
|
||||
dd-sdk-android-gradle-plugin = "1.18.0"
|
||||
detekt = "1.23.8"
|
||||
devtools-ksp = "2.2.0-2.0.2"
|
||||
emoji2 = "1.5.0"
|
||||
|
|
@ -43,7 +46,9 @@ protobuf-gradle-plugin = "0.9.5"
|
|||
protobuf-kotlin = "4.31.1"
|
||||
retrofit = "3.0.0"
|
||||
room = "2.7.2"
|
||||
secrets-gradle-plugin = "2.0.1"
|
||||
streamsupport-minifuture = "1.7.4"
|
||||
timber = "5.0.1"
|
||||
usb-serial-android = "3.9.0"
|
||||
work-runtime-ktx = "2.10.3"
|
||||
zxing-android-embedded = "4.3.0"
|
||||
|
|
@ -82,6 +87,11 @@ core-location-altitude = { group = "androidx.core", name = "core-location-altitu
|
|||
core-splashscreen = { group = "androidx.core", name = "core-splashscreen", version.ref = "core-splashscreen" }
|
||||
datastore = { group = "androidx.datastore", name = "datastore", version.ref = "datastore" }
|
||||
datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "datastore" }
|
||||
dd-sdk-android-compose = { group = "com.datadoghq", name = "dd-sdk-android-compose", version.ref = "dd-sdk-android" }
|
||||
dd-sdk-android-gradle-plugin = { group = "com.datadoghq", name = "dd-sdk-android-gradle-plugin", version.ref = "dd-sdk-android-gradle-plugin" }
|
||||
dd-sdk-android-logs = { group = "com.datadoghq", name = "dd-sdk-android-logs", version.ref = "dd-sdk-android" }
|
||||
dd-sdk-android-rum = { group = "com.datadoghq", name = "dd-sdk-android-rum", version.ref = "dd-sdk-android" }
|
||||
dd-sdk-android-timber = { group = "com.datadoghq", name = "dd-sdk-android-timber", version.ref = "dd-sdk-android" }
|
||||
detekt-formatting = { group = "io.gitlab.arturbosch.detekt", name = "detekt-formatting", version.ref = "detekt" }
|
||||
emoji2-emojipicker = { group = "androidx.emoji2", name = "emoji2-emojipicker", version.ref = "emoji2" }
|
||||
espresso-core = { group = "androidx.test.espresso", name = "espresso-core", version.ref = "espresso-core" }
|
||||
|
|
@ -134,7 +144,9 @@ room-compiler = { group = "androidx.room", name = "room-compiler", version.ref =
|
|||
room-ktx = { group = "androidx.room", name = "room-ktx", version.ref = "room" }
|
||||
room-runtime = { group = "androidx.room", name = "room-runtime", version.ref = "room" }
|
||||
room-testing = { group = "androidx.room", name = "room-testing", version.ref = "room" }
|
||||
secrets-gradle-plugin = { group = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin", name = "secrets-gradle-plugin", version.ref = "secrets-gradle-plugin" }
|
||||
streamsupport-minifuture = { group = "net.sourceforge.streamsupport", name = "streamsupport-minifuture", version.ref = "streamsupport-minifuture" }
|
||||
timber = { group = "com.jakewharton.timber", name = "timber", version.ref = "timber" }
|
||||
usb-serial-android = { group = "com.github.mik3y", name = "usb-serial-for-android", version.ref = "usb-serial-android" }
|
||||
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime-ktx" }
|
||||
zxing-android-embedded = { group = "com.journeyapps", name = "zxing-android-embedded", version.ref = "zxing-android-embedded" }
|
||||
|
|
@ -180,6 +192,9 @@ osm = ["osmdroid-android", "osmbonuspack", "mgrs"]
|
|||
# Firebase
|
||||
firebase = ["firebase-analytics", "firebase-crashlytics"]
|
||||
|
||||
# Datadog
|
||||
datadog = ["dd-sdk-android-compose", "dd-sdk-android-logs", "dd-sdk-android-timber", "dd-sdk-android-rum"]
|
||||
|
||||
# Protobuf
|
||||
protobuf = ["protobuf-kotlin"]
|
||||
|
||||
|
|
@ -192,6 +207,7 @@ coil = ["coil", "coil-network-core", "coil-network-okhttp", "coil-svg"]
|
|||
[plugins]
|
||||
android-application = { id = "com.android.application" }
|
||||
compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" }
|
||||
datadog = { id = "com.datadoghq.dd-sdk-android-gradle-plugin"}
|
||||
detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
|
||||
devtools-ksp = { id = "com.google.devtools.ksp", version.ref = "devtools-ksp" }
|
||||
hilt = { id = "com.google.dagger.hilt.android" }
|
||||
|
|
@ -203,4 +219,5 @@ protobuf = { id = "com.google.protobuf" }
|
|||
android-library = { id = "com.android.library" }
|
||||
google-services = { id = "com.google.gms.google-services" }
|
||||
firebase-crashlytics = { id = "com.google.firebase.crashlytics" }
|
||||
secrets-gradle-plugin = { id = "com.google.android.libraries.mapsplatform.secrets-gradle-plugin"}
|
||||
spotless = { id = "com.diffplug.spotless", version .ref= "spotless" }
|
||||
|
|
|
|||
25
secrets.defaults.properties
Normal file
25
secrets.defaults.properties
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#
|
||||
# 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/>.
|
||||
#
|
||||
|
||||
# These are placeholder values for the Meshtastic Android App secrets.
|
||||
|
||||
# Datadog API keys for crash reporting and analytics
|
||||
# Replace these with actual keys when building the app to enable datadog reporting
|
||||
datadogClientToken=faketoken1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
|
||||
datadogApplicationId=fakeappid1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef
|
||||
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue