Skip to content

Commit

Permalink
Merge branch 'android-auto-start-migration'
Browse files Browse the repository at this point in the history
  • Loading branch information
Rawa committed Sep 26, 2024
2 parents 585d919 + a984929 commit c61a054
Show file tree
Hide file tree
Showing 21 changed files with 313 additions and 66 deletions.
4 changes: 4 additions & 0 deletions android/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ Line wrap the file at 100 chars. Th
- Add DAITA (Defence against AI-guided Traffic Analysis) setting.
- Add WireGuard over Shadowsocks.
- Add feature indicators to the main view along with redesigning the connection details.
- Add new "Connect on device start-up" setting for devices without system VPN settings.

### Removed
- Legacy auto-connect feature.

### Changed
- Update colors in the app to be more in line with material design.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,6 @@ class VpnSettingsScreenTest {
// Arrange
setContentWithTheme { VpnSettingsScreen(state = VpnSettingsUiState.createDefault()) }

onNodeWithText("Auto-connect (legacy)").assertExists()

onNodeWithTag(LAZY_LIST_TEST_TAG)
.performScrollToNode(hasTestTag(LAZY_LIST_LAST_ITEM_TEST_TAG))

Expand Down Expand Up @@ -524,6 +522,43 @@ class VpnSettingsScreenTest {
verify { mockOnShowCustomPortDialog.invoke() }
}

@Test
fun ensureConnectOnStartIsShownWhenSystemVpnSettingsAvailableIsFalse() =
composeExtension.use {
// Arrange
setContentWithTheme {
VpnSettingsScreen(
state = VpnSettingsUiState.createDefault(systemVpnSettingsAvailable = false)
)
}

// Assert
onNodeWithText("Connect on device start-up").assertExists()
}

@Test
fun whenClickingOnConnectOnStartShouldCallOnToggleAutoStartAndConnectOnBoot() =
composeExtension.use {
// Arrange
val mockOnToggleAutoStartAndConnectOnBoot: (Boolean) -> Unit = mockk(relaxed = true)
setContentWithTheme {
VpnSettingsScreen(
state =
VpnSettingsUiState.createDefault(
systemVpnSettingsAvailable = false,
autoStartAndConnectOnBoot = false,
),
onToggleAutoStartAndConnectOnBoot = mockOnToggleAutoStartAndConnectOnBoot,
)
}

// Act
onNodeWithText("Connect on device start-up").performClick()

// Assert
verify { mockOnToggleAutoStartAndConnectOnBoot.invoke(true) }
}

companion object {
private const val LOCAL_DNS_SERVER_WARNING =
"The local DNS server will not work unless you enable " +
Expand Down
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 @@ -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>
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@ class VpnSettingsUiStatePreviewParameterProvider : PreviewParameterProvider<VpnS
VpnSettingsUiState.createDefault(),
VpnSettingsUiState.createDefault(
mtu = Mtu(MTU),
isAutoConnectEnabled = true,
isLocalNetworkSharingEnabled = true,
isDaitaEnabled = true,
isCustomDnsEnabled = true,
Expand All @@ -38,6 +37,7 @@ class VpnSettingsUiStatePreviewParameterProvider : PreviewParameterProvider<VpnS
customWireguardPort = PORT1,
availablePortRanges = listOf(PORT1..PORT2),
systemVpnSettingsAvailable = true,
autoStartAndConnectOnBoot = true,
),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,6 @@ private fun PreviewVpnSettings(
onToggleBlockTrackers = {},
onToggleBlockAds = {},
onToggleBlockMalware = {},
onToggleAutoConnect = {},
onToggleLocalNetworkSharing = {},
onToggleBlockAdultContent = {},
onToggleBlockGambling = {},
Expand Down Expand Up @@ -232,7 +231,6 @@ fun VpnSettings(
onToggleBlockTrackers = vm::onToggleBlockTrackers,
onToggleBlockAds = vm::onToggleBlockAds,
onToggleBlockMalware = vm::onToggleBlockMalware,
onToggleAutoConnect = vm::onToggleAutoConnect,
onToggleLocalNetworkSharing = vm::onToggleLocalNetworkSharing,
onDisableDaita = { vm.onToggleDaita(false) },
onToggleBlockAdultContent = vm::onToggleBlockAdultContent,
Expand Down Expand Up @@ -264,6 +262,7 @@ fun VpnSettings(
dropUnlessResumed { navigator.navigate(ShadowsocksSettingsDestination) },
navigateToUdp2TcpSettings =
dropUnlessResumed { navigator.navigate(Udp2TcpSettingsDestination) },
onToggleAutoStartAndConnectOnBoot = vm::onToggleAutoStartAndConnectOnBoot,
)
}

Expand All @@ -288,7 +287,6 @@ fun VpnSettingsScreen(
onToggleBlockTrackers: (Boolean) -> Unit = {},
onToggleBlockAds: (Boolean) -> Unit = {},
onToggleBlockMalware: (Boolean) -> Unit = {},
onToggleAutoConnect: (Boolean) -> Unit = {},
onToggleLocalNetworkSharing: (Boolean) -> Unit = {},
onDisableDaita: () -> Unit = {},
onToggleBlockAdultContent: (Boolean) -> Unit = {},
Expand All @@ -303,6 +301,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
Expand Down Expand Up @@ -330,33 +329,28 @@ 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) {
} 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.auto_connect_footer_legacy,
R.string.connect_on_start_footer,
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)
)
}
}

