refactor: migrate to Compose navigation

pull/1519/head
andrekir 2025-01-02 08:31:45 -03:00
rodzic d14a8de78e
commit 04d94a6eb1
20 zmienionych plików z 479 dodań i 1066 usunięć

Wyświetl plik

@ -151,10 +151,10 @@ dependencies {
implementation 'androidx.core:core-ktx:1.15.0' implementation 'androidx.core:core-ktx:1.15.0'
implementation 'androidx.core:core-location-altitude:1.0.0-alpha03' implementation 'androidx.core:core-location-altitude:1.0.0-alpha03'
implementation 'androidx.fragment:fragment-ktx:1.8.5' implementation 'androidx.fragment:fragment-ktx:1.8.5'
implementation 'androidx.fragment:fragment-compose:1.8.5'
implementation 'androidx.cardview:cardview:1.0.0' implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.2.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
implementation 'com.google.android.material:material:1.12.0' implementation 'com.google.android.material:material:1.12.0'
implementation 'androidx.viewpager2:viewpager2:1.1.0'
implementation 'androidx.datastore:datastore:1.1.1' implementation 'androidx.datastore:datastore:1.1.1'
// Lifecycle // Lifecycle
@ -197,7 +197,6 @@ dependencies {
implementation 'androidx.compose.material:material-icons-extended' implementation 'androidx.compose.material:material-icons-extended'
implementation 'androidx.activity:activity-compose' implementation 'androidx.activity:activity-compose'
implementation 'androidx.compose.runtime:runtime-livedata' implementation 'androidx.compose.runtime:runtime-livedata'
implementation "com.google.accompanist:accompanist-themeadapter-appcompat:0.36.0"
// Android Studio Preview support // Android Studio Preview support
implementation 'androidx.compose.ui:ui-tooling-preview' implementation 'androidx.compose.ui:ui-tooling-preview'

Wyświetl plik

@ -26,29 +26,17 @@ import android.content.pm.PackageManager
import android.hardware.usb.UsbManager import android.hardware.usb.UsbManager
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.RemoteException import android.os.RemoteException
import android.text.method.LinkMovementMethod import android.text.method.LinkMovementMethod
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent import android.view.MotionEvent
import android.widget.TextView import android.widget.TextView
import android.widget.Toast import android.widget.Toast
import androidx.activity.compose.setContent
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.activity.viewModels import androidx.activity.viewModels
import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.app.AppCompatDelegate import androidx.appcompat.app.AppCompatDelegate
import androidx.appcompat.widget.Toolbar
import androidx.compose.runtime.getValue
import androidx.core.content.ContextCompat
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.asLiveData
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.geeksville.mesh.android.BindFailedException import com.geeksville.mesh.android.BindFailedException
import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
@ -61,7 +49,6 @@ import com.geeksville.mesh.android.permissionMissing
import com.geeksville.mesh.android.rationaleDialog import com.geeksville.mesh.android.rationaleDialog
import com.geeksville.mesh.android.shouldShowRequestPermissionRationale import com.geeksville.mesh.android.shouldShowRequestPermissionRationale
import com.geeksville.mesh.concurrent.handledLaunch import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.BluetoothViewModel import com.geeksville.mesh.model.BluetoothViewModel
import com.geeksville.mesh.model.DeviceVersion import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
@ -69,88 +56,23 @@ import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.service.MeshServiceNotifications import com.geeksville.mesh.service.MeshServiceNotifications
import com.geeksville.mesh.service.ServiceRepository import com.geeksville.mesh.service.ServiceRepository
import com.geeksville.mesh.service.startService import com.geeksville.mesh.service.startService
import com.geeksville.mesh.ui.ChannelFragment import com.geeksville.mesh.ui.MainMenuAction
import com.geeksville.mesh.ui.ContactsFragment import com.geeksville.mesh.ui.MainScreen
import com.geeksville.mesh.ui.DebugFragment
import com.geeksville.mesh.ui.QuickChatSettingsFragment
import com.geeksville.mesh.ui.SettingsFragment
import com.geeksville.mesh.ui.UsersFragment
import com.geeksville.mesh.ui.components.ScannedQrCodeDialog
import com.geeksville.mesh.ui.map.MapFragment
import com.geeksville.mesh.ui.message.navigateToMessages
import com.geeksville.mesh.ui.navigateToNavGraph
import com.geeksville.mesh.ui.navigateToShareMessage
import com.geeksville.mesh.ui.theme.AppTheme import com.geeksville.mesh.ui.theme.AppTheme
import com.geeksville.mesh.util.Exceptions import com.geeksville.mesh.util.Exceptions
import com.geeksville.mesh.util.LanguageUtils import com.geeksville.mesh.util.LanguageUtils
import com.geeksville.mesh.util.getPackageInfoCompat import com.geeksville.mesh.util.getPackageInfoCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import java.text.DateFormat
import java.util.Date
import javax.inject.Inject import javax.inject.Inject
/*
UI design
material setup instructions: https://material.io/develop/android/docs/getting-started/
dark theme (or use system eventually) https://material.io/develop/android/theming/dark/
NavDrawer is a standard draw which can be dragged in from the left or the menu icon inside the app
title.
Fragments:
SettingsFragment shows "Settings"
username
shortname
bluetooth pairing list
(eventually misc device settings that are not channel related)
Channel fragment "Channel"
qr code, copy link button
ch number
misc other settings
(eventually a way of choosing between past channels)
ChatFragment "Messages"
a text box to enter new texts
a scrolling list of rows. each row is a text and a sender info layout
NodeListFragment "Users"
a node info row for every node
ViewModels:
BTScanModel starts/stops bt scan and provides list of devices (manages entire scan lifecycle)
MeshModel contains: (manages entire service relationship)
current received texts
current radio macaddr
current node infos (updated dynamically)
eventually use bottom navigation bar to switch between, Members, Chat, Channel, Settings. https://material.io/develop/android/components/bottom-navigation-view/
use numbers of # chat messages and # of members in the badges.
(per this recommendation to not use top tabs: https://ux.stackexchange.com/questions/102439/android-ux-when-to-use-bottom-navigation-and-when-to-use-tabs )
eventually:
make a custom theme: https://github.com/material-components/material-components-android/tree/master/material-theme-builder
*/
@AndroidEntryPoint @AndroidEntryPoint
class MainActivity : AppCompatActivity(), Logging { class MainActivity : AppCompatActivity(), Logging {
private lateinit var binding: ActivityMainBinding
// Used to schedule a coroutine in the GUI thread // Used to schedule a coroutine in the GUI thread
private val mainScope = CoroutineScope(Dispatchers.Main + Job()) private val mainScope = CoroutineScope(Dispatchers.Main + Job())
@ -166,7 +88,7 @@ class MainActivity : AppCompatActivity(), Logging {
info("Bluetooth permissions granted") info("Bluetooth permissions granted")
} else { } else {
warn("Bluetooth permissions denied") warn("Bluetooth permissions denied")
showSnackbar(permissionMissing) model.showSnackbar(permissionMissing)
} }
requestedEnable = false requestedEnable = false
bluetoothViewModel.permissionsUpdated() bluetoothViewModel.permissionsUpdated()
@ -178,45 +100,10 @@ class MainActivity : AppCompatActivity(), Logging {
info("Notification permissions granted") info("Notification permissions granted")
} else { } else {
warn("Notification permissions denied") warn("Notification permissions denied")
showSnackbar(getString(R.string.notification_denied), Snackbar.LENGTH_SHORT) model.showSnackbar(getString(R.string.notification_denied))
} }
} }
data class TabInfo(val text: String, val icon: Int, val content: Fragment)
private val tabInfos = arrayOf(
TabInfo(
"Messages",
R.drawable.ic_twotone_message_24,
ContactsFragment()
),
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(supportFragmentManager, lifecycle) {
override fun getItemCount(): Int = tabInfos.size
override fun createFragment(position: Int): Fragment = tabInfos[position].content
}
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
@ -239,45 +126,9 @@ class MainActivity : AppCompatActivity(), Logging {
(application as GeeksvilleApplication).askToRate(this) (application as GeeksvilleApplication).askToRate(this)
} }
binding = ActivityMainBinding.inflate(layoutInflater) setContent {
setContentView(binding.root)
initToolbar()
binding.pager.adapter = tabsAdapter
binding.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(binding.tabLayout, binding.pager, false, false) { tab, position ->
// tab.text = tabInfos[position].text // I think it looks better with icons only
tab.icon = ContextCompat.getDrawable(this, tabInfos[position].icon)
}.attach()
binding.tabLayout.addOnTabSelectedListener(object : TabLayout.OnTabSelectedListener {
override fun onTabSelected(tab: TabLayout.Tab?) {
val mainTab = tab?.position ?: 0
model.setCurrentTab(mainTab)
}
override fun onTabUnselected(tab: TabLayout.Tab?) { }
override fun onTabReselected(tab: TabLayout.Tab?) { }
})
binding.composeView.setContent {
val connState by model.connectionState.collectAsStateWithLifecycle()
val channels by model.channels.collectAsStateWithLifecycle()
val requestChannelSet by model.requestChannelSet.collectAsStateWithLifecycle()
AppTheme { AppTheme {
if (connState.isConnected()) { MainScreen(model, ::onMainMenuAction)
if (requestChannelSet != null) {
ScannedQrCodeDialog(
channels = channels,
incoming = requestChannelSet!!,
onDismiss = model::clearRequestChannelUrl,
onConfirm = model::setChannels,
)
}
}
} }
} }
@ -285,28 +136,6 @@ class MainActivity : AppCompatActivity(), Logging {
handleIntent(intent) handleIntent(intent)
} }
private fun initToolbar() {
val toolbar = binding.toolbar as Toolbar
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
}
private fun updateConnectionStatusImage(connected: MeshService.ConnectionState) {
if (model.actionBarMenu == null) return
val (image, tooltip) = when (connected) {
MeshService.ConnectionState.CONNECTED -> R.drawable.cloud_on to R.string.connected
MeshService.ConnectionState.DEVICE_SLEEP -> R.drawable.ic_twotone_cloud_upload_24 to R.string.device_sleeping
MeshService.ConnectionState.DISCONNECTED -> R.drawable.cloud_off to R.string.disconnected
}
val item = model.actionBarMenu?.findItem(R.id.connectStatusImage)
if (item != null) {
item.setIcon(image)
item.setTitle(tooltip)
}
}
override fun onNewIntent(intent: Intent) { override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent) super.onNewIntent(intent)
handleIntent(intent) handleIntent(intent)
@ -328,7 +157,9 @@ class MainActivity : AppCompatActivity(), Logging {
MeshServiceNotifications.OPEN_MESSAGE_ACTION -> { MeshServiceNotifications.OPEN_MESSAGE_ACTION -> {
val contactKey = val contactKey =
intent.getStringExtra(MeshServiceNotifications.OPEN_MESSAGE_EXTRA_CONTACT_KEY) intent.getStringExtra(MeshServiceNotifications.OPEN_MESSAGE_EXTRA_CONTACT_KEY)
showMessages(contactKey) if (contactKey != null) {
// showMessages(contactKey)
}
} }
UsbManager.ACTION_USB_DEVICE_ATTACHED -> { UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
@ -341,7 +172,7 @@ class MainActivity : AppCompatActivity(), Logging {
Intent.ACTION_SEND -> { Intent.ACTION_SEND -> {
val text = intent.getStringExtra(Intent.EXTRA_TEXT) val text = intent.getStringExtra(Intent.EXTRA_TEXT)
if (text != null) { if (text != null) {
shareMessages(text) // shareMessages(text)
} }
} }
@ -440,27 +271,6 @@ class MainActivity : AppCompatActivity(), Logging {
} }
} }
private fun showSnackbar(msgId: Int) {
try {
Snackbar.make(binding.root, msgId, Snackbar.LENGTH_LONG).show()
} catch (ex: IllegalStateException) {
errormsg("Snackbar couldn't find view for msgId $msgId")
}
}
private fun showSnackbar(msg: String, duration: Int = Snackbar.LENGTH_INDEFINITE) {
try {
Snackbar.make(binding.root, msg, duration)
.apply { view.findViewById<TextView>(R.id.snackbar_text).isSingleLine = false }
.setAction(R.string.okay) {
// dismiss
}
.show()
} catch (ex: IllegalStateException) {
errormsg("Snackbar couldn't find view for msgString $msg")
}
}
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean { override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return try { return try {
super.dispatchTouchEvent(ev) super.dispatchTouchEvent(ev)
@ -475,10 +285,7 @@ class MainActivity : AppCompatActivity(), Logging {
private var connectionJob: Job? = null private var connectionJob: Job? = null
private val mesh = object : private val mesh = object : ServiceClient<IMeshService>(IMeshService.Stub::asInterface) {
ServiceClient<IMeshService>({
IMeshService.Stub.asInterface(it)
}) {
override fun onConnected(service: IMeshService) { override fun onConnected(service: IMeshService) {
connectionJob = mainScope.handledLaunch { connectionJob = mainScope.handledLaunch {
serviceRepository.setMeshService(service) serviceRepository.setMeshService(service)
@ -551,11 +358,6 @@ class MainActivity : AppCompatActivity(), Logging {
override fun onStart() { override fun onStart() {
super.onStart() super.onStart()
model.connectionState.asLiveData().observe(this) { state ->
onMeshConnectionChanged(state)
updateConnectionStatusImage(state)
}
bluetoothViewModel.enabled.observe(this) { enabled -> bluetoothViewModel.enabled.observe(this) { enabled ->
if (!enabled && !requestedEnable && model.selectedBluetooth) { if (!enabled && !requestedEnable && model.selectedBluetooth) {
requestedEnable = true requestedEnable = true
@ -571,17 +373,6 @@ class MainActivity : AppCompatActivity(), Logging {
} }
} }
// Call showSnackbar() whenever [snackbarText] updates with a non-null value
model.snackbarText.observe(this) { text ->
if (text is Int) showSnackbar(text)
if (text is String) showSnackbar(text)
if (text != null) model.clearSnackbarText()
}
model.currentTab.observe(this) {
binding.tabLayout.getTabAt(it)?.select()
}
model.tracerouteResponse.observe(this) { response -> model.tracerouteResponse.observe(this) { response ->
MaterialAlertDialogBuilder(this) MaterialAlertDialogBuilder(this)
.setCancelable(false) .setCancelable(false)
@ -605,118 +396,32 @@ class MainActivity : AppCompatActivity(), Logging {
} }
private fun showSettingsPage() { private fun showSettingsPage() {
binding.pager.currentItem = 5 // binding.pager.currentItem = 5
} }
private fun showMessages(contactKey: String?) { private fun onMainMenuAction(action: MainMenuAction) {
model.setCurrentTab(0) when (action) {
if (contactKey != null) { MainMenuAction.ABOUT -> {
supportFragmentManager.navigateToMessages(contactKey)
}
}
private fun shareMessages(message: String?) {
model.setCurrentTab(0)
if (message != null) {
supportFragmentManager.navigateToShareMessage(message)
}
}
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
model.actionBarMenu = menu
updateConnectionStatusImage(model.connectionState.value)
return true
}
private val handler: Handler by lazy {
Handler(Looper.getMainLooper())
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
menu.findItem(R.id.stress_test).isVisible =
BuildConfig.DEBUG // only show stress test for debug builds (for now)
menu.findItem(R.id.radio_config).isEnabled = !model.isManaged
return super.onPrepareOptionsMenu(menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
R.id.about -> {
getVersionInfo() getVersionInfo()
return true
} }
R.id.connectStatusImage -> { MainMenuAction.EXPORT_MESSAGES -> {
Toast.makeText(applicationContext, item.title, Toast.LENGTH_SHORT).show()
return true
}
R.id.debug -> {
val fragmentManager: FragmentManager = supportFragmentManager
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
val nameFragment = DebugFragment()
fragmentTransaction.add(R.id.mainActivityLayout, nameFragment)
fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
return true
}
R.id.stress_test -> {
fun postPing() {
// Send ping message and arrange delayed recursion.
debug("Sending ping")
val str = "Ping " + DateFormat.getTimeInstance(DateFormat.MEDIUM)
.format(Date(System.currentTimeMillis()))
model.sendMessage(str)
handler.postDelayed({ postPing() }, 30000)
}
item.isChecked = !item.isChecked // toggle ping test
if (item.isChecked) {
postPing()
} else {
handler.removeCallbacksAndMessages(null)
}
return true
}
R.id.radio_config -> {
supportFragmentManager.navigateToNavGraph()
return true
}
R.id.save_messages_csv -> {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
type = "application/csv" type = "application/csv"
putExtra(Intent.EXTRA_TITLE, "rangetest.csv") putExtra(Intent.EXTRA_TITLE, "rangetest.csv")
} }
createDocumentLauncher.launch(intent) createDocumentLauncher.launch(intent)
return true
} }
R.id.theme -> { MainMenuAction.THEME -> {
chooseThemeDialog() chooseThemeDialog()
return true
} }
R.id.preferences_language -> { MainMenuAction.LANGUAGE -> {
chooseLangDialog() chooseLangDialog()
return true
} }
R.id.show_intro -> { MainMenuAction.SHOW_INTRO -> {
startActivity(Intent(this, AppIntroduction::class.java)) startActivity(Intent(this, AppIntroduction::class.java))
return true
} }
R.id.preferences_quick_chat -> { else -> {}
val fragmentManager: FragmentManager = supportFragmentManager
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
val nameFragment = QuickChatSettingsFragment()
fragmentTransaction.add(R.id.mainActivityLayout, nameFragment)
fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
return true
}
else -> super.onOptionsItemSelected(item)
} }
} }

Wyświetl plik

@ -22,7 +22,7 @@ import android.content.Context
import android.content.SharedPreferences import android.content.SharedPreferences
import android.net.Uri import android.net.Uri
import android.os.RemoteException import android.os.RemoteException
import android.view.Menu import androidx.compose.material.SnackbarHostState
import androidx.core.content.edit import androidx.core.content.edit
import androidx.lifecycle.LiveData import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData import androidx.lifecycle.MutableLiveData
@ -171,8 +171,6 @@ class UIViewModel @Inject constructor(
private val quickChatActionRepository: QuickChatActionRepository, private val quickChatActionRepository: QuickChatActionRepository,
private val preferences: SharedPreferences private val preferences: SharedPreferences
) : ViewModel(), Logging { ) : ViewModel(), Logging {
var actionBarMenu: Menu? = null
val meshService: IMeshService? get() = radioConfigRepository.meshService val meshService: IMeshService? get() = radioConfigRepository.meshService
val bondedAddress get() = radioInterfaceService.getBondedDeviceAddress() val bondedAddress get() = radioInterfaceService.getBondedDeviceAddress()
@ -255,12 +253,15 @@ class UIViewModel @Inject constructor(
fun getNode(userId: String?) = nodeDB.getNode(userId ?: DataPacket.ID_BROADCAST) fun getNode(userId: String?) = nodeDB.getNode(userId ?: DataPacket.ID_BROADCAST)
fun getUser(userId: String?) = nodeDB.getUser(userId ?: DataPacket.ID_BROADCAST) fun getUser(userId: String?) = nodeDB.getUser(userId ?: DataPacket.ID_BROADCAST)
private val _snackbarText = MutableLiveData<Any?>(null) val snackbarState = SnackbarHostState()
val snackbarText: LiveData<Any?> get() = _snackbarText fun showSnackbar(text: Int) = showSnackbar(app.getString(text))
fun showSnackbar(text: String) = viewModelScope.launch {
snackbarState.showSnackbar(text)
}
init { init {
radioConfigRepository.errorMessage.filterNotNull().onEach { radioConfigRepository.errorMessage.filterNotNull().onEach {
_snackbarText.value = it showSnackbar(it)
radioConfigRepository.clearErrorMessage() radioConfigRepository.clearErrorMessage()
}.launchIn(viewModelScope) }.launchIn(viewModelScope)
@ -460,17 +461,6 @@ class UIViewModel @Inject constructor(
_requestChannelSet.value = null _requestChannelSet.value = null
} }
fun showSnackbar(resString: Any) {
_snackbarText.value = resString
}
/**
* Called immediately after activity observes [snackbarText]
*/
fun clearSnackbarText() {
_snackbarText.value = null
}
var txEnabled: Boolean var txEnabled: Boolean
get() = config.lora.txEnabled get() = config.lora.txEnabled
set(value) { set(value) {
@ -694,13 +684,6 @@ class UIViewModel @Inject constructor(
radioConfigRepository.clearTracerouteResponse() radioConfigRepository.clearTracerouteResponse()
} }
private val _currentTab = MutableLiveData(0)
val currentTab: LiveData<Int> get() = _currentTab
fun setCurrentTab(tab: Int) {
_currentTab.value = tab
}
fun setNodeFilterText(text: String) { fun setNodeFilterText(text: String) {
nodeFilterText.value = text nodeFilterText.value = text
} }

Wyświetl plik

@ -18,11 +18,7 @@
package com.geeksville.mesh.ui package com.geeksville.mesh.ui
import android.net.Uri import android.net.Uri
import android.os.Bundle
import android.os.RemoteException import android.os.RemoteException
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
@ -63,12 +59,10 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.text.AnnotatedString
@ -76,7 +70,6 @@ import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.activityViewModels
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos.ChannelSet import com.geeksville.mesh.AppOnlyProtos.ChannelSet
@ -87,7 +80,6 @@ import com.geeksville.mesh.analytics.DataPair
import com.geeksville.mesh.android.BuildUtils.debug import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.BuildUtils.errormsg import com.geeksville.mesh.android.BuildUtils.errormsg
import com.geeksville.mesh.android.GeeksvilleApplication import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.getCameraPermissions import com.geeksville.mesh.android.getCameraPermissions
import com.geeksville.mesh.android.hasCameraPermission import com.geeksville.mesh.android.hasCameraPermission
import com.geeksville.mesh.channelSet import com.geeksville.mesh.channelSet
@ -109,32 +101,9 @@ import com.geeksville.mesh.ui.components.config.EditChannelDialog
import com.geeksville.mesh.ui.components.dragContainer import com.geeksville.mesh.ui.components.dragContainer
import com.geeksville.mesh.ui.components.dragDropItemsIndexed import com.geeksville.mesh.ui.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.components.rememberDragDropState import com.geeksville.mesh.ui.components.rememberDragDropState
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class ChannelFragment : ScreenFragment("Channel"), Logging {
private val model: UIViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
AppCompatTheme {
ChannelScreen(model)
}
}
}
}
}
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable @Composable
@ -291,8 +260,11 @@ fun ChannelScreen(
modemPresetName = modemPresetName, modemPresetName = modemPresetName,
onAddClick = { onAddClick = {
with(channelSet) { with(channelSet) {
if (settingsCount > index) channelSet = copy { settings[index] = it } if (settingsCount > index) {
else channelSet = copy { settings.add(it) } channelSet = copy { settings[index] = it }
} else {
channelSet = copy { settings.add(it) }
}
} }
showEditChannelDialog = null showEditChannelDialog = null
}, },

Wyświetl plik

@ -46,7 +46,6 @@ import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.model.Contact import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.message.navigateToMessages
import com.geeksville.mesh.ui.theme.AppTheme import com.geeksville.mesh.ui.theme.AppTheme
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
@ -71,7 +70,7 @@ class ContactsFragment : ScreenFragment("Messages"), Logging {
onLongClick(contact) onLongClick(contact)
} else { } else {
debug("calling MessagesFragment filter:${contact.contactKey}") debug("calling MessagesFragment filter:${contact.contactKey}")
parentFragmentManager.navigateToMessages(contact.contactKey) // parentFragmentManager.navigateToMessages(contact.contactKey)
} }
} }

Wyświetl plik

@ -17,13 +17,8 @@
package com.geeksville.mesh.ui package com.geeksville.mesh.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -36,13 +31,9 @@ import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.Button import androidx.compose.material.Button
import androidx.compose.material.Card import androidx.compose.material.Card
import androidx.compose.material.Icon import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Surface import androidx.compose.material.Surface
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.outlined.CloudDownload import androidx.compose.material.icons.outlined.CloudDownload
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -51,8 +42,6 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.SpanStyle
@ -64,63 +53,15 @@ import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp import androidx.compose.ui.unit.sp
import androidx.core.content.ContextCompat
import androidx.fragment.app.Fragment
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.database.entity.MeshLog import com.geeksville.mesh.database.entity.MeshLog
import com.geeksville.mesh.model.DebugViewModel import com.geeksville.mesh.model.DebugViewModel
import com.geeksville.mesh.ui.theme.AppTheme import com.geeksville.mesh.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint
import java.text.DateFormat import java.text.DateFormat
import java.util.Locale import java.util.Locale
@AndroidEntryPoint
class DebugFragment : Fragment() {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
setContent {
val viewModel: DebugViewModel = hiltViewModel()
AppTheme {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = R.string.debug_panel)) },
navigationIcon = {
IconButton(onClick = { parentFragmentManager.popBackStack() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
stringResource(id = R.string.navigate_back),
)
}
},
actions = {
Button(onClick = viewModel::deleteAllLogs) {
Text(text = stringResource(R.string.clear))
}
}
)
},
) { innerPadding ->
DebugScreen(
viewModel = viewModel,
contentPadding = innerPadding,
)
}
}
}
}
}
}
private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE) private val REGEX_ANNOTATED_NODE_ID = Regex("\\(![0-9a-fA-F]{8}\\)$", RegexOption.MULTILINE)
/** /**
@ -185,7 +126,6 @@ private fun Int.asNodeId(): String {
@Composable @Composable
internal fun DebugScreen( internal fun DebugScreen(
viewModel: DebugViewModel = hiltViewModel(), viewModel: DebugViewModel = hiltViewModel(),
contentPadding: PaddingValues,
) { ) {
val listState = rememberLazyListState() val listState = rememberLazyListState()
val logs by viewModel.meshLog.collectAsStateWithLifecycle() val logs by viewModel.meshLog.collectAsStateWithLifecycle()
@ -203,7 +143,6 @@ internal fun DebugScreen(
LazyColumn( LazyColumn(
modifier = Modifier.fillMaxSize(), modifier = Modifier.fillMaxSize(),
state = listState, state = listState,
contentPadding = contentPadding,
) { ) {
items(logs, key = { it.uuid }) { log -> DebugItem(annotateMeshLog(log)) } items(logs, key = { it.uuid }) { log -> DebugItem(annotateMeshLog(log)) }
} }
@ -300,3 +239,16 @@ private fun DebugScreenPreview() {
) )
} }
} }
@Composable
fun DebugMenuActions(
viewModel: DebugViewModel = hiltViewModel(),
modifier: Modifier = Modifier,
) {
Button(
onClick = viewModel::deleteAllLogs,
modifier = modifier,
) {
Text(text = stringResource(R.string.clear))
}
}

Wyświetl plik

@ -0,0 +1,327 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui
import android.widget.Toast
import androidx.annotation.StringRes
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.core.tween
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material.BottomNavigation
import androidx.compose.material.BottomNavigationItem
import androidx.compose.material.DropdownMenu
import androidx.compose.material.DropdownMenuItem
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Scaffold
import androidx.compose.material.SnackbarHost
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.twotone.Chat
import androidx.compose.material.icons.twotone.CloudDone
import androidx.compose.material.icons.twotone.CloudOff
import androidx.compose.material.icons.twotone.CloudUpload
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.twotone.Contactless
import androidx.compose.material.icons.twotone.Map
import androidx.compose.material.icons.twotone.People
import androidx.compose.material.icons.twotone.Settings
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavController
import androidx.navigation.NavDestination
import androidx.navigation.NavDestination.Companion.hasRoute
import androidx.navigation.NavDestination.Companion.hierarchy
import androidx.navigation.NavGraph.Companion.findStartDestination
import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController
import com.geeksville.mesh.R
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.TopLevelDestination.Companion.isTopLevel
import com.geeksville.mesh.ui.components.ScannedQrCodeDialog
enum class TopLevelDestination(val label: String, val icon: ImageVector, val route: Route) {
Contacts("Contacts", Icons.AutoMirrored.TwoTone.Chat, Route.Contacts),
Nodes("Nodes", Icons.TwoTone.People, Route.Nodes),
Map("Map", Icons.TwoTone.Map, Route.Map),
Channels("Channels", Icons.TwoTone.Contactless, Route.Channels),
Settings("Settings", Icons.TwoTone.Settings, Route.Settings),
;
companion object {
fun NavDestination.isTopLevel(): Boolean = entries.any { hasRoute(it.route::class) }
fun fromNavDestination(destination: NavDestination?): TopLevelDestination? = entries
.find { dest -> destination?.hierarchy?.any { it.hasRoute(dest.route::class) } == true }
}
}
@Composable
fun MainScreen(
viewModel: UIViewModel = hiltViewModel(),
onAction: (MainMenuAction) -> Unit
) {
val navController = rememberNavController()
val backStackEntry by navController.currentBackStackEntryAsState()
val currentDestination = backStackEntry?.destination
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
val localConfig by viewModel.localConfig.collectAsStateWithLifecycle()
val requestChannelSet by viewModel.requestChannelSet.collectAsStateWithLifecycle()
if (connectionState.isConnected()) {
requestChannelSet?.let { newChannelSet ->
ScannedQrCodeDialog(viewModel, newChannelSet)
}
}
Scaffold(
topBar = {
MainAppBar(
isManaged = localConfig.security.isManaged,
connectionState = connectionState,
currentDestination = currentDestination,
canNavigateBack = navController.previousBackStackEntry != null,
navigateUp = navController::navigateUp,
) { action ->
when (action) {
MainMenuAction.DEBUG -> navController.navigate(Route.DebugPanel)
MainMenuAction.RADIO_CONFIG -> navController.navigate(Route.RadioConfig())
MainMenuAction.QUICK_CHAT -> navController.navigate(Route.QuickChat)
else -> onAction(action)
}
}
},
bottomBar = {
BottomNavigation(
navController = navController,
currentDestination = currentDestination,
)
},
snackbarHost = { SnackbarHost(hostState = viewModel.snackbarState) }
) { innerPadding ->
NavGraph(
model = viewModel,
navController = navController,
modifier = Modifier.padding(innerPadding),
)
}
}
enum class MainMenuAction(@StringRes val stringRes: Int) {
DEBUG(R.string.debug_panel),
RADIO_CONFIG(R.string.device_settings),
EXPORT_MESSAGES(R.string.save_messages),
THEME(R.string.theme),
LANGUAGE(R.string.preferences_language),
SHOW_INTRO(R.string.intro_show),
QUICK_CHAT(R.string.quick_chat),
ABOUT(R.string.about),
}
@Composable
private fun MainAppBar(
isManaged: Boolean,
connectionState: MeshService.ConnectionState,
currentDestination: NavDestination?,
canNavigateBack: Boolean,
navigateUp: () -> Unit,
modifier: Modifier = Modifier,
onAction: (MainMenuAction) -> Unit
) = AnimatedVisibility(
visible = currentDestination?.hasRoute<Route.Messages>() == false,
enter = slideInVertically(animationSpec = tween(durationMillis = 200)),
exit = slideOutVertically(animationSpec = tween(durationMillis = 200)),
) {
val isTopLevelRoute = currentDestination?.isTopLevel() == true
TopAppBar(
title = {
when {
currentDestination == null || isTopLevelRoute -> {
Row(verticalAlignment = Alignment.CenterVertically) {
Icon(
imageVector = ImageVector.vectorResource(id = R.drawable.app_icon),
contentDescription = stringResource(id = R.string.application_icon),
modifier = Modifier
.size(36.dp)
.padding(end = 8.dp)
)
Text(text = stringResource(id = R.string.app_name))
}
}
currentDestination.hasRoute<Route.DebugPanel>() ->
Text(stringResource(id = R.string.debug_panel))
currentDestination.hasRoute<Route.QuickChat>() ->
Text(stringResource(id = R.string.quick_chat))
else -> Text("Node name here") // TODO show destNode longName
}
},
modifier = modifier,
navigationIcon = if (canNavigateBack && !isTopLevelRoute) {
{
IconButton(onClick = navigateUp) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.navigate_back),
)
}
}
} else {
null
},
actions = {
when {
currentDestination == null || isTopLevelRoute ->
MainMenuActions(isManaged, connectionState, onAction)
currentDestination.hasRoute<Route.DebugPanel>() ->
DebugMenuActions()
else -> {}
}
},
)
}
@Composable
private fun MainMenuActions(
isManaged: Boolean,
connectionState: MeshService.ConnectionState,
onAction: (MainMenuAction) -> Unit
) {
val context = LocalContext.current
val (image, tooltip) = when (connectionState) {
MeshService.ConnectionState.CONNECTED -> Icons.TwoTone.CloudDone to R.string.connected
MeshService.ConnectionState.DEVICE_SLEEP -> Icons.TwoTone.CloudUpload to R.string.device_sleeping
MeshService.ConnectionState.DISCONNECTED -> Icons.TwoTone.CloudOff to R.string.disconnected
}
var showMenu by remember { mutableStateOf(false) }
IconButton(
onClick = {
Toast.makeText(context, tooltip, Toast.LENGTH_SHORT).show()
},
) {
Icon(
imageVector = image,
contentDescription = stringResource(id = tooltip),
)
}
IconButton(onClick = { showMenu = true }) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = "Overflow menu",
)
}
DropdownMenu(
expanded = showMenu,
onDismissRequest = { showMenu = false },
modifier = Modifier.background(MaterialTheme.colors.background.copy(alpha = 1f)),
) {
MainMenuAction.entries.forEach { action ->
DropdownMenuItem(
onClick = {
onAction(action)
showMenu = false
},
enabled = when (action) {
MainMenuAction.RADIO_CONFIG -> !isManaged
else -> true
},
) { Text(stringResource(id = action.stringRes)) }
}
}
}
@Composable
private fun BottomNavigation(
navController: NavController,
currentDestination: NavDestination?,
) {
val topLevelDestination = TopLevelDestination.fromNavDestination(currentDestination)
AnimatedVisibility(
visible = topLevelDestination != null,
enter = slideInVertically(
initialOffsetY = { it / 2 },
animationSpec = tween(durationMillis = 200),
),
exit = slideOutVertically(
targetOffsetY = { it / 2 },
animationSpec = tween(durationMillis = 200),
),
) {
BottomNavigation {
TopLevelDestination.entries.forEach {
val isSelected = it == topLevelDestination
BottomNavigationItem(
icon = {
Icon(
imageVector = it.icon,
contentDescription = it.name,
)
},
// label = { Text(it.label) },
selected = isSelected,
onClick = {
if (!isSelected) {
navController.navigate(it.route) {
// Pop up to the start destination of the graph to
// avoid building up a large stack of destinations
// on the back stack as users select items
popUpTo(navController.graph.findStartDestination().id) {
saveState = true
}
// Avoid multiple copies of the same destination when
// reselecting the same item
launchSingleTop = true
// Restore state when reselecting a previously selected item
restoreState = true
}
}
}
)
}
}
}
}

Wyświetl plik

@ -17,19 +17,9 @@
package com.geeksville.mesh.ui package com.geeksville.mesh.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material.Icon
import androidx.compose.material.IconButton
import androidx.compose.material.Scaffold
import androidx.compose.material.Text
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.automirrored.filled.Forward import androidx.compose.material.icons.automirrored.filled.Forward
import androidx.compose.material.icons.automirrored.filled.List import androidx.compose.material.icons.automirrored.filled.List
import androidx.compose.material.icons.automirrored.filled.Message import androidx.compose.material.icons.automirrored.filled.Message
@ -54,35 +44,27 @@ import androidx.compose.material.icons.filled.Speed
import androidx.compose.material.icons.filled.Usb import androidx.compose.material.icons.filled.Usb
import androidx.compose.material.icons.filled.Wifi import androidx.compose.material.icons.filled.Wifi
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.ComposeView import androidx.fragment.compose.AndroidFragment
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.stringResource
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.viewModels
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.navigation.NavHostController import androidx.navigation.NavHostController
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import androidx.navigation.toRoute
import com.geeksville.mesh.MeshProtos.DeviceMetadata import com.geeksville.mesh.MeshProtos.DeviceMetadata
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.model.MetricsViewModel import com.geeksville.mesh.model.MetricsViewModel
import com.geeksville.mesh.model.RadioConfigViewModel import com.geeksville.mesh.model.RadioConfigViewModel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.components.DeviceMetricsScreen import com.geeksville.mesh.ui.components.DeviceMetricsScreen
import com.geeksville.mesh.ui.components.EnvironmentMetricsScreen import com.geeksville.mesh.ui.components.EnvironmentMetricsScreen
import com.geeksville.mesh.ui.components.NodeMapScreen import com.geeksville.mesh.ui.components.NodeMapScreen
import com.geeksville.mesh.ui.components.PositionLogScreen import com.geeksville.mesh.ui.components.PositionLogScreen
import com.geeksville.mesh.ui.components.SignalMetricsScreen import com.geeksville.mesh.ui.components.SignalMetricsScreen
import com.geeksville.mesh.ui.components.TracerouteLogScreen import com.geeksville.mesh.ui.components.TracerouteLogScreen
import com.geeksville.mesh.util.UiText
import com.geeksville.mesh.ui.components.config.AmbientLightingConfigScreen import com.geeksville.mesh.ui.components.config.AmbientLightingConfigScreen
import com.geeksville.mesh.ui.components.config.AudioConfigScreen import com.geeksville.mesh.ui.components.config.AudioConfigScreen
import com.geeksville.mesh.ui.components.config.BluetoothConfigScreen import com.geeksville.mesh.ui.components.config.BluetoothConfigScreen
@ -106,76 +88,11 @@ import com.geeksville.mesh.ui.components.config.SerialConfigScreen
import com.geeksville.mesh.ui.components.config.StoreForwardConfigScreen import com.geeksville.mesh.ui.components.config.StoreForwardConfigScreen
import com.geeksville.mesh.ui.components.config.TelemetryConfigScreen import com.geeksville.mesh.ui.components.config.TelemetryConfigScreen
import com.geeksville.mesh.ui.components.config.UserConfigScreen import com.geeksville.mesh.ui.components.config.UserConfigScreen
import com.google.accompanist.themeadapter.appcompat.AppCompatTheme import com.geeksville.mesh.ui.map.MapView
import dagger.hilt.android.AndroidEntryPoint import com.geeksville.mesh.ui.message.MessageScreen
import com.geeksville.mesh.util.UiText
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
internal fun FragmentManager.navigateToNavGraph(
destNum: Int? = null,
startDestination: String = "RadioConfig",
) {
val radioConfigFragment = NavGraphFragment().apply {
arguments = bundleOf("destNum" to destNum, "startDestination" to startDestination)
}
beginTransaction()
.replace(R.id.mainActivityLayout, radioConfigFragment)
.addToBackStack(null)
.commit()
}
@AndroidEntryPoint
class NavGraphFragment : ScreenFragment("NavGraph"), Logging {
private val model: RadioConfigViewModel by viewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
@Suppress("DEPRECATION")
val destNum = arguments?.getSerializable("destNum") as? Int
val startDestination: Any = when (arguments?.getString("startDestination")) {
"NodeDetails" -> Route.NodeDetail(destNum!!)
else -> Route.RadioConfig(destNum)
}
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
setContent {
val node by model.destNode.collectAsStateWithLifecycle()
AppCompatTheme {
val navController: NavHostController = rememberNavController()
Scaffold(
topBar = {
MeshAppBar(
currentScreen = node?.user?.longName
?: stringResource(R.string.unknown_username),
canNavigateBack = true,
navigateUp = {
if (navController.previousBackStackEntry != null) {
navController.navigateUp()
} else {
parentFragmentManager.popBackStack()
}
},
)
}
) { innerPadding ->
NavGraph(
navController = navController,
startDestination = startDestination,
modifier = Modifier.padding(innerPadding),
)
}
}
}
}
}
}
enum class AdminRoute(@StringRes val title: Int) { enum class AdminRoute(@StringRes val title: Int) {
REBOOT(R.string.reboot), REBOOT(R.string.reboot),
SHUTDOWN(R.string.shutdown), SHUTDOWN(R.string.shutdown),
@ -184,13 +101,21 @@ enum class AdminRoute(@StringRes val title: Int) {
} }
sealed interface Route { sealed interface Route {
@Serializable data object Contacts : Route
@Serializable data object Nodes : Route
@Serializable data object Map : Route
@Serializable data object Channels : Route
@Serializable data object Settings : Route
@Serializable data object DebugPanel : Route
@Serializable @Serializable
data class Messages(val contactKey: String, val message: String = "") : Route data class Messages(val contactKey: String, val message: String = "") : Route
@Serializable data object QuickChat : Route
@Serializable @Serializable
data class RadioConfig(val destNum: Int? = null) : Route data class RadioConfig(val destNum: Int? = null) : Route
@Serializable data object User : Route @Serializable data object User : Route
@Serializable data object Channels : Route @Serializable data object ChannelConfig : Route
@Serializable data object Device : Route @Serializable data object Device : Route
@Serializable data object Position : Route @Serializable data object Position : Route
@Serializable data object Power : Route @Serializable data object Power : Route
@ -227,7 +152,7 @@ sealed interface Route {
// Config (type = AdminProtos.AdminMessage.ConfigType) // Config (type = AdminProtos.AdminMessage.ConfigType)
enum class ConfigRoute(val title: String, val route: Route, val icon: ImageVector?, val type: Int = 0) { enum class ConfigRoute(val title: String, val route: Route, val icon: ImageVector?, val type: Int = 0) {
USER("User", Route.User, Icons.Default.Person, 0), USER("User", Route.User, Icons.Default.Person, 0),
CHANNELS("Channels", Route.Channels, Icons.AutoMirrored.Default.List, 0), CHANNELS("Channels", Route.ChannelConfig, Icons.AutoMirrored.Default.List, 0),
DEVICE("Device", Route.Device, Icons.Default.Router, 0), DEVICE("Device", Route.Device, Icons.Default.Router, 0),
POSITION("Position", Route.Position, Icons.Default.LocationOn, 1), POSITION("Position", Route.Position, Icons.Default.LocationOn, 1),
POWER("Power", Route.Power, Icons.Default.Power, 2), POWER("Power", Route.Power, Icons.Default.Power, 2),
@ -291,41 +216,54 @@ sealed class ResponseState<out T> {
fun isWaiting() = this !is Empty fun isWaiting() = this !is Empty
} }
@Composable
private fun MeshAppBar(
currentScreen: String,
canNavigateBack: Boolean,
navigateUp: () -> Unit,
modifier: Modifier = Modifier,
) {
TopAppBar(
title = { Text(currentScreen) },
modifier = modifier,
navigationIcon = {
if (canNavigateBack) {
IconButton(onClick = navigateUp) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(id = R.string.navigate_back),
)
}
}
}
)
}
@Suppress("LongMethod") @Suppress("LongMethod")
@Composable @Composable
fun NavGraph( fun NavGraph(
model: UIViewModel = hiltViewModel(),
navController: NavHostController = rememberNavController(), navController: NavHostController = rememberNavController(),
startDestination: Any,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = startDestination, startDestination = Route.Contacts,
modifier = modifier, modifier = modifier,
) { ) {
composable<Route.Contacts> {
AndroidFragment<ContactsFragment>()
}
composable<Route.Nodes> {
NodeScreen(
model = model,
navigateToMessages = { navController.navigate(Route.Messages(it)) },
navigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) },
)
}
composable<Route.Map> {
MapView(model)
}
composable<Route.Channels> {
ChannelScreen(model)
}
composable<Route.Settings> {
AndroidFragment<SettingsFragment>(Modifier.fillMaxSize())
}
composable<Route.DebugPanel> {
DebugScreen()
}
composable<Route.Messages> { backStackEntry ->
val args = backStackEntry.toRoute<Route.Messages>()
MessageScreen(
contactKey = args.contactKey,
message = args.message,
viewModel = model,
navigateToMessages = { navController.navigate(Route.Messages(it)) },
navigateToNodeDetails = { navController.navigate(Route.NodeDetail(it)) },
onNavigateBack = navController::navigateUp
)
}
composable<Route.QuickChat> {
QuickChatScreen()
}
composable<Route.NodeDetail> { composable<Route.NodeDetail> {
NodeDetailScreen { navController.navigate(route = it) } NodeDetailScreen { navController.navigate(route = it) }
} }
@ -360,7 +298,7 @@ fun NavGraph(
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() } val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
UserConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry)) UserConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }
composable<Route.Channels> { composable<Route.ChannelConfig> {
val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() } val parentEntry = remember { navController.getBackStackEntry<Route.RadioConfig>() }
ChannelConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry)) ChannelConfigScreen(hiltViewModel<RadioConfigViewModel>(parentEntry))
} }

Wyświetl plik

@ -17,10 +17,6 @@
package com.geeksville.mesh.ui package com.geeksville.mesh.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
@ -31,67 +27,21 @@ import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.fragment.app.activityViewModels
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.DataPacket import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.components.NodeMenuAction
import com.geeksville.mesh.ui.components.NodeFilterTextField import com.geeksville.mesh.ui.components.NodeFilterTextField
import com.geeksville.mesh.ui.components.NodeMenuAction
import com.geeksville.mesh.ui.components.rememberTimeTickWithLifecycle import com.geeksville.mesh.ui.components.rememberTimeTickWithLifecycle
import com.geeksville.mesh.ui.message.navigateToMessages
import com.geeksville.mesh.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class UsersFragment : ScreenFragment("Users"), Logging {
private val model: UIViewModel by activityViewModels()
private fun navigateToMessages(node: Node) = node.user.let { user ->
val hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC // TODO use meta.hasPKC
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
val contactKey = "$channel${user.id}"
info("calling MessagesFragment filter: $contactKey")
parentFragmentManager.navigateToMessages(contactKey)
}
private fun navigateToNodeDetails(nodeNum: Int) {
info("calling NodeDetails --> destNum: $nodeNum")
parentFragmentManager.navigateToNavGraph(nodeNum, "NodeDetails")
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
AppTheme {
NodesScreen(
model = model,
navigateToMessages = ::navigateToMessages,
navigateToNodeDetails = ::navigateToNodeDetails,
)
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable
@Suppress("LongMethod") @Suppress("LongMethod")
fun NodesScreen( @Composable
fun NodeScreen(
model: UIViewModel = hiltViewModel(), model: UIViewModel = hiltViewModel(),
navigateToMessages: (Node) -> Unit, navigateToMessages: (String) -> Unit,
navigateToNodeDetails: (Int) -> Unit, navigateToNodeDetails: (Int) -> Unit,
) { ) {
val state by model.nodesUiState.collectAsStateWithLifecycle() val state by model.nodesUiState.collectAsStateWithLifecycle()
@ -135,7 +85,12 @@ fun NodesScreen(
when (menuItem) { when (menuItem) {
is NodeMenuAction.Remove -> model.removeNode(node.num) is NodeMenuAction.Remove -> model.removeNode(node.num)
is NodeMenuAction.Ignore -> model.ignoreNode(node) is NodeMenuAction.Ignore -> model.ignoreNode(node)
is NodeMenuAction.DirectMessage -> navigateToMessages(node) is NodeMenuAction.DirectMessage -> {
val hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
navigateToMessages("$channel${node.user.id}")
}
is NodeMenuAction.RequestUserInfo -> model.requestUserInfo(node.num) is NodeMenuAction.RequestUserInfo -> model.requestUserInfo(node.num)
is NodeMenuAction.RequestPosition -> model.requestPosition(node.num) is NodeMenuAction.RequestPosition -> model.requestPosition(node.num)
is NodeMenuAction.TraceRoute -> model.requestTraceroute(node.num) is NodeMenuAction.TraceRoute -> model.requestTraceroute(node.num)

Wyświetl plik

@ -17,10 +17,6 @@
package com.geeksville.mesh.ui package com.geeksville.mesh.ui
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.animation.core.animateDpAsState import androidx.compose.animation.core.animateDpAsState
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
@ -49,13 +45,10 @@ import androidx.compose.material.IconButton
import androidx.compose.material.ListItem import androidx.compose.material.ListItem
import androidx.compose.material.MaterialTheme import androidx.compose.material.MaterialTheme
import androidx.compose.material.OutlinedTextField import androidx.compose.material.OutlinedTextField
import androidx.compose.material.Scaffold
import androidx.compose.material.Switch import androidx.compose.material.Switch
import androidx.compose.material.Text import androidx.compose.material.Text
import androidx.compose.material.TextButton import androidx.compose.material.TextButton
import androidx.compose.material.TopAppBar
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.ArrowBack
import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Add
import androidx.compose.material.icons.filled.FastForward import androidx.compose.material.icons.filled.FastForward
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
@ -66,9 +59,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
@ -76,55 +67,15 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.entity.QuickChatAction import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.components.dragContainer import com.geeksville.mesh.ui.components.dragContainer
import com.geeksville.mesh.ui.components.dragDropItemsIndexed import com.geeksville.mesh.ui.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.components.rememberDragDropState import com.geeksville.mesh.ui.components.rememberDragDropState
import com.geeksville.mesh.ui.theme.AppTheme import com.geeksville.mesh.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint
@AndroidEntryPoint
class QuickChatSettingsFragment : ScreenFragment("Quick Chat Settings"), Logging {
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
setContent {
AppTheme {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(id = R.string.quick_chat)) },
navigationIcon = {
IconButton(onClick = { parentFragmentManager.popBackStack() }) {
Icon(
Icons.AutoMirrored.Filled.ArrowBack,
stringResource(id = R.string.navigate_back),
)
}
},
)
},
) { innerPadding ->
QuickChatScreen(
modifier = Modifier.padding(innerPadding)
)
}
}
}
}
}
}
@Composable @Composable
internal fun QuickChatScreen( internal fun QuickChatScreen(

Wyświetl plik

@ -23,36 +23,22 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.os.bundleOf
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels import androidx.fragment.app.activityViewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.android.Logging import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.R
import com.geeksville.mesh.databinding.ShareFragmentBinding import com.geeksville.mesh.databinding.ShareFragmentBinding
import com.geeksville.mesh.model.Contact import com.geeksville.mesh.model.Contact
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.message.navigateToMessages
import com.geeksville.mesh.ui.theme.AppTheme import com.geeksville.mesh.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint import dagger.hilt.android.AndroidEntryPoint
internal fun FragmentManager.navigateToShareMessage(message: String) {
val shareFragment = ShareFragment().apply {
arguments = bundleOf("message" to message)
}
beginTransaction()
.add(R.id.mainActivityLayout, shareFragment)
.addToBackStack(null)
.commit()
}
@AndroidEntryPoint @AndroidEntryPoint
class ShareFragment : ScreenFragment("Messages"), Logging { class ShareFragment : ScreenFragment("Messages"), Logging {
@ -67,10 +53,10 @@ class ShareFragment : ScreenFragment("Messages"), Logging {
private fun shareMessage(contact: Contact) { private fun shareMessage(contact: Contact) {
debug("calling MessagesFragment filter:${contact.contactKey}") debug("calling MessagesFragment filter:${contact.contactKey}")
parentFragmentManager.navigateToMessages( // parentFragmentManager.navigateToMessages(
contact.contactKey, // contact.contactKey,
arguments?.getString("message").toString() // arguments?.getString("message").toString()
) // )
} }
private fun onClick(contact: Contact) { private fun onClick(contact: Contact) {

Wyświetl plik

@ -49,13 +49,30 @@ import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.AppOnlyProtos.ChannelSet import com.geeksville.mesh.AppOnlyProtos.ChannelSet
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.channelSet import com.geeksville.mesh.channelSet
import com.geeksville.mesh.copy import com.geeksville.mesh.copy
import com.geeksville.mesh.model.Channel import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.components.config.ChannelSelection import com.geeksville.mesh.ui.components.config.ChannelSelection
@Composable
fun ScannedQrCodeDialog(
viewModel: UIViewModel,
incoming: ChannelSet,
) {
val channels by viewModel.channels.collectAsStateWithLifecycle()
ScannedQrCodeDialog(
channels = channels,
incoming = incoming,
onDismiss = viewModel::clearRequestChannelUrl,
onConfirm = viewModel::setChannels,
)
}
/** /**
* Enables the user to select which channels to accept after scanning a QR code. * Enables the user to select which channels to accept after scanning a QR code.
*/ */

Wyświetl plik

@ -18,10 +18,6 @@
package com.geeksville.mesh.ui.map package com.geeksville.mesh.ui.map
import android.content.Context import android.content.Context
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.content.res.AppCompatResources import androidx.appcompat.content.res.AppCompatResources
@ -45,21 +41,17 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.fragment.app.activityViewModels
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import com.geeksville.mesh.DataPacket import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.MeshProtos.Waypoint import com.geeksville.mesh.MeshProtos.Waypoint
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.android.BuildUtils.debug import com.geeksville.mesh.android.BuildUtils.debug
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.getLocationPermissions import com.geeksville.mesh.android.getLocationPermissions
import com.geeksville.mesh.android.gpsDisabled import com.geeksville.mesh.android.gpsDisabled
import com.geeksville.mesh.android.hasGps import com.geeksville.mesh.android.hasGps
@ -71,8 +63,6 @@ import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.map.CustomTileSource import com.geeksville.mesh.model.map.CustomTileSource
import com.geeksville.mesh.model.map.MarkerWithLabel import com.geeksville.mesh.model.map.MarkerWithLabel
import com.geeksville.mesh.model.map.clustering.RadiusMarkerClusterer import com.geeksville.mesh.model.map.clustering.RadiusMarkerClusterer
import com.geeksville.mesh.ui.ScreenFragment
import com.geeksville.mesh.ui.theme.AppTheme
import com.geeksville.mesh.util.SqlTileWriterExt import com.geeksville.mesh.util.SqlTileWriterExt
import com.geeksville.mesh.util.addCopyright import com.geeksville.mesh.util.addCopyright
import com.geeksville.mesh.util.addScaleBarOverlay import com.geeksville.mesh.util.addScaleBarOverlay
@ -81,7 +71,6 @@ import com.geeksville.mesh.util.formatAgo
import com.geeksville.mesh.util.zoomIn import com.geeksville.mesh.util.zoomIn
import com.geeksville.mesh.waypoint import com.geeksville.mesh.waypoint
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dagger.hilt.android.AndroidEntryPoint
import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable import org.osmdroid.bonuspack.utils.BonusPackHelper.getBitmapFromVectorDrawable
import org.osmdroid.config.Configuration import org.osmdroid.config.Configuration
import org.osmdroid.events.MapEventsReceiver import org.osmdroid.events.MapEventsReceiver
@ -104,27 +93,6 @@ import org.osmdroid.views.overlay.mylocation.MyLocationNewOverlay
import java.io.File import java.io.File
import java.text.DateFormat import java.text.DateFormat
@AndroidEntryPoint
class MapFragment : ScreenFragment("Map Fragment"), Logging {
private val model: UIViewModel by activityViewModels()
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setContent {
AppTheme {
MapView(model)
}
}
}
}
}
@Composable @Composable
private fun MapView.UpdateMarkers( private fun MapView.UpdateMarkers(
nodeMarkers: List<MarkerWithLabel>, nodeMarkers: List<MarkerWithLabel>,

Wyświetl plik

@ -17,10 +17,6 @@
package com.geeksville.mesh.ui.message package com.geeksville.mesh.ui.message
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
@ -66,10 +62,8 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.focus.onFocusEvent import androidx.compose.ui.focus.onFocusEvent
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalClipboardManager import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.colorResource
import androidx.compose.ui.res.pluralStringResource import androidx.compose.ui.res.pluralStringResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -80,95 +74,26 @@ import androidx.compose.ui.text.input.TextFieldValue
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.PreviewLightDark import androidx.compose.ui.tooling.preview.PreviewLightDark
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.content.ContextCompat
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.activityViewModels
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.geeksville.mesh.DataPacket import com.geeksville.mesh.DataPacket
import com.geeksville.mesh.R import com.geeksville.mesh.R
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.database.entity.QuickChatAction import com.geeksville.mesh.database.entity.QuickChatAction
import com.geeksville.mesh.model.Node
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.getChannel import com.geeksville.mesh.model.getChannel
import com.geeksville.mesh.ui.components.NodeKeyStatusIcon import com.geeksville.mesh.ui.components.NodeKeyStatusIcon
import com.geeksville.mesh.ui.components.NodeMenuAction import com.geeksville.mesh.ui.components.NodeMenuAction
import com.geeksville.mesh.ui.message.components.MessageList import com.geeksville.mesh.ui.message.components.MessageList
import com.geeksville.mesh.ui.navigateToNavGraph
import com.geeksville.mesh.ui.theme.AppTheme import com.geeksville.mesh.ui.theme.AppTheme
import dagger.hilt.android.AndroidEntryPoint
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
internal fun FragmentManager.navigateToMessages(contactKey: String, message: String = "") {
val messagesFragment = MessagesFragment().apply {
arguments = bundleOf("contactKey" to contactKey, "message" to message)
}
beginTransaction()
.add(R.id.mainActivityLayout, messagesFragment)
.addToBackStack(null)
.commit()
}
@AndroidEntryPoint
class MessagesFragment : Fragment(), Logging {
private val model: UIViewModel by activityViewModels()
private fun navigateToMessages(node: Node) = node.user.let { user ->
val hasPKC = model.ourNodeInfo.value?.hasPKC == true && node.hasPKC // TODO use meta.hasPKC
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else node.channel
val contactKey = "$channel${user.id}"
info("calling MessagesFragment filter: $contactKey")
parentFragmentManager.navigateToMessages(contactKey)
}
private fun navigateToNodeDetails(nodeNum: Int) {
info("calling NodeDetails --> destNum: $nodeNum")
parentFragmentManager.navigateToNavGraph(nodeNum, "NodeDetails")
}
override fun onCreateView(
inflater: LayoutInflater,
container: ViewGroup?,
savedInstanceState: Bundle?
): View {
val contactKey = arguments?.getString("contactKey").toString()
val message = arguments?.getString("message").toString()
return ComposeView(requireContext()).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setBackgroundColor(ContextCompat.getColor(context, R.color.colorAdvancedBackground))
setContent {
AppTheme {
MessageScreen(
contactKey = contactKey,
message = message,
viewModel = model,
navigateToMessages = ::navigateToMessages,
navigateToNodeDetails = ::navigateToNodeDetails,
) { parentFragmentManager.popBackStack() }
}
}
}
}
}
sealed class MessageMenuAction {
data object ClipboardCopy : MessageMenuAction()
data object Delete : MessageMenuAction()
data object Dismiss : MessageMenuAction()
data object SelectAll : MessageMenuAction()
}
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable @Composable
internal fun MessageScreen( internal fun MessageScreen(
contactKey: String, contactKey: String,
message: String, message: String,
viewModel: UIViewModel = hiltViewModel(), viewModel: UIViewModel = hiltViewModel(),
navigateToMessages: (Node) -> Unit, navigateToMessages: (String) -> Unit,
navigateToNodeDetails: (Int) -> Unit, navigateToNodeDetails: (Int) -> Unit,
onNavigateBack: () -> Unit onNavigateBack: () -> Unit
) { ) {
@ -281,7 +206,11 @@ internal fun MessageScreen(
when (action) { when (action) {
is NodeMenuAction.Remove -> viewModel.removeNode(action.node.num) is NodeMenuAction.Remove -> viewModel.removeNode(action.node.num)
is NodeMenuAction.Ignore -> viewModel.ignoreNode(action.node) is NodeMenuAction.Ignore -> viewModel.ignoreNode(action.node)
is NodeMenuAction.DirectMessage -> navigateToMessages(action.node) is NodeMenuAction.DirectMessage -> {
val hasPKC = viewModel.ourNodeInfo.value?.hasPKC == true && action.node.hasPKC
val channel = if (hasPKC) DataPacket.PKC_CHANNEL_INDEX else action.node.channel
navigateToMessages("$channel${action.node.user.id}")
}
is NodeMenuAction.RequestUserInfo -> viewModel.requestUserInfo(action.node.num) is NodeMenuAction.RequestUserInfo -> viewModel.requestUserInfo(action.node.num)
is NodeMenuAction.RequestPosition -> viewModel.requestPosition(action.node.num) is NodeMenuAction.RequestPosition -> viewModel.requestPosition(action.node.num)
is NodeMenuAction.TraceRoute -> viewModel.requestTraceroute(action.node.num) is NodeMenuAction.TraceRoute -> viewModel.requestTraceroute(action.node.num)
@ -324,6 +253,13 @@ private fun DeleteMessageDialog(
) )
} }
sealed class MessageMenuAction {
data object ClipboardCopy : MessageMenuAction()
data object Delete : MessageMenuAction()
data object Dismiss : MessageMenuAction()
data object SelectAll : MessageMenuAction()
}
@Composable @Composable
private fun ActionModeTopBar( private fun ActionModeTopBar(
selectedList: Set<Long>, selectedList: Set<Long>,

Wyświetl plik

@ -1,28 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.theme
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.Shapes
import androidx.compose.ui.unit.dp
val Shapes = Shapes(
small = RoundedCornerShape(4.dp),
medium = RoundedCornerShape(4.dp),
large = RoundedCornerShape(0.dp)
)

Wyświetl plik

@ -61,8 +61,6 @@ fun AppTheme(
MaterialTheme( MaterialTheme(
colors = colors, colors = colors,
typography = Typography,
shapes = Shapes,
content = content content = content
) )
} }

Wyświetl plik

@ -1,58 +0,0 @@
/*
* Copyright (c) 2025 Meshtastic LLC
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
package com.geeksville.mesh.ui.theme
import androidx.compose.material.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
h3 = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 24.sp
),
h4 = TextStyle(
fontWeight = FontWeight.Bold,
fontSize = 20.sp
),
body1 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp
),
body2 = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Bold,
fontSize = 16.sp
),
/* Other default text styles to override
button = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.W500,
fontSize = 14.sp
),
caption = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 12.sp
)
*/
)

Wyświetl plik

@ -1,97 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2025 Meshtastic LLC
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<FrameLayout 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"
android:id="@+id/mainActivityLayout">
<androidx.coordinatorlayout.widget.CoordinatorLayout
android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBarLayout"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<com.google.android.material.appbar.MaterialToolbar
android:id="@+id/toolbar"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/MyToolbar"
android:visibility="visible"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<ImageView
android:id="@+id/appIconImageVIew"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:contentDescription="@string/application_icon"
android:scaleType="center"
android:scaleX="1.5"
android:scaleY="1.5"
app:srcCompat="@drawable/app_icon"
tools:layout_editor_absoluteX="16dp"
tools:layout_editor_absoluteY="18dp" />
<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" />
</com.google.android.material.appbar.MaterialToolbar>
</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"
app:tabGravity="fill"
app:tabIconTint="@color/tab_color_selector"
app:tabIndicatorColor="@color/selectedColor" />
</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>
<androidx.compose.ui.platform.ComposeView
android:id="@+id/composeView"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</FrameLayout>

Wyświetl plik

@ -1,70 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Copyright (c) 2025 Meshtastic LLC
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<menu
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
>
<item
android:id="@+id/connectStatusImage"
android:contentDescription="@string/connection_status"
android:icon="@drawable/cloud_off"
app:iconTint="@color/toolbarText"
android:title="@string/disconnected"
app:showAsAction="ifRoom"
/>
<item
android:id="@+id/debug"
android:title="@string/debug_panel"
app:showAsAction="withText" />
<item
android:id="@+id/stress_test"
android:checkable="true"
android:checked="false"
android:title="@string/protocol_stress_test" />
<item
android:id="@+id/radio_config"
app:showAsAction="withText"
android:title="@string/device_settings" />
<item
android:id="@+id/save_messages_csv"
app:showAsAction="withText"
android:title="@string/save_messages" />
<item
android:id="@+id/theme"
android:title="@string/theme"
app:showAsAction="withText" />
<item
android:id="@+id/preferences_language"
android:title="@string/preferences_language"
app:showAsAction="withText" />
<item
android:id="@+id/show_intro"
android:title="@string/intro_show"
app:showAsAction="withText" />
<item
android:id="@+id/preferences_quick_chat"
android:title="@string/quick_chat"
app:showAsAction="withText" />
<item
android:id="@+id/about"
android:title="@string/about"
app:showAsAction="withText" />
</menu>

Wyświetl plik

@ -72,7 +72,6 @@
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "contact_key") val contact_key: String</ID> <ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "contact_key") val contact_key: String</ID>
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "port_num") val port_num: Int</ID> <ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "port_num") val port_num: Int</ID>
<ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "received_time") val received_time: Long</ID> <ID>ConstructorParameterNaming:Packet.kt$Packet$@ColumnInfo(name = "received_time") val received_time: Long</ID>
<ID>CyclomaticComplexMethod:MainActivity.kt$MainActivity$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
<ID>CyclomaticComplexMethod:MapFragment.kt$@Composable fun MapView( model: UIViewModel = viewModel(), )</ID> <ID>CyclomaticComplexMethod:MapFragment.kt$@Composable fun MapView( model: UIViewModel = viewModel(), )</ID>
<ID>CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID> <ID>CyclomaticComplexMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID> <ID>CyclomaticComplexMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
@ -81,7 +80,6 @@
<ID>EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }</ID> <ID>EmptyClassBlock:DebugLogFile.kt$BinaryLogFile${ }</ID>
<ID>EmptyDefaultConstructor:SqlTileWriterExt.kt$SqlTileWriterExt$()</ID> <ID>EmptyDefaultConstructor:SqlTileWriterExt.kt$SqlTileWriterExt$()</ID>
<ID>EmptyDefaultConstructor:SqlTileWriterExt.kt$SqlTileWriterExt.SourceCount$()</ID> <ID>EmptyDefaultConstructor:SqlTileWriterExt.kt$SqlTileWriterExt.SourceCount$()</ID>
<ID>EmptyFunctionBlock:MainActivity.kt$MainActivity.&lt;no name provided&gt;${ }</ID>
<ID>EmptyFunctionBlock:NopInterface.kt$NopInterface${ }</ID> <ID>EmptyFunctionBlock:NopInterface.kt$NopInterface${ }</ID>
<ID>EmptyFunctionBlock:NsdManager.kt$&lt;no name provided&gt;${ }</ID> <ID>EmptyFunctionBlock:NsdManager.kt$&lt;no name provided&gt;${ }</ID>
<ID>EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${}</ID> <ID>EmptyFunctionBlock:TrustAllX509TrustManager.kt$TrustAllX509TrustManager${}</ID>
@ -129,7 +127,6 @@
<ID>FinalNewline:SoftwareUpdateService.kt$com.geeksville.mesh.service.SoftwareUpdateService.kt</ID> <ID>FinalNewline:SoftwareUpdateService.kt$com.geeksville.mesh.service.SoftwareUpdateService.kt</ID>
<ID>FinalNewline:SqlTileWriterExt.kt$com.geeksville.mesh.util.SqlTileWriterExt.kt</ID> <ID>FinalNewline:SqlTileWriterExt.kt$com.geeksville.mesh.util.SqlTileWriterExt.kt</ID>
<ID>FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID> <ID>FinalNewline:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
<ID>FinalNewline:Theme.kt$com.geeksville.mesh.ui.theme.Theme.kt</ID>
<ID>FinalNewline:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID> <ID>FinalNewline:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID>
<ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID> <ID>FinalNewline:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>ForbiddenComment:MapFragment.kt$// TODO: Accept filename input param from user</ID> <ID>ForbiddenComment:MapFragment.kt$// TODO: Accept filename input param from user</ID>
@ -158,10 +155,8 @@
<ID>LongMethod:EditListPreference.kt$@Composable inline fun &lt;reified T&gt; EditListPreference( title: String, list: List&lt;T&gt;, maxCount: Int, enabled: Boolean, keyboardActions: KeyboardActions, crossinline onValuesChanged: (List&lt;T&gt;) -&gt; Unit, modifier: Modifier = Modifier, )</ID> <ID>LongMethod:EditListPreference.kt$@Composable inline fun &lt;reified T&gt; EditListPreference( title: String, list: List&lt;T&gt;, maxCount: Int, enabled: Boolean, keyboardActions: KeyboardActions, crossinline onValuesChanged: (List&lt;T&gt;) -&gt; Unit, modifier: Modifier = Modifier, )</ID>
<ID>LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigItemList( ringtone: String, extNotificationConfig: ExternalNotificationConfig, enabled: Boolean, onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -&gt; Unit, )</ID> <ID>LongMethod:ExternalNotificationConfigItemList.kt$@Composable fun ExternalNotificationConfigItemList( ringtone: String, extNotificationConfig: ExternalNotificationConfig, enabled: Boolean, onSaveClicked: (ringtone: String, config: ExternalNotificationConfig) -&gt; Unit, )</ID>
<ID>LongMethod:MQTTConfigItemList.kt$@Composable fun MQTTConfigItemList( mqttConfig: MQTTConfig, enabled: Boolean, onSaveClicked: (MQTTConfig) -&gt; Unit, )</ID> <ID>LongMethod:MQTTConfigItemList.kt$@Composable fun MQTTConfigItemList( mqttConfig: MQTTConfig, enabled: Boolean, onSaveClicked: (MQTTConfig) -&gt; Unit, )</ID>
<ID>LongMethod:MainActivity.kt$MainActivity$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
<ID>LongMethod:MapFragment.kt$@Composable fun MapView( model: UIViewModel = viewModel(), )</ID> <ID>LongMethod:MapFragment.kt$@Composable fun MapView( model: UIViewModel = viewModel(), )</ID>
<ID>LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID> <ID>LongMethod:MeshService.kt$MeshService$private fun handleReceivedData(packet: MeshPacket)</ID>
<ID>LongMethod:NetworkConfigItemList.kt$@Composable fun NetworkConfigItemList( networkConfig: NetworkConfig, enabled: Boolean, onSaveClicked: (NetworkConfig) -&gt; Unit, )</ID>
<ID>LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigItemList( powerConfig: PowerConfig, enabled: Boolean, onSaveClicked: (PowerConfig) -&gt; Unit, )</ID> <ID>LongMethod:PowerConfigItemList.kt$@Composable fun PowerConfigItemList( powerConfig: PowerConfig, enabled: Boolean, onSaveClicked: (PowerConfig) -&gt; Unit, )</ID>
<ID>LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID> <ID>LongMethod:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigItemList( serialConfig: SerialConfig, enabled: Boolean, onSaveClicked: (SerialConfig) -&gt; Unit, )</ID> <ID>LongMethod:SerialConfigItemList.kt$@Composable fun SerialConfigItemList( serialConfig: SerialConfig, enabled: Boolean, onSaveClicked: (SerialConfig) -&gt; Unit, )</ID>
@ -219,7 +214,7 @@
<ID>MagicNumber:ContactsFragment.kt$ContactsFragment.ActionModeCallback$8</ID> <ID>MagicNumber:ContactsFragment.kt$ContactsFragment.ActionModeCallback$8</ID>
<ID>MagicNumber:ContextServices.kt$33</ID> <ID>MagicNumber:ContextServices.kt$33</ID>
<ID>MagicNumber:DataPacket.kt$DataPacket.CREATOR$16</ID> <ID>MagicNumber:DataPacket.kt$DataPacket.CREATOR$16</ID>
<ID>MagicNumber:DebugFragment.kt$3</ID> <ID>MagicNumber:Debug.kt$3</ID>
<ID>MagicNumber:DeviceVersion.kt$DeviceVersion$100</ID> <ID>MagicNumber:DeviceVersion.kt$DeviceVersion$100</ID>
<ID>MagicNumber:DeviceVersion.kt$DeviceVersion$10000</ID> <ID>MagicNumber:DeviceVersion.kt$DeviceVersion$10000</ID>
<ID>MagicNumber:DownloadButton.kt$1.25f</ID> <ID>MagicNumber:DownloadButton.kt$1.25f</ID>
@ -257,8 +252,6 @@
<ID>MagicNumber:LocationUtils.kt$6366000</ID> <ID>MagicNumber:LocationUtils.kt$6366000</ID>
<ID>MagicNumber:LocationUtils.kt$GPSFormat$3</ID> <ID>MagicNumber:LocationUtils.kt$GPSFormat$3</ID>
<ID>MagicNumber:MQTTRepository.kt$MQTTRepository$512</ID> <ID>MagicNumber:MQTTRepository.kt$MQTTRepository$512</ID>
<ID>MagicNumber:MainActivity.kt$MainActivity$30000</ID>
<ID>MagicNumber:MainActivity.kt$MainActivity$5</ID>
<ID>MagicNumber:MapFragment.kt$0.5f</ID> <ID>MagicNumber:MapFragment.kt$0.5f</ID>
<ID>MagicNumber:MapFragment.kt$1.3</ID> <ID>MagicNumber:MapFragment.kt$1.3</ID>
<ID>MagicNumber:MapFragment.kt$1000</ID> <ID>MagicNumber:MapFragment.kt$1000</ID>
@ -396,12 +389,9 @@
<ID>MaxLineLength:LoRaConfigItemList.kt$value = if (isFocused || loraInput.overrideFrequency != 0f) loraInput.overrideFrequency else primaryChannel.radioFreq</ID> <ID>MaxLineLength:LoRaConfigItemList.kt$value = if (isFocused || loraInput.overrideFrequency != 0f) loraInput.overrideFrequency else primaryChannel.radioFreq</ID>
<ID>MaxLineLength:LocationRepository.kt$LocationRepository$info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")</ID> <ID>MaxLineLength:LocationRepository.kt$LocationRepository$info("Starting location updates with $providerList intervalMs=${intervalMs}ms and minDistanceM=${minDistanceM}m")</ID>
<ID>MaxLineLength:MQTTRepository.kt$MQTTRepository.Companion$*</ID> <ID>MaxLineLength:MQTTRepository.kt$MQTTRepository.Companion$*</ID>
<ID>MaxLineLength:MainActivity.kt$/* UI design material setup instructions: https://material.io/develop/android/docs/getting-started/ dark theme (or use system eventually) https://material.io/develop/android/theming/dark/ NavDrawer is a standard draw which can be dragged in from the left or the menu icon inside the app title. Fragments: SettingsFragment shows "Settings" username shortname bluetooth pairing list (eventually misc device settings that are not channel related) Channel fragment "Channel" qr code, copy link button ch number misc other settings (eventually a way of choosing between past channels) ChatFragment "Messages" a text box to enter new texts a scrolling list of rows. each row is a text and a sender info layout NodeListFragment "Users" a node info row for every node ViewModels: BTScanModel starts/stops bt scan and provides list of devices (manages entire scan lifecycle) MeshModel contains: (manages entire service relationship) current received texts current radio macaddr current node infos (updated dynamically) eventually use bottom navigation bar to switch between, Members, Chat, Channel, Settings. https://material.io/develop/android/components/bottom-navigation-view/ use numbers of # chat messages and # of members in the badges. (per this recommendation to not use top tabs: https://ux.stackexchange.com/questions/102439/android-ux-when-to-use-bottom-navigation-and-when-to-use-tabs ) eventually: make a custom theme: https://github.com/material-components/material-components-android/tree/master/material-theme-builder */</ID>
<ID>MaxLineLength:MainActivity.kt$MainActivity$/* This problem can occur if we unbind, but there is already an onConnected job waiting to run. That job runs and then makes meshService != null again I think I've fixed this by cancelling connectionJob. We'll see! */</ID> <ID>MaxLineLength:MainActivity.kt$MainActivity$/* This problem can occur if we unbind, but there is already an onConnected job waiting to run. That job runs and then makes meshService != null again I think I've fixed this by cancelling connectionJob. We'll see! */</ID>
<ID>MaxLineLength:MainActivity.kt$MainActivity$// Old samsung phones have a race condition andthis might rarely fail. Which is probably find because the bind will be sufficient most of the time</ID> <ID>MaxLineLength:MainActivity.kt$MainActivity$// Old samsung phones have a race condition andthis might rarely fail. Which is probably find because the bind will be sufficient most of the time</ID>
<ID>MaxLineLength:MainActivity.kt$MainActivity$// We now wait for the device to connect, once connected, we ask the user if they want to switch to the new channel</ID> <ID>MaxLineLength:MainActivity.kt$MainActivity$// We now wait for the device to connect, once connected, we ask the user if they want to switch to the new channel</ID>
<ID>MaxLineLength:MainActivity.kt$MainActivity$// pager.offscreenPageLimit = 0 // Don't keep any offscreen pages around, because we want to make sure our bluetooth scanning stops</ID>
<ID>MaxLineLength:MainActivity.kt$MainActivity$MeshService.ConnectionState.DEVICE_SLEEP -&gt; R.drawable.ic_twotone_cloud_upload_24 to R.string.device_sleeping</ID>
<ID>MaxLineLength:MainActivity.kt$MainActivity$debug("Asked to open a channel URL - ask user if they want to switch to that channel. If so send the config to the radio")</ID> <ID>MaxLineLength:MainActivity.kt$MainActivity$debug("Asked to open a channel URL - ask user if they want to switch to that channel. If so send the config to the radio")</ID>
<ID>MaxLineLength:MeshService.kt$MeshService$*</ID> <ID>MaxLineLength:MeshService.kt$MeshService$*</ID>
<ID>MaxLineLength:MeshService.kt$MeshService$* Send a mesh packet to the radio, if the radio is not currently connected this function will throw NotConnectedException</ID> <ID>MaxLineLength:MeshService.kt$MeshService$* Send a mesh packet to the radio, if the radio is not currently connected this function will throw NotConnectedException</ID>
@ -480,8 +470,6 @@
<ID>MultiLineIfElse:BuildUtils.kt$BuildUtils$return false</ID> <ID>MultiLineIfElse:BuildUtils.kt$BuildUtils$return false</ID>
<ID>MultiLineIfElse:Channel.kt$Channel$"Custom"</ID> <ID>MultiLineIfElse:Channel.kt$Channel$"Custom"</ID>
<ID>MultiLineIfElse:Channel.kt$Channel$when (loraConfig.modemPreset) { ModemPreset.SHORT_TURBO -&gt; "ShortTurbo" ModemPreset.SHORT_FAST -&gt; "ShortFast" ModemPreset.SHORT_SLOW -&gt; "ShortSlow" ModemPreset.MEDIUM_FAST -&gt; "MediumFast" ModemPreset.MEDIUM_SLOW -&gt; "MediumSlow" ModemPreset.LONG_FAST -&gt; "LongFast" ModemPreset.LONG_SLOW -&gt; "LongSlow" ModemPreset.LONG_MODERATE -&gt; "LongMod" ModemPreset.VERY_LONG_SLOW -&gt; "VLongSlow" else -&gt; "Invalid" }</ID> <ID>MultiLineIfElse:Channel.kt$Channel$when (loraConfig.modemPreset) { ModemPreset.SHORT_TURBO -&gt; "ShortTurbo" ModemPreset.SHORT_FAST -&gt; "ShortFast" ModemPreset.SHORT_SLOW -&gt; "ShortSlow" ModemPreset.MEDIUM_FAST -&gt; "MediumFast" ModemPreset.MEDIUM_SLOW -&gt; "MediumSlow" ModemPreset.LONG_FAST -&gt; "LongFast" ModemPreset.LONG_SLOW -&gt; "LongSlow" ModemPreset.LONG_MODERATE -&gt; "LongMod" ModemPreset.VERY_LONG_SLOW -&gt; "VLongSlow" else -&gt; "Invalid" }</ID>
<ID>MultiLineIfElse:ChannelFragment.kt$channelSet = copy { settings.add(it) }</ID>
<ID>MultiLineIfElse:ChannelFragment.kt$channelSet = copy { settings[index] = it }</ID>
<ID>MultiLineIfElse:ChannelOption.kt$when (bandwidth) { 31 -&gt; .03125f 62 -&gt; .0625f 200 -&gt; .203125f 400 -&gt; .40625f 800 -&gt; .8125f 1600 -&gt; 1.6250f else -&gt; bandwidth / 1000f }</ID> <ID>MultiLineIfElse:ChannelOption.kt$when (bandwidth) { 31 -&gt; .03125f 62 -&gt; .0625f 200 -&gt; .203125f 400 -&gt; .40625f 800 -&gt; .8125f 1600 -&gt; 1.6250f else -&gt; bandwidth / 1000f }</ID>
<ID>MultiLineIfElse:ContextServices.kt$MaterialAlertDialogBuilder(this) .setTitle(title) .setMessage(rationale) .setNeutralButton(R.string.cancel) { _, _ -&gt; } .setPositiveButton(R.string.accept) { _, _ -&gt; invokeFun() } .show()</ID> <ID>MultiLineIfElse:ContextServices.kt$MaterialAlertDialogBuilder(this) .setTitle(title) .setMessage(rationale) .setNeutralButton(R.string.cancel) { _, _ -&gt; } .setPositiveButton(R.string.accept) { _, _ -&gt; invokeFun() } .show()</ID>
<ID>MultiLineIfElse:ContextServices.kt$invokeFun()</ID> <ID>MultiLineIfElse:ContextServices.kt$invokeFun()</ID>
@ -514,7 +502,6 @@
<ID>MultiLineIfElse:NodeInfo.kt$MeshUser$hwModel.name.replace('_', '-').replace('p', '.').lowercase()</ID> <ID>MultiLineIfElse:NodeInfo.kt$MeshUser$hwModel.name.replace('_', '-').replace('p', '.').lowercase()</ID>
<ID>MultiLineIfElse:NodeInfo.kt$MeshUser$null</ID> <ID>MultiLineIfElse:NodeInfo.kt$MeshUser$null</ID>
<ID>MultiLineIfElse:RadioConfigScreen.kt$AlertDialog( onDismissRequest = {}, shape = RoundedCornerShape(16.dp), backgroundColor = MaterialTheme.colors.background, title = { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, ) { Icon( imageVector = Icons.TwoTone.Warning, contentDescription = "warning", modifier = Modifier.padding(end = 8.dp) ) Text( text = "${stringResource(title)}?\n") Icon( imageVector = Icons.TwoTone.Warning, contentDescription = "warning", modifier = Modifier.padding(start = 8.dp) ) } }, buttons = { Row( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { TextButton( modifier = Modifier.weight(1f), onClick = { showDialog = false }, ) { Text(stringResource(R.string.cancel)) } Button( modifier = Modifier.weight(1f), onClick = { showDialog = false onClick() }, ) { Text(stringResource(R.string.send)) } } } )</ID> <ID>MultiLineIfElse:RadioConfigScreen.kt$AlertDialog( onDismissRequest = {}, shape = RoundedCornerShape(16.dp), backgroundColor = MaterialTheme.colors.background, title = { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.Center, ) { Icon( imageVector = Icons.TwoTone.Warning, contentDescription = "warning", modifier = Modifier.padding(end = 8.dp) ) Text( text = "${stringResource(title)}?\n") Icon( imageVector = Icons.TwoTone.Warning, contentDescription = "warning", modifier = Modifier.padding(start = 8.dp) ) } }, buttons = { Row( modifier = Modifier .fillMaxWidth() .padding(start = 16.dp, end = 16.dp, bottom = 16.dp), horizontalArrangement = Arrangement.spacedBy(8.dp), verticalAlignment = Alignment.CenterVertically, ) { TextButton( modifier = Modifier.weight(1f), onClick = { showDialog = false }, ) { Text(stringResource(R.string.cancel)) } Button( modifier = Modifier.weight(1f), onClick = { showDialog = false onClick() }, ) { Text(stringResource(R.string.send)) } } } )</ID>
<ID>MultiLineIfElse:RadioConfigViewModel.kt$RadioConfigViewModel$try { setChannels(channelUrl) } catch (ex: Exception) { errormsg("DeviceProfile channel import error", ex) setResponseStateError(ex.customMessage) }</ID>
<ID>MultiLineIfElse:RadioConfigViewModel.kt$RadioConfigViewModel$viewModelScope.launch { radioConfigRepository.replaceAllSettings(new) }</ID> <ID>MultiLineIfElse:RadioConfigViewModel.kt$RadioConfigViewModel$viewModelScope.launch { radioConfigRepository.replaceAllSettings(new) }</ID>
<ID>MultiLineIfElse:RadioInterfaceService.kt$RadioInterfaceService$startInterface()</ID> <ID>MultiLineIfElse:RadioInterfaceService.kt$RadioInterfaceService$startInterface()</ID>
<ID>MultiLineIfElse:SafeBluetooth.kt$SafeBluetooth$cb</ID> <ID>MultiLineIfElse:SafeBluetooth.kt$SafeBluetooth$cb</ID>
@ -582,7 +569,6 @@
<ID>NewLineAtEndOfFile:SoftwareUpdateService.kt$com.geeksville.mesh.service.SoftwareUpdateService.kt</ID> <ID>NewLineAtEndOfFile:SoftwareUpdateService.kt$com.geeksville.mesh.service.SoftwareUpdateService.kt</ID>
<ID>NewLineAtEndOfFile:SqlTileWriterExt.kt$com.geeksville.mesh.util.SqlTileWriterExt.kt</ID> <ID>NewLineAtEndOfFile:SqlTileWriterExt.kt$com.geeksville.mesh.util.SqlTileWriterExt.kt</ID>
<ID>NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID> <ID>NewLineAtEndOfFile:TCPInterfaceFactory.kt$com.geeksville.mesh.repository.radio.TCPInterfaceFactory.kt</ID>
<ID>NewLineAtEndOfFile:Theme.kt$com.geeksville.mesh.ui.theme.Theme.kt</ID>
<ID>NewLineAtEndOfFile:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID> <ID>NewLineAtEndOfFile:UsbBroadcastReceiver.kt$com.geeksville.mesh.repository.usb.UsbBroadcastReceiver.kt</ID>
<ID>NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID> <ID>NewLineAtEndOfFile:UsbRepositoryModule.kt$com.geeksville.mesh.repository.usb.UsbRepositoryModule.kt</ID>
<ID>NoBlankLineBeforeRbrace:BluetoothInterface.kt$BluetoothInterface$ </ID> <ID>NoBlankLineBeforeRbrace:BluetoothInterface.kt$BluetoothInterface$ </ID>
@ -617,8 +603,6 @@
<ID>NoWildcardImports:BluetoothInterface.kt$import com.geeksville.mesh.service.*</ID> <ID>NoWildcardImports:BluetoothInterface.kt$import com.geeksville.mesh.service.*</ID>
<ID>NoWildcardImports:DeviceVersionTest.kt$import org.junit.Assert.*</ID> <ID>NoWildcardImports:DeviceVersionTest.kt$import org.junit.Assert.*</ID>
<ID>NoWildcardImports:ExampleUnitTest.kt$import org.junit.Assert.*</ID> <ID>NoWildcardImports:ExampleUnitTest.kt$import org.junit.Assert.*</ID>
<ID>NoWildcardImports:MeshService.kt$import com.geeksville.mesh.*</ID>
<ID>NoWildcardImports:MeshService.kt$import com.geeksville.mesh.util.*</ID>
<ID>NoWildcardImports:MockInterface.kt$import com.geeksville.mesh.*</ID> <ID>NoWildcardImports:MockInterface.kt$import com.geeksville.mesh.*</ID>
<ID>NoWildcardImports:PreferenceFooter.kt$import androidx.compose.foundation.layout.*</ID> <ID>NoWildcardImports:PreferenceFooter.kt$import androidx.compose.foundation.layout.*</ID>
<ID>NoWildcardImports:PreferenceFooter.kt$import androidx.compose.material.*</ID> <ID>NoWildcardImports:PreferenceFooter.kt$import androidx.compose.material.*</ID>
@ -635,7 +619,6 @@
<ID>ParameterListWrapping:SafeBluetooth.kt$SafeBluetooth$( c: BluetoothGattDescriptor, cont: Continuation&lt;BluetoothGattDescriptor&gt;, timeout: Long = 0 )</ID> <ID>ParameterListWrapping:SafeBluetooth.kt$SafeBluetooth$( c: BluetoothGattDescriptor, cont: Continuation&lt;BluetoothGattDescriptor&gt;, timeout: Long = 0 )</ID>
<ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID> <ID>RethrowCaughtException:SyncContinuation.kt$Continuation$throw ex</ID>
<ID>ReturnCount:ChannelOption.kt$internal fun LoRaConfig.radioFreq(channelNum: Int): Float</ID> <ID>ReturnCount:ChannelOption.kt$internal fun LoRaConfig.radioFreq(channelNum: Int): Float</ID>
<ID>ReturnCount:MainActivity.kt$MainActivity$override fun onOptionsItemSelected(item: MenuItem): Boolean</ID>
<ID>ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID> <ID>ReturnCount:RadioConfigViewModel.kt$RadioConfigViewModel$private fun processPacketResponse(packet: MeshProtos.MeshPacket)</ID>
<ID>SpacingAroundColon:PreviewParameterProviders.kt$NodeInfoPreviewParameterProvider$:</ID> <ID>SpacingAroundColon:PreviewParameterProviders.kt$NodeInfoPreviewParameterProvider$:</ID>
<ID>SpacingAroundCurly:AppPrefs.kt$FloatPref$}</ID> <ID>SpacingAroundCurly:AppPrefs.kt$FloatPref$}</ID>
@ -650,7 +633,6 @@
<ID>SwallowedException:DeviceVersion.kt$DeviceVersion$e: Exception</ID> <ID>SwallowedException:DeviceVersion.kt$DeviceVersion$e: Exception</ID>
<ID>SwallowedException:Exceptions.kt$ex: Throwable</ID> <ID>SwallowedException:Exceptions.kt$ex: Throwable</ID>
<ID>SwallowedException:MainActivity.kt$MainActivity$ex: BindFailedException</ID> <ID>SwallowedException:MainActivity.kt$MainActivity$ex: BindFailedException</ID>
<ID>SwallowedException:MainActivity.kt$MainActivity$ex: IllegalStateException</ID>
<ID>SwallowedException:MeshLog.kt$MeshLog$e: IOException</ID> <ID>SwallowedException:MeshLog.kt$MeshLog$e: IOException</ID>
<ID>SwallowedException:MeshService.kt$MeshService$e: Exception</ID> <ID>SwallowedException:MeshService.kt$MeshService$e: Exception</ID>
<ID>SwallowedException:MeshService.kt$MeshService$e: TimeoutException</ID> <ID>SwallowedException:MeshService.kt$MeshService$e: TimeoutException</ID>
@ -664,7 +646,7 @@
<ID>SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException</ID> <ID>SwallowedException:TCPInterface.kt$TCPInterface$ex: SocketTimeoutException</ID>
<ID>TooGenericExceptionCaught:BTScanModel.kt$BTScanModel$ex: Throwable</ID> <ID>TooGenericExceptionCaught:BTScanModel.kt$BTScanModel$ex: Throwable</ID>
<ID>TooGenericExceptionCaught:BluetoothInterface.kt$BluetoothInterface$ex: Exception</ID> <ID>TooGenericExceptionCaught:BluetoothInterface.kt$BluetoothInterface$ex: Exception</ID>
<ID>TooGenericExceptionCaught:ChannelFragment.kt$ex: Exception</ID> <ID>TooGenericExceptionCaught:Channel.kt$ex: Exception</ID>
<ID>TooGenericExceptionCaught:ChannelSet.kt$ex: Throwable</ID> <ID>TooGenericExceptionCaught:ChannelSet.kt$ex: Throwable</ID>
<ID>TooGenericExceptionCaught:DeviceVersion.kt$DeviceVersion$e: Exception</ID> <ID>TooGenericExceptionCaught:DeviceVersion.kt$DeviceVersion$e: Exception</ID>
<ID>TooGenericExceptionCaught:Exceptions.kt$ex: Throwable</ID> <ID>TooGenericExceptionCaught:Exceptions.kt$ex: Throwable</ID>
@ -732,8 +714,6 @@
<ID>WildcardImport:BluetoothInterface.kt$import com.geeksville.mesh.service.*</ID> <ID>WildcardImport:BluetoothInterface.kt$import com.geeksville.mesh.service.*</ID>
<ID>WildcardImport:DeviceVersionTest.kt$import org.junit.Assert.*</ID> <ID>WildcardImport:DeviceVersionTest.kt$import org.junit.Assert.*</ID>
<ID>WildcardImport:ExampleUnitTest.kt$import org.junit.Assert.*</ID> <ID>WildcardImport:ExampleUnitTest.kt$import org.junit.Assert.*</ID>
<ID>WildcardImport:MeshService.kt$import com.geeksville.mesh.*</ID>
<ID>WildcardImport:MeshService.kt$import com.geeksville.mesh.util.*</ID>
<ID>WildcardImport:MockInterface.kt$import com.geeksville.mesh.*</ID> <ID>WildcardImport:MockInterface.kt$import com.geeksville.mesh.*</ID>
<ID>WildcardImport:PreferenceFooter.kt$import androidx.compose.foundation.layout.*</ID> <ID>WildcardImport:PreferenceFooter.kt$import androidx.compose.foundation.layout.*</ID>
<ID>WildcardImport:PreferenceFooter.kt$import androidx.compose.material.*</ID> <ID>WildcardImport:PreferenceFooter.kt$import androidx.compose.material.*</ID>