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) } } }