Skip to content

Commit

Permalink
Merge branch 'use-deeplink-to-do-vpnpermission-intent-droid-1110'
Browse files Browse the repository at this point in the history
  • Loading branch information
Pururun committed Dec 13, 2024
2 parents ee7681e + b95e54a commit 87669a9
Show file tree
Hide file tree
Showing 15 changed files with 97 additions and 153 deletions.
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

0 comments on commit 87669a9

Please sign in to comment.