2020-01-23 05:46:41 +00:00
|
|
|
package com.geeksville.mesh
|
2020-01-20 23:53:22 +00:00
|
|
|
|
2020-01-21 21:12:01 +00:00
|
|
|
import android.Manifest
|
2020-01-24 20:49:27 +00:00
|
|
|
import android.bluetooth.BluetoothAdapter
|
|
|
|
import android.bluetooth.BluetoothManager
|
2020-01-23 16:09:50 +00:00
|
|
|
import android.content.ComponentName
|
2020-01-21 17:37:39 +00:00
|
|
|
import android.content.Context
|
|
|
|
import android.content.Intent
|
2020-01-23 16:09:50 +00:00
|
|
|
import android.content.ServiceConnection
|
2020-01-21 21:12:01 +00:00
|
|
|
import android.content.pm.PackageManager
|
2020-02-04 21:24:04 +00:00
|
|
|
import android.os.Build
|
2020-01-20 23:53:22 +00:00
|
|
|
import android.os.Bundle
|
2020-01-25 18:00:57 +00:00
|
|
|
import android.os.Debug
|
2020-01-23 16:09:50 +00:00
|
|
|
import android.os.IBinder
|
2020-01-20 23:53:22 +00:00
|
|
|
import android.view.Menu
|
|
|
|
import android.view.MenuItem
|
2020-01-22 21:02:24 +00:00
|
|
|
import android.widget.Toast
|
2020-01-24 20:49:27 +00:00
|
|
|
import androidx.appcompat.app.AppCompatActivity
|
2020-01-22 22:48:06 +00:00
|
|
|
import androidx.compose.Composable
|
2020-01-23 16:09:50 +00:00
|
|
|
import androidx.compose.Model
|
2020-01-21 21:12:01 +00:00
|
|
|
import androidx.core.app.ActivityCompat
|
|
|
|
import androidx.core.content.ContextCompat
|
2020-01-22 22:48:06 +00:00
|
|
|
import androidx.ui.core.Text
|
|
|
|
import androidx.ui.core.dp
|
|
|
|
import androidx.ui.core.setContent
|
|
|
|
import androidx.ui.layout.Column
|
|
|
|
import androidx.ui.layout.Spacing
|
|
|
|
import androidx.ui.material.Button
|
|
|
|
import androidx.ui.material.MaterialTheme
|
|
|
|
import androidx.ui.tooling.preview.Preview
|
2020-01-22 22:27:22 +00:00
|
|
|
import com.geeksville.android.Logging
|
2020-02-05 05:23:52 +00:00
|
|
|
import com.geeksville.util.exceptionReporter
|
2020-01-25 18:00:57 +00:00
|
|
|
import com.google.firebase.crashlytics.FirebaseCrashlytics
|
2020-01-20 23:53:22 +00:00
|
|
|
|
2020-01-21 17:37:39 +00:00
|
|
|
|
2020-01-22 22:27:22 +00:00
|
|
|
class MainActivity : AppCompatActivity(), Logging {
|
2020-01-20 23:53:22 +00:00
|
|
|
|
2020-01-21 17:37:39 +00:00
|
|
|
companion object {
|
|
|
|
const val REQUEST_ENABLE_BT = 10
|
2020-01-21 21:12:01 +00:00
|
|
|
const val DID_REQUEST_PERM = 11
|
2020-01-21 17:37:39 +00:00
|
|
|
}
|
|
|
|
|
2020-01-23 16:09:50 +00:00
|
|
|
@Model
|
2020-01-24 20:49:27 +00:00
|
|
|
class MeshServiceState(
|
|
|
|
var connected: Boolean = false,
|
|
|
|
var onlineIds: Array<String> = arrayOf()
|
|
|
|
)
|
|
|
|
|
2020-01-23 16:09:50 +00:00
|
|
|
val meshServiceState = MeshServiceState()
|
|
|
|
|
2020-01-22 21:02:24 +00:00
|
|
|
private val bluetoothAdapter: BluetoothAdapter? by lazy(LazyThreadSafetyMode.NONE) {
|
2020-01-21 17:37:39 +00:00
|
|
|
val bluetoothManager = getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
|
2020-01-22 21:02:24 +00:00
|
|
|
bluetoothManager.adapter
|
2020-01-21 17:37:39 +00:00
|
|
|
}
|
|
|
|
|
2020-01-21 21:12:01 +00:00
|
|
|
fun requestPermission() {
|
2020-01-22 22:27:22 +00:00
|
|
|
debug("Checking permissions")
|
|
|
|
|
2020-01-23 00:45:27 +00:00
|
|
|
val perms = arrayOf(
|
|
|
|
Manifest.permission.ACCESS_FINE_LOCATION,
|
2020-01-21 21:12:01 +00:00
|
|
|
Manifest.permission.ACCESS_BACKGROUND_LOCATION,
|
|
|
|
Manifest.permission.BLUETOOTH,
|
|
|
|
Manifest.permission.BLUETOOTH_ADMIN,
|
2020-01-25 01:47:32 +00:00
|
|
|
Manifest.permission.WAKE_LOCK,
|
|
|
|
Manifest.permission.WRITE_EXTERNAL_STORAGE
|
2020-01-23 00:45:27 +00:00
|
|
|
)
|
|
|
|
|
|
|
|
val missingPerms = perms.filter {
|
|
|
|
ContextCompat.checkSelfPermission(
|
|
|
|
this,
|
|
|
|
it
|
|
|
|
) != PackageManager.PERMISSION_GRANTED
|
|
|
|
}
|
2020-01-21 21:12:01 +00:00
|
|
|
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
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-01-23 00:45:27 +00:00
|
|
|
@Preview
|
2020-01-22 22:48:06 +00:00
|
|
|
@Composable
|
2020-01-23 16:09:50 +00:00
|
|
|
fun previewView() {
|
|
|
|
composeView(meshServiceState)
|
|
|
|
}
|
|
|
|
|
2020-01-28 03:23:34 +00:00
|
|
|
private fun sendTestPackets() {
|
2020-02-05 05:23:52 +00:00
|
|
|
exceptionReporter {
|
|
|
|
val m = meshService!!
|
|
|
|
|
|
|
|
// Do some test operations
|
|
|
|
m.setOwner("+16508675309", "Kevin Xter", "kx")
|
|
|
|
val testPayload = "hello world".toByteArray()
|
|
|
|
m.sendData(
|
|
|
|
"+16508675310",
|
|
|
|
testPayload,
|
|
|
|
MeshProtos.Data.Type.SIGNAL_OPAQUE_VALUE
|
|
|
|
)
|
|
|
|
m.sendData(
|
|
|
|
"+16508675310",
|
|
|
|
testPayload,
|
|
|
|
MeshProtos.Data.Type.CLEAR_TEXT_VALUE
|
|
|
|
)
|
|
|
|
}
|
2020-01-28 03:23:34 +00:00
|
|
|
}
|
|
|
|
|
2020-01-23 16:09:50 +00:00
|
|
|
@Composable
|
|
|
|
fun composeView(meshServiceState: MeshServiceState) {
|
2020-01-22 22:48:06 +00:00
|
|
|
MaterialTheme {
|
2020-01-23 14:46:23 +00:00
|
|
|
Column(modifier = Spacing(8.dp)) {
|
|
|
|
Text(text = "Meshtastic", modifier = Spacing(8.dp))
|
2020-01-22 22:48:06 +00:00
|
|
|
|
2020-01-23 16:09:50 +00:00
|
|
|
Text("Radio connected: ${meshServiceState.connected}")
|
|
|
|
meshServiceState.onlineIds.forEach {
|
|
|
|
Text("User: $it")
|
|
|
|
}
|
|
|
|
|
2020-01-22 22:48:06 +00:00
|
|
|
Button(text = "Start scan",
|
|
|
|
onClick = {
|
|
|
|
if (bluetoothAdapter != null) {
|
2020-01-23 18:39:54 +00:00
|
|
|
// Note: We don't want this service to die just because our activity goes away (because it is doing a software update)
|
|
|
|
// So we use the application context instead of the activity
|
2020-01-24 20:49:27 +00:00
|
|
|
SoftwareUpdateService.enqueueWork(
|
|
|
|
applicationContext,
|
2020-01-24 05:58:23 +00:00
|
|
|
SoftwareUpdateService.startUpdateIntent
|
2020-01-23 00:45:27 +00:00
|
|
|
)
|
2020-01-22 22:48:06 +00:00
|
|
|
}
|
|
|
|
})
|
2020-01-28 00:58:47 +00:00
|
|
|
|
|
|
|
Button(text = "send packets",
|
2020-01-28 03:23:34 +00:00
|
|
|
onClick = { sendTestPackets() })
|
2020-01-22 22:48:06 +00:00
|
|
|
}
|
2020-01-23 00:45:27 +00:00
|
|
|
}
|
2020-01-22 22:48:06 +00:00
|
|
|
}
|
2020-01-22 22:27:22 +00:00
|
|
|
|
2020-01-22 22:48:06 +00:00
|
|
|
override fun onCreate(savedInstanceState: Bundle?) {
|
|
|
|
super.onCreate(savedInstanceState)
|
2020-01-25 18:00:57 +00:00
|
|
|
|
|
|
|
// We default to off in the manifest, FIXME turn on only if user approves
|
|
|
|
// leave off when running in the debugger
|
|
|
|
if (false && !Debug.isDebuggerConnected())
|
|
|
|
FirebaseCrashlytics.getInstance().setCrashlyticsCollectionEnabled(true)
|
|
|
|
|
2020-01-22 22:48:06 +00:00
|
|
|
setContent {
|
2020-01-23 16:09:50 +00:00
|
|
|
composeView(meshServiceState)
|
2020-01-21 17:37:39 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// Ensures Bluetooth is available on the device and it is enabled. If not,
|
|
|
|
// displays a dialog requesting user permission to enable Bluetooth.
|
2020-01-23 00:45:27 +00:00
|
|
|
if (bluetoothAdapter != null) {
|
2020-01-22 21:02:24 +00:00
|
|
|
bluetoothAdapter!!.takeIf { !it.isEnabled }?.apply {
|
|
|
|
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
|
|
|
|
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
|
|
|
|
}
|
2020-01-23 00:45:27 +00:00
|
|
|
} else {
|
2020-01-22 21:02:24 +00:00
|
|
|
Toast.makeText(this, "Error - this app requires bluetooth", Toast.LENGTH_LONG).show()
|
2020-01-20 23:53:22 +00:00
|
|
|
}
|
2020-01-21 20:07:03 +00:00
|
|
|
|
2020-01-21 21:12:01 +00:00
|
|
|
requestPermission()
|
2020-01-20 23:53:22 +00:00
|
|
|
}
|
|
|
|
|
2020-02-04 21:24:04 +00:00
|
|
|
private var meshService: IMeshService? = null
|
|
|
|
private var isBound = false
|
2020-01-23 16:09:50 +00:00
|
|
|
|
2020-02-04 21:24:04 +00:00
|
|
|
private var serviceConnection = object : ServiceConnection {
|
2020-01-23 16:09:50 +00:00
|
|
|
override fun onServiceConnected(name: ComponentName, service: IBinder) {
|
2020-01-25 18:00:57 +00:00
|
|
|
val m = IMeshService.Stub.asInterface(service)
|
|
|
|
meshService = m
|
2020-01-28 00:00:00 +00:00
|
|
|
|
2020-01-28 03:23:34 +00:00
|
|
|
// FIXME: this still can't work this early because the send to +6508675310
|
|
|
|
// requires a DB lookup which isn't yet populated (until the sim test packets
|
|
|
|
// from the radio arrive)
|
|
|
|
// sendTestPackets() // send some traffic ASAP
|
|
|
|
|
2020-01-23 16:09:50 +00:00
|
|
|
// FIXME this doesn't work because the model has already been copied into compose land?
|
2020-01-25 18:00:57 +00:00
|
|
|
// runOnUiThread { // FIXME - this can be removed?
|
|
|
|
meshServiceState.connected = m.isConnected
|
|
|
|
meshServiceState.onlineIds = m.online
|
|
|
|
// }
|
2020-01-23 16:09:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onServiceDisconnected(name: ComponentName) {
|
2020-01-24 20:49:27 +00:00
|
|
|
meshService = null
|
2020-01-23 16:09:50 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private fun bindMeshService() {
|
2020-01-23 17:04:06 +00:00
|
|
|
debug("Binding to mesh service!")
|
2020-01-23 16:09:50 +00:00
|
|
|
// we bind using the well known name, to make sure 3rd party apps could also
|
|
|
|
logAssert(meshService == null)
|
2020-01-23 17:04:06 +00:00
|
|
|
|
2020-01-26 18:29:55 +00:00
|
|
|
// bind to our service using the same mechanism an external client would use (for testing coverage)
|
|
|
|
// The following would work for us, but not external users
|
|
|
|
//val intent = Intent(this, MeshService::class.java)
|
|
|
|
//intent.action = IMeshService::class.java.name
|
2020-02-04 21:24:04 +00:00
|
|
|
val intent = Intent()
|
|
|
|
intent.setClassName("com.geeksville.mesh", "com.geeksville.mesh.MeshService")
|
|
|
|
|
|
|
|
// Before binding we want to explicitly create - so the service stays alive forever (so it can keep
|
|
|
|
// listening for the bluetooth packets arriving from the radio. And when they arrive forward them
|
|
|
|
// to Signal or whatever.
|
|
|
|
|
|
|
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
|
|
startForegroundService(intent)
|
|
|
|
} else {
|
|
|
|
startService(intent)
|
|
|
|
}
|
2020-01-26 18:29:55 +00:00
|
|
|
|
2020-02-04 21:24:04 +00:00
|
|
|
// ALSO bind so we can use the api
|
2020-01-28 00:00:00 +00:00
|
|
|
logAssert(bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE))
|
2020-02-04 21:24:04 +00:00
|
|
|
isBound = true;
|
2020-01-23 16:09:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
private fun unbindMeshService() {
|
|
|
|
// If we have received the service, and hence registered with
|
|
|
|
// it, then now is the time to unregister.
|
|
|
|
// if we never connected, do nothing
|
2020-01-28 00:00:00 +00:00
|
|
|
debug("Unbinding from mesh service!")
|
2020-02-04 21:24:04 +00:00
|
|
|
if (isBound)
|
|
|
|
unbindService(serviceConnection)
|
2020-01-28 00:00:00 +00:00
|
|
|
meshService = null
|
2020-01-23 16:09:50 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
override fun onPause() {
|
|
|
|
unbindMeshService()
|
|
|
|
|
|
|
|
super.onPause()
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onResume() {
|
|
|
|
super.onResume()
|
|
|
|
|
|
|
|
bindMeshService()
|
|
|
|
}
|
|
|
|
|
2020-01-20 23:53:22 +00:00
|
|
|
override fun onCreateOptionsMenu(menu: Menu): Boolean {
|
|
|
|
// Inflate the menu; this adds items to the action bar if it is present.
|
|
|
|
menuInflater.inflate(R.menu.menu_main, menu)
|
|
|
|
return true
|
|
|
|
}
|
|
|
|
|
|
|
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
|
|
|
// Handle action bar item clicks here. The action bar will
|
|
|
|
// automatically handle clicks on the Home/Up button, so long
|
|
|
|
// as you specify a parent activity in AndroidManifest.xml.
|
2020-01-21 00:13:40 +00:00
|
|
|
return when (item.itemId) {
|
2020-01-20 23:53:22 +00:00
|
|
|
R.id.action_settings -> true
|
|
|
|
else -> super.onOptionsItemSelected(item)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2020-01-21 17:37:39 +00:00
|
|
|
|