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

1215 lines
47 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
2020-01-21 13:12:01 -08:00
import android.Manifest
import android.annotation.SuppressLint
import android.app.Activity
2020-01-24 12:49:27 -08:00
import android.bluetooth.BluetoothAdapter
2021-04-11 12:10:17 +02:00
import android.content.*
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.UsbDevice
import android.hardware.usb.UsbManager
import android.net.Uri
import android.os.*
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
2020-06-18 23:05:33 -04:00
import android.view.View
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
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
2020-01-21 13:12:01 -08:00
import androidx.core.app.ActivityCompat
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
2020-04-07 09:36:12 -07:00
import androidx.viewpager2.adapter.FragmentStateAdapter
2021-03-17 15:53:08 +08:00
import com.geeksville.android.BindFailedException
2020-04-11 13:20:30 -07:00
import com.geeksville.android.GeeksvilleApplication
2020-01-22 14:27:22 -08:00
import com.geeksville.android.Logging
2020-02-25 08:10:23 -08:00
import com.geeksville.android.ServiceClient
import com.geeksville.concurrent.handledLaunch
import com.geeksville.mesh.android.*
import com.geeksville.mesh.databinding.ActivityMainBinding
import com.geeksville.mesh.model.BluetoothViewModel
2021-02-27 13:43:55 +08:00
import com.geeksville.mesh.model.ChannelSet
2021-03-02 15:12:57 +08:00
import com.geeksville.mesh.model.DeviceVersion
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.radio.SerialInterface
import com.geeksville.mesh.service.*
2020-04-08 18:42:17 -07:00
import com.geeksville.mesh.ui.*
2020-03-05 09:50:33 -08:00
import com.geeksville.util.Exceptions
2020-02-04 21:23:52 -08:00
import com.geeksville.util.exceptionReporter
2020-02-14 07:47:20 -08:00
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
2020-04-11 09:39:34 -07:00
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
2020-02-14 07:47:20 -08:00
import com.google.android.gms.tasks.Task
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2022-01-24 14:56:17 -03:00
import com.google.android.material.snackbar.Snackbar
2020-04-07 09:36:12 -07:00
import com.google.android.material.tabs.TabLayoutMediator
2020-04-11 09:39:34 -07:00
import com.vorlonsoft.android.rate.AppRate
import com.vorlonsoft.android.rate.StoreType
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
2020-02-09 05:52:17 -08:00
import java.nio.charset.Charset
import java.text.DateFormat
import java.util.*
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
*/
2022-05-16 23:32:49 -03:00
val utf8: Charset = Charset.forName("UTF-8")
@AndroidEntryPoint
2022-03-30 23:14:02 +02:00
class MainActivity : BaseActivity(), Logging,
ActivityCompat.OnRequestPermissionsResultCallback {
2020-01-20 15:53:22 -08:00
2020-01-21 09:37:39 -08:00
companion object {
2022-05-16 23:32:49 -03:00
// const val REQUEST_ENABLE_BT = 10
2020-01-21 13:12:01 -08:00
const val DID_REQUEST_PERM = 11
2020-02-14 07:47:20 -08:00
const val RC_SIGN_IN = 12 // google signin completed
2022-05-16 23:32:49 -03:00
// const val SELECT_DEVICE_REQUEST_CODE = 13
const val CREATE_CSV_FILE = 14
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()
2022-05-16 23:32:49 -03:00
private val scanModel: BTScanModel by viewModels()
val model: UIViewModel by viewModels()
2020-04-08 08:16:06 -07:00
@Inject
internal lateinit var radioInterfaceService: RadioInterfaceService
2020-04-07 10:40:01 -07:00
data class TabInfo(val text: String, val icon: Int, val content: Fragment)
// private val tabIndexes = generateSequence(0) { it + 1 } FIXME, instead do withIndex or zip? to get the ids below, also stop duplicating strings
private val tabInfos = arrayOf(
2020-04-08 16:49:27 -07:00
TabInfo(
"Messages",
R.drawable.ic_twotone_message_24,
2022-04-03 11:25:50 -03:00
ContactsFragment()
2020-04-08 16:49:27 -07:00
),
2020-04-08 15:25:57 -07:00
TabInfo(
"Users",
R.drawable.ic_twotone_people_24,
UsersFragment()
),
2020-04-08 18:42:17 -07:00
TabInfo(
"Map",
R.drawable.ic_twotone_map_24,
MapFragment()
),
2020-04-07 10:40:01 -07:00
TabInfo(
"Channel",
R.drawable.ic_twotone_contactless_24,
2020-04-07 16:04:58 -07:00
ChannelFragment()
),
2020-04-07 12:13:50 -07:00
TabInfo(
"Settings",
R.drawable.ic_twotone_settings_applications_24,
2020-04-08 18:42:17 -07:00
SettingsFragment()
)
2020-04-07 10:40:01 -07:00
)
2022-01-03 21:59:30 -03:00
private val tabsAdapter = object : FragmentStateAdapter(this) {
2020-04-07 10:40:01 -07:00
override fun getItemCount(): Int = tabInfos.size
2020-04-07 09:36:12 -07:00
override fun createFragment(position: Int): Fragment {
// Return a NEW fragment instance in createFragment(int)
/*
fragment.arguments = Bundle().apply {
// Our object is just an integer :-P
putInt(ARG_OBJECT, position + 1)
} */
2020-04-07 10:40:01 -07:00
return tabInfos[position].content
2020-04-07 09:36:12 -07:00
}
}
/** Get the minimum permissions our app needs to run correctly
*/
private fun getMinimumPermissions(): List<String> {
2020-02-14 09:09:40 -08:00
val perms = mutableListOf(
Manifest.permission.WAKE_LOCK
// We only need this for logging to capture files for the simulator - turn off for most users
// Manifest.permission.WRITE_EXTERNAL_STORAGE
2020-01-22 16:45:27 -08:00
)
/* TODO - wait for targetSdkVersion 31
2022-01-19 01:06:38 -03:00
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
perms.add(Manifest.permission.BLUETOOTH_CONNECT)
} else {
perms.add(Manifest.permission.BLUETOOTH)
}
*/
perms.add(Manifest.permission.BLUETOOTH)
2022-01-19 01:06:38 -03:00
// Some old phones complain about requesting perms they don't understand
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
perms.add(Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND)
perms.add(Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND)
}
return getMissingPermissions(perms)
}
2022-01-25 18:14:10 -03:00
/** Ask the user to grant Bluetooth scan/discovery permission */
fun requestScanPermission() = requestPermission(getScanPermissions(), true)
2021-12-15 09:04:44 -03:00
/** Ask the user to grant foreground location permission */
fun requestLocationPermission() = requestPermission(getLocationPermissions())
2021-12-15 09:04:44 -03:00
/** Ask the user to grant background location permission */
fun requestBackgroundPermission() = requestPermission(getBackgroundPermissions())
2021-06-23 11:40:15 -07:00
/**
* @return a localized string warning user about missing permissions. Or null if everything is find
*/
@SuppressLint("InlinedApi")
2022-01-25 15:59:45 -03:00
fun getMissingMessage(
missingPerms: List<String> = getMinimumPermissions()
): String? {
2021-06-23 11:40:15 -07:00
val renamedPermissions = mapOf(
// Older versions of android don't know about these permissions - ignore failure to grant
Manifest.permission.ACCESS_COARSE_LOCATION to null,
Manifest.permission.REQUEST_COMPANION_RUN_IN_BACKGROUND to null,
Manifest.permission.REQUEST_COMPANION_USE_DATA_IN_BACKGROUND to null,
2022-01-25 15:59:45 -03:00
Manifest.permission.ACCESS_FINE_LOCATION to getString(R.string.location),
Manifest.permission.BLUETOOTH_CONNECT to "Bluetooth"
2021-06-23 11:40:15 -07:00
)
2022-01-25 15:59:45 -03:00
val deniedPermissions = missingPerms.mapNotNull {
if (renamedPermissions.containsKey(it))
2021-06-23 11:40:15 -07:00
renamedPermissions[it]
else // No localization found - just show the nasty android string
it
}
return if (deniedPermissions.isEmpty())
2021-06-23 11:40:15 -07:00
null
else {
val asEnglish = deniedPermissions.joinToString(" & ")
getString(R.string.permission_missing).format(asEnglish)
}
}
/** Possibly prompt user to grant permissions
* @param shouldShowDialog usually false in cases where we've already shown a dialog elsewhere we skip it.
*
* @return true if we already have the needed permissions
*/
2022-01-25 18:14:10 -03:00
private fun requestPermission(
missingPerms: List<String> = getMinimumPermissions(),
shouldShowDialog: Boolean = false
): Boolean =
2020-01-21 13:12:01 -08:00
if (missingPerms.isNotEmpty()) {
2021-06-23 11:40:15 -07:00
val shouldShow = missingPerms.filter {
ActivityCompat.shouldShowRequestPermissionRationale(this, it)
2020-01-21 13:12:01 -08:00
}
2021-06-23 11:40:15 -07:00
fun doRequest() {
info("requesting permissions")
// Ask for all the missing perms
ActivityCompat.requestPermissions(
this,
missingPerms.toTypedArray(),
DID_REQUEST_PERM
)
}
2021-06-23 12:17:06 -07:00
if (shouldShow.isNotEmpty() && shouldShowDialog) {
2021-06-23 11:40:15 -07:00
// DID_REQUEST_PERM is an
// app-defined int constant. The callback method gets the
// result of the request.
warn("Permissions $shouldShow missing, we should show dialog")
MaterialAlertDialogBuilder(this)
.setTitle(getString(R.string.required_permissions))
2022-01-25 15:59:45 -03:00
.setMessage(getMissingMessage(missingPerms))
2021-12-14 16:43:22 -03:00
.setNeutralButton(R.string.cancel) { _, _ ->
2021-12-14 08:49:23 -03:00
warn("User bailed due to permissions")
2021-06-23 11:40:15 -07:00
}
2021-12-14 16:43:22 -03:00
.setPositiveButton(R.string.accept) { _, _ ->
2021-06-23 11:40:15 -07:00
doRequest()
}
.show()
} else {
info("Permissions $missingPerms missing, no need to show dialog, just asking OS")
doRequest()
}
2020-01-21 13:12:01 -08:00
false
2020-01-21 13:12:01 -08:00
} else {
// Permission has already been granted
debug("We have our required permissions")
true
2020-01-21 13:12:01 -08:00
}
2020-01-23 08:09:50 -08:00
/**
* Remind user he's disabled permissions we need
*
* @return true if we did warn
*/
@SuppressLint("InlinedApi") // This function is careful to work with old APIs correctly
fun warnMissingPermissions(): Boolean {
2021-06-23 11:40:15 -07:00
val message = getMissingMessage()
2021-06-23 11:40:15 -07:00
return if (message != null) {
errormsg("Denied permissions: $message")
2022-02-03 02:16:31 -03:00
showSnackbar(message)
true
} else
false
}
override fun onRequestPermissionsResult(
requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray
) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
when (requestCode) {
DID_REQUEST_PERM -> {
// If request is cancelled, the result arrays are empty.
if ((grantResults.isNotEmpty() &&
grantResults[0] == PackageManager.PERMISSION_GRANTED)
) {
// Permission is granted. Continue the action or workflow
// in your app.
// yay!
} else {
// Explain to the user that the feature is unavailable because
// the features requires a permission that the user has denied.
// At the same time, respect the user's decision. Don't link to
// system settings in an effort to convince the user to change
// their decision.
warnMissingPermissions()
}
}
else -> {
// ignore other requests
}
}
bluetoothViewModel.permissionsUpdated()
}
2020-02-14 07:47:20 -08:00
private fun sendTestPackets() {
exceptionReporter {
val m = model.meshService!!
2020-02-14 07:47:20 -08:00
// Do some test operations
val testPayload = "hello world".toByteArray()
m.send(
DataPacket(
"+16508675310",
testPayload,
2020-12-07 19:50:06 +08:00
Portnums.PortNum.PRIVATE_APP_VALUE
)
2020-02-14 07:47:20 -08:00
)
m.send(
DataPacket(
"+16508675310",
testPayload,
2020-12-07 19:50:06 +08:00
Portnums.PortNum.TEXT_MESSAGE_APP_VALUE
)
2020-02-14 07:47:20 -08:00
)
}
}
2020-04-11 09:39:34 -07:00
/// Ask user to rate in play store
private fun askToRate() {
2020-07-08 07:50:24 -07:00
exceptionReporter { // Got one IllegalArgumentException from inside this lib, but we don't want to crash our app because of bugs in this optional feature
val hasGooglePlay = GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(this) != ConnectionResult.SERVICE_MISSING
val rater = AppRate.with(this)
2020-07-08 07:50:24 -07:00
.setInstallDays(10.toByte()) // default is 10, 0 means install day, 10 means app is launched 10 or more days later than installation
.setLaunchTimes(10.toByte()) // default is 10, 3 means app is launched 3 or more times
.setRemindInterval(1.toByte()) // default is 1, 1 means app is launched 1 or more days after neutral button clicked
.setRemindLaunchesNumber(1.toByte()) // default is 0, 1 means app is launched 1 or more times after neutral button clicked
.setStoreType(if (hasGooglePlay) StoreType.GOOGLEPLAY else StoreType.AMAZON)
rater.monitor() // Monitors the app launch times
2020-07-08 07:50:24 -07:00
// Only ask to rate if the user has a suitable store
2021-03-29 20:33:06 +08:00
AppRate.showRateDialogIfMeetsConditions(this) // Shows the Rate Dialog when conditions are met
2020-04-11 09:39:34 -07:00
}
}
private val isInTestLab: Boolean by lazy {
(application as GeeksvilleApplication).isInTestLab
}
2020-04-11 13:20:30 -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)
binding = ActivityMainBinding.inflate(layoutInflater)
val prefs = UIViewModel.getPreferences(this)
2022-04-22 17:22:06 -03:00
model.setOwner(prefs.getString("owner", ""))
2021-04-11 12:10:17 +02:00
/// Set theme
setUITheme(prefs)
2020-02-17 11:22:47 -08:00
/* not yet working
2020-02-14 07:47:20 -08:00
// Configure sign-in to request the user's ID, email address, and basic
// profile. ID and basic profile are included in DEFAULT_SIGN_IN.
val gso =
GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestEmail()
.build()
2020-01-25 10:00:57 -08:00
2020-02-14 07:47:20 -08:00
// Build a GoogleSignInClient with the options specified by gso.
UIState.googleSignInClient = GoogleSignIn.getClient(this, gso);
2020-02-17 11:22:47 -08:00
*/
2020-04-07 09:36:12 -07:00
/* setContent {
2020-03-17 11:35:19 -07:00
MeshApp()
2020-04-07 09:36:12 -07:00
} */
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
2021-02-21 11:34:43 +08:00
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
2022-04-24 12:12:13 -03:00
model.connectionState.observe(this) { connected ->
2020-06-10 21:50:34 -04:00
updateConnectionStatusImage(connected)
2022-01-31 21:55:24 -03:00
}
2020-04-11 09:39:34 -07:00
// Handle any intent
handleIntent(intent)
2020-04-11 09:39:34 -07:00
askToRate()
2022-01-25 15:59:45 -03:00
// if (!isInTestLab) - very important - even in test lab we must request permissions because we need location perms for some of our tests to pass
requestPermission()
}
2020-06-18 23:05:33 -04:00
private fun initToolbar() {
val toolbar =
findViewById<View>(R.id.toolbar) as Toolbar
setSupportActionBar(toolbar)
supportActionBar?.setDisplayShowTitleEnabled(false)
}
2020-06-10 21:50:34 -04:00
private fun updateConnectionStatusImage(connected: MeshService.ConnectionState) {
2020-07-17 17:06:29 -04:00
if (model.actionBarMenu == null)
2020-06-18 23:05:33 -04:00
return
2020-06-10 21:50:34 -04:00
val (image, tooltip) = when (connected) {
MeshService.ConnectionState.CONNECTED -> Pair(R.drawable.cloud_on, R.string.connected)
MeshService.ConnectionState.DEVICE_SLEEP -> Pair(
R.drawable.ic_twotone_cloud_upload_24,
R.string.device_sleeping
)
MeshService.ConnectionState.DISCONNECTED -> Pair(
R.drawable.cloud_off,
R.string.disconnected
)
}
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)
}
private var requestedChannelUrl: Uri? = null
/** We keep the usb device here, so later we can give it to our service */
private var usbDevice: UsbDevice? = null
/// Handle any itents 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")
requestedChannelUrl = appLinkData
// if the device is connected already, process it now
2022-05-17 17:29:21 -03:00
perhapsChangeChannel()
// We now wait for the device to connect, once connected, we ask the user if they want to switch to the new channel
}
UsbManager.ACTION_USB_DEVICE_ATTACHED -> {
val device: UsbDevice? = intent.getParcelableExtra(UsbManager.EXTRA_DEVICE)
if (device != null) {
debug("Handle USB device attached! $device")
usbDevice = device
}
}
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
}
2020-02-14 07:47:20 -08:00
override fun onDestroy() {
unregisterMeshReceiver()
mainScope.cancel("Activity going away")
2020-02-14 07:47:20 -08:00
super.onDestroy()
}
2020-02-14 04:41:20 -08:00
2020-02-14 07:47:20 -08:00
/**
* Dispatch incoming result to the correct fragment.
*/
2020-04-07 10:40:01 -07:00
override fun onActivityResult(
requestCode: Int,
resultCode: Int,
data: Intent?
) {
2020-02-14 07:47:20 -08:00
super.onActivityResult(requestCode, resultCode, data)
// Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...);
when (requestCode) {
RC_SIGN_IN -> {
// The Task returned from this call is always completed, no need to attach
// a listener.
val task: Task<GoogleSignInAccount> =
GoogleSignIn.getSignedInAccountFromIntent(data)
handleSignInResult(task)
}
CREATE_CSV_FILE -> {
if (resultCode == Activity.RESULT_OK) {
2022-02-03 18:15:06 -08:00
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
2020-02-14 07:47:20 -08:00
private fun handleSignInResult(completedTask: Task<GoogleSignInAccount>) {
2020-02-18 10:40:02 -08:00
/*
2020-02-14 07:47:20 -08:00
try {
2020-02-18 10:40:02 -08:00
val account = completedTask.getResult(ApiException::class.java)
2020-02-14 07:47:20 -08:00
// Signed in successfully, show authenticated UI.
//updateUI(account)
} catch (e: ApiException) { // The ApiException status code indicates the detailed failure reason.
// Please refer to the GoogleSignInStatusCodes class reference for more information.
warn("signInResult:failed code=" + e.statusCode)
//updateUI(null)
2020-02-18 10:40:02 -08:00
} */
2020-02-14 07:47:20 -08:00
}
2022-01-03 21:59:30 -03:00
private var receiverRegistered = false
2020-02-14 07:47:20 -08:00
private fun registerMeshReceiver() {
unregisterMeshReceiver()
2020-02-14 07:47:20 -08:00
val filter = IntentFilter()
filter.addAction(MeshService.ACTION_MESH_CONNECTED)
filter.addAction(MeshService.ACTION_NODE_CHANGE)
filter.addAction(MeshService.actionReceived(Portnums.PortNum.TEXT_MESSAGE_APP_VALUE))
filter.addAction((MeshService.ACTION_MESSAGE_STATUS))
2020-02-14 07:47:20 -08:00
registerReceiver(meshServiceReceiver, filter)
2021-03-29 20:33:06 +08:00
receiverRegistered = true
2020-02-14 07:47:20 -08:00
}
2020-02-09 05:52:17 -08:00
2020-02-14 07:47:20 -08:00
private fun unregisterMeshReceiver() {
if (receiverRegistered) {
receiverRegistered = false
unregisterReceiver(meshServiceReceiver)
}
2020-02-14 07:47:20 -08:00
}
2020-02-09 05:52:17 -08:00
2021-03-02 15:12:57 +08:00
/** Show an alert that may contain HTML */
private fun showAlert(titleText: Int, messageText: Int) {
// make links clickable per https://stackoverflow.com/a/62642807
// val messageStr = getText(messageText)
val builder = MaterialAlertDialogBuilder(this)
.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
}
2020-02-14 07:47:20 -08:00
/// 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) {
2022-04-24 12:12:13 -03:00
val oldConnection = model.connectionState.value!!
2022-04-22 17:22:06 -03:00
debug("connchange $oldConnection -> $newConnection")
2021-01-08 15:19:20 +08:00
2022-04-22 17:22:06 -03:00
if (newConnection == MeshService.ConnectionState.CONNECTED) {
2020-04-09 12:22:41 -07:00
model.meshService?.let { service ->
2022-04-22 17:22:06 -03:00
model.setConnectionState(newConnection)
2022-05-26 16:23:47 -03:00
debug("Getting latest DeviceConfig from service")
try {
2021-03-23 13:21:51 +08:00
val info: MyNodeInfo? = service.myNodeInfo // this can be null
2022-04-22 17:22:06 -03:00
model.setMyNodeInfo(info)
2021-03-23 13:21:51 +08:00
if (info != null) {
val isOld = info.minAppVersion > BuildConfig.VERSION_CODE
if (isOld)
showAlert(R.string.app_too_old, R.string.must_update)
2021-03-02 15:12:57 +08:00
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.minFirmwareVersion)
showAlert(R.string.firmware_too_old, R.string.firmware_old)
else {
2022-05-26 16:23:47 -03:00
// If our app is too old/new, we probably don't understand the new DeviceConfig messages, so we don't read them until here
2021-03-23 13:21:51 +08:00
2022-05-26 16:23:47 -03:00
model.setDeviceConfig(ConfigProtos.Config.parseFrom(service.deviceConfig))
2021-03-02 13:22:55 +08:00
2022-04-22 17:22:06 -03:00
model.setChannels(ChannelSet(AppOnlyProtos.ChannelSet.parseFrom(service.channels)))
2021-03-05 14:14:17 +08:00
2022-04-22 17:22:06 -03:00
model.updateNodesFromDevice()
2021-03-02 13:22:55 +08:00
// we have a connection to our device now, do the channel change
perhapsChangeChannel()
}
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-04-22 17:22:06 -03:00
model.setConnectionState(oldConnection)
}
2022-01-03 21:59:30 -03:00
// if provideLocation enabled: Start providing location (from phone GPS) to mesh
2022-01-25 01:20:31 -03:00
if (model.provideLocation.value == true)
2022-05-20 09:13:59 -03:00
service.startProvideLocation()
2020-04-09 12:22:41 -07:00
}
} else {
// For other connection states, just slam them in
2022-04-22 17:22:06 -03:00
model.setConnectionState(newConnection)
}
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 {
Snackbar.make(
findViewById(android.R.id.content),
msgId,
Snackbar.LENGTH_LONG
).show()
} 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
}
}
2022-02-03 02:16:31 -03:00
private fun showSnackbar(msg: String) {
2022-02-12 18:54:10 -03:00
try {
Snackbar.make(
findViewById(android.R.id.content),
msg,
Snackbar.LENGTH_INDEFINITE
)
.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
}
}
2022-05-17 17:29:21 -03:00
private fun perhapsChangeChannel(url: Uri? = requestedChannelUrl) {
// if the device is connected already, process it now
if (url != null && model.isConnected()) {
requestedChannelUrl = null
try {
2021-02-27 13:43:55 +08:00
val channels = ChannelSet(url)
val primary = channels.primaryChannel
if (primary == null)
2022-02-03 02:16:31 -03:00
showSnackbar(R.string.channel_invalid)
else {
MaterialAlertDialogBuilder(this)
.setTitle(R.string.new_channel_rcvd)
.setMessage(getString(R.string.do_you_want_switch).format(primary.name))
.setNeutralButton(R.string.cancel) { _, _ ->
// Do nothing
}
.setPositiveButton(R.string.accept) { _, _ ->
debug("Setting channel from URL")
try {
model.setChannels(channels)
} catch (ex: RemoteException) {
errormsg("Couldn't change channel ${ex.message}")
2022-02-03 02:16:31 -03:00
showSnackbar(R.string.cant_change_no_radio)
}
}
.show()
}
2022-02-03 02:16:31 -03:00
} catch (ex: Throwable) {
errormsg("Channel url error: ${ex.message}")
showSnackbar("${getString(R.string.channel_invalid)}: ${ex.message}")
}
}
}
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
}
}
2022-01-03 21:59:30 -03:00
private val meshServiceReceiver = object : BroadcastReceiver() {
2020-02-09 05:52:17 -08:00
2020-04-07 10:40:01 -07:00
override fun onReceive(context: Context, intent: Intent) =
exceptionReporter {
debug("Received from mesh service $intent")
2020-02-09 05:52:17 -08:00
2020-04-07 10:40:01 -07:00
when (intent.action) {
MeshService.ACTION_NODE_CHANGE -> {
val info: NodeInfo =
intent.getParcelableExtra(EXTRA_NODEINFO)!!
debug("UI nodechange $info")
2020-02-09 05:52:17 -08:00
2020-04-07 10:40:01 -07:00
// We only care about nodes that have user info
info.user?.id?.let {
2022-04-22 17:22:06 -03:00
val nodes = model.nodeDB.nodes.value!! + Pair(it, info)
model.nodeDB.setNodes(nodes)
2020-04-07 10:40:01 -07:00
}
2020-02-09 05:52:17 -08:00
}
MeshService.actionReceived(Portnums.PortNum.TEXT_MESSAGE_APP_VALUE) -> {
debug("received new message from service")
2020-04-07 10:40:01 -07:00
val payload =
intent.getParcelableExtra<DataPacket>(EXTRA_PAYLOAD)!!
2020-04-07 10:40:01 -07:00
model.messagesState.addMessage(payload)
2020-02-09 05:52:17 -08:00
}
MeshService.ACTION_MESSAGE_STATUS -> {
debug("received message status from service")
val id = intent.getIntExtra(EXTRA_PACKET_ID, 0)
val status = intent.getParcelableExtra<MessageStatus>(EXTRA_STATUS)!!
model.messagesState.updateStatus(id, status)
}
2020-04-07 10:40:01 -07:00
MeshService.ACTION_MESH_CONNECTED -> {
2021-04-02 13:55:41 +08:00
val extra = intent.getStringExtra(EXTRA_CONNECTED)
if (extra != null) {
onMeshConnectionChanged(MeshService.ConnectionState.valueOf(extra))
}
2020-04-07 10:40:01 -07:00
}
else -> TODO()
2020-02-09 05:52:17 -08:00
}
}
2020-02-14 07:47:20 -08:00
}
2020-01-20 15:53:22 -08:00
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) {
/*
Note: we must call this callback in a coroutine. Because apparently there is only a single activity looper thread. and if that onConnected override
also tries to do a service operation we can deadlock.
Old buggy stack trace:
at sun.misc.Unsafe.park (Unsafe.java)
- waiting on an unknown object
at java.util.concurrent.locks.LockSupport.park (LockSupport.java:190)
at java.util.concurrent.locks.AbstractQueuedSynchronizer$ConditionObject.await (AbstractQueuedSynchronizer.java:2067)
at com.geeksville.android.ServiceClient.waitConnect (ServiceClient.java:46)
at com.geeksville.android.ServiceClient.getService (ServiceClient.java:27)
at com.geeksville.mesh.service.MeshService$binder$1$setDeviceAddress$1.invoke (MeshService.java:1519)
at com.geeksville.mesh.service.MeshService$binder$1$setDeviceAddress$1.invoke (MeshService.java:1514)
at com.geeksville.util.ExceptionsKt.toRemoteExceptions (ExceptionsKt.java:56)
at com.geeksville.mesh.service.MeshService$binder$1.setDeviceAddress (MeshService.java:1516)
at com.geeksville.mesh.MainActivity$mesh$1$onConnected$1.invoke (MainActivity.java:743)
at com.geeksville.mesh.MainActivity$mesh$1$onConnected$1.invoke (MainActivity.java:734)
at com.geeksville.util.ExceptionsKt.exceptionReporter (ExceptionsKt.java:34)
at com.geeksville.mesh.MainActivity$mesh$1.onConnected (MainActivity.java:738)
at com.geeksville.mesh.MainActivity$mesh$1.onConnected (MainActivity.java:734)
at com.geeksville.android.ServiceClient$connection$1$onServiceConnected$1.invoke (ServiceClient.java:89)
at com.geeksville.android.ServiceClient$connection$1$onServiceConnected$1.invoke (ServiceClient.java:84)
at com.geeksville.util.ExceptionsKt.exceptionReporter (ExceptionsKt.java:34)
at com.geeksville.android.ServiceClient$connection$1.onServiceConnected (ServiceClient.java:85)
at android.app.LoadedApk$ServiceDispatcher.doConnected (LoadedApk.java:2067)
at android.app.LoadedApk$ServiceDispatcher$RunConnection.run (LoadedApk.java:2099)
at android.os.Handler.handleCallback (Handler.java:883)
at android.os.Handler.dispatchMessage (Handler.java:100)
at android.os.Looper.loop (Looper.java:237)
at android.app.ActivityThread.main (ActivityThread.java:8016)
at java.lang.reflect.Method.invoke (Method.java)
at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run (RuntimeInit.java:493)
at com.android.internal.os.ZygoteInit.main (ZygoteInit.java:1076)
*/
connectionJob = mainScope.handledLaunch {
model.meshService = service
try {
usbDevice?.let { usb ->
debug("Switching to USB radio ${usb.deviceName}")
service.setDeviceAddress(SerialInterface.toInterfaceName(usb.deviceName))
usbDevice =
null // Only switch once - thereafter it should be stored in settings
}
// We don't start listening for packets until after we are connected to the service
registerMeshReceiver()
// Init our messages table with the service's record of past text messages (ignore all other message types)
val allMsgs = service.oldMessages
val msgs =
allMsgs.filter { p -> p.dataType == Portnums.PortNum.TEXT_MESSAGE_APP_VALUE }
2021-03-04 09:08:29 +08:00
2022-04-22 17:22:06 -03:00
model.setMyNodeInfo(service.myNodeInfo) // Note: this could be NULL!
2021-03-04 09:08:29 +08:00
debug("Service provided ${msgs.size} messages and myNodeNum ${model.myNodeInfo.value?.myNodeNum}")
model.messagesState.setMessages(msgs)
val connectionState =
MeshService.ConnectionState.valueOf(service.connectionState())
// if we are not connected, onMeshConnectionChange won't fetch nodes from the service
// in that case, we do it here - because the service certainly has a better idea of node db that we have
if (connectionState != MeshService.ConnectionState.CONNECTED)
2022-04-22 17:22:06 -03:00
model.updateNodesFromDevice()
// We won't receive a notify for the initial state of connection, so we force an update here
onMeshConnectionChanged(connectionState)
} catch (ex: RemoteException) {
// If we get an exception while reading our service config, the device might have gone away, double check to see if we are really connected
errormsg("Device error during init ${ex.message}")
2022-04-22 17:22:06 -03:00
model.setConnectionState(MeshService.ConnectionState.valueOf(service.connectionState()))
} 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() {
2020-02-14 07:47:20 -08:00
unregisterMeshReceiver()
model.meshService = 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 (model.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()
model.meshService = 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
unregisterMeshReceiver() // No point in receiving updates while the GUI is gone, we'll get them when the user launches the activity
unbindMeshService()
2020-01-23 08:09:50 -08:00
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
bluetoothViewModel.enabled.observe(this) { enabled ->
2022-05-16 23:32:49 -03:00
if (!enabled && !requestedEnable) {
if (!isInTestLab && scanModel.selectedBluetooth) {
requestedEnable = true
val enableBtIntent = Intent(BluetoothAdapter.ACTION_REQUEST_ENABLE)
bleRequestEnable.launch(enableBtIntent)
}
}
}
2022-05-17 17:29:21 -03:00
// Call perhapsChangeChannel() whenever [changeChannelUrl] updates with a non-null value
model.requestChannelUrl.observe(this) { url ->
url?.let {
requestedChannelUrl = url
model.clearRequestChannelUrl()
perhapsChangeChannel()
}
}
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 = radioInterfaceService.getBondedDeviceAddress() != null
if (!bonded && usbDevice == null) // we will handle USB later
showSettingsPage()
}
private fun showSettingsPage() {
binding.pager.currentItem = 5
2020-02-14 07:47:20 -08:00
}
2020-01-23 08:09:50 -08:00
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
2022-04-24 12:12:13 -03:00
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
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)
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()))
model.messagesState.sendMessage(str)
2022-01-31 21:55:24 -03:00
handler.postDelayed({ postPing() }, 30000)
}
item.isChecked = !item.isChecked // toggle ping test
2021-03-02 13:22:55 +08:00
if (item.isChecked)
postPing()
else
handler.removeCallbacksAndMessages(null)
return true
}
R.id.advanced_settings -> {
val fragmentManager: FragmentManager = supportFragmentManager
val fragmentTransaction: FragmentTransaction = fragmentManager.beginTransaction()
val nameFragment = AdvancedSettingsFragment()
fragmentTransaction.add(R.id.mainActivityLayout, nameFragment)
fragmentTransaction.addToBackStack(null)
fragmentTransaction.commit()
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")
}
startActivityForResult(intent, CREATE_CSV_FILE)
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.preferences_map_style -> {
chooseMapStyle()
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.getPackageInfo(packageName, 0)
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}")
}
}
2021-04-11 12:10:17 +02:00
/// Theme functions
private fun chooseThemeDialog() {
/// Prepare dialog and its items
2022-02-05 19:43:56 -03:00
val builder = MaterialAlertDialogBuilder(this)
2021-04-11 12:10:17 +02:00
builder.setTitle(getString(R.string.choose_theme_title))
val styles = arrayOf(
getString(R.string.theme_light),
getString(R.string.theme_dark),
getString(R.string.theme_system)
)
2021-04-11 12:10:17 +02:00
/// Load preferences and its value
val prefs = UIViewModel.getPreferences(this)
val editor: SharedPreferences.Editor = prefs.edit()
val checkedItem = prefs.getInt("theme", 2)
builder.setSingleChoiceItems(styles, checkedItem) { dialog, which ->
when (which) {
0 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
editor.putInt("theme", 0)
editor.apply()
delegate.applyDayNight()
dialog.dismiss()
}
1 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
editor.putInt("theme", 1)
editor.apply()
delegate.applyDayNight()
dialog.dismiss()
}
2 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
editor.putInt("theme", 2)
editor.apply()
delegate.applyDayNight()
dialog.dismiss()
}
}
}
val dialog = builder.create()
dialog.show()
}
private fun setUITheme(prefs: SharedPreferences) {
/// Read theme settings from preferences and set it
/// If nothing is found set FOLLOW SYSTEM option
when (prefs.getInt("theme", 2)) {
0 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_NO)
delegate.applyDayNight()
}
1 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_YES)
delegate.applyDayNight()
}
2 -> {
AppCompatDelegate.setDefaultNightMode(AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM)
delegate.applyDayNight()
}
}
}
2022-03-30 23:14:02 +02:00
private fun chooseLangDialog() {
/// Prepare dialog and its items
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(getString(R.string.preferences_language))
val languageLabels by lazy { resources.getStringArray(R.array.language_entries) }
val languageValues by lazy { resources.getStringArray(R.array.language_values) }
/// Load preferences and its value
val prefs = UIViewModel.getPreferences(this)
val editor: SharedPreferences.Editor = prefs.edit()
val lang = prefs.getString("lang", "zz")
debug("Lang from prefs: $lang")
builder.setSingleChoiceItems(languageLabels, languageValues.indexOf(lang)) { dialog, which ->
val selectedLang = languageValues[which]
debug("Set lang pref to $selectedLang")
editor.putString("lang", selectedLang)
editor.apply()
dialog.dismiss()
}
val dialog = builder.create()
dialog.show()
}
private fun chooseMapStyle() {
/// Prepare dialog and its items
val builder = MaterialAlertDialogBuilder(this)
builder.setTitle(getString(R.string.preferences_map_style))
val mapStyles by lazy { resources.getStringArray(R.array.map_styles) }
/// Load preferences and its value
val prefs = UIViewModel.getPreferences(this)
val editor: SharedPreferences.Editor = prefs.edit()
val mapStyleId = prefs.getInt("map_style_id", 1)
debug("mapStyleId from prefs: $mapStyleId")
builder.setSingleChoiceItems(mapStyles, mapStyleId) { dialog, which ->
debug("Set mapStyleId pref to $which")
editor.putInt("map_style_id", which)
2022-03-30 23:14:02 +02:00
editor.apply()
dialog.dismiss()
}
val dialog = builder.create()
dialog.show()
}
2020-02-14 07:47:20 -08:00
}