Skip to content

Commit

Permalink
Add auto-start on launch to devices without always-on setting
Browse files Browse the repository at this point in the history
  • Loading branch information
Pururun committed Jun 18, 2024
1 parent bdfca5c commit 73becde
Show file tree
Hide file tree
Showing 9 changed files with 152 additions and 29 deletions.
9 changes: 9 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -105,5 +106,13 @@
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<receiver android:name="net.mullvad.mullvadvpn.receiver.BootCompletedReceiver"
android:enabled="false"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
</manifest>
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,7 @@ fun VpnSettings(
onSelectQuantumResistanceSetting = vm::onSelectQuantumResistanceSetting,
onWireguardPortSelected = vm::onWireguardPortSelected,
onObfuscationPortSelected = vm::onObfuscationPortSelected,
onToggleConnectOnStart = vm::onToggleConnectOnStart
)
}

Expand Down Expand Up @@ -288,6 +289,7 @@ fun VpnSettingsScreen(
onSelectQuantumResistanceSetting: (quantumResistant: QuantumResistantState) -> Unit = {},
onWireguardPortSelected: (port: Constraint<Port>) -> Unit = {},
onObfuscationPortSelected: (port: Constraint<Port>) -> Unit = {},
onToggleConnectOnStart: (Boolean) -> Unit = {}
) {
var expandContentBlockersState by rememberSaveable { mutableStateOf(false) }
var expandUdp2TcpPortSettings by rememberSaveable { mutableStateOf(false) }
Expand Down Expand Up @@ -316,33 +318,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.connectOnStart,
onCellClicked = { newValue -> onToggleConnectOnStart(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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ data class VpnSettingsUiState(
val customWireguardPort: Constraint<Port>?,
val availablePortRanges: List<PortRange>,
val systemVpnSettingsAvailable: Boolean,
val connectOnStart: Boolean
) {
val selectObfuscationPortEnabled = selectedObfuscation != SelectedObfuscation.Off

Expand All @@ -41,6 +42,7 @@ data class VpnSettingsUiState(
customWireguardPort: Constraint.Only<Port>? = null,
availablePortRanges: List<PortRange> = emptyList(),
systemVpnSettingsAvailable: Boolean = false,
connectOnStart: Boolean = false,
) =
VpnSettingsUiState(
mtu,
Expand All @@ -55,7 +57,8 @@ data class VpnSettingsUiState(
selectedWireguardPort,
customWireguardPort,
availablePortRanges,
systemVpnSettingsAvailable
systemVpnSettingsAvailable,
connectOnStart
)
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -15,8 +16,10 @@ import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
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.ChangelogRepository
import net.mullvad.mullvadvpn.repository.ConnectOnStartRepository
import net.mullvad.mullvadvpn.repository.CustomListsRepository
import net.mullvad.mullvadvpn.repository.InAppNotificationController
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
Expand Down Expand Up @@ -98,6 +101,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))) }

Expand All @@ -122,6 +129,7 @@ val uiModule = module {
single { VoucherRepository(get(), get()) }
single { SplitTunnelingRepository(get()) }
single { ApiAccessRepository(get()) }
single { ConnectOnStartRepository(get(), get(named(BOOT_COMPLETED_RECEIVER_COMPONENT_NAME))) }

single { AccountExpiryNotificationUseCase(get()) }
single { TunnelStateNotificationUseCase(get()) }
Expand Down Expand Up @@ -188,7 +196,7 @@ val uiModule = module {
viewModel { SettingsViewModel(get(), get(), IS_PLAY_BUILD) }
viewModel { SplashViewModel(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()) }
Expand Down Expand Up @@ -232,3 +240,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"
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package net.mullvad.mullvadvpn.receiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.net.VpnService
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 == "android.intent.action.BOOT_COMPLETED") {
context?.let { startAndConnectTunnel(context) }
}
}

private fun startAndConnectTunnel(context: Context) {
// Check for vpn permission
if (VpnService.prepare(context) == null) {
val intent =
Intent().apply {
setClassName(context.packageName, VPN_SERVICE_CLASS)
action = KEY_CONNECT_ACTION
}
context.startForegroundService(intent)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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_ENABLED
import android.content.pm.PackageManager.DONT_KILL_APP
import kotlinx.coroutines.flow.MutableStateFlow

class ConnectOnStartRepository(
private val packageManager: PackageManager,
private val bootCompletedComponentName: ComponentName
) {
val connectOnStart = MutableStateFlow(isConnectOnStart())

fun setConnectOnStart(connect: Boolean) {
packageManager.setComponentEnabledSetting(
bootCompletedComponentName,
if (connect) {
COMPONENT_ENABLED_STATE_ENABLED
} else {
COMPONENT_ENABLED_STATE_DISABLED
},
DONT_KILL_APP
)

connectOnStart.value = isConnectOnStart()
}

private fun isConnectOnStart(): Boolean =
when (packageManager.getComponentEnabledSetting(bootCompletedComponentName)) {
COMPONENT_ENABLED_STATE_DEFAULT -> BOOT_COMPLETED_DEFAULT_STATE
COMPONENT_ENABLED_STATE_ENABLED -> true
COMPONENT_ENABLED_STATE_DISABLED -> false
else -> error("Unknown component enabled setting")
}

companion object {
private const val BOOT_COMPLETED_DEFAULT_STATE = false
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
import net.mullvad.mullvadvpn.repository.ConnectOnStartRepository
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsUseCase
Expand All @@ -46,6 +47,7 @@ class VpnSettingsViewModel(
private val repository: SettingsRepository,
private val relayListRepository: RelayListRepository,
private val systemVpnSettingsUseCase: SystemVpnSettingsUseCase,
private val connectOnStartRepository: ConnectOnStartRepository,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {

Expand All @@ -59,7 +61,8 @@ class VpnSettingsViewModel(
repository.settingsUpdates,
relayListRepository.portRanges,
customPort,
) { settings, portRanges, customWgPort ->
connectOnStartRepository.connectOnStart
) { settings, portRanges, customWgPort, connectOnStart ->
VpnSettingsViewModelState(
mtuValue = settings?.tunnelOptions?.wireguard?.mtu,
isAutoConnectEnabled = settings?.autoConnect ?: false,
Expand All @@ -77,7 +80,8 @@ class VpnSettingsViewModel(
customWireguardPort = customWgPort,
availablePortRanges = portRanges,
systemVpnSettingsAvailable =
systemVpnSettingsUseCase.systemVpnSettingsAvailable()
systemVpnSettingsUseCase.systemVpnSettingsAvailable(),
connectOnStart = connectOnStart
)
}
.stateIn(
Expand Down Expand Up @@ -245,6 +249,10 @@ class VpnSettingsViewModel(
}
}

fun onToggleConnectOnStart(connect: Boolean) {
viewModelScope.launch(dispatcher) { connectOnStartRepository.setConnectOnStart(connect) }
}

private fun updateDefaultDnsOptionsViaRepository(contentBlockersOption: DefaultDnsOptions) =
viewModelScope.launch(dispatcher) {
repository
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ data class VpnSettingsViewModelState(
val customWireguardPort: Constraint<Port>?,
val availablePortRanges: List<PortRange>,
val systemVpnSettingsAvailable: Boolean,
val connectOnStart: Boolean,
) {
fun toUiState(): VpnSettingsUiState =
VpnSettingsUiState(
Expand All @@ -38,7 +39,8 @@ data class VpnSettingsViewModelState(
selectedWireguardPort,
customWireguardPort,
availablePortRanges,
systemVpnSettingsAvailable
systemVpnSettingsAvailable,
connectOnStart
)

companion object {
Expand All @@ -56,7 +58,8 @@ data class VpnSettingsViewModelState(
selectedWireguardPort = Constraint.Any,
customWireguardPort = null,
availablePortRanges = emptyList(),
systemVpnSettingsAvailable = false
systemVpnSettingsAvailable = false,
connectOnStart = false
)
}
}
Expand Down
2 changes: 2 additions & 0 deletions android/lib/resource/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -385,4 +385,6 @@
<string name="delete_method_question">Delete method?</string>
<string name="failed_to_set_current_test_error">Failed to set to current - API not reachable</string>
<string name="failed_to_set_current_unknown_error">Failed to set to current - Unknown reason</string>
<string name="connect_on_start">Connect on start</string>
<string name="connect_on_start_footer">Automatically connect on device start-up</string>
</resources>

0 comments on commit 73becde

Please sign in to comment.