item {
Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
HeaderSwitchComposeCell(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@ import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem

data class VpnSettingsUiState(
val mtu: Mtu?,
val isAutoConnectEnabled: Boolean,
val isLocalNetworkSharingEnabled: Boolean,
val isDaitaEnabled: Boolean,
val isCustomDnsEnabled: Boolean,
Expand All @@ -25,6 +24,7 @@ data class VpnSettingsUiState(
val customWireguardPort: Port?,
val availablePortRanges: List<PortRange>,
val systemVpnSettingsAvailable: Boolean,
val autoStartAndConnectOnBoot: Boolean,
) {
val isCustomWireguardPort =
selectedWireguardPort is Constraint.Only &&
Expand All @@ -33,7 +33,6 @@ data class VpnSettingsUiState(
companion object {
fun createDefault(
mtu: Mtu? = null,
isAutoConnectEnabled: Boolean = false,
isLocalNetworkSharingEnabled: Boolean = false,
isDaitaEnabled: Boolean = false,
isCustomDnsEnabled: Boolean = false,
Expand All @@ -47,10 +46,10 @@ data class VpnSettingsUiState(
customWireguardPort: Port? = null,
availablePortRanges: List<PortRange> = emptyList(),
systemVpnSettingsAvailable: Boolean = false,
autoStartAndConnectOnBoot: Boolean = false,
) =
VpnSettingsUiState(
mtu,
isAutoConnectEnabled,
isLocalNetworkSharingEnabled,
isDaitaEnabled,
isCustomDnsEnabled,
Expand All @@ -64,6 +63,7 @@ data class VpnSettingsUiState(
customWireguardPort,
availablePortRanges,
systemVpnSettingsAvailable,
autoStartAndConnectOnBoot,
)
}
}
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 @@ -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
Expand Down Expand Up @@ -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))) }
Expand All @@ -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()) }
Expand Down Expand Up @@ -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()) }
Expand Down Expand Up @@ -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"
Original file line number Diff line number Diff line change
@@ -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)
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,8 +68,6 @@ class SettingsRepository(

suspend fun setObfuscation(value: ObfuscationMode) = managementService.setObfuscation(value)

suspend fun setAutoConnect(isEnabled: Boolean) = managementService.setAutoConnect(isEnabled)

suspend fun setLocalNetworkSharing(isEnabled: Boolean) =
managementService.setAllowLan(isEnabled)

Expand Down
Loading

0 comments on commit c61a054

Please sign in to comment.