diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml
index 03c5bbed1..b266b618b 100644
--- a/.github/workflows/android.yml
+++ b/.github/workflows/android.yml
@@ -52,13 +52,20 @@ jobs:
- name: Run local tests
run: ./gradlew testFdroidDebug testGoogleDebug
- - name: Upload debug artifact
+ - name: Upload fdroid debug artifact
uses: actions/upload-artifact@v4
with:
name: fdroidDebug
path: app/build/outputs/apk/fdroid/debug/app-fdroid-debug.apk
retention-days: 30
+ - name: Upload sideload debug artifact
+ uses: actions/upload-artifact@v4
+ with:
+ name: sideloadDebug
+ path: app/build/outputs/apk/sideload/debug/app-sideload-debug.apk
+ retention-days: 30
+
- name: Upload build reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v4
diff --git a/app/build.gradle b/app/build.gradle
index c85287cf3..2b9229987 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -41,6 +41,11 @@ android {
}
flavorDimensions = ['default']
productFlavors {
+ sideload {
+ dimension = 'default'
+ applicationIdSuffix ".sideload"
+ versionNameSuffix "-sideload"
+ }
fdroid {
dimension = 'default'
dependenciesInfo {
diff --git a/app/src/sideload/java/com/geeksville/mesh/MeshUtilApplication.kt b/app/src/sideload/java/com/geeksville/mesh/MeshUtilApplication.kt
new file mode 100644
index 000000000..1674ad754
--- /dev/null
+++ b/app/src/sideload/java/com/geeksville/mesh/MeshUtilApplication.kt
@@ -0,0 +1,33 @@
+/*
+ * 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 .
+ */
+
+package com.geeksville.mesh
+
+import com.geeksville.mesh.android.GeeksvilleApplication
+import com.geeksville.mesh.android.Logging
+import dagger.hilt.android.HiltAndroidApp
+
+@HiltAndroidApp
+class MeshUtilApplication : GeeksvilleApplication() {
+
+ override fun onCreate() {
+ super.onCreate()
+
+ Logging.showLogs = BuildConfig.DEBUG
+
+ }
+}
\ No newline at end of file
diff --git a/app/src/sideload/java/com/geeksville/mesh/analytics/NopAnalytics.kt b/app/src/sideload/java/com/geeksville/mesh/analytics/NopAnalytics.kt
new file mode 100644
index 000000000..658c4370b
--- /dev/null
+++ b/app/src/sideload/java/com/geeksville/mesh/analytics/NopAnalytics.kt
@@ -0,0 +1,69 @@
+/*
+ * 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 .
+ */
+
+package com.geeksville.mesh.analytics
+
+import android.content.Context
+import com.geeksville.mesh.android.Logging
+
+class DataPair(val name: String, valueIn: Any?) {
+ val value = valueIn ?: "null"
+
+ /// An accumulating firebase event - only one allowed per event
+ constructor(d: Double) : this("BOGUS", d)
+ constructor(d: Int) : this("BOGUS", d)
+}
+
+/**
+ * Implement our analytics API using Firebase Analytics
+ */
+@Suppress("UNUSED_PARAMETER")
+class NopAnalytics(context: Context) : AnalyticsProvider, Logging {
+
+ init {
+ }
+
+ override fun setEnabled(on: Boolean) {
+ }
+
+ override fun endSession() {
+ }
+
+ override fun trackLowValue(event: String, vararg properties: DataPair) {
+ }
+
+ override fun track(event: String, vararg properties: DataPair) {
+ }
+
+ override fun startSession() {
+ }
+
+ override fun setUserInfo(vararg p: DataPair) {
+ }
+
+ override fun increment(name: String, amount: Double) {
+ }
+
+ /**
+ * Send a google analytics screen view event
+ */
+ override fun sendScreenView(name: String) {
+ }
+
+ override fun endScreenView() {
+ }
+}
diff --git a/app/src/sideload/java/com/geeksville/mesh/android/GeeksvilleApplication.kt b/app/src/sideload/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
new file mode 100644
index 000000000..f58046e30
--- /dev/null
+++ b/app/src/sideload/java/com/geeksville/mesh/android/GeeksvilleApplication.kt
@@ -0,0 +1,73 @@
+/*
+ * 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 .
+ */
+
+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.core.content.edit
+import com.geeksville.mesh.analytics.AnalyticsProvider
+
+open 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") ?: null
+ if(testLabSetting != null)
+ info("Testlab is $testLabSetting")
+ return "true" == testLabSetting
+ }
+
+ private val analyticsPrefs: SharedPreferences by lazy {
+ getSharedPreferences("analytics-prefs", Context.MODE_PRIVATE)
+ }
+
+ var isAnalyticsAllowed: Boolean
+ get() = analyticsPrefs.getBoolean("allowed", true)
+ set(value) {
+ analyticsPrefs.edit {
+ putBoolean("allowed", value)
+ }
+
+ // Change the flag with the providers
+ analytics.setEnabled(value && !isInTestLab) // Never do analytics in the test lab
+ }
+
+ @Suppress("UNUSED_PARAMETER")
+ fun askToRate(activity: AppCompatActivity) {
+ // do nothing
+ }
+
+ override fun onCreate() {
+ super.onCreate()
+
+ val nopAnalytics = com.geeksville.mesh.analytics.NopAnalytics(this)
+ analytics = nopAnalytics
+ isAnalyticsAllowed = false
+ }
+}
+
+fun Context.isGooglePlayAvailable(): Boolean = false
\ No newline at end of file
diff --git a/app/src/sideload/res/drawable/ic_launcher2_background.xml b/app/src/sideload/res/drawable/ic_launcher2_background.xml
new file mode 100644
index 000000000..1af9b2589
--- /dev/null
+++ b/app/src/sideload/res/drawable/ic_launcher2_background.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
diff --git a/app/src/sideload/res/drawable/ic_launcher2_foreground.xml b/app/src/sideload/res/drawable/ic_launcher2_foreground.xml
new file mode 100644
index 000000000..8a7b5767a
--- /dev/null
+++ b/app/src/sideload/res/drawable/ic_launcher2_foreground.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
diff --git a/app/src/sideload/res/values/strings.xml b/app/src/sideload/res/values/strings.xml
new file mode 100644
index 000000000..fdad9f7a7
--- /dev/null
+++ b/app/src/sideload/res/values/strings.xml
@@ -0,0 +1,4 @@
+
+
+ Meshtastic-sideload"
+
\ No newline at end of file