diff options
Diffstat (limited to 'android')
9 files changed, 168 insertions, 28 deletions
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 19e468783c..8dc3c4e385 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -7,6 +7,7 @@ <uses-permission android:name="android.permission.POST_NOTIFICATIONS" /> <!-- https://developer.android.com/guide/components/fg-service-types#system-exempted --> <uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" /> + <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" /> <uses-feature android:name="android.hardware.touchscreen" android:required="false" /> <uses-feature android:name="android.hardware.faketouch" @@ -111,5 +112,13 @@ <action android:name="android.intent.action.LOCALE_CHANGED" /> </intent-filter> </receiver> + <receiver android:name=".receiver.BootCompletedReceiver" + android:enabled="false" + android:exported="false"> + <intent-filter> + <action android:name="android.intent.action.QUICKBOOT_POWERON" /> + <action android:name="android.intent.action.BOOT_COMPLETED" /> + </intent-filter> + </receiver> </application> </manifest> diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt index 75d12bd717..99de9066e7 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt @@ -264,6 +264,7 @@ fun VpnSettings( dropUnlessResumed { navigator.navigate(ShadowsocksSettingsDestination) }, navigateToUdp2TcpSettings = dropUnlessResumed { navigator.navigate(Udp2TcpSettingsDestination) }, + onToggleAutoStartAndConnectOnBoot = vm::onToggleAutoStartAndConnectOnBoot, ) } @@ -303,6 +304,7 @@ fun VpnSettingsScreen( onWireguardPortSelected: (port: Constraint<Port>) -> Unit = {}, navigateToShadowSocksSettings: () -> Unit = {}, navigateToUdp2TcpSettings: () -> Unit = {}, + onToggleAutoStartAndConnectOnBoot: (Boolean) -> Unit = {}, ) { var expandContentBlockersState by rememberSaveable { mutableStateOf(false) } val biggerPadding = 54.dp @@ -330,33 +332,50 @@ fun VpnSettingsScreen( text = stringResource(id = R.string.auto_connect_and_lockdown_mode_footer) ) } - } - item { - Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding)) - HeaderSwitchComposeCell( - title = stringResource(R.string.auto_connect_legacy), - isToggled = state.isAutoConnectEnabled, - isEnabled = true, - onCellClicked = { newValue -> onToggleAutoConnect(newValue) }, - ) - } - item { - SwitchComposeSubtitleCell( - text = - HtmlCompat.fromHtml( - if (state.systemVpnSettingsAvailable) { + item { + Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding)) + HeaderSwitchComposeCell( + title = stringResource(R.string.auto_connect_legacy), + isToggled = state.isAutoConnectEnabled, + isEnabled = true, + onCellClicked = { newValue -> onToggleAutoConnect(newValue) }, + ) + } + item { + SwitchComposeSubtitleCell( + text = + HtmlCompat.fromHtml( textResource( R.string.auto_connect_footer_legacy, textResource(R.string.auto_connect_and_lockdown_mode), - ) - } else { - textResource(R.string.auto_connect_footer_legacy_tv) - }, - HtmlCompat.FROM_HTML_MODE_COMPACT, - ) - .toAnnotatedString(boldFontWeight = FontWeight.ExtraBold) - ) + ), + HtmlCompat.FROM_HTML_MODE_COMPACT, + ) + .toAnnotatedString(boldFontWeight = FontWeight.ExtraBold) + ) + } + } else { + item { + Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding)) + HeaderSwitchComposeCell( + title = stringResource(R.string.connect_on_start), + isToggled = state.autoStartAndConnectOnBoot, + onCellClicked = { newValue -> onToggleAutoStartAndConnectOnBoot(newValue) }, + ) + SwitchComposeSubtitleCell( + text = + HtmlCompat.fromHtml( + textResource( + R.string.connect_on_start_footer, + textResource(R.string.auto_connect_and_lockdown_mode), + ), + HtmlCompat.FROM_HTML_MODE_COMPACT, + ) + .toAnnotatedString(boldFontWeight = FontWeight.ExtraBold) + ) + } } + item { Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding)) HeaderSwitchComposeCell( diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt index 7884f199f2..4c5f2c2ffb 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt @@ -25,6 +25,7 @@ data class VpnSettingsUiState( val customWireguardPort: Port?, val availablePortRanges: List<PortRange>, val systemVpnSettingsAvailable: Boolean, + val autoStartAndConnectOnBoot: Boolean, ) { val isCustomWireguardPort = selectedWireguardPort is Constraint.Only && @@ -47,6 +48,7 @@ data class VpnSettingsUiState( customWireguardPort: Port? = null, availablePortRanges: List<PortRange> = emptyList(), systemVpnSettingsAvailable: Boolean = false, + autoStartAndConnectOnBoot: Boolean = false, ) = VpnSettingsUiState( mtu, @@ -64,6 +66,7 @@ data class VpnSettingsUiState( customWireguardPort, availablePortRanges, systemVpnSettingsAvailable, + autoStartAndConnectOnBoot, ) } } 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 b08708826d..61d491094a 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 @@ -1,5 +1,6 @@ package net.mullvad.mullvadvpn.di +import android.content.ComponentName import android.content.Context import android.content.SharedPreferences import android.content.pm.PackageManager @@ -11,7 +12,9 @@ import net.mullvad.mullvadvpn.constant.IS_PLAY_BUILD import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport import net.mullvad.mullvadvpn.lib.payment.PaymentProvider import net.mullvad.mullvadvpn.lib.shared.VoucherRepository +import net.mullvad.mullvadvpn.receiver.BootCompletedReceiver import net.mullvad.mullvadvpn.repository.ApiAccessRepository +import net.mullvad.mullvadvpn.repository.AutoStartAndConnectOnBootRepository import net.mullvad.mullvadvpn.repository.ChangelogRepository import net.mullvad.mullvadvpn.repository.CustomListsRepository import net.mullvad.mullvadvpn.repository.InAppNotificationController @@ -102,6 +105,10 @@ val uiModule = module { single<PackageManager> { androidContext().packageManager } single<String>(named(SELF_PACKAGE_NAME)) { androidContext().packageName } + single<ComponentName>(named(BOOT_COMPLETED_RECEIVER_COMPONENT_NAME)) { + ComponentName(androidContext(), BootCompletedReceiver::class.java) + } + viewModel { SplitTunnelingViewModel(get(), get(), Dispatchers.Default) } single { ApplicationsProvider(get(), get(named(SELF_PACKAGE_NAME))) } @@ -127,6 +134,12 @@ val uiModule = module { single { ApiAccessRepository(get()) } single { NewDeviceRepository() } single { SplashCompleteRepository() } + single { + AutoStartAndConnectOnBootRepository( + get(), + get(named(BOOT_COMPLETED_RECEIVER_COMPONENT_NAME)), + ) + } single { AccountExpiryNotificationUseCase(get()) } single { TunnelStateNotificationUseCase(get()) } @@ -195,7 +208,7 @@ val uiModule = module { viewModel { SettingsViewModel(get(), get(), IS_PLAY_BUILD) } viewModel { SplashViewModel(get(), get(), get(), get()) } viewModel { VoucherDialogViewModel(get()) } - viewModel { VpnSettingsViewModel(get(), get(), get()) } + viewModel { VpnSettingsViewModel(get(), get(), get(), get()) } viewModel { WelcomeViewModel(get(), get(), get(), get(), isPlayBuild = IS_PLAY_BUILD) } viewModel { ReportProblemViewModel(get(), get()) } viewModel { ViewLogsViewModel(get()) } @@ -226,3 +239,4 @@ val uiModule = module { const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME" const val APP_PREFERENCES_NAME = "${BuildConfig.APPLICATION_ID}.app_preferences" +const val BOOT_COMPLETED_RECEIVER_COMPONENT_NAME = "BOOT_COMPLETED_RECEIVER_COMPONENT_NAME" diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt new file mode 100644 index 0000000000..9f153e724b --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt @@ -0,0 +1,30 @@ +package net.mullvad.mullvadvpn.receiver + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.net.VpnService +import co.touchlab.kermit.Logger +import net.mullvad.mullvadvpn.lib.common.constant.KEY_CONNECT_ACTION +import net.mullvad.mullvadvpn.lib.common.constant.VPN_SERVICE_CLASS + +class BootCompletedReceiver : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action == Intent.ACTION_BOOT_COMPLETED) { + context?.let { startAndConnectTunnel(context) } + } + } + + private fun startAndConnectTunnel(context: Context) { + val hasVpnPermission = VpnService.prepare(context) == null + Logger.i("AutoStart on boot and connect, hasVpnPermission: $hasVpnPermission") + if (hasVpnPermission) { + val intent = + Intent().apply { + setClassName(context.packageName, VPN_SERVICE_CLASS) + action = KEY_CONNECT_ACTION + } + context.startForegroundService(intent) + } + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/AutoStartAndConnectOnBootRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/AutoStartAndConnectOnBootRepository.kt new file mode 100644 index 0000000000..3fc0f38c7c --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/AutoStartAndConnectOnBootRepository.kt @@ -0,0 +1,49 @@ +package net.mullvad.mullvadvpn.repository + +import android.content.ComponentName +import android.content.pm.PackageManager +import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT +import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED +import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED +import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER +import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED +import android.content.pm.PackageManager.DONT_KILL_APP +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow + +class AutoStartAndConnectOnBootRepository( + private val packageManager: PackageManager, + private val bootCompletedComponentName: ComponentName, +) { + private val _autoStartAndConnectOnBoot = MutableStateFlow(isAutoStartAndConnectOnBoot()) + val autoStartAndConnectOnBoot: StateFlow<Boolean> = _autoStartAndConnectOnBoot + + fun setAutoStartAndConnectOnBoot(enabled: Boolean) { + packageManager.setComponentEnabledSetting( + bootCompletedComponentName, + if (enabled) { + COMPONENT_ENABLED_STATE_ENABLED + } else { + COMPONENT_ENABLED_STATE_DISABLED + }, + DONT_KILL_APP, + ) + + _autoStartAndConnectOnBoot.value = isAutoStartAndConnectOnBoot() + } + + private fun isAutoStartAndConnectOnBoot(): Boolean = + when (packageManager.getComponentEnabledSetting(bootCompletedComponentName)) { + COMPONENT_ENABLED_STATE_DEFAULT -> BOOT_COMPLETED_DEFAULT_STATE + COMPONENT_ENABLED_STATE_ENABLED -> true + COMPONENT_ENABLED_STATE_DISABLED -> false + COMPONENT_ENABLED_STATE_DISABLED_USER, + COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED -> + error("Enabled setting only applicable for application") + else -> error("Unknown component enabled setting") + } + + companion object { + private const val BOOT_COMPLETED_DEFAULT_STATE = false + } +} diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt index 3baeda244a..ec73067687 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt @@ -28,6 +28,7 @@ import net.mullvad.mullvadvpn.lib.model.Port import net.mullvad.mullvadvpn.lib.model.QuantumResistantState import net.mullvad.mullvadvpn.lib.model.Settings import net.mullvad.mullvadvpn.lib.model.WireguardConstraints +import net.mullvad.mullvadvpn.repository.AutoStartAndConnectOnBootRepository import net.mullvad.mullvadvpn.repository.RelayListRepository import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsAvailableUseCase @@ -46,6 +47,7 @@ class VpnSettingsViewModel( private val repository: SettingsRepository, private val relayListRepository: RelayListRepository, private val systemVpnSettingsUseCase: SystemVpnSettingsAvailableUseCase, + private val autoStartAndConnectOnBootRepository: AutoStartAndConnectOnBootRepository, private val dispatcher: CoroutineDispatcher = Dispatchers.IO, ) : ViewModel() { @@ -55,10 +57,12 @@ class VpnSettingsViewModel( private val customPort = MutableStateFlow<Port?>(null) private val vmState = - combine(repository.settingsUpdates, relayListRepository.portRanges, customPort) { - settings, - portRanges, - customWgPort -> + combine( + repository.settingsUpdates, + relayListRepository.portRanges, + customPort, + autoStartAndConnectOnBootRepository.autoStartAndConnectOnBoot, + ) { settings, portRanges, customWgPort, autoStartAndConnectOnBoot -> VpnSettingsViewModelState( mtuValue = settings?.tunnelOptions?.wireguard?.mtu, isAutoConnectEnabled = settings?.autoConnect ?: false, @@ -78,6 +82,7 @@ class VpnSettingsViewModel( customWireguardPort = customWgPort, availablePortRanges = portRanges, systemVpnSettingsAvailable = systemVpnSettingsUseCase(), + autoStartAndConnectOnBoot = autoStartAndConnectOnBoot, ) } .stateIn( @@ -254,6 +259,12 @@ class VpnSettingsViewModel( } } + fun onToggleAutoStartAndConnectOnBoot(autoStartAndConnect: Boolean) { + viewModelScope.launch(dispatcher) { + autoStartAndConnectOnBootRepository.setAutoStartAndConnectOnBoot(autoStartAndConnect) + } + } + private fun updateDefaultDnsOptionsViaRepository(contentBlockersOption: DefaultDnsOptions) = viewModelScope.launch(dispatcher) { repository diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt index 31d5515a3c..4798a8a91b 100644 --- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt @@ -25,6 +25,7 @@ data class VpnSettingsViewModelState( val customWireguardPort: Port?, val availablePortRanges: List<PortRange>, val systemVpnSettingsAvailable: Boolean, + val autoStartAndConnectOnBoot: Boolean, ) { val isCustomWireguardPort = selectedWireguardPort is Constraint.Only && @@ -47,6 +48,7 @@ data class VpnSettingsViewModelState( customWireguardPort, availablePortRanges, systemVpnSettingsAvailable, + autoStartAndConnectOnBoot, ) companion object { @@ -67,6 +69,7 @@ data class VpnSettingsViewModelState( customWireguardPort = null, availablePortRanges = emptyList(), systemVpnSettingsAvailable = false, + autoStartAndConnectOnBoot = false, ) } } diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index 338aae32ee..fc02f8cc0d 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -403,4 +403,6 @@ <string name="connection_details_in">In</string> <string name="feature_indicators_show_more">%1$s more...</string> <string name="connect">Connect</string> + <string name="connect_on_start">Connect on device start-up</string> + <string name="connect_on_start_footer">Automatically connect on device start-up. Only works if the app has been granted the VPN permission.</string> </resources> |
