Merge pull request #12 from geeksville/decompose

Big changes to remove Compose and switch back to classic android
1.2-legacy
Kevin Hester 2020-04-09 11:05:26 -07:00 zatwierdzone przez GitHub
commit 82f994ab9a
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 4AEE18F83AFDEB23
48 zmienionych plików z 1998 dodań i 1664 usunięć

Wyświetl plik

@ -16,7 +16,6 @@
</set>
</option>
<option name="resolveModulePerSourceSet" value="false" />
<option name="useQualifiedModuleNames" value="true" />
</GradleProjectSettings>
</option>
</component>

Wyświetl plik

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RenderSettings">
<option name="useLiveRendering" value="false" />
</component>
</project>

Wyświetl plik

@ -29,7 +29,7 @@ android {
buildFeatures {
// Enables Jetpack Compose for this module
compose true // NOTE, if true main app crashes if you use regular view layout functions
// compose true // NOTE, if true main app crashes if you use regular view layout functions
}
// Set both the Java and Kotlin compilers to target Java 8.
@ -44,8 +44,8 @@ android {
}
composeOptions {
kotlinCompilerVersion "1.3.61-dev-withExperimentalGoogleExtensions-20200129"
kotlinCompilerExtensionVersion "$compose_version"
//kotlinCompilerVersion "1.3.61-dev-withExperimentalGoogleExtensions-20200129"
//kotlinCompilerExtensionVersion "$compose_version"
}
}
@ -73,11 +73,17 @@ protobuf {
dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.core:core-ktx:1.2.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'com.google.android.material:material:1.0.0'
implementation "androidx.fragment:fragment-ktx:1.2.4"
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-beta4'
implementation 'com.google.android.material:material:1.1.0'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'androidx.lifecycle:lifecycle-extensions:2.1.0'
implementation 'androidx.lifecycle:lifecycle-livedata-ktx:2.1.0'
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test.ext:junit:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0'
@ -95,17 +101,6 @@ dependencies {
// mapbox
implementation 'com.mapbox.mapboxsdk:mapbox-android-sdk:9.0.0'
// You also need to include the following Compose toolkit dependencies.
implementation("androidx.compose:compose-runtime:$compose_version")
implementation("androidx.ui:ui-graphics:$compose_version")
implementation("androidx.ui:ui-layout:$compose_version")
implementation("androidx.ui:ui-material:$compose_version")
implementation("androidx.ui:ui-unit:$compose_version")
implementation("androidx.ui:ui-util:$compose_version")
implementation "androidx.ui:ui-tooling:$compose_version"
androidTestImplementation("androidx.ui:ui-platform:$compose_version")
androidTestImplementation("androidx.ui:ui-test:$compose_version")
// location services
implementation 'com.google.android.gms:play-services-location:17.0.0'

Wyświetl plik

@ -34,7 +34,9 @@
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<!-- the xing library will try to bring this permission in but we don't want it -->
<uses-permission android:name="android.permission.CAMERA" tools:node="remove" />
<uses-permission
android:name="android.permission.CAMERA"
tools:node="remove" />
<uses-feature
android:name="android.hardware.bluetooth_le"
@ -84,6 +86,7 @@
android:name="com.geeksville.mesh.MainActivity"
android:label="@string/app_name"
android:screenOrientation="portrait"
android:windowSoftInputMode="stateAlwaysHidden"
android:theme="@style/AppTheme.NoActionBar">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

Wyświetl plik

@ -1,104 +0,0 @@
/*
* Copyright 2020 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package androidx.ui.fakeandroidview
import android.content.Context
import android.view.LayoutInflater
import android.view.MotionEvent
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import androidx.annotation.LayoutRes
import androidx.compose.Composable
/**
* Composes an Android [View] given a layout resource [resId]. The method handles the inflation
* of the [View] and will call the [postInflationCallback] after this happens. Note that the
* callback will always be invoked on the main thread.
*
* @param resId The id of the layout resource to be inflated.
* @param postInflationCallback The callback to be invoked after the layout is inflated.
*/
@Composable
// TODO(popam): support modifiers here
fun AndroidView(@LayoutRes resId: Int, postInflationCallback: (View) -> Unit = { _ -> }) {
AndroidViewHolder(postInflationCallback = postInflationCallback, resId = resId)
}
private class AndroidViewHolder(context: Context) : ViewGroup(context) {
var view: View? = null
set(value) {
if (value != field) {
field = value
removeAllViews()
addView(view)
}
}
var postInflationCallback: (View) -> Unit = {}
var resId: Int? = null
set(value) {
if (value != field) {
field = value
val inflater = LayoutInflater.from(context)
val view = inflater.inflate(resId!!, this, false)
this.view = view
postInflationCallback(view)
}
}
init {
isClickable = true
}
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
view?.measure(widthMeasureSpec, heightMeasureSpec)
setMeasuredDimension(view?.measuredWidth ?: 0, view?.measuredHeight ?: 0)
}
override fun onLayout(changed: Boolean, l: Int, t: Int, r: Int, b: Int) {
view?.layout(l, t, r, b)
}
override fun getLayoutParams(): LayoutParams? {
return view?.layoutParams ?: LayoutParams(MATCH_PARENT, MATCH_PARENT)
}
/**
* Implement this method to handle touch screen motion events.
*
*
* If this method is used to detect click actions, it is recommended that
* the actions be performed by implementing and calling
* [.performClick]. This will ensure consistent system behavior,
* including:
*
* * obeying click sound preferences
* * dispatching OnClickListener calls
* * handling [ACTION_CLICK][AccessibilityNodeInfo.ACTION_CLICK] when
* accessibility features are enabled
*
*
* @param event The motion event.
* @return True if the event was handled, false otherwise.
*/
override fun onTouchEvent(event: MotionEvent?): Boolean {
return super.onTouchEvent(event)
}
}

Wyświetl plik

@ -1,5 +1,6 @@
package com.geeksville.mesh
// import kotlinx.android.synthetic.main.tabs.*
import android.Manifest
import android.bluetooth.BluetoothAdapter
import android.bluetooth.BluetoothManager
@ -15,29 +16,28 @@ import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.widget.Toast
import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import androidx.ui.core.setContent
import androidx.fragment.app.Fragment
import androidx.lifecycle.Observer
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.geeksville.android.Logging
import com.geeksville.android.ServiceClient
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.model.UIViewModel
import com.geeksville.mesh.service.*
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.mesh.ui.*
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
import com.google.android.gms.tasks.Task
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.android.synthetic.main.activity_main.*
import java.nio.charset.Charset
/*
UI design
@ -105,6 +105,55 @@ class MainActivity : AppCompatActivity(), Logging,
bluetoothManager.adapter
}
private val model: UIViewModel by viewModels()
data class TabInfo(val text: String, val icon: Int, val content: Fragment)
// private val tabIndexes = generateSequence(0) { it + 1 } FIXME, instead do withIndex or zip? to get the ids below, also stop duplicating strings
private val tabInfos = arrayOf(
TabInfo(
"Messages",
R.drawable.ic_twotone_message_24,
MessagesFragment()
),
TabInfo(
"Users",
R.drawable.ic_twotone_people_24,
UsersFragment()
),
TabInfo(
"Map",
R.drawable.ic_twotone_map_24,
MapFragment()
),
TabInfo(
"Channel",
R.drawable.ic_twotone_contactless_24,
ChannelFragment()
),
TabInfo(
"Settings",
R.drawable.ic_twotone_settings_applications_24,
SettingsFragment()
)
)
private
val tabsAdapter = object : FragmentStateAdapter(this) {
override fun getItemCount(): Int = tabInfos.size
override fun createFragment(position: Int): Fragment {
// Return a NEW fragment instance in createFragment(int)
/*
fragment.arguments = Bundle().apply {
// Our object is just an integer :-P
putInt(ARG_OBJECT, position + 1)
} */
return tabInfos[position].content
}
}
private fun requestPermission() {
debug("Checking permissions")
@ -139,7 +188,11 @@ class MainActivity : AppCompatActivity(), Logging,
}
// Ask for all the missing perms
ActivityCompat.requestPermissions(this, missingPerms.toTypedArray(), DID_REQUEST_PERM)
ActivityCompat.requestPermissions(
this,
missingPerms.toTypedArray(),
DID_REQUEST_PERM
)
// DID_REQUEST_PERM is an
// app-defined int constant. The callback method gets the
@ -161,7 +214,7 @@ class MainActivity : AppCompatActivity(), Logging,
private fun sendTestPackets() {
exceptionReporter {
val m = UIState.meshService!!
val m = model.meshService!!
// Do some test operations
val testPayload = "hello world".toByteArray()
@ -182,10 +235,8 @@ class MainActivity : AppCompatActivity(), Logging,
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val prefs = UIState.getPreferences(this)
UIState.ownerName = prefs.getString("owner", "")!!
UIState.meshService = null
UIState.savedInstanceState = savedInstanceState
val prefs = UIViewModel.getPreferences(this)
model.ownerName.value = prefs.getString("owner", "")!!
// Ensures Bluetooth is available on the device and it is enabled. If not,
// displays a dialog requesting user permission to enable Bluetooth.
@ -195,7 +246,11 @@ class MainActivity : AppCompatActivity(), Logging,
startActivityForResult(enableBtIntent, REQUEST_ENABLE_BT)
}
} else {
Toast.makeText(this, "Error - this app requires bluetooth", Toast.LENGTH_LONG)
Toast.makeText(
this,
"Error - this app requires bluetooth",
Toast.LENGTH_LONG
)
.show()
}
@ -220,11 +275,32 @@ class MainActivity : AppCompatActivity(), Logging,
// Handle any intent
handleIntent(intent)
setContent {
/* setContent {
MeshApp()
}
} */
setContentView(R.layout.activity_main)
pager.adapter = tabsAdapter
pager.isUserInputEnabled =
false // Gestures for screen switching doesn't work so good with the map view
// pager.offscreenPageLimit = 0 // Don't keep any offscreen pages around, because we want to make sure our bluetooth scanning stops
TabLayoutMediator(tab_layout, pager) { tab, position ->
// tab.text = tabInfos[position].text // I think it looks better with icons only
tab.icon = getDrawable(tabInfos[position].icon)
}.attach()
model.isConnected.observe(this, Observer { connected ->
val image = when (connected) {
MeshService.ConnectionState.CONNECTED -> R.drawable.cloud_on
MeshService.ConnectionState.DEVICE_SLEEP -> R.drawable.ic_twotone_cloud_upload_24
MeshService.ConnectionState.DISCONNECTED -> R.drawable.cloud_off
}
connectStatusImage.setImageDrawable(getDrawable(image))
})
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
@ -235,26 +311,26 @@ class MainActivity : AppCompatActivity(), Logging,
val appLinkAction = intent.action
val appLinkData: Uri? = intent.data
UIState.requestedChannelUrl = null // assume none
// Were we asked to open one our channel URLs?
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")
UIState.requestedChannelUrl = appLinkData
val requestedChannelUrl = appLinkData
}
}
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()
}
/**
* Dispatch incoming result to the correct fragment.
*/
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
super.onActivityResult(requestCode, resultCode, data)
// Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...);
@ -281,7 +357,8 @@ class MainActivity : AppCompatActivity(), Logging,
} */
}
private var receiverRegistered = false
private
var receiverRegistered = false
private fun registerMeshReceiver() {
logAssert(!receiverRegistered)
@ -300,40 +377,27 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
/// 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
val config = MeshProtos.RadioConfig.parseFrom(bytes)
UIState.setRadioConfig(this, config)
debug("Read config from radio")
}
/// Called when we gain/lose a connection to our mesh radio
private fun onMeshConnectionChanged(connected: MeshService.ConnectionState) {
UIState.isConnected.value = connected
debug("connchange ${UIState.isConnected.value}")
model.isConnected.value = connected
debug("connchange ${model.isConnected.value}")
if (connected == MeshService.ConnectionState.CONNECTED) {
// 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
UIState.setOwner(this)
model.setOwner(this)
val m = UIState.meshService!!
val m = model.meshService!!
// Pull down our real node ID
NodeDB.myId.value = m.myId
model.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
}
)
val nodes = m.nodes.map {
it.user?.id!! to it
}.toMap()
model.nodeDB.nodes.value = nodes
}
}
@ -349,76 +413,95 @@ class MainActivity : AppCompatActivity(), Logging,
}
}
private val meshServiceReceiver = object : BroadcastReceiver() {
private
val meshServiceReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) = exceptionReporter {
debug("Received from mesh service $intent")
override fun onReceive(context: Context, intent: Intent) =
exceptionReporter {
debug("Received from mesh service $intent")
when (intent.action) {
MeshService.ACTION_NODE_CHANGE -> {
val info: NodeInfo = intent.getParcelableExtra(EXTRA_NODEINFO)!!
debug("UI nodechange $info")
when (intent.action) {
MeshService.ACTION_NODE_CHANGE -> {
val info: NodeInfo =
intent.getParcelableExtra(EXTRA_NODEINFO)!!
debug("UI nodechange $info")
// We only care about nodes that have user info
info.user?.id?.let {
NodeDB.nodes[it] = info
}
}
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
// 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)
// We only care about nodes that have user info
info.user?.id?.let {
val newnodes = model.nodeDB.nodes.value!! + Pair(it, info)
model.nodeDB.nodes.value = newnodes
}
else -> TODO()
}
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
// 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)
)
model.messagesState.addMessage(msg)
}
else -> TODO()
}
}
MeshService.ACTION_MESH_CONNECTED -> {
val connected =
MeshService.ConnectionState.valueOf(
intent.getStringExtra(
EXTRA_CONNECTED
)!!
)
onMeshConnectionChanged(connected)
}
else -> TODO()
}
MeshService.ACTION_MESH_CONNECTED -> {
val connected =
MeshService.ConnectionState.valueOf(intent.getStringExtra(EXTRA_CONNECTED)!!)
onMeshConnectionChanged(connected)
}
else -> TODO()
}
}
}
private val mesh = object : ServiceClient<com.geeksville.mesh.IMeshService>({
com.geeksville.mesh.IMeshService.Stub.asInterface(it)
}) {
override fun onConnected(service: com.geeksville.mesh.IMeshService) {
UIState.meshService = service
private
val mesh = object :
ServiceClient<com.geeksville.mesh.IMeshService>({
com.geeksville.mesh.IMeshService.Stub.asInterface(it)
}) {
override fun onConnected(service: com.geeksville.mesh.IMeshService) = exceptionReporter {
model.meshService = service
debug("Getting latest radioconfig from service")
model.radioConfig.value = MeshProtos.RadioConfig.parseFrom(service.radioConfig)
// We don't start listening for packets until after we are connected to the service
registerMeshReceiver()
// 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())
val connectionState =
MeshService.ConnectionState.valueOf(service.connectionState())
onMeshConnectionChanged(connectionState)
debug("connected to mesh service, isConnected=${UIState.isConnected.value}")
debug("connected to mesh service, isConnected=${model.isConnected.value}")
}
override fun onDisconnected() {
unregisterMeshReceiver()
UIState.meshService = null
model.meshService = null
}
}
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)
if (model.meshService != null)
Exceptions.reportError("meshService was supposed to be null, ignoring (but reporting a bug)")
MeshService.startService(this)?.let { intent ->
@ -433,11 +516,10 @@ class MainActivity : AppCompatActivity(), Logging,
// if we never connected, do nothing
debug("Unbinding from mesh service!")
mesh.close()
UIState.meshService = null
model.meshService = null
}
override fun onStop() {
ScanState.stopScan()
unregisterMeshReceiver() // No point in receiving updates while the GUI is gone, we'll get them when the user launches the activity
unbindMeshService()
@ -449,9 +531,12 @@ class MainActivity : AppCompatActivity(), Logging,
bindMeshService()
val bonded = RadioInterfaceService.getBondedDeviceAddress(this) != null
val bonded =
RadioInterfaceService.getBondedDeviceAddress(this) != null
/* FIXME - not yet working
if (!bonded)
AppStatus.currentScreen = Screen.settings
*/
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {

Wyświetl plik

@ -3,7 +3,6 @@ package com.geeksville.mesh.model
import android.graphics.Bitmap
import android.net.Uri
import android.util.Base64
import androidx.compose.Model
import com.geeksville.mesh.MeshProtos
import com.google.zxing.BarcodeFormat
import com.google.zxing.MultiFormatWriter
@ -11,7 +10,6 @@ import com.journeyapps.barcodescanner.BarcodeEncoder
import java.net.MalformedURLException
@Model
data class Channel(
var name: String,
var modemConfig: MeshProtos.ChannelSettings.ModemConfig,

Wyświetl plik

@ -1,7 +1,7 @@
package com.geeksville.mesh.model
import android.os.RemoteException
import androidx.compose.frames.modelListOf
import androidx.lifecycle.MutableLiveData
import com.geeksville.android.BuildUtils.isEmulator
import com.geeksville.android.Logging
import com.geeksville.mesh.MeshProtos
@ -21,8 +21,8 @@ data class TextMessage(
)
object MessagesState : Logging {
private val testTexts = arrayOf(
class MessagesState(private val ui: UIViewModel) : Logging {
private val testTexts = listOf(
TextMessage(
"+16508765310",
"I found the cache"
@ -35,17 +35,20 @@ object MessagesState : Logging {
// If the following (unused otherwise) line is commented out, the IDE preview window works.
// if left in the preview always renders as empty.
val messages = modelListOf(* if (isEmulator) testTexts else arrayOf())
val messages =
object : MutableLiveData<List<TextMessage>>(if (isEmulator) testTexts else listOf()) {
}
/// add a message our GUI list of past msgs
fun addMessage(m: TextMessage) {
messages.add(m)
messages.value = messages.value!! + m
}
/// Send a message and added it to our GUI log
fun sendMessage(str: String, dest: String? = null) {
var error: String? = null
val service = UIState.meshService
val service = ui.meshService
if (service != null)
try {
service.sendData(
@ -59,9 +62,9 @@ object MessagesState : Logging {
else
error = "Error: No Mesh service"
MessagesState.addMessage(
addMessage(
TextMessage(
NodeDB.myId.value,
ui.nodeDB.myId.value!!,
str,
errorMessage = error
)

Wyświetl plik

@ -1,13 +1,14 @@
package com.geeksville.mesh.model
import androidx.compose.frames.modelMapOf
import androidx.compose.mutableStateOf
import androidx.lifecycle.MutableLiveData
import com.geeksville.android.BuildUtils.isEmulator
import com.geeksville.mesh.MeshUser
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.Position
object NodeDB {
/// NodeDB lives inside the UIViewModel, but it needs a backpointer to reach the service
class NodeDB(private val ui: UIViewModel) {
private val testPositions = arrayOf(
Position(32.776665, -96.796989, 35), // dallas
Position(32.960758, -96.733521, 35), // richardson
@ -43,12 +44,14 @@ object NodeDB {
private val seedWithTestNodes = isEmulator
/// The unique ID of our node
val myId = mutableStateOf(if (isEmulator) "+16508765309" else "invalid")
val myId = object : MutableLiveData<String>(if (isEmulator) "+16508765309" else "invalid") {}
/// A map from nodeid to to nodeinfo
val nodes =
modelMapOf(* (if (isEmulator) testNodes else listOf()).map { it.user!!.id to it }.toTypedArray())
object :
MutableLiveData<Map<String, NodeInfo>>(mapOf(*(if (isEmulator) testNodes else listOf()).map { it.user!!.id to it }
.toTypedArray())) {}
/// Could be null if we haven't received our node DB yet
val ourNodeInfo get() = nodes[myId.value]
val ourNodeInfo get() = nodes.value!![myId.value]
}

Wyświetl plik

@ -3,72 +3,98 @@ package com.geeksville.mesh.model
import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.os.Bundle
import android.os.RemoteException
import androidx.compose.mutableStateOf
import androidx.core.content.edit
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import com.geeksville.android.BuildUtils.isEmulator
import com.geeksville.android.Logging
import com.geeksville.mesh.IMeshService
import com.geeksville.mesh.MeshProtos
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.getInitials
/// FIXME - figure out how to merge this staate with the AppStatus Model
object UIState : Logging {
/// Given a human name, strip out the first letter of the first three words and return that as the initials for
/// that user.
fun getInitials(name: String): String {
val words = name.split(Regex("\\s+")).filter { it.isNotEmpty() }.take(3).map { it.first() }
.joinToString("")
return words
}
class UIViewModel : ViewModel(), Logging {
init {
debug("ViewModel created")
}
companion object {
/**
* Return the current channel info
* FIXME, we should sim channels at the MeshService level if we are running on an emulator,
* for now I just fake it by returning a canned channel.
*/
fun getChannel(c: MeshProtos.RadioConfig?): Channel? {
val channel = c?.channelSettings?.let { Channel(it) }
return if (channel == null && isEmulator)
Channel.emulated
else
channel
}
fun getPreferences(context: Context): SharedPreferences =
context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
}
var meshService: IMeshService? = null
val nodeDB = NodeDB(this)
val messagesState = MessagesState(this)
/// Are we connected to our radio device
val isConnected =
object :
MutableLiveData<MeshService.ConnectionState>(MeshService.ConnectionState.DISCONNECTED) {
}
/// various radio settings (including the channel)
val radioConfig = object : MutableLiveData<MeshProtos.RadioConfig?>(null) {
}
override fun onCleared() {
super.onCleared()
debug("ViewModel cleared")
}
/// Set the radio config (also updates our saved copy in preferences)
fun setRadioConfig(context: Context, c: MeshProtos.RadioConfig) {
debug("Setting new radio config!")
meshService?.radioConfig = c.toByteArray()
radioConfig.value = c
getPreferences(context).edit(commit = true) {
this.putString("channel-url", getChannel(c)!!.getChannelUrl().toString())
}
}
/// Kinda ugly - created in the activity but used from Compose - figure out if there is a cleaner way GIXME
// lateinit var googleSignInClient: GoogleSignInClient
var meshService: IMeshService? = null
/// Are we connected to our radio device
val isConnected = mutableStateOf(MeshService.ConnectionState.DISCONNECTED)
/// various radio settings (including the channel)
private val radioConfig = mutableStateOf<MeshProtos.RadioConfig?>(null)
/// our name in hte radio
/// Note, we generate owner initials automatically for now
/// our activity will read this from prefs or set it to the empty string
var ownerName: String = "MrInIDE Ownername"
val ownerName = object : MutableLiveData<String>("MrIDE Test") {
}
/// If the app was launched because we received a new channel intent, the Url will be here
var requestedChannelUrl: Uri? = null
var savedInstanceState: Bundle? = null
/**
* Return the current channel info
* FIXME, we should sim channels at the MeshService level if we are running on an emulator,
* for now I just fake it by returning a canned channel.
*/
fun getChannel(): Channel? {
val channel = radioConfig.value?.channelSettings?.let { Channel(it) }
return if (channel == null && isEmulator)
Channel.emulated
else
channel
}
fun getPreferences(context: Context): SharedPreferences =
context.getSharedPreferences("ui-prefs", Context.MODE_PRIVATE)
/// Set the radio config (also updates our saved copy in preferences)
fun setRadioConfig(context: Context, c: MeshProtos.RadioConfig) {
radioConfig.value = c
getPreferences(context).edit(commit = true) {
this.putString("channel-url", getChannel()!!.getChannelUrl().toString())
}
}
// clean up all this nasty owner state management FIXME
fun setOwner(context: Context, s: String? = null) {
if (s != null) {
ownerName = s
ownerName.value = s
// note: we allow an empty userstring to be written to prefs
getPreferences(context).edit(commit = true) {
@ -77,15 +103,16 @@ object UIState : Logging {
}
// Note: we are careful to not set a new unique ID
if (ownerName.isNotEmpty())
if (ownerName.value!!.isNotEmpty())
try {
meshService?.setOwner(
null,
ownerName,
getInitials(ownerName)
ownerName.value,
getInitials(ownerName.value!!)
) // Note: we use ?. here because we might be running in the emulator
} catch (ex: RemoteException) {
errormsg("Can't set username on device, is device offline? ${ex.message}")
}
}
}

Wyświetl plik

@ -316,7 +316,7 @@ class RadioInterfaceService : Service(), Logging {
debug("requested MTU result=$mtuRes")
mtuRes.getOrThrow() // FIXME - why sometimes is the result Unit!?!
fromNum = service.getCharacteristic(BTM_FROMNUM_CHARACTER)
fromNum = service.getCharacteristic(BTM_FROMNUM_CHARACTER)!!
// We must set this to true before broadcasting connectionChanged
isConnected = true

Wyświetl plik

@ -1,19 +0,0 @@
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()
}
}
}

Wyświetl plik

@ -1,88 +0,0 @@
package com.geeksville.mesh.ui
import android.graphics.Bitmap
import androidx.compose.Composable
import androidx.ui.core.DensityAmbient
import androidx.ui.core.DrawModifier
import androidx.ui.core.Modifier
import androidx.ui.core.asModifier
import androidx.ui.foundation.Box
import androidx.ui.graphics.*
import androidx.ui.graphics.colorspace.ColorSpaces
import androidx.ui.graphics.painter.ImagePainter
import androidx.ui.unit.Density
import androidx.ui.unit.PxSize
import androidx.ui.unit.toRect
/// 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: ImageAsset,
modifier: Modifier = Modifier.None,
tint: Color? = null
) {
with(DensityAmbient.current) {
val imageModifier = ImagePainter(image).asModifier(
scaleFit = ScaleFit.FillMaxDimension,
colorFilter = tint?.let { ColorFilter(it, BlendMode.srcIn) }
)
Box(modifier + ClipModifier + imageModifier)
}
}
/// Borrowed from Compose
class AndroidImage(val bitmap: Bitmap) : ImageAsset {
/**
* @see Image.width
*/
override val width: Int
get() = bitmap.width
/**
* @see Image.height
*/
override val height: Int
get() = bitmap.height
override val config: ImageAssetConfig get() = ImageAssetConfig.Argb8888
/**
* @see Image.colorSpace
*/
override val colorSpace: androidx.ui.graphics.colorspace.ColorSpace
get() = ColorSpaces.Srgb
/**
* @see Image.hasAlpha
*/
override val hasAlpha: Boolean
get() = bitmap.hasAlpha()
/**
* @see Image.nativeImage
*/
override val nativeImage: NativeImageAsset
get() = bitmap
/**
* @see
*/
override fun prepareToDraw() {
bitmap.prepareToDraw()
}
}

Wyświetl plik

@ -1,114 +0,0 @@
package com.geeksville.mesh.ui
import androidx.annotation.DrawableRes
import androidx.compose.Composable
import androidx.ui.core.Modifier
import androidx.ui.foundation.Text
import androidx.ui.foundation.shape.corner.RoundedCornerShape
import androidx.ui.graphics.Color
import androidx.ui.layout.*
import androidx.ui.material.Divider
import androidx.ui.material.MaterialTheme
import androidx.ui.material.Surface
import androidx.ui.material.TextButton
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.mesh.R
@Composable
fun AppDrawer(
currentScreen: ScreenInfo,
closeDrawer: () -> Unit
) {
Column(modifier = LayoutSize.Fill) {
Spacer(LayoutHeight(24.dp))
Row(modifier = LayoutPadding(16.dp)) {
VectorImage(
id = R.drawable.ic_launcher_new_foreground,
tint = MaterialTheme.colors.primary
)
Spacer(LayoutWidth(8.dp))
// VectorImage(id = R.drawable.ic_launcher_new_foreground)
}
Divider(color = Color(0x14333333))
@Composable
fun ScreenButton(screen: ScreenInfo) {
DrawerButton(
icon = screen.icon,
label = screen.label,
isSelected = currentScreen == screen
) {
navigateTo(screen)
closeDrawer()
}
}
ScreenButton(Screen.messages)
ScreenButton(Screen.users)
ScreenButton(Screen.map) // turn off for now
ScreenButton(Screen.channel)
ScreenButton(Screen.settings)
}
}
@Composable
private fun DrawerButton(
modifier: Modifier = Modifier.None,
@DrawableRes icon: Int,
label: String,
isSelected: Boolean,
action: () -> Unit
) {
val colors = MaterialTheme.colors
val textIconColor = if (isSelected) {
colors.primary
} else {
colors.onSurface.copy(alpha = 0.6f)
}
val backgroundColor = if (isSelected) {
colors.primary.copy(alpha = 0.12f)
} else {
colors.surface
}
Surface(
modifier = modifier + LayoutPadding(
start = 8.dp,
top = 8.dp,
end = 8.dp,
bottom = 0.dp
),
color = backgroundColor,
shape = RoundedCornerShape(4.dp)
) {
TextButton(onClick = action) {
Row {
VectorImage(
modifier = LayoutGravity.Center,
id = icon,
tint = textIconColor
)
Spacer(LayoutWidth(16.dp))
Text(
text = label,
style = (MaterialTheme.typography).body2.copy(
color = textIconColor
),
modifier = LayoutWidth.Fill
)
}
}
}
}
@Preview
@Composable
fun previewDrawer() {
AppDrawer(
currentScreen = AppStatus.currentScreen,
closeDrawer = { }
)
}

Wyświetl plik

@ -1,242 +0,0 @@
package com.geeksville.mesh.ui
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.le.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.ParcelUuid
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.foundation.Text
import androidx.ui.layout.Column
import androidx.ui.layout.LayoutGravity
import androidx.ui.material.CircularProgressIndicator
import androidx.ui.material.MaterialTheme
import androidx.ui.material.ProvideEmphasis
import androidx.ui.material.RadioGroup
import androidx.ui.tooling.preview.Preview
import com.geeksville.android.Logging
import com.geeksville.mesh.service.RadioInterfaceService
import com.geeksville.util.exceptionReporter
@Model
object ScanUIState {
var selectedMacAddr: String? = null
var errorText: String? = null
val devices = modelMapOf<String, BTScanEntry>()
/// Change to a new macaddr selection, updating GUI and radio
fun changeSelection(context: Context, newAddr: String) {
ScanState.info("Changing BT device to $newAddr")
selectedMacAddr = newAddr
RadioInterfaceService.setBondedDeviceAddress(context, newAddr)
}
}
/// FIXME, remove once compose has better lifecycle management
object ScanState : Logging {
var scanner: BluetoothLeScanner? = null
var callback: ScanCallback? = null // SUPER NASTY FIXME
fun stopScan() {
if (callback != null) {
debug("stopping scan")
try {
scanner!!.stopScan(callback)
} catch (ex: Throwable) {
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
}
callback = null
}
}
}
@Model
data class BTScanEntry(val name: String, val macAddress: String, val bonded: Boolean) {
val isSelected get() = macAddress == ScanUIState.selectedMacAddr
}
@Composable
fun BTScanScreen() {
val context = ContextAmbient.current
/// Note: may be null on platforms without a bluetooth driver (ie. the emulator)
val bluetoothAdapter =
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter
analyticsScreen(name = "settings")
onCommit(AppStatus.currentScreen) {
ScanState.debug("BTScan component active")
ScanUIState.selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
val scanCallback = object : ScanCallback() {
override fun onScanFailed(errorCode: Int) {
val msg = "Unexpected bluetooth scan failure: $errorCode"
// error code2 seeems to be indicate hung bluetooth stack
ScanUIState.errorText = msg
ScanState.reportError(msg)
}
// 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) {
val addr = result.device.address
// prevent logspam because weill get get lots of redundant scan results
val isBonded = result.device.bondState == BluetoothDevice.BOND_BONDED
val oldEntry = ScanUIState.devices[addr]
if (oldEntry == null || oldEntry.bonded != isBonded) {
val entry = BTScanEntry(
result.device.name,
addr,
isBonded
)
ScanState.debug("onScanResult ${entry}")
ScanUIState.devices[addr] = entry
// If nothing was selected, by default select the first thing we see
if (ScanUIState.selectedMacAddr == null && entry.bonded)
ScanUIState.changeSelection(context, addr)
}
}
}
if (bluetoothAdapter == null) {
ScanState.warn("No bluetooth adapter. Running under emulation?")
val testnodes = listOf(
BTScanEntry("Meshtastic_ab12", "xx", false),
BTScanEntry("Meshtastic_32ac", "xb", true)
)
ScanUIState.devices.putAll(testnodes.map { it.macAddress to it })
// If nothing was selected, by default select the first thing we see
if (ScanUIState.selectedMacAddr == null)
ScanUIState.changeSelection(context, testnodes.first().macAddress)
} else {
/// 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 {
ScanState.debug("starting scan")
// filter and only accept devices that have a sw update service
val filter =
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID))
.build()
val settings =
ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY).build()
s.startScan(listOf(filter), settings, scanCallback)
ScanState.scanner = s
ScanState.callback = scanCallback
}
}
onDispose {
ScanState.debug("BTScan component deactivated")
ScanState.stopScan()
}
}
Column {
if (ScanUIState.errorText != null) {
Text(text = ScanUIState.errorText!!)
} else {
if (ScanUIState.devices.isEmpty()) {
Text(
text = "Looking for Meshtastic devices... (zero found)",
modifier = LayoutGravity.Center
)
CircularProgressIndicator() // Show that we are searching still
} else {
// val allPaired = bluetoothAdapter?.bondedDevices.orEmpty().map { it.address }.toSet()
/* Only let user select paired devices
val paired = devices.values.filter { allPaired.contains(it.macAddress) }
if (paired.size < devices.size) {
Text(
"Warning: there are nearby Meshtastic devices that are not paired with this phone. Before you can select a device, you will need to pair it in Bluetooth Settings."
)
} */
RadioGroup {
Column {
ScanUIState.devices.values.forEach {
// disabled pending https://issuetracker.google.com/issues/149528535
ProvideEmphasis(emphasis = if (it.bonded) MaterialTheme.emphasisLevels.high else MaterialTheme.emphasisLevels.disabled) {
RadioGroupTextItem(
selected = (it.isSelected),
onSelect = {
// If the device is paired, let user select it, otherwise start the pairing flow
if (it.bonded) {
ScanUIState.changeSelection(context, it.macAddress)
} else {
ScanState.info("Starting bonding for $it")
// We need this receiver to get informed when the bond attempt finished
val bondChangedReceiver = object : BroadcastReceiver() {
override fun onReceive(
context: Context,
intent: Intent
) = exceptionReporter {
val state =
intent.getIntExtra(
BluetoothDevice.EXTRA_BOND_STATE,
-1
)
ScanState.debug("Received bond state changed $state")
context.unregisterReceiver(this)
if (state == BluetoothDevice.BOND_BONDED || state == BluetoothDevice.BOND_BONDING) {
ScanState.debug("Bonding completed, connecting service")
ScanUIState.changeSelection(
context,
it.macAddress
)
}
}
}
val filter = IntentFilter()
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
context.registerReceiver(bondChangedReceiver, filter)
// We ignore missing BT adapters, because it lets us run on the emulator
bluetoothAdapter
?.getRemoteDevice(it.macAddress)
?.createBond()
}
},
text = it.name
)
}
}
}
}
}
}
}
}
@Preview
@Composable
fun btScanScreenPreview() {
BTScanScreen()
}

Wyświetl plik

@ -1,129 +0,0 @@
package com.geeksville.mesh.ui
import android.content.Intent
import androidx.compose.Composable
import androidx.ui.core.ContextAmbient
import androidx.ui.foundation.Text
import androidx.ui.input.ImeAction
import androidx.ui.layout.*
import androidx.ui.material.MaterialTheme
import androidx.ui.material.OutlinedButton
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.analytics.DataPair
import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.mesh.R
import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.toHumanString
object ChannelLog : Logging
@Composable
fun ChannelContent(channel: Channel?) {
analyticsScreen(name = "channel")
val typography = MaterialTheme.typography
val context = ContextAmbient.current
Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) {
if (channel != null) {
Row(modifier = LayoutGravity.Center) {
Text(text = "Channel ", modifier = LayoutGravity.Center)
if (channel.editable) {
// FIXME - limit to max length
StyledTextField(
value = channel.name,
onValueChange = { channel.name = it },
textStyle = typography.h4.copy(
color = palette.onSecondary.copy(alpha = 0.8f)
),
imeAction = ImeAction.Done,
onImeActionPerformed = {
ChannelLog.errormsg("FIXME, implement channel edit button")
}
)
} else {
Text(
text = channel.name,
style = typography.h4
)
}
}
// simulated qr code
// val image = imageResource(id = R.drawable.qrcode)
val image = AndroidImage(channel.getChannelQR())
ScaledImage(
image = image,
modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp)
)
Text(
text = "Mode: ${channel.modemConfig.toHumanString()}",
modifier = LayoutGravity.Center + LayoutPadding(bottom = 16.dp)
)
Row(modifier = LayoutGravity.Center) {
OutlinedButton(onClick = {
channel.editable = !channel.editable
}) {
if (channel.editable)
VectorImage(
id = R.drawable.ic_twotone_lock_open_24,
tint = palette.onBackground
)
else
VectorImage(
id = R.drawable.ic_twotone_lock_24,
tint = palette.onBackground
)
}
// Only show the share buttone once we are locked
if (!channel.editable)
OutlinedButton(modifier = LayoutPadding(start = 24.dp),
onClick = {
GeeksvilleApplication.analytics.track(
"share",
DataPair("content_type", "channel")
) // track how many times users share channels
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, channel.getChannelUrl().toString())
putExtra(
Intent.EXTRA_TITLE,
"A URL for joining a Meshtastic mesh"
)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
context.startActivity(shareIntent)
}) {
VectorImage(
id = R.drawable.ic_twotone_share_24,
tint = palette.onBackground
)
}
}
}
}
}
@Preview
@Composable
fun previewChannel() {
// another bug? It seems modaldrawerlayout not yet supported in preview
MaterialTheme(colors = palette) {
ChannelContent(Channel.emulated)
}
}

Wyświetl plik

@ -0,0 +1,187 @@
package com.geeksville.mesh.ui
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ArrayAdapter
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import com.geeksville.analytics.DataPair
import com.geeksville.android.GeeksvilleApplication
import com.geeksville.android.Logging
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.google.android.material.snackbar.Snackbar
import kotlinx.android.synthetic.main.channel_fragment.*
class ChannelFragment : ScreenFragment("Channel"), Logging {
private val model: UIViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.channel_fragment, container, false)
}
private fun onEditingChanged() {
val isEditing = editableCheckbox.isChecked
channelOptions.isEnabled = false // Not yet ready
shareButton.isEnabled = !isEditing
channelNameView.isEnabled = isEditing
qrView.visibility =
if (isEditing) View.INVISIBLE else View.VISIBLE // Don't show the user a stale QR code
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
onEditingChanged() // Set initial state
editableCheckbox.setOnCheckedChangeListener { _, checked ->
onEditingChanged()
if (!checked) {
// User just locked it, we should warn and then apply changes to radio FIXME not ready yet
Snackbar.make(
editableCheckbox,
"Changing channels is not yet supported",
Snackbar.LENGTH_SHORT
).show()
}
}
model.radioConfig.observe(viewLifecycleOwner, Observer { config ->
val channel = UIViewModel.getChannel(config)
if (channel != null) {
qrView.visibility = View.VISIBLE
channelNameEdit.visibility = View.VISIBLE
channelNameEdit.setText(channel.name)
editableCheckbox.isEnabled = true
qrView.setImageBitmap(channel.getChannelQR())
// Share this particular channel if someone clicks share
shareButton.setOnClickListener {
GeeksvilleApplication.analytics.track(
"share",
DataPair("content_type", "channel")
) // track how many times users share channels
val sendIntent: Intent = Intent().apply {
action = Intent.ACTION_SEND
putExtra(Intent.EXTRA_TEXT, channel.getChannelUrl().toString())
putExtra(
Intent.EXTRA_TITLE,
"A URL for joining a Meshtastic mesh"
)
type = "text/plain"
}
val shareIntent = Intent.createChooser(sendIntent, null)
requireActivity().startActivity(shareIntent)
}
} else {
qrView.visibility = View.INVISIBLE
channelNameEdit.visibility = View.INVISIBLE
editableCheckbox.isEnabled = false
}
val adapter = ArrayAdapter(
requireContext(),
R.layout.dropdown_menu_popup_item,
arrayOf("Item 1", "Item 2", "Item 3", "Item 4")
)
filled_exposed_dropdown.setAdapter(adapter)
})
}
}
/*
@Composable
fun ChannelContent(channel: Channel?) {
val typography = MaterialTheme.typography
val context = ContextAmbient.current
Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) {
if (channel != null) {
Row(modifier = LayoutGravity.Center) {
Text(text = "Channel ", modifier = LayoutGravity.Center)
if (channel.editable) {
// FIXME - limit to max length
StyledTextField(
value = channel.name,
onValueChange = { channel.name = it },
textStyle = typography.h4.copy(
color = palette.onSecondary.copy(alpha = 0.8f)
),
imeAction = ImeAction.Done,
onImeActionPerformed = {
ChannelLog.errormsg("FIXME, implement channel edit button")
}
)
} else {
Text(
text = channel.name,
style = typography.h4
)
}
}
// simulated qr code
// val image = imageResource(id = R.drawable.qrcode)
val image = AndroidImage(channel.getChannelQR())
ScaledImage(
image = image,
modifier = LayoutGravity.Center + LayoutSize.Min(200.dp, 200.dp)
)
Text(
text = "Mode: ${channel.modemConfig.toHumanString()}",
modifier = LayoutGravity.Center + LayoutPadding(bottom = 16.dp)
)
Row(modifier = LayoutGravity.Center) {
OutlinedButton(onClick = {
channel.editable = !channel.editable
}) {
if (channel.editable)
VectorImage(
id = R.drawable.ic_twotone_lock_open_24,
tint = palette.onBackground
)
else
VectorImage(
id = R.drawable.ic_twotone_lock_24,
tint = palette.onBackground
)
}
// Only show the share buttone once we are locked
if (!channel.editable)
OutlinedButton(modifier = LayoutPadding(start = 24.dp),
onClick = {
}) {
VectorImage(
id = R.drawable.ic_twotone_share_24,
tint = palette.onBackground
)
}
}
}
}
}
*/

Wyświetl plik

@ -1,187 +0,0 @@
package com.geeksville.mesh.ui
import android.app.Activity
import android.app.Application
import android.graphics.Color
import android.os.Bundle
import androidx.compose.Composable
import androidx.compose.onCommit
import androidx.ui.core.ContextAmbient
import androidx.ui.fakeandroidview.AndroidView
import androidx.ui.material.MaterialTheme
import androidx.ui.tooling.preview.Preview
import com.geeksville.android.Logging
import com.geeksville.mesh.R
import com.geeksville.mesh.model.NodeDB
import com.geeksville.mesh.model.UIState
import com.mapbox.geojson.Feature
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Point
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.geometry.LatLngBounds
import com.mapbox.mapboxsdk.maps.MapView
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.style.expressions.Expression
import com.mapbox.mapboxsdk.style.layers.Property
import com.mapbox.mapboxsdk.style.layers.Property.TEXT_ANCHOR_TOP
import com.mapbox.mapboxsdk.style.layers.Property.TEXT_JUSTIFY_AUTO
import com.mapbox.mapboxsdk.style.layers.PropertyFactory.*
import com.mapbox.mapboxsdk.style.layers.SymbolLayer
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource
object mapLog : Logging
/**
* mapbox requires this, until compose has a nicer way of doing it, do it here
*/
private val mapLifecycleCallbacks = object : Application.ActivityLifecycleCallbacks {
var view: MapView? = null
override fun onActivityPaused(activity: Activity) {
view?.onPause()
}
override fun onActivityStarted(activity: Activity) {
view?.onStart()
}
override fun onActivityDestroyed(activity: Activity) {
view?.onDestroy()
}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {
view?.onSaveInstanceState(outState)
}
override fun onActivityStopped(activity: Activity) {
view?.onStop()
}
/**
* Called when the Activity calls [super.onCreate()][Activity.onCreate].
*/
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
}
override fun onActivityResumed(activity: Activity) {
view?.onResume()
}
}
@Composable
fun MapContent() {
analyticsScreen(name = "map")
val context = ContextAmbient.current
onCommit(AppStatus.currentScreen) {
onDispose {
// We no longer care about activity lifecycle
(context.applicationContext as Application).unregisterActivityLifecycleCallbacks(
mapLifecycleCallbacks
)
mapLifecycleCallbacks.view = null
}
}
// Find all nodes with valid locations
val nodesWithPosition = NodeDB.nodes.values.filter { it.validPosition != null }
val locations = nodesWithPosition.map { node ->
val p = node.position!!
mapLog.debug("Showing on map: $node")
val f = Feature.fromGeometry(
Point.fromLngLat(
p.longitude,
p.latitude
)
)
node.user?.let {
f.addStringProperty("name", it.longName)
}
f
}
val nodeSourceId = "node-positions"
val nodeLayerId = "node-layer"
val labelLayerId = "label-layer"
val markerImageId = "my-marker-image"
val nodePositions =
GeoJsonSource(nodeSourceId, FeatureCollection.fromFeatures(locations))
// val markerIcon = BitmapFactory.decodeResource(context.resources, R.drawable.ic_twotone_person_pin_24)
val markerIcon = context.getDrawable(R.drawable.ic_twotone_person_pin_24)!!
val nodeLayer = SymbolLayer(nodeLayerId, nodeSourceId).withProperties(
iconImage(markerImageId),
iconAnchor(Property.ICON_ANCHOR_BOTTOM),
iconAllowOverlap(true)
)
val labelLayer = SymbolLayer(labelLayerId, nodeSourceId).withProperties(
textField(Expression.get("name")),
textSize(12f),
textColor(Color.RED),
textVariableAnchor(arrayOf(TEXT_ANCHOR_TOP)),
textJustify(TEXT_JUSTIFY_AUTO),
textAllowOverlap(true)
)
AndroidView(R.layout.map_view) { view ->
view as MapView
view.onCreate(UIState.savedInstanceState)
mapLifecycleCallbacks.view = view
(context.applicationContext as Application).registerActivityLifecycleCallbacks(
mapLifecycleCallbacks
)
view.getMapAsync { map ->
map.setStyle(Style.OUTDOORS) { style ->
style.addSource(nodePositions)
style.addImage(markerImageId, markerIcon)
style.addLayer(nodeLayer)
style.addLayer(labelLayer)
}
//map.uiSettings.isScrollGesturesEnabled = true
//map.uiSettings.isZoomGesturesEnabled = true
if (nodesWithPosition.isNotEmpty()) {
val update = if (nodesWithPosition.size >= 2) {
// Multiple nodes, make them all fit on the map view
val bounds = LatLngBounds.Builder()
// Add all positions
bounds.includes(nodesWithPosition.map { it.position!! }
.map { LatLng(it.latitude, it.longitude) })
CameraUpdateFactory.newLatLngBounds(bounds.build(), 150)
} else {
// Only one node, just zoom in on it
val it = nodesWithPosition[0].position!!
val cameraPos = CameraPosition.Builder().target(
LatLng(it.latitude, it.longitude)
).zoom(9.0).build()
CameraUpdateFactory.newCameraPosition(cameraPos)
}
map.animateCamera(update, 1000)
}
}
}
}
@Preview
@Composable
fun previewMap() {
// another bug? It seems modaldrawerlayout not yet supported in preview
MaterialTheme(colors = palette) {
MapContent()
}
}

Wyświetl plik

@ -0,0 +1,183 @@
package com.geeksville.mesh.ui
import android.graphics.Color
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import com.geeksville.android.Logging
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.mapbox.geojson.Feature
import com.mapbox.geojson.FeatureCollection
import com.mapbox.geojson.Point
import com.mapbox.mapboxsdk.camera.CameraPosition
import com.mapbox.mapboxsdk.camera.CameraUpdateFactory
import com.mapbox.mapboxsdk.geometry.LatLng
import com.mapbox.mapboxsdk.geometry.LatLngBounds
import com.mapbox.mapboxsdk.maps.MapView
import com.mapbox.mapboxsdk.maps.MapboxMap
import com.mapbox.mapboxsdk.maps.Style
import com.mapbox.mapboxsdk.style.expressions.Expression
import com.mapbox.mapboxsdk.style.layers.Property
import com.mapbox.mapboxsdk.style.layers.Property.TEXT_ANCHOR_TOP
import com.mapbox.mapboxsdk.style.layers.Property.TEXT_JUSTIFY_AUTO
import com.mapbox.mapboxsdk.style.layers.PropertyFactory.*
import com.mapbox.mapboxsdk.style.layers.SymbolLayer
import com.mapbox.mapboxsdk.style.sources.GeoJsonSource
class MapFragment : ScreenFragment("Map"), Logging {
private val model: UIViewModel by activityViewModels()
private val nodeSourceId = "node-positions"
private val nodeLayerId = "node-layer"
private val labelLayerId = "label-layer"
private val markerImageId = "my-marker-image"
private val nodePositions = GeoJsonSource(nodeSourceId)
private val nodeLayer = SymbolLayer(nodeLayerId, nodeSourceId).withProperties(
iconImage(markerImageId),
iconAnchor(Property.ICON_ANCHOR_BOTTOM),
iconAllowOverlap(true)
)
private val labelLayer = SymbolLayer(labelLayerId, nodeSourceId).withProperties(
textField(Expression.get("name")),
textSize(12f),
textColor(Color.RED),
textVariableAnchor(arrayOf(TEXT_ANCHOR_TOP)),
textJustify(TEXT_JUSTIFY_AUTO),
textAllowOverlap(true)
)
private fun onNodesChanged(map: MapboxMap, nodes: Collection<NodeInfo>) {
val nodesWithPosition = nodes.filter { it.validPosition != null }
/**
* Using the latest nodedb, generate geojson features
*/
fun getCurrentNodes(): FeatureCollection {
// Find all nodes with valid locations
val locations = nodesWithPosition.map { node ->
val p = node.position!!
debug("Showing on map: $node")
val f = Feature.fromGeometry(
Point.fromLngLat(
p.longitude,
p.latitude
)
)
node.user?.let {
f.addStringProperty("name", it.longName)
}
f
}
return FeatureCollection.fromFeatures(locations)
}
fun zoomToNodes(map: MapboxMap) {
if (nodesWithPosition.isNotEmpty()) {
val update = if (nodesWithPosition.size >= 2) {
// Multiple nodes, make them all fit on the map view
val bounds = LatLngBounds.Builder()
// Add all positions
bounds.includes(nodes.map { it.position!! }
.map { LatLng(it.latitude, it.longitude) })
CameraUpdateFactory.newLatLngBounds(bounds.build(), 150)
} else {
// Only one node, just zoom in on it
val it = nodesWithPosition[0].position!!
val cameraPos = CameraPosition.Builder().target(
LatLng(it.latitude, it.longitude)
).zoom(9.0).build()
CameraUpdateFactory.newCameraPosition(cameraPos)
}
map.animateCamera(update, 1000)
}
}
nodePositions.setGeoJson(getCurrentNodes()) // Update node positions
zoomToNodes(map)
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? = inflater.inflate(R.layout.map_view, container, false)
lateinit var mapView: MapView
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
mapView = view.findViewById(R.id.mapView)
mapView.onCreate(savedInstanceState)
mapView.getMapAsync { map ->
// val markerIcon = BitmapFactory.decodeResource(context.resources, R.drawable.ic_twotone_person_pin_24)
val markerIcon = requireActivity().getDrawable(R.drawable.ic_twotone_person_pin_24)!!
map.setStyle(Style.OUTDOORS) { style ->
style.addSource(nodePositions)
style.addImage(markerImageId, markerIcon)
style.addLayer(nodeLayer)
style.addLayer(labelLayer)
}
model.nodeDB.nodes.observe(viewLifecycleOwner, Observer { nodes ->
onNodesChanged(map, nodes.values)
})
//map.uiSettings.isScrollGesturesEnabled = true
//map.uiSettings.isZoomGesturesEnabled = true
}
}
override fun onPause() {
mapView.onPause()
super.onPause()
}
override fun onStart() {
super.onStart()
mapView.onStart()
}
override fun onStop() {
mapView.onStop()
super.onStop()
}
override fun onResume() {
super.onResume()
mapView.onResume()
}
override fun onDestroy() {
mapView.onDestroy()
super.onDestroy()
}
override fun onSaveInstanceState(outState: Bundle) {
mapView.onSaveInstanceState(outState)
super.onSaveInstanceState(outState)
}
}

Wyświetl plik

@ -1,19 +1,13 @@
package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.compose.state
import androidx.ui.foundation.Text
import androidx.ui.material.*
import androidx.ui.tooling.preview.Preview
import com.geeksville.android.Logging
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIState
object UILog : Logging
/*
val palette = lightColorPalette() // darkColorPalette()
@Composable
fun MeshApp() {
val (drawerState, onDrawerStateChange) = state { DrawerState.Closed }
@ -73,3 +67,4 @@ private fun AppContent(openDrawer: () -> Unit) {
}
//}
}
*/

Wyświetl plik

@ -1,124 +0,0 @@
package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.compose.state
import androidx.ui.core.Modifier
import androidx.ui.foundation.Text
import androidx.ui.foundation.VerticalScroller
import androidx.ui.graphics.Color
import androidx.ui.input.ImeAction
import androidx.ui.layout.Column
import androidx.ui.layout.LayoutPadding
import androidx.ui.layout.LayoutSize
import androidx.ui.layout.Row
import androidx.ui.material.Emphasis
import androidx.ui.material.MaterialTheme
import androidx.ui.material.ProvideEmphasis
import androidx.ui.text.TextStyle
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.mesh.model.MessagesState
import com.geeksville.mesh.model.MessagesState.messages
import com.geeksville.mesh.model.NodeDB
import com.geeksville.mesh.model.TextMessage
import java.text.SimpleDateFormat
private val dateFormat = SimpleDateFormat("h:mm a")
val TimestampEmphasis = object : Emphasis {
override fun emphasize(color: Color) = color.copy(alpha = 0.25f)
}
/**
* A pretty version the text, with user icon to the left, name and time of arrival (copy slack look and feel)
*/
@Composable
fun MessageCard(msg: TextMessage, modifier: Modifier = Modifier.None) {
Row(modifier = modifier) {
UserIcon(NodeDB.nodes[msg.from])
Column(modifier = LayoutPadding(start = 12.dp)) {
Row {
val nodes = NodeDB.nodes
// If we can't find the sender, just use the ID
val node = nodes.get(msg.from)
val user = node?.user
val senderName = user?.longName ?: msg.from
Text(text = senderName)
ProvideEmphasis(emphasis = TimestampEmphasis) {
Text(
text = dateFormat.format(msg.date),
modifier = LayoutPadding(start = 8.dp),
style = MaterialTheme.typography.caption
)
}
}
if (msg.errorMessage != null)
Text(text = msg.errorMessage, style = TextStyle(color = palette.error))
else
Text(text = msg.text)
}
}
}
@Composable
fun MessagesContent() {
analyticsScreen(name = "messages")
Column(modifier = LayoutSize.Fill) {
val sidePad = 8.dp
val topPad = 4.dp
VerticalScroller(
modifier = LayoutWeight(1f)
) {
Column {
messages.forEach { msg ->
MessageCard(
msg, modifier = LayoutPadding(
start = sidePad,
end = sidePad,
top = topPad,
bottom = topPad
)
)
}
}
}
// Spacer(LayoutFlexible(1f))
val message = state { "" }
StyledTextField(
value = message.value,
onValueChange = { message.value = it },
textStyle = TextStyle(
color = palette.onSecondary.copy(alpha = 0.8f)
),
imeAction = ImeAction.Send,
onImeActionPerformed = {
MessagesState.info("did IME action")
val str = message.value
MessagesState.sendMessage(str)
message.value = "" // blow away the string the user just entered
},
hintText = "Type your message here..."
)
}
}
@Preview
@Composable
fun previewMessagesView() {
// another bug? It seems modaldrawerlayout not yet supported in preview
MaterialTheme(colors = palette) {
MessagesContent()
}
}

Wyświetl plik

@ -0,0 +1,280 @@
package com.geeksville.mesh.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.EditText
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.android.Logging
import com.geeksville.mesh.R
import com.geeksville.mesh.model.TextMessage
import com.geeksville.mesh.model.UIViewModel
import kotlinx.android.synthetic.main.adapter_message_layout.view.*
import kotlinx.android.synthetic.main.messages_fragment.*
// Allows usage like email.on(EditorInfo.IME_ACTION_NEXT, { confirm() })
fun EditText.on(actionId: Int, func: () -> Unit) {
setOnEditorActionListener { _, receivedActionId, _ ->
if (actionId == receivedActionId) {
func()
}
true
}
}
class MessagesFragment : ScreenFragment("Messages"), Logging {
private val model: UIViewModel by activityViewModels()
// Provide a direct reference to each of the views within a data item
// Used to cache the views within the item layout for fast access
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val username = itemView.username
val messageText = itemView.messageText
}
private val messagesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
/**
* Called when RecyclerView needs a new [ViewHolder] of the given type to represent
* an item.
*
*
* This new ViewHolder should be constructed with a new View that can represent the items
* of the given type. You can either create a new View manually or inflate it from an XML
* layout file.
*
*
* The new ViewHolder will be used to display items of the adapter using
* [.onBindViewHolder]. Since it will be re-used to display
* different items in the data set, it is a good idea to cache references to sub views of
* the View to avoid unnecessary [View.findViewById] calls.
*
* @param parent The ViewGroup into which the new View will be added after it is bound to
* an adapter position.
* @param viewType The view type of the new View.
*
* @return A new ViewHolder that holds a View of the given view type.
* @see .getItemViewType
* @see .onBindViewHolder
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(requireContext())
// Inflate the custom layout
// Inflate the custom layout
val contactView: View = inflater.inflate(R.layout.adapter_message_layout, parent, false)
// Return a new holder instance
return ViewHolder(contactView)
}
/**
* Returns the total number of items in the data set held by the adapter.
*
* @return The total number of items in this adapter.
*/
override fun getItemCount(): Int = messages.size
/**
* Called by RecyclerView to display the data at the specified position. This method should
* update the contents of the [ViewHolder.itemView] to reflect the item at the given
* position.
*
*
* Note that unlike [android.widget.ListView], RecyclerView will not call this method
* again if the position of the item changes in the data set unless the item itself is
* invalidated or the new position cannot be determined. For this reason, you should only
* use the `position` parameter while acquiring the related data item inside
* this method and should not keep a copy of it. If you need the position of an item later
* on (e.g. in a click listener), use [ViewHolder.getAdapterPosition] which will
* have the updated adapter position.
*
* Override [.onBindViewHolder] instead if Adapter can
* handle efficient partial bind.
*
* @param holder The ViewHolder which should be updated to represent the contents of the
* item at the given position in the data set.
* @param position The position of the item within the adapter's data set.
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val msg = messages[position]
val nodes = model.nodeDB.nodes.value!!
// If we can't find the sender, just use the ID
val node = nodes.get(msg.from)
val user = node?.user
holder.username.text = user?.shortName ?: msg.from
if (msg.errorMessage != null) {
// FIXME, set the style to show a red error message
holder.messageText.text = msg.errorMessage
} else {
holder.messageText.text = msg.text
}
}
private var messages = arrayOf<TextMessage>()
/// Called when our node DB changes
fun onMessagesChanged(nodesIn: Collection<TextMessage>) {
messages = nodesIn.toTypedArray()
notifyDataSetChanged() // FIXME, this is super expensive and redraws all messages
// scroll to the last line
messageListView.scrollToPosition(this.itemCount - 1)
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.messages_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
messageInputText.on(EditorInfo.IME_ACTION_DONE) {
debug("did IME action")
val str = messageInputText.text.toString()
model.messagesState.sendMessage(str)
messageInputText.setText("") // blow away the string the user just entered
// requireActivity().hideKeyboard()
}
messageListView.adapter = messagesAdapter
val layoutManager = LinearLayoutManager(requireContext())
layoutManager.stackFromEnd = true // We want the last rows to always be shown
messageListView.layoutManager = layoutManager
model.messagesState.messages.observe(viewLifecycleOwner, Observer { it ->
messagesAdapter.onMessagesChanged(it)
})
}
}
/*
import androidx.compose.Composable
import androidx.compose.state
import androidx.ui.core.Modifier
import androidx.ui.foundation.Text
import androidx.ui.foundation.VerticalScroller
import androidx.ui.graphics.Color
import androidx.ui.input.ImeAction
import androidx.ui.layout.Column
import androidx.ui.layout.LayoutPadding
import androidx.ui.layout.LayoutSize
import androidx.ui.layout.Row
import androidx.ui.material.Emphasis
import androidx.ui.material.MaterialTheme
import androidx.ui.material.ProvideEmphasis
import androidx.ui.text.TextStyle
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.mesh.model.MessagesState
import com.geeksville.mesh.model.MessagesState.messages
import com.geeksville.mesh.model.NodeDB
import com.geeksville.mesh.model.TextMessage
import java.text.SimpleDateFormat
private val dateFormat = SimpleDateFormat("h:mm a")
val TimestampEmphasis = object : Emphasis {
override fun emphasize(color: Color) = color.copy(alpha = 0.25f)
}
/// A pretty version the text, with user icon to the left, name and time of arrival (copy slack look and feel)
@Composable
fun MessageCard(msg: TextMessage, modifier: Modifier = Modifier.None) {
Row(modifier = modifier) {
UserIcon(NodeDB.nodes[msg.from])
Column(modifier = LayoutPadding(start = 12.dp)) {
Row {
val nodes = NodeDB.nodes
// If we can't find the sender, just use the ID
val node = nodes.get(msg.from)
val user = node?.user
val senderName = user?.longName ?: msg.from
Text(text = senderName)
ProvideEmphasis(emphasis = TimestampEmphasis) {
Text(
text = dateFormat.format(msg.date),
modifier = LayoutPadding(start = 8.dp),
style = MaterialTheme.typography.caption
)
}
}
if (msg.errorMessage != null)
Text(text = msg.errorMessage, style = TextStyle(color = palette.error))
else
Text(text = msg.text)
}
}
}
@Composable
fun MessagesContent() {
Column(modifier = LayoutSize.Fill) {
val sidePad = 8.dp
val topPad = 4.dp
VerticalScroller(
modifier = LayoutWeight(1f)
) {
Column {
messages.forEach { msg ->
MessageCard(
msg, modifier = LayoutPadding(
start = sidePad,
end = sidePad,
top = topPad,
bottom = topPad
)
)
}
}
}
// Spacer(LayoutFlexible(1f))
val message = state { "" }
StyledTextField(
value = message.value,
onValueChange = { message.value = it },
textStyle = TextStyle(
color = palette.onSecondary.copy(alpha = 0.8f)
),
imeAction = ImeAction.Send,
onImeActionPerformed = {
MessagesState.info("did IME action")
val str = message.value
MessagesState.sendMessage(str)
message.value = "" // blow away the string the user just entered
},
hintText = "Type your message here..."
)
}
}
*/

Wyświetl plik

@ -1,91 +0,0 @@
package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.ui.foundation.Text
import androidx.ui.layout.*
import androidx.ui.material.MaterialTheme
import androidx.ui.material.ProvideEmphasis
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.model.NodeDB
import androidx.ui.core.Modifier as Modifier1
/*
@Composable
fun NodeIcon(modifier: Modifier1 = Modifier1.None, node: NodeInfo) {
Column {
Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
VectorImage(id = if (node.user?.shortName != null) R.drawable.person else R.drawable.help)
}
// Show our shortname if possible
/* node.user?.shortName?.let {
Text(it)
} */
}
}
*/
@Composable
fun CompassHeading(modifier: Modifier1 = Modifier1.None, node: NodeInfo) {
Column {
if (node.position != null) {
Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
VectorImage(id = R.drawable.navigation)
}
} else Container(modifier = modifier + LayoutSize(40.dp, 40.dp)) {
VectorImage(id = R.drawable.help)
}
Text("2.3 km") // always reserve space for the distance even if we aren't showing it
}
}
@Composable
fun NodeHeading(node: NodeInfo) {
ProvideEmphasis(emphasis = MaterialTheme.emphasisLevels.high) {
Text(
node.user?.longName ?: "unknown",
style = MaterialTheme.typography.subtitle1
//modifier = LayoutWidth.Fill
)
}
}
/**
* An info card for a node:
*
* on left, the icon for the user (or shortname if that is all we have) (this includes user's distance and heading arrow)
*
* Middle is users fullname
*
*/
@Composable
fun NodeInfoCard(node: NodeInfo) {
// Text("Node: ${it.user?.longName}")
Row(modifier = LayoutPadding(16.dp)) {
UILog.debug("showing NodeInfo $node")
UserIcon(
modifier = LayoutPadding(start = 0.dp, top = 0.dp, end = 0.dp, bottom = 0.dp),
user = node
)
NodeHeading(node)
// FIXME - show compass instead
// CompassHeading(node = node)
}
}
@Preview
@Composable
fun nodeInfoPreview() {
Column {
NodeInfoCard(NodeDB.testNodes[0])
NodeInfoCard(NodeDB.testNodeNoPosition)
}
}

Wyświetl plik

@ -0,0 +1,21 @@
package com.geeksville.mesh.ui
import androidx.fragment.app.Fragment
import com.geeksville.android.GeeksvilleApplication
/**
* A fragment that represents a current 'screen' in our app.
*
* Useful for tracking analytics
*/
open class ScreenFragment(private val screenName: String) : Fragment() {
override fun onResume() {
super.onResume()
GeeksvilleApplication.analytics.sendScreenView(screenName)
}
override fun onPause() {
GeeksvilleApplication.analytics.endScreenView()
super.onPause()
}
}

Wyświetl plik

@ -1,79 +0,0 @@
package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.compose.state
import androidx.ui.core.ContextAmbient
import androidx.ui.foundation.Text
import androidx.ui.input.ImeAction
import androidx.ui.layout.*
import androidx.ui.material.MaterialTheme
import androidx.ui.text.TextStyle
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.android.Logging
import com.geeksville.mesh.model.MessagesState
import com.geeksville.mesh.model.UIState
import com.geeksville.mesh.service.RadioInterfaceService
object SettingsLog : Logging
@Composable
fun SettingsContent() {
//val typography = MaterialTheme.typography()
val context = ContextAmbient.current
Column(modifier = LayoutSize.Fill + LayoutPadding(16.dp)) {
Row {
Text("Your name ", modifier = LayoutGravity.Center)
val name = state { UIState.ownerName }
StyledTextField(
value = name.value,
onValueChange = { name.value = it },
textStyle = TextStyle(
color = palette.onSecondary.copy(alpha = 0.8f)
),
imeAction = ImeAction.Done,
onImeActionPerformed = {
MessagesState.info("did IME action")
val n = name.value.trim()
if (n.isNotEmpty())
UIState.setOwner(context, n)
},
hintText = "Type your name here...",
modifier = LayoutGravity.Center
)
}
BTScanScreen()
val bonded = RadioInterfaceService.getBondedDeviceAddress(context) != null
if (!bonded) {
val typography = MaterialTheme.typography
Text(
text =
"""
You haven't yet paired a Meshtastic compatible radio with this phone.
This application is an early alpha release, if you find problems please post on our website chat.
For more information see our web page - www.meshtastic.org.
""".trimIndent(), style = typography.body2
)
}
}
}
@Preview
@Composable
fun previewSettings() {
// another bug? It seems modaldrawerlayout not yet supported in preview
MaterialTheme(colors = palette) {
SettingsContent()
}
}

Wyświetl plik

@ -0,0 +1,296 @@
package com.geeksville.mesh.ui
import android.app.Application
import android.bluetooth.BluetoothDevice
import android.bluetooth.BluetoothManager
import android.bluetooth.le.*
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.Bundle
import android.os.ParcelUuid
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.EditorInfo
import android.widget.RadioButton
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.Observer
import com.geeksville.android.Logging
import com.geeksville.android.hideKeyboard
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.RadioInterfaceService
import com.geeksville.util.exceptionReporter
import kotlinx.android.synthetic.main.settings_fragment.*
class BTScanModel(app: Application) : AndroidViewModel(app), Logging {
private val context = getApplication<Application>().applicationContext
init {
debug("BTScanModel created")
}
data class BTScanEntry(val name: String, val macAddress: String, val bonded: Boolean) {
// val isSelected get() = macAddress == selectedMacAddr
}
override fun onCleared() {
super.onCleared()
debug("BTScanModel cleared")
}
/// Note: may be null on platforms without a bluetooth driver (ie. the emulator)
val bluetoothAdapter =
(context.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager?)?.adapter
var selectedMacAddr: String? = null
val errorText = object : MutableLiveData<String?>(null) {}
private var scanner: BluetoothLeScanner? = null
private val scanCallback = object : ScanCallback() {
override fun onScanFailed(errorCode: Int) {
val msg = "Unexpected bluetooth scan failure: $errorCode"
// error code2 seeems to be indicate hung bluetooth stack
errorText.value = msg
}
// 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) {
val addr = result.device.address
// prevent logspam because weill get get lots of redundant scan results
val isBonded = result.device.bondState == BluetoothDevice.BOND_BONDED
val oldDevs = devices.value!!
val oldEntry = oldDevs[addr]
if (oldEntry == null || oldEntry.bonded != isBonded) {
val entry = BTScanEntry(
result.device.name,
addr,
isBonded
)
debug("onScanResult ${entry}")
// If nothing was selected, by default select the first thing we see
if (selectedMacAddr == null && entry.bonded)
changeSelection(context, addr)
devices.value = oldDevs + Pair(addr, entry) // trigger gui updates
}
}
}
fun stopScan() {
if (scanner != null) {
debug("stopping scan")
try {
scanner?.stopScan(scanCallback)
} catch (ex: Throwable) {
warn("Ignoring error stopping scan, probably BT adapter was disabled suddenly: ${ex.message}")
}
scanner = null
}
}
fun startScan() {
debug("BTScan component active")
selectedMacAddr = RadioInterfaceService.getBondedDeviceAddress(context)
if (bluetoothAdapter == null) {
warn("No bluetooth adapter. Running under emulation?")
val testnodes = listOf(
BTScanEntry("Meshtastic_ab12", "xx", false),
BTScanEntry("Meshtastic_32ac", "xb", true)
)
devices.value = (testnodes.map { it.macAddress to it }).toMap()
// If nothing was selected, by default select the first thing we see
if (selectedMacAddr == null)
changeSelection(context, testnodes.first().macAddress)
} else {
/// The following call might return null if the user doesn't have bluetooth access permissions
val s: BluetoothLeScanner? = bluetoothAdapter.bluetoothLeScanner
if (s == null) {
errorText.value =
"This application requires bluetooth access. Please grant access in android settings."
} else {
debug("starting scan")
// filter and only accept devices that have a sw update service
val filter =
ScanFilter.Builder()
.setServiceUuid(ParcelUuid(RadioInterfaceService.BTM_SERVICE_UUID))
.build()
val settings =
ScanSettings.Builder().setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
s.startScan(listOf(filter), settings, scanCallback)
scanner = s
}
}
}
val devices = object : MutableLiveData<Map<String, BTScanEntry>>(mapOf()) {
/**
* Called when the number of active observers change from 1 to 0.
*
*
* This does not mean that there are no observers left, there may still be observers but their
* lifecycle states aren't [Lifecycle.State.STARTED] or [Lifecycle.State.RESUMED]
* (like an Activity in the back stack).
*
*
* You can check if there are observers via [.hasObservers].
*/
override fun onInactive() {
super.onInactive()
stopScan()
}
}
/// Called by the GUI when a new device has been selected by the user
/// Returns true if we were able to change to that item
fun onSelected(it: BTScanEntry): Boolean {
// If the device is paired, let user select it, otherwise start the pairing flow
if (it.bonded) {
changeSelection(context, it.macAddress)
return true
} else {
info("Starting bonding for $it")
// We need this receiver to get informed when the bond attempt finished
val bondChangedReceiver = object : BroadcastReceiver() {
override fun onReceive(
context: Context,
intent: Intent
) = exceptionReporter {
val state =
intent.getIntExtra(
BluetoothDevice.EXTRA_BOND_STATE,
-1
)
debug("Received bond state changed $state")
context.unregisterReceiver(this)
if (state == BluetoothDevice.BOND_BONDED || state == BluetoothDevice.BOND_BONDING) {
debug("Bonding completed, connecting service")
changeSelection(
context,
it.macAddress
)
}
}
}
val filter = IntentFilter()
filter.addAction(BluetoothDevice.ACTION_BOND_STATE_CHANGED)
context.registerReceiver(bondChangedReceiver, filter)
// We ignore missing BT adapters, because it lets us run on the emulator
bluetoothAdapter
?.getRemoteDevice(it.macAddress)
?.createBond()
return false
}
}
/// Change to a new macaddr selection, updating GUI and radio
fun changeSelection(context: Context, newAddr: String) {
info("Changing BT device to $newAddr")
selectedMacAddr = newAddr
RadioInterfaceService.setBondedDeviceAddress(context, newAddr)
}
}
class SettingsFragment : ScreenFragment("Settings"), Logging {
private val scanModel: BTScanModel by activityViewModels()
private val model: UIViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.settings_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
model.ownerName.observe(viewLifecycleOwner, Observer { name ->
usernameEditText.setText(name)
})
usernameEditText.on(EditorInfo.IME_ACTION_DONE) {
debug("did IME action")
val n = usernameEditText.text.toString().trim()
if (n.isNotEmpty())
model.setOwner(requireContext(), n)
requireActivity().hideKeyboard()
}
analyticsOkayCheckbox.setOnCheckedChangeListener { _, isChecked ->
// FIXME, preserve this in settings
analyticsOkayCheckbox.isChecked = true // so users will complain and I'll fix the bug
}
scanModel.errorText.observe(viewLifecycleOwner, Observer { errMsg ->
if (errMsg != null) {
scanStatusText.text = errMsg
}
})
scanModel.devices.observe(viewLifecycleOwner, Observer { devices ->
// Remove the old radio buttons and repopulate
deviceRadioGroup.removeAllViews()
devices.values.forEach { device ->
val b = RadioButton(requireActivity())
b.text = device.name
b.id = View.generateViewId()
b.isEnabled =
true // Now we always want to enable, if the user clicks we'll try to bond device.bonded
b.isSelected = device.macAddress == scanModel.selectedMacAddr
deviceRadioGroup.addView(b)
b.setOnClickListener {
b.isChecked = scanModel.onSelected(device)
}
}
val hasBonded = RadioInterfaceService.getBondedDeviceAddress(requireContext()) != null
// get rid of the warning text once at least one device is paired
warningNotPaired.visibility = if (hasBonded) View.GONE else View.VISIBLE
})
}
override fun onPause() {
super.onPause()
scanModel.stopScan()
}
override fun onResume() {
super.onResume()
scanModel.startScan()
}
}

Wyświetl plik

@ -1,30 +0,0 @@
package com.geeksville.mesh.ui
import androidx.compose.Model
import com.geeksville.mesh.R
data class ScreenInfo(val icon: Int, val label: String)
// defines the screens we have in the app
object Screen {
val settings = ScreenInfo(R.drawable.ic_twotone_settings_applications_24, "Settings")
val channel = ScreenInfo(R.drawable.ic_twotone_contactless_24, "Channel")
val users = ScreenInfo(R.drawable.ic_twotone_people_24, "Users")
val messages = ScreenInfo(R.drawable.ic_twotone_message_24, "Messages")
val map = ScreenInfo(R.drawable.ic_twotone_map_24, "Map")
}
@Model
object AppStatus {
var currentScreen: ScreenInfo = Screen.messages
}
/**
* Temporary solution pending navigation support.
*/
fun navigateTo(destination: ScreenInfo) {
AppStatus.currentScreen = destination
}

Wyświetl plik

@ -1,75 +0,0 @@
package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.compose.state
import androidx.ui.core.Modifier
import androidx.ui.foundation.TextField
import androidx.ui.foundation.shape.corner.RoundedCornerShape
import androidx.ui.graphics.Color
import androidx.ui.input.ImeAction
import androidx.ui.input.KeyboardType
import androidx.ui.input.VisualTransformation
import androidx.ui.layout.LayoutPadding
import androidx.ui.material.Emphasis
import androidx.ui.material.MaterialTheme
import androidx.ui.material.ProvideEmphasis
import androidx.ui.material.Surface
import androidx.ui.text.TextStyle
import androidx.ui.unit.dp
val HintEmphasis = object : Emphasis {
override fun emphasize(color: Color) = color.copy(alpha = 0.05f)
}
/// A text field that visually conveys that it is editable - FIXME, once Compose has material
/// design text fields use that instead.
@Composable
fun StyledTextField(
value: String,
modifier: Modifier = Modifier.None,
onValueChange: (String) -> Unit = {},
textStyle: TextStyle = TextStyle.Default,
keyboardType: KeyboardType = KeyboardType.Text,
imeAction: ImeAction = ImeAction.Unspecified,
onFocus: () -> Unit = {},
onBlur: () -> Unit = {},
focusIdentifier: String? = null,
onImeActionPerformed: (ImeAction) -> Unit = {},
visualTransformation: VisualTransformation? = null,
hintText: String = ""
) {
val backgroundColor = palette.secondary.copy(alpha = 0.12f)
Surface(
modifier = LayoutPadding(8.dp),
color = backgroundColor,
shape = RoundedCornerShape(4.dp)
) {
val showingHint = state { value.isEmpty() }
val level = if (showingHint.value) HintEmphasis else MaterialTheme.emphasisLevels.medium
ProvideEmphasis(level) {
TextField(
value.ifEmpty { if (showingHint.value) hintText else "" },
modifier + LayoutPadding(4.dp),
onValueChange,
textStyle,
keyboardType,
imeAction,
{
showingHint.value = false // Stop showing the hint now
onFocus()
},
{
// if the string is empty again, return to the hint text
showingHint.value = value.isEmpty()
onBlur()
},
focusIdentifier,
onImeActionPerformed,
visualTransformation
)
}
}
}

Wyświetl plik

@ -1,44 +0,0 @@
package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.ui.core.Modifier
import androidx.ui.foundation.Text
import androidx.ui.layout.Column
import androidx.ui.layout.LayoutGravity
import androidx.ui.layout.LayoutWidth
import androidx.ui.material.MaterialTheme
import androidx.ui.tooling.preview.Preview
import androidx.ui.unit.dp
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.model.NodeDB
/**
* Show the user icon for a particular user with distance from the operator and a small pointer
* indicating their direction
*
* This component is fixed width to simplify layouts.
*/
@Composable
fun UserIcon(user: NodeInfo? = null, modifier: Modifier = Modifier.None) {
Column(modifier = modifier + LayoutWidth(60.dp)) {
VectorImage(
id = R.drawable.ic_twotone_person_24,
tint = palette.onSecondary,
modifier = LayoutGravity.Center
)
val ourNodeInfo = NodeDB.ourNodeInfo
val distance = ourNodeInfo?.distanceStr(user)
if (distance != null)
Text(distance, modifier = LayoutGravity.Center)
}
}
@Preview
@Composable
fun previewUserIcon() {
// another bug? It seems modaldrawerlayout not yet supported in preview
MaterialTheme(colors = palette) {
UserIcon(NodeDB.testNodes[1])
}
}

Wyświetl plik

@ -1,92 +0,0 @@
package com.geeksville.mesh.ui
import androidx.compose.Composable
import androidx.ui.core.ContextAmbient
import androidx.ui.foundation.Text
import androidx.ui.layout.Column
import androidx.ui.layout.LayoutPadding
import androidx.ui.layout.Row
import androidx.ui.material.Button
import androidx.ui.unit.dp
import com.geeksville.mesh.R
import com.geeksville.mesh.model.NodeDB
import com.geeksville.mesh.model.UIState
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.service.RadioInterfaceService
import com.geeksville.mesh.service.SoftwareUpdateService
/// Given a human name, strip out the first letter of the first three words and return that as the initials for
/// that user.
fun getInitials(name: String): String {
val words = name.split(Regex("\\s+")).filter { it.isNotEmpty() }.take(3).map { it.first() }
.joinToString("")
return words
}
@Composable
fun UsersContent() {
analyticsScreen(name = "users")
Column {
Row {
fun connected() = UIState.isConnected.value != MeshService.ConnectionState.DISCONNECTED
VectorImage(
id = if (connected()) R.drawable.cloud_on else R.drawable.cloud_off,
tint = palette.onBackground,
modifier = LayoutPadding(start = 8.dp)
)
Column {
Text(
when (UIState.isConnected.value) {
MeshService.ConnectionState.CONNECTED -> "Connected"
MeshService.ConnectionState.DISCONNECTED -> "Disconnected"
MeshService.ConnectionState.DEVICE_SLEEP -> "Power Saving"
},
modifier = LayoutPadding(start = 8.dp)
)
if (false) { // hide the firmware update button for now, it is kinda ugly and users don't need it yet
/// Create a software update button
val context = ContextAmbient.current
RadioInterfaceService.getBondedDeviceAddress(context)?.let { macAddress ->
Button(
onClick = {
SoftwareUpdateService.enqueueWork(
context,
SoftwareUpdateService.startUpdateIntent(macAddress)
)
}
) {
Text(text = "Update firmware")
}
}
}
}
}
NodeDB.nodes.values.forEach {
NodeInfoCard(it)
}
/* FIXME - doens't work yet - probably because I'm not using release keys
// If account is null, then show the signin button, otherwise
val context = ambient(ContextAmbient)
val account = GoogleSignIn.getLastSignedInAccount(context)
if (account != null)
Text("We have an account")
else {
Text("No account yet")
if (context is Activity) {
Button("Google sign-in", onClick = {
val signInIntent: Intent = UIState.googleSignInClient.signInIntent
context.startActivityForResult(signInIntent, MainActivity.RC_SIGN_IN)
})
}
} */
}
}

Wyświetl plik

@ -0,0 +1,182 @@
package com.geeksville.mesh.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.Observer
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.geeksville.android.Logging
import com.geeksville.mesh.NodeInfo
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import kotlinx.android.synthetic.main.adapter_node_layout.view.*
import kotlinx.android.synthetic.main.nodelist_fragment.*
class UsersFragment : ScreenFragment("Users"), Logging {
private val model: UIViewModel by activityViewModels()
// Provide a direct reference to each of the views within a data item
// Used to cache the views within the item layout for fast access
class ViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val nodeNameView = itemView.nodeNameView
val distance_view = itemView.distance_view
}
private val nodesAdapter = object : RecyclerView.Adapter<ViewHolder>() {
/**
* Called when RecyclerView needs a new [ViewHolder] of the given type to represent
* an item.
*
*
* This new ViewHolder should be constructed with a new View that can represent the items
* of the given type. You can either create a new View manually or inflate it from an XML
* layout file.
*
*
* The new ViewHolder will be used to display items of the adapter using
* [.onBindViewHolder]. Since it will be re-used to display
* different items in the data set, it is a good idea to cache references to sub views of
* the View to avoid unnecessary [View.findViewById] calls.
*
* @param parent The ViewGroup into which the new View will be added after it is bound to
* an adapter position.
* @param viewType The view type of the new View.
*
* @return A new ViewHolder that holds a View of the given view type.
* @see .getItemViewType
* @see .onBindViewHolder
*/
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder {
val inflater = LayoutInflater.from(requireContext())
// Inflate the custom layout
// Inflate the custom layout
val contactView: View = inflater.inflate(R.layout.adapter_node_layout, parent, false)
// Return a new holder instance
return ViewHolder(contactView)
}
/**
* Returns the total number of items in the data set held by the adapter.
*
* @return The total number of items in this adapter.
*/
override fun getItemCount(): Int = nodes.size
/**
* Called by RecyclerView to display the data at the specified position. This method should
* update the contents of the [ViewHolder.itemView] to reflect the item at the given
* position.
*
*
* Note that unlike [android.widget.ListView], RecyclerView will not call this method
* again if the position of the item changes in the data set unless the item itself is
* invalidated or the new position cannot be determined. For this reason, you should only
* use the `position` parameter while acquiring the related data item inside
* this method and should not keep a copy of it. If you need the position of an item later
* on (e.g. in a click listener), use [ViewHolder.getAdapterPosition] which will
* have the updated adapter position.
*
* Override [.onBindViewHolder] instead if Adapter can
* handle efficient partial bind.
*
* @param holder The ViewHolder which should be updated to represent the contents of the
* item at the given position in the data set.
* @param position The position of the item within the adapter's data set.
*/
override fun onBindViewHolder(holder: ViewHolder, position: Int) {
val n = nodes[position]
holder.nodeNameView.text = n.user?.longName ?: n.user?.id ?: "Unknown node"
val ourNodeInfo = model.nodeDB.ourNodeInfo
val distance = ourNodeInfo?.distanceStr(n)
if (distance != null) {
holder.distance_view.text = distance
holder.distance_view.visibility = View.VISIBLE
} else {
holder.distance_view.visibility = View.INVISIBLE
}
}
private var nodes = arrayOf<NodeInfo>()
/// Called when our node DB changes
fun onNodesChanged(nodesIn: Collection<NodeInfo>) {
nodes = nodesIn.toTypedArray()
notifyDataSetChanged() // FIXME, this is super expensive and redraws all nodes
}
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
return inflater.inflate(R.layout.nodelist_fragment, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
nodeListView.adapter = nodesAdapter
nodeListView.layoutManager = LinearLayoutManager(requireContext())
model.nodeDB.nodes.observe(viewLifecycleOwner, Observer { it ->
nodesAdapter.onNodesChanged(it.values)
})
}
}
/*
if (false) { // hide the firmware update button for now, it is kinda ugly and users don't need it yet
/// Create a software update button
val context = ContextAmbient.current
RadioInterfaceService.getBondedDeviceAddress(context)?.let { macAddress ->
Button(
onClick = {
SoftwareUpdateService.enqueueWork(
context,
SoftwareUpdateService.startUpdateIntent(macAddress)
)
}
) {
Text(text = "Update firmware")
}
}
}
}
}
/* FIXME - doens't work yet - probably because I'm not using release keys
// If account is null, then show the signin button, otherwise
val context = ambient(ContextAmbient)
val account = GoogleSignIn.getLastSignedInAccount(context)
if (account != null)
Text("We have an account")
else {
Text("No account yet")
if (context is Activity) {
Button("Google sign-in", onClick = {
val signInIntent: Intent = UIState.googleSignInClient.signInIntent
context.startActivityForResult(signInIntent, MainActivity.RC_SIGN_IN)
})
}
} */
}
}
*/

Wyświetl plik

@ -1,49 +0,0 @@
package com.geeksville.mesh.ui
import androidx.annotation.DrawableRes
import androidx.compose.Composable
import androidx.ui.core.Modifier
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.IconButton
import androidx.ui.res.vectorResource
@Composable
fun VectorImageButton(@DrawableRes id: Int, onClick: () -> Unit) {
//Ripple(bounded = false) {
IconButton(onClick = onClick) {
Icon(vectorResource(id) /* , modifier = LayoutSize(40.dp, 40.dp) */)
}
//}
}
/* fun AppBarIcon(icon: Image, onClick: () -> Unit) {
Container(width = ActionIconDiameter, height = ActionIconDiameter) {
Ripple(bounded = false) {
Clickable(onClick = onClick) {
SimpleImage(icon)
}
}
}
} */
@Composable
fun VectorImage(
modifier: Modifier = Modifier.None, @DrawableRes id: Int,
tint: Color = Color.Transparent
) {
val vector = vectorResource(id)
// WithDensity {
Container(
modifier = modifier + LayoutSize(
vector.defaultWidth,
vector.defaultHeight
) + drawVector(vector, tint)
) {
}
// }
}

Wyświetl plik

@ -0,0 +1,15 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24"
android:tint="?attr/colorControlNormal">
<path
android:fillColor="@android:color/white"
android:pathData="M19.21,12.04l-1.53,-0.11 -0.3,-1.5C16.88,7.86 14.62,6 12,6 9.94,6 8.08,7.14 7.12,8.96l-0.5,0.95 -1.07,0.11C3.53,10.24 2,11.95 2,14c0,2.21 1.79,4 4,4h13c1.65,0 3,-1.35 3,-3 0,-1.55 -1.22,-2.86 -2.79,-2.96zM13.45,13v3h-2.91v-3L8,13l4,-4 4,4h-2.55z"
android:strokeAlpha="0.3"
android:fillAlpha="0.3"/>
<path
android:fillColor="@android:color/white"
android:pathData="M19.35,10.04C18.67,6.59 15.64,4 12,4 9.11,4 6.6,5.64 5.35,8.04 2.34,8.36 0,10.91 0,14c0,3.31 2.69,6 6,6h13c2.76,0 5,-2.24 5,-5 0,-2.64 -2.05,-4.78 -4.65,-4.96zM19,18H6c-2.21,0 -4,-1.79 -4,-4 0,-2.05 1.53,-3.76 3.56,-3.97l1.07,-0.11 0.5,-0.95C8.08,7.14 9.94,6 12,6c2.62,0 4.88,1.86 5.39,4.43l0.3,1.5 1.53,0.11c1.56,0.1 2.78,1.41 2.78,2.96 0,1.65 -1.35,3 -3,3zM8,13h2.55v3h2.9v-3H16l-4,-4z"/>
</vector>

Wyświetl plik

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:drawable="@drawable/ic_twotone_lock_24"
android:state_checked="false"
/>
<item
android:drawable="@drawable/ic_twotone_lock_open_24"
android:state_checked="true"
/>
<item android:drawable="@drawable/ic_twotone_lock_24" />
</selector>

Wyświetl plik

@ -0,0 +1,96 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<com.google.android.material.appbar.AppBarLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:theme="@style/AppTheme.AppBarOverlay">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView4"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:contentDescription="@string/application_icon"
android:scaleType="center"
android:scaleX="1.5"
android:scaleY="1.5"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_baseline_settings_input_antenna_24" />
<TextView
android:id="@+id/title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:gravity="center"
android:minHeight="?actionBarSize"
android:padding="16dp"
android:text="@string/app_name"
android:textAppearance="@style/TextAppearance.Widget.AppCompat.Toolbar.Title"
app:layout_constraintStart_toEndOf="@+id/imageView4"
app:layout_constraintTop_toTopOf="parent" />
<ImageView
android:id="@+id/connectStatusImage"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_weight="1"
android:contentDescription="@string/connection_status"
android:tint="#FFFFFF"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintHeight_percent=".5"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/cloud_off" />
</androidx.constraintlayout.widget.ConstraintLayout>
<com.google.android.material.tabs.TabLayout
android:id="@+id/tab_layout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<!-- Screen.messages -> MessagesContent()
Screen.settings -> SettingsContent()
Screen.users -> UsersContent()
Screen.channel -> ChannelContent(UIState.getChannel())
Screen.map -> MapContent()
<com.google.android.material.tabs.TabItem
android:icon="@drawable/ic_twotone_message_24"
android:text="Messages"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
<com.google.android.material.tabs.TabItem
android:icon="@drawable/ic_twotone_settings_applications_24"
android:text="Settings"
android:layout_height="wrap_content"
android:layout_width="wrap_content" />
-->
</com.google.android.material.tabs.TabLayout>
</com.google.android.material.appbar.AppBarLayout>
<androidx.viewpager2.widget.ViewPager2
android:id="@+id/pager"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:layout_behavior="com.google.android.material.appbar.AppBarLayout$ScrollingViewBehavior" />
</androidx.coordinatorlayout.widget.CoordinatorLayout>

Wyświetl plik

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="4dp"
android:clipToPadding="false">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.chip.Chip
android:id="@+id/username"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="@string/some_username"
app:chipIcon="@drawable/ic_twotone_person_24"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/messageText"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@string/sample_message"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@+id/username"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>

Wyświetl plik

@ -0,0 +1,54 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:clipToPadding="false">
<com.google.android.material.card.MaterialCardView
style="@style/Widget.App.CardView"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/imageView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:contentDescription="@string/user_avatar"
android:scaleType="center"
android:scaleX="1.5"
android:scaleY="1.5"
app:layout_constraintEnd_toEndOf="@+id/distance_view"
app:layout_constraintStart_toStartOf="@+id/distance_view"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_twotone_person_24" />
<TextView
android:id="@+id/nodeNameView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:text="@string/unknown_username"
app:layout_constraintStart_toEndOf="@+id/distance_view"
app:layout_constraintTop_toTopOf="@+id/imageView" />
<TextView
android:id="@+id/distance_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:text="@string/sample_distance"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/imageView" />
</androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.card.MaterialCardView>
</LinearLayout>

Wyświetl plik

@ -0,0 +1,113 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/channelNameView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="64dp"
android:hint="@string/channel_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/channelNameEdit"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/channel_name"
android:text="@string/unset" />
</com.google.android.material.textfield.TextInputLayout>
<ImageView
android:id="@+id/qrView"
android:layout_width="0dp"
android:layout_height="200dp"
android:layout_marginStart="96dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="96dp"
android:contentDescription="@string/qr_code"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/channelNameView"
app:srcCompat="@drawable/qrcode" />
<!--
geeksville: no longer used but keeping as a good example of a button group. instead I use
a toggleable icon.
<com.google.android.material.button.MaterialButtonToggleGroup
android:id="@+id/editGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="96dp"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:singleSelection="true"
app:selectionRequired="true"
app:layout_constraintTop_toBottomOf="@+id/channelOptions">
<Button
android:id="@+id/locked"
style="@style/Widget.App.Button.OutlinedButton.IconOnly"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
app:icon="@drawable/ic_twotone_lock_24" />
<Button
android:id="@+id/unlocked"
style="@style/Widget.App.Button.OutlinedButton.IconOnly"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_weight="1"
app:icon="@drawable/ic_twotone_lock_open_24" />
</com.google.android.material.button.MaterialButtonToggleGroup>
-->
<CheckBox
android:id="@+id/editableCheckbox"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="96dp"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
android:button="@drawable/sl_lock_24dp"
app:layout_constraintTop_toBottomOf="@+id/channelOptions"></CheckBox>
<ImageButton
android:id="@+id/shareButton"
style="@android:style/Widget.Material.ImageButton"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="96dp"
android:contentDescription="@string/share_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/editableCheckbox"
app:srcCompat="@drawable/ic_twotone_share_24" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/channelOptions"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="64dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="64dp"
android:hint="@string/channel_options"
style="@style/Widget.MaterialComponents.TextInputLayout.OutlinedBox.ExposedDropdownMenu"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/qrView">
<AutoCompleteTextView
android:id="@+id/filled_exposed_dropdown"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/channel_options" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<TextView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:padding="16dp"
android:ellipsize="end"
android:maxLines="1"
android:textAppearance="?attr/textAppearanceSubtitle1" />

Wyświetl plik

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/constraintLayout"
android:layout_width="match_parent"
android:layout_height="match_parent">
</FrameLayout>

Wyświetl plik

@ -1,10 +1,17 @@
<?xml version="1.0" encoding="utf-8"?>
<com.mapbox.mapboxsdk.maps.MapView xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:mapbox="http://schemas.android.com/apk/res-auto"
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
mapbox:mapbox_uiZoomGestures="true"
mapbox:mapbox_uiScrollGestures="true"></com.mapbox.mapboxsdk.maps.MapView>
android:id="@+id/mapFrame"> <!-- tab layout requires a unique ID -->
<com.mapbox.mapboxsdk.maps.MapView xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/mapView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clickable="true"
mapbox:mapbox_uiZoomGestures="true"
mapbox:mapbox_uiScrollGestures="true"></com.mapbox.mapboxsdk.maps.MapView>
</FrameLayout>

Wyświetl plik

@ -0,0 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/messageListView"
android:layout_width="0dp"
android:layout_height="0dp"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toTopOf="@+id/textInputLayout"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
android:hint="@string/send_text"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/messageInputText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:imeOptions="actionDone"
android:singleLine="true"
android:text="" />
</com.google.android.material.textfield.TextInputLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/nodeListView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -0,0 +1,98 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.textview.MaterialTextView
android:id="@+id/warningNotPaired"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:ems="10"
android:gravity="start|top"
android:text="@string/warning_not_paired"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deviceRadioGroup" />
<com.google.android.material.textfield.TextInputLayout
android:id="@+id/textInputLayout2"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
android:layout_marginEnd="16dp"
android:hint="@string/your_name"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.textfield.TextInputEditText
android:id="@+id/usernameEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:singleLine="true"
android:imeOptions="actionDone" />
</com.google.android.material.textfield.TextInputLayout>
<com.google.android.material.textview.MaterialTextView
android:id="@+id/scanStatusText"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="32dp"
android:text="@string/looking_for_meshtastic_devices"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/textInputLayout2" />
<ProgressBar
android:id="@+id/scanProgressBar"
style="?android:attr/progressBarStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="@+id/deviceRadioGroup" />
<RadioGroup
android:id="@+id/deviceRadioGroup"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginTop="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/scanStatusText">
<RadioButton
android:id="@+id/radioButton2"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/test__devname1" />
<RadioButton
android:id="@+id/radioButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/test_devname2" />
</RadioGroup>
<CheckBox
android:id="@+id/analyticsOkayCheckbox"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
android:checked="true"
android:text="@string/analytics_okay"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/deviceRadioGroup"
app:layout_constraintVertical_bias="1.0" />
</androidx.constraintlayout.widget.ConstraintLayout>

Wyświetl plik

@ -1,4 +1,24 @@
<resources>
<string name="app_name">Meshtastic</string>
<string name="app_name" translatable="false">Meshtastic</string>
<string name="action_settings">Settings</string>
<string name="channel_name">Channel Name</string>
<string name="channel_options">Channel options</string>
<string name="share_button">Share button</string>
<string name="qr_code">QR code</string>
<string name="unset">Unset</string>
<string name="connection_status">Connection status</string>
<string name="application_icon">application icon</string>
<string name="unknown_username">Unknown Username</string>
<string name="user_avatar">User avatar</string>
<string name="sample_distance" translatable="false">2.13 km</string>
<string name="sample_message">hey I found the cache, it is over here next to the big tiger. I\'m kinda scared.</string>
<string name="some_username">Some Username</string>
<string name="send_text">Send Text</string>
<string name="warning_not_paired">You haven\'t yet paired a Meshtastic compatible radio with this phone. Please pair a device and set your username.\n\nThis open-source application is in alpha-testing, if you find problems please post on our website chat.\n\nFor more information see our web page - www.meshtastic.org.</string>
<string name="username_unset">Username unset</string>
<string name="your_name">Your Name</string>
<string name="analytics_okay">Anonymous usage statistics and crash reports.</string>
<string name="looking_for_meshtastic_devices">Looking for Meshtastic devices...</string>
<string name="test__devname1" translatable="false">Meshtastic_ac23</string>
<string name="test_devname2" translatable="false">Meshtastic_1267</string>
</resources>

Wyświetl plik

@ -1,14 +1,15 @@
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.AppCompat.Light.NoActionBar">
<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<style name="AppTheme.NoActionBar">
<style name="AppTheme.NoActionBar" parent="Theme.MaterialComponents.Light.NoActionBar">
<!-- Customize your theme here. -->
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
</style>
@ -17,4 +18,15 @@
<style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" />
<style name="Widget.App.Button.OutlinedButton.IconOnly" parent="Widget.MaterialComponents.Button.OutlinedButton">
<item name="iconPadding">0dp</item>
<item name="android:insetTop">0dp</item>
<item name="android:insetBottom">0dp</item>
<item name="android:paddingLeft">12dp</item>
<item name="android:paddingRight">12dp</item>
<item name="android:minWidth">48dp</item>
<item name="android:minHeight">48dp</item>
</style>
<style name="Widget.App.CardView" parent="Widget.MaterialComponents.CardView"></style>
</resources>

Wyświetl plik

@ -1,8 +1,7 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {
ext.kotlin_version = '1.3.61'
ext.compose_version = '0.1.0-dev08'
ext.kotlin_version = '1.3.71'
ext.coroutines_version = "1.3.5"
repositories {
@ -11,7 +10,7 @@ buildscript {
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:4.1.0-alpha04'
classpath 'com.android.tools.build:gradle:4.0.0-beta04'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files
@ -23,7 +22,7 @@ buildscript {
classpath 'com.google.firebase:firebase-crashlytics-gradle:2.0.0-beta03'
// protobuf plugin - docs here https://github.com/google/protobuf-gradle-plugin
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.11'
classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.12'
}
}

@ -1 +1 @@
Subproject commit 65f39f90ce365263620d5f9cbddca0c8abebcf9a
Subproject commit ebc40c05fd8c30aabb3070468627e5fe6ae59cd5