refactor: channel qr clean (#1983)

pull/1988/head
Robert-0410 2025-05-30 17:50:45 -07:00 zatwierdzone przez GitHub
rodzic 7dc2147169
commit 5edc2a8d57
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: B5690EEEBB952194
6 zmienionych plików z 208 dodań i 167 usunięć

Wyświetl plik

@ -0,0 +1,67 @@
/*
* 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.navigation
import androidx.compose.runtime.remember
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavGraphBuilder
import androidx.navigation.NavHostController
import androidx.navigation.compose.composable
import androidx.navigation.navigation
import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.ui.sharing.ChannelScreen
import com.geeksville.mesh.ui.radioconfig.components.ChannelConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.LoRaConfigScreen
/**
* Navigation graph for for the top level ChannelScreen - [Route.Channels].
*/
fun NavGraphBuilder.channelsGraph(navController: NavHostController, uiViewModel: UIViewModel) {
navigation<Graph.ChannelsGraph>(
startDestination = Route.Channels,
) {
composable<Route.Channels> { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<Graph.ChannelsGraph>()
}
ChannelScreen(
viewModel = uiViewModel,
radioConfigViewModel = hiltViewModel(parentEntry),
onNavigate = { route -> navController.navigate(route) }
)
}
configRoutes(navController)
}
}
private fun NavGraphBuilder.configRoutes(
navController: NavHostController,
) {
ConfigRoute.entries.forEach { configRoute ->
composable(configRoute.route::class) { backStackEntry ->
val parentEntry = remember(backStackEntry) {
navController.getBackStackEntry<Graph.ChannelsGraph>()
}
when (configRoute) {
ConfigRoute.CHANNELS -> ChannelConfigScreen(hiltViewModel(parentEntry))
ConfigRoute.LORA -> LoRaConfigScreen(hiltViewModel(parentEntry))
else -> Unit
}
}
}
}

Wyświetl plik

@ -39,7 +39,6 @@ import com.geeksville.mesh.ui.map.MapView
import com.geeksville.mesh.ui.message.MessageScreen import com.geeksville.mesh.ui.message.MessageScreen
import com.geeksville.mesh.ui.message.QuickChatScreen import com.geeksville.mesh.ui.message.QuickChatScreen
import com.geeksville.mesh.ui.node.NodeScreen import com.geeksville.mesh.ui.node.NodeScreen
import com.geeksville.mesh.ui.sharing.ChannelScreen
import com.geeksville.mesh.ui.sharing.ShareScreen import com.geeksville.mesh.ui.sharing.ShareScreen
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -54,6 +53,9 @@ const val DEEP_LINK_BASE_URI = "meshtastic://meshtastic"
@Serializable @Serializable
sealed interface Graph : Route { sealed interface Graph : Route {
@Serializable
data class ChannelsGraph(val destNum: Int?)
@Serializable @Serializable
data class NodeDetailGraph(val destNum: Int) : Graph data class NodeDetailGraph(val destNum: Int) : Graph
@ -241,9 +243,9 @@ fun NavGraph(
composable<Route.Map> { composable<Route.Map> {
MapView(uIViewModel) MapView(uIViewModel)
} }
composable<Route.Channels> {
ChannelScreen(uIViewModel) channelsGraph(navController, uIViewModel)
}
composable<Route.Connections>( composable<Route.Connections>(
deepLinks = listOf( deepLinks = listOf(
navDeepLink { navDeepLink {

Wyświetl plik

@ -77,6 +77,11 @@ import com.geeksville.mesh.ui.radioconfig.components.StoreForwardConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.TelemetryConfigScreen import com.geeksville.mesh.ui.radioconfig.components.TelemetryConfigScreen
import com.geeksville.mesh.ui.radioconfig.components.UserConfigScreen import com.geeksville.mesh.ui.radioconfig.components.UserConfigScreen
fun getNavRouteFrom(routeName: String): Route? {
return ConfigRoute.entries.find { it.name == routeName }?.route
?: ModuleRoute.entries.find { it.name == routeName }?.route
}
fun NavGraphBuilder.radioConfigGraph(navController: NavHostController, uiViewModel: UIViewModel) { fun NavGraphBuilder.radioConfigGraph(navController: NavHostController, uiViewModel: UIViewModel) {
navigation<Graph.RadioConfigGraph>( navigation<Graph.RadioConfigGraph>(
startDestination = Route.RadioConfig(), startDestination = Route.RadioConfig(),

Wyświetl plik

@ -68,16 +68,12 @@ import com.geeksville.mesh.navigation.AdminRoute
import com.geeksville.mesh.navigation.ConfigRoute import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.ModuleRoute import com.geeksville.mesh.navigation.ModuleRoute
import com.geeksville.mesh.navigation.Route import com.geeksville.mesh.navigation.Route
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.ui.common.components.PreferenceCategory import com.geeksville.mesh.ui.common.components.PreferenceCategory
import com.geeksville.mesh.ui.common.theme.AppTheme import com.geeksville.mesh.ui.common.theme.AppTheme
import com.geeksville.mesh.ui.radioconfig.components.EditDeviceProfileDialog import com.geeksville.mesh.ui.radioconfig.components.EditDeviceProfileDialog
import com.geeksville.mesh.ui.radioconfig.components.PacketResponseStateDialog import com.geeksville.mesh.ui.radioconfig.components.PacketResponseStateDialog
private fun getNavRouteFrom(routeName: String): Route? {
return ConfigRoute.entries.find { it.name == routeName }?.route
?: ModuleRoute.entries.find { it.name == routeName }?.route
}
@Suppress("LongMethod", "CyclomaticComplexMethod") @Suppress("LongMethod", "CyclomaticComplexMethod")
@Composable @Composable
fun RadioConfigScreen( fun RadioConfigScreen(

Wyświetl plik

@ -23,14 +23,22 @@ import android.os.RemoteException
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.compose.foundation.Image import androidx.compose.foundation.Image
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ChevronRight
import androidx.compose.material.icons.twotone.Check import androidx.compose.material.icons.twotone.Check
import androidx.compose.material.icons.twotone.Close import androidx.compose.material.icons.twotone.Close
import androidx.compose.material.icons.twotone.ContentCopy import androidx.compose.material.icons.twotone.ContentCopy
@ -56,6 +64,7 @@ import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.runtime.snapshots.SnapshotStateList
import androidx.compose.runtime.toMutableStateList import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.asImageBitmap import androidx.compose.ui.graphics.asImageBitmap
import androidx.compose.ui.graphics.painter.BitmapPainter import androidx.compose.ui.graphics.painter.BitmapPainter
@ -64,13 +73,14 @@ import androidx.compose.ui.platform.ClipEntry
import androidx.compose.ui.platform.LocalClipboard import androidx.compose.ui.platform.LocalClipboard
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.text.input.KeyboardType import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.tooling.preview.PreviewScreenSizes import androidx.compose.ui.tooling.preview.PreviewScreenSizes
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.hilt.navigation.compose.hiltViewModel import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.compose.collectAsStateWithLifecycle
@ -85,24 +95,21 @@ import com.geeksville.mesh.android.GeeksvilleApplication
import com.geeksville.mesh.android.getCameraPermissions import com.geeksville.mesh.android.getCameraPermissions
import com.geeksville.mesh.android.hasCameraPermission import com.geeksville.mesh.android.hasCameraPermission
import com.geeksville.mesh.channelSet import com.geeksville.mesh.channelSet
import com.geeksville.mesh.channelSettings
import com.geeksville.mesh.copy import com.geeksville.mesh.copy
import com.geeksville.mesh.model.Channel import com.geeksville.mesh.model.Channel
import com.geeksville.mesh.model.ChannelOption
import com.geeksville.mesh.model.UIViewModel import com.geeksville.mesh.model.UIViewModel
import com.geeksville.mesh.model.getChannelUrl import com.geeksville.mesh.model.getChannelUrl
import com.geeksville.mesh.model.qrCode import com.geeksville.mesh.model.qrCode
import com.geeksville.mesh.model.toChannelSet import com.geeksville.mesh.model.toChannelSet
import com.geeksville.mesh.navigation.ConfigRoute
import com.geeksville.mesh.navigation.Route
import com.geeksville.mesh.navigation.getNavRouteFrom
import com.geeksville.mesh.service.MeshService import com.geeksville.mesh.service.MeshService
import com.geeksville.mesh.ui.radioconfig.RadioConfigViewModel
import com.geeksville.mesh.ui.common.components.AdaptiveTwoPane import com.geeksville.mesh.ui.common.components.AdaptiveTwoPane
import com.geeksville.mesh.ui.common.components.DropDownPreference
import com.geeksville.mesh.ui.common.components.PreferenceFooter import com.geeksville.mesh.ui.common.components.PreferenceFooter
import com.geeksville.mesh.ui.common.components.dragContainer
import com.geeksville.mesh.ui.common.components.dragDropItemsIndexed
import com.geeksville.mesh.ui.common.components.rememberDragDropState
import com.geeksville.mesh.ui.radioconfig.components.ChannelCard
import com.geeksville.mesh.ui.radioconfig.components.ChannelSelection import com.geeksville.mesh.ui.radioconfig.components.ChannelSelection
import com.geeksville.mesh.ui.radioconfig.components.EditChannelDialog import com.geeksville.mesh.ui.radioconfig.components.PacketResponseStateDialog
import com.journeyapps.barcodescanner.ScanContract import com.journeyapps.barcodescanner.ScanContract
import com.journeyapps.barcodescanner.ScanOptions import com.journeyapps.barcodescanner.ScanOptions
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -111,20 +118,44 @@ import kotlinx.coroutines.launch
@Composable @Composable
fun ChannelScreen( fun ChannelScreen(
viewModel: UIViewModel = hiltViewModel(), viewModel: UIViewModel = hiltViewModel(),
radioConfigViewModel: RadioConfigViewModel = hiltViewModel(),
onNavigate: (Route) -> Unit
) { ) {
val context = LocalContext.current val context = LocalContext.current
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val connectionState by viewModel.connectionState.collectAsStateWithLifecycle() val connectionState by viewModel.connectionState.collectAsStateWithLifecycle()
val radioConfigState by radioConfigViewModel.radioConfigState.collectAsStateWithLifecycle()
val enabled = connectionState == MeshService.ConnectionState.CONNECTED && !viewModel.isManaged val enabled = connectionState == MeshService.ConnectionState.CONNECTED && !viewModel.isManaged
val channels by viewModel.channels.collectAsStateWithLifecycle() val channels by viewModel.channels.collectAsStateWithLifecycle()
var channelSet by remember(channels) { mutableStateOf(channels) } var channelSet by remember(channels) { mutableStateOf(channels) }
var showChannelEditor by rememberSaveable { mutableStateOf(false) } val modemPresetName by remember(channels) {
var showSendDialog by remember { mutableStateOf(false) } mutableStateOf(Channel(loraConfig = channels.loraConfig).name)
}
var showResetDialog by remember { mutableStateOf(false) } var showResetDialog by remember { mutableStateOf(false) }
var showScanDialog by remember { mutableStateOf(false) } var showScanDialog by remember { mutableStateOf(false) }
val isEditing = channelSet != channels || showChannelEditor
/* Animate waiting for the channel configurations */
var isWaiting by remember { mutableStateOf(false) }
if (isWaiting) {
PacketResponseStateDialog(
state = radioConfigState.responseState,
onDismiss = {
isWaiting = false
radioConfigViewModel.clearPacketResponse()
},
onComplete = {
getNavRouteFrom(radioConfigState.route)?.let { route ->
isWaiting = false
radioConfigViewModel.clearPacketResponse()
onNavigate(route)
}
},
)
}
/* Holds selections made by the user for QR generation. */ /* Holds selections made by the user for QR generation. */
val channelSelections = rememberSaveable( val channelSelections = rememberSaveable(
@ -139,7 +170,6 @@ fun ChannelScreen(
settings.clear() settings.clear()
settings.addAll(result) settings.addAll(result)
} }
val modemPresetName = Channel(loraConfig = channelSet.loraConfig).name
val barcodeLauncher = rememberLauncherForActivityResult(ScanContract()) { result -> val barcodeLauncher = rememberLauncherForActivityResult(ScanContract()) { result ->
if (result.contents != null) { if (result.contents != null) {
@ -147,19 +177,6 @@ fun ChannelScreen(
} }
} }
fun updateSettingsList(update: MutableList<ChannelProtos.ChannelSettings>.() -> Unit) {
try {
val list = channelSet.settingsList.toMutableList()
list.update()
channelSet = channelSet.copy {
settings.clear()
settings.addAll(list)
}
} catch (ex: Exception) {
errormsg("Error updating ChannelSettings list:", ex)
}
}
fun zxingScan() { fun zxingScan() {
debug("Starting zxing QR code scanner") debug("Starting zxing QR code scanner")
val zxingScan = ScanOptions() val zxingScan = ScanOptions()
@ -209,8 +226,6 @@ fun ChannelScreen(
// Tell the user to try again // Tell the user to try again
viewModel.showSnackbar(R.string.cant_change_no_radio) viewModel.showSnackbar(R.string.cant_change_no_radio)
} finally {
showChannelEditor = false
} }
} }
@ -255,146 +270,51 @@ fun ChannelScreen(
) )
} }
if (showSendDialog) {
AlertDialog(
onDismissRequest = {
showSendDialog = false
showChannelEditor = false
channelSet = channels
},
title = { Text(text = stringResource(id = R.string.change_channel)) },
text = { Text(text = stringResource(id = R.string.are_you_sure_channel)) },
confirmButton = {
TextButton(onClick = {
installSettings(channelSet)
showSendDialog = false
}) { Text(text = stringResource(id = R.string.accept)) }
installSettings(channelSet)
}
)
}
var showEditChannelDialog: Int? by remember { mutableStateOf(null) }
if (showEditChannelDialog != null) {
val index = showEditChannelDialog ?: return
EditChannelDialog(
channelSettings = with(channelSet) {
if (settingsCount > index) getSettings(index) else channelSettings { }
},
modemPresetName = modemPresetName,
onAddClick = {
with(channelSet) {
if (settingsCount > index) {
channelSet = copy { settings[index] = it }
} else {
channelSet = copy { settings.add(it) }
}
}
showEditChannelDialog = null
},
onDismissRequest = { showEditChannelDialog = null }
)
}
val listState = rememberLazyListState() val listState = rememberLazyListState()
val dragDropState = rememberDragDropState(listState) { fromIndex, toIndex ->
updateSettingsList { add(toIndex, removeAt(fromIndex)) }
}
LazyColumn( LazyColumn(
modifier = Modifier.dragContainer(
dragDropState = dragDropState,
haptics = LocalHapticFeedback.current,
),
state = listState, state = listState,
contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp), contentPadding = PaddingValues(horizontal = 24.dp, vertical = 16.dp),
) { ) {
if (!showChannelEditor) {
item {
ChannelListView(
enabled = enabled,
channelSet = channelSet,
modemPresetName = modemPresetName,
channelSelections = channelSelections,
onClick = { showChannelEditor = true }
)
EditChannelUrl(
enabled = enabled,
channelUrl = selectedChannelSet.getChannelUrl(),
onConfirm = viewModel::requestChannelUrl
)
}
} else {
dragDropItemsIndexed(
items = channelSet.settingsList,
dragDropState = dragDropState,
) { index, channel, isDragging ->
ChannelCard(
index = index,
title = channel.name.ifEmpty { modemPresetName },
enabled = enabled,
onEditClick = { showEditChannelDialog = index },
onDeleteClick = { updateSettingsList { removeAt(index) } }
)
}
item {
OutlinedButton(
modifier = Modifier.fillMaxWidth(),
onClick = {
channelSet = channelSet.copy {
settings.add(channelSettings { psk = Channel.default.settings.psk })
}
showEditChannelDialog = channelSet.settingsList.lastIndex
},
enabled = enabled && viewModel.maxChannels > channelSet.settingsCount,
) { Text(text = stringResource(R.string.add)) }
}
}
item { item {
DropDownPreference( ChannelListView(
title = stringResource(id = R.string.channel_options),
enabled = enabled, enabled = enabled,
items = ChannelOption.entries channelSet = channelSet,
.map { it.modemPreset to stringResource(it.configRes) }, modemPresetName = modemPresetName,
selectedItem = channelSet.loraConfig.modemPreset, channelSelections = channelSelections,
onItemSelected = { onClick = {
val lora = channelSet.loraConfig.copy { modemPreset = it } isWaiting = true
channelSet = channelSet.copy { loraConfig = lora } radioConfigViewModel.setResponseStateLoading(ConfigRoute.CHANNELS)
} }
) )
EditChannelUrl(
enabled = enabled,
channelUrl = selectedChannelSet.getChannelUrl(),
onConfirm = viewModel::requestChannelUrl
)
} }
item { item {
if (isEditing) { ModemPresetInfo(
PreferenceFooter( modemPresetName = modemPresetName,
enabled = enabled, onClick = {
onCancelClicked = { isWaiting = true
focusManager.clearFocus() radioConfigViewModel.setResponseStateLoading(ConfigRoute.LORA)
showChannelEditor = false }
channelSet = channels )
}, }
onSaveClicked = { item {
focusManager.clearFocus() PreferenceFooter(
showSendDialog = true enabled = enabled,
} negativeText = R.string.reset,
) onNegativeClicked = {
} else { focusManager.clearFocus()
PreferenceFooter( showResetDialog = true
enabled = enabled, },
negativeText = R.string.reset, positiveText = R.string.scan,
onNegativeClicked = { onPositiveClicked = {
focusManager.clearFocus() focusManager.clearFocus()
showResetDialog = true if (context.hasCameraPermission()) zxingScan() else showScanDialog = true
}, }
positiveText = R.string.scan, )
onPositiveClicked = {
focusManager.clearFocus()
if (context.hasCameraPermission()) zxingScan() else showScanDialog = true
}
)
}
} }
} }
} }
@ -433,6 +353,7 @@ private fun EditChannelUrl(
enabled = enabled, enabled = enabled,
label = { Text(stringResource(R.string.url)) }, label = { Text(stringResource(R.string.url)) },
isError = isError, isError = isError,
shape = RoundedCornerShape(8.dp),
trailingIcon = { trailingIcon = {
val label = stringResource(R.string.url) val label = stringResource(R.string.url)
val isUrlEqual = valueState == channelUrl val isUrlEqual = valueState == channelUrl
@ -560,6 +481,55 @@ private fun ChannelListView(
) )
} }
@Composable
private fun ModemPresetInfo(
modemPresetName: String,
onClick: () -> Unit
) {
Row(
modifier = Modifier
.padding(top = 12.dp)
.fillMaxWidth()
.clickable(onClick = onClick)
.border(
1.dp,
MaterialTheme.colorScheme.onBackground,
RoundedCornerShape(8.dp)
),
verticalAlignment = Alignment.CenterVertically
) {
Column(
modifier = Modifier
.weight(1f)
.padding(16.dp)
) {
Text(
text = stringResource(R.string.modem_preset),
fontSize = 16.sp,
)
Text(
text = modemPresetName,
fontSize = 14.sp,
)
}
Spacer(modifier = Modifier.width(16.dp))
Icon(
imageVector = Icons.Default.ChevronRight,
contentDescription = stringResource(R.string.navigate_into_label),
modifier = Modifier.padding(end = 16.dp)
)
}
}
@Preview(showBackground = true)
@Composable
fun ModemPresetInfoPreview() {
ModemPresetInfo(
modemPresetName = "Long Fast",
onClick = {}
)
}
@PreviewScreenSizes @PreviewScreenSizes
@Composable @Composable
private fun ChannelScreenPreview() { private fun ChannelScreenPreview() {

Wyświetl plik

@ -481,7 +481,7 @@
<string name="use_i2s_as_buzzer">Use I2S as buzzer</string> <string name="use_i2s_as_buzzer">Use I2S as buzzer</string>
<string name="lora_config">LoRa Config</string> <string name="lora_config">LoRa Config</string>
<string name="use_modem_preset">Use modem preset</string> <string name="use_modem_preset">Use modem preset</string>
<string name="modem_preset">Modem preset</string> <string name="modem_preset">Modem Preset</string>
<string name="bandwidth">Bandwidth</string> <string name="bandwidth">Bandwidth</string>
<string name="spread_factor">Spread factor</string> <string name="spread_factor">Spread factor</string>
<string name="coding_rate">Coding rate</string> <string name="coding_rate">Coding rate</string>
@ -655,6 +655,7 @@
<string name="disk_free">Disk Free</string> <string name="disk_free">Disk Free</string>
<string name="load">Load</string> <string name="load">Load</string>
<string name="user_string">User String</string> <string name="user_string">User String</string>
<string name="navigate_into_label">Navigate Into</string>
<string name="connections">Connections</string> <string name="connections">Connections</string>
<string name="map">Map</string> <string name="map">Map</string>
<string name="contacts">Contacts</string> <string name="contacts">Contacts</string>