diff --git a/app/build.gradle b/app/build.gradle
index 25dcd9a7..44b5570a 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -9,7 +9,7 @@ android {
buildToolsVersion "29.0.2"
defaultConfig {
applicationId "com.geeksville.meshutil"
- minSdkVersion 18
+ minSdkVersion 21
targetSdkVersion 29
versionCode 1
versionName "1.0"
diff --git a/app/src/main/.gitignore b/app/src/main/.gitignore
new file mode 100644
index 00000000..4dfa5dec
--- /dev/null
+++ b/app/src/main/.gitignore
@@ -0,0 +1 @@
+assets/firmware.bin
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 73d4e9bb..599edd59 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -8,6 +8,7 @@
+
diff --git a/app/src/main/java/com/geeksville/meshutil/MainActivity.kt b/app/src/main/java/com/geeksville/meshutil/MainActivity.kt
index 1db701ae..bcf00a66 100644
--- a/app/src/main/java/com/geeksville/meshutil/MainActivity.kt
+++ b/app/src/main/java/com/geeksville/meshutil/MainActivity.kt
@@ -1,8 +1,10 @@
package com.geeksville.meshutil
+import android.Manifest
import android.bluetooth.*
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageManager
import android.os.Bundle
import android.os.Handler
import android.util.Log
@@ -10,6 +12,8 @@ import com.google.android.material.snackbar.Snackbar
import androidx.appcompat.app.AppCompatActivity
import android.view.Menu
import android.view.MenuItem
+import androidx.core.app.ActivityCompat
+import androidx.core.content.ContextCompat
import kotlinx.android.synthetic.main.activity_main.*
import java.util.*
@@ -19,6 +23,7 @@ class MainActivity : AppCompatActivity() {
companion object {
const val REQUEST_ENABLE_BT = 10
+ const val DID_REQUEST_PERM = 11
}
private val bluetoothAdapter: BluetoothAdapter by lazy(LazyThreadSafetyMode.NONE) {
@@ -26,6 +31,38 @@ class MainActivity : AppCompatActivity() {
bluetoothManager.adapter!!
}
+ fun requestPermission() {
+ val perms = arrayOf(Manifest.permission.ACCESS_FINE_LOCATION,
+ Manifest.permission.ACCESS_BACKGROUND_LOCATION,
+ Manifest.permission.BLUETOOTH,
+ Manifest.permission.BLUETOOTH_ADMIN,
+ Manifest.permission.WAKE_LOCK)
+
+ val missingPerms = perms.filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }
+ if (missingPerms.isNotEmpty()) {
+ missingPerms.forEach {
+ // Permission is not granted
+ // Should we show an explanation?
+ if (ActivityCompat.shouldShowRequestPermissionRationale(this, it)) {
+ // FIXME
+ // Show an explanation to the user *asynchronously* -- don't block
+ // this thread waiting for the user's response! After the user
+ // sees the explanation, try again to request the permission.
+ }
+ }
+
+ // Ask for all the missing perms
+ ActivityCompat.requestPermissions(this, missingPerms.toTypedArray(), DID_REQUEST_PERM)
+
+ // DID_REQUEST_PERM is an
+ // app-defined int constant. The callback method gets the
+ // result of the request.
+ } else {
+ // Permission has already been granted
+ SoftwareUpdateService.enqueueWork(this, SoftwareUpdateService.scanDevicesIntent)
+ }
+ }
+
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
@@ -43,7 +80,7 @@ class MainActivity : AppCompatActivity() {
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
- SoftwareUpdateService.enqueueWork(this, SoftwareUpdateService.scanDevicesIntent)
+ requestPermission()
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
diff --git a/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt b/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt
index d48e602d..d4519f18 100644
--- a/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt
+++ b/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt
@@ -1,9 +1,11 @@
package com.geeksville.meshutil
import android.bluetooth.*
+import android.bluetooth.le.*
import android.content.Context
import android.content.Intent
import android.os.Handler
+import android.os.ParcelUuid
import android.os.SystemClock
import android.util.Log
import android.widget.Toast
@@ -76,90 +78,102 @@ class SoftwareUpdateService : JobIntentService() {
}
}
- // 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
- private val leScanCallback = BluetoothAdapter.LeScanCallback { device, _, _ ->
- lateinit var bluetoothGatt: BluetoothGatt // late init so we can declare our callback and use this there
-
- //var connectionState = STATE_DISCONNECTED
-
- // Various callback methods defined by the BLE API.
- val gattCallback = object : BluetoothGattCallback() {
- override fun onConnectionStateChange(
- gatt: BluetoothGatt,
- status: Int,
- newState: Int
- ) {
- //val intentAction: String
- when (newState) {
- BluetoothProfile.STATE_CONNECTED -> {
- //intentAction = ACTION_GATT_CONNECTED
- //connectionState = STATE_CONNECTED
- // broadcastUpdate(intentAction)
- assert(bluetoothGatt.discoverServices())
- }
- BluetoothProfile.STATE_DISCONNECTED -> {
- //intentAction = ACTION_GATT_DISCONNECTED
- //connectionState = STATE_DISCONNECTED
- // broadcastUpdate(intentAction)
- }
- }
- }
-
- // New services discovered
- override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
- assert(status == BluetoothGatt.GATT_SUCCESS)
-
- // broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED)
-
- val service = gatt.services.find { it.uuid == SW_UPDATE_UUID }
- if (service != null) {
- // FIXME instead of slamming in the target device here, instead make it a param for startUpdate
- updateService = service
- // FIXME instead of keeping the connection open, make start update just reconnect (needed once user can choose devices)
- updateGatt = bluetoothGatt
- enqueueWork(this@SoftwareUpdateService, startUpdateIntent)
- } else {
- // drop our connection - we don't care about this device
- bluetoothGatt.disconnect()
- }
- }
-
- // Result of a characteristic read operation
- override fun onCharacteristicRead(
- gatt: BluetoothGatt,
- characteristic: BluetoothGattCharacteristic,
- status: Int
- ) {
- assert(status == BluetoothGatt.GATT_SUCCESS)
-
- if (characteristic == totalSizeDesc) {
- // Our read of this has completed, either fail or continue updating
- val readvalue =
- characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0)
- assert(readvalue != 0) // FIXME - handle this case
- enqueueWork(this@SoftwareUpdateService, sendNextBlockIntent)
- }
-
- // broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic)
- }
-
- override fun onCharacteristicWrite(
- gatt: BluetoothGatt?,
- characteristic: BluetoothGattCharacteristic?,
- status: Int
- ) {
- assert(status == BluetoothGatt.GATT_SUCCESS)
-
- if (characteristic == dataDesc) {
- enqueueWork(this@SoftwareUpdateService, sendNextBlockIntent)
- }
- }
+ private val scanCallback = object : ScanCallback() {
+ override fun onScanFailed(errorCode: Int) {
+ throw NotImplementedError()
+ }
+
+ // 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
+ override fun onScanResult(callbackType: Int, result: ScanResult) {
+
+ // We don't need any more results now
+ bluetoothAdapter.bluetoothLeScanner.stopScan(this)
+
+ lateinit var bluetoothGatt: BluetoothGatt // late init so we can declare our callback and use this there
+
+ //var connectionState = STATE_DISCONNECTED
+
+ // Various callback methods defined by the BLE API.
+ val gattCallback = object : BluetoothGattCallback() {
+ override fun onConnectionStateChange(
+ gatt: BluetoothGatt,
+ status: Int,
+ newState: Int
+ ) {
+ //val intentAction: String
+ when (newState) {
+ BluetoothProfile.STATE_CONNECTED -> {
+ //intentAction = ACTION_GATT_CONNECTED
+ //connectionState = STATE_CONNECTED
+ // broadcastUpdate(intentAction)
+ assert(bluetoothGatt.discoverServices())
+ }
+ BluetoothProfile.STATE_DISCONNECTED -> {
+ //intentAction = ACTION_GATT_DISCONNECTED
+ //connectionState = STATE_DISCONNECTED
+ // broadcastUpdate(intentAction)
+ }
+ }
+ }
+
+ // New services discovered
+ override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) {
+ assert(status == BluetoothGatt.GATT_SUCCESS)
+
+ // broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED)
+
+ val service = gatt.services.find { it.uuid == SW_UPDATE_UUID }
+ if (service != null) {
+ // FIXME instead of slamming in the target device here, instead make it a param for startUpdate
+ updateService = service
+ // FIXME instead of keeping the connection open, make start update just reconnect (needed once user can choose devices)
+ updateGatt = bluetoothGatt
+ enqueueWork(this@SoftwareUpdateService, startUpdateIntent)
+ } else {
+ // drop our connection - we don't care about this device
+ bluetoothGatt.disconnect()
+ }
+ }
+
+ // Result of a characteristic read operation
+ override fun onCharacteristicRead(
+ gatt: BluetoothGatt,
+ characteristic: BluetoothGattCharacteristic,
+ status: Int
+ ) {
+ assert(status == BluetoothGatt.GATT_SUCCESS)
+
+ if (characteristic == totalSizeDesc) {
+ // Our read of this has completed, either fail or continue updating
+ val readvalue =
+ characteristic.getIntValue(BluetoothGattCharacteristic.FORMAT_UINT32, 0)
+ assert(readvalue != 0) // FIXME - handle this case
+ enqueueWork(this@SoftwareUpdateService, sendNextBlockIntent)
+ }
+
+ // broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic)
+ }
+
+ override fun onCharacteristicWrite(
+ gatt: BluetoothGatt?,
+ characteristic: BluetoothGattCharacteristic?,
+ status: Int
+ ) {
+ assert(status == BluetoothGatt.GATT_SUCCESS)
+
+ if (characteristic == dataDesc) {
+ enqueueWork(this@SoftwareUpdateService, sendNextBlockIntent)
+ }
+ }
+ }
+ bluetoothGatt = result.device.connectGatt(this@SoftwareUpdateService, false, gattCallback)!!
}
- bluetoothGatt = device.connectGatt(this, false, gattCallback)!!
}
+
+
private fun scanLeDevice(enable: Boolean) {
when (enable) {
true -> {
@@ -169,11 +183,21 @@ class SoftwareUpdateService : JobIntentService() {
bluetoothAdapter.stopLeScan(leScanCallback)
}, SCAN_PERIOD)
mScanning = true */
- assert(bluetoothAdapter.startLeScan(leScanCallback))
+
+ val scanner = bluetoothAdapter.bluetoothLeScanner
+
+ // filter and only accept devices that have a sw update service
+ val filter = ScanFilter.Builder().setServiceUuid(ParcelUuid(SW_UPDATE_UUID)).build()
+ val settings = ScanSettings.Builder().
+ setScanMode(ScanSettings.SCAN_MODE_BALANCED).
+ setMatchMode(ScanSettings.MATCH_NUM_ONE_ADVERTISEMENT).
+ setCallbackType(ScanSettings.CALLBACK_TYPE_FIRST_MATCH).
+ build()
+ scanner.startScan(listOf(filter), settings, scanCallback)
}
else -> {
// mScanning = false
- bluetoothAdapter.stopLeScan(leScanCallback)
+ // bluetoothAdapter.stopLeScan(leScanCallback)
}
}
}