Merge remote-tracking branch 'origin/master' into dev

# Conflicts:
#	app/src/main/java/com/geeksville/mesh/service/MeshService.kt
#	app/src/main/java/com/geeksville/mesh/ui/BTScanScreen.kt
This commit is contained in:
geeksville 2020-03-11 18:17:20 -07:00
commit 48ea4f50fa
19 changed files with 260 additions and 82 deletions

View file

@ -13,6 +13,7 @@ import android.os.Build
import android.os.Bundle
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
@ -29,6 +30,7 @@ import com.geeksville.mesh.ui.AppStatus
import com.geeksville.mesh.ui.MeshApp
import com.geeksville.mesh.ui.ScanState
import com.geeksville.mesh.ui.Screen
import com.geeksville.util.Exceptions
import com.geeksville.util.exceptionReporter
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
@ -239,6 +241,8 @@ class MainActivity : AppCompatActivity(), Logging,
override fun onDestroy() {
unregisterMeshReceiver()
UIState.meshService =
null // When our activity goes away make sure we don't keep a ptr around to the service
super.onDestroy()
}
@ -328,6 +332,18 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return try {
super.dispatchTouchEvent(ev)
} catch (ex: Throwable) {
Exceptions.report(
ex,
"dispatchTouchEvent"
) // hide this Compose error from the user but report to the mothership
false
}
}
private val meshServiceReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
@ -395,7 +411,8 @@ class MainActivity : AppCompatActivity(), Logging,
private fun bindMeshService() {
debug("Binding to mesh service!")
// we bind using the well known name, to make sure 3rd party apps could also
logAssert(UIState.meshService == null)
if (UIState.meshService != null)
Exceptions.reportError("meshService was supposed to be null, ignoring (but reporting a bug)")
MeshService.startService(this)?.let { intent ->
// ALSO bind so we can use the api

View file

@ -530,6 +530,12 @@ class MeshService : Service(), Logging {
else -> TODO()
}
GeeksvilleApplication.analytics.track(
"data_receive",
DataPair("num_bytes", bytes.size),
DataPair("type", data.typValue)
)
}
/// Update our DB of users based on someone sending out a User subpacket
@ -686,10 +692,20 @@ class MeshService : Service(), Logging {
try {
reinitFromRadio()
val radioModel = DataPair("radio_model", myNodeInfo?.model ?: "unknown")
GeeksvilleApplication.analytics.track(
"mesh_connect",
DataPair("num_nodes", numNodes),
DataPair("num_online", numOnlineNodes)
DataPair("num_online", numOnlineNodes),
radioModel
)
// Once someone connects to hardware start tracking the approximate number of nodes in their mesh
// this allows us to collect stats on what typical mesh size is and to tell difference between users who just
// downloaded the app, vs has connected it to some hardware.
GeeksvilleApplication.analytics.setUserInfo(
DataPair("num_nodes", numNodes),
radioModel
)
} catch (ex: RemoteException) {
// It seems that when the ESP32 goes offline it can briefly come back for a 100ms ish which
@ -838,6 +854,12 @@ class MeshService : Service(), Logging {
sendToRadio(ToRadio.newBuilder().apply {
this.packet = packet
})
GeeksvilleApplication.analytics.track(
"data_send",
DataPair("num_bytes", payloadIn.size),
DataPair("type", typ)
)
}
override fun getRadioConfig(): ByteArray = toRemoteExceptions {

View file

@ -221,7 +221,7 @@ class RadioInterfaceService : Service(), Logging {
// Handle an incoming packet from the radio, broadcasts it as an android intent
private fun handleFromRadio(p: ByteArray) {
if(logReceives) {
if (logReceives) {
receivedPacketsLog.write(p)
receivedPacketsLog.flush()
}
@ -279,6 +279,9 @@ class RadioInterfaceService : Service(), Logging {
fromNum = service.getCharacteristic(BTM_FROMNUM_CHARACTER)
// We must set this to true before broadcasting connectionChanged
isConnected = true
safe!!.setNotify(fromNum, true) {
debug("fromNum changed, so we are reading new messages")
doReadFromRadio()
@ -286,7 +289,6 @@ class RadioInterfaceService : Service(), Logging {
// Now tell clients they can (finally use the api)
broadcastConnectionChanged(true)
isConnected = true
// Immediately broadcast any queued packets sitting on the device
doReadFromRadio()
@ -349,7 +351,7 @@ class RadioInterfaceService : Service(), Logging {
info("Closing radio interface service")
if (logSends)
sentPacketsLog.close()
if(logReceives)
if (logReceives)
receivedPacketsLog.close()
safe?.close()
safe = null

View file

@ -0,0 +1,19 @@
package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.compose.onCommit
import com.geeksville.android.GeeksvilleApplication
/**
* Track compose screen visibility
*/
@Composable
fun analyticsScreen(name: String) {
onCommit(AppStatus.currentScreen) {
GeeksvilleApplication.analytics.sendScreenView(name)
onDispose {
GeeksvilleApplication.analytics.endScreenView()
}
}
}

View file

@ -7,7 +7,6 @@ import androidx.ui.core.Text
import androidx.ui.foundation.shape.corner.RoundedCornerShape
import androidx.ui.graphics.Color
import androidx.ui.layout.*
import androidx.ui.material.Button
import androidx.ui.material.Divider
import androidx.ui.material.MaterialTheme
import androidx.ui.material.TextButton
@ -75,9 +74,9 @@ private fun DrawerButton(
Surface(
modifier = modifier + LayoutPadding(
left = 8.dp,
start = 8.dp,
top = 8.dp,
right = 8.dp,
end = 8.dp,
bottom = 0.dp
),
color = backgroundColor,

View file

@ -3,11 +3,13 @@ package com.geeksville.mesh.ui
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.le.*
import android.content.Context
import android.os.ParcelUuid
import androidx.compose.*
import androidx.compose.Composable
import androidx.compose.Model
import androidx.compose.frames.modelMapOf
import androidx.compose.onCommit
import androidx.ui.core.ContextAmbient
import androidx.ui.core.LayoutModifier
import androidx.ui.core.Text
import androidx.ui.layout.Column
import androidx.ui.layout.LayoutGravity
@ -45,7 +47,7 @@ object ScanState : Logging {
debug("stopping scan")
try {
scanner!!.stopScan(callback)
} catch(ex: Throwable) {
} catch (ex: Throwable) {
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
}
callback = null
@ -67,7 +69,9 @@ fun BTScanScreen() {
val bluetoothAdapter =
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter
onActive {
analyticsScreen(name = "settings")
onCommit(AppStatus.currentScreen) {
ScanState.debug("BTScan component active")
ScanUIState.selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
val scanCallback = object : ScanCallback() {
@ -117,10 +121,10 @@ fun BTScanScreen() {
/// The following call might return null if the user doesn't have bluetooth access permissions
val s: BluetoothLeScanner? = bluetoothAdapter.bluetoothLeScanner
if(s == null) {
ScanUIState.errorText = "This application requires bluetooth access. Please grant access in android settings."
}
else {
if (s == null) {
ScanUIState.errorText =
"This application requires bluetooth access. Please grant access in android settings."
} else {
ScanState.debug("starting scan")
// filter and only accept devices that have a sw update service
@ -138,6 +142,7 @@ fun BTScanScreen() {
}
onDispose {
ScanState.debug("BTScan component deactivated")
ScanState.stopScan()
}
}
@ -147,7 +152,10 @@ fun BTScanScreen() {
Text("An unexpected error was encountered. Please file a bug on our github: ${ScanUIState.errorText}")
} else {
if (ScanUIState.devices.isEmpty()) {
Text(text = "Looking for Meshtastic devices... (zero found)", modifier = LayoutGravity.Center)
Text(
text = "Looking for Meshtastic devices... (zero found)",
modifier = LayoutGravity.Center
)
CircularProgressIndicator() // Show that we are searching still
} else {

View file

@ -3,20 +3,22 @@ package com.geeksville.mesh.ui
import android.content.Intent
import android.graphics.Bitmap
import androidx.compose.Composable
import androidx.ui.core.ContextAmbient
import androidx.ui.core.Text
import androidx.ui.foundation.DrawImage
import androidx.ui.graphics.Image
import androidx.ui.graphics.ImageConfig
import androidx.ui.graphics.NativeImage
import androidx.ui.core.*
import androidx.ui.foundation.Box
import androidx.ui.graphics.*
import androidx.ui.graphics.colorspace.ColorSpace
import androidx.ui.graphics.colorspace.ColorSpaces
import androidx.ui.graphics.painter.ImagePainter
import androidx.ui.layout.*
import androidx.ui.material.MaterialTheme
import androidx.ui.material.OutlinedButton
import androidx.ui.material.ripple.Ripple
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.Density
import androidx.ui.unit.PxSize
import androidx.ui.unit.dp
import androidx.ui.unit.toRect
import com.geeksville.analytics.DataPair
import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.mesh.R
@ -70,8 +72,39 @@ class AndroidImage(val bitmap: Bitmap) : Image {
}
}
/// Stolen from the Compose SimpleImage, replace with their real Image component someday
// TODO(mount, malkov) : remove when RepaintBoundary is a modifier: b/149982905
// This is class and not val because if b/149985596
private object ClipModifier : DrawModifier {
override fun draw(density: Density, drawContent: () -> Unit, canvas: Canvas, size: PxSize) {
canvas.save()
canvas.clipRect(size.toRect())
drawContent()
canvas.restore()
}
}
/// Stolen from the Compose SimpleImage, replace with their real Image component someday
@Composable
fun ScaledImage(
image: Image,
modifier: Modifier = Modifier.None,
tint: Color? = null
) {
with(DensityAmbient.current) {
val imageModifier = ImagePainter(image).toModifier(
scaleFit = ScaleFit.FillMaxDimension,
colorFilter = tint?.let { ColorFilter(it, BlendMode.srcIn) }
)
Box(modifier + ClipModifier + imageModifier)
}
}
@Composable
fun ChannelContent(channel: Channel = Channel("Default", 7)) {
analyticsScreen(name = "channel")
val typography = MaterialTheme.typography()
val context = ContextAmbient.current
@ -87,14 +120,18 @@ fun ChannelContent(channel: Channel = Channel("Default", 7)) {
// val image = imageResource(id = R.drawable.qrcode)
val image = AndroidImage(UIState.getChannelQR(context))
Container(modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp)) {
DrawImage(image = image)
}
ScaledImage(
image = image,
modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp)
)
Ripple(bounded = false) {
OutlinedButton(modifier = LayoutGravity.Center + LayoutPadding(left = 24.dp),
OutlinedButton(modifier = LayoutGravity.Center + LayoutPadding(start = 24.dp),
onClick = {
GeeksvilleApplication.analytics.track("channel_share") // track how many times users share channels
GeeksvilleApplication.analytics.track(
"share",
DataPair("content_type", "channel")
) // track how many times users share channels
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND

View file

@ -2,7 +2,6 @@ package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.compose.state
import androidx.ui.animation.Crossfade
import androidx.ui.core.ContextAmbient
import androidx.ui.core.Text
import androidx.ui.layout.Column
@ -34,6 +33,8 @@ fun getInitials(name: String): String {
@Composable
fun HomeContent() {
analyticsScreen(name = "users")
Column {
Row {
VectorImage(
@ -124,35 +125,30 @@ fun previewView() {
@Composable
private fun AppContent(openDrawer: () -> Unit) {
Crossfade(AppStatus.currentScreen) { screen ->
Surface(color = (MaterialTheme.colors()).background) {
// crossfade breaks onCommit behavior because it keeps old views around
//Crossfade(AppStatus.currentScreen) { screen ->
Surface(color = (MaterialTheme.colors()).background) {
Column {
TopAppBar(
title = { Text(text = "Meshtastic") },
navigationIcon = {
Container(LayoutSize(40.dp, 40.dp)) {
VectorImageButton(R.drawable.ic_launcher_new_foreground) {
openDrawer()
}
Column {
TopAppBar(
title = { Text(text = "Meshtastic") },
navigationIcon = {
Container(LayoutSize(40.dp, 40.dp)) {
VectorImageButton(R.drawable.ic_launcher_new_foreground) {
openDrawer()
}
}
)
// VerticalScroller breaks flexible layouts - because verticalscrollers have 'infinite' height
// VerticalScroller(modifier = LayoutFlexible(1f)) {
//if (screen != Screen.settings)
// ScanState.stopScan() // Nasty hack to teardown the bt scanner
when (screen) {
Screen.messages -> MessagesContent()
Screen.settings -> SettingsContent()
Screen.users -> HomeContent()
Screen.channel -> ChannelContent()
else -> TODO()
}
//}
)
when (AppStatus.currentScreen) {
Screen.messages -> MessagesContent()
Screen.settings -> SettingsContent()
Screen.users -> HomeContent()
Screen.channel -> ChannelContent()
else -> TODO()
}
}
}
//}
}

View file

@ -39,7 +39,7 @@ fun MessageCard(msg: TextMessage, modifier: Modifier = Modifier.None) {
Row(modifier = modifier) {
UserIcon(NodeDB.nodes[msg.from])
Column(modifier = LayoutPadding(left = 12.dp)) {
Column(modifier = LayoutPadding(start = 12.dp)) {
Row {
val nodes = NodeDB.nodes
@ -51,7 +51,7 @@ fun MessageCard(msg: TextMessage, modifier: Modifier = Modifier.None) {
ProvideEmphasis(emphasis = TimestampEmphasis) {
Text(
text = dateFormat.format(msg.date),
modifier = LayoutPadding(left = 8.dp),
modifier = LayoutPadding(start = 8.dp),
style = MaterialTheme.typography().caption
)
}
@ -67,6 +67,8 @@ fun MessageCard(msg: TextMessage, modifier: Modifier = Modifier.None) {
@Composable
fun MessagesContent() {
analyticsScreen(name = "messages")
Column(modifier = LayoutSize.Fill) {
val sidePad = 8.dp
@ -79,8 +81,8 @@ fun MessagesContent() {
messages.forEach { msg ->
MessageCard(
msg, modifier = LayoutPadding(
left = sidePad,
right = sidePad,
start = sidePad,
end = sidePad,
top = topPad,
bottom = topPad
)

View file

@ -69,7 +69,7 @@ fun NodeInfoCard(node: NodeInfo) {
// Text("Node: ${it.user?.longName}")
Row(modifier = LayoutPadding(16.dp)) {
UserIcon(
modifier = LayoutPadding(left = 0.dp, top = 0.dp, right = 0.dp, bottom = 0.dp),
modifier = LayoutPadding(start = 0.dp, top = 0.dp, end = 0.dp, bottom = 0.dp),
user = node
)

View file

@ -1,7 +1,6 @@
package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.compose.ambient
import androidx.compose.state
import androidx.ui.core.ContextAmbient
import androidx.ui.core.Text

View file

@ -2,25 +2,23 @@ package com.geeksville.mesh.ui
import androidx.annotation.DrawableRes
import androidx.compose.Composable
import androidx.ui.core.DensityAmbient
import androidx.ui.core.Modifier
import androidx.ui.foundation.Clickable
import androidx.ui.foundation.Icon
import androidx.ui.graphics.Color
import androidx.ui.graphics.vector.DrawVector
import androidx.ui.layout.Container
import androidx.ui.layout.LayoutSize
import androidx.ui.material.ripple.Ripple
import androidx.ui.material.IconButton
import androidx.ui.res.vectorResource
import androidx.ui.unit.dp
@Composable
fun VectorImageButton(@DrawableRes id: Int, onClick: () -> Unit) {
Ripple(bounded = false) {
Clickable(onClick = onClick) {
VectorImage(id = id /* , modifier = LayoutSize(40.dp, 40.dp) */)
}
//Ripple(bounded = false) {
IconButton(onClick = onClick) {
Icon(vectorResource(id) /* , modifier = LayoutSize(40.dp, 40.dp) */)
}
//}
}
/* fun AppBarIcon(icon: Image, onClick: () -> Unit) {
@ -40,13 +38,13 @@ fun VectorImage(
) {
val vector = vectorResource(id)
// WithDensity {
Container(
modifier = modifier + LayoutSize(
vector.defaultWidth,
vector.defaultHeight
)
) {
DrawVector(vector, tint)
}
Container(
modifier = modifier + LayoutSize(
vector.defaultWidth,
vector.defaultHeight
)
) {
DrawVector(vector, tint)
}
// }
}

@ -1 +1 @@
Subproject commit e1a48b6e75c2ec3ad3995165a7b4fb64ce597e02
Subproject commit f309ee8f9e9db37daabd7c76da683e052ef62f7a