mirror of
https://github.com/meshtastic/Meshtastic-Android.git
synced 2026-04-20 22:23:37 +00:00
More map modularization (#3319)
This commit is contained in:
parent
bc114c618a
commit
51fa634e11
28 changed files with 145 additions and 146 deletions
|
|
@ -1,35 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.android
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.Toast
|
||||
|
||||
// / show a toast
|
||||
fun Context.toast(message: CharSequence) = Toast.makeText(this, message, Toast.LENGTH_SHORT).show()
|
||||
|
||||
// / Utility function to hide the soft keyboard per stack overflow
|
||||
fun Activity.hideKeyboard() {
|
||||
// Check if no view has focus:
|
||||
currentFocus?.let { v ->
|
||||
val imm = getSystemService(Context.INPUT_METHOD_SERVICE) as? InputMethodManager
|
||||
imm?.hideSoftInputFromWindow(v.windowToken, 0)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,75 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.android
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.location.LocationManager
|
||||
import android.os.Build
|
||||
import androidx.core.content.ContextCompat
|
||||
|
||||
/** Checks if the device has a GPS receiver. */
|
||||
fun Context.hasGps(): Boolean {
|
||||
val lm = getSystemService(Context.LOCATION_SERVICE) as? LocationManager
|
||||
return lm?.allProviders?.contains(LocationManager.GPS_PROVIDER) == true
|
||||
}
|
||||
|
||||
/** Checks if the device has a GPS receiver and it is currently disabled. */
|
||||
fun Context.gpsDisabled(): Boolean {
|
||||
val lm = getSystemService(Context.LOCATION_SERVICE) as? LocationManager ?: return false
|
||||
return if (lm.allProviders.contains(LocationManager.GPS_PROVIDER)) {
|
||||
!lm.isProviderEnabled(LocationManager.GPS_PROVIDER)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines the list of Bluetooth permissions that are currently missing. Internal helper for
|
||||
* [hasBluetoothPermission].
|
||||
*
|
||||
* For Android S (API 31) and above, this includes [Manifest.permission.BLUETOOTH_SCAN] and
|
||||
* [Manifest.permission.BLUETOOTH_CONNECT]. For older versions, it includes [Manifest.permission.ACCESS_FINE_LOCATION]
|
||||
* as it is required for Bluetooth scanning.
|
||||
*
|
||||
* @return Array of missing Bluetooth permission strings. Empty if all are granted.
|
||||
*/
|
||||
private fun Context.getBluetoothPermissions(): Array<String> {
|
||||
val requiredPermissions = mutableListOf<String>()
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
requiredPermissions.add(Manifest.permission.BLUETOOTH_SCAN)
|
||||
requiredPermissions.add(Manifest.permission.BLUETOOTH_CONNECT)
|
||||
} else {
|
||||
// ACCESS_FINE_LOCATION is required for Bluetooth scanning on pre-S devices.
|
||||
requiredPermissions.add(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
}
|
||||
return requiredPermissions
|
||||
.filter { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED }
|
||||
.toTypedArray()
|
||||
}
|
||||
|
||||
/** Checks if all necessary Bluetooth permissions have been granted. */
|
||||
fun Context.hasBluetoothPermission(): Boolean = getBluetoothPermissions().isEmpty()
|
||||
|
||||
/** @return true if the user already has location permission (ACCESS_FINE_LOCATION). */
|
||||
fun Context.hasLocationPermission(): Boolean {
|
||||
val perms = listOf(Manifest.permission.ACCESS_FINE_LOCATION)
|
||||
return perms.all { ContextCompat.checkSelfPermission(this, it) == PackageManager.PERMISSION_GRANTED }
|
||||
}
|
||||
|
|
@ -21,10 +21,10 @@ import androidx.navigation.NavGraphBuilder
|
|||
import androidx.navigation.NavHostController
|
||||
import androidx.navigation.compose.composable
|
||||
import androidx.navigation.navDeepLink
|
||||
import com.geeksville.mesh.ui.map.MapScreen
|
||||
import org.meshtastic.core.navigation.DEEP_LINK_BASE_URI
|
||||
import org.meshtastic.core.navigation.MapRoutes
|
||||
import org.meshtastic.core.navigation.NodesRoutes
|
||||
import org.meshtastic.feature.map.MapScreen
|
||||
|
||||
fun NavGraphBuilder.mapGraph(navController: NavHostController) {
|
||||
composable<MapRoutes.Map>(deepLinks = listOf(navDeepLink<MapRoutes.Map>(basePath = "$DEEP_LINK_BASE_URI/map"))) {
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ import androidx.annotation.RequiresPermission
|
|||
import androidx.lifecycle.Lifecycle
|
||||
import androidx.lifecycle.coroutineScope
|
||||
import com.geeksville.mesh.CoroutineDispatchers
|
||||
import com.geeksville.mesh.android.hasBluetoothPermission
|
||||
import com.geeksville.mesh.util.registerReceiverCompat
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
|
|
@ -37,6 +36,7 @@ import kotlinx.coroutines.flow.asStateFlow
|
|||
import kotlinx.coroutines.flow.emptyFlow
|
||||
import kotlinx.coroutines.flow.filter
|
||||
import kotlinx.coroutines.launch
|
||||
import org.meshtastic.core.common.hasBluetoothPermission
|
||||
import timber.log.Timber
|
||||
import javax.inject.Inject
|
||||
import javax.inject.Singleton
|
||||
|
|
|
|||
|
|
@ -49,7 +49,6 @@ import com.geeksville.mesh.StoreAndForwardProtos
|
|||
import com.geeksville.mesh.TelemetryProtos
|
||||
import com.geeksville.mesh.TelemetryProtos.LocalStats
|
||||
import com.geeksville.mesh.XmodemProtos
|
||||
import com.geeksville.mesh.android.hasLocationPermission
|
||||
import com.geeksville.mesh.concurrent.handledLaunch
|
||||
import com.geeksville.mesh.copy
|
||||
import com.geeksville.mesh.fromRadio
|
||||
|
|
@ -78,6 +77,7 @@ import kotlinx.coroutines.flow.flowOf
|
|||
import kotlinx.coroutines.flow.launchIn
|
||||
import kotlinx.coroutines.flow.onEach
|
||||
import org.meshtastic.core.analytics.DataPair
|
||||
import org.meshtastic.core.common.hasLocationPermission
|
||||
import org.meshtastic.core.data.repository.MeshLogRepository
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.data.repository.PacketRepository
|
||||
|
|
|
|||
|
|
@ -1,62 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.map
|
||||
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.feature.map.MapViewModel
|
||||
|
||||
@Composable
|
||||
fun MapScreen(
|
||||
onClickNodeChip: (Int) -> Unit,
|
||||
navigateToNodeDetails: (Int) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
mapViewModel: MapViewModel = hiltViewModel(),
|
||||
) {
|
||||
val ourNodeInfo by mapViewModel.ourNodeInfo.collectAsStateWithLifecycle()
|
||||
val isConnected by mapViewModel.isConnected.collectAsStateWithLifecycle()
|
||||
|
||||
@Suppress("ViewModelForwarding")
|
||||
Scaffold(
|
||||
modifier = modifier,
|
||||
topBar = {
|
||||
MainAppBar(
|
||||
title = stringResource(R.string.map),
|
||||
ourNode = ourNodeInfo,
|
||||
showNodeChip = ourNodeInfo != null && isConnected,
|
||||
canNavigateUp = false,
|
||||
onNavigateUp = {},
|
||||
actions = {},
|
||||
onClickChip = { onClickNodeChip(it.num) },
|
||||
)
|
||||
},
|
||||
) { paddingValues ->
|
||||
Box(modifier = Modifier.padding(paddingValues)) {
|
||||
MapView(mapViewModel = mapViewModel, navigateToNodeDetails = navigateToNodeDetails)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2025 Meshtastic LLC
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package com.geeksville.mesh.ui.map
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import dagger.hilt.android.lifecycle.HiltViewModel
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import org.meshtastic.core.common.BuildConfigProvider
|
||||
import org.meshtastic.core.data.repository.NodeRepository
|
||||
import org.meshtastic.core.database.model.Node
|
||||
import javax.inject.Inject
|
||||
|
||||
@HiltViewModel
|
||||
class NodeMapViewModel @Inject constructor(nodeRepository: NodeRepository, buildConfigProvider: BuildConfigProvider) :
|
||||
ViewModel() {
|
||||
val ourNodeInfo: StateFlow<Node?> = nodeRepository.ourNodeInfo
|
||||
|
||||
val applicationId = buildConfigProvider.applicationId
|
||||
}
|
||||
|
|
@ -67,11 +67,11 @@ import com.geeksville.mesh.MeshProtos
|
|||
import com.geeksville.mesh.model.MetricsViewModel
|
||||
import org.meshtastic.core.model.util.metersIn
|
||||
import org.meshtastic.core.model.util.toString
|
||||
import org.meshtastic.core.proto.formatPositionTime
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
import org.meshtastic.core.ui.theme.AppTheme
|
||||
import java.text.DateFormat
|
||||
import kotlin.time.Duration.Companion.days
|
||||
|
||||
@Composable
|
||||
private fun RowScope.PositionText(text: String, weight: Float) {
|
||||
|
|
@ -106,7 +106,6 @@ private fun HeaderItem(compactWidth: Boolean) {
|
|||
|
||||
const val DEG_D = 1e-7
|
||||
const val HEADING_DEG = 1e-5
|
||||
private const val SECONDS_TO_MILLIS = 1000L
|
||||
|
||||
@Composable
|
||||
fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, dateFormat: DateFormat, system: DisplayUnits) {
|
||||
|
|
@ -122,24 +121,10 @@ fun PositionItem(compactWidth: Boolean, position: MeshProtos.Position, dateForma
|
|||
PositionText("${position.groundSpeed} Km/h", WEIGHT_15)
|
||||
PositionText("%.0f°".format(position.groundTrack * HEADING_DEG), WEIGHT_15)
|
||||
}
|
||||
PositionText(formatPositionTime(position, dateFormat), WEIGHT_40)
|
||||
PositionText(position.formatPositionTime(dateFormat), WEIGHT_40)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun formatPositionTime(position: MeshProtos.Position, dateFormat: DateFormat): String {
|
||||
val currentTime = System.currentTimeMillis()
|
||||
val sixMonthsAgo = currentTime - 180.days.inWholeMilliseconds
|
||||
val isOlderThanSixMonths = position.time * SECONDS_TO_MILLIS < sixMonthsAgo
|
||||
val timeText =
|
||||
if (isOlderThanSixMonths) {
|
||||
stringResource(id = R.string.unknown_age)
|
||||
} else {
|
||||
dateFormat.format(position.time * SECONDS_TO_MILLIS)
|
||||
}
|
||||
return timeText
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun ActionButtons(
|
||||
clearButtonEnabled: Boolean,
|
||||
|
|
|
|||
|
|
@ -60,7 +60,6 @@ import androidx.hilt.lifecycle.viewmodel.compose.hiltViewModel
|
|||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import com.geeksville.mesh.BuildConfig
|
||||
import com.geeksville.mesh.ClientOnlyProtos.DeviceProfile
|
||||
import com.geeksville.mesh.android.gpsDisabled
|
||||
import com.geeksville.mesh.navigation.getNavRouteFrom
|
||||
import com.geeksville.mesh.ui.settings.radio.RadioConfigItemList
|
||||
import com.geeksville.mesh.ui.settings.radio.RadioConfigViewModel
|
||||
|
|
@ -71,6 +70,7 @@ import com.geeksville.mesh.util.LanguageUtils.getLanguageMap
|
|||
import com.google.accompanist.permissions.ExperimentalPermissionsApi
|
||||
import com.google.accompanist.permissions.rememberMultiplePermissionsState
|
||||
import kotlinx.coroutines.delay
|
||||
import org.meshtastic.core.common.gpsDisabled
|
||||
import org.meshtastic.core.navigation.Route
|
||||
import org.meshtastic.core.strings.R
|
||||
import org.meshtastic.core.ui.component.MainAppBar
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue