Skip to content

Commit 6ebece1

Browse files
PM-19978: Build out flight recorder UI (#5009)
1 parent c540d3e commit 6ebece1

File tree

8 files changed

+374
-7
lines changed

8 files changed

+374
-7
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package com.x8bit.bitwarden.data.platform.repository.model
2+
3+
/**
4+
* The selectable durations allowed for the flight recorder.
5+
*/
6+
enum class FlightRecorderDuration(
7+
val milliseconds: Long,
8+
) {
9+
ONE_HOUR(milliseconds = 3_600_000L),
10+
EIGHT_HOURS(milliseconds = 28_800_000L),
11+
TWENTY_FOUR_HOURS(milliseconds = 86_400_000L),
12+
ONE_WEEK(milliseconds = 604_800_000L),
13+
}

app/src/main/java/com/x8bit/bitwarden/ui/platform/components/text/BitwardenHyperTextLink.kt

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import androidx.compose.ui.text.TextStyle
1212
import androidx.compose.ui.text.style.TextAlign
1313
import com.x8bit.bitwarden.ui.platform.base.util.toAnnotatedString
1414
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
15+
import com.x8bit.bitwarden.ui.platform.util.spanStyleOf
1516

1617
/**
1718
* Uses an annotated string resource to create a string with clickable text.
@@ -30,7 +31,10 @@ fun BitwardenHyperTextLink(
3031
color: Color = BitwardenTheme.colorScheme.text.secondary,
3132
) {
3233
Text(
33-
text = annotatedResId.toAnnotatedString(args = args) { key ->
34+
text = annotatedResId.toAnnotatedString(
35+
args = args,
36+
style = spanStyleOf(color = color, textStyle = style),
37+
) { key ->
3438
when (key) {
3539
annotationKey -> onClick()
3640
}

app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderScreen.kt

+152-1
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,45 @@
11
package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder
22

3+
import androidx.compose.foundation.layout.Column
4+
import androidx.compose.foundation.layout.Spacer
5+
import androidx.compose.foundation.layout.fillMaxWidth
6+
import androidx.compose.foundation.layout.height
7+
import androidx.compose.foundation.layout.navigationBarsPadding
8+
import androidx.compose.foundation.rememberScrollState
9+
import androidx.compose.foundation.verticalScroll
310
import androidx.compose.material3.ExperimentalMaterial3Api
11+
import androidx.compose.material3.Text
412
import androidx.compose.material3.TopAppBarDefaults
513
import androidx.compose.material3.rememberTopAppBarState
614
import androidx.compose.runtime.Composable
15+
import androidx.compose.runtime.getValue
716
import androidx.compose.runtime.remember
817
import androidx.compose.ui.Modifier
918
import androidx.compose.ui.input.nestedscroll.nestedScroll
19+
import androidx.compose.ui.platform.LocalContext
20+
import androidx.compose.ui.platform.testTag
1021
import androidx.compose.ui.res.stringResource
22+
import androidx.compose.ui.text.style.TextAlign
23+
import androidx.compose.ui.unit.dp
24+
import androidx.core.net.toUri
1125
import androidx.hilt.navigation.compose.hiltViewModel
26+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
1227
import com.x8bit.bitwarden.R
28+
import com.x8bit.bitwarden.data.platform.repository.model.FlightRecorderDuration
1329
import com.x8bit.bitwarden.ui.platform.base.util.EventsEffect
30+
import com.x8bit.bitwarden.ui.platform.base.util.standardHorizontalMargin
1431
import com.x8bit.bitwarden.ui.platform.components.appbar.BitwardenTopAppBar
32+
import com.x8bit.bitwarden.ui.platform.components.button.BitwardenTextButton
33+
import com.x8bit.bitwarden.ui.platform.components.dropdown.BitwardenMultiSelectButton
34+
import com.x8bit.bitwarden.ui.platform.components.model.CardStyle
1535
import com.x8bit.bitwarden.ui.platform.components.scaffold.BitwardenScaffold
36+
import com.x8bit.bitwarden.ui.platform.components.text.BitwardenHyperTextLink
1637
import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
38+
import com.x8bit.bitwarden.ui.platform.composition.LocalIntentManager
39+
import com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder.util.displayText
40+
import com.x8bit.bitwarden.ui.platform.manager.intent.IntentManager
41+
import com.x8bit.bitwarden.ui.platform.theme.BitwardenTheme
42+
import kotlinx.collections.immutable.toImmutableList
1743

1844
/**
1945
* Displays the flight recorder configuration screen.
@@ -23,10 +49,15 @@ import com.x8bit.bitwarden.ui.platform.components.util.rememberVectorPainter
2349
fun FlightRecorderScreen(
2450
onNavigateBack: () -> Unit,
2551
viewModel: FlightRecorderViewModel = hiltViewModel(),
52+
intentManager: IntentManager = LocalIntentManager.current,
2653
) {
54+
val state by viewModel.stateFlow.collectAsStateWithLifecycle()
2755
EventsEffect(viewModel) { event ->
2856
when (event) {
2957
FlightRecorderEvent.NavigateBack -> onNavigateBack()
58+
FlightRecorderEvent.NavigateToHelpCenter -> {
59+
intentManager.launchUri(uri = "https://bitwarden.com/help".toUri())
60+
}
3061
}
3162
}
3263
val scrollBehavior = TopAppBarDefaults.pinnedScrollBehavior(rememberTopAppBarState())
@@ -40,10 +71,130 @@ fun FlightRecorderScreen(
4071
{ viewModel.trySendAction(FlightRecorderAction.BackClick) }
4172
},
4273
scrollBehavior = scrollBehavior,
74+
actions = {
75+
BitwardenTextButton(
76+
label = stringResource(id = R.string.save),
77+
onClick = remember(viewModel) {
78+
{ viewModel.trySendAction(FlightRecorderAction.SaveClick) }
79+
},
80+
modifier = Modifier.testTag("SaveButton"),
81+
)
82+
},
4383
)
4484
},
4585
modifier = Modifier.nestedScroll(scrollBehavior.nestedScrollConnection),
4686
) {
47-
// TODO: PM-19592 Create the flight recorder UI.
87+
FlightRecorderContent(
88+
state = state,
89+
onDurationSelected = remember(viewModel) {
90+
{ viewModel.trySendAction(FlightRecorderAction.DurationSelect(it)) }
91+
},
92+
onHelpCenterClick = remember(viewModel) {
93+
{ viewModel.trySendAction(FlightRecorderAction.HelpCenterClick) }
94+
},
95+
)
96+
}
97+
}
98+
99+
@Suppress("LongMethod")
100+
@Composable
101+
private fun FlightRecorderContent(
102+
state: FlightRecorderState,
103+
onDurationSelected: (FlightRecorderDuration) -> Unit,
104+
onHelpCenterClick: () -> Unit,
105+
modifier: Modifier = Modifier,
106+
) {
107+
Column(
108+
modifier = modifier
109+
.verticalScroll(state = rememberScrollState()),
110+
) {
111+
Spacer(modifier = Modifier.height(height = 24.dp))
112+
Text(
113+
text = stringResource(id = R.string.experiencing_an_issue),
114+
color = BitwardenTheme.colorScheme.text.primary,
115+
style = BitwardenTheme.typography.titleMedium,
116+
textAlign = TextAlign.Center,
117+
modifier = Modifier
118+
.fillMaxWidth()
119+
.standardHorizontalMargin(),
120+
)
121+
Spacer(modifier = Modifier.height(height = 12.dp))
122+
Text(
123+
text = stringResource(
124+
id = R.string.enable_temporary_logging_to_collect_and_inspect_logs_locally,
125+
),
126+
color = BitwardenTheme.colorScheme.text.primary,
127+
style = BitwardenTheme.typography.bodyMedium,
128+
textAlign = TextAlign.Center,
129+
modifier = Modifier
130+
.fillMaxWidth()
131+
.standardHorizontalMargin(),
132+
)
133+
Spacer(modifier = Modifier.height(height = 12.dp))
134+
Text(
135+
text = stringResource(id = R.string.to_get_started_set_a_logging_duration),
136+
color = BitwardenTheme.colorScheme.text.primary,
137+
style = BitwardenTheme.typography.bodyMedium,
138+
textAlign = TextAlign.Center,
139+
modifier = Modifier
140+
.fillMaxWidth()
141+
.standardHorizontalMargin(),
142+
)
143+
Spacer(modifier = Modifier.height(height = 24.dp))
144+
DurationSelectButton(
145+
selectedOption = state.selectedDuration,
146+
onOptionSelected = onDurationSelected,
147+
modifier = Modifier
148+
.fillMaxWidth()
149+
.standardHorizontalMargin(),
150+
)
151+
Spacer(modifier = Modifier.height(height = 24.dp))
152+
Text(
153+
text = stringResource(id = R.string.logs_will_be_automatically_deleted_after_30_days),
154+
color = BitwardenTheme.colorScheme.text.secondary,
155+
style = BitwardenTheme.typography.bodySmall,
156+
textAlign = TextAlign.Center,
157+
modifier = Modifier
158+
.fillMaxWidth()
159+
.standardHorizontalMargin(),
160+
)
161+
Spacer(modifier = Modifier.height(height = 8.dp))
162+
BitwardenHyperTextLink(
163+
annotatedResId = R.string.for_details_on_what_is_and_isnt_logged,
164+
annotationKey = "helpCenter",
165+
accessibilityString = stringResource(id = R.string.bitwarden_help_center),
166+
onClick = onHelpCenterClick,
167+
color = BitwardenTheme.colorScheme.text.secondary,
168+
style = BitwardenTheme.typography.bodySmall,
169+
modifier = Modifier
170+
.fillMaxWidth()
171+
.standardHorizontalMargin(),
172+
)
173+
Spacer(modifier = Modifier.height(height = 16.dp))
174+
Spacer(modifier = Modifier.navigationBarsPadding())
48175
}
49176
}
177+
178+
@Composable
179+
private fun DurationSelectButton(
180+
selectedOption: FlightRecorderDuration,
181+
onOptionSelected: (FlightRecorderDuration) -> Unit,
182+
modifier: Modifier = Modifier,
183+
) {
184+
val resources = LocalContext.current.resources
185+
val options = FlightRecorderDuration.entries.map { it.displayText() }.toImmutableList()
186+
BitwardenMultiSelectButton(
187+
label = stringResource(id = R.string.logging_duration),
188+
options = options,
189+
selectedOption = selectedOption.displayText(),
190+
onOptionSelected = { selectedOption ->
191+
onOptionSelected(
192+
FlightRecorderDuration
193+
.entries
194+
.first { selectedOption == it.displayText.toString(resources) },
195+
)
196+
},
197+
cardStyle = CardStyle.Full,
198+
modifier = modifier,
199+
)
200+
}

app/src/main/java/com/x8bit/bitwarden/ui/platform/feature/settings/flightrecorder/FlightRecorderViewModel.kt

+46-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder
22

33
import androidx.lifecycle.SavedStateHandle
4+
import com.x8bit.bitwarden.data.platform.repository.model.FlightRecorderDuration
45
import com.x8bit.bitwarden.ui.platform.base.BaseViewModel
56
import dagger.hilt.android.lifecycle.HiltViewModel
7+
import kotlinx.coroutines.flow.update
68
import javax.inject.Inject
79

810
private const val KEY_STATE = "state"
@@ -15,23 +17,43 @@ class FlightRecorderViewModel @Inject constructor(
1517
savedStateHandle: SavedStateHandle,
1618
) : BaseViewModel<FlightRecorderState, FlightRecorderEvent, FlightRecorderAction>(
1719
// We load the state from the savedStateHandle for testing purposes.
18-
initialState = savedStateHandle[KEY_STATE] ?: FlightRecorderState,
20+
initialState = savedStateHandle[KEY_STATE]
21+
?: FlightRecorderState(
22+
selectedDuration = FlightRecorderDuration.ONE_HOUR,
23+
),
1924
) {
2025
override fun handleAction(action: FlightRecorderAction) {
2126
when (action) {
2227
FlightRecorderAction.BackClick -> handleBackClick()
28+
is FlightRecorderAction.DurationSelect -> handleOnDurationSelect(action)
29+
FlightRecorderAction.HelpCenterClick -> handleHelpCenterClick()
30+
FlightRecorderAction.SaveClick -> handleSaveClick()
2331
}
2432
}
2533

2634
private fun handleBackClick() {
2735
sendEvent(FlightRecorderEvent.NavigateBack)
2836
}
37+
38+
private fun handleOnDurationSelect(action: FlightRecorderAction.DurationSelect) {
39+
mutableStateFlow.update { it.copy(selectedDuration = action.duration) }
40+
}
41+
42+
private fun handleHelpCenterClick() {
43+
sendEvent(FlightRecorderEvent.NavigateToHelpCenter)
44+
}
45+
46+
private fun handleSaveClick() {
47+
// TODO: PM-19592 Persist the flight recorder state.
48+
}
2949
}
3050

3151
/**
3252
* Models the UI state for the flight recorder screen.
3353
*/
34-
data object FlightRecorderState
54+
data class FlightRecorderState(
55+
val selectedDuration: FlightRecorderDuration,
56+
)
3557

3658
/**
3759
* Models events for the flight recorder screen.
@@ -41,6 +63,11 @@ sealed class FlightRecorderEvent {
4163
* Navigates back.
4264
*/
4365
data object NavigateBack : FlightRecorderEvent()
66+
67+
/**
68+
* Launches the the help center link.
69+
*/
70+
data object NavigateToHelpCenter : FlightRecorderEvent()
4471
}
4572

4673
/**
@@ -51,4 +78,21 @@ sealed class FlightRecorderAction {
5178
* Indicates that the user clicked the close button.
5279
*/
5380
data object BackClick : FlightRecorderAction()
81+
82+
/**
83+
* Indicates that the user clicked the help center link.
84+
*/
85+
data object HelpCenterClick : FlightRecorderAction()
86+
87+
/**
88+
* Indicates that the user clicked the save button.
89+
*/
90+
data object SaveClick : FlightRecorderAction()
91+
92+
/**
93+
* Indicates that the user selected a duration.
94+
*/
95+
data class DurationSelect(
96+
val duration: FlightRecorderDuration,
97+
) : FlightRecorderAction()
5498
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.x8bit.bitwarden.ui.platform.feature.settings.flightrecorder.util
2+
3+
import com.x8bit.bitwarden.R
4+
import com.x8bit.bitwarden.data.platform.repository.model.FlightRecorderDuration
5+
import com.x8bit.bitwarden.ui.platform.base.util.Text
6+
import com.x8bit.bitwarden.ui.platform.base.util.asText
7+
8+
/**
9+
* A helper function to map the [FlightRecorderDuration] to a displayable label.
10+
*/
11+
val FlightRecorderDuration.displayText: Text
12+
get() = when (this) {
13+
FlightRecorderDuration.ONE_HOUR -> R.string.flight_recorder_one_hour.asText()
14+
FlightRecorderDuration.EIGHT_HOURS -> R.string.flight_recorder_eight_hours.asText()
15+
FlightRecorderDuration.TWENTY_FOUR_HOURS -> {
16+
R.string.flight_recorder_twenty_four_hours.asText()
17+
}
18+
19+
FlightRecorderDuration.ONE_WEEK -> R.string.flight_recorder_one_week.asText()
20+
}

app/src/main/res/values/strings.xml

+10
Original file line numberDiff line numberDiff line change
@@ -1226,4 +1226,14 @@ Do you want to switch to this account?</string>
12261226
<string name="enable_flight_recorder_title">Enable flight recorder</string>
12271227
<string name="recorded_logs_title">Recorded logs</string>
12281228
<string name="no_logs_recorded">No logs recorded</string>
1229+
<string name="experiencing_an_issue">Experiencing an issue?</string>
1230+
<string name="enable_temporary_logging_to_collect_and_inspect_logs_locally">Enable temporary logging to collect and inspect logs locally. When enabled, application states and network calls may be logged—never sensitive vault information.</string>
1231+
<string name="to_get_started_set_a_logging_duration">To get started, set a logging duration. Logging 
will automatically turn off after this period, giving you time to capture activity while reproducing 
any issues.</string>
1232+
<string name="logs_will_be_automatically_deleted_after_30_days">Logs will be automatically deleted after 30 days. Bitwarden is only able to access your log data when you share it.</string>
1233+
<string name="for_details_on_what_is_and_isnt_logged">For details on what is and isn’t logged, visit the\n<annotation link="helpCenter">Bitwarden help center.</annotation></string>
1234+
<string name="logging_duration">Logging duration</string>
1235+
<string name="flight_recorder_one_hour">1 hour</string>
1236+
<string name="flight_recorder_eight_hours">8 hours</string>
1237+
<string name="flight_recorder_twenty_four_hours">24 hours</string>
1238+
<string name="flight_recorder_one_week">1 week</string>
12291239
</resources>

0 commit comments

Comments
 (0)