diff --git a/.idea/vcs.xml b/.idea/vcs.xml
index e8f81507d..bc2dfb5ae 100644
--- a/.idea/vcs.xml
+++ b/.idea/vcs.xml
@@ -3,6 +3,7 @@
+
diff --git a/app/build.gradle b/app/build.gradle
index 112bb7572..589fa3613 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -31,8 +31,8 @@ android {
applicationId "com.geeksville.mesh"
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
targetSdkVersion 29
- versionCode 20139 // format is Mmmss (where M is 1+the numeric major number
- versionName "1.1.39"
+ versionCode 20142 // format is Mmmss (where M is 1+the numeric major number
+ versionName "1.1.42"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
// per https://developer.android.com/studio/write/vector-asset-studio
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
index 47e05df48..b8a4ff2f4 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshService.kt
@@ -73,7 +73,9 @@ class MeshService : Service(), Logging {
class IdNotFoundException(id: String) : Exception("ID not found $id")
class NodeNumNotFoundException(id: Int) : Exception("NodeNum not found $id")
- class IsUpdatingException() : Exception("Operation prohibited during firmware update")
+
+ /** We treat software update as similar to loss of comms to the regular bluetooth service (so things like sendPosition for background GPS ignores the problem */
+ class IsUpdatingException() : RadioNotConnectedException("Operation prohibited during firmware update")
/**
* Talk to our running service and try to set a new device address. And then immediately
@@ -342,6 +344,9 @@ class MeshService : Service(), Logging {
radio.close()
saveSettings()
+ stopForeground(true) // Make sure we aren't using the notification first
+ serviceNotifications.close()
+
super.onDestroy()
serviceJob.cancel()
}
@@ -1085,6 +1090,9 @@ class MeshService : Service(), Logging {
setFirmwareUpdateFilename(myInfo)
+ val a = RadioInterfaceService.getBondedDeviceAddress(this)
+ val isBluetoothInterface = a != null && a.startsWith("x")
+
val mi = with(myInfo) {
MyNodeInfo(
myNodeNum,
@@ -1093,7 +1101,7 @@ class MeshService : Service(), Logging {
hwModel,
firmwareVersion,
firmwareUpdateFilename != null,
- SoftwareUpdateService.shouldUpdate(
+ isBluetoothInterface && SoftwareUpdateService.shouldUpdate(
this@MeshService,
DeviceVersion(firmwareVersion)
),
diff --git a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt
index ddf55a306..1217c4699 100644
--- a/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/MeshServiceNotifications.kt
@@ -6,27 +6,40 @@ import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
+import android.graphics.Bitmap
+import android.graphics.BitmapFactory
+import android.graphics.Canvas
import android.graphics.Color
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.core.app.NotificationCompat
+import androidx.core.content.ContextCompat
+import androidx.core.graphics.drawable.DrawableCompat
import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MainActivity
import com.geeksville.mesh.R
import com.geeksville.mesh.android.notificationManager
import com.geeksville.mesh.utf8
+import java.io.Closeable
+
class MeshServiceNotifications(
private val context: Context
-) {
+) : Closeable
+{
private val notificationManager: NotificationManager get() = context.notificationManager
val notifyId = 101
+ private var largeIcon: Bitmap? = null
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel(): String {
val channelId = "my_service"
val channelName = context.getString(R.string.meshtastic_service_notifications)
- val channel = NotificationChannel(channelId, channelName, NotificationManager.IMPORTANCE_HIGH).apply {
+ val channel = NotificationChannel(
+ channelId,
+ channelName,
+ NotificationManager.IMPORTANCE_HIGH
+ ).apply {
lightColor = Color.BLUE
importance = NotificationManager.IMPORTANCE_NONE
lockscreenVisibility = Notification.VISIBILITY_PRIVATE
@@ -61,6 +74,25 @@ class MeshServiceNotifications(
PendingIntent.getActivity(context, 0, Intent(context, MainActivity::class.java), 0)
}
+ /**
+ * Generate a bitmap from a vector drawable (even on old builds)
+ * https://stackoverflow.com/questions/33696488/getting-bitmap-from-vector-drawable
+ */
+ fun getBitmapFromVectorDrawable(drawableId: Int): Bitmap {
+ var drawable = ContextCompat.getDrawable(context, drawableId)!!
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+ drawable = DrawableCompat.wrap(drawable).mutate()
+ }
+ val bitmap = Bitmap.createBitmap(
+ drawable.intrinsicWidth,
+ drawable.intrinsicHeight, Bitmap.Config.ARGB_8888
+ )
+ val canvas = Canvas(bitmap)
+ drawable.setBounds(0, 0, canvas.width, canvas.height)
+ drawable.draw(canvas)
+ return bitmap
+ }
+
/**
* Generate a new version of our notification - reflecting current app state
*/
@@ -69,11 +101,16 @@ class MeshServiceNotifications(
summaryString: String,
senderName: String
): Notification {
+ // We delay making this bitmap until we know we need it
+ if(largeIcon == null)
+ largeIcon = getBitmapFromVectorDrawable(R.mipmap.ic_launcher2)
+
val category = if (recentReceivedText != null) Notification.CATEGORY_SERVICE else Notification.CATEGORY_MESSAGE
val builder = NotificationCompat.Builder(context, channelId).setOngoing(true)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setCategory(category)
- .setSmallIcon(R.drawable.app_icon)
+ .setSmallIcon(if (Build.VERSION.SDK_INT < Build.VERSION_CODES.N) R.drawable.app_icon_novect else R.drawable.app_icon) // vector form icons don't work reliably on older androids
+ .setLargeIcon(largeIcon) // we must include a large icon because of a bug in cyanogenmod https://github.com/open-keychain/open-keychain/issues/1356#issue-89493995
.setContentTitle(summaryString) // leave this off for now so our notification looks smaller
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
.setContentIntent(openAppIntent)
@@ -93,4 +130,9 @@ class MeshServiceNotifications(
return builder.build()
}
+
+ override fun close() {
+ largeIcon?.recycle()
+ largeIcon = null
+ }
}
diff --git a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
index c24e5aa87..78890c334 100644
--- a/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
+++ b/app/src/main/java/com/geeksville/mesh/service/RadioInterfaceService.kt
@@ -22,7 +22,7 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
-class RadioNotConnectedException(message: String = "Not connected to radio") :
+open class RadioNotConnectedException(message: String = "Not connected to radio") :
BLEException(message)
diff --git a/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt
index 21d5a650c..7a694e408 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/MapFragment.kt
@@ -136,7 +136,7 @@ class MapFragment : ScreenFragment("Map"), Logging {
* Mapbox native code can crash painfully if you ever call a mapbox view function while the view is not actively being show
*/
private val isViewVisible: Boolean
- get() = view != null && isResumed
+ get() = view != null && isResumed && (mapView?.isDestroyed != false)
override fun onViewCreated(viewIn: View, savedInstanceState: Bundle?) {
super.onViewCreated(viewIn, savedInstanceState)
@@ -145,9 +145,9 @@ class MapFragment : ScreenFragment("Map"), Logging {
if ((requireContext().applicationContext as GeeksvilleApplication).isAnalyticsAllowed) {
val vIn = viewIn.findViewById(R.id.mapView)
mapView = vIn
- vIn.onCreate(savedInstanceState)
-
mapView?.let { v ->
+ v.onCreate(savedInstanceState)
+
// Each time the pane is shown start fetching new map info (we do this here instead of
// onCreate because getMapAsync can die in native code if the view goes away)
v.getMapAsync { map ->
@@ -205,14 +205,25 @@ class MapFragment : ScreenFragment("Map"), Logging {
}
override fun onDestroyView() {
- mapView?.onDestroy()
super.onDestroyView()
+ mapView?.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
- mapView?.onSaveInstanceState(outState)
+ mapView?.let {
+ if (!it.isDestroyed)
+ it.onSaveInstanceState(outState)
+ }
super.onSaveInstanceState(outState)
}
+
+ override fun onLowMemory() {
+ mapView?.let {
+ if (!it.isDestroyed)
+ it.onLowMemory()
+ }
+ super.onLowMemory()
+ }
}
diff --git a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
index ea591e498..92933db54 100644
--- a/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
+++ b/app/src/main/java/com/geeksville/mesh/ui/SettingsFragment.kt
@@ -698,6 +698,56 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
binding.scanProgressBar.visibility = visible
binding.deviceRadioGroup.visibility = visible
}
+ private fun updateDevicesButtons( devices: MutableMap?) {
+ // Remove the old radio buttons and repopulate
+ binding.deviceRadioGroup.removeAllViews()
+
+ if(devices == null) return
+
+ val adapter = scanModel.bluetoothAdapter
+ var hasShownOurDevice = false
+ devices.values.forEach { device ->
+ if (device.address == scanModel.selectedNotNull)
+ hasShownOurDevice = true
+ addDeviceButton(device, true)
+ }
+
+ // The selected device is not in the scan; it is either offline, or it doesn't advertise
+ // itself (most BLE devices don't advertise when connected).
+ // Show it in the list, greyed out based on connection status.
+ if (!hasShownOurDevice) {
+ // Note: we pull this into a tempvar, because otherwise some other thread can change selectedAddress after our null check
+ // and before use
+ val bleAddr = scanModel.selectedBluetooth
+
+ if (bleAddr != null && adapter != null && adapter.isEnabled) {
+ val bDevice =
+ adapter.getRemoteDevice(bleAddr)
+ if (bDevice.name != null) { // ignore nodes that node have a name, that means we've lost them since they appeared
+ val curDevice = BTScanModel.DeviceListEntry(
+ bDevice.name,
+ scanModel.selectedAddress!!,
+ bDevice.bondState == BOND_BONDED
+ )
+ addDeviceButton(curDevice, model.isConnected.value == MeshService.ConnectionState.CONNECTED)
+ }
+ } else if (scanModel.selectedUSB != null) {
+ // Must be a USB device, show a placeholder disabled entry
+ val curDevice = BTScanModel.DeviceListEntry(
+ scanModel.selectedUSB!!,
+ scanModel.selectedAddress!!,
+ false
+ )
+ addDeviceButton(curDevice, false)
+ }
+ }
+
+ val hasBonded =
+ RadioInterfaceService.getBondedDeviceAddress(requireContext()) != null
+
+ // get rid of the warning text once at least one device is paired
+ binding.warningNotPaired.visibility = if (hasBonded) View.GONE else View.VISIBLE
+ }
/// Setup the GUI to do a classic (pre SDK 26 BLE scan)
private fun initClassicScan() {
@@ -719,54 +769,13 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
}
})
- scanModel.devices.observe(viewLifecycleOwner, Observer { devices ->
- // Remove the old radio buttons and repopulate
- binding.deviceRadioGroup.removeAllViews()
+ scanModel.devices.observe(
+ viewLifecycleOwner,
+ Observer { devices -> updateDevicesButtons(devices) })
- val adapter = scanModel.bluetoothAdapter
-
- var hasShownOurDevice = false
- devices.values.forEach { device ->
- if (device.address == scanModel.selectedNotNull)
- hasShownOurDevice = true
- addDeviceButton(device, true)
- }
-
- // The device the user is already paired with is offline currently, still show it
- // it in the list, but greyed out
- if (!hasShownOurDevice) {
- // Note: we pull this into a tempvar, because otherwise some other thread can change selectedAddress after our null check
- // and before use
- val bleAddr = scanModel.selectedBluetooth
-
- if (bleAddr != null && adapter != null && adapter.isEnabled) {
- val bDevice =
- adapter.getRemoteDevice(bleAddr)
- if (bDevice.name != null) { // ignore nodes that node have a name, that means we've lost them since they appeared
- val curDevice = BTScanModel.DeviceListEntry(
- bDevice.name,
- scanModel.selectedAddress!!,
- bDevice.bondState == BOND_BONDED
- )
- addDeviceButton(curDevice, false)
- }
- } else if (scanModel.selectedUSB != null) {
- // Must be a USB device, show a placeholder disabled entry
- val curDevice = BTScanModel.DeviceListEntry(
- scanModel.selectedUSB!!,
- scanModel.selectedAddress!!,
- false
- )
- addDeviceButton(curDevice, false)
- }
- }
-
- val hasBonded =
- RadioInterfaceService.getBondedDeviceAddress(requireContext()) != null
-
- // get rid of the warning text once at least one device is paired
- binding.warningNotPaired.visibility = if (hasBonded) View.GONE else View.VISIBLE
- })
+ model.isConnected.observe(
+ viewLifecycleOwner,
+ { updateDevicesButtons(scanModel.devices.value) })
}
/// Start running the modern scan, once it has one result we enable the
diff --git a/app/src/main/res/drawable-hdpi/app_icon_novect.png b/app/src/main/res/drawable-hdpi/app_icon_novect.png
new file mode 120000
index 000000000..ef5958e92
--- /dev/null
+++ b/app/src/main/res/drawable-hdpi/app_icon_novect.png
@@ -0,0 +1 @@
+app_icon.png
\ No newline at end of file
diff --git a/app/src/main/res/drawable-mdpi/app_icon_novect.png b/app/src/main/res/drawable-mdpi/app_icon_novect.png
new file mode 120000
index 000000000..ef5958e92
--- /dev/null
+++ b/app/src/main/res/drawable-mdpi/app_icon_novect.png
@@ -0,0 +1 @@
+app_icon.png
\ No newline at end of file
diff --git a/app/src/main/res/drawable-xhdpi/app_icon_novect.png b/app/src/main/res/drawable-xhdpi/app_icon_novect.png
new file mode 120000
index 000000000..ef5958e92
--- /dev/null
+++ b/app/src/main/res/drawable-xhdpi/app_icon_novect.png
@@ -0,0 +1 @@
+app_icon.png
\ No newline at end of file
diff --git a/app/src/main/res/drawable-xxhdpi/app_icon_novect.png b/app/src/main/res/drawable-xxhdpi/app_icon_novect.png
new file mode 120000
index 000000000..ef5958e92
--- /dev/null
+++ b/app/src/main/res/drawable-xxhdpi/app_icon_novect.png
@@ -0,0 +1 @@
+app_icon.png
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
index 5c61f9d69..cfef6d972 100644
--- a/build.gradle
+++ b/build.gradle
@@ -1,7 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
- ext.kotlin_version = '1.4.21'
+ ext.kotlin_version = '1.4.30'
ext.coroutines_version = "1.3.9"
repositories {
diff --git a/geeksville-androidlib b/geeksville-androidlib
index f3812d848..d7c3fa8ab 160000
--- a/geeksville-androidlib
+++ b/geeksville-androidlib
@@ -1 +1 @@
-Subproject commit f3812d8484c571f62c72d1509a1e02357fda5b8e
+Subproject commit d7c3fa8ab6a47169e5dc8761d03d24588c3dd845