2020-01-23 05:46:41 +00:00
package com.geeksville.mesh
2020-01-20 23:53:22 +00:00
2020-01-21 21:12:01 +00:00
import android.Manifest
2020-04-18 23:30:30 +00:00
import android.annotation.SuppressLint
import android.app.Activity
2020-01-24 20:49:27 +00:00
import android.bluetooth.BluetoothAdapter
2020-04-18 23:30:30 +00:00
import android.bluetooth.BluetoothDevice
2020-01-24 20:49:27 +00:00
import android.bluetooth.BluetoothManager
2020-04-18 23:30:30 +00:00
import android.companion.CompanionDeviceManager
2021-04-11 10:10:17 +00:00
import android.content.*
2020-06-19 03:05:33 +00:00
import android.content.pm.PackageInfo
2020-01-21 21:12:01 +00:00
import android.content.pm.PackageManager
2020-06-08 21:04:56 +00:00
import android.hardware.usb.UsbDevice
import android.hardware.usb.UsbManager
2020-03-02 16:54:57 +00:00
import android.net.Uri
2020-02-14 17:09:40 +00:00
import android.os.Build
2020-01-20 23:53:22 +00:00
import android.os.Bundle
2021-02-11 05:34:26 +00:00
import android.os.Handler
2020-06-10 01:46:23 +00:00
import android.os.RemoteException
2021-03-02 05:22:55 +00:00
import android.text.method.LinkMovementMethod
2020-04-07 18:27:51 +00:00
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
2020-06-19 03:05:33 +00:00
import android.view.View
2021-03-02 05:22:55 +00:00
import android.widget.TextView
2020-01-22 21:02:24 +00:00
import android.widget.Toast
2020-04-08 15:16:06 +00:00
import androidx.activity.viewModels
2021-04-11 10:10:17 +00:00
import androidx.appcompat.app.AlertDialog
2020-01-24 20:49:27 +00:00
import androidx.appcompat.app.AppCompatActivity
2021-04-11 10:10:17 +00:00
import androidx.appcompat.app.AppCompatDelegate
2020-06-19 03:05:33 +00:00
import androidx.appcompat.widget.Toolbar
2020-01-21 21:12:01 +00:00
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
2020-04-07 16:36:12 +00:00
import androidx.fragment.app.Fragment
2020-09-24 02:47:45 +00:00
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentTransaction
2020-04-08 18:57:31 +00:00
import androidx.lifecycle.Observer
2020-04-07 16:36:12 +00:00
import androidx.viewpager2.adapter.FragmentStateAdapter
2021-03-17 07:53:08 +00:00
import com.geeksville.android.BindFailedException
2020-04-11 20:20:30 +00:00
import com.geeksville.android.GeeksvilleApplication
2020-01-22 22:27:22 +00:00
import com.geeksville.android.Logging
2020-02-25 16:10:23 +00:00
import com.geeksville.android.ServiceClient
2020-09-11 23:19:23 +00:00
import com.geeksville.concurrent.handledLaunch
2021-06-10 17:58:45 +00:00
import com.geeksville.mesh.android.getBackgroundPermissions
2021-11-19 04:20:54 +00:00
import com.geeksville.mesh.android.getCameraPermissions
2021-06-10 17:58:45 +00:00
import com.geeksville.mesh.android.getMissingPermissions
2021-03-18 04:00:01 +00:00
import com.geeksville.mesh.database.entity.Packet
2020-12-07 12:33:29 +00:00
import com.geeksville.mesh.databinding.ActivityMainBinding
2021-02-27 05:43:55 +00:00
import com.geeksville.mesh.model.ChannelSet
2021-03-02 07:12:57 +00:00
import com.geeksville.mesh.model.DeviceVersion
2020-04-08 16:53:04 +00:00
import com.geeksville.mesh.model.UIViewModel
2020-02-10 23:31:56 +00:00
import com.geeksville.mesh.service.*
2020-04-09 01:42:17 +00:00
import com.geeksville.mesh.ui.*
2020-03-05 17:50:33 +00:00
import com.geeksville.util.Exceptions
2020-02-05 05:23:52 +00:00
import com.geeksville.util.exceptionReporter
2020-02-14 15:47:20 +00:00
import com.google.android.gms.auth.api.signin.GoogleSignIn
import com.google.android.gms.auth.api.signin.GoogleSignInAccount
2020-04-11 16:39:34 +00:00
import com.google.android.gms.common.ConnectionResult
import com.google.android.gms.common.GoogleApiAvailability
2020-02-14 15:47:20 +00:00
import com.google.android.gms.tasks.Task
2020-04-10 00:06:41 +00:00
import com.google.android.material.dialog.MaterialAlertDialogBuilder
2020-04-07 16:36:12 +00:00
import com.google.android.material.tabs.TabLayoutMediator
2020-07-18 20:17:30 +00:00
import com.google.protobuf.InvalidProtocolBufferException
2020-04-11 16:39:34 +00:00
import com.vorlonsoft.android.rate.AppRate
import com.vorlonsoft.android.rate.StoreType
2021-03-19 21:33:55 +00:00
import kotlinx.coroutines.*
2021-03-18 04:00:01 +00:00
import java.io.FileOutputStream
2021-03-19 21:33:55 +00:00
import java.lang.Runnable
2020-02-09 13:52:17 +00:00
import java.nio.charset.Charset
2021-02-11 05:34:26 +00:00
import java.text.DateFormat
import java.util.*
2021-03-18 04:00:01 +00:00
import kotlin.math.roundToInt
2020-01-20 23:53:22 +00:00
2021-03-02 05:22:55 +00:00
2020-02-16 22:22:24 +00: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-17 02:54:29 +00:00
SettingsFragment shows " Settings "
2020-02-16 22:22:24 +00:00
username
shortname
bluetooth pairing list
( eventually misc device settings that are not channel related )
2020-02-17 02:54:29 +00:00
Channel fragment " Channel "
2020-02-16 22:22:24 +00:00
qr code , copy link button
ch number
misc other settings
( eventually a way of choosing between past channels )
2020-02-17 02:54:29 +00:00
ChatFragment " Messages "
2020-02-16 22:22:24 +00: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-17 02:54:29 +00:00
NodeListFragment " Users "
2020-02-16 22:22:24 +00: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
* /
2020-02-17 23:39:49 +00:00
val utf8 = Charset . forName ( " UTF-8 " )
2020-02-11 04:17:42 +00:00
class MainActivity : AppCompatActivity ( ) , Logging ,
ActivityCompat . OnRequestPermissionsResultCallback {
2020-01-20 23:53:22 +00:00
2020-01-21 17:37:39 +00:00
companion object {
const val REQUEST _ENABLE _BT = 10
2020-01-21 21:12:01 +00:00
const val DID _REQUEST _PERM = 11
2020-02-14 15:47:20 +00:00
const val RC _SIGN _IN = 12 // google signin completed
2020-04-19 01:45:50 +00:00
const val RC _SELECT _DEVICE =
13 // seems to be hardwired in CompanionDeviceManager to add 65536
2021-03-18 04:00:01 +00:00
const val CREATE _CSV _FILE = 14
2020-01-21 17:37:39 +00:00
}
2020-12-07 12:33:29 +00:00
private lateinit var binding : ActivityMainBinding
2020-09-11 23:19:23 +00:00
// Used to schedule a coroutine in the GUI thread
private val mainScope = CoroutineScope ( Dispatchers . Main + Job ( ) )
2020-02-09 15:28:24 +00:00
2020-01-22 21:02:24 +00:00
private val bluetoothAdapter : BluetoothAdapter ? by lazy ( LazyThreadSafetyMode . NONE ) {
2020-01-21 17:37:39 +00:00
val bluetoothManager = getSystemService ( Context . BLUETOOTH _SERVICE ) as BluetoothManager
2020-01-22 21:02:24 +00:00
bluetoothManager . adapter
2020-01-21 17:37:39 +00:00
}
2020-04-20 14:46:06 +00:00
val model : UIViewModel by viewModels ( )
2020-04-08 15:16:06 +00:00
2020-04-07 17:40:01 +00: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 23:49:27 +00:00
TabInfo (
" Messages " ,
R . drawable . ic _twotone _message _24 ,
MessagesFragment ( )
) ,
2020-04-08 22:25:57 +00:00
TabInfo (
" Users " ,
R . drawable . ic _twotone _people _24 ,
UsersFragment ( )
) ,
2020-04-09 01:42:17 +00:00
TabInfo (
" Map " ,
R . drawable . ic _twotone _map _24 ,
MapFragment ( )
) ,
2020-04-07 17:40:01 +00:00
TabInfo (
" Channel " ,
R . drawable . ic _twotone _contactless _24 ,
2020-04-07 23:04:58 +00:00
ChannelFragment ( )
) ,
2020-04-07 19:13:50 +00:00
TabInfo (
" Settings " ,
R . drawable . ic _twotone _settings _applications _24 ,
2020-04-09 01:42:17 +00:00
SettingsFragment ( )
)
2020-04-07 17:40:01 +00:00
)
private
val tabsAdapter = object : FragmentStateAdapter ( this ) {
override fun getItemCount ( ) : Int = tabInfos . size
2020-04-07 16:36:12 +00: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 17:40:01 +00:00
return tabInfos [ position ] . content
2020-04-07 16:36:12 +00:00
}
}
2020-04-23 15:52:25 +00:00
2020-06-08 00:11:30 +00:00
private val btStateReceiver = BluetoothStateReceiver { _ ->
2020-04-23 15:52:25 +00:00
updateBluetoothEnabled ( )
}
/ * *
* Don ' t tell our app we have bluetooth until we have bluetooth _and _ location access
* /
private fun updateBluetoothEnabled ( ) {
var enabled = false // assume failure
val requiredPerms = listOf (
Manifest . permission . ACCESS _FINE _LOCATION ,
Manifest . permission . BLUETOOTH ,
Manifest . permission . BLUETOOTH _ADMIN
)
if ( getMissingPermissions ( requiredPerms ) . isEmpty ( ) ) {
/// ask the adapter if we have access
bluetoothAdapter ?. apply {
enabled = isEnabled
}
} else
errormsg ( " Still missing needed bluetooth permissions " )
debug ( " Detected our bluetooth access= $enabled " )
2020-04-20 16:56:38 +00:00
model . bluetoothEnabled . value = enabled
}
2021-06-10 17:58:45 +00:00
/ * * Get the minimum permissions our app needs to run correctly
2020-04-23 15:52:25 +00:00
* /
2021-06-10 17:58:45 +00:00
private fun getMinimumPermissions ( ) : List < String > {
2020-02-14 17:09:40 +00:00
val perms = mutableListOf (
2020-02-16 22:22:24 +00:00
Manifest . permission . ACCESS _COARSE _LOCATION ,
2020-01-23 00:45:27 +00:00
Manifest . permission . ACCESS _FINE _LOCATION ,
2020-01-21 21:12:01 +00:00
Manifest . permission . BLUETOOTH ,
Manifest . permission . BLUETOOTH _ADMIN ,
2020-04-20 03:03:38 +00:00
Manifest . permission . WAKE _LOCK
2020-04-20 14:46:06 +00:00
2020-04-13 00:13:13 +00: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-23 00:45:27 +00:00
)
2020-04-20 03:03:38 +00: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 18:17:52 +00:00
return getMissingPermissions ( perms )
}
2021-11-19 04:20:54 +00:00
/** Ask the user to grant camera permission */
fun requestCameraPermission ( ) = requestPermission ( getCameraPermissions ( ) , false )
2020-06-12 18:17:52 +00:00
2021-06-10 17:58:45 +00:00
/** Ask the user to grant background location permission */
2021-06-23 19:17:06 +00:00
fun requestBackgroundPermission ( ) = requestPermission ( getBackgroundPermissions ( ) , false )
2021-06-10 17:58:45 +00:00
2021-06-23 18:40:15 +00:00
/ * *
* @return a localized string warning user about missing permissions . Or null if everything is find
* /
fun getMissingMessage ( ) : String ? {
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 ,
Manifest . permission . ACCESS _FINE _LOCATION to getString ( R . string . location )
)
val deniedPermissions = getMinimumPermissions ( ) . mapNotNull {
2021-07-26 23:18:40 +00:00
if ( renamedPermissions . containsKey ( it ) )
2021-06-23 18:40:15 +00:00
renamedPermissions [ it ]
else // No localization found - just show the nasty android string
it
}
2021-07-26 23:18:40 +00:00
return if ( deniedPermissions . isEmpty ( ) )
2021-06-23 18:40:15 +00:00
null
else {
val asEnglish = deniedPermissions . joinToString ( " & " )
getString ( R . string . permission _missing ) . format ( asEnglish )
}
}
2021-06-10 17:58:45 +00:00
/ * * Possibly prompt user to grant permissions
2021-06-23 19:17:06 +00:00
* @param shouldShowDialog usually true , but in cases where we ' ve already shown a dialog elsewhere we skip it .
2021-06-10 17:58:45 +00:00
*
* @return true if we already have the needed permissions
* /
2021-07-26 23:18:40 +00:00
private fun requestPermission (
missingPerms : List < String > = getMinimumPermissions ( ) ,
shouldShowDialog : Boolean = true
) : Boolean =
2020-01-21 21:12:01 +00:00
if ( missingPerms . isNotEmpty ( ) ) {
2021-06-23 18:40:15 +00:00
val shouldShow = missingPerms . filter {
ActivityCompat . shouldShowRequestPermissionRationale ( this , it )
2020-01-21 21:12:01 +00:00
}
2021-06-23 18:40:15 +00:00
fun doRequest ( ) {
info ( " requesting permissions " )
// Ask for all the missing perms
ActivityCompat . requestPermissions (
this ,
missingPerms . toTypedArray ( ) ,
DID _REQUEST _PERM
)
}
2021-06-23 19:17:06 +00:00
if ( shouldShow . isNotEmpty ( ) && shouldShowDialog ) {
2021-06-23 18:40:15 +00: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 ) )
. setMessage ( getMissingMessage ( ) )
2021-06-23 19:17:06 +00:00
. setNeutralButton ( R . string . cancel _no _radio ) { _ , _ ->
2021-06-23 18:40:15 +00:00
error ( " User bailed due to permissions " )
}
2021-06-23 19:17:06 +00:00
. setPositiveButton ( R . string . allow _will _show ) { _ , _ ->
2021-06-23 18:40:15 +00:00
doRequest ( )
}
. show ( )
} else {
info ( " Permissions $missingPerms missing, no need to show dialog, just asking OS " )
doRequest ( )
}
2020-01-21 21:12:01 +00:00
2021-06-10 17:58:45 +00:00
false
2020-01-21 21:12:01 +00:00
} else {
// Permission has already been granted
2021-06-10 17:58:45 +00:00
debug ( " We have our required permissions " )
true
2020-01-21 21:12:01 +00:00
}
2020-01-23 16:09:50 +00:00
2020-06-12 18:17:52 +00:00
/ * *
* Remind user he ' s disabled permissions we need
2021-05-10 00:09:42 +00:00
*
* @return true if we did warn
2020-06-12 18:17:52 +00:00
* /
2021-06-10 17:58:45 +00:00
@SuppressLint ( " InlinedApi " ) // This function is careful to work with old APIs correctly
2021-05-10 00:09:42 +00:00
fun warnMissingPermissions ( ) : Boolean {
2021-06-23 18:40:15 +00:00
val message = getMissingMessage ( )
2020-04-19 16:48:12 +00:00
2021-06-23 18:40:15 +00:00
return if ( message != null ) {
errormsg ( " Denied permissions: $message " )
showToast ( message )
2021-05-10 00:09:42 +00:00
true
} else
false
2020-06-12 18:17:52 +00:00
}
override fun onRequestPermissionsResult (
requestCode : Int ,
permissions : Array < out String > ,
grantResults : IntArray
) {
super . onRequestPermissionsResult ( requestCode , permissions , grantResults )
2021-07-26 23:18:40 +00:00
when ( requestCode ) {
2021-06-23 18:56:29 +00:00
DID _REQUEST _PERM -> {
// If request is cancelled, the result arrays are empty.
if ( ( grantResults . isNotEmpty ( ) &&
2021-07-26 23:18:40 +00:00
grantResults [ 0 ] == PackageManager . PERMISSION _GRANTED )
) {
2021-06-23 18:56:29 +00: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
}
}
2020-04-23 15:52:25 +00:00
updateBluetoothEnabled ( )
2020-02-11 04:17:42 +00:00
}
2020-02-14 15:47:20 +00:00
private fun sendTestPackets ( ) {
exceptionReporter {
2020-04-08 16:53:04 +00:00
val m = model . meshService !!
2020-02-14 15:47:20 +00:00
// Do some test operations
val testPayload = " hello world " . toByteArray ( )
2020-05-30 22:48:50 +00:00
m . send (
DataPacket (
" +16508675310 " ,
testPayload ,
2020-12-07 11:50:06 +00:00
Portnums . PortNum . PRIVATE _APP _VALUE
2020-05-30 22:48:50 +00:00
)
2020-02-14 15:47:20 +00:00
)
2020-05-30 22:48:50 +00:00
m . send (
DataPacket (
" +16508675310 " ,
testPayload ,
2020-12-07 11:50:06 +00:00
Portnums . PortNum . TEXT _MESSAGE _APP _VALUE
2020-05-30 22:48:50 +00:00
)
2020-02-14 15:47:20 +00:00
)
}
}
2020-02-11 04:17:42 +00:00
2020-04-11 16:39:34 +00:00
/// Ask user to rate in play store
private fun askToRate ( ) {
2020-07-08 14:50:24 +00: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 15:29:53 +00:00
2020-07-15 05:21:54 +00:00
val hasGooglePlay = GoogleApiAvailability . getInstance ( )
. isGooglePlayServicesAvailable ( this ) != ConnectionResult . SERVICE _MISSING
val rater = AppRate . with ( this )
2020-07-08 14:50:24 +00: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-15 05:21:54 +00:00
. setStoreType ( if ( hasGooglePlay ) StoreType . GOOGLEPLAY else StoreType . AMAZON )
rater . monitor ( ) // Monitors the app launch times
2020-07-08 14:50:24 +00:00
// Only ask to rate if the user has a suitable store
2021-03-29 12:33:06 +00:00
AppRate . showRateDialogIfMeetsConditions ( this ) // Shows the Rate Dialog when conditions are met
2020-04-11 16:39:34 +00:00
}
}
2020-04-20 16:56:38 +00:00
private val isInTestLab : Boolean by lazy {
( application as GeeksvilleApplication ) . isInTestLab
}
2020-04-11 20:20:30 +00:00
2020-02-14 15:47:20 +00:00
override fun onCreate ( savedInstanceState : Bundle ? ) {
super . onCreate ( savedInstanceState )
2020-12-07 12:33:29 +00:00
binding = ActivityMainBinding . inflate ( layoutInflater )
2020-04-08 16:53:04 +00:00
val prefs = UIViewModel . getPreferences ( this )
model . ownerName . value = prefs . getString ( " owner " , " " ) !!
2020-02-11 04:17:42 +00:00
2021-04-11 10:10:17 +00:00
/// Set theme
setUITheme ( prefs )
2020-04-20 16:56:38 +00:00
/// Set initial bluetooth state
2020-04-23 15:52:25 +00:00
updateBluetoothEnabled ( )
2020-01-28 03:23:34 +00:00
2020-04-20 16:56:38 +00:00
/// We now want to be informed of bluetooth state
2020-09-17 18:47:54 +00:00
registerReceiver ( btStateReceiver , btStateReceiver . intentFilter )
2020-04-20 16:56:38 +00:00
2020-02-17 19:22:47 +00:00
/ * not yet working
2020-02-14 15:47:20 +00: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.
// 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 18:00:57 +00:00
2020-02-14 15:47:20 +00:00
// Build a GoogleSignInClient with the options specified by gso.
UIState . googleSignInClient = GoogleSignIn . getClient ( this , gso ) ;
2020-02-17 19:22:47 +00:00
* /
2020-03-02 16:54:57 +00:00
2020-04-07 16:36:12 +00:00
/ * setContent {
2020-03-17 18:35:19 +00:00
MeshApp ( )
2020-04-07 16:36:12 +00:00
} * /
2020-12-07 12:33:29 +00:00
setContentView ( binding . root )
2020-04-07 16:36:12 +00:00
2020-06-19 03:05:33 +00:00
initToolbar ( )
2020-12-07 12:33:29 +00:00
binding . pager . adapter = tabsAdapter
binding . pager . isUserInputEnabled =
2020-04-07 19:32:42 +00:00
false // Gestures for screen switching doesn't work so good with the map view
2020-04-09 18:03:17 +00:00
// pager.offscreenPageLimit = 0 // Don't keep any offscreen pages around, because we want to make sure our bluetooth scanning stops
2020-12-07 12:33:29 +00:00
TabLayoutMediator ( binding . tabLayout , binding . pager ) { tab , position ->
2020-04-07 19:13:50 +00:00
// tab.text = tabInfos[position].text // I think it looks better with icons only
2021-02-21 03:34:43 +00:00
tab . icon = ContextCompat . getDrawable ( this , tabInfos [ position ] . icon )
2020-04-07 16:36:12 +00:00
} . attach ( )
2020-04-08 18:57:31 +00:00
model . isConnected . observe ( this , Observer { connected ->
2020-06-11 01:50:34 +00:00
updateConnectionStatusImage ( connected )
2020-04-08 18:57:31 +00:00
} )
2020-04-11 16:39:34 +00:00
2020-06-10 00:10:49 +00:00
// Handle any intent
handleIntent ( intent )
2020-04-11 16:39:34 +00:00
askToRate ( )
2021-06-23 18:56:29 +00: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 16:54:57 +00:00
}
2021-06-23 18:56:29 +00:00
2020-06-19 03:05:33 +00:00
private fun initToolbar ( ) {
val toolbar =
findViewById < View > ( R . id . toolbar ) as Toolbar
setSupportActionBar ( toolbar )
supportActionBar ?. setDisplayShowTitleEnabled ( false )
}
2020-06-11 01:50:34 +00:00
private fun updateConnectionStatusImage ( connected : MeshService . ConnectionState ) {
2020-07-17 21:06:29 +00:00
if ( model . actionBarMenu == null )
2020-06-19 03:05:33 +00:00
return
2020-06-11 01:50:34 +00: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 21:06:29 +00:00
val item = model . actionBarMenu ?. findItem ( R . id . connectStatusImage )
2020-06-19 03:05:33 +00:00
if ( item != null ) {
item . setIcon ( image )
item . setTitle ( tooltip )
}
2020-06-11 01:50:34 +00:00
}
2020-03-02 16:54:57 +00:00
override fun onNewIntent ( intent : Intent ) {
super . onNewIntent ( intent )
handleIntent ( intent )
}
2020-04-10 00:06:41 +00:00
private var requestedChannelUrl : Uri ? = null
2020-06-11 18:29:54 +00:00
/** We keep the usb device here, so later we can give it to our service */
private var usbDevice : UsbDevice ? = null
2020-03-02 16:54:57 +00: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 16:10:51 +00: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-10 00:06:41 +00:00
2020-06-09 16:10:51 +00:00
// if the device is connected already, process it now
if ( model . isConnected . value == MeshService . ConnectionState . CONNECTED )
perhapsChangeChannel ( )
2020-04-10 00:06:41 +00:00
2020-06-09 16:10:51 +00: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-10 00:10:49 +00:00
UsbManager . ACTION _USB _DEVICE _ATTACHED -> {
2021-03-31 11:40:33 +00:00
val device : UsbDevice ? = intent . getParcelableExtra ( UsbManager . EXTRA _DEVICE )
if ( device != null ) {
debug ( " Handle USB device attached! $device " )
usbDevice = device
}
2020-06-09 16:10:51 +00:00
}
2020-06-08 21:04:56 +00:00
2020-06-09 16:10:51 +00:00
Intent . ACTION _MAIN -> {
}
else -> {
warn ( " Unexpected action $appLinkAction " )
}
2020-06-08 21:04:56 +00:00
}
2020-02-14 15:47:20 +00:00
}
2020-01-21 17:37:39 +00:00
2020-02-14 15:47:20 +00:00
override fun onDestroy ( ) {
2020-04-20 16:56:38 +00:00
unregisterReceiver ( btStateReceiver )
2020-02-14 15:47:20 +00:00
unregisterMeshReceiver ( )
2020-09-11 23:19:23 +00:00
mainScope . cancel ( " Activity going away " )
2020-02-14 15:47:20 +00:00
super . onDestroy ( )
}
2020-02-14 12:41:20 +00:00
2020-02-14 15:47:20 +00:00
/ * *
* Dispatch incoming result to the correct fragment .
* /
2020-04-18 23:30:30 +00:00
@SuppressLint ( " InlinedApi " )
2020-04-07 17:40:01 +00:00
override fun onActivityResult (
requestCode : Int ,
resultCode : Int ,
data : Intent ?
) {
2020-02-14 15:47:20 +00:00
super . onActivityResult ( requestCode , resultCode , data )
// Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...);
// Result returned from launching the Intent from GoogleSignInClient.getSignInIntent(...);
2020-04-18 23:30:30 +00: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 )
}
2020-04-19 01:45:50 +00:00
( 65536 + RC _SELECT _DEVICE ) -> when ( resultCode ) {
2020-04-18 23:30:30 +00:00
Activity . RESULT _OK -> {
// User has chosen to pair with the Bluetooth device.
val device : BluetoothDevice =
2020-04-19 23:24:47 +00:00
data ?. getParcelableExtra ( CompanionDeviceManager . EXTRA _DEVICE ) !!
2020-04-18 23:30:30 +00:00
debug ( " Received BLE pairing ${device.address} " )
2020-04-19 01:45:50 +00:00
if ( device . bondState != BluetoothDevice . BOND _BONDED ) {
device . createBond ( )
// FIXME - wait for bond to complete
}
2020-04-18 23:30:30 +00:00
// ... Continue interacting with the paired device.
2020-04-20 02:23:20 +00:00
model . meshService ?. let { service ->
2020-06-08 03:17:47 +00:00
MeshService . changeDeviceAddress ( this @MainActivity , service , device . address )
2020-04-20 02:23:20 +00:00
}
2020-04-18 23:30:30 +00:00
}
else ->
warn ( " BLE device select intent failed " )
}
2021-03-18 04:00:01 +00:00
CREATE _CSV _FILE -> {
if ( resultCode == Activity . RESULT _OK ) {
data ?. data ?. let { file _uri ->
2021-03-19 21:33:55 +00:00
// model.allPackets is a result of a query, so we need to use observer for
// the query to materialize
2021-03-18 04:00:01 +00:00
model . allPackets . observe ( this , { packets ->
if ( packets != null ) {
2021-03-19 21:33:55 +00:00
// no need for observer once got non-null list
2021-03-18 04:00:01 +00:00
model . allPackets . removeObservers ( this )
2021-03-19 21:33:55 +00:00
// execute on the default thread pool to not block the main thread
CoroutineScope ( Dispatchers . Default + Job ( ) ) . handledLaunch {
saveMessagesCSV ( file _uri , packets )
}
2021-03-18 04:00:01 +00:00
}
} )
}
}
}
2020-01-20 23:53:22 +00:00
}
2020-02-14 15:47:20 +00:00
}
2020-02-09 13:52:17 +00:00
2020-02-14 15:47:20 +00:00
private fun handleSignInResult ( completedTask : Task < GoogleSignInAccount > ) {
2020-02-18 18:40:02 +00:00
/ *
2020-02-14 15:47:20 +00:00
try {
2020-02-18 18:40:02 +00:00
val account = completedTask . getResult ( ApiException :: class . java )
2020-02-14 15:47:20 +00: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 18:40:02 +00:00
} * /
2020-02-14 15:47:20 +00:00
}
2020-02-11 04:17:42 +00:00
2020-04-07 17:40:01 +00:00
private
var receiverRegistered = false
2020-02-11 04:17:42 +00:00
2020-02-14 15:47:20 +00:00
private fun registerMeshReceiver ( ) {
2020-04-09 20:28:44 +00:00
unregisterMeshReceiver ( )
2020-02-14 15:47:20 +00:00
val filter = IntentFilter ( )
filter . addAction ( MeshService . ACTION _MESH _CONNECTED )
filter . addAction ( MeshService . ACTION _NODE _CHANGE )
2021-01-11 09:15:19 +00:00
filter . addAction ( MeshService . actionReceived ( Portnums . PortNum . TEXT _MESSAGE _APP _VALUE ) )
2020-05-31 18:23:25 +00:00
filter . addAction ( ( MeshService . ACTION _MESSAGE _STATUS ) )
2020-02-14 15:47:20 +00:00
registerReceiver ( meshServiceReceiver , filter )
2021-03-29 12:33:06 +00:00
receiverRegistered = true
2020-02-14 15:47:20 +00:00
}
2020-02-09 13:52:17 +00:00
2020-02-14 15:47:20 +00:00
private fun unregisterMeshReceiver ( ) {
if ( receiverRegistered ) {
receiverRegistered = false
unregisterReceiver ( meshServiceReceiver )
2020-02-11 04:17:42 +00:00
}
2020-02-14 15:47:20 +00:00
}
2020-02-09 13:52:17 +00:00
2020-02-17 02:14:40 +00:00
2020-04-19 19:15:42 +00:00
/// Pull our latest node db from the device
private fun updateNodesFromDevice ( ) {
model . meshService ?. let { service ->
// Update our nodeinfos based on data from the device
val nodes = service . nodes . map {
it . user ?. id !! to it
} . toMap ( )
model . nodeDB . nodes . value = nodes
try {
// Pull down our real node ID - This must be done AFTER reading the nodedb because we need the DB to find our nodeinof object
model . nodeDB . myId . value = service . myId
2020-09-03 00:16:41 +00:00
val ourNodeInfo = model . nodeDB . ourNodeInfo
model . ownerName . value = ourNodeInfo ?. user ?. longName
2020-04-19 19:15:42 +00:00
} catch ( ex : Exception ) {
warn ( " Ignoring failure to get myId, service is probably just uninited... ${ex.message} " )
}
}
}
2021-03-02 07:12:57 +00: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 08:27:34 +00:00
. setPositiveButton ( R . string . okay ) { _ , _ ->
2021-03-02 07:12:57 +00: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 15:47:20 +00:00
/// Called when we gain/lose a connection to our mesh radio
2020-04-04 22:29:16 +00:00
private fun onMeshConnectionChanged ( connected : MeshService . ConnectionState ) {
2021-02-01 02:56:47 +00:00
debug ( " connchange ${model.isConnected.value} -> $connected " )
2021-01-08 07:19:20 +00:00
2020-04-04 22:29:16 +00:00
if ( connected == MeshService . ConnectionState . CONNECTED ) {
2020-04-09 19:22:41 +00:00
model . meshService ?. let { service ->
2021-01-08 06:51:19 +00:00
val oldConnection = model . isConnected . value
model . isConnected . value = connected
2020-04-09 19:22:41 +00:00
debug ( " Getting latest radioconfig from service " )
2021-01-08 06:51:19 +00:00
try {
2021-03-23 05:21:51 +00:00
val info : MyNodeInfo ? = service . myNodeInfo // this can be null
2021-01-08 06:51:19 +00:00
model . myNodeInfo . value = info
2021-03-23 05:21:51 +00: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 07:12:57 +00:00
else {
2021-07-26 23:18:40 +00: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 05:22:55 +00:00
2021-07-26 23:18:40 +00:00
if ( curVer < MeshService . minFirmwareVersion )
showAlert ( R . string . firmware _too _old , R . string . firmware _old )
else {
// If our app is too old/new, we probably don't understand the new radioconfig messages, so we don't read them until here
2021-03-23 05:21:51 +00:00
2021-07-26 23:18:40 +00:00
model . radioConfig . value =
RadioConfigProtos . RadioConfig . parseFrom ( service . radioConfig )
2021-03-02 05:22:55 +00:00
2021-07-26 23:18:40 +00:00
model . channels . value =
ChannelSet ( AppOnlyProtos . ChannelSet . parseFrom ( service . channels ) )
2021-03-05 06:14:17 +00:00
2021-07-26 23:18:40 +00:00
updateNodesFromDevice ( )
2021-03-02 05:22:55 +00:00
2021-07-26 23:18:40 +00:00
// we have a connection to our device now, do the channel change
perhapsChangeChannel ( )
}
2021-03-23 05:21:51 +00:00
}
2021-03-02 07:12:57 +00:00
}
2021-03-02 05:22:55 +00:00
}
2021-01-08 06:51:19 +00:00
} catch ( ex : RemoteException ) {
warn ( " Abandoning connect $ex , because we probably just lost device connection " )
model . isConnected . value = oldConnection
}
2020-04-09 19:22:41 +00:00
}
2021-01-11 09:15:19 +00:00
} else {
2021-01-08 06:51:19 +00:00
// For other connection states, just slam them in
model . isConnected . value = connected
}
2020-02-14 15:47:20 +00:00
}
2020-02-09 13:52:17 +00:00
2021-03-17 07:37:09 +00:00
private fun showToast ( msgId : Int ) {
Toast . makeText (
this ,
msgId ,
Toast . LENGTH _LONG
) . show ( )
}
private fun showToast ( msg : String ) {
Toast . makeText (
this ,
msg ,
Toast . LENGTH _LONG
) . show ( )
}
2020-04-10 00:06:41 +00:00
private fun perhapsChangeChannel ( ) {
// If the is opening a channel URL, handle it now
requestedChannelUrl ?. let { url ->
2020-07-18 20:17:30 +00:00
try {
2021-02-27 05:43:55 +00:00
val channels = ChannelSet ( url )
val primary = channels . primaryChannel
2021-03-17 07:37:09 +00:00
if ( primary == null )
showToast ( R . string . channel _invalid )
else {
requestedChannelUrl = null
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 20:17:30 +00:00
}
2021-03-17 07:37:09 +00:00
. setPositiveButton ( R . string . accept ) { _ , _ ->
debug ( " Setting channel from URL " )
try {
model . setChannels ( channels )
} catch ( ex : RemoteException ) {
errormsg ( " Couldn't change channel ${ex.message} " )
showToast ( R . string . cant _change _no _radio )
}
}
. show ( )
}
2020-07-18 20:17:30 +00:00
} catch ( ex : InvalidProtocolBufferException ) {
2021-03-17 07:37:09 +00:00
showToast ( R . string . channel _invalid )
2020-07-18 20:17:30 +00:00
}
2020-04-10 00:06:41 +00:00
}
}
2020-03-05 17:50:33 +00:00
override fun dispatchTouchEvent ( ev : MotionEvent ? ) : Boolean {
return try {
super . dispatchTouchEvent ( ev )
2020-03-10 01:54:33 +00:00
} catch ( ex : Throwable ) {
2020-03-05 17:50:33 +00:00
Exceptions . report (
ex ,
" dispatchTouchEvent "
) // hide this Compose error from the user but report to the mothership
false
}
}
2020-04-07 17:40:01 +00:00
private
val meshServiceReceiver = object : BroadcastReceiver ( ) {
2020-02-09 13:52:17 +00:00
2020-04-07 17:40:01 +00:00
override fun onReceive ( context : Context , intent : Intent ) =
exceptionReporter {
debug ( " Received from mesh service $intent " )
2020-02-09 13:52:17 +00:00
2020-04-07 17:40:01 +00:00
when ( intent . action ) {
MeshService . ACTION _NODE _CHANGE -> {
val info : NodeInfo =
intent . getParcelableExtra ( EXTRA _NODEINFO ) !!
debug ( " UI nodechange $info " )
2020-02-09 13:52:17 +00:00
2020-04-07 17:40:01 +00:00
// We only care about nodes that have user info
info . user ?. id ?. let {
2020-04-08 16:53:04 +00:00
val newnodes = model . nodeDB . nodes . value !! + Pair ( it , info )
model . nodeDB . nodes . value = newnodes
2020-04-07 17:40:01 +00:00
}
2020-02-09 13:52:17 +00:00
}
2020-02-17 23:56:04 +00:00
2021-01-11 09:15:19 +00:00
MeshService . actionReceived ( Portnums . PortNum . TEXT _MESSAGE _APP _VALUE ) -> {
debug ( " received new message from service " )
2020-04-07 17:40:01 +00:00
val payload =
2020-04-19 18:47:34 +00:00
intent . getParcelableExtra < DataPacket > ( EXTRA _PAYLOAD ) !!
2020-04-07 17:40:01 +00:00
2021-01-11 09:15:19 +00:00
model . messagesState . addMessage ( payload )
2020-02-09 13:52:17 +00:00
}
2020-05-31 02:58:36 +00:00
MeshService . ACTION _MESSAGE _STATUS -> {
debug ( " received message status from service " )
val id = intent . getIntExtra ( EXTRA _PACKET _ID , 0 )
2020-06-08 00:11:30 +00:00
val status = intent . getParcelableExtra < MessageStatus > ( EXTRA _STATUS ) !!
2020-05-31 02:58:36 +00:00
model . messagesState . updateStatus ( id , status )
}
2020-04-07 17:40:01 +00:00
MeshService . ACTION _MESH _CONNECTED -> {
2021-04-02 05:55:41 +00:00
val extra = intent . getStringExtra ( EXTRA _CONNECTED )
if ( extra != null ) {
onMeshConnectionChanged ( MeshService . ConnectionState . valueOf ( extra ) )
}
2020-04-07 17:40:01 +00:00
}
else -> TODO ( )
2020-02-09 13:52:17 +00:00
}
}
2020-02-14 15:47:20 +00:00
}
2020-01-20 23:53:22 +00:00
2020-09-22 19:52:15 +00:00
private var connectionJob : Job ? = null
2020-04-07 17:40:01 +00:00
private
val mesh = object :
ServiceClient < com . geeksville . mesh . IMeshService > ( {
com . geeksville . mesh . IMeshService . Stub . asInterface ( it )
} ) {
2020-09-11 23:19:23 +00:00
override fun onConnected ( service : com . geeksville . mesh . IMeshService ) {
2020-02-11 04:17:42 +00:00
2020-09-11 23:19:23 +00: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 19:52:15 +00:00
connectionJob = mainScope . handledLaunch {
2020-09-11 23:19:23 +00:00
model . meshService = service
2021-02-01 04:53:40 +00: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 05:24:48 +00:00
val allMsgs = service . oldMessages
2021-02-01 04:53:40 +00:00
val msgs =
2021-02-06 05:24:48 +00:00
allMsgs . filter { p -> p . dataType == Portnums . PortNum . TEXT _MESSAGE _APP _VALUE }
2021-03-04 01:08:29 +00:00
model . myNodeInfo . value = service . myNodeInfo // Note: this could be NULL!
debug ( " Service provided ${msgs.size} messages and myNodeNum ${model.myNodeInfo.value?.myNodeNum} " )
2021-02-01 04:53:40 +00: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 )
updateNodesFromDevice ( )
2020-06-11 18:29:54 +00:00
2020-09-11 23:19:23 +00: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} " )
model . isConnected . value =
MeshService . ConnectionState . valueOf ( service . connectionState ( ) )
2021-03-17 07:37:09 +00:00
} finally {
2021-03-04 01:08:29 +00:00
connectionJob = null
}
2020-04-19 19:15:42 +00:00
2020-09-11 23:19:23 +00:00
debug ( " connected to mesh service, isConnected= ${model.isConnected.value} " )
2020-06-10 01:46:23 +00:00
}
2020-02-25 16:10:23 +00:00
}
2020-02-14 15:47:20 +00:00
2020-02-25 16:10:23 +00:00
override fun onDisconnected ( ) {
2020-02-14 15:47:20 +00:00
unregisterMeshReceiver ( )
2020-04-08 16:53:04 +00:00
model . meshService = null
2020-01-23 16:09:50 +00:00
}
2020-02-14 15:47:20 +00:00
}
2020-01-23 16:09:50 +00:00
2020-04-20 02:23:20 +00:00
private fun bindMeshService ( ) {
2020-02-14 15:47:20 +00:00
debug ( " Binding to mesh service! " )
// we bind using the well known name, to make sure 3rd party apps could also
2020-09-22 19:52:15 +00: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 21:47:00 +00:00
Exceptions . reportError ( " meshService was supposed to be null, ignoring (but reporting a bug) " )
2020-09-22 19:52:15 +00:00
}
2020-01-23 17:04:06 +00:00
2020-07-02 16:53:52 +00: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 20:12:44 +00:00
// ALSO bind so we can use the api
2021-01-08 06:51:19 +00:00
mesh . connect (
this ,
MeshService . createIntent ( ) ,
Context . BIND _AUTO _CREATE + Context . BIND _ABOVE _CLIENT
)
2020-02-14 15:47:20 +00:00
}
2020-01-23 16:09:50 +00:00
2020-04-20 02:23:20 +00:00
private fun unbindMeshService ( ) {
2020-02-14 15:47:20 +00: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 23:24:47 +00:00
debug ( " Unbinding from mesh service! " )
2020-09-22 19:52:15 +00:00
connectionJob ?. let { job ->
connectionJob = null
warn ( " We had a pending onConnection job, so we are cancelling it " )
job . cancel ( " unbinding " )
}
2020-04-19 23:24:47 +00:00
mesh . close ( )
model . meshService = null
2020-02-14 15:47:20 +00:00
}
2020-01-23 16:09:50 +00:00
2020-02-18 17:09:49 +00:00
override fun onStop ( ) {
2020-02-14 15:47:20 +00: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 16:09:50 +00:00
2020-02-18 17:09:49 +00:00
super . onStop ( )
2020-02-14 15:47:20 +00:00
}
2020-01-23 16:09:50 +00:00
2020-02-18 17:09:49 +00:00
override fun onStart ( ) {
super . onStart ( )
2020-01-23 16:09:50 +00:00
2020-06-11 18:29:54 +00:00
// Ask to start bluetooth if no USB devices are visible
val hasUSB = SerialInterface . findDrivers ( this ) . isNotEmpty ( )
if ( !is InTestLab && ! hasUSB ) {
bluetoothAdapter ?. let {
if ( ! it . isEnabled ) {
val enableBtIntent = Intent ( BluetoothAdapter . ACTION _REQUEST _ENABLE )
startActivityForResult ( enableBtIntent , REQUEST _ENABLE _BT )
}
2020-04-20 16:56:38 +00:00
}
}
2021-03-17 07:53:08 +00:00
try {
bindMeshService ( )
2021-03-23 05:21:51 +00:00
} catch ( ex : BindFailedException ) {
2021-03-17 07:53:08 +00:00
// App is probably shutting down, ignore
errormsg ( " Bind of MeshService failed " )
}
2021-03-23 05:21:51 +00:00
2020-06-08 00:11:30 +00:00
val bonded = RadioInterfaceService . getBondedDeviceAddress ( this ) != null
2020-06-11 18:29:54 +00:00
if ( ! bonded && usbDevice == null ) // we will handle USB later
2020-06-10 00:10:49 +00:00
showSettingsPage ( )
}
2020-06-11 18:29:54 +00:00
private fun showSettingsPage ( ) {
2020-12-07 12:33:29 +00:00
binding . pager . currentItem = 5
2020-02-14 15:47:20 +00:00
}
2020-01-23 16:09:50 +00:00
2020-02-14 15:47:20 +00: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 21:06:29 +00:00
model . actionBarMenu = menu
updateConnectionStatusImage ( model . isConnected . value !! )
2020-02-14 15:47:20 +00:00
return true
}
2020-01-20 23:53:22 +00:00
2021-02-11 05:34:26 +00:00
val handler : Handler by lazy {
Handler ( mainLooper )
}
override fun onPrepareOptionsMenu ( menu : Menu ) : Boolean {
2021-03-02 05:22:55 +00:00
menu . findItem ( R . id . stress _test ) . isVisible =
BuildConfig . DEBUG // only show stress test for debug builds (for now)
2021-02-11 05:34:26 +00:00
return super . onPrepareOptionsMenu ( menu )
}
2020-02-14 15:47:20 +00: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-19 03:05:33 +00:00
R . id . about -> {
getVersionInfo ( )
return true
}
R . id . connectStatusImage -> {
2020-07-24 03:12:46 +00:00
Toast . makeText ( applicationContext , item . title , Toast . LENGTH _SHORT ) . show ( )
2020-06-19 03:05:33 +00:00
return true
}
2020-09-24 02:47:45 +00: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 08:29:26 +00:00
R . id . stress _test -> {
2021-02-11 05:34:26 +00:00
fun postPing ( ) {
2021-02-11 05:36:45 +00:00
// Send ping message and arrange delayed recursion.
debug ( " Sending ping " )
2021-02-11 08:54:17 +00:00
val str = " Ping " + DateFormat . getTimeInstance ( DateFormat . MEDIUM )
2021-02-11 05:36:45 +00:00
. format ( Date ( System . currentTimeMillis ( ) ) )
model . messagesState . sendMessage ( str )
2021-02-11 05:34:26 +00:00
handler . postDelayed (
Runnable {
postPing ( )
} ,
30000
)
}
2021-02-11 08:29:26 +00:00
item . isChecked = ! item . isChecked // toggle ping test
2021-03-02 05:22:55 +00:00
if ( item . isChecked )
2021-02-11 08:29:26 +00:00
postPing ( )
else
handler . removeCallbacksAndMessages ( null )
2021-02-11 05:34:26 +00:00
return true
}
2021-02-14 05:35:57 +00: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-18 04:00:01 +00:00
R . id . save _messages _csv -> {
val intent = Intent ( Intent . ACTION _CREATE _DOCUMENT ) . apply {
addCategory ( Intent . CATEGORY _OPENABLE )
type = " application/csv "
putExtra ( Intent . EXTRA _TITLE , " messages.csv " )
}
startActivityForResult ( intent , CREATE _CSV _FILE )
return true
}
2021-04-11 10:10:17 +00:00
R . id . theme -> {
chooseThemeDialog ( )
return true
}
2020-02-14 15:47:20 +00:00
else -> super . onOptionsItemSelected ( item )
2020-01-20 23:53:22 +00:00
}
}
2020-06-19 03:05:33 +00:00
private fun getVersionInfo ( ) {
try {
val packageInfo : PackageInfo = packageManager . getPackageInfo ( packageName , 0 )
val versionName = packageInfo . versionName
2021-03-17 07:37:09 +00:00
showToast ( versionName )
2020-06-19 03:05:33 +00:00
} catch ( e : PackageManager . NameNotFoundException ) {
errormsg ( " Can not find the version: ${e.message} " )
}
}
2021-03-18 04:00:01 +00:00
private fun saveMessagesCSV ( file _uri : Uri , packets : List < Packet > ) {
// Extract distances to this device from position messages and put (node,SNR,distance) in
// the file_uri
val myNodeNum = model . myNodeInfo . value ?. myNodeNum ?: return
applicationContext . contentResolver . openFileDescriptor ( file _uri , " w " ) ?. use {
FileOutputStream ( it . fileDescriptor ) . use { fs ->
// Write header
2021-03-29 12:33:06 +00:00
fs . write ( ( " from,rssi,snr,time,dist \n " ) . toByteArray ( ) )
2021-03-18 04:00:01 +00:00
// Packets are ordered by time, we keep most recent position of
// our device in my_position.
var my _position : MeshProtos . Position ? = null
packets . forEach {
it . proto ?. let { packet _proto ->
it . position ?. let { position ->
if ( packet _proto . from == myNodeNum ) {
my _position = position
} else if ( my _position != null ) {
2021-03-23 04:21:58 +00:00
val dist = positionToMeter ( my _position !! , position ) . roundToInt ( )
2021-03-29 12:33:06 +00:00
fs . write (
" %x,%d,%f,%d,%d \n " . format (
packet _proto . from , packet _proto . rxRssi ,
packet _proto . rxSnr , packet _proto . rxTime , dist
) . toByteArray ( )
)
2021-03-18 04:00:01 +00:00
}
}
}
}
}
}
}
2021-04-11 10:10:17 +00:00
/// Theme functions
private fun chooseThemeDialog ( ) {
/// Prepare dialog and its items
val builder = AlertDialog . Builder ( this )
builder . setTitle ( getString ( R . string . choose _theme _title ) )
val styles = arrayOf (
getString ( R . string . theme _light ) ,
getString ( R . string . theme _dark ) ,
2021-05-10 00:09:42 +00:00
getString ( R . string . theme _system )
)
2021-04-11 10:10:17 +00: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 ( )
}
}
}
2020-02-14 15:47:20 +00:00
}
2021-03-18 04:00:01 +00:00