meshtastic-android/app/src/main/java/com/geeksville/mesh/MainActivity.kt

475 wiersze
16 KiB
Kotlin
Czysty Zwykły widok Historia

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-02-25 16:10:23 +00:00
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
2020-01-21 21:12:01 +00:00
import android.content.pm.PackageManager
import android.net.Uri
2020-02-14 17:09:40 +00:00
import android.os.Build
2020-01-20 23:53:22 +00:00
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
2020-03-05 17:50:33 +00:00
import android.view.MotionEvent
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-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.setContent
2020-01-22 22:27:22 +00:00
import com.geeksville.android.Logging
2020-02-25 16:10:23 +00:00
import com.geeksville.android.ServiceClient
2020-02-17 21:34:52 +00:00
import com.geeksville.mesh.model.MessagesState
import com.geeksville.mesh.model.NodeDB
import com.geeksville.mesh.model.TextMessage
import com.geeksville.mesh.model.UIState
import com.geeksville.mesh.service.*
2020-02-25 23:07:09 +00:00
import com.geeksville.mesh.ui.AppStatus
2020-02-17 21:34:52 +00:00
import com.geeksville.mesh.ui.MeshApp
2020-02-18 17:09:49 +00:00
import com.geeksville.mesh.ui.ScanState
2020-02-25 23:07:09 +00:00
import com.geeksville.mesh.ui.Screen
2020-03-05 17:50:33 +00:00
import com.geeksville.util.Exceptions
2020-02-05 05:23:52 +00:00
import com.geeksville.util.exceptionReporter
2020-02-14 15:47:20 +00:00
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
import com.google.android.gms.tasks.Task
2020-02-09 13:52:17 +00:00
import java.nio.charset.Charset
2020-01-20 23:53:22 +00:00
2020-01-21 17:37:39 +00:00
/*
UI design
material setup instructions: https://material.io/develop/android/docs/getting-started/
dark theme (or use system eventually) https://material.io/develop/android/theming/dark/
NavDrawer is a standard draw which can be dragged in from the left or the menu icon inside the app
title.
Fragments:
2020-02-17 02:54:29 +00:00
SettingsFragment shows "Settings"
username
shortname
bluetooth pairing list
(eventually misc device settings that are not channel related)
2020-02-17 02:54:29 +00:00
Channel fragment "Channel"
qr code, copy link button
ch number
misc other settings
(eventually a way of choosing between past channels)
2020-02-17 02:54:29 +00:00
ChatFragment "Messages"
a text box to enter new texts
a scrolling list of rows. each row is a text and a sender info layout
2020-02-17 02:54:29 +00:00
NodeListFragment "Users"
a node info row for every node
ViewModels:
BTScanModel starts/stops bt scan and provides list of devices (manages entire scan lifecycle)
MeshModel contains: (manages entire service relationship)
current received texts
current radio macaddr
current node infos (updated dynamically)
eventually use bottom navigation bar to switch between, Members, Chat, Channel, Settings. https://material.io/develop/android/components/bottom-navigation-view/
use numbers of # chat messages and # of members in the badges.
(per this recommendation to not use top tabs: https://ux.stackexchange.com/questions/102439/android-ux-when-to-use-bottom-navigation-and-when-to-use-tabs )
eventually:
make a custom theme: https://github.com/material-components/material-components-android/tree/master/material-theme-builder
*/
val utf8 = Charset.forName("UTF-8")
class MainActivity : AppCompatActivity(), Logging,
ActivityCompat.OnRequestPermissionsResultCallback {
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-02-14 15:47:20 +00:00
const val RC_SIGN_IN = 12 // google signin completed
2020-01-21 17:37:39 +00:00
}
2020-02-09 15:28:24 +00:00
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
}
private fun requestPermission() {
2020-01-22 22:27:22 +00:00
debug("Checking permissions")
2020-02-14 17:09:40 +00:00
val perms = mutableListOf(
Manifest.permission.ACCESS_COARSE_LOCATION,
2020-01-23 00:45:27 +00:00
Manifest.permission.ACCESS_FINE_LOCATION,
2020-01-21 21:12:01 +00:00
Manifest.permission.BLUETOOTH,
Manifest.permission.BLUETOOTH_ADMIN,
2020-01-25 01:47:32 +00:00
Manifest.permission.WAKE_LOCK,
2020-02-14 12:41:20 +00:00
Manifest.permission.WRITE_EXTERNAL_STORAGE
2020-01-23 00:45:27 +00:00
)
2020-02-14 17:09:40 +00:00
if (Build.VERSION.SDK_INT >= 29) // only added later
perms.add(Manifest.permission.ACCESS_BACKGROUND_LOCATION)
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 16:09:50 +00:00
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
}
2020-02-14 15:47:20 +00:00
private fun sendTestPackets() {
exceptionReporter {
val m = UIState.meshService!!
2020-02-14 15:47:20 +00:00
// Do some test operations
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-02-14 15:47:20 +00:00
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
2020-03-02 16:41:16 +00:00
val prefs = UIState.getPreferences(this)
2020-02-18 20:22:45 +00:00
UIState.ownerName = prefs.getString("owner", "")!!
UIState.meshService = null
2020-03-30 17:26:16 +00:00
UIState.savedInstanceState = savedInstanceState
2020-02-14 15:47:20 +00:00
// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
if (bluetoothAdapter != null) {
bluetoothAdapter!!.takeIf { !it.isEnabled }?.apply {
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
2020-02-14 12:41:20 +00:00
}
2020-02-14 15:47:20 +00:00
} else {
Toast.makeText(this, "Error - this app requires bluetooth", Toast.LENGTH_LONG)
.show()
2020-02-05 05:23:52 +00:00
}
2020-02-14 15:47:20 +00:00
requestPermission()
2020-02-10 15:40:45 +00:00
2020-02-18 18:40:02 +00:00
2020-02-17 19:22:47 +00:00
/* not yet working
2020-02-14 15:47:20 +00:00
// Configure sign-in to request the user's ID, email address, and basic
// profile. ID and basic profile are included in DEFAULT_SIGN_IN.
// Configure sign-in to request the user's ID, email address, and basic
// profile. ID and basic profile are included in DEFAULT_SIGN_IN.
val gso =
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.build()
2020-01-25 18:00:57 +00:00
2020-02-14 15:47:20 +00:00
// Build a GoogleSignInClient with the options specified by gso.
UIState.googleSignInClient = GoogleSignIn.getClient(this, gso);
2020-02-17 19:22:47 +00:00
*/
// Handle any intent
handleIntent(intent)
2020-03-17 18:35:19 +00:00
setContent {
MeshApp()
}
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
/// Handle any itents that were passed into us
private fun handleIntent(intent: Intent) {
val appLinkAction = intent.action
val appLinkData: Uri? = intent.data
2020-03-17 18:35:19 +00:00
UIState.requestedChannelUrl = null // assume none
// Were we asked to open one our channel URLs?
2020-03-17 18:35:19 +00:00
if (Intent.ACTION_VIEW == appLinkAction) {
debug("Asked to open a channel URL - FIXME, ask user if they want to switch to that channel. If so send the config to the radio")
2020-03-17 18:35:19 +00:00
UIState.requestedChannelUrl = appLinkData
}
2020-02-14 15:47:20 +00:00
}
2020-01-21 17:37:39 +00:00
2020-02-14 15:47:20 +00:00
override fun onDestroy() {
unregisterMeshReceiver()
2020-03-07 04:47:45 +00:00
UIState.meshService =
null // When our activity goes away make sure we don't keep a ptr around to the service
2020-02-14 15:47:20 +00:00
super.onDestroy()
}
2020-02-14 12:41:20 +00:00
2020-02-14 15:47:20 +00:00
/**
* Dispatch incoming result to the correct fragment.
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
// Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...);
// Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...);
2020-02-18 18:40:02 +00:00
if (requestCode == RC_SIGN_IN) {
2020-02-14 15:47:20 +00:00
// The Task returned from this call is always completed, no need to attach
// a listener.
val task: Task<GoogleSignInAccount> =
GoogleSignIn.getSignedInAccountFromIntent(data)
handleSignInResult(task)
2020-01-20 23:53:22 +00:00
}
2020-02-14 15:47:20 +00:00
}
2020-02-09 13:52:17 +00:00
2020-02-14 15:47:20 +00:00
private fun handleSignInResult(completedTask: Task<GoogleSignInAccount>) {
2020-02-18 18:40:02 +00:00
/*
2020-02-14 15:47:20 +00:00
try {
2020-02-18 18:40:02 +00:00
val account = completedTask.getResult(ApiException::class.java)
2020-02-14 15:47:20 +00:00
// Signed in successfully, show authenticated UI.
//updateUI(account)
} catch (e: ApiException) { // The ApiException status code indicates the detailed failure reason.
// Please refer to the GoogleSignInStatusCodes class reference for more information.
warn("signInResult:failed code=" + e.statusCode)
//updateUI(null)
2020-02-18 18:40:02 +00:00
} */
2020-02-14 15:47:20 +00:00
}
2020-02-14 15:47:20 +00:00
private var receiverRegistered = false
2020-02-14 15:47:20 +00:00
private fun registerMeshReceiver() {
logAssert(!receiverRegistered)
val filter = IntentFilter()
filter.addAction(MeshService.ACTION_MESH_CONNECTED)
filter.addAction(MeshService.ACTION_NODE_CHANGE)
filter.addAction(MeshService.ACTION_RECEIVED_DATA)
registerReceiver(meshServiceReceiver, filter)
receiverRegistered = true;
2020-02-14 15:47:20 +00:00
}
2020-02-09 13:52:17 +00:00
2020-02-14 15:47:20 +00:00
private fun unregisterMeshReceiver() {
if (receiverRegistered) {
receiverRegistered = false
unregisterReceiver(meshServiceReceiver)
}
2020-02-14 15:47:20 +00:00
}
2020-02-09 13:52:17 +00:00
2020-02-17 02:14:40 +00:00
/// Read the config bytes from the radio so we can show them in our GUI, the radio's copy is ground truth
private fun readRadioConfig() {
val bytes = UIState.meshService!!.radioConfig
2020-02-17 02:14:40 +00:00
val config = MeshProtos.RadioConfig.parseFrom(bytes)
2020-03-02 16:41:16 +00:00
UIState.setRadioConfig(this, config)
2020-02-17 02:14:40 +00:00
2020-03-02 16:41:16 +00:00
debug("Read config from radio")
2020-02-17 02:14:40 +00:00
}
2020-02-14 15:47:20 +00:00
/// Called when we gain/lose a connection to our mesh radio
private fun onMeshConnectionChanged(connected: MeshService.ConnectionState) {
2020-02-14 15:47:20 +00:00
UIState.isConnected.value = connected
debug("connchange ${UIState.isConnected.value}")
if (connected == MeshService.ConnectionState.CONNECTED) {
2020-02-17 02:14:40 +00:00
// always get the current radio config when we connect
readRadioConfig()
// everytime the radio reconnects, we slam in our current owner data, the radio is smart enough to only broadcast if needed
2020-02-18 18:40:02 +00:00
UIState.setOwner(this)
2020-02-25 18:30:10 +00:00
val m = UIState.meshService!!
// Pull down our real node ID
NodeDB.myId.value = m.myId
// Update our nodeinfos based on data from the device
NodeDB.nodes.clear()
NodeDB.nodes.putAll(
m.nodes.map
{
it.user?.id!! to it
}
)
}
2020-02-14 15:47:20 +00:00
}
2020-02-09 13:52:17 +00:00
2020-03-05 17:50:33 +00:00
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return try {
super.dispatchTouchEvent(ev)
} catch (ex: Throwable) {
2020-03-05 17:50:33 +00:00
Exceptions.report(
ex,
"dispatchTouchEvent"
) // hide this Compose error from the user but report to the mothership
false
}
}
2020-02-14 15:47:20 +00:00
private val meshServiceReceiver = object : BroadcastReceiver() {
2020-02-09 13:52:17 +00:00
2020-02-14 15:47:20 +00:00
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
debug("Received from mesh service $intent")
2020-02-09 13:52:17 +00:00
2020-02-14 15:47:20 +00:00
when (intent.action) {
MeshService.ACTION_NODE_CHANGE -> {
val info: NodeInfo = intent.getParcelableExtra(EXTRA_NODEINFO)!!
debug("UI nodechange $info")
2020-02-09 13:52:17 +00:00
2020-02-14 15:47:20 +00:00
// We only care about nodes that have user info
info.user?.id?.let {
NodeDB.nodes[it] = info
2020-02-09 13:52:17 +00:00
}
2020-02-14 15:47:20 +00:00
}
2020-02-09 15:28:24 +00:00
2020-02-14 15:47:20 +00:00
MeshService.ACTION_RECEIVED_DATA -> {
debug("TODO rxdata")
val sender = intent.getStringExtra(EXTRA_SENDER)!!
val payload = intent.getByteArrayExtra(EXTRA_PAYLOAD)!!
val typ = intent.getIntExtra(EXTRA_TYP, -1)
when (typ) {
MeshProtos.Data.Type.CLEAR_TEXT_VALUE -> {
// FIXME - use the real time from the packet
2020-02-17 17:06:22 +00:00
// FIXME - don't just slam in a new list each time, it probably causes extra drawing. Figure out how to be Compose smarter...
val msg = TextMessage(sender, payload.toString(utf8))
MessagesState.addMessage(msg)
2020-02-09 13:52:17 +00:00
}
2020-02-14 15:47:20 +00:00
else -> TODO()
2020-02-09 13:52:17 +00:00
}
}
2020-02-14 15:47:20 +00:00
MeshService.ACTION_MESH_CONNECTED -> {
val connected =
MeshService.ConnectionState.valueOf(intent.getStringExtra(EXTRA_CONNECTED)!!)
2020-02-14 15:47:20 +00:00
onMeshConnectionChanged(connected)
}
else -> TODO()
2020-02-09 13:52:17 +00:00
}
}
2020-02-14 15:47:20 +00:00
}
2020-01-20 23:53:22 +00:00
2020-02-25 16:10:23 +00:00
private val mesh = object : ServiceClient<com.geeksville.mesh.IMeshService>({
com.geeksville.mesh.IMeshService.Stub.asInterface(it)
}) {
2020-02-29 21:42:15 +00:00
override fun onConnected(service: com.geeksville.mesh.IMeshService) {
UIState.meshService = service
2020-02-25 16:10:23 +00:00
// We don't start listening for packets until after we are connected to the service
registerMeshReceiver()
2020-02-25 16:10:23 +00:00
// We won't receive a notify for the initial state of connection, so we force an update here
val connectionState = MeshService.ConnectionState.valueOf(service.connectionState())
onMeshConnectionChanged(connectionState)
2020-02-10 23:39:04 +00:00
2020-02-25 16:10:23 +00:00
debug("connected to mesh service, isConnected=${UIState.isConnected.value}")
}
2020-02-14 15:47:20 +00:00
2020-02-25 16:10:23 +00:00
override fun onDisconnected() {
2020-02-14 15:47:20 +00:00
unregisterMeshReceiver()
UIState.meshService = null
2020-01-23 16:09:50 +00:00
}
2020-02-14 15:47:20 +00:00
}
2020-01-23 16:09:50 +00:00
2020-02-14 15:47:20 +00:00
private fun bindMeshService() {
debug("Binding to mesh service!")
// we bind using the well known name, to make sure 3rd party apps could also
if (UIState.meshService != null)
Exceptions.reportError("meshService was supposed to be null, ignoring (but reporting a bug)")
2020-02-25 16:10:23 +00:00
MeshService.startService(this)?.let { intent ->
2020-02-14 15:47:20 +00:00
// ALSO bind so we can use the api
2020-02-25 16:10:23 +00:00
mesh.connect(this, intent, Context.BIND_AUTO_CREATE)
2020-02-04 21:24:04 +00:00
}
2020-02-14 15:47:20 +00:00
}
2020-01-23 16:09:50 +00:00
2020-02-14 15:47:20 +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
debug("Unbinding from mesh service!")
2020-02-25 16:10:23 +00:00
mesh.close()
UIState.meshService = null
2020-02-14 15:47:20 +00:00
}
2020-01-23 16:09:50 +00:00
2020-02-18 17:09:49 +00:00
override fun onStop() {
ScanState.stopScan()
2020-02-14 15:47:20 +00:00
unregisterMeshReceiver() // No point in receiving updates while the GUI is gone, we'll get them when the user launches the activity
unbindMeshService()
2020-01-23 16:09:50 +00:00
2020-02-18 17:09:49 +00:00
super.onStop()
2020-02-14 15:47:20 +00:00
}
2020-01-23 16:09:50 +00:00
2020-02-18 17:09:49 +00:00
override fun onStart() {
super.onStart()
2020-01-23 16:09:50 +00:00
2020-02-14 15:47:20 +00:00
bindMeshService()
2020-02-25 23:07:09 +00:00
val bonded = RadioInterfaceService.getBondedDeviceAddress(this) != null
if (!bonded)
AppStatus.currentScreen = Screen.settings
2020-02-14 15:47:20 +00:00
}
2020-01-23 16:09:50 +00:00
2020-02-14 15:47:20 +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
}
2020-01-20 23:53:22 +00:00
2020-02-14 15:47:20 +00:00
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.
return when (item.itemId) {
R.id.action_settings -> true
else -> super.onOptionsItemSelected(item)
2020-01-20 23:53:22 +00:00
}
}
2020-02-14 15:47:20 +00:00
}
2020-01-21 17:37:39 +00:00
2020-02-09 18:18:26 +00:00