Meshtastic-Android/app/src/main/java/com/geeksville/mesh/MainActivity.kt

767 lines
29 KiB
Kotlin
Raw Normal View History

2020-01-22 21:46:41 -08:00
package com.geeksville.mesh
2020-01-20 15:53:22 -08:00
import android.app.Activity
2020-01-24 12:49:27 -08:00
import android.bluetooth.BluetoothAdapter
import android.content.Context
import android.content.Intent
2020-06-18 23:05:33 -04:00
import android.content.pm.PackageInfo
2020-01-21 13:12:01 -08:00
import android.content.pm.PackageManager
2020-06-08 14:04:56 -07:00
import android.hardware.usb.UsbManager
import android.net.Uri
2022-11-04 18:31:18 -03:00
import android.os.Bundle
import android.os.Handler
import android.os.Looper
import android.os.RemoteException
2021-03-02 13:22:55 +08:00
import android.text.method.LinkMovementMethod
2020-04-07 11:27:51 -07:00
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
2021-03-02 13:22:55 +08:00
import android.widget.TextView
2020-01-22 13:02:24 -08:00
import android.widget.Toast
2022-05-16 23:32:49 -03:00
import androidx.activity.result.contract.ActivityResultContracts
2020-04-08 08:16:06 -07:00
import androidx.activity.viewModels
2022-12-10 11:03:14 -03:00
import androidx.appcompat.app.AppCompatActivity
2021-04-11 12:10:17 +02:00
import androidx.appcompat.app.AppCompatDelegate
2020-06-18 23:05:33 -04:00
import androidx.appcompat.widget.Toolbar
import androidx.compose.runtime.getValue
2020-01-21 13:12:01 -08:00
import androidx.core.content.ContextCompat
2022-02-04 00:57:27 -03:00
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
2020-04-07 09:36:12 -07:00
import androidx.fragment.app.Fragment
2020-09-23 22:47:45 -04:00
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
import androidx.lifecycle.asLiveData
import androidx.lifecycle.compose.collectAsStateWithLifecycle
2020-04-07 09:36:12 -07:00
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.geeksville.mesh.android.BindFailedException
import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.Logging
import com.geeksville.mesh.android.ServiceClient
import com.geeksville.mesh.android.getBluetoothPermissions
import com.geeksville.mesh.android.getNotificationPermissions
import com.geeksville.mesh.android.hasBluetoothPermission
import com.geeksville.mesh.android.hasNotificationPermission
import com.geeksville.mesh.android.permissionMissing
import com.geeksville.mesh.android.rationaleDialog
import com.geeksville.mesh.android.shouldShowRequestPermissionRationale
2022-09-04 22:52:40 -03:00
import com.geeksville.mesh.concurrent.handledLaunch
import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.BluetoothViewModel
2021-03-02 15:12:57 +08:00
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.toChannelSet
import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.service.MeshServiceNotifications
import com.geeksville.mesh.service.ServiceRepository
import com.geeksville.mesh.service.startService
import com.geeksville.mesh.ui.ChannelFragment
import com.geeksville.mesh.ui.ContactsFragment
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.navigateToMessages
import com.geeksville.mesh.ui.navigateToNavGraph
import com.geeksville.mesh.ui.theme.AppTheme
2022-09-04 22:52:40 -03:00
import com.geeksville.mesh.util.Exceptions
2022-12-10 11:03:14 -03:00
import com.geeksville.mesh.util.LanguageUtils
import com.geeksville.mesh.util.getPackageInfoCompat
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2022-01-24 14:56:17 -03:00
import com.google.android.material.snackbar.Snackbar
import com.google.android.material.tabs.TabLayout
2020-04-07 09:36:12 -07:00
import com.google.android.material.tabs.TabLayoutMediator
import dagger.hilt.android.AndroidEntryPoint
2022-02-03 18:15:06 -08:00
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import java.text.DateFormat
2022-10-16 19:16:33 -03:00
import java.util.Date
import javax.inject.Inject
2021-03-02 13:22:55 +08:00
/*
UI design
material setup instructions: https://material.io/develop/android/docs/getting-started/
dark theme (or use system eventually) https://material.io/develop/android/theming/dark/
NavDrawer is a standard draw which can be dragged in from the left or the menu icon inside the app
title.
Fragments:
2020-02-16 18:54:29 -08:00
SettingsFragment shows "Settings"
username
shortname
bluetooth pairing list
(eventually misc device settings that are not channel related)
2020-02-16 18:54:29 -08:00
Channel fragment "Channel"
qr code, copy link button
ch number
misc other settings
(eventually a way of choosing between past channels)
2020-02-16 18:54:29 -08:00
ChatFragment "Messages"
a text box to enter new texts
a scrolling list of rows. each row is a text and a sender info layout
2020-02-16 18:54:29 -08:00
NodeListFragment "Users"
a node info row for every node
ViewModels:
BTScanModel starts/stops bt scan and provides list of devices (manages entire scan lifecycle)
MeshModel contains: (manages entire service relationship)
current received texts
current radio macaddr
current node infos (updated dynamically)
eventually use bottom navigation bar to switch between, Members, Chat, Channel, Settings. https://material.io/develop/android/components/bottom-navigation-view/
use numbers of # chat messages and # of members in the badges.
(per this recommendation to not use top tabs: https://ux.stackexchange.com/questions/102439/android-ux-when-to-use-bottom-navigation-and-when-to-use-tabs )
eventually:
make a custom theme: https://github.com/material-components/material-components-android/tree/master/material-theme-builder
*/
@AndroidEntryPoint
2022-12-10 11:03:14 -03:00
class MainActivity : AppCompatActivity(), Logging {
2020-01-21 09:37:39 -08:00
private lateinit var binding: ActivityMainBinding
// Used to schedule a coroutine in the GUI thread
private val mainScope = CoroutineScope(Dispatchers.Main + Job())
2020-02-09 07:28:24 -08:00
private val bluetoothViewModel: BluetoothViewModel by viewModels()
private val model: UIViewModel by viewModels()
2020-04-08 08:16:06 -07:00
@Inject
internal lateinit var serviceRepository: ServiceRepository
private val bluetoothPermissionsLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
if (result.entries.all { it.value }) {
info("Bluetooth permissions granted")
} else {
warn("Bluetooth permissions denied")
2022-11-04 18:31:18 -03:00
showSnackbar(permissionMissing)
2022-09-03 11:07:10 -03:00
}
2022-11-04 18:31:18 -03:00
requestedEnable = false
2022-09-03 11:07:10 -03:00
bluetoothViewModel.permissionsUpdated()
}
private val notificationPermissionsLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { result ->
if (result.entries.all { it.value }) {
info("Notification permissions granted")
} else {
warn("Notification permissions denied")
showSnackbar(getString(R.string.notification_denied), Snackbar.LENGTH_SHORT)
}
}
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
2020-04-07 09:36:12 -07:00
}
2020-02-14 07:47:20 -08:00
override fun onCreate(savedInstanceState: Bundle?) {
2022-02-04 00:57:27 -03:00
installSplashScreen()
2020-02-14 07:47:20 -08:00
super.onCreate(savedInstanceState)
if (savedInstanceState == null) {
val prefs = UIViewModel.getPreferences(this)
// First run: migrate in-app language prefs to appcompat
val lang = prefs.getString("lang", LanguageUtils.SYSTEM_DEFAULT)
if (lang != LanguageUtils.SYSTEM_MANAGED) LanguageUtils.migrateLanguagePrefs(prefs)
info("in-app language is ${LanguageUtils.getLocale()}")
// Set theme
AppCompatDelegate.setDefaultNightMode(
prefs.getInt("theme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
)
// First run: show AppIntroduction
if (!prefs.getBoolean("app_intro_completed", false)) {
startActivity(Intent(this, AppIntroduction::class.java))
}
// Ask user to rate in play store
(application as GeeksvilleApplication).askToRate(this)
2022-12-10 11:03:14 -03:00
}
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
2020-04-07 09:36:12 -07:00
2020-06-18 23:05:33 -04:00
initToolbar()
binding.pager.adapter = tabsAdapter
binding.pager.isUserInputEnabled =
false // Gestures for screen switching doesn't work so good with the map view
2020-04-09 11:03:17 -07:00
// pager.offscreenPageLimit = 0 // Don't keep any offscreen pages around, because we want to make sure our bluetooth scanning stops
2022-01-02 15:29:27 -03:00
TabLayoutMediator(binding.tabLayout, binding.pager, false, false) { tab, position ->
2020-04-07 12:13:50 -07:00
// tab.text = tabInfos[position].text // I think it looks better with icons only
tab.icon = ContextCompat.getDrawable(this, tabInfos[position].icon)
2020-04-07 09:36:12 -07:00
}.attach()
2020-04-08 11:57:31 -07:00
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 {
if (connState.isConnected()) {
if (requestChannelSet != null) {
ScannedQrCodeDialog(
channels = channels,
incoming = requestChannelSet!!,
onDismiss = model::clearRequestChannelUrl,
onConfirm = model::setChannels,
)
}
}
}
}
// Handle any intent
handleIntent(intent)
}
2020-06-18 23:05:33 -04:00
private fun initToolbar() {
2022-10-16 19:16:33 -03:00
val toolbar = binding.toolbar as Toolbar
2020-06-18 23:05:33 -04:00
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
}
2020-06-10 21:50:34 -04:00
private fun updateConnectionStatusImage(connected: MeshService.ConnectionState) {
if (model.actionBarMenu == null) return
2020-06-18 23:05:33 -04:00
2020-06-10 21:50:34 -04:00
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
2020-06-10 21:50:34 -04:00
}
2020-07-17 17:06:29 -04:00
val item = model.actionBarMenu?.findItem(R.id.connectStatusImage)
2020-06-18 23:05:33 -04:00
if (item != null) {
item.setIcon(image)
item.setTitle(tooltip)
}
2020-06-10 21:50:34 -04:00
}
override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
handleIntent(intent)
}
// Handle any intents that were passed into us
private fun handleIntent(intent: Intent) {
val appLinkAction = intent.action
val appLinkData: Uri? = intent.data
when (appLinkAction) {
Intent.ACTION_VIEW -> {
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")
try {
appLinkData?.let { model.requestChannelSet(it.toChannelSet()) }
} catch (ex: Throwable) {
errormsg("Channel url error: ${ex.message}")
showSnackbar("${getString(R.string.channel_invalid)}: ${ex.message}")
}
// We now wait for the device to connect, once connected, we ask the user if they want to switch to the new channel
}
MeshServiceNotifications.OPEN_MESSAGE_ACTION -> {
val contactKey =
intent.getStringExtra(MeshServiceNotifications.OPEN_MESSAGE_EXTRA_CONTACT_KEY)
val contactName =
intent.getStringExtra(MeshServiceNotifications.OPEN_MESSAGE_EXTRA_CONTACT_NAME)
showMessages(contactKey, contactName)
}
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
showSettingsPage()
}
2020-06-08 14:04:56 -07:00
Intent.ACTION_MAIN -> {
}
else -> {
warn("Unexpected action $appLinkAction")
}
2020-06-08 14:04:56 -07:00
}
2020-02-14 07:47:20 -08:00
}
2020-01-21 09:37:39 -08:00
2022-05-16 23:32:49 -03:00
private var requestedEnable = false
private val bleRequestEnable = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
) {
requestedEnable = false
}
2022-08-30 22:20:44 -03:00
private val createDocumentLauncher = registerForActivityResult(
ActivityResultContracts.StartActivityForResult()
2020-04-07 10:40:01 -07:00
) {
2022-08-30 22:20:44 -03:00
if (it.resultCode == Activity.RESULT_OK) {
it.data?.data?.let { file_uri -> model.saveMessagesCSV(file_uri) }
2020-01-20 15:53:22 -08:00
}
2020-02-14 07:47:20 -08:00
}
2020-02-09 05:52:17 -08:00
2022-08-30 22:20:44 -03:00
override fun onDestroy() {
mainScope.cancel("Activity going away")
super.onDestroy()
2020-02-14 07:47:20 -08:00
}
2021-03-02 15:12:57 +08:00
/** Show an alert that may contain HTML */
private fun showAlert(titleText: Int, messageText: Int) {
2021-03-02 15:12:57 +08:00
// make links clickable per https://stackoverflow.com/a/62642807
// val messageStr = getText(messageText)
val builder = MaterialAlertDialogBuilder(this)
.setCancelable(false)
2021-03-02 15:12:57 +08:00
.setTitle(titleText)
.setMessage(messageText)
2021-03-02 16:27:34 +08:00
.setPositiveButton(R.string.okay) { _, _ ->
2021-03-02 15:12:57 +08:00
info("User acknowledged")
}
val dialog = builder.show()
// Make the textview clickable. Must be called after show()
val view = (dialog.findViewById(android.R.id.message) as TextView?)!!
// Linkify.addLinks(view, Linkify.ALL) // not needed with this method
view.movementMethod = LinkMovementMethod.getInstance()
showSettingsPage() // Default to the settings page in this case
}
// Called when we gain/lose a connection to our mesh radio
2022-04-22 17:22:06 -03:00
private fun onMeshConnectionChanged(newConnection: MeshService.ConnectionState) {
if (newConnection == MeshService.ConnectionState.CONNECTED) {
serviceRepository.meshService?.let { service ->
try {
2021-03-23 13:21:51 +08:00
val info: MyNodeInfo? = service.myNodeInfo // this can be null
2021-03-23 13:21:51 +08:00
if (info != null) {
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
if (isOld) {
2021-03-23 13:21:51 +08:00
showAlert(R.string.app_too_old, R.string.must_update)
} else {
// If we are already doing an update don't put up a dialog or try to get device info
val isUpdating = service.updateStatus >= 0
if (!isUpdating) {
val curVer = DeviceVersion(info.firmwareVersion ?: "0.0.0")
2021-03-02 13:22:55 +08:00
if (curVer < MeshService.minDeviceVersion) {
showAlert(R.string.firmware_too_old, R.string.firmware_old)
}
2021-03-23 13:21:51 +08:00
}
2021-03-02 15:12:57 +08:00
}
2021-03-02 13:22:55 +08:00
}
} catch (ex: RemoteException) {
warn("Abandoning connect $ex, because we probably just lost device connection")
}
2022-01-03 21:59:30 -03:00
// if provideLocation enabled: Start providing location (from phone GPS) to mesh
if (model.provideLocation.value == true) {
2022-05-20 09:13:59 -03:00
service.startProvideLocation()
}
2020-04-09 12:22:41 -07:00
}
if (!hasNotificationPermission()) {
val notificationPermissions = getNotificationPermissions()
rationaleDialog(
shouldShowRequestPermissionRationale(notificationPermissions),
R.string.notification_required,
getString(R.string.why_notification_required),
) {
notificationPermissionsLauncher.launch(notificationPermissions)
}
}
}
2020-02-14 07:47:20 -08:00
}
2020-02-09 05:52:17 -08:00
2022-02-03 02:16:31 -03:00
private fun showSnackbar(msgId: Int) {
2022-02-12 18:54:10 -03:00
try {
2022-10-16 19:16:33 -03:00
Snackbar.make(binding.root, msgId, Snackbar.LENGTH_LONG).show()
2022-02-12 18:54:10 -03:00
} catch (ex: IllegalStateException) {
2022-04-08 18:37:22 -03:00
errormsg("Snackbar couldn't find view for msgId $msgId")
2022-02-12 18:54:10 -03:00
}
}
private fun showSnackbar(msg: String, duration: Int = Snackbar.LENGTH_INDEFINITE) {
2022-02-12 18:54:10 -03:00
try {
Snackbar.make(binding.root, msg, duration)
2022-02-12 18:54:10 -03:00
.apply { view.findViewById<TextView>(R.id.snackbar_text).isSingleLine = false }
.setAction(R.string.okay) {
// dismiss
}
.show()
} catch (ex: IllegalStateException) {
2022-04-08 18:37:22 -03:00
errormsg("Snackbar couldn't find view for msgString $msg")
2022-02-12 18:54:10 -03:00
}
}
2020-03-05 09:50:33 -08:00
override fun dispatchTouchEvent(ev: MotionEvent?): Boolean {
return try {
super.dispatchTouchEvent(ev)
} catch (ex: Throwable) {
2020-03-05 09:50:33 -08:00
Exceptions.report(
ex,
"dispatchTouchEvent"
) // hide this Compose error from the user but report to the mothership
false
}
}
private var connectionJob: Job? = null
2022-01-03 21:59:30 -03:00
private val mesh = object :
2022-01-31 21:55:24 -03:00
ServiceClient<IMeshService>({
IMeshService.Stub.asInterface(it)
2020-04-07 10:40:01 -07:00
}) {
2022-01-31 21:55:24 -03:00
override fun onConnected(service: IMeshService) {
connectionJob = mainScope.handledLaunch {
serviceRepository.setMeshService(service)
try {
val connectionState =
MeshService.ConnectionState.valueOf(service.connectionState())
// We won't receive a notify for the initial state of connection, so we force an update here
onMeshConnectionChanged(connectionState)
} catch (ex: RemoteException) {
errormsg("Device error during init ${ex.message}")
} finally {
2021-03-04 09:08:29 +08:00
connectionJob = null
}
2022-04-28 11:54:04 -03:00
debug("connected to mesh service, connectionState=${model.connectionState.value}")
}
2020-02-25 08:10:23 -08:00
}
2020-02-14 07:47:20 -08:00
2020-02-25 08:10:23 -08:00
override fun onDisconnected() {
serviceRepository.setMeshService(null)
2020-01-23 08:09:50 -08:00
}
2020-02-14 07:47:20 -08:00
}
2020-01-23 08:09:50 -08:00
private fun bindMeshService() {
2020-02-14 07:47:20 -08:00
debug("Binding to mesh service!")
// we bind using the well known name, to make sure 3rd party apps could also
if (serviceRepository.meshService != null) {
/* 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!
*/
Exceptions.reportError("meshService was supposed to be null, ignoring (but reporting a bug)")
}
2020-07-02 09:53:52 -07:00
try {
MeshService.startService(this) // Start the service so it stays running even after we unbind
} catch (ex: Exception) {
// 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
errormsg("Failed to start service from activity - but ignoring because bind will work ${ex.message}")
}
// ALSO bind so we can use the api
mesh.connect(
this,
MeshService.createIntent(),
Context.BIND_AUTO_CREATE + Context.BIND_ABOVE_CLIENT
)
2020-02-14 07:47:20 -08:00
}
2020-01-23 08:09:50 -08:00
private fun unbindMeshService() {
2020-02-14 07:47:20 -08:00
// If we have received the service, and hence registered with
// it, then now is the time to unregister.
// if we never connected, do nothing
2020-04-19 16:24:47 -07:00
debug("Unbinding from mesh service!")
connectionJob?.let { job ->
connectionJob = null
warn("We had a pending onConnection job, so we are cancelling it")
job.cancel("unbinding")
}
2020-04-19 16:24:47 -07:00
mesh.close()
serviceRepository.setMeshService(null)
2020-02-14 07:47:20 -08:00
}
2020-01-23 08:09:50 -08:00
2020-02-18 09:09:49 -08:00
override fun onStop() {
2020-02-14 07:47:20 -08:00
unbindMeshService()
2020-02-18 09:09:49 -08:00
super.onStop()
2020-02-14 07:47:20 -08:00
}
2020-01-23 08:09:50 -08:00
2020-02-18 09:09:49 -08:00
override fun onStart() {
super.onStart()
2020-01-23 08:09:50 -08:00
model.connectionState.asLiveData().observe(this) { state ->
onMeshConnectionChanged(state)
updateConnectionStatusImage(state)
}
bluetoothViewModel.enabled.observe(this) { enabled ->
if (!enabled && !requestedEnable && model.selectedBluetooth) {
2022-11-04 18:31:18 -03:00
requestedEnable = true
if (hasBluetoothPermission()) {
2022-05-16 23:32:49 -03:00
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
bleRequestEnable.launch(enableBtIntent)
2022-11-04 18:31:18 -03:00
} else {
val bluetoothPermissions = getBluetoothPermissions()
rationaleDialog(shouldShowRequestPermissionRationale(bluetoothPermissions)) {
bluetoothPermissionsLauncher.launch(bluetoothPermissions)
}
}
}
}
// 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 ->
MaterialAlertDialogBuilder(this)
.setCancelable(false)
.setTitle(R.string.traceroute)
.setMessage(response ?: return@observe)
.setPositiveButton(R.string.okay) { _, _ -> }
.show()
model.clearTracerouteResponse()
}
2021-03-17 15:53:08 +08:00
try {
bindMeshService()
2021-03-23 13:21:51 +08:00
} catch (ex: BindFailedException) {
2021-03-17 15:53:08 +08:00
// App is probably shutting down, ignore
errormsg("Bind of MeshService failed")
}
2021-03-23 13:21:51 +08:00
val bonded = model.bondedAddress != null
if (!bonded) showSettingsPage()
}
private fun showSettingsPage() {
binding.pager.currentItem = 5
2020-02-14 07:47:20 -08:00
}
2020-01-23 08:09:50 -08:00
private fun showMessages(contactKey: String?, contactName: String?) {
model.setCurrentTab(0)
if (contactKey != null && contactName != null) {
supportFragmentManager.navigateToMessages(contactKey, contactName)
}
}
2020-02-14 07:47:20 -08:00
override fun onCreateOptionsMenu(menu: Menu): Boolean {
// Inflate the menu; this adds items to the action bar if it is present.
menuInflater.inflate(R.menu.menu_main, menu)
2020-07-17 17:06:29 -04:00
model.actionBarMenu = menu
updateConnectionStatusImage(model.connectionState.value)
2020-07-17 17:06:29 -04:00
2020-02-14 07:47:20 -08:00
return true
}
2020-01-20 15:53:22 -08:00
private val handler: Handler by lazy {
2022-04-03 11:25:50 -03:00
Handler(Looper.getMainLooper())
}
override fun onPrepareOptionsMenu(menu: Menu): Boolean {
2021-03-02 13:22:55 +08:00
menu.findItem(R.id.stress_test).isVisible =
BuildConfig.DEBUG // only show stress test for debug builds (for now)
2023-05-13 18:18:49 -03:00
menu.findItem(R.id.radio_config).isEnabled = !model.isManaged
return super.onPrepareOptionsMenu(menu)
}
2020-02-14 07:47:20 -08:00
override fun onOptionsItemSelected(item: MenuItem): Boolean {
// Handle action bar item clicks here. The action bar will
// automatically handle clicks on the Home/Up button, so long
// as you specify a parent activity in AndroidManifest.xml.
return when (item.itemId) {
2020-06-18 23:05:33 -04:00
R.id.about -> {
getVersionInfo()
return true
}
R.id.connectStatusImage -> {
Toast.makeText(applicationContext, item.title, Toast.LENGTH_SHORT).show()
2020-06-18 23:05:33 -04:00
return true
}
2020-09-23 22:47:45 -04:00
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() {
2021-02-10 21:36:45 -08:00
// Send ping message and arrange delayed recursion.
debug("Sending ping")
val str = "Ping " + DateFormat.getTimeInstance(DateFormat.MEDIUM)
2021-02-10 21:36:45 -08:00
.format(Date(System.currentTimeMillis()))
2022-09-15 22:24:04 -03:00
model.sendMessage(str)
2022-01-31 21:55:24 -03:00
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()
2022-11-22 22:01:37 -03:00
return true
}
R.id.save_messages_csv -> {
val intent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply {
addCategory(Intent.CATEGORY_OPENABLE)
type = "application/csv"
putExtra(Intent.EXTRA_TITLE, "rangetest.csv")
}
2022-08-30 22:20:44 -03:00
createDocumentLauncher.launch(intent)
return true
}
2021-04-11 12:10:17 +02:00
R.id.theme -> {
chooseThemeDialog()
return true
}
2022-03-30 23:14:02 +02:00
R.id.preferences_language -> {
chooseLangDialog()
return true
}
R.id.show_intro -> {
startActivity(Intent(this, AppIntroduction::class.java))
return true
2022-08-24 12:16:57 -04:00
}
2022-08-10 17:26:15 +01:00
R.id.preferences_quick_chat -> {
2022-08-09 15:26:52 +01:00
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
}
2020-02-14 07:47:20 -08:00
else -> super.onOptionsItemSelected(item)
2020-01-20 15:53:22 -08:00
}
}
2020-06-18 23:05:33 -04:00
private fun getVersionInfo() {
try {
val packageInfo: PackageInfo = packageManager.getPackageInfoCompat(packageName, 0)
2020-06-18 23:05:33 -04:00
val versionName = packageInfo.versionName
2022-02-03 02:16:31 -03:00
Toast.makeText(this, versionName, Toast.LENGTH_LONG).show()
2020-06-18 23:05:33 -04:00
} catch (e: PackageManager.NameNotFoundException) {
errormsg("Can not find the version: ${e.message}")
}
}
// Theme functions
2021-04-11 12:10:17 +02:00
private fun chooseThemeDialog() {
// Prepare dialog and its items
2022-02-05 19:43:56 -03:00
val builder = MaterialAlertDialogBuilder(this)
2022-12-28 17:37:25 -03:00
builder.setTitle(getString(R.string.choose_theme))
2021-04-11 12:10:17 +02:00
val styles = mapOf(
getString(R.string.theme_light) to AppCompatDelegate.MODE_NIGHT_NO,
getString(R.string.theme_dark) to AppCompatDelegate.MODE_NIGHT_YES,
getString(R.string.theme_system) to AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM
)
2021-04-11 12:10:17 +02:00
// Load preferences and its value
2021-04-11 12:10:17 +02:00
val prefs = UIViewModel.getPreferences(this)
val theme = prefs.getInt("theme", AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
debug("Theme from prefs: $theme")
2021-04-11 12:10:17 +02:00
builder.setSingleChoiceItems(
styles.keys.toTypedArray(),
styles.values.indexOf(theme)
) { dialog, position ->
val selectedTheme = styles.values.elementAt(position)
debug("Set theme pref to $selectedTheme")
prefs.edit().putInt("theme", selectedTheme).apply()
AppCompatDelegate.setDefaultNightMode(selectedTheme)
dialog.dismiss()
2021-04-11 12:10:17 +02:00
}
val dialog = builder.create()
dialog.show()
}
2022-03-30 23:14:02 +02:00
private fun chooseLangDialog() {
// Prepare dialog and its items
2022-03-30 23:14:02 +02:00
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(getString(R.string.preferences_language))
2022-12-10 11:03:14 -03:00
val languageTags = LanguageUtils.getLanguageTags(this)
2022-03-30 23:14:02 +02:00
// Load preferences and its value
2022-12-10 11:03:14 -03:00
val lang = LanguageUtils.getLocale()
2022-03-30 23:14:02 +02:00
debug("Lang from prefs: $lang")
2022-08-24 12:16:57 -04:00
builder.setSingleChoiceItems(
languageTags.keys.toTypedArray(),
languageTags.values.indexOf(lang)
) { dialog, position ->
val selectedLang = languageTags.values.elementAt(position)
debug("Set lang pref to $selectedLang")
2022-12-10 11:03:14 -03:00
LanguageUtils.setLocale(selectedLang)
dialog.dismiss()
}
val dialog = builder.create()
dialog.show()
}
2020-02-14 07:47:20 -08:00
}