Skip to content

Commit 73becde

Browse files
committed
Add auto-start on launch to devices without always-on setting
1 parent bdfca5c commit 73becde

File tree

9 files changed

+152
-29
lines changed

9 files changed

+152
-29
lines changed

android/app/src/main/AndroidManifest.xml

+9
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
88
<!-- https://developer.android.com/guide/components/fg-service-types#system-exempted -->
99
<uses-permission android:name="android.permission.FOREGROUND_SERVICE_SYSTEM_EXEMPTED" />
10+
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
1011
<uses-feature android:name="android.hardware.touchscreen"
1112
android:required="false" />
1213
<uses-feature android:name="android.hardware.faketouch"
@@ -105,5 +106,13 @@
105106
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
106107
android:resource="@xml/provider_paths" />
107108
</provider>
109+
<receiver android:name="net.mullvad.mullvadvpn.receiver.BootCompletedReceiver"
110+
android:enabled="false"
111+
android:exported="true">
112+
<intent-filter>
113+
<action android:name="android.intent.action.QUICKBOOT_POWERON" />
114+
<action android:name="android.intent.action.BOOT_COMPLETED" />
115+
</intent-filter>
116+
</receiver>
108117
</application>
109118
</manifest>

android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt

+42-23
Original file line numberDiff line numberDiff line change
@@ -252,6 +252,7 @@ fun VpnSettings(
252252
onSelectQuantumResistanceSetting = vm::onSelectQuantumResistanceSetting,
253253
onWireguardPortSelected = vm::onWireguardPortSelected,
254254
onObfuscationPortSelected = vm::onObfuscationPortSelected,
255+
onToggleConnectOnStart = vm::onToggleConnectOnStart
255256
)
256257
}
257258

@@ -288,6 +289,7 @@ fun VpnSettingsScreen(
288289
onSelectQuantumResistanceSetting: (quantumResistant: QuantumResistantState) -> Unit = {},
289290
onWireguardPortSelected: (port: Constraint<Port>) -> Unit = {},
290291
onObfuscationPortSelected: (port: Constraint<Port>) -> Unit = {},
292+
onToggleConnectOnStart: (Boolean) -> Unit = {}
291293
) {
292294
var expandContentBlockersState by rememberSaveable { mutableStateOf(false) }
293295
var expandUdp2TcpPortSettings by rememberSaveable { mutableStateOf(false) }
@@ -316,33 +318,50 @@ fun VpnSettingsScreen(
316318
text = stringResource(id = R.string.auto_connect_and_lockdown_mode_footer)
317319
)
318320
}
319-
}
320-
item {
321-
Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
322-
HeaderSwitchComposeCell(
323-
title = stringResource(R.string.auto_connect_legacy),
324-
isToggled = state.isAutoConnectEnabled,
325-
isEnabled = true,
326-
onCellClicked = { newValue -> onToggleAutoConnect(newValue) }
327-
)
328-
}
329-
item {
330-
SwitchComposeSubtitleCell(
331-
text =
332-
HtmlCompat.fromHtml(
333-
if (state.systemVpnSettingsAvailable) {
321+
item {
322+
Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
323+
HeaderSwitchComposeCell(
324+
title = stringResource(R.string.auto_connect_legacy),
325+
isToggled = state.isAutoConnectEnabled,
326+
isEnabled = true,
327+
onCellClicked = { newValue -> onToggleAutoConnect(newValue) }
328+
)
329+
}
330+
item {
331+
SwitchComposeSubtitleCell(
332+
text =
333+
HtmlCompat.fromHtml(
334334
textResource(
335335
R.string.auto_connect_footer_legacy,
336336
textResource(R.string.auto_connect_and_lockdown_mode)
337-
)
338-
} else {
339-
textResource(R.string.auto_connect_footer_legacy_tv)
340-
},
341-
HtmlCompat.FROM_HTML_MODE_COMPACT
342-
)
343-
.toAnnotatedString(boldFontWeight = FontWeight.ExtraBold)
344-
)
337+
),
338+
HtmlCompat.FROM_HTML_MODE_COMPACT
339+
)
340+
.toAnnotatedString(boldFontWeight = FontWeight.ExtraBold)
341+
)
342+
}
343+
} else {
344+
item {
345+
Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
346+
HeaderSwitchComposeCell(
347+
title = stringResource(R.string.connect_on_start),
348+
isToggled = state.connectOnStart,
349+
onCellClicked = { newValue -> onToggleConnectOnStart(newValue) }
350+
)
351+
SwitchComposeSubtitleCell(
352+
text =
353+
HtmlCompat.fromHtml(
354+
textResource(
355+
R.string.connect_on_start_footer,
356+
textResource(R.string.auto_connect_and_lockdown_mode)
357+
),
358+
HtmlCompat.FROM_HTML_MODE_COMPACT
359+
)
360+
.toAnnotatedString(boldFontWeight = FontWeight.ExtraBold)
361+
)
362+
}
345363
}
364+
346365
item {
347366
Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
348367
HeaderSwitchComposeCell(

android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt

+4-1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ data class VpnSettingsUiState(
2323
val customWireguardPort: Constraint<Port>?,
2424
val availablePortRanges: List<PortRange>,
2525
val systemVpnSettingsAvailable: Boolean,
26+
val connectOnStart: Boolean
2627
) {
2728
val selectObfuscationPortEnabled = selectedObfuscation != SelectedObfuscation.Off
2829

@@ -41,6 +42,7 @@ data class VpnSettingsUiState(
4142
customWireguardPort: Constraint.Only<Port>? = null,
4243
availablePortRanges: List<PortRange> = emptyList(),
4344
systemVpnSettingsAvailable: Boolean = false,
45+
connectOnStart: Boolean = false,
4446
) =
4547
VpnSettingsUiState(
4648
mtu,
@@ -55,7 +57,8 @@ data class VpnSettingsUiState(
5557
selectedWireguardPort,
5658
customWireguardPort,
5759
availablePortRanges,
58-
systemVpnSettingsAvailable
60+
systemVpnSettingsAvailable,
61+
connectOnStart
5962
)
6063
}
6164
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt

+10-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package net.mullvad.mullvadvpn.di
22

3+
import android.content.ComponentName
34
import android.content.Context
45
import android.content.SharedPreferences
56
import android.content.pm.PackageManager
@@ -15,8 +16,10 @@ import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName
1516
import net.mullvad.mullvadvpn.lib.model.GeoLocationId
1617
import net.mullvad.mullvadvpn.lib.payment.PaymentProvider
1718
import net.mullvad.mullvadvpn.lib.shared.VoucherRepository
19+
import net.mullvad.mullvadvpn.receiver.BootCompletedReceiver
1820
import net.mullvad.mullvadvpn.repository.ApiAccessRepository
1921
import net.mullvad.mullvadvpn.repository.ChangelogRepository
22+
import net.mullvad.mullvadvpn.repository.ConnectOnStartRepository
2023
import net.mullvad.mullvadvpn.repository.CustomListsRepository
2124
import net.mullvad.mullvadvpn.repository.InAppNotificationController
2225
import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository
@@ -98,6 +101,10 @@ val uiModule = module {
98101
single<PackageManager> { androidContext().packageManager }
99102
single<String>(named(SELF_PACKAGE_NAME)) { androidContext().packageName }
100103

104+
single<ComponentName>(named(BOOT_COMPLETED_RECEIVER_COMPONENT_NAME)) {
105+
ComponentName(androidContext(), BootCompletedReceiver::class.java)
106+
}
107+
101108
viewModel { SplitTunnelingViewModel(get(), get(), Dispatchers.Default) }
102109
single { ApplicationsProvider(get(), get(named(SELF_PACKAGE_NAME))) }
103110

@@ -122,6 +129,7 @@ val uiModule = module {
122129
single { VoucherRepository(get(), get()) }
123130
single { SplitTunnelingRepository(get()) }
124131
single { ApiAccessRepository(get()) }
132+
single { ConnectOnStartRepository(get(), get(named(BOOT_COMPLETED_RECEIVER_COMPONENT_NAME))) }
125133

126134
single { AccountExpiryNotificationUseCase(get()) }
127135
single { TunnelStateNotificationUseCase(get()) }
@@ -188,7 +196,7 @@ val uiModule = module {
188196
viewModel { SettingsViewModel(get(), get(), IS_PLAY_BUILD) }
189197
viewModel { SplashViewModel(get(), get(), get()) }
190198
viewModel { VoucherDialogViewModel(get()) }
191-
viewModel { VpnSettingsViewModel(get(), get(), get()) }
199+
viewModel { VpnSettingsViewModel(get(), get(), get(), get()) }
192200
viewModel { WelcomeViewModel(get(), get(), get(), get(), isPlayBuild = IS_PLAY_BUILD) }
193201
viewModel { ReportProblemViewModel(get(), get()) }
194202
viewModel { ViewLogsViewModel(get()) }
@@ -232,3 +240,4 @@ val uiModule = module {
232240

233241
const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME"
234242
const val APP_PREFERENCES_NAME = "${BuildConfig.APPLICATION_ID}.app_preferences"
243+
const val BOOT_COMPLETED_RECEIVER_COMPONENT_NAME = "BOOT_COMPLETED_RECEIVER_COMPONENT_NAME"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package net.mullvad.mullvadvpn.receiver
2+
3+
import android.content.BroadcastReceiver
4+
import android.content.Context
5+
import android.content.Intent
6+
import android.net.VpnService
7+
import net.mullvad.mullvadvpn.lib.common.constant.KEY_CONNECT_ACTION
8+
import net.mullvad.mullvadvpn.lib.common.constant.VPN_SERVICE_CLASS
9+
10+
class BootCompletedReceiver : BroadcastReceiver() {
11+
override fun onReceive(context: Context?, intent: Intent?) {
12+
if (intent?.action == "android.intent.action.BOOT_COMPLETED") {
13+
context?.let { startAndConnectTunnel(context) }
14+
}
15+
}
16+
17+
private fun startAndConnectTunnel(context: Context) {
18+
// Check for vpn permission
19+
if (VpnService.prepare(context) == null) {
20+
val intent =
21+
Intent().apply {
22+
setClassName(context.packageName, VPN_SERVICE_CLASS)
23+
action = KEY_CONNECT_ACTION
24+
}
25+
context.startForegroundService(intent)
26+
}
27+
}
28+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
package net.mullvad.mullvadvpn.repository
2+
3+
import android.content.ComponentName
4+
import android.content.pm.PackageManager
5+
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
6+
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
7+
import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
8+
import android.content.pm.PackageManager.DONT_KILL_APP
9+
import kotlinx.coroutines.flow.MutableStateFlow
10+
11+
class ConnectOnStartRepository(
12+
private val packageManager: PackageManager,
13+
private val bootCompletedComponentName: ComponentName
14+
) {
15+
val connectOnStart = MutableStateFlow(isConnectOnStart())
16+
17+
fun setConnectOnStart(connect: Boolean) {
18+
packageManager.setComponentEnabledSetting(
19+
bootCompletedComponentName,
20+
if (connect) {
21+
COMPONENT_ENABLED_STATE_ENABLED
22+
} else {
23+
COMPONENT_ENABLED_STATE_DISABLED
24+
},
25+
DONT_KILL_APP
26+
)
27+
28+
connectOnStart.value = isConnectOnStart()
29+
}
30+
31+
private fun isConnectOnStart(): Boolean =
32+
when (packageManager.getComponentEnabledSetting(bootCompletedComponentName)) {
33+
COMPONENT_ENABLED_STATE_DEFAULT -> BOOT_COMPLETED_DEFAULT_STATE
34+
COMPONENT_ENABLED_STATE_ENABLED -> true
35+
COMPONENT_ENABLED_STATE_DISABLED -> false
36+
else -> error("Unknown component enabled setting")
37+
}
38+
39+
companion object {
40+
private const val BOOT_COMPLETED_DEFAULT_STATE = false
41+
}
42+
}

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt

+10-2
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
2727
import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
2828
import net.mullvad.mullvadvpn.lib.model.Settings
2929
import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
30+
import net.mullvad.mullvadvpn.repository.ConnectOnStartRepository
3031
import net.mullvad.mullvadvpn.repository.RelayListRepository
3132
import net.mullvad.mullvadvpn.repository.SettingsRepository
3233
import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsUseCase
@@ -46,6 +47,7 @@ class VpnSettingsViewModel(
4647
private val repository: SettingsRepository,
4748
private val relayListRepository: RelayListRepository,
4849
private val systemVpnSettingsUseCase: SystemVpnSettingsUseCase,
50+
private val connectOnStartRepository: ConnectOnStartRepository,
4951
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
5052
) : ViewModel() {
5153

@@ -59,7 +61,8 @@ class VpnSettingsViewModel(
5961
repository.settingsUpdates,
6062
relayListRepository.portRanges,
6163
customPort,
62-
) { settings, portRanges, customWgPort ->
64+
connectOnStartRepository.connectOnStart
65+
) { settings, portRanges, customWgPort, connectOnStart ->
6366
VpnSettingsViewModelState(
6467
mtuValue = settings?.tunnelOptions?.wireguard?.mtu,
6568
isAutoConnectEnabled = settings?.autoConnect ?: false,
@@ -77,7 +80,8 @@ class VpnSettingsViewModel(
7780
customWireguardPort = customWgPort,
7881
availablePortRanges = portRanges,
7982
systemVpnSettingsAvailable =
80-
systemVpnSettingsUseCase.systemVpnSettingsAvailable()
83+
systemVpnSettingsUseCase.systemVpnSettingsAvailable(),
84+
connectOnStart = connectOnStart
8185
)
8286
}
8387
.stateIn(
@@ -245,6 +249,10 @@ class VpnSettingsViewModel(
245249
}
246250
}
247251

252+
fun onToggleConnectOnStart(connect: Boolean) {
253+
viewModelScope.launch(dispatcher) { connectOnStartRepository.setConnectOnStart(connect) }
254+
}
255+
248256
private fun updateDefaultDnsOptionsViaRepository(contentBlockersOption: DefaultDnsOptions) =
249257
viewModelScope.launch(dispatcher) {
250258
repository

android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt

+5-2
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ data class VpnSettingsViewModelState(
2323
val customWireguardPort: Constraint<Port>?,
2424
val availablePortRanges: List<PortRange>,
2525
val systemVpnSettingsAvailable: Boolean,
26+
val connectOnStart: Boolean,
2627
) {
2728
fun toUiState(): VpnSettingsUiState =
2829
VpnSettingsUiState(
@@ -38,7 +39,8 @@ data class VpnSettingsViewModelState(
3839
selectedWireguardPort,
3940
customWireguardPort,
4041
availablePortRanges,
41-
systemVpnSettingsAvailable
42+
systemVpnSettingsAvailable,
43+
connectOnStart
4244
)
4345

4446
companion object {
@@ -56,7 +58,8 @@ data class VpnSettingsViewModelState(
5658
selectedWireguardPort = Constraint.Any,
5759
customWireguardPort = null,
5860
availablePortRanges = emptyList(),
59-
systemVpnSettingsAvailable = false
61+
systemVpnSettingsAvailable = false,
62+
connectOnStart = false
6063
)
6164
}
6265
}

android/lib/resource/src/main/res/values/strings.xml

+2
Original file line numberDiff line numberDiff line change
@@ -385,4 +385,6 @@
385385
<string name="delete_method_question">Delete method?</string>
386386
<string name="failed_to_set_current_test_error">Failed to set to current - API not reachable</string>
387387
<string name="failed_to_set_current_unknown_error">Failed to set to current - Unknown reason</string>
388+
<string name="connect_on_start">Connect on start</string>
389+
<string name="connect_on_start_footer">Automatically connect on device start-up</string>
388390
</resources>

0 commit comments

Comments
 (0)