diff --git a/CHANGELOG.md b/CHANGELOG.md index c84df59a903e..aaa69f91842e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,8 @@ Line wrap the file at 100 chars. Th - Migrate out of time view to compose. - Migrate login view to compose. - Add Social media to content blockers. +- Migrate Report Problem view to compose. +- Migrate View Logs view to compose. #### Linux - Don't block forwarding of traffic when the split tunnel mark (ct mark) is set. diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt index e0f84db5ea21..0d032f962ab2 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scaffolding.kt @@ -34,6 +34,7 @@ fun ScaffoldWithTopBar( topBarColor: Color, statusBarColor: Color, navigationBarColor: Color, + modifier: Modifier = Modifier, iconTintColor: Color = MaterialTheme.colorScheme.onPrimary.copy(alpha = AlphaTopBar), onSettingsClicked: (() -> Unit)?, onAccountClicked: (() -> Unit)?, @@ -48,6 +49,7 @@ fun ScaffoldWithTopBar( } Scaffold( + modifier = modifier, topBar = { TopBar( backgroundColor = topBarColor, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scrollbar.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scrollbar.kt index 35ddc585923f..52f2f1d7268e 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scrollbar.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/component/Scrollbar.kt @@ -72,19 +72,33 @@ import kotlinx.coroutines.flow.collectLatest fun Modifier.drawHorizontalScrollbar( state: ScrollState, reverseScrolling: Boolean = false -): Modifier = drawScrollbar(state, Orientation.Horizontal, reverseScrolling) +): Modifier = composed { drawScrollbar(state, Orientation.Horizontal, BarColor, reverseScrolling) } fun Modifier.drawVerticalScrollbar( state: ScrollState, reverseScrolling: Boolean = false -): Modifier = drawScrollbar(state, Orientation.Vertical, reverseScrolling) +): Modifier = composed { drawScrollbar(state, Orientation.Vertical, BarColor, reverseScrolling) } + +fun Modifier.drawHorizontalScrollbar( + state: ScrollState, + color: Color, + reverseScrolling: Boolean = false +): Modifier = drawScrollbar(state, Orientation.Horizontal, color, reverseScrolling) + +fun Modifier.drawVerticalScrollbar( + state: ScrollState, + color: Color, + reverseScrolling: Boolean = false +): Modifier = drawScrollbar(state, Orientation.Vertical, color, reverseScrolling) private fun Modifier.drawScrollbar( state: ScrollState, orientation: Orientation, + color: Color, reverseScrolling: Boolean ): Modifier = - drawScrollbar(orientation, reverseScrolling) { reverseDirection, atEnd, color, alpha -> + drawScrollbar(orientation, color, reverseScrolling) { reverseDirection, atEnd, paintColor, alpha + -> if (state.maxValue > 0) { val canvasSize = if (orientation == Orientation.Horizontal) size.width else size.height val totalSize = canvasSize + state.maxValue @@ -94,7 +108,7 @@ private fun Modifier.drawScrollbar( orientation, reverseDirection, atEnd, - color, + paintColor, alpha, thumbSize, startOffset @@ -105,19 +119,21 @@ private fun Modifier.drawScrollbar( fun Modifier.drawHorizontalScrollbar( state: LazyListState, reverseScrolling: Boolean = false -): Modifier = drawScrollbar(state, Orientation.Horizontal, reverseScrolling) +): Modifier = composed { drawScrollbar(state, Orientation.Horizontal, BarColor, reverseScrolling) } fun Modifier.drawVerticalScrollbar( state: LazyListState, reverseScrolling: Boolean = false -): Modifier = drawScrollbar(state, Orientation.Vertical, reverseScrolling) +): Modifier = composed { drawScrollbar(state, Orientation.Vertical, BarColor, reverseScrolling) } private fun Modifier.drawScrollbar( state: LazyListState, orientation: Orientation, + color: Color, reverseScrolling: Boolean ): Modifier = - drawScrollbar(orientation, reverseScrolling) { reverseDirection, atEnd, color, alpha -> + drawScrollbar(orientation, color, reverseScrolling) { reverseDirection, atEnd, paintColor, alpha + -> val layoutInfo = state.layoutInfo val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset val items = layoutInfo.visibleItemsInfo @@ -137,7 +153,7 @@ private fun Modifier.drawScrollbar( orientation, reverseDirection, atEnd, - color, + paintColor, alpha, thumbSize, startOffset @@ -148,9 +164,14 @@ private fun Modifier.drawScrollbar( fun Modifier.drawVerticalScrollbar( state: LazyGridState, spanCount: Int, - reverseScrolling: Boolean = false + color: Color, + reverseScrolling: Boolean = false, ): Modifier = - drawScrollbar(Orientation.Vertical, reverseScrolling) { reverseDirection, atEnd, color, alpha -> + drawScrollbar(Orientation.Vertical, color, reverseScrolling) { + reverseDirection, + atEnd, + paintColor, + alpha -> val layoutInfo = state.layoutInfo val viewportSize = layoutInfo.viewportEndOffset - layoutInfo.viewportStartOffset val items = layoutInfo.visibleItemsInfo @@ -176,7 +197,7 @@ fun Modifier.drawVerticalScrollbar( Orientation.Vertical, reverseDirection, atEnd, - color, + paintColor, alpha, thumbSize, startOffset @@ -225,6 +246,7 @@ private fun DrawScope.drawScrollbar( private fun Modifier.drawScrollbar( orientation: Orientation, + color: Color, reverseScrolling: Boolean, onDraw: DrawScope.( @@ -269,8 +291,6 @@ private fun Modifier.drawScrollbar( } else reverseScrolling val atEnd = if (orientation == Orientation.Vertical) isLtr else true - val color = BarColor - Modifier.nestedScroll(nestedScrollConnection).drawWithContent { drawContent() onDraw(reverseDirection, atEnd, color, alpha::value) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ReportProblemNoEmailDialog.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ReportProblemNoEmailDialog.kt new file mode 100644 index 000000000000..7417d6ae7c21 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/dialog/ReportProblemNoEmailDialog.kt @@ -0,0 +1,77 @@ +package net.mullvad.mullvadvpn.compose.dialog + +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.size +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.button.ActionButton +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.lib.theme.Dimens + +@Preview +@Composable +private fun PreviewReportProblemNoEmailDialog() { + AppTheme { + ReportProblemNoEmailDialog( + onDismiss = {}, + onConfirm = {}, + ) + } +} + +@Composable +fun ReportProblemNoEmailDialog(onDismiss: () -> Unit, onConfirm: () -> Unit) { + AlertDialog( + onDismissRequest = { onDismiss() }, + icon = { + Icon( + painter = painterResource(id = R.drawable.icon_alert), + contentDescription = null, + modifier = Modifier.size(Dimens.dialogIconHeight), + tint = Color.Unspecified + ) + }, + text = { + Text( + text = stringResource(id = R.string.confirm_no_email), + modifier = Modifier.fillMaxWidth(), + style = MaterialTheme.typography.bodySmall + ) + }, + dismissButton = { + ActionButton( + modifier = Modifier.fillMaxWidth(), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + onClick = onConfirm, + text = stringResource(id = R.string.send_anyway) + ) + }, + confirmButton = { + ActionButton( + modifier = Modifier.fillMaxWidth(), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + onClick = { onDismiss() }, + text = stringResource(id = R.string.back) + ) + }, + containerColor = MaterialTheme.colorScheme.background + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt index 3c4d9e12023e..388963866f1c 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/LoginScreen.kt @@ -13,7 +13,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.offset import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.rememberScrollState @@ -28,7 +27,6 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.material3.TextField -import androidx.compose.material3.TextFieldDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -59,6 +57,7 @@ import net.mullvad.mullvadvpn.compose.state.LoginError import net.mullvad.mullvadvpn.compose.state.LoginState import net.mullvad.mullvadvpn.compose.state.LoginState.* import net.mullvad.mullvadvpn.compose.state.LoginUiState +import net.mullvad.mullvadvpn.compose.textfield.mullvadWhiteTextFieldColors import net.mullvad.mullvadvpn.compose.util.accountTokenVisualTransformation import net.mullvad.mullvadvpn.lib.theme.AlphaTopBar import net.mullvad.mullvadvpn.lib.theme.AppTheme @@ -195,21 +194,7 @@ private fun LoginContent( maxLines = 1, visualTransformation = accountTokenVisualTransformation(), enabled = uiState.loginState is Idle, - colors = - TextFieldDefaults.colors( - focusedTextColor = Color.Black, - unfocusedTextColor = Color.Gray, - disabledTextColor = Color.Gray, - errorTextColor = Color.Black, - cursorColor = MaterialTheme.colorScheme.background, - focusedPlaceholderColor = MaterialTheme.colorScheme.background, - unfocusedPlaceholderColor = MaterialTheme.colorScheme.primary, - focusedLabelColor = MaterialTheme.colorScheme.background, - disabledLabelColor = Color.Gray, - unfocusedLabelColor = MaterialTheme.colorScheme.background, - focusedLeadingIconColor = Color.Black, - unfocusedSupportingTextColor = Color.Black, - ), + colors = mullvadWhiteTextFieldColors(), isError = uiState.loginState.isError(), ) @@ -266,14 +251,13 @@ private fun LoginIcon(loginState: LoginState, modifier: Modifier = Modifier) { } is Loading -> CircularProgressIndicator( - modifier = Modifier.size(Dimens.progressIndicatorSize), + modifier = Modifier.fillMaxSize(), color = MaterialTheme.colorScheme.onPrimary, strokeWidth = Dimens.loadingSpinnerStrokeWidth, strokeCap = StrokeCap.Round ) Success -> Image( - modifier = Modifier.offset(-Dimens.smallPadding, -Dimens.smallPadding), painter = painterResource(id = R.drawable.icon_success), contentDescription = stringResource(id = R.string.logged_in_title), ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt new file mode 100644 index 000000000000..4d524c28dca1 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ReportProblemScreen.kt @@ -0,0 +1,320 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text +import androidx.compose.material3.TextField +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.StrokeCap +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.SpanStyle +import androidx.compose.ui.text.buildAnnotatedString +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.withStyle +import androidx.compose.ui.tooling.preview.Preview +import me.onebone.toolbar.ScrollStrategy +import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.button.ActionButton +import net.mullvad.mullvadvpn.compose.component.CollapsingToolbarScaffold +import net.mullvad.mullvadvpn.compose.component.CollapsingTopBar +import net.mullvad.mullvadvpn.compose.dialog.ReportProblemNoEmailDialog +import net.mullvad.mullvadvpn.compose.textfield.mullvadWhiteTextFieldColors +import net.mullvad.mullvadvpn.dataproxy.SendProblemReportResult +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.viewmodel.ReportProblemUiState +import net.mullvad.mullvadvpn.viewmodel.SendingReportUiState + +@Preview +@Composable +private fun PreviewReportProblemScreen() { + AppTheme { ReportProblemScreen(uiState = ReportProblemUiState()) } +} + +@Preview +@Composable +private fun PreviewReportProblemSendingScreen() { + AppTheme { + ReportProblemScreen(uiState = ReportProblemUiState(false, SendingReportUiState.Sending)) + } +} + +@Preview +@Composable +private fun PreviewReportProblemConfirmNoEmailScreen() { + AppTheme { ReportProblemScreen(uiState = ReportProblemUiState(true)) } +} + +@Preview +@Composable +private fun PreviewReportProblemSuccessScreen() { + AppTheme { + ReportProblemScreen( + uiState = ReportProblemUiState(false, SendingReportUiState.Success("email@mail.com")) + ) + } +} + +@Preview +@Composable +private fun PreviewReportProblemErrorScreen() { + AppTheme { + ReportProblemScreen( + uiState = + ReportProblemUiState( + false, + SendingReportUiState.Error(SendProblemReportResult.Error.CollectLog) + ) + ) + } +} + +@Composable +fun ReportProblemScreen( + uiState: ReportProblemUiState, + onSendReport: (String, String) -> Unit = { _, _ -> }, + onDismissNoEmailDialog: () -> Unit = {}, + onClearSendResult: () -> Unit = {}, + onNavigateToViewLogs: () -> Unit = {}, + onBackClick: () -> Unit = {} +) { + + val scaffoldState = rememberCollapsingToolbarScaffoldState() + val progress = scaffoldState.toolbarState.progress + CollapsingToolbarScaffold( + backgroundColor = MaterialTheme.colorScheme.background, + modifier = Modifier.fillMaxSize(), + state = scaffoldState, + scrollStrategy = ScrollStrategy.ExitUntilCollapsed, + isEnabledWhenCollapsable = false, + toolbar = { + val scaffoldModifier = + Modifier.road( + whenCollapsed = Alignment.TopCenter, + whenExpanded = Alignment.BottomStart + ) + CollapsingTopBar( + backgroundColor = MaterialTheme.colorScheme.background, + onBackClicked = onBackClick, + title = stringResource(id = R.string.report_a_problem), + progress = progress, + modifier = scaffoldModifier, + ) + }, + ) { + var email by rememberSaveable { mutableStateOf("") } + var description by rememberSaveable { mutableStateOf("") } + + // Show sending states + if (uiState.sendingState != null) { + Column( + modifier = + Modifier.fillMaxSize() + .padding(vertical = Dimens.mediumPadding, horizontal = Dimens.sideMargin) + ) { + when (uiState.sendingState) { + SendingReportUiState.Sending -> SendingContent() + is SendingReportUiState.Error -> + ErrorContent({ onSendReport(email, description) }, onClearSendResult) + is SendingReportUiState.Success -> SentContent(uiState.sendingState) + } + return@CollapsingToolbarScaffold + } + } + + // Dialog to show confirm if no email was added + if (uiState.showConfirmNoEmail) { + ReportProblemNoEmailDialog( + onDismiss = onDismissNoEmailDialog, + onConfirm = { onSendReport(email, description) } + ) + } + + Surface(color = MaterialTheme.colorScheme.background) { + Column( + modifier = + Modifier.padding( + start = Dimens.sideMargin, + end = Dimens.sideMargin, + bottom = Dimens.verticalSpace, + ), + verticalArrangement = Arrangement.spacedBy(Dimens.mediumPadding) + ) { + Text(text = stringResource(id = R.string.problem_report_description)) + + TextField( + modifier = Modifier.fillMaxWidth(), + value = email, + onValueChange = { email = it }, + maxLines = 1, + singleLine = true, + placeholder = { Text(text = stringResource(id = R.string.user_email_hint)) }, + colors = mullvadWhiteTextFieldColors() + ) + + TextField( + modifier = Modifier.fillMaxWidth().weight(1f), + value = description, + onValueChange = { description = it }, + placeholder = { Text(stringResource(R.string.user_message_hint)) }, + colors = mullvadWhiteTextFieldColors() + ) + + ActionButton( + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + onClick = onNavigateToViewLogs, + text = stringResource(id = R.string.view_logs) + ) + + ActionButton( + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onSurface + ), + onClick = { onSendReport(email, description) }, + isEnabled = description.isNotEmpty(), + text = stringResource(id = R.string.send) + ) + } + } + } +} + +@Composable +private fun ColumnScope.SendingContent() { + CircularProgressIndicator( + modifier = Modifier.align(Alignment.CenterHorizontally), + strokeCap = StrokeCap.Round, + strokeWidth = Dimens.loadingSpinnerStrokeWidth + ) + Spacer(modifier = Modifier.height(Dimens.problemReportIconToTitlePadding)) + Text( + text = stringResource(id = R.string.sending), + style = MaterialTheme.typography.headlineLarge, + color = MaterialTheme.colorScheme.onBackground + ) +} + +@Composable +private fun ColumnScope.SentContent(sendingState: SendingReportUiState.Success) { + Icon( + painter = painterResource(id = R.drawable.icon_success), + contentDescription = stringResource(id = R.string.sent), + modifier = Modifier.align(Alignment.CenterHorizontally).size(Dimens.dialogIconHeight), + tint = Color.Unspecified + ) + + Spacer(modifier = Modifier.height(Dimens.problemReportIconToTitlePadding)) + Text( + text = stringResource(id = R.string.sent), + style = MaterialTheme.typography.headlineLarge, + color = MaterialTheme.colorScheme.onBackground + ) + Text( + text = + buildAnnotatedString { + withStyle(SpanStyle(color = MaterialTheme.colorScheme.surface)) { + append(stringResource(id = R.string.sent_thanks)) + } + append(" ") + withStyle(SpanStyle(color = MaterialTheme.colorScheme.onPrimary)) { + append(stringResource(id = R.string.we_will_look_into_this)) + } + }, + style = MaterialTheme.typography.bodySmall, + modifier = Modifier.fillMaxWidth() + ) + + Spacer(modifier = Modifier.height(Dimens.smallPadding)) + sendingState.email?.let { + val emailTemplate = stringResource(R.string.sent_contact) + val annotatedEmailString = + remember(it) { + val emailStart = emailTemplate.indexOf('%') + + buildAnnotatedString { + append(emailTemplate.substring(0, emailStart)) + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(sendingState.email) + } + } + } + + Text( + text = annotatedEmailString, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@Composable +private fun ColumnScope.ErrorContent(retry: () -> Unit, onDismiss: () -> Unit) { + Icon( + painter = painterResource(id = R.drawable.icon_fail), + contentDescription = stringResource(id = R.string.failed_to_send), + modifier = Modifier.size(Dimens.dialogIconHeight).align(Alignment.CenterHorizontally), + tint = Color.Unspecified + ) + Spacer(modifier = Modifier.height(Dimens.problemReportIconToTitlePadding)) + Text( + text = stringResource(id = R.string.failed_to_send), + style = MaterialTheme.typography.headlineLarge, + color = MaterialTheme.colorScheme.onBackground, + ) + Text( + text = stringResource(id = R.string.failed_to_send_details), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onBackground, + modifier = Modifier.fillMaxWidth() + ) + Spacer(modifier = Modifier.weight(1f)) + ActionButton( + modifier = Modifier.fillMaxWidth().padding(vertical = Dimens.mediumPadding), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.primary, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + onClick = onDismiss, + text = stringResource(id = R.string.edit_message) + ) + ActionButton( + modifier = Modifier.fillMaxWidth(), + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.onPrimary, + ), + onClick = retry, + text = stringResource(id = R.string.try_again) + ) +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ViewLogsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ViewLogsScreen.kt new file mode 100644 index 000000000000..091d19f480a3 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ViewLogsScreen.kt @@ -0,0 +1,116 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Card +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TextField +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import me.onebone.toolbar.ScrollStrategy +import me.onebone.toolbar.rememberCollapsingToolbarScaffoldState +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.component.CollapsingToolbarScaffold +import net.mullvad.mullvadvpn.compose.component.CollapsingTopBar +import net.mullvad.mullvadvpn.compose.component.drawVerticalScrollbar +import net.mullvad.mullvadvpn.lib.theme.AppTheme +import net.mullvad.mullvadvpn.lib.theme.Dimens +import net.mullvad.mullvadvpn.viewmodel.ViewLogsUiState + +@Preview +@Composable +private fun PreviewViewLogsScreen() { + AppTheme { ViewLogsScreen(uiState = ViewLogsUiState("Lorem ipsum")) } +} + +@Preview +@Composable +private fun PreviewViewLogsLoadingScreen() { + AppTheme { ViewLogsScreen(uiState = ViewLogsUiState()) } +} + +@Composable +fun ViewLogsScreen( + uiState: ViewLogsUiState, + onBackClick: () -> Unit = {}, +) { + + val scaffoldState = rememberCollapsingToolbarScaffoldState() + val progress = scaffoldState.toolbarState.progress + CollapsingToolbarScaffold( + backgroundColor = MaterialTheme.colorScheme.background, + modifier = Modifier.fillMaxSize(), + state = scaffoldState, + scrollStrategy = ScrollStrategy.ExitUntilCollapsed, + isEnabledWhenCollapsable = false, + toolbar = { + val scaffoldModifier = + Modifier.road( + whenCollapsed = Alignment.TopCenter, + whenExpanded = Alignment.BottomStart + ) + CollapsingTopBar( + backgroundColor = MaterialTheme.colorScheme.secondary, + onBackClicked = onBackClick, + title = stringResource(id = R.string.view_logs), + progress = progress, + modifier = scaffoldModifier, + ) + }, + ) { + Card( + modifier = + Modifier.fillMaxSize() + .padding( + start = Dimens.sideMargin, + end = Dimens.sideMargin, + bottom = Dimens.screenVerticalMargin + ), + ) { + if (uiState.isLoading) { + CircularProgressIndicator( + modifier = + Modifier.padding(Dimens.mediumPadding).align(Alignment.CenterHorizontally) + ) + } else { + SelectionContainer { + val scrollState = rememberScrollState() + Column( + modifier = + Modifier.drawVerticalScrollbar( + scrollState, + color = MaterialTheme.colorScheme.primary + ) + ) { + TextField( + modifier = + Modifier.verticalScroll(scrollState) + .padding(horizontal = Dimens.smallPadding), + value = uiState.allLines, + textStyle = MaterialTheme.typography.bodySmall, + onValueChange = {}, + readOnly = true, + colors = + TextFieldDefaults.colors( + focusedTextColor = Color.Black, + unfocusedTextColor = Color.Black, + disabledTextColor = Color.Black, + cursorColor = MaterialTheme.colorScheme.background, + ) + ) + } + } + } + } + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/TextFieldColors.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/TextFieldColors.kt new file mode 100644 index 000000000000..c3060a46d596 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/textfield/TextFieldColors.kt @@ -0,0 +1,24 @@ +package net.mullvad.mullvadvpn.compose.textfield + +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.TextFieldColors +import androidx.compose.material3.TextFieldDefaults +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +fun mullvadWhiteTextFieldColors(): TextFieldColors = + TextFieldDefaults.colors( + focusedTextColor = Color.Black, + unfocusedTextColor = Color.Gray, + disabledTextColor = Color.Gray, + errorTextColor = Color.Black, + cursorColor = MaterialTheme.colorScheme.background, + focusedPlaceholderColor = MaterialTheme.colorScheme.background, + unfocusedPlaceholderColor = MaterialTheme.colorScheme.primary, + focusedLabelColor = MaterialTheme.colorScheme.background, + disabledLabelColor = Color.Gray, + unfocusedLabelColor = MaterialTheme.colorScheme.background, + focusedLeadingIconColor = Color.Black, + unfocusedSupportingTextColor = Color.Black, + ) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/TimingConstant.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/TimingConstant.kt new file mode 100644 index 000000000000..cace9b2b7922 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/constant/TimingConstant.kt @@ -0,0 +1,4 @@ +package net.mullvad.mullvadvpn.constant + +const val MINIMUM_LOADING_TIME_MILLIS = 500L +const val NAVIGATION_DELAY_MILLIS = 500L diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt index 69fd7275e70a..67eaeca48d1a 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/dataproxy/MullvadProblemReport.kt @@ -1,135 +1,88 @@ package net.mullvad.mullvadvpn.dataproxy +import android.content.Context import java.io.File -import kotlinx.coroutines.CompletableDeferred -import kotlinx.coroutines.Deferred +import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.async -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ClosedReceiveChannelException -import kotlinx.coroutines.channels.actor -import kotlinx.coroutines.channels.trySendBlocking +import kotlinx.coroutines.withContext -const val PROBLEM_REPORT_FILE = "problem_report.txt" +const val PROBLEM_REPORT_LOGS_FILE = "problem_report.txt" -class MullvadProblemReport { - private sealed class Command { - data object Collect : Command() +sealed interface SendProblemReportResult { + data object Success : SendProblemReportResult - class Load(val logs: CompletableDeferred) : Command() + sealed interface Error : SendProblemReportResult { + data object CollectLog : Error - class Send(val result: CompletableDeferred) : Command() - - data object Delete : Command() + // This is usually due to network error or bad email address + data object SendReport : Error } +} - val logDirectory = CompletableDeferred() - val cacheDirectory = CompletableDeferred() - - private val commandChannel = spawnActor() - - private val problemReportPath = - GlobalScope.async(Dispatchers.Default) { File(logDirectory.await(), PROBLEM_REPORT_FILE) } - - private var isCollected = false +data class UserReport(val email: String?, val message: String) - var confirmNoEmail: CompletableDeferred? = null +class MullvadProblemReport(context: Context, val dispatcher: CoroutineDispatcher = Dispatchers.IO) { - var userEmail = "" - var userMessage = "" + private val cacheDirectory = File(context.cacheDir.toURI()) + private val logDirectory = File(context.filesDir.toURI()) + private val logsPath = File(logDirectory, PROBLEM_REPORT_LOGS_FILE) init { System.loadLibrary("mullvad_jni") } - fun collect() { - commandChannel.trySendBlocking(Command.Collect) - } - - suspend fun load(): String { - val logs = CompletableDeferred() - - commandChannel.send(Command.Load(logs)) + suspend fun collectLogs(): Boolean = + withContext(dispatcher) { + // Delete any old report + deleteLogs() - return logs.await() - } - - fun send(): Deferred { - val result = CompletableDeferred() - - commandChannel.trySendBlocking(Command.Send(result)) - - return result - } - - fun deleteReportFile() { - commandChannel.trySendBlocking(Command.Delete) - } - - private fun spawnActor() = - GlobalScope.actor(Dispatchers.Default, Channel.UNLIMITED) { - try { - while (true) { - - when (val command = channel.receive()) { - is Command.Collect -> doCollect() - is Command.Load -> command.logs.complete(doLoad()) - is Command.Send -> command.result.complete(doSend()) - is Command.Delete -> doDelete() - } - } - } catch (exception: ClosedReceiveChannelException) {} + collectReport(logDirectory.absolutePath, logsPath.absolutePath) } - private suspend fun doCollect() { - val logDirectoryPath = logDirectory.await().absolutePath - val reportPath = problemReportPath.await().absolutePath - - doDelete() - - isCollected = collectReport(logDirectoryPath, reportPath) - } - - private suspend fun doLoad(): String { - if (!isCollected) { - doCollect() + suspend fun sendReport(userReport: UserReport): SendProblemReportResult { + // If report is not collected then, collect it, if it fails then return error + if (!logsExists() && !collectLogs()) { + return SendProblemReportResult.Error.CollectLog } - return if (isCollected) { - problemReportPath.await().readText() + val sentSuccessfully = + withContext(dispatcher) { + sendProblemReport( + userReport.email ?: "", + userReport.message, + logsPath.absolutePath, + cacheDirectory.absolutePath + ) + } + + return if (sentSuccessfully) { + deleteLogs() + SendProblemReportResult.Success } else { - "Failed to collect logs for problem report" + SendProblemReportResult.Error.SendReport } } - private suspend fun doSend(): Boolean { - if (!isCollected) { - doCollect() + suspend fun readLogs(): List { + if (!logsExists()) { + collectLogs() } - val result = - isCollected && - sendProblemReport( - userEmail, - userMessage, - problemReportPath.await().absolutePath, - cacheDirectory.await().absolutePath - ) - - if (result) { - doDelete() + return if (logsExists()) { + logsPath.readLines() + } else { + listOf("Failed to collect logs for problem report") } - - return result } - private suspend fun doDelete() { - problemReportPath.await().delete() - isCollected = false + private fun logsExists() = logsPath.exists() + + fun deleteLogs() { + logsPath.delete() } - private external fun collectReport(logDirectory: String, reportPath: String): Boolean + // TODO We should remove the external functions from this class and migrate it to the service + private external fun collectReport(logDirectory: String, logsPath: String): Boolean private external fun sendProblemReport( userEmail: String, diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt index 987a55b45f7b..18e98964e8d3 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.Dispatchers import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.applist.ApplicationsIconManager import net.mullvad.mullvadvpn.applist.ApplicationsProvider +import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport import net.mullvad.mullvadvpn.lib.ipc.EventDispatcher import net.mullvad.mullvadvpn.repository.AccountRepository import net.mullvad.mullvadvpn.repository.ChangelogRepository @@ -26,9 +27,11 @@ import net.mullvad.mullvadvpn.viewmodel.DeviceRevokedViewModel import net.mullvad.mullvadvpn.viewmodel.LoginViewModel import net.mullvad.mullvadvpn.viewmodel.OutOfTimeViewModel import net.mullvad.mullvadvpn.viewmodel.PrivacyDisclaimerViewModel +import net.mullvad.mullvadvpn.viewmodel.ReportProblemViewModel import net.mullvad.mullvadvpn.viewmodel.SelectLocationViewModel import net.mullvad.mullvadvpn.viewmodel.SettingsViewModel import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel +import net.mullvad.mullvadvpn.viewmodel.ViewLogsViewModel import net.mullvad.mullvadvpn.viewmodel.VpnSettingsViewModel import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel import org.apache.commons.validator.routines.InetAddressValidator @@ -70,6 +73,7 @@ val uiModule = module { ) } single { SettingsRepository(get()) } + single { MullvadProblemReport(get()) } single { ChangelogDataProvider(get()) } @@ -87,6 +91,8 @@ val uiModule = module { viewModel { SettingsViewModel(get(), get()) } viewModel { VpnSettingsViewModel(get(), get(), get(), get()) } viewModel { WelcomeViewModel(get(), get(), get()) } + viewModel { ReportProblemViewModel(get()) } + viewModel { ViewLogsViewModel(get()) } viewModel { OutOfTimeViewModel(get(), get()) } } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt deleted file mode 100644 index 1c37945602fa..000000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/CollapsibleTitleController.kt +++ /dev/null @@ -1,219 +0,0 @@ -package net.mullvad.mullvadvpn.ui - -import android.view.View -import android.view.View.OnLayoutChangeListener -import android.view.ViewGroup.MarginLayoutParams -import kotlin.properties.Delegates.observable -import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.util.LinearInterpolation -import net.mullvad.mullvadvpn.util.ListenableScrollableView - -// In order to use this view controller, the parent view must contain four views with specific IDs: -// -// 1. A scroll area `View` with the `scrollAreaId` that implements `ListenableScrollableView`, which -// is used to animate the title based on the scroll offset. -// 2. A view inside the scroll area with the ID `expanded_title`. This view is made invisible so -// that it's not drawn, but it is used to measure the layout and the animation positions. -// 3. A view outside the scroll area with the ID `collapsed_title`. This view is also made -// invisible just like the `expanded_view`. -// 4. A view with the ID `title`. This is the view that's actually drawn, and it's position and size -// are interpolated from the expanded title to the collapsed title. This view should be placed -// somewhere where it is drawn over all other views. -// -// The animation interpolation is calculated based on the Y scroll offset of the scroll area. Once -// the offset reaches a value that completely hides the expanded title inside the scroll view, the -// animation finishes with the title being in the collapsed state. -class CollapsibleTitleController(val parentView: View, scrollAreaId: Int = R.id.scroll_area) { - private inner class LayoutListener(val listener: (View) -> Unit) : OnLayoutChangeListener { - override fun onLayoutChange( - view: View, - left: Int, - top: Int, - right: Int, - bottom: Int, - oldLeft: Int, - oldTop: Int, - oldRight: Int, - oldBottom: Int - ) { - listener.invoke(view) - update() - } - } - - private val scaleInterpolation = LinearInterpolation() - private val scrollInterpolation = LinearInterpolation() - private val xOffsetInterpolation = LinearInterpolation() - private val yOffsetInterpolation = LinearInterpolation() - - private val collapsedTitleLayoutListener: LayoutListener = LayoutListener { collapsedTitle -> - val (x, y) = calculateViewCoordinates(collapsedTitle) - - collapsedTitleHeight = collapsedTitle.height.toFloat() - - scaleInterpolation.end = collapsedTitleHeight / maxOf(1.0f, titleHeight) - xOffsetInterpolation.end = x - yOffsetInterpolation.end = y - } - - private val collapsedTitleView = - parentView.findViewById(R.id.collapsed_title).apply { - addOnLayoutChangeListener(collapsedTitleLayoutListener) - visibility = View.INVISIBLE - } - - private val expandedTitleLayoutListener: LayoutListener = LayoutListener { expandedTitle -> - val (x, y) = calculateViewCoordinates(expandedTitle) - - val expandedTitleMarginTop = - when (val layoutParams = expandedTitle.layoutParams) { - is MarginLayoutParams -> layoutParams.topMargin - else -> 0 - } - - expandedTitleHeight = expandedTitle.height.toFloat() - - scaleInterpolation.start = expandedTitleHeight / maxOf(1.0f, titleHeight) - xOffsetInterpolation.start = x - yOffsetInterpolation.start = y - - scrollInterpolation.end = expandedTitleHeight + expandedTitleMarginTop - } - - private val titleLayoutListener: LayoutListener = LayoutListener { title -> - val (x, y) = calculateViewCoordinates(title) - - titleWidth = title.width.toFloat() - titleHeight = title.height.toFloat() - - scaleInterpolation.start = expandedTitleHeight / maxOf(1.0f, titleHeight) - scaleInterpolation.end = collapsedTitleHeight / maxOf(1.0f, titleHeight) - xOffsetInterpolation.reference = x - yOffsetInterpolation.reference = y - } - - private val titleView = - parentView.findViewById(R.id.title).apply { - addOnLayoutChangeListener(titleLayoutListener) - - // Setting the scale pivot point to the left corner simplifies the calculations - pivotX = 0.0f - pivotY = 0.0f - } - - private val scrollAreaLayoutListener: LayoutListener = LayoutListener { - scrollOffset = scrollArea.verticalScrollOffset.toFloat() - } - - private val scrollArea = - parentView.findViewById(scrollAreaId).let { view -> - val scrollableView = view as ListenableScrollableView - - view.addOnLayoutChangeListener(scrollAreaLayoutListener) - - scrollableView.onScrollListener = { _, top, _, _ -> - scrollOffset = top.toFloat() - update() - } - - scrollableView - } - - private var scrollOffsetUpdated = false - get() { - if (field == true) { - field = false - return true - } else { - return false - } - } - - private var collapsedTitleHeight = 0.0f - private var expandedTitleHeight = 0.0f - private var titleWidth = 0.0f - private var titleHeight = 0.0f - - private var scrollOffset: Float by - observable(0.0f) { _, old, new -> - if (scrollOffsetUpdated == false && old != new) { - scrollOffsetUpdated = true - } - } - - val fullCollapseScrollOffset: Float - get() = scrollInterpolation.end - - var expandedTitleView by - observable(null) { _, oldView, newView -> - oldView?.removeOnLayoutChangeListener(expandedTitleLayoutListener) - newView?.apply { - addOnLayoutChangeListener(expandedTitleLayoutListener) - expandedTitleLayoutListener.listener(this) - visibility = View.INVISIBLE - } - } - - init { - expandedTitleView = parentView.findViewById(R.id.expanded_title) - update() - } - - fun onDestroy() { - scrollArea.onScrollListener = null - (scrollArea as View).removeOnLayoutChangeListener(scrollAreaLayoutListener) - - collapsedTitleView.removeOnLayoutChangeListener(collapsedTitleLayoutListener) - expandedTitleView?.removeOnLayoutChangeListener(expandedTitleLayoutListener) - titleView.removeOnLayoutChangeListener(titleLayoutListener) - } - - private fun update() { - val shouldUpdate = - scrollOffsetUpdated || - scaleInterpolation.updated || - xOffsetInterpolation.updated || - yOffsetInterpolation.updated - - if (shouldUpdate) { - val progress = - if (expandedTitleView != null) { - maxOf(0.0f, minOf(1.0f, scrollInterpolation.progress(scrollOffset))) - } else { - 1.0f - } - - val scale = scaleInterpolation.interpolate(progress) - val offsetX = xOffsetInterpolation.interpolate(progress) - val offsetY = yOffsetInterpolation.interpolate(progress) - - titleView.apply { - scaleX = scale - scaleY = scale - translationX = offsetX - translationY = offsetY - } - } - } - - private fun calculateViewCoordinates(view: View): Pair { - var currentView = view - var x = 0.0f - var y = 0.0f - - while (currentView != parentView) { - val parent = currentView.parent - - x += currentView.x - currentView.translationX - y += currentView.y - currentView.translationY - - if (parent is View) { - currentView = parent - } else { - break - } - } - - return Pair(x, y) - } -} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ListItemDividerDecoration.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ListItemDividerDecoration.kt deleted file mode 100644 index 4fcde0e31459..000000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/ListItemDividerDecoration.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.mullvad.mullvadvpn.ui - -import android.graphics.Rect -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.ItemDecoration -import androidx.recyclerview.widget.RecyclerView.State - -class ListItemDividerDecoration(private val bottomOffset: Int = 0, private val topOffset: Int = 0) : - ItemDecoration() { - - override fun getItemOffsets(offsets: Rect, view: View, parent: RecyclerView, state: State) { - offsets.bottom = bottomOffset - offsets.top = topOffset - } -} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt index fa88696cd9d8..3d30d2884576 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/MainActivity.kt @@ -30,7 +30,6 @@ import kotlinx.coroutines.withTimeoutOrNull import net.mullvad.mullvadvpn.BuildConfig import net.mullvad.mullvadvpn.R import net.mullvad.mullvadvpn.compose.dialog.ChangelogDialog -import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport import net.mullvad.mullvadvpn.di.uiModule import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.isNotificationPermissionGranted import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointConfiguration @@ -59,7 +58,6 @@ import org.koin.android.ext.android.getKoin import org.koin.core.context.loadKoinModules open class MainActivity : FragmentActivity() { - val problemReport = MullvadProblemReport() private var requestNotificationPermissionLauncher: ActivityResultLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { // NotificationManager.areNotificationsEnabled is used to check the state rather than @@ -105,11 +103,6 @@ open class MainActivity : FragmentActivity() { super.onCreate(savedInstanceState) - problemReport.apply { - logDirectory.complete(filesDir) - cacheDirectory.complete(cacheDir) - } - setContentView(R.layout.main) } diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/ConfirmNoEmailDialogFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/ConfirmNoEmailDialogFragment.kt deleted file mode 100644 index d31c5551b555..000000000000 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragment/ConfirmNoEmailDialogFragment.kt +++ /dev/null @@ -1,64 +0,0 @@ -package net.mullvad.mullvadvpn.ui.fragment - -import android.app.Dialog -import android.content.Context -import android.content.DialogInterface -import android.graphics.drawable.ColorDrawable -import android.os.Bundle -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.view.ViewGroup.LayoutParams -import android.widget.Button -import androidx.fragment.app.DialogFragment -import kotlinx.coroutines.CompletableDeferred -import net.mullvad.mullvadvpn.R -import net.mullvad.mullvadvpn.ui.MainActivity - -class ConfirmNoEmailDialogFragment : DialogFragment() { - private var confirmNoEmail: CompletableDeferred? = null - - override fun onAttach(context: Context) { - super.onAttach(context) - - val parentActivity = context as MainActivity - - confirmNoEmail = parentActivity.problemReport.confirmNoEmail - } - - override fun onCreateView( - inflater: LayoutInflater, - container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - val view = inflater.inflate(R.layout.confirm_no_email, container, false) - - view.findViewById