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
2020-04-18 16:30:30 -07:00
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
2020-03-02 08:54:57 -08:00
import android.net.Uri
2022-04-08 11:34:44 -07:00
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
2020-09-11 16:19:23 -07:00
import com.geeksville.concurrent.handledLaunch
2022-01-31 21:19:54 -03:00
import com.geeksville.mesh.android.*
2020-12-07 20:33:29 +08:00
import com.geeksville.mesh.databinding.ActivityMainBinding
2022-02-26 22:59:20 -08:00
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
2020-04-08 09:53:04 -07:00
import com.geeksville.mesh.model.UIViewModel
2022-04-22 10:22:03 -07:00
import com.geeksville.mesh.repository.radio.RadioInterfaceService
import com.geeksville.mesh.repository.radio.SerialInterface
2020-02-10 15:31:56 -08:00
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
2020-04-09 17:06:41 -07:00
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
2022-02-08 13:50:21 -08:00
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
2021-02-10 21:34:26 -08:00
import java.text.DateFormat
import java.util.*
2022-04-08 11:34:44 -07:00
import javax.inject.Inject
2021-03-02 13:22:55 +08:00
2020-02-16 14:22:24 -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 "
2020-02-16 14:22:24 -08:00
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 "
2020-02-16 14:22:24 -08:00
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 "
2020-02-16 14:22:24 -08:00
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 "
2020-02-16 14:22:24 -08:00
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 " )
2020-02-17 15:39:49 -08:00
2022-02-08 13:50:21 -08:00
@AndroidEntryPoint
2022-03-30 23:14:02 +02:00
class MainActivity : BaseActivity ( ) , Logging ,
2020-02-10 20:17:42 -08:00
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
2021-03-17 21:00:01 -07:00
const val CREATE _CSV _FILE = 14
2020-01-21 09:37:39 -08:00
}
2020-12-07 20:33:29 +08:00
private lateinit var binding : ActivityMainBinding
2020-09-11 16:19:23 -07:00
// Used to schedule a coroutine in the GUI thread
private val mainScope = CoroutineScope ( Dispatchers . Main + Job ( ) )
2020-02-09 07:28:24 -08:00
2022-02-27 11:35:22 -08:00
private val bluetoothViewModel : BluetoothViewModel by viewModels ( )
2022-05-16 23:32:49 -03:00
private val scanModel : BTScanModel by viewModels ( )
2020-04-20 07:46:06 -07:00
val model : UIViewModel by viewModels ( )
2020-04-08 08:16:06 -07:00
2022-04-08 11:34:44 -07:00
@Inject
2022-05-20 14:26:03 -07:00
internal lateinit var radioInterfaceService : RadioInterfaceService
2022-04-08 11:34:44 -07:00
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
}
}
2021-06-10 10:58:45 -07:00
/ * * Get the minimum permissions our app needs to run correctly
2020-04-23 08:52:25 -07:00
* /
2021-06-10 10:58:45 -07:00
private fun getMinimumPermissions ( ) : List < String > {
2020-02-14 09:09:40 -08:00
val perms = mutableListOf (
2020-04-19 20:03:38 -07:00
Manifest . permission . WAKE _LOCK
2020-04-20 07:46:06 -07:00
2020-04-12 17:13:13 -07:00
// 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
)
2022-02-09 22:10:25 -03: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 )
}
2022-02-09 22:10:25 -03:00
* /
perms . add ( Manifest . permission . BLUETOOTH )
2022-01-19 01:06:38 -03:00
2020-04-19 20:03:38 -07: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 )
}
2020-06-12 11:17:52 -07:00
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 */
2022-01-26 14:43:32 -03:00
fun requestLocationPermission ( ) = requestPermission ( getLocationPermissions ( ) )
2021-12-15 09:04:44 -03:00
2021-06-10 10:58:45 -07:00
/** Ask the user to grant background location permission */
2022-01-26 14:43:32 -03:00
fun requestBackgroundPermission ( ) = requestPermission ( getBackgroundPermissions ( ) )
2021-06-10 10:58:45 -07:00
2021-06-23 11:40:15 -07:00
/ * *
* @return a localized string warning user about missing permissions . Or null if everything is find
* /
2022-01-31 21:19:54 -03:00
@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 {
2021-07-26 16:18:40 -07:00
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
}
2021-07-26 16:18:40 -07:00
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 )
}
}
2021-06-10 10:58:45 -07:00
/ * * Possibly prompt user to grant permissions
2022-01-31 21:19:54 -03:00
* @param shouldShowDialog usually false in cases where we ' ve already shown a dialog elsewhere we skip it .
2021-06-10 10:58:45 -07:00
*
* @return true if we already have the needed permissions
* /
2022-01-25 18:14:10 -03:00
private fun requestPermission (
2021-07-26 16:18:40 -07:00
missingPerms : List < String > = getMinimumPermissions ( ) ,
2022-01-26 14:43:32 -03:00
shouldShowDialog : Boolean = false
2021-07-26 16:18:40 -07:00
) : 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
2021-06-10 10:58:45 -07:00
false
2020-01-21 13:12:01 -08:00
} else {
// Permission has already been granted
2021-06-10 10:58:45 -07:00
debug ( " We have our required permissions " )
true
2020-01-21 13:12:01 -08:00
}
2020-01-23 08:09:50 -08:00
2020-06-12 11:17:52 -07:00
/ * *
* Remind user he ' s disabled permissions we need
2021-05-10 08:09:42 +08:00
*
* @return true if we did warn
2020-06-12 11:17:52 -07:00
* /
2021-06-10 10:58:45 -07:00
@SuppressLint ( " InlinedApi " ) // This function is careful to work with old APIs correctly
2021-05-10 08:09:42 +08:00
fun warnMissingPermissions ( ) : Boolean {
2021-06-23 11:40:15 -07:00
val message = getMissingMessage ( )
2020-04-19 09:48:12 -07:00
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 )
2021-05-10 08:09:42 +08:00
true
} else
false
2020-06-12 11:17:52 -07:00
}
override fun onRequestPermissionsResult (
requestCode : Int ,
permissions : Array < out String > ,
grantResults : IntArray
) {
super . onRequestPermissionsResult ( requestCode , permissions , grantResults )
2021-07-26 16:18:40 -07:00
when ( requestCode ) {
2021-06-23 11:56:29 -07:00
DID _REQUEST _PERM -> {
// If request is cancelled, the result arrays are empty.
if ( ( grantResults . isNotEmpty ( ) &&
2021-07-26 16:18:40 -07:00
grantResults [ 0 ] == PackageManager . PERMISSION _GRANTED )
) {
2021-06-23 11:56:29 -07:00
// 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
}
}
2022-02-27 11:35:22 -08:00
bluetoothViewModel . permissionsUpdated ( )
2020-02-10 20:17:42 -08:00
}
2020-02-14 07:47:20 -08:00
private fun sendTestPackets ( ) {
exceptionReporter {
2020-04-08 09:53:04 -07:00
val m = model . meshService !!
2020-02-14 07:47:20 -08:00
// Do some test operations
val testPayload = " hello world " . toByteArray ( )
2020-05-30 15:48:50 -07:00
m . send (
DataPacket (
" +16508675310 " ,
testPayload ,
2020-12-07 19:50:06 +08:00
Portnums . PortNum . PRIVATE _APP _VALUE
2020-05-30 15:48:50 -07:00
)
2020-02-14 07:47:20 -08:00
)
2020-05-30 15:48:50 -07:00
m . send (
DataPacket (
" +16508675310 " ,
testPayload ,
2020-12-07 19:50:06 +08:00
Portnums . PortNum . TEXT _MESSAGE _APP _VALUE
2020-05-30 15:48:50 -07:00
)
2020-02-14 07:47:20 -08:00
)
}
}
2020-02-10 20:17:42 -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
2020-07-08 08:29:53 -07:00
2020-07-14 22:21:54 -07:00
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
2020-07-14 22:21:54 -07:00
. 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
}
}
2020-04-20 09:56:38 -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 )
2020-12-07 20:33:29 +08:00
binding = ActivityMainBinding . inflate ( layoutInflater )
2020-04-08 09:53:04 -07:00
val prefs = UIViewModel . getPreferences ( this )
2022-04-22 17:22:06 -03:00
model . setOwner ( prefs . getString ( " owner " , " " ) )
2020-02-10 20:17:42 -08:00
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-03-02 08:54:57 -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
} * /
2020-12-07 20:33:29 +08:00
setContentView ( binding . root )
2020-04-07 09:36:12 -07:00
2020-06-18 23:05:33 -04:00
initToolbar ( )
2020-12-07 20:33:29 +08:00
binding . pager . adapter = tabsAdapter
binding . pager . isUserInputEnabled =
2020-04-07 12:32:42 -07:00
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
2020-06-09 17:10:49 -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-03-02 08:54:57 -08:00
}
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
}
2020-03-02 08:54:57 -08:00
override fun onNewIntent ( intent : Intent ) {
super . onNewIntent ( intent )
handleIntent ( intent )
}
2020-04-09 17:06:41 -07:00
private var requestedChannelUrl : Uri ? = null
2020-06-11 11:29:54 -07:00
/** We keep the usb device here, so later we can give it to our service */
private var usbDevice : UsbDevice ? = null
2020-03-02 08:54:57 -08:00
/// Handle any itents that were passed into us
private fun handleIntent ( intent : Intent ) {
val appLinkAction = intent . action
val appLinkData : Uri ? = intent . data
2020-06-09 09:10:51 -07:00
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
2020-04-09 17:06:41 -07:00
2020-06-09 09:10:51 -07:00
// if the device is connected already, process it now
2022-05-17 17:29:21 -03:00
perhapsChangeChannel ( )
2020-04-09 17:06:41 -07:00
2020-06-09 09:10:51 -07:00
// We now wait for the device to connect, once connected, we ask the user if they want to switch to the new channel
}
2020-06-09 17:10:49 -07:00
UsbManager . ACTION _USB _DEVICE _ATTACHED -> {
2021-03-31 19:40:33 +08:00
val device : UsbDevice ? = intent . getParcelableExtra ( UsbManager . EXTRA _DEVICE )
if ( device != null ) {
debug ( " Handle USB device attached! $device " )
usbDevice = device
}
2020-06-09 09:10:51 -07:00
}
2020-06-08 14:04:56 -07:00
2020-06-09 09:10:51 -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 ( )
2020-09-11 16:19:23 -07:00
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(...);
2020-04-18 16:30:30 -07:00
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 )
}
2021-03-17 21:00:01 -07:00
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 ) }
2021-03-17 21:00:01 -07:00
}
}
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
}
2020-02-10 20:17:42 -08:00
2022-01-03 21:59:30 -03:00
private var receiverRegistered = false
2020-02-10 20:17:42 -08:00
2020-02-14 07:47:20 -08:00
private fun registerMeshReceiver ( ) {
2020-04-09 13:28:44 -07:00
unregisterMeshReceiver ( )
2020-02-14 07:47:20 -08:00
val filter = IntentFilter ( )
filter . addAction ( MeshService . ACTION _MESH _CONNECTED )
filter . addAction ( MeshService . ACTION _NODE _CHANGE )
2021-01-11 17:15:19 +08:00
filter . addAction ( MeshService . actionReceived ( Portnums . PortNum . TEXT _MESSAGE _APP _VALUE ) )
2020-05-31 11:23:25 -07:00
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-10 20:17:42 -08:00
}
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 ->
2021-01-08 14:51:19 +08:00
2022-04-22 17:22:06 -03:00
model . setConnectionState ( newConnection )
2021-01-08 14:51:19 +08:00
2022-05-26 16:23:47 -03:00
debug ( " Getting latest DeviceConfig from service " )
2021-01-08 14:51:19 +08:00
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-01-08 14:51:19 +08:00
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 {
2021-07-26 16:18:40 -07:00
// 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 ( !is Updating ) {
val curVer = DeviceVersion ( info . firmwareVersion ?: " 0.0.0 " )
2021-03-02 13:22:55 +08:00
2021-07-26 16:18:40 -07: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
2021-07-26 16:18:40 -07: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
}
2021-01-08 14:51:19 +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 )
2021-01-08 14:51:19 +08:00
}
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
}
2021-01-11 17:15:19 +08:00
} else {
2021-01-08 14:51:19 +08:00
// For other connection states, just slam them in
2022-04-22 17:22:06 -03:00
model . setConnectionState ( newConnection )
2021-01-08 14:51:19 +08:00
}
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
}
2021-03-17 15:37:09 +08: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
}
2021-03-17 15:37:09 +08: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
2020-07-18 13:17:30 -07:00
try {
2021-02-27 13:43:55 +08:00
val channels = ChannelSet ( url )
val primary = channels . primaryChannel
2021-03-17 15:37:09 +08:00
if ( primary == null )
2022-02-03 02:16:31 -03:00
showSnackbar ( R . string . channel _invalid )
2021-03-17 15:37:09 +08:00
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
2020-07-18 13:17:30 -07:00
}
2021-03-17 15:37:09 +08:00
. 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 )
2021-03-17 15:37:09 +08:00
}
}
. 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-07-18 13:17:30 -07:00
}
2020-04-09 17:06:41 -07:00
}
}
2020-03-05 09:50:33 -08:00
override fun dispatchTouchEvent ( ev : MotionEvent ? ) : Boolean {
return try {
super . dispatchTouchEvent ( ev )
2020-03-09 18:54:33 -07:00
} 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
}
2020-02-17 15:56:04 -08:00
2021-01-11 17:15:19 +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 =
2020-04-19 11:47:34 -07:00
intent . getParcelableExtra < DataPacket > ( EXTRA _PAYLOAD ) !!
2020-04-07 10:40:01 -07:00
2021-01-11 17:15:19 +08:00
model . messagesState . addMessage ( payload )
2020-02-09 05:52:17 -08:00
}
2020-05-30 19:58:36 -07:00
MeshService . ACTION _MESSAGE _STATUS -> {
debug ( " received message status from service " )
val id = intent . getIntExtra ( EXTRA _PACKET _ID , 0 )
2020-06-07 17:11:30 -07:00
val status = intent . getParcelableExtra < MessageStatus > ( EXTRA _STATUS ) !!
2020-05-30 19:58:36 -07:00
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
2020-09-22 12:52:15 -07: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 ) {
2020-02-10 20:17:42 -08:00
2020-09-11 16:19:23 -07:00
/ *
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 )
* /
2020-09-22 12:52:15 -07:00
connectionJob = mainScope . handledLaunch {
2020-09-11 16:19:23 -07:00
model . meshService = service
2021-01-31 20:53:40 -08:00
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)
2021-02-06 13:24:48 +08:00
val allMsgs = service . oldMessages
2021-01-31 20:53:40 -08:00
val msgs =
2021-02-06 13:24:48 +08:00
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} " )
2021-01-31 20:53:40 -08:00
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 ( )
2020-06-11 11:29:54 -07:00
2020-09-11 16:19:23 -07:00
// 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 ( ) ) )
2021-03-17 15:37:09 +08:00
} finally {
2021-03-04 09:08:29 +08:00
connectionJob = null
}
2020-04-19 12:15:42 -07:00
2022-04-28 11:54:04 -03:00
debug ( " connected to mesh service, connectionState= ${model.connectionState.value} " )
2020-06-09 18:46:23 -07:00
}
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 ( )
2020-04-08 09:53:04 -07:00
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
2020-04-19 19:23:20 -07: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
2020-09-22 12:52:15 -07:00
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 !
* /
2020-03-08 14:47:00 -07:00
Exceptions . reportError ( " meshService was supposed to be null, ignoring (but reporting a bug) " )
2020-09-22 12:52:15 -07:00
}
2020-01-23 09:04:06 -08:00
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} " )
}
2020-05-11 13:12:44 -07:00
// ALSO bind so we can use the api
2021-01-08 14:51:19 +08:00
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
2020-04-19 19:23:20 -07: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! " )
2020-09-22 12:52:15 -07:00
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
2022-02-26 22:59:20 -08:00
bluetoothViewModel . enabled . observe ( this ) { enabled ->
2022-05-16 23:32:49 -03:00
if ( ! enabled && ! requestedEnable ) {
if ( !is InTestLab && scanModel . selectedBluetooth ) {
requestedEnable = true
val enableBtIntent = Intent ( BluetoothAdapter . ACTION _REQUEST _ENABLE )
bleRequestEnable . launch ( enableBtIntent )
2020-06-11 11:29:54 -07:00
}
2022-02-26 22:59:20 -08:00
}
2020-04-20 09:56:38 -07:00
}
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
2022-05-20 14:26:03 -07:00
val bonded = radioInterfaceService . getBondedDeviceAddress ( ) != null
2020-06-11 11:29:54 -07:00
if ( ! bonded && usbDevice == null ) // we will handle USB later
2020-06-09 17:10:49 -07:00
showSettingsPage ( )
}
2020-06-11 11:29:54 -07:00
private fun showSettingsPage ( ) {
2020-12-07 20:33:29 +08:00
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
2021-02-10 21:34:26 -08:00
val handler : Handler by lazy {
2022-04-03 11:25:50 -03:00
Handler ( Looper . getMainLooper ( ) )
2021-02-10 21:34:26 -08:00
}
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)
2021-02-10 21:34:26 -08:00
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 -> {
2020-07-23 23:12:46 -04:00
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
}
2021-02-11 16:29:26 +08:00
R . id . stress _test -> {
2021-02-10 21:34:26 -08:00
fun postPing ( ) {
2021-02-10 21:36:45 -08:00
// Send ping message and arrange delayed recursion.
debug ( " Sending ping " )
2021-02-11 16:54:17 +08:00
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 )
2021-02-10 21:34:26 -08:00
}
2021-02-11 16:29:26 +08:00
item . isChecked = ! item . isChecked // toggle ping test
2021-03-02 13:22:55 +08:00
if ( item . isChecked )
2021-02-11 16:29:26 +08:00
postPing ( )
else
handler . removeCallbacksAndMessages ( null )
2021-02-10 21:34:26 -08:00
return true
}
2021-02-13 21:35:57 -08:00
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
}
2021-03-17 21:00:01 -07:00
R . id . save _messages _csv -> {
val intent = Intent ( Intent . ACTION _CREATE _DOCUMENT ) . apply {
addCategory ( Intent . CATEGORY _OPENABLE )
type = " application/csv "
2022-02-05 19:50:20 -03:00
putExtra ( Intent . EXTRA _TITLE , " rangetest.csv " )
2021-03-17 21:00:01 -07:00
}
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
}
2022-04-22 12:28:04 +02:00
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-03-17 21:00:01 -07:00
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 ) ,
2021-05-10 08:09:42 +08:00
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 ->
2022-04-22 12:28:04 +02:00
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
}