Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Remove the use of intent provider for request vpn permission #7198

Merged
merged 2 commits into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion android/app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,6 @@ dependencies {
implementation(projects.lib.common)
implementation(projects.lib.daemonGrpc)
implementation(projects.lib.endpoint)
implementation(projects.lib.intentProvider)
implementation(projects.lib.map)
implementation(projects.lib.model)
implementation(projects.lib.payment)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,32 +1,23 @@
package net.mullvad.mullvadvpn.compose.screen

import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.ExperimentalComposeUiApi
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.semantics.testTagsAsResourceId
import androidx.navigation.NavHostController
import arrow.core.merge
import co.touchlab.kermit.Logger
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.generated.NavGraphs
import com.ramcosta.composedestinations.generated.destinations.NoDaemonDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.rememberNavHostEngine
import com.ramcosta.composedestinations.utils.rememberDestinationsNavigator
import net.mullvad.mullvadvpn.compose.util.CreateVpnProfile
import net.mullvad.mullvadvpn.lib.common.util.prepareVpnSafe
import net.mullvad.mullvadvpn.lib.model.PrepareError
import net.mullvad.mullvadvpn.lib.model.Prepared
import net.mullvad.mullvadvpn.viewmodel.DaemonScreenEvent
import net.mullvad.mullvadvpn.viewmodel.NoDaemonViewModel
import net.mullvad.mullvadvpn.viewmodel.VpnProfileSideEffect
import net.mullvad.mullvadvpn.viewmodel.VpnProfileViewModel
import net.mullvad.mullvadvpn.viewmodel.MullvadAppViewModel
import org.koin.androidx.compose.koinViewModel

@OptIn(ExperimentalComposeUiApi::class)
Expand All @@ -36,12 +27,11 @@ fun MullvadApp() {
val navHostController: NavHostController = engine.rememberNavController()
val navigator: DestinationsNavigator = navHostController.rememberDestinationsNavigator()

val serviceVm = koinViewModel<NoDaemonViewModel>()
val permissionVm = koinViewModel<VpnProfileViewModel>()
val mullvadAppViewModel = koinViewModel<MullvadAppViewModel>()

DisposableEffect(Unit) {
navHostController.addOnDestinationChangedListener(serviceVm)
onDispose { navHostController.removeOnDestinationChangedListener(serviceVm) }
navHostController.addOnDestinationChangedListener(mullvadAppViewModel)
onDispose { navHostController.removeOnDestinationChangedListener(mullvadAppViewModel) }
}

DestinationsNavHost(
Expand All @@ -56,7 +46,7 @@ fun MullvadApp() {

// Globally handle daemon dropped connection with NoDaemonScreen
LaunchedEffect(Unit) {
serviceVm.uiSideEffect.collect {
mullvadAppViewModel.uiSideEffect.collect {
Logger.i { "DaemonScreenEvent: $it" }
when (it) {
DaemonScreenEvent.Show ->
Expand All @@ -66,24 +56,4 @@ fun MullvadApp() {
}
}
}

// Ask for VPN Permission
val launchVpnPermission =
rememberLauncherForActivityResult(CreateVpnProfile()) { _ -> permissionVm.connect() }
val context = LocalContext.current
LaunchedEffect(Unit) {
permissionVm.uiSideEffect.collect {
if (it is VpnProfileSideEffect.RequestVpnProfile) {
val prepareResult = context.prepareVpnSafe().merge()
when (prepareResult) {
is PrepareError.NotPrepared ->
launchVpnPermission.launch(prepareResult.prepareIntent)
// If legacy or other always on connect at let daemon generate a error state
is PrepareError.OtherLegacyAlwaysOnVpn,
is PrepareError.OtherAlwaysOnApp,
Prepared -> permissionVm.connect()
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import net.mullvad.mullvadvpn.BuildConfig
import net.mullvad.mullvadvpn.lib.common.constant.GRPC_SOCKET_FILE_NAME
import net.mullvad.mullvadvpn.lib.common.constant.GRPC_SOCKET_FILE_NAMED_ARGUMENT
import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
import net.mullvad.mullvadvpn.lib.intent.IntentProvider
import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointFromIntentHolder
import net.mullvad.mullvadvpn.lib.model.BuildVersion
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
Expand All @@ -33,7 +33,7 @@ val appModule = module {
single { PrepareVpnUseCase(androidContext()) }

single { BuildVersion(BuildConfig.VERSION_NAME, BuildConfig.VERSION_CODE) }
single { IntentProvider() }
single { ApiEndpointFromIntentHolder() }
single { AccountRepository(get(), get(), MainScope()) }
single { DeviceRepository(get()) }
single { ConnectionProxy(get(), get(), get()) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ import net.mullvad.mullvadvpn.viewmodel.EditCustomListViewModel
import net.mullvad.mullvadvpn.viewmodel.FilterViewModel
import net.mullvad.mullvadvpn.viewmodel.LoginViewModel
import net.mullvad.mullvadvpn.viewmodel.MtuDialogViewModel
import net.mullvad.mullvadvpn.viewmodel.MullvadAppViewModel
import net.mullvad.mullvadvpn.viewmodel.MultihopViewModel
import net.mullvad.mullvadvpn.viewmodel.NoDaemonViewModel
import net.mullvad.mullvadvpn.viewmodel.OutOfTimeViewModel
import net.mullvad.mullvadvpn.viewmodel.PaymentViewModel
import net.mullvad.mullvadvpn.viewmodel.PrivacyDisclaimerViewModel
Expand All @@ -92,7 +92,6 @@ import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel
import net.mullvad.mullvadvpn.viewmodel.Udp2TcpSettingsViewModel
import net.mullvad.mullvadvpn.viewmodel.ViewLogsViewModel
import net.mullvad.mullvadvpn.viewmodel.VoucherDialogViewModel
import net.mullvad.mullvadvpn.viewmodel.VpnProfileViewModel
import net.mullvad.mullvadvpn.viewmodel.VpnSettingsViewModel
import net.mullvad.mullvadvpn.viewmodel.WelcomeViewModel
import net.mullvad.mullvadvpn.viewmodel.WireguardCustomPortDialogViewModel
Expand Down Expand Up @@ -237,7 +236,6 @@ val uiModule = module {
viewModel { DeleteCustomListConfirmationViewModel(get(), get()) }
viewModel { ServerIpOverridesViewModel(get(), get()) }
viewModel { ResetServerIpOverridesConfirmationViewModel(get()) }
viewModel { VpnProfileViewModel(get(), get()) }
viewModel { ApiAccessListViewModel(get()) }
viewModel { EditApiAccessMethodViewModel(get(), get(), get()) }
viewModel { SaveApiAccessMethodViewModel(get(), get()) }
Expand Down Expand Up @@ -268,7 +266,7 @@ val uiModule = module {
viewModel { DaitaViewModel(get()) }

// This view model must be single so we correctly attach lifecycle and share it with activity
single { NoDaemonViewModel(get()) }
single { MullvadAppViewModel(get(), get()) }
}

const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,21 +12,31 @@ import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import arrow.core.merge
import co.touchlab.kermit.Logger
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.compose.screen.MullvadApp
import net.mullvad.mullvadvpn.compose.util.CreateVpnProfile
import net.mullvad.mullvadvpn.di.paymentModule
import net.mullvad.mullvadvpn.di.uiModule
import net.mullvad.mullvadvpn.lib.common.constant.KEY_REQUEST_VPN_PROFILE
import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.requestNotificationPermissionIfMissing
import net.mullvad.mullvadvpn.lib.common.util.prepareVpnSafe
import net.mullvad.mullvadvpn.lib.daemon.grpc.GrpcConnectivityState
import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
import net.mullvad.mullvadvpn.lib.intent.IntentProvider
import net.mullvad.mullvadvpn.lib.endpoint.ApiEndpointFromIntentHolder
import net.mullvad.mullvadvpn.lib.endpoint.getApiEndpointConfigurationExtras
import net.mullvad.mullvadvpn.lib.model.PrepareError
import net.mullvad.mullvadvpn.lib.model.Prepared
import net.mullvad.mullvadvpn.lib.theme.AppTheme
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
import net.mullvad.mullvadvpn.repository.SplashCompleteRepository
import net.mullvad.mullvadvpn.ui.serviceconnection.ServiceConnectionManager
import net.mullvad.mullvadvpn.viewmodel.NoDaemonViewModel
import net.mullvad.mullvadvpn.viewmodel.MullvadAppViewModel
import org.koin.android.ext.android.inject
import org.koin.android.scope.AndroidScopeComponent
import org.koin.androidx.scope.activityScope
Expand All @@ -40,9 +50,11 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {
// NotificationManager.areNotificationsEnabled is used to check the state rather than
// handling the callback value.
}
private val launchVpnPermission =
registerForActivityResult(CreateVpnProfile()) { _ -> mullvadAppViewModel.connect() }

private val intentProvider by inject<IntentProvider>()
private val noDaemonViewModel by inject<NoDaemonViewModel>()
private val apiEndpointFromIntentHolder by inject<ApiEndpointFromIntentHolder>()
private val mullvadAppViewModel by inject<MullvadAppViewModel>()
private val privacyDisclaimerRepository by inject<PrivacyDisclaimerRepository>()
private val serviceConnectionManager by inject<ServiceConnectionManager>()
private val splashCompleteRepository by inject<SplashCompleteRepository>()
Expand All @@ -53,7 +65,7 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {
override fun onCreate(savedInstanceState: Bundle?) {
loadKoinModules(listOf(uiModule, paymentModule))

lifecycle.addObserver(noDaemonViewModel)
lifecycle.addObserver(mullvadAppViewModel)

installSplashScreen().setKeepOnScreenCondition {
val isReady = isReadyNextDraw
Expand All @@ -68,15 +80,14 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {

super.onCreate(savedInstanceState)

// Needs to be before set content since we want to access the intent in compose
if (savedInstanceState == null) {
intentProvider.setStartIntent(intent)
}
setContent { AppTheme { MullvadApp() } }

// This is to protect against tapjacking attacks
window.decorView.filterTouchesWhenObscured = true

// Needs to be before we start the service, since we need to access the intent there
lifecycleScope.launch { intents().collect(::handleIntent) }

// We use lifecycleScope here to get less start service in background exceptions
// Se this article for more information:
// https://medium.com/@lepicekmichal/android-background-service-without-hiccup-501e4479110f
Expand All @@ -103,11 +114,6 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {
}
}

override fun onNewIntent(intent: Intent) {
super.onNewIntent(intent)
intentProvider.setStartIntent(intent)
}

fun bindService() {
requestNotificationPermissionIfMissing(requestNotificationPermissionLauncher)
serviceConnectionManager.bind()
Expand All @@ -121,7 +127,40 @@ class MainActivity : ComponentActivity(), AndroidScopeComponent {
}

override fun onDestroy() {
lifecycle.removeObserver(noDaemonViewModel)
lifecycle.removeObserver(mullvadAppViewModel)
super.onDestroy()
}

private fun handleIntent(intent: Intent) {
when (val action = intent.action) {
Intent.ACTION_MAIN ->
apiEndpointFromIntentHolder.setApiEndpointOverride(
intent.getApiEndpointConfigurationExtras()
)
KEY_REQUEST_VPN_PROFILE -> handleRequestVpnProfileIntent()
else -> Logger.w("Unhandled intent action: $action")
}
}

private fun handleRequestVpnProfileIntent() {
val prepareResult = prepareVpnSafe().merge()
when (prepareResult) {
is PrepareError.NotPrepared -> launchVpnPermission.launch(prepareResult.prepareIntent)
// If legacy or other always on connect at let daemon generate a error state
is PrepareError.OtherLegacyAlwaysOnVpn,
is PrepareError.OtherAlwaysOnApp,
Prepared -> mullvadAppViewModel.connect()
}
}

private fun ComponentActivity.intents() =
callbackFlow<Intent> {
send(intent)

val listener: (Intent) -> Unit = { trySend(it) }

addOnNewIntentListener(listener)

awaitClose { removeOnNewIntentListener(listener) }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,14 @@ import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.launch
import net.mullvad.mullvadvpn.lib.daemon.grpc.GrpcConnectivityState
import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy

private val noServiceDestinations = listOf(SplashDestination, PrivacyDisclaimerDestination)

class NoDaemonViewModel(managementService: ManagementService) :
ViewModel(), LifecycleEventObserver, NavController.OnDestinationChangedListener {
class MullvadAppViewModel(
private val connectionProxy: ConnectionProxy,
managementService: ManagementService,
) : ViewModel(), LifecycleEventObserver, NavController.OnDestinationChangedListener {

private val lifecycleFlow: MutableSharedFlow<Lifecycle.Event> = MutableSharedFlow()
private val destinationFlow: MutableSharedFlow<DestinationSpec> = MutableSharedFlow()
Expand Down Expand Up @@ -99,6 +102,10 @@ class NoDaemonViewModel(managementService: ManagementService) :
}
}

fun connect() {
viewModelScope.launch { connectionProxy.connectWithoutPermissionCheck() }
}

companion object {
private val SERVICE_DISCONNECT_DEBOUNCE = 2.seconds
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package net.mullvad.mullvadvpn.lib.endpoint

class ApiEndpointFromIntentHolder {
var apiEndpointOverride: ApiEndpointOverride? = null
private set

fun setApiEndpointOverride(apiEndpointOverride: ApiEndpointOverride?) {
this.apiEndpointOverride = apiEndpointOverride
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package net.mullvad.mullvadvpn.lib.endpoint

// Overridding the API endpoint is not supported in release builds
class ApiEndpointFromIntentHolder {
val apiEndpointOverride: ApiEndpointOverride? = null

@Suppress("UnusedParameter")
fun setApiEndpointOverride(apiEndpointOverride: ApiEndpointOverride?) {
// No-op
}
}
35 changes: 0 additions & 35 deletions android/lib/intent-provider/build.gradle.kts

This file was deleted.

1 change: 0 additions & 1 deletion android/lib/intent-provider/src/main/AndroidManifest.xml

This file was deleted.

Loading
Loading