diff --git a/TODO.md b/TODO.md new file mode 100644 index 000000000..6dfda74fc --- /dev/null +++ b/TODO.md @@ -0,0 +1,10 @@ + + +# Medium priority + +* remove example code boilerplate from the service +* add analytics (make them optional) + +# Low priority + + * also add a receiver that fires after a new update was installed from the play stoe \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bf5b939ac..73d4e9bb0 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,19 @@ + package="com.geeksville.meshutil" + xmlns:tools="http://schemas.android.com/tools" + tools:ignore="GoogleAppIndexingWarning"> + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/meshutil/BootCompleteReceiver.kt b/app/src/main/java/com/geeksville/meshutil/BootCompleteReceiver.kt new file mode 100644 index 000000000..f5cfbb442 --- /dev/null +++ b/app/src/main/java/com/geeksville/meshutil/BootCompleteReceiver.kt @@ -0,0 +1,14 @@ +package com.geeksville.meshutil + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent + + +class BootCompleteReceiver : BroadcastReceiver() { + override fun onReceive(mContext: Context, intent: Intent) { + if (intent.action == Intent.ACTION_BOOT_COMPLETED) { + // FIXME - start listening for bluetooth messages from our device + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/geeksville/meshutil/MainActivity.kt b/app/src/main/java/com/geeksville/meshutil/MainActivity.kt index df4f957ad..af6f6203c 100644 --- a/app/src/main/java/com/geeksville/meshutil/MainActivity.kt +++ b/app/src/main/java/com/geeksville/meshutil/MainActivity.kt @@ -1,15 +1,160 @@ package com.geeksville.meshutil +import android.bluetooth.* +import android.content.Context +import android.content.Intent import android.os.Bundle +import android.os.Handler +import android.util.Log import com.google.android.material.snackbar.Snackbar import androidx.appcompat.app.AppCompatActivity import android.view.Menu import android.view.MenuItem import kotlinx.android.synthetic.main.activity_main.* +import java.util.* + class MainActivity : AppCompatActivity() { + companion object { + const val REQUEST_ENABLE_BT = 10 + + private const val SCAN_PERIOD: Long = 10000 + + const val ACTION_GATT_CONNECTED = "com.example.bluetooth.le.ACTION_GATT_CONNECTED" + const val ACTION_GATT_DISCONNECTED = "com.example.bluetooth.le.ACTION_GATT_DISCONNECTED" + + private const val STATE_DISCONNECTED = 0 + private const val STATE_CONNECTING = 1 + private const val STATE_CONNECTED = 2 + + private val TAG = MainActivity::class.java.simpleName // FIXME - use my logging class instead + + private val SW_UPDATE_UUID = UUID.fromString("cb0b9a0b-a84c-4c0d-bdbb-442e3144ee30") + + private val SW_UPDATE_TOTALSIZE_CHARACTER = UUID.fromString("e74dd9c0-a301-4a6f-95a1-f0e1dbea8e1e") // write|read total image size, 32 bit, write this first, then read read back to see if it was acceptable (0 mean not accepted) + private val SW_UPDATE_DATA_CHARACTER = UUID.fromString("e272ebac-d463-4b98-bc84-5cc1a39ee517") // write data, variable sized, recommended 512 bytes, write one for each block of file + private val SW_UPDATE_CRC32_CHARACTER = UUID.fromString("4826129c-c22a-43a3-b066-ce8f0d5bacc6") // write crc32, write last - writing this will complete the OTA operation, now you can read result + private val SW_UPDATE_RESULT_CHARACTER = UUID.fromString("5e134862-7411-4424-ac4a-210937432c77") // read|notify result code, readable but will notify when the OTA operation completes + } + + private val BluetoothAdapter.isDisabled: Boolean + get() = !isEnabled + + private val bluetoothAdapter: BluetoothAdapter by lazy(LazyThreadSafetyMode.NONE) { + val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager + bluetoothManager.adapter!! + } + + private var mScanning: Boolean = false + private val handler = Handler() + + private val leScanCallback = BluetoothAdapter.LeScanCallback { device, _, _ -> + runOnUiThread { + /* + leDeviceListAdapter.addDevice(device) + leDeviceListAdapter.notifyDataSetChanged() + */ + + lateinit var bluetoothGatt: BluetoothGatt + + //var connectionState = STATE_DISCONNECTED + + lateinit var totalSizeDesc: BluetoothGattCharacteristic + + // Send the next block of our file to the device + fun sendNextBlock() { + + } + + // 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) + Log.i(TAG, "Connected to GATT server.") + Log.i(TAG, "Attempting to start service discovery: " + + bluetoothGatt.discoverServices()) + } + BluetoothProfile.STATE_DISCONNECTED -> { + //intentAction = ACTION_GATT_DISCONNECTED + //connectionState = STATE_DISCONNECTED + Log.i(TAG, "Disconnected from GATT server.") + // broadcastUpdate(intentAction) + } + } + } + + // New services discovered + override fun onServicesDiscovered(gatt: BluetoothGatt, status: Int) { + when (status) { + BluetoothGatt.GATT_SUCCESS -> { + // broadcastUpdate(ACTION_GATT_SERVICES_DISCOVERED) + + val updateService = gatt.services.find { it.uuid == SW_UPDATE_UUID } + if(updateService != null) { + + // Start the update by writing the # of bytes in the image + val numBytes = 45 + totalSizeDesc = updateService.getCharacteristic(SW_UPDATE_TOTALSIZE_CHARACTER)!! + assert(totalSizeDesc.setValue(numBytes, BluetoothGattCharacteristic.FORMAT_UINT32, 0)) + assert(bluetoothGatt.writeCharacteristic(totalSizeDesc)) + assert(bluetoothGatt.readCharacteristic(totalSizeDesc)) + } + } + else -> Log.w(TAG, "onServicesDiscovered received: $status") + } + } + + // 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 + sendNextBlock() // FIXME, call this in a job queue of the service + } + + // broadcastUpdate(ACTION_DATA_AVAILABLE, characteristic) + } + } + bluetoothGatt = device.connectGatt(this, false, gattCallback)!! + } + } + + private fun scanLeDevice(enable: Boolean) { + when (enable) { + true -> { + // Stops scanning after a pre-defined scan period. + handler.postDelayed({ + mScanning = false + bluetoothAdapter.stopLeScan(leScanCallback) + }, SCAN_PERIOD) + mScanning = true + bluetoothAdapter.startLeScan(leScanCallback) + } + else -> { + mScanning = false + bluetoothAdapter.stopLeScan(leScanCallback) + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) @@ -17,7 +162,14 @@ class MainActivity : AppCompatActivity() { fab.setOnClickListener { view -> Snackbar.make(view, "Replace with your own action", Snackbar.LENGTH_LONG) - .setAction("Action", null).show() + .setAction("Action", null).show() + } + + // Ensures Bluetooth is available on the device and it is enabled. If not, + // displays a dialog requesting user permission to enable Bluetooth. + bluetoothAdapter.takeIf { it.isDisabled }?.apply { + val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE) + startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT) } } @@ -37,3 +189,4 @@ class MainActivity : AppCompatActivity() { } } } + diff --git a/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt b/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt new file mode 100644 index 000000000..63661aa1a --- /dev/null +++ b/app/src/main/java/com/geeksville/meshutil/SoftwareUpdateService.kt @@ -0,0 +1,69 @@ +package com.geeksville.meshutil + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.os.SystemClock +import android.util.Log +import android.widget.Toast +import androidx.core.app.JobIntentService + + +/** + * Example implementation of a JobIntentService. + */ +class SimpleJobIntentService : JobIntentService() { + override fun onHandleWork(intent: Intent) { // We have received work to do. The system or framework is already +// holding a wake lock for us at this point, so we can just go. + Log.i("SimpleJobIntentService", "Executing work: $intent") + var label = intent.getStringExtra("label") + if (label == null) { + label = intent.toString() + } + toast("Executing: $label") + for (i in 0..4) { + Log.i( + "SimpleJobIntentService", "Running service " + (i + 1) + + "/5 @ " + SystemClock.elapsedRealtime() + ) + try { + Thread.sleep(1000) + } catch (e: InterruptedException) { + } + } + Log.i( + "SimpleJobIntentService", + "Completed service @ " + SystemClock.elapsedRealtime() + ) + } + + override fun onDestroy() { + super.onDestroy() + toast("All work complete") + } + + val mHandler = Handler() + // Helper for showing tests + fun toast(text: CharSequence?) { + mHandler.post { + Toast.makeText(this@SimpleJobIntentService, text, Toast.LENGTH_SHORT).show() + } + } + + companion object { + /** + * Unique job ID for this service. Must be the same for all work. + */ + const val JOB_ID = 1000 + + /** + * Convenience method for enqueuing work in to this service. + */ + fun enqueueWork(context: Context, work: Intent) { + enqueueWork( + context, + SimpleJobIntentService::class.java, JOB_ID, work + ) + } + } +} \ No newline at end of file