Skip to content

Commit

Permalink
Remove listener when VPN is disabled
Browse files Browse the repository at this point in the history
  • Loading branch information
karlenDimla committed Apr 24, 2024
1 parent d659f4d commit 1379028
Show file tree
Hide file tree
Showing 3 changed files with 127 additions and 170 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,11 @@ class NetPVpnSettingsViewModel @Inject constructor(
super.onStart(owner)
viewModelScope.launch(dispatcherProvider.io()) {
val excludeLocalRoutes = netPSettingsLocalConfig.vpnExcludeLocalNetworkRoutes().isEnabled()
val pauseDuringWifiCalls = netPSettingsLocalConfig.vpnPauseDuringCalls().isEnabled()
_viewState.emit(
_viewState.value.copy(
excludeLocalNetworks = excludeLocalRoutes,
pauseDuringWifiCalls = vpnDisableOnCall.isEnabled(),
pauseDuringWifiCalls = pauseDuringWifiCalls,
),
)
}
Expand Down Expand Up @@ -133,11 +134,13 @@ class NetPVpnSettingsViewModel @Inject constructor(

internal fun onEnablePauseDuringWifiCalls() {
networkProtectionPixels.reportEnabledPauseDuringCalls()
netPSettingsLocalConfig.vpnPauseDuringCalls().setEnabled(Toggle.State(enable = true))
vpnDisableOnCall.enable()
}

internal fun onDisablePauseDuringWifiCalls() {
networkProtectionPixels.reportDisabledPauseDuringCalls()
netPSettingsLocalConfig.vpnPauseDuringCalls().setEnabled(Toggle.State(enable = false))
vpnDisableOnCall.disable()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,190 +16,34 @@

package com.duckduckgo.networkprotection.impl.snooze

import android.annotation.SuppressLint
import android.content.*
import android.telephony.PhoneStateListener
import android.telephony.TelephonyManager
import androidx.lifecycle.LifecycleOwner
import com.duckduckgo.anvil.annotations.InjectWith
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.app.di.ProcessName
import com.duckduckgo.app.lifecycle.MainProcessLifecycleObserver
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.common.utils.extensions.registerExportedReceiver
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.di.scopes.ReceiverScope
import com.duckduckgo.feature.toggles.api.Toggle
import com.duckduckgo.mobile.android.vpn.Vpn
import com.duckduckgo.mobile.android.vpn.service.VpnServiceCallbacks
import com.duckduckgo.mobile.android.vpn.state.VpnStateMonitor.VpnStopReason
import com.duckduckgo.networkprotection.impl.settings.NetPSettingsLocalConfig
import com.squareup.anvil.annotations.ContributesBinding
import com.squareup.anvil.annotations.ContributesMultibinding
import javax.inject.Inject
import kotlin.properties.Delegates
import kotlinx.coroutines.*
import logcat.LogPriority
import logcat.asLog
import logcat.logcat

interface VpnDisableOnCall {
fun enable()
fun disable()

suspend fun isEnabled(): Boolean
}

@InjectWith(ReceiverScope::class)
@ContributesMultibinding(
scope = AppScope::class,
boundType = MainProcessLifecycleObserver::class,
)
@ContributesBinding(
scope = AppScope::class,
boundType = VpnDisableOnCall::class,
boundType = VpnServiceCallbacks::class,
)
class VpnCallStateReceiver @Inject constructor(
private val context: Context,
private val vpn: Vpn,
private val dispatcherProvider: DispatcherProvider,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val vpnDisableOnCall: VpnDisableOnCall,
private val netPSettingsLocalConfig: NetPSettingsLocalConfig,
@ProcessName private val processName: String,
) : BroadcastReceiver(), MainProcessLifecycleObserver, VpnDisableOnCall {

private val telephonyManager by lazy {
context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?
}
private val _listener: PhoneStateListener =
object : PhoneStateListener() {
@Deprecated("Deprecated in Java")
override fun onCallStateChanged(
state: Int,
phoneNumber: String?,
) {
appCoroutineScope.launch(dispatcherProvider.io()) {
logcat { "Call state: $state" }
if (state == TelephonyManager.CALL_STATE_IDLE) {
vpn.start()
} else {
vpn.stop()
}
}
}
}
private var currentListener by Delegates.observable<PhoneStateListener?>(null) { _, old, new ->
logcat { "CALL_STATE listener registered" }
old?.let {
telephonyManager?.listen(it, PhoneStateListener.LISTEN_NONE)
}
new?.let {
telephonyManager?.listen(new, PhoneStateListener.LISTEN_CALL_STATE)
) : VpnServiceCallbacks {
override fun onVpnStarted(coroutineScope: CoroutineScope) {
if (netPSettingsLocalConfig.vpnPauseDuringCalls().isEnabled()) {
vpnDisableOnCall.enable()
}
}

override fun enable() {
appCoroutineScope.launch(dispatcherProvider.io()) {
netPSettingsLocalConfig.vpnPauseDuringCalls().setEnabled(Toggle.State(enable = true))
context.sendBroadcast(Intent(ACTION_REGISTER_STATE_CALL_LISTENER))
}
}

override fun disable() {
appCoroutineScope.launch(dispatcherProvider.io()) {
context.sendBroadcast(Intent(ACTION_UNREGISTER_STATE_CALL_LISTENER))
netPSettingsLocalConfig.vpnPauseDuringCalls().setEnabled(Toggle.State(enable = false))
}
}

override suspend fun isEnabled(): Boolean = withContext(dispatcherProvider.io()) {
return@withContext netPSettingsLocalConfig.vpnPauseDuringCalls().isEnabled()
}

override fun onReceive(
context: Context,
intent: Intent,
override fun onVpnStopped(
coroutineScope: CoroutineScope,
vpnStopReason: VpnStopReason,
) {
logcat { "onReceive ${intent.action} in $processName" }
val pendingResult = goAsync()

when (intent.action) {
ACTION_REGISTER_STATE_CALL_LISTENER -> {
logcat { "ACTION_REGISTER_STATE_CALL_LISTENER" }
goAsync(pendingResult) {
registerListener()
}
}

ACTION_UNREGISTER_STATE_CALL_LISTENER -> {
logcat { "ACTION_UNREGISTER_STATE_CALL_LISTENER" }
goAsync(pendingResult) {
unregisterListener()
}
}

else -> {
logcat { "Unknown action ${intent.action}" }
}
}
}

override fun onCreate(owner: LifecycleOwner) {
register()
appCoroutineScope.launch(dispatcherProvider.io()) {
if (isEnabled()) {
registerListener()
} else {
logcat { "CALL_STATE listener feature is disabled" }
}
}
}

@SuppressLint("UnspecifiedRegisterReceiverFlag")
private fun register() {
unregister()
logcat { "Registering vpn call state receiver" }
context.registerExportedReceiver(
this,
IntentFilter().apply {
addAction(ACTION_REGISTER_STATE_CALL_LISTENER)
addAction(ACTION_UNREGISTER_STATE_CALL_LISTENER)
},
)
}

private fun unregister() {
kotlin.runCatching { context.unregisterReceiver(this) }
}

private fun registerListener() {
runCatching {
currentListener = _listener
}.onFailure { t ->
logcat(LogPriority.ERROR) { "CALL_STATE error registering: ${t.asLog()}" }
}
}

private fun unregisterListener() {
currentListener = null
}

companion object {
private const val ACTION_REGISTER_STATE_CALL_LISTENER = "com.duckduckgo.netp.feature.snooze.ACTION_REGISTER_STATE_CALL_LISTENER"
private const val ACTION_UNREGISTER_STATE_CALL_LISTENER = "com.duckduckgo.netp.feature.snooze.ACTION_UNREGISTER_STATE_CALL_LISTENER"
}
}

@Suppress("NoHardcodedCoroutineDispatcher")
private fun goAsync(
pendingResult: BroadcastReceiver.PendingResult?,
coroutineScope: CoroutineScope = GlobalScope,
block: suspend () -> Unit,
) {
coroutineScope.launch(Dispatchers.IO) {
try {
block()
} finally {
// Always call finish(), even if the coroutineScope was cancelled
pendingResult?.finish()
if (netPSettingsLocalConfig.vpnPauseDuringCalls().isEnabled() && !vpnDisableOnCall.isCallInProgress()) {
vpnDisableOnCall.disable()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/*
* Copyright (c) 2024 DuckDuckGo
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package com.duckduckgo.networkprotection.impl.snooze

import android.content.Context
import android.telephony.PhoneStateListener
import android.telephony.TelephonyManager
import com.duckduckgo.app.di.AppCoroutineScope
import com.duckduckgo.common.utils.DispatcherProvider
import com.duckduckgo.di.scopes.AppScope
import com.duckduckgo.mobile.android.vpn.Vpn
import com.squareup.anvil.annotations.ContributesBinding
import dagger.SingleInstanceIn
import javax.inject.Inject
import kotlin.properties.Delegates
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.launch
import logcat.LogPriority
import logcat.asLog
import logcat.logcat

interface VpnDisableOnCall {
fun enable()
fun disable()
fun isCallInProgress(): Boolean
}

@ContributesBinding(AppScope::class)
@SingleInstanceIn(AppScope::class)
class RealVpnDisableOnCall @Inject constructor(
private val dispatcherProvider: DispatcherProvider,
@AppCoroutineScope private val appCoroutineScope: CoroutineScope,
private val context: Context,
private val vpn: Vpn,
) : VpnDisableOnCall {
private var _callInProgress: Boolean = false
private val telephonyManager by lazy {
context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager?
}
private val _listener: PhoneStateListener =
object : PhoneStateListener() {
@Deprecated("Deprecated in Java")
override fun onCallStateChanged(
state: Int,
phoneNumber: String?,
) {
appCoroutineScope.launch(dispatcherProvider.io()) {
logcat { "Call state: $state" }
if (state == TelephonyManager.CALL_STATE_IDLE) {
_callInProgress = false
vpn.start()
} else {
_callInProgress = true
vpn.stop()
}
}
}
}
private var currentListener by Delegates.observable<PhoneStateListener?>(null) { _, old, new ->
logcat { "CALL_STATE listener registered" }
old?.let {
telephonyManager?.listen(it, PhoneStateListener.LISTEN_NONE)
}
new?.let {
telephonyManager?.listen(new, PhoneStateListener.LISTEN_CALL_STATE)
}
}

override fun enable() {
appCoroutineScope.launch(dispatcherProvider.io()) {
_callInProgress = false
registerListener()
}
}

override fun disable() {
appCoroutineScope.launch(dispatcherProvider.io()) {
_callInProgress = false
unregisterListener()
}
}

override fun isCallInProgress(): Boolean = _callInProgress

private fun registerListener() {
runCatching {
currentListener = _listener
}.onFailure { t ->
logcat(LogPriority.ERROR) { "CALL_STATE error registering: ${t.asLog()}" }
}
}

private fun unregisterListener() {
currentListener = null
}
}

0 comments on commit 1379028

Please sign in to comment.