summaryrefslogtreecommitdiffhomepage
path: root/android/app
diff options
context:
space:
mode:
authorAlbin <albin@mullvad.net>2023-07-26 11:49:22 +0200
committerAlbin <albin@mullvad.net>2023-07-27 10:41:29 +0200
commitd4e7a6f0b63027d752e19a55d6df9dcf4a7095ff (patch)
tree797dc237e0b5dc51f8b7135cc95e7472d16ab085 /android/app
parente320571c388aacc74e7c9e73a54374ec189c4792 (diff)
downloadmullvadvpn-d4e7a6f0b63027d752e19a55d6df9dcf4a7095ff.tar.xz
mullvadvpn-d4e7a6f0b63027d752e19a55d6df9dcf4a7095ff.zip
Move tile classes to tile module
Diffstat (limited to 'android/app')
-rw-r--r--android/app/src/main/AndroidManifest.xml2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/ServiceConnection.kt132
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadTileService.kt143
3 files changed, 1 insertions, 276 deletions
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index 4671de2d88..7eede7ed33 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -73,7 +73,7 @@
Tile services must be exported and protected by the bind tile permission
(android.permission.BIND_QUICK_SETTINGS_TILE).
-->
- <service android:name="net.mullvad.mullvadvpn.service.MullvadTileService"
+ <service android:name="net.mullvad.mullvadvpn.tile.MullvadTileService"
android:exported="true"
android:permission="android.permission.BIND_QUICK_SETTINGS_TILE"
android:label="@string/toggle_vpn"
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/ServiceConnection.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/ServiceConnection.kt
deleted file mode 100644
index 17682911e3..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/ipc/ServiceConnection.kt
+++ /dev/null
@@ -1,132 +0,0 @@
-package net.mullvad.mullvadvpn.ipc
-
-import android.content.Context
-import android.content.Intent
-import android.os.IBinder
-import android.os.Looper
-import android.os.Messenger
-import kotlin.reflect.KClass
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.channels.Channel
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.collect
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.consumeAsFlow
-import kotlinx.coroutines.flow.filterNotNull
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onCompletion
-import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.lib.common.util.DispatchingFlow
-import net.mullvad.mullvadvpn.lib.common.util.bindServiceFlow
-import net.mullvad.mullvadvpn.lib.common.util.dispatchTo
-import net.mullvad.mullvadvpn.lib.ipc.Event
-import net.mullvad.mullvadvpn.lib.ipc.HandlerFlow
-import net.mullvad.mullvadvpn.lib.ipc.Request
-import net.mullvad.mullvadvpn.model.ServiceResult
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.mullvadvpn.service.MullvadVpnService
-
-@FlowPreview
-class ServiceConnection(context: Context, scope: CoroutineScope) {
- private val activeListeners = MutableStateFlow<Pair<Messenger, Int>?>(null)
- private val handler = HandlerFlow(Looper.getMainLooper(), Event.Companion::fromMessage)
- private val listener = Messenger(handler)
- private val listenerId = MutableStateFlow<Int?>(null)
-
- private lateinit var listenerRegistrations: StateFlow<Pair<Messenger, Int>?>
-
- lateinit var tunnelState: Flow<Pair<TunnelState, ServiceResult.ConnectionState>>
- private set
-
- private val serviceConnectionStateChannel =
- Channel<ServiceResult.ConnectionState>(Channel.RENDEZVOUS)
-
- init {
- val dispatcher =
- handler.filterNotNull().dispatchTo {
- listenerRegistrations =
- subscribeToState(Event.ListenerReady::class, scope) {
- Pair(connection, listenerId)
- }
-
- val tunnelStateEvents =
- subscribeToState(
- Event.TunnelStateChange::class,
- scope,
- TunnelState.Disconnected
- ) {
- tunnelState
- }
-
- tunnelState =
- tunnelStateEvents.combine(serviceConnectionStateChannel.consumeAsFlow()) {
- tunnelState,
- serviceConnectionState ->
- tunnelState to serviceConnectionState
- }
- }
-
- scope.launch { connect(context) }
- scope.launch { dispatcher.collect() }
- scope.launch { unregisterOldListeners() }
- scope.launch { listenerRegistrations.collect { activeListeners.value = it } }
- }
-
- private suspend fun connect(context: Context) {
- val intent = Intent(context, MullvadVpnService::class.java)
-
- context
- .bindServiceFlow(intent)
- .onStart { emit(ServiceResult.NOT_CONNECTED) }
- .onEach { result -> serviceConnectionStateChannel.send(result.connectionState) }
- .collect { result ->
- activeListeners.value = null
- result.binder?.let(::registerListener)
- }
- }
-
- private fun registerListener(binder: IBinder) {
- val request = Request.RegisterListener(listener)
- val messenger = Messenger(binder)
-
- messenger.send(request.message)
- }
-
- private suspend fun unregisterOldListeners() {
- var oldListener: Pair<Messenger, Int>? = null
-
- activeListeners
- .onCompletion { oldListener?.let(::unregisterListener) }
- .collect { newListener ->
- oldListener?.let(::unregisterListener)
- oldListener = newListener
- }
- }
-
- private fun unregisterListener(registration: Pair<Messenger, Int>) {
- val (messenger, listenerId) = registration
- val request = Request.UnregisterListener(listenerId)
-
- messenger.send(request.message)
- }
-
- private fun <V : Any, D> DispatchingFlow<in V>.subscribeToState(
- event: KClass<V>,
- scope: CoroutineScope,
- dataExtractor: suspend V.() -> D
- ) = subscribe(event).map(dataExtractor).stateIn(scope, SharingStarted.Lazily, null)
-
- private fun <V : Any, D> DispatchingFlow<in V>.subscribeToState(
- event: KClass<V>,
- scope: CoroutineScope,
- initialValue: D,
- dataExtractor: suspend V.() -> D
- ) = subscribe(event).map(dataExtractor).stateIn(scope, SharingStarted.Lazily, initialValue)
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadTileService.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadTileService.kt
deleted file mode 100644
index 021c20bc05..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/service/MullvadTileService.kt
+++ /dev/null
@@ -1,143 +0,0 @@
-package net.mullvad.mullvadvpn.service
-
-import android.content.Intent
-import android.graphics.drawable.Icon
-import android.service.quicksettings.Tile
-import android.service.quicksettings.TileService
-import android.util.Log
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.FlowPreview
-import kotlinx.coroutines.MainScope
-import kotlinx.coroutines.cancel
-import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.debounce
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.withTimeoutOrNull
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.ipc.ServiceConnection
-import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.setSubtitleIfSupported
-import net.mullvad.mullvadvpn.model.ServiceResult
-import net.mullvad.mullvadvpn.model.TunnelState
-import net.mullvad.talpid.tunnel.ActionAfterDisconnect
-
-class MullvadTileService : TileService() {
- private var scope: CoroutineScope? = null
-
- private lateinit var securedIcon: Icon
- private lateinit var unsecuredIcon: Icon
-
- override fun onCreate() {
- securedIcon = Icon.createWithResource(this, R.drawable.small_logo_white)
- unsecuredIcon = Icon.createWithResource(this, R.drawable.small_logo_black)
- }
-
- override fun onClick() {
- // Workaround for the reported bug: https://issuetracker.google.com/issues/236862865
- suspend fun isUnlockStatusPropagatedWithinTimeout(
- unlockTimeoutMillis: Long,
- unlockCheckDelayMillis: Long
- ): Boolean {
- return withTimeoutOrNull(unlockTimeoutMillis) {
- while (isLocked) {
- delay(unlockCheckDelayMillis)
- }
- return@withTimeoutOrNull true
- }
- ?: false
- }
-
- unlockAndRun {
- runBlocking {
- val isUnlockStatusPropagated =
- isUnlockStatusPropagatedWithinTimeout(
- unlockTimeoutMillis = 1000L,
- unlockCheckDelayMillis = 100L
- )
-
- if (isUnlockStatusPropagated) {
- toggleTunnel()
- } else {
- Log.e("mullvad", "Unable to toggle tunnel state")
- }
- }
- }
- }
-
- override fun onStartListening() {
- scope = MainScope().apply { launchListenToTunnelState() }
- }
-
- override fun onStopListening() {
- scope?.cancel()
- }
-
- private fun toggleTunnel() {
- val intent =
- Intent(this, MullvadVpnService::class.java).apply {
- action =
- if (qsTile.state == Tile.STATE_INACTIVE) {
- MullvadVpnService.KEY_CONNECT_ACTION
- } else {
- MullvadVpnService.KEY_DISCONNECT_ACTION
- }
- }
-
- // Always start as foreground in case tile is out-of-sync.
- startForegroundService(intent)
- }
-
- @OptIn(FlowPreview::class)
- private fun CoroutineScope.launchListenToTunnelState() = launch {
- ServiceConnection(this@MullvadTileService, this)
- .tunnelState
- .debounce(300L)
- .map { (tunnelState, connectionState) -> mapToTileState(tunnelState, connectionState) }
- .collect { updateTileState(it) }
- }
-
- private fun mapToTileState(
- tunnelState: TunnelState,
- connectionState: ServiceResult.ConnectionState
- ): Int {
- return if (connectionState == ServiceResult.ConnectionState.CONNECTED) {
- when (tunnelState) {
- is TunnelState.Disconnected -> Tile.STATE_INACTIVE
- is TunnelState.Connecting -> Tile.STATE_ACTIVE
- is TunnelState.Connected -> Tile.STATE_ACTIVE
- is TunnelState.Disconnecting -> {
- if (tunnelState.actionAfterDisconnect == ActionAfterDisconnect.Reconnect) {
- Tile.STATE_ACTIVE
- } else {
- Tile.STATE_INACTIVE
- }
- }
- is TunnelState.Error -> {
- if (tunnelState.errorState.isBlocking) {
- Tile.STATE_ACTIVE
- } else {
- Tile.STATE_INACTIVE
- }
- }
- }
- } else {
- Tile.STATE_INACTIVE
- }
- }
-
- private fun updateTileState(newState: Int) {
- qsTile?.apply {
- if (newState == Tile.STATE_ACTIVE) {
- state = Tile.STATE_ACTIVE
- icon = securedIcon
- setSubtitleIfSupported(resources.getText(R.string.secured))
- } else {
- state = Tile.STATE_INACTIVE
- icon = unsecuredIcon
- setSubtitleIfSupported(resources.getText(R.string.unsecured))
- }
- updateTile()
- }
- }
-}