mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
commit
88a4825b28
7 changed files with 92 additions and 72 deletions
|
|
@ -42,8 +42,8 @@ android {
|
|||
applicationId "com.geeksville.mesh"
|
||||
minSdkVersion 21 // The oldest emulator image I have tried is 22 (though 21 probably works)
|
||||
targetSdkVersion 30 // 30 can't work until an explicit location permissions dialog is added
|
||||
versionCode 20254 // format is Mmmss (where M is 1+the numeric major number
|
||||
versionName "1.2.54"
|
||||
versionCode 20255 // format is Mmmss (where M is 1+the numeric major number
|
||||
versionName "1.2.55"
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
|
||||
// per https://developer.android.com/studio/write/vector-asset-studio
|
||||
|
|
@ -88,10 +88,10 @@ android {
|
|||
kotlinOptions {
|
||||
jvmTarget = "1.8"
|
||||
}
|
||||
|
||||
lintOptions {
|
||||
lint {
|
||||
abortOnError false
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
play {
|
||||
|
|
|
|||
|
|
@ -36,18 +36,13 @@ import androidx.core.content.ContextCompat
|
|||
import androidx.fragment.app.Fragment
|
||||
import androidx.fragment.app.FragmentManager
|
||||
import androidx.fragment.app.FragmentTransaction
|
||||
import androidx.lifecycle.Observer
|
||||
import androidx.viewpager2.adapter.FragmentStateAdapter
|
||||
import com.geeksville.android.BindFailedException
|
||||
import com.geeksville.android.GeeksvilleApplication
|
||||
import com.geeksville.android.Logging
|
||||
import com.geeksville.android.ServiceClient
|
||||
import com.geeksville.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.android.getLocationPermissions
|
||||
import com.geeksville.mesh.android.getBackgroundPermissions
|
||||
import com.geeksville.mesh.android.getCameraPermissions
|
||||
import com.geeksville.mesh.android.getMissingPermissions
|
||||
import com.geeksville.mesh.android.getScanPermissions
|
||||
import com.geeksville.mesh.android.*
|
||||
import com.geeksville.mesh.database.entity.Packet
|
||||
import com.geeksville.mesh.databinding.ActivityMainBinding
|
||||
import com.geeksville.mesh.model.ChannelSet
|
||||
|
|
@ -70,7 +65,6 @@ import com.vorlonsoft.android.rate.AppRate
|
|||
import com.vorlonsoft.android.rate.StoreType
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.FileOutputStream
|
||||
import java.lang.Runnable
|
||||
import java.nio.charset.Charset
|
||||
import java.text.DateFormat
|
||||
import java.util.*
|
||||
|
|
@ -198,7 +192,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
}
|
||||
}
|
||||
|
||||
private val btStateReceiver = BluetoothStateReceiver { _ ->
|
||||
private val btStateReceiver = BluetoothStateReceiver {
|
||||
updateBluetoothEnabled()
|
||||
}
|
||||
|
||||
|
|
@ -249,15 +243,8 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
*/
|
||||
private fun updateBluetoothEnabled() {
|
||||
var enabled = false // assume failure
|
||||
val requiredPerms: MutableList<String> = mutableListOf()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
requiredPerms.add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
} else {
|
||||
requiredPerms.add(Manifest.permission.BLUETOOTH)
|
||||
}
|
||||
|
||||
if (getMissingPermissions(requiredPerms).isEmpty()) {
|
||||
if (hasConnectPermission()) {
|
||||
/// ask the adapter if we have access
|
||||
bluetoothAdapter?.apply {
|
||||
enabled = isEnabled
|
||||
|
|
@ -309,6 +296,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
/**
|
||||
* @return a localized string warning user about missing permissions. Or null if everything is find
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
fun getMissingMessage(
|
||||
missingPerms: List<String> = getMinimumPermissions()
|
||||
): String? {
|
||||
|
|
@ -338,7 +326,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
}
|
||||
|
||||
/** Possibly prompt user to grant permissions
|
||||
* @param shouldShowDialog usually true, but in cases where we've already shown a dialog elsewhere we skip it.
|
||||
* @param shouldShowDialog usually false in cases where we've already shown a dialog elsewhere we skip it.
|
||||
*
|
||||
* @return true if we already have the needed permissions
|
||||
*/
|
||||
|
|
@ -542,9 +530,9 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
tab.icon = ContextCompat.getDrawable(this, tabInfos[position].icon)
|
||||
}.attach()
|
||||
|
||||
model.isConnected.observe(this, Observer { connected ->
|
||||
model.isConnected.observe(this) { connected ->
|
||||
updateConnectionStatusImage(connected)
|
||||
})
|
||||
}
|
||||
|
||||
// Handle any intent
|
||||
handleIntent(intent)
|
||||
|
|
@ -640,7 +628,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
/**
|
||||
* Dispatch incoming result to the correct fragment.
|
||||
*/
|
||||
@SuppressLint("InlinedApi")
|
||||
@SuppressLint("InlinedApi", "MissingPermission")
|
||||
override fun onActivityResult(
|
||||
requestCode: Int,
|
||||
resultCode: Int,
|
||||
|
|
@ -937,10 +925,10 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
private var connectionJob: Job? = null
|
||||
|
||||
private val mesh = object :
|
||||
ServiceClient<com.geeksville.mesh.IMeshService>({
|
||||
com.geeksville.mesh.IMeshService.Stub.asInterface(it)
|
||||
ServiceClient<IMeshService>({
|
||||
IMeshService.Stub.asInterface(it)
|
||||
}) {
|
||||
override fun onConnected(service: com.geeksville.mesh.IMeshService) {
|
||||
override fun onConnected(service: IMeshService) {
|
||||
|
||||
/*
|
||||
Note: we must call this callback in a coroutine. Because apparently there is only a single activity looper thread. and if that onConnected override
|
||||
|
|
@ -1075,18 +1063,21 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
super.onStop()
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onStart() {
|
||||
super.onStart()
|
||||
|
||||
// Ask to start bluetooth if no USB devices are visible
|
||||
val hasUSB = SerialInterface.findDrivers(this).isNotEmpty()
|
||||
if (!isInTestLab && !hasUSB) {
|
||||
bluetoothAdapter?.let {
|
||||
if (!it.isEnabled) {
|
||||
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
|
||||
if (hasConnectPermission()) {
|
||||
bluetoothAdapter?.let {
|
||||
if (!it.isEnabled) {
|
||||
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
||||
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else requestPermission()
|
||||
}
|
||||
|
||||
try {
|
||||
|
|
@ -1154,12 +1145,7 @@ class MainActivity : AppCompatActivity(), Logging,
|
|||
val str = "Ping " + DateFormat.getTimeInstance(DateFormat.MEDIUM)
|
||||
.format(Date(System.currentTimeMillis()))
|
||||
model.messagesState.sendMessage(str)
|
||||
handler.postDelayed(
|
||||
Runnable {
|
||||
postPing()
|
||||
},
|
||||
30000
|
||||
)
|
||||
handler.postDelayed({ postPing() }, 30000)
|
||||
}
|
||||
item.isChecked = !item.isChecked // toggle ping test
|
||||
if (item.isChecked)
|
||||
|
|
|
|||
|
|
@ -29,6 +29,24 @@ fun Context.getMissingPermissions(perms: List<String>) = perms.filter {
|
|||
) != PackageManager.PERMISSION_GRANTED
|
||||
}
|
||||
|
||||
/**
|
||||
* Bluetooth connect permissions (or empty if we already have what we need)
|
||||
*/
|
||||
fun Context.getConnectPermissions(): List<String> {
|
||||
val perms = mutableListOf<String>()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
perms.add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
} else {
|
||||
perms.add(Manifest.permission.BLUETOOTH)
|
||||
}
|
||||
|
||||
return getMissingPermissions(perms)
|
||||
}
|
||||
|
||||
/** @return true if the user already has Bluetooth connect permission */
|
||||
fun Context.hasConnectPermission() = getConnectPermissions().isEmpty()
|
||||
|
||||
/**
|
||||
* Bluetooth scan/discovery permissions (or empty if we already have what we need)
|
||||
*/
|
||||
|
|
|
|||
|
|
@ -1,9 +1,11 @@
|
|||
package com.geeksville.mesh.service
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.bluetooth.BluetoothAdapter
|
||||
import android.bluetooth.BluetoothGattCharacteristic
|
||||
import android.bluetooth.BluetoothGattService
|
||||
import android.bluetooth.BluetoothManager
|
||||
import android.companion.CompanionDeviceManager
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build
|
||||
|
|
@ -91,13 +93,13 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
|||
}
|
||||
|
||||
/// this service UUID is publically visible for scanning
|
||||
val BTM_SERVICE_UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd")
|
||||
val BTM_SERVICE_UUID: UUID = UUID.fromString("6ba1b218-15a8-461f-9fa8-5dcae273eafd")
|
||||
|
||||
val BTM_FROMRADIO_CHARACTER =
|
||||
val BTM_FROMRADIO_CHARACTER: UUID =
|
||||
UUID.fromString("8ba2bcc2-ee02-4a55-a531-c525c5e454d5")
|
||||
val BTM_TORADIO_CHARACTER =
|
||||
val BTM_TORADIO_CHARACTER: UUID =
|
||||
UUID.fromString("f75c76d2-129e-4dad-a1dd-7866124401e7")
|
||||
val BTM_FROMNUM_CHARACTER =
|
||||
val BTM_FROMNUM_CHARACTER: UUID =
|
||||
UUID.fromString("ed9da18c-a800-4f66-a670-aa7547e34453")
|
||||
|
||||
/// Get our bluetooth adapter (should always succeed except on emulator
|
||||
|
|
@ -108,10 +110,15 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
|||
}
|
||||
|
||||
/** Return true if this address is still acceptable. For BLE that means, still bonded */
|
||||
@SuppressLint("NewApi", "MissingPermission")
|
||||
override fun addressValid(context: Context, rest: String): Boolean {
|
||||
val allPaired =
|
||||
getBluetoothAdapter(context)?.bondedDevices.orEmpty().map { it.address }.toSet()
|
||||
|
||||
val allPaired = if (hasCompanionDeviceApi(context)) {
|
||||
val deviceManager = context.getSystemService(CompanionDeviceManager::class.java)
|
||||
deviceManager.associations.map { it }.toSet()
|
||||
} else {
|
||||
getBluetoothAdapter(context)?.bondedDevices.orEmpty()
|
||||
.map { it.address }.toSet()
|
||||
}
|
||||
return if (!allPaired.contains(rest)) {
|
||||
warn("Ignoring stale bond to ${rest.anonymize}")
|
||||
false
|
||||
|
|
@ -191,7 +198,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
|||
?: throw RadioNotConnectedException("No GATT")
|
||||
|
||||
/// Our service - note - it is possible to get back a null response for getService if the device services haven't yet been found
|
||||
val bservice
|
||||
private val bservice
|
||||
get(): BluetoothGattService = device.getService(BTM_SERVICE_UUID)
|
||||
?: throw RadioNotConnectedException("BLE service not found")
|
||||
|
||||
|
|
@ -263,7 +270,7 @@ class BluetoothInterface(val service: RadioInterfaceService, val address: String
|
|||
/**
|
||||
* We had some problem, schedule a reconnection attempt (if one isn't already queued)
|
||||
*/
|
||||
fun scheduleReconnect(reason: String) {
|
||||
private fun scheduleReconnect(reason: String) {
|
||||
if (reconnectJob == null) {
|
||||
warn("Scheduling reconnect because $reason")
|
||||
reconnectJob = service.serviceScope.handledLaunch { retryDueToException() }
|
||||
|
|
|
|||
|
|
@ -33,11 +33,7 @@ import com.geeksville.android.isGooglePlayAvailable
|
|||
import com.geeksville.mesh.MainActivity
|
||||
import com.geeksville.mesh.R
|
||||
import com.geeksville.mesh.RadioConfigProtos
|
||||
import com.geeksville.mesh.android.bluetoothManager
|
||||
import com.geeksville.mesh.android.hasScanPermission
|
||||
import com.geeksville.mesh.android.hasLocationPermission
|
||||
import com.geeksville.mesh.android.hasBackgroundPermission
|
||||
import com.geeksville.mesh.android.usbManager
|
||||
import com.geeksville.mesh.android.*
|
||||
import com.geeksville.mesh.databinding.SettingsFragmentBinding
|
||||
import com.geeksville.mesh.model.UIViewModel
|
||||
import com.geeksville.mesh.service.*
|
||||
|
|
@ -68,6 +64,7 @@ fun changeDeviceSelection(context: MainActivity, newAddr: String?) {
|
|||
}
|
||||
|
||||
/// Show the UI asking the user to bond with a device, call changeSelection() if/when bonding completes
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun requestBonding(
|
||||
activity: MainActivity,
|
||||
device: BluetoothDevice,
|
||||
|
|
@ -102,7 +99,11 @@ private fun requestBonding(
|
|||
activity.registerReceiver(bondChangedReceiver, filter)
|
||||
|
||||
// We ignore missing BT adapters, because it lets us run on the emulator
|
||||
device.createBond()
|
||||
try {
|
||||
device.createBond()
|
||||
} catch (ex: Throwable) {
|
||||
SLogging.warn("Failed creating Bluetooth bond: ${ex.message}")
|
||||
}
|
||||
}
|
||||
|
||||
class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
||||
|
|
@ -180,13 +181,14 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
|||
// For each device that appears in our scan, ask for its GATT, when the gatt arrives,
|
||||
// check if it is an eligable device and store it in our list of candidates
|
||||
// if that device later disconnects remove it as a candidate
|
||||
@SuppressLint("MissingPermission")
|
||||
override fun onScanResult(callbackType: Int, result: ScanResult) {
|
||||
|
||||
if ((result.device.name?.startsWith("Mesh") == true)) {
|
||||
val addr = result.device.address
|
||||
val fullAddr = "x$addr" // full address with the bluetooth prefix added
|
||||
// prevent logspam because weill get get lots of redundant scan results
|
||||
val isBonded = result.device.bondState == BluetoothDevice.BOND_BONDED
|
||||
val isBonded = result.device.bondState == BOND_BONDED
|
||||
val oldDevs = devices.value!!
|
||||
val oldEntry = oldDevs[fullAddr]
|
||||
if (oldEntry == null || oldEntry.bonded != isBonded) { // Don't spam the GUI with endless updates for non changing nodes
|
||||
|
|
@ -222,6 +224,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
|||
devices.value = oldDevs // trigger gui updates
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun stopScan() {
|
||||
if (scanner != null) {
|
||||
debug("stopping scan")
|
||||
|
|
@ -296,6 +299,7 @@ class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
fun startScan() {
|
||||
/// The following call might return null if the user doesn't have bluetooth access permissions
|
||||
val bluetoothLeScanner: BluetoothLeScanner? = bluetoothAdapter?.bluetoothLeScanner
|
||||
|
|
@ -615,44 +619,44 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
regionAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
spinner.adapter = regionAdapter
|
||||
|
||||
model.bluetoothEnabled.observe(viewLifecycleOwner, {
|
||||
model.bluetoothEnabled.observe(viewLifecycleOwner) {
|
||||
if (it) binding.changeRadioButton.show()
|
||||
else binding.changeRadioButton.hide()
|
||||
})
|
||||
}
|
||||
|
||||
model.ownerName.observe(viewLifecycleOwner, { name ->
|
||||
model.ownerName.observe(viewLifecycleOwner) { name ->
|
||||
binding.usernameEditText.setText(name)
|
||||
})
|
||||
}
|
||||
|
||||
// Only let user edit their name or set software update while connected to a radio
|
||||
model.isConnected.observe(viewLifecycleOwner, {
|
||||
model.isConnected.observe(viewLifecycleOwner) {
|
||||
updateNodeInfo()
|
||||
updateDevicesButtons(scanModel.devices.value)
|
||||
})
|
||||
}
|
||||
|
||||
model.radioConfig.observe(viewLifecycleOwner, {
|
||||
model.radioConfig.observe(viewLifecycleOwner) {
|
||||
binding.provideLocationCheckbox.isEnabled =
|
||||
isGooglePlayAvailable(requireContext()) && model.locationShare ?: true
|
||||
if (model.locationShare == false) {
|
||||
model.provideLocation.value = false
|
||||
binding.provideLocationCheckbox.isChecked = false
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// Also watch myNodeInfo because it might change later
|
||||
model.myNodeInfo.observe(viewLifecycleOwner, {
|
||||
model.myNodeInfo.observe(viewLifecycleOwner) {
|
||||
updateNodeInfo()
|
||||
})
|
||||
}
|
||||
|
||||
scanModel.errorText.observe(viewLifecycleOwner, { errMsg ->
|
||||
scanModel.errorText.observe(viewLifecycleOwner) { errMsg ->
|
||||
if (errMsg != null) {
|
||||
binding.scanStatusText.text = errMsg
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
scanModel.devices.observe(viewLifecycleOwner, { devices ->
|
||||
scanModel.devices.observe(viewLifecycleOwner) { devices ->
|
||||
updateDevicesButtons(devices)
|
||||
})
|
||||
}
|
||||
|
||||
binding.updateFirmwareButton.setOnClickListener {
|
||||
doFirmwareUpdate()
|
||||
|
|
@ -745,6 +749,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
}
|
||||
}
|
||||
|
||||
@SuppressLint("MissingPermission")
|
||||
private fun updateDevicesButtons(devices: MutableMap<String, BTScanModel.DeviceListEntry>?) {
|
||||
// Remove the old radio buttons and repopulate
|
||||
binding.deviceRadioGroup.removeAllViews()
|
||||
|
|
@ -767,7 +772,7 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
// and before use
|
||||
val bleAddr = scanModel.selectedBluetooth
|
||||
|
||||
if (bleAddr != null && adapter != null) {
|
||||
if (bleAddr != null && adapter != null && myActivity.hasConnectPermission()) {
|
||||
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
|
||||
|
|
@ -956,7 +961,11 @@ class SettingsFragment : ScreenFragment("Settings"), Logging {
|
|||
if (!hasUSB) {
|
||||
// Warn user if BLE is disabled
|
||||
if (scanModel.bluetoothAdapter?.isEnabled != true) {
|
||||
Snackbar.make(binding.changeRadioButton, R.string.error_bluetooth, Snackbar.LENGTH_INDEFINITE)
|
||||
Snackbar.make(
|
||||
binding.changeRadioButton,
|
||||
R.string.error_bluetooth,
|
||||
Snackbar.LENGTH_INDEFINITE
|
||||
)
|
||||
.setAction(R.string.okay) {
|
||||
// dismiss
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ buildscript {
|
|||
mavenCentral()
|
||||
}
|
||||
dependencies {
|
||||
classpath 'com.android.tools.build:gradle:7.0.4'
|
||||
classpath 'com.android.tools.build:gradle:7.1.0'
|
||||
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
|
||||
classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version"
|
||||
|
||||
|
|
|
|||
2
gradle/wrapper/gradle-wrapper.properties
vendored
2
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME
|
|||
distributionPath=wrapper/dists
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.0.2-all.zip
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue