diff options
| author | Albin <albin@mullvad.net> | 2022-05-31 16:59:18 +0200 |
|---|---|---|
| committer | Albin <albin@mullvad.net> | 2022-06-15 10:25:32 +0200 |
| commit | a0c4b0dfe624b9b3e1b61e6b8fa43d5651fff799 (patch) | |
| tree | 2111dab0055015978f236fa7e8fde5fc223e7d33 /android | |
| parent | de883f61d39f99b7a6d636d11ef1a36a4a5217a0 (diff) | |
| download | mullvadvpn-a0c4b0dfe624b9b3e1b61e6b8fa43d5651fff799.tar.xz mullvadvpn-a0c4b0dfe624b9b3e1b61e6b8fa43d5651fff799.zip | |
Add Android device revoked view
Diffstat (limited to 'android')
12 files changed, 299 insertions, 26 deletions
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceRevokedScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceRevokedScreen.kt new file mode 100644 index 0000000000..a559cd0cf9 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/DeviceRevokedScreen.kt @@ -0,0 +1,113 @@ +package net.mullvad.mullvadvpn.compose.screen + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.material.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.res.colorResource +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.constraintlayout.compose.ConstraintLayout +import androidx.constraintlayout.compose.Dimension +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.component.ActionButton +import net.mullvad.mullvadvpn.viewmodel.DeviceRevokedViewModel + +@Composable +fun DeviceRevokedScreen( + deviceRevokedViewModel: DeviceRevokedViewModel +) { + val state = deviceRevokedViewModel.uiState.collectAsState().value + + ConstraintLayout( + modifier = Modifier + .fillMaxHeight() + .fillMaxWidth() + .background(colorResource(id = R.color.darkBlue)) + ) { + val (icon, body, actionButtons) = createRefs() + + Image( + painter = painterResource(id = R.drawable.icon_fail), + contentDescription = null, // No meaningful user info or action. + modifier = Modifier + .constrainAs(icon) { + top.linkTo(parent.top, margin = 30.dp) + start.linkTo(parent.start) + end.linkTo(parent.end) + } + .padding(horizontal = 12.dp) + .width(80.dp) + .height(80.dp) + ) + + Column( + modifier = Modifier + .constrainAs(body) { + top.linkTo(icon.bottom, margin = 22.dp) + start.linkTo(parent.start, margin = 22.dp) + end.linkTo(parent.end, margin = 22.dp) + width = Dimension.fillToConstraints + }, + ) { + Text( + text = stringResource(id = R.string.device_inactive_title), + fontSize = 24.sp, + color = Color.White, + fontWeight = FontWeight.Bold + ) + + Text( + text = stringResource(id = R.string.device_inactive_description), + fontSize = 12.sp, + color = Color.White, + modifier = Modifier.padding(top = 10.dp) + ) + + if (state.isSecured) { + Text( + text = stringResource(id = R.string.device_inactive_unblock_warning), + fontSize = 12.sp, + color = Color.White, + modifier = Modifier.padding(top = 10.dp) + ) + } + } + + Column( + modifier = Modifier + .constrainAs(actionButtons) { + bottom.linkTo(parent.bottom, margin = 22.dp) + start.linkTo(parent.start, margin = 22.dp) + end.linkTo(parent.end, margin = 22.dp) + width = Dimension.fillToConstraints + } + ) { + val buttonColor = colorResource( + if (state.isSecured) { + R.color.red60 + } else { + R.color.blue + } + ) + + ActionButton( + text = stringResource(id = R.string.go_to_login), + onClick = { deviceRevokedViewModel.onGoToLoginClicked() }, + buttonColor = buttonColor + ) + } + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceRevokedUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceRevokedUiState.kt new file mode 100644 index 0000000000..f8465423ed --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/DeviceRevokedUiState.kt @@ -0,0 +1,11 @@ +package net.mullvad.mullvadvpn.compose.state + +data class DeviceRevokedUiState( + val isSecured: Boolean +) { + companion object { + val DEFAULT = DeviceRevokedUiState( + isSecured = false + ) + } +} 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 7e503e4a33..0910754a8d 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 @@ -9,6 +9,7 @@ import net.mullvad.mullvadvpn.ipc.EventDispatcher import net.mullvad.mullvadvpn.ui.serviceconnection.DeviceRepository import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager import net.mullvad.mullvadvpn.ui.serviceconnection.SplitTunneling +import net.mullvad.mullvadvpn.viewmodel.DeviceRevokedViewModel import net.mullvad.mullvadvpn.viewmodel.LoginViewModel import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel import org.koin.android.ext.koin.androidContext @@ -37,7 +38,9 @@ val uiModule = module { single { ServiceConnectionManager(androidContext()) } single { DeviceRepository(get()) } viewModel { LoginViewModel() } + viewModel { DeviceRevokedViewModel(get()) } } + const val APPS_SCOPE = "APPS_SCOPE" const val SERVICE_CONNECTION_SCOPE = "SERVICE_CONNECTION_SCOPE" const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt index 918396a263..6edfb2dc24 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt @@ -31,6 +31,16 @@ sealed class TunnelState() : Parcelable { @Parcelize class Error(val errorState: ErrorState) : TunnelState(), Parcelable + fun isSecured(): Boolean { + return when (this) { + is Connected, + is Connecting, + is Disconnecting, -> true + is Disconnected -> false + is Error -> this.errorState.isBlocking + } + } + companion object { const val DISCONNECTED = "disconnected" const val CONNECTING = "connecting" 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 56533d479a..5e00472bc4 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 @@ -33,7 +33,6 @@ import org.koin.android.ext.android.getKoin import org.koin.core.context.loadKoinModules open class MainActivity : FragmentActivity() { - private val deviceRepository: DeviceRepository by inject() val problemReport = MullvadProblemReport() private var visibleSecureScreens = HashSet<Fragment>() @@ -47,10 +46,15 @@ open class MainActivity : FragmentActivity() { var backButtonHandler: (() -> Boolean)? = null private lateinit var serviceConnectionManager: ServiceConnectionManager + private lateinit var deviceRepository: DeviceRepository override fun onCreate(savedInstanceState: Bundle?) { loadKoinModules(uiModule) - serviceConnectionManager = getKoin().get() + + getKoin().apply { + serviceConnectionManager = get() + deviceRepository = get() + } requestedOrientation = if (deviceIsTv) { ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE @@ -164,7 +168,7 @@ open class MainActivity : FragmentActivity() { is DeviceState.Initial, is DeviceState.Unknown -> openLaunchView() is DeviceState.LoggedOut -> openLoginView() - is DeviceState.Revoked -> openLoginView() + is DeviceState.Revoked -> openRevokedView() is DeviceState.LoggedIn -> openConnectView() } currentState = newState @@ -212,6 +216,19 @@ open class MainActivity : FragmentActivity() { } } + private fun openRevokedView() { + supportFragmentManager.beginTransaction().apply { + setCustomAnimations( + R.anim.fragment_enter_from_right, + R.anim.fragment_exit_to_left, + R.anim.fragment_half_enter_from_left, + R.anim.fragment_exit_to_right + ) + replace(R.id.main_fragment, DeviceRevokedFragment()) + commit() + } + } + companion object { private const val ZERO_DEBOUNCE_DELAY_MILLISECONDS = 0L private const val UNKNOWN_STATE_DEBOUNCE_DELAY_MILLISECONDS = 2000L diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragments/DeviceRevokedFragment.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragments/DeviceRevokedFragment.kt new file mode 100644 index 0000000000..69b7f31111 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/fragments/DeviceRevokedFragment.kt @@ -0,0 +1,55 @@ +package net.mullvad.mullvadvpn.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.compose.runtime.collectAsState +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.res.colorResource +import androidx.fragment.app.Fragment +import net.mullvad.mullvadvpn.R +import net.mullvad.mullvadvpn.compose.component.AppTheme +import net.mullvad.mullvadvpn.compose.component.ScaffoldWithTopBar +import net.mullvad.mullvadvpn.compose.screen.DeviceRevokedScreen +import net.mullvad.mullvadvpn.ui.MainActivity +import net.mullvad.mullvadvpn.viewmodel.DeviceRevokedViewModel +import org.koin.androidx.viewmodel.ext.android.viewModel + +class DeviceRevokedFragment : Fragment() { + private val deviceRevokedViewModel: DeviceRevokedViewModel by viewModel() + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + return inflater.inflate(R.layout.fragment_compose, container, false).apply { + findViewById<ComposeView>(R.id.compose_view).setContent { + AppTheme { + val state = deviceRevokedViewModel.uiState.collectAsState().value + + val topColor = colorResource( + if (state.isSecured) { + R.color.green + } else { + R.color.red + } + ) + + ScaffoldWithTopBar( + topBarColor = topColor, + statusBarColor = topColor, + navigationBarColor = colorResource(id = R.color.darkBlue), + onSettingsClicked = this@DeviceRevokedFragment::openSettingsView, + content = { DeviceRevokedScreen(deviceRevokedViewModel) } + ) + } + } + } + } + + private fun openSettingsView() { + (context as? MainActivity)?.openSettings() + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/HeaderBar.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/HeaderBar.kt index 877fcd9c66..052739c826 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/HeaderBar.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ui/widget/HeaderBar.kt @@ -27,19 +27,12 @@ class HeaderBar @JvmOverloads constructor( private val unsecuredColor = ContextCompat.getColor(context, R.color.red) var tunnelState by observable<TunnelState?>(null) { _, _, state -> - val backgroundColor = when (state) { - null -> disabledColor - is TunnelState.Disconnected -> unsecuredColor - is TunnelState.Connecting -> securedColor - is TunnelState.Connected -> securedColor - is TunnelState.Disconnecting -> securedColor - is TunnelState.Error -> { - if (state.errorState.isBlocking) { - securedColor - } else { - unsecuredColor - } - } + val backgroundColor = if (state == null) { + disabledColor + } else if (state.isSecured()) { + securedColor + } else { + unsecuredColor } container.setBackgroundColor(backgroundColor) diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt new file mode 100644 index 0000000000..9b66b10ae0 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/DeviceRevokedViewModel.kt @@ -0,0 +1,49 @@ +package net.mullvad.mullvadvpn.viewmodel + +import androidx.lifecycle.ViewModel +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import net.mullvad.mullvadvpn.compose.state.DeviceRevokedUiState +import net.mullvad.mullvadvpn.model.TunnelState +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionContainer +import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager +import net.mullvad.talpid.util.callbackFlowFromSubscription + +// TODO: Refactor AccountCache and ConnectionProxy and inject those rather than injecting +// ServiceConnectionManager here. +class DeviceRevokedViewModel( + private val serviceConnectionManager: ServiceConnectionManager, + private val scope: CoroutineScope = CoroutineScope(Dispatchers.IO) +) : ViewModel() { + + val uiState = serviceConnectionManager.connectionState + .map { connectionState -> connectionState.readyContainer()?.connectionProxy } + .flatMapLatest { proxy -> + proxy?.onUiStateChange?.callbackFlowFromSubscription(this) + ?: flowOf(TunnelState.Disconnected) + } + .map { DeviceRevokedUiState(it.isSecured()) } + .stateIn( + scope, + SharingStarted.Lazily, + DeviceRevokedUiState.DEFAULT + ) + + fun onGoToLoginClicked() { + serviceContainer()?.let { container -> + if (container.connectionProxy.state.isSecured()) { + container.connectionProxy.disconnect() + } + container.accountCache.logout() + } + } + + private fun serviceContainer(): ServiceConnectionContainer? { + return serviceConnectionManager.connectionState.value.readyContainer() + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt b/android/app/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt new file mode 100644 index 0000000000..454cae6133 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt @@ -0,0 +1,13 @@ +package net.mullvad.talpid.util + +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.callbackFlow + +fun <T> EventNotifier<T>.callbackFlowFromSubscription(id: Any) = callbackFlow { + this@callbackFlowFromSubscription.subscribe(id) { + this.trySend(it) + } + awaitClose { + this@callbackFlowFromSubscription.unsubscribe(id) + } +} diff --git a/android/app/src/main/res/anim/fragment_exit_to_left.xml b/android/app/src/main/res/anim/fragment_exit_to_left.xml new file mode 100644 index 0000000000..9ffa2c9877 --- /dev/null +++ b/android/app/src/main/res/anim/fragment_exit_to_left.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<set xmlns:android="http://schemas.android.com/apk/res/android"> + <translate android:fromXDelta="0%p" + android:toXDelta="-100%p" + android:duration="@integer/transition_animation_duration" /> +</set> diff --git a/android/app/src/main/res/layout/fragment_compose.xml b/android/app/src/main/res/layout/fragment_compose.xml index e54d17348c..3417de83cb 100644 --- a/android/app/src/main/res/layout/fragment_compose.xml +++ b/android/app/src/main/res/layout/fragment_compose.xml @@ -1,13 +1,10 @@ <?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".DeviceInactiveFragment"> - - <androidx.compose.ui.platform.ComposeView - android:id="@+id/compose_view" - android:layout_width="match_parent" - android:layout_height="match_parent" /> - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:context=".DeviceInactiveFragment"> + <androidx.compose.ui.platform.ComposeView android:id="@+id/compose_view" + android:layout_width="match_parent" + android:layout_height="match_parent" /> </FrameLayout> diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml index 6c08df83ee..bcdee0d5c8 100644 --- a/android/app/src/main/res/values/strings.xml +++ b/android/app/src/main/res/values/strings.xml @@ -174,4 +174,10 @@ <string name="copied_to_clipboard">Copied to clipboard</string> <string name="show_system_apps">Show system apps</string> <string name="toggle_vpn">Toggle VPN</string> + <string name="go_to_login">Go to login</string> + <string name="device_inactive_title">Device is inactive</string> + <string name="device_inactive_description">You have removed this device. To connect again, you + will need to log back in.</string> + <string name="device_inactive_unblock_warning">Going to login will unblock the internet on this + device.</string> </resources> |
