From 630028774a87976bb970b2cebc71c10c75736a4d Mon Sep 17 00:00:00 2001 From: Ovsyannikov_M Date: Thu, 10 Apr 2025 19:38:09 +0300 Subject: [PATCH 1/6] ISSUE-712: add airplane mode changer --- .../kaspresso/device/network/Network.kt | 9 +++- .../kaspresso/device/network/NetworkImpl.kt | 43 ++++++++++++++++++- .../AirplaneModeSettingsScreen.kt | 33 ++++++++++++++ 3 files changed, 81 insertions(+), 4 deletions(-) create mode 100644 kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/AirplaneModeSettingsScreen.kt diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/Network.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/Network.kt index b7ff445eb..84f347d79 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/Network.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/Network.kt @@ -22,12 +22,17 @@ interface Network { fun disable() /** - * Toggles only mobile data. Note: it works only if flight mode is off. + * Toggles only mobile data. Note: it works only if airplane mode is off. */ fun toggleMobileData(enable: Boolean) /** - * Toggles only wi-fi. Note: it works only if flight mode is off. + * Toggles only wi-fi. Note: it works only if airplane mode is off. */ fun toggleWiFi(enable: Boolean) + + /** + * Toggles airplane mode + */ + fun toggleAirplaneMode(enable: Boolean) } diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt index dd2186fcb..b822cbdad 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt @@ -10,6 +10,7 @@ import com.kaspersky.components.kautomator.system.UiSystem import com.kaspersky.kaspresso.device.server.AdbServer import com.kaspersky.kaspresso.flakysafety.algorithm.FlakySafetyAlgorithm import com.kaspersky.kaspresso.internal.exceptions.AdbServerException +import com.kaspersky.kaspresso.internal.systemscreen.AirplaneModeSettingsScreen import com.kaspersky.kaspresso.internal.systemscreen.DataUsageSettingsScreen import com.kaspersky.kaspresso.internal.systemscreen.NotificationsFullScreen import com.kaspersky.kaspresso.internal.systemscreen.NotificationsMobileDataScreen @@ -40,6 +41,11 @@ class NetworkImpl( private const val WIFI_STATE_CHECK_CMD = "settings get global wifi_on" private const val WIFI_STATE_CHECK_RESULT_ENABLED = "1" private const val WIFI_STATE_CHECK_RESULT_DISABLED = "0" + private const val AIRPLANE_MODE_CHANGE_CMD = "cmd connectivity airplane-mode" + private const val AIRPLANE_MODE_CHANGE_ROOT_CMD = "su 0 cmd connectivity airplane-mode" + private const val AIRPLANE_MODE_STATE_CHECK_CMD = "settings get global airplane_mode_on" + private const val AIRPLANE_MODE_STATE_CHECK_RESULT_ENABLED = "1" + private const val AIRPLANE_MODE_STATE_CHECK_RESULT_DISABLED = "0" private val ADB_RESULT_REGEX = Regex("exitCode=(\\d+), message=(.+)") } @@ -83,8 +89,8 @@ class NetworkImpl( !toggleMobileDataUsingAdbServer(enable, NETWORK_STATE_CHANGE_CMD) ) { toggleMobileDataUsingAndroidSettings(enable) - logger.i("Mobile data ${if (enable) "en" else "dis"}abled") } + logger.i("Mobile data ${if (enable) "en" else "dis"}abled") } private fun toggleMobileDataUsingAdbServer(enable: Boolean, changeCommand: String): Boolean = @@ -157,8 +163,8 @@ class NetworkImpl( !changeWiFiStateUsingAdbServer(enable, WIFI_STATE_CHANGE_CMD) ) { changeWifiStateUsingAndroidSettings(enable) - logger.i("Wi-fi ${if (enable) "en" else "dis"}abled") } + logger.i("Wi-fi ${if (enable) "en" else "dis"}abled") } /** @@ -221,6 +227,39 @@ class NetworkImpl( } } + override fun toggleAirplaneMode(enable: Boolean) { + if (!toggleAirplaneModeUsingAdb(enable, AIRPLANE_MODE_CHANGE_CMD) && + !toggleAirplaneModeUsingAdb(enable, AIRPLANE_MODE_CHANGE_ROOT_CMD) + ) { + toggleAirplaneModeAndroidSettings(enable) + } + logger.i("Airplane mode ${if (enable) "en" else "dis"}abled") + } + + private fun toggleAirplaneModeUsingAdb(isEnabled: Boolean, changeCommand: String) = + try { + val (state, expectedResult) = when (isEnabled) { + true -> CMD_STATE_ENABLE to AIRPLANE_MODE_STATE_CHECK_RESULT_ENABLED + false -> CMD_STATE_DISABLE to AIRPLANE_MODE_STATE_CHECK_RESULT_DISABLED + } + adbServer.performShell("$changeCommand $state") + flakySafetyAlgorithm.invokeFlakySafely(flakySafetyParams) { + val result = adbServer.performShell(AIRPLANE_MODE_STATE_CHECK_CMD) + if (parseAdbResponse(result)?.trim() == expectedResult) true else + throw AdbServerException("Failed to change airplane mode state using ABD") + } + } catch (e: AdbServerException) { + false + } + + private fun toggleAirplaneModeAndroidSettings(isEnabled: Boolean) { + AirplaneModeSettingsScreen { + open(targetContext) + airplaneModeSwitch.setChecked(isEnabled) + close(targetContext) + } + } + private fun parseAdbResponse(response: List): String? { val result = response.firstOrNull()?.lineSequence()?.first() ?: return null val match = ADB_RESULT_REGEX.find(result) ?: return null diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/AirplaneModeSettingsScreen.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/AirplaneModeSettingsScreen.kt new file mode 100644 index 000000000..310fdc364 --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/AirplaneModeSettingsScreen.kt @@ -0,0 +1,33 @@ +package com.kaspersky.kaspresso.internal.systemscreen + +import android.content.Context +import android.content.Intent +import android.provider.Settings +import android.widget.Switch +import com.kaspersky.components.kautomator.component.switch.UiSwitch +import com.kaspersky.components.kautomator.screen.UiScreen + +object AirplaneModeSettingsScreen : UiScreen() { + + private const val TIMEOUT = 5_000L + + override val packageName: String = "com.android.settings" + val airplaneModeSwitch = UiSwitch { + withClassName(Switch::class.java) + } + + fun open(context: Context) { + context.startActivity( + Intent(Settings.ACTION_AIRPLANE_MODE_SETTINGS).apply { + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + } + ) + waitForWindowUpdate(WiFiSettingsScreen.packageName, TIMEOUT) + } + + fun close(context: Context) { + pressBack() + waitForWindowUpdate(context.packageName, TIMEOUT) + } + +} From 377d2f5b31b93932cb425888e121f4f698937034 Mon Sep 17 00:00:00 2001 From: Ovsyannikov_M Date: Sun, 2 Nov 2025 15:38:53 +0300 Subject: [PATCH 2/6] ISSUE-712: add test --- .../kaspressample/device_tests/DeviceNetworkSampleTest.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt index eebe2a9a9..11fb61a6a 100644 --- a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt +++ b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt @@ -69,6 +69,13 @@ class DeviceNetworkSampleTest : TestCase() { device.network.toggleMobileData(true) assertTrueSafely { isDataConnected() } } + + step("Toggle Airplane mode") { + device.network.toggleAirplaneMode(true) + assertFalseSafely { isDataConnected() } + device.network.toggleAirplaneMode(false) + assertTrueSafely { isDataConnected() } + } } } From c4102c293fb269d84091578e9011e5dac70dd827 Mon Sep 17 00:00:00 2001 From: Ovsyannikov_M Date: Sun, 2 Nov 2025 16:02:45 +0300 Subject: [PATCH 3/6] ISSUE-712: fix detekt --- .../internal/systemscreen/AirplaneModeSettingsScreen.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/AirplaneModeSettingsScreen.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/AirplaneModeSettingsScreen.kt index 310fdc364..def1b8cc9 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/AirplaneModeSettingsScreen.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/AirplaneModeSettingsScreen.kt @@ -29,5 +29,4 @@ object AirplaneModeSettingsScreen : UiScreen() { pressBack() waitForWindowUpdate(context.packageName, TIMEOUT) } - } From 2f613028550f9c4938fbf91969e05bda1fcb1c24 Mon Sep 17 00:00:00 2001 From: Ovsyannikov_M Date: Sun, 2 Nov 2025 16:10:37 +0300 Subject: [PATCH 4/6] ISSUE-712: fix detekt --- .../com/kaspersky/kaspressample/visual/VisualTestSample.kt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/visual/VisualTestSample.kt b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/visual/VisualTestSample.kt index ad897db6d..2b1c78dce 100644 --- a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/visual/VisualTestSample.kt +++ b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/visual/VisualTestSample.kt @@ -8,7 +8,6 @@ import com.kaspersky.kaspressample.MainActivity import com.kaspersky.kaspressample.screen.MainScreen import com.kaspersky.kaspresso.testcases.api.testcase.VisualTestCase import org.junit.Assume -import org.junit.Ignore import org.junit.Rule import org.junit.Test @@ -24,7 +23,7 @@ class VisualTestSample : VisualTestCase() { val activityRule = activityScenarioRule() @Test - fun test() = runScreenshotTest{ + fun test() = runScreenshotTest { Assume.assumeTrue( "Granting READ_MEDIA_IMAGES fails on the lower APIs", Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU From b2231d8ba39d5fac102a45bdc0942a439408b8b6 Mon Sep 17 00:00:00 2001 From: Nikita Evdokimov Date: Fri, 10 Apr 2026 16:09:15 +0300 Subject: [PATCH 5/6] ISSUE-712: Fix for the older APIs --- .../kaspresso/device/network/Network.kt | 23 ++++++++++++- .../kaspresso/device/network/NetworkImpl.kt | 34 ++++++++++++++----- .../device_tests/DeviceNetworkSampleTest.kt | 10 +++--- 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/Network.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/Network.kt index d5b45a1ec..c580ff1b0 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/Network.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/Network.kt @@ -51,7 +51,28 @@ interface Network { fun toggleWiFi(enable: Boolean) /** - * Toggles airplane mode + * Toggles airplane mode on or off. + * + * The implementation strategy depends on the Android API level: + * + * **API <= 23 (up to Android 6.0 Marshmallow):** + * Uses `settings put global airplane_mode_on <0|1>` to update the global setting, + * then fires an `android.intent.action.AIRPLANE_MODE` broadcast to notify the system. + * This is the only reliable method on older devices, where `cmd connectivity` does not exist. + * + * **API >= 25 (Android 7.1 Nougat and above):** + * Uses `adb shell cmd connectivity airplane-mode `, which is the modern + * and preferred way to toggle airplane mode programmatically without requiring root. + * If this command fails (e.g. ADB server is unavailable), falls back to toggling the + * switch via the Android Settings UI. + * + * **Known edge case on API 24 (Android 7.0 Nougat):** + * API 24 is in an unfortunate middle ground: sending the `AIRPLANE_MODE` broadcast was + * already restricted for third-party apps in this version, but `cmd connectivity airplane-mode` + * had not been introduced yet. As a result, the ADB command path may fail silently on API 24, + * and the implementation will fall back to toggling the setting via the Android Settings UI. + * + * @param enable `true` to enable airplane mode, `false` to disable it. */ fun toggleAirplaneMode(enable: Boolean) } diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt index b3b583131..4bd59ca5b 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt @@ -50,16 +50,19 @@ class NetworkImpl( companion object { private const val CMD_STATE_ENABLE = "enable" private const val CMD_STATE_DISABLE = "disable" + private const val NETWORK_STATE_CHANGE_CMD = "svc data" private const val NETWORK_STATE_CHANGE_ROOT_CMD = "su 0 svc data" private const val NETWORK_STATE_CHECK_CMD = "settings get global mobile_data" private const val NETWORK_STATE_CHECK_RESULT_ENABLED = "1" private const val NETWORK_STATE_CHECK_RESULT_DISABLED = "0" + private const val WIFI_STATE_CHANGE_CMD = "svc wifi" private const val WIFI_STATE_CHANGE_ROOT_CMD = "su 0 svc wifi" private const val WIFI_STATE_CHECK_CMD = "settings get global wifi_on" private const val WIFI_STATE_CHECK_RESULT_ENABLED = "1" private const val WIFI_STATE_CHECK_RESULT_DISABLED = "0" + private const val AIRPLANE_MODE_CHANGE_CMD = "cmd connectivity airplane-mode" private const val AIRPLANE_MODE_CHANGE_ROOT_CMD = "su 0 cmd connectivity airplane-mode" private const val AIRPLANE_MODE_STATE_CHECK_CMD = "settings get global airplane_mode_on" @@ -247,16 +250,24 @@ class NetworkImpl( } override fun toggleAirplaneMode(enable: Boolean) { - if (!toggleAirplaneModeUsingAdb(enable, AIRPLANE_MODE_CHANGE_CMD) && - !toggleAirplaneModeUsingAdb(enable, AIRPLANE_MODE_CHANGE_ROOT_CMD) - ) { - toggleAirplaneModeAndroidSettings(enable) + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { + toggleAirplaneModeUsingAdbOnOdlerApi(enable) + } else { + if (!toggleAirplaneModeUsingAdb(enable, AIRPLANE_MODE_CHANGE_CMD) + && !toggleAirplaneModeUsingAdb(enable, AIRPLANE_MODE_CHANGE_ROOT_CMD)) { + toggleAirplaneModeAndroidSettings(enable) + } + } + + if (enable) { + logger.i("Airplane mode enabled") + } else { + logger.i("Airplane mode disabled") } - logger.i("Airplane mode ${if (enable) "en" else "dis"}abled") } - private fun toggleAirplaneModeUsingAdb(isEnabled: Boolean, changeCommand: String) = - try { + private fun toggleAirplaneModeUsingAdb(isEnabled: Boolean, changeCommand: String): Boolean { + return try { val (state, expectedResult) = when (isEnabled) { true -> CMD_STATE_ENABLE to AIRPLANE_MODE_STATE_CHECK_RESULT_ENABLED false -> CMD_STATE_DISABLE to AIRPLANE_MODE_STATE_CHECK_RESULT_DISABLED @@ -267,9 +278,16 @@ class NetworkImpl( if (parseAdbResponse(result)?.trim() == expectedResult) true else throw AdbServerException("Failed to change airplane mode state using ABD") } - } catch (e: AdbServerException) { + } catch (_: AdbServerException) { false } + } + + private fun toggleAirplaneModeUsingAdbOnOdlerApi(isEnabled: Boolean) { + val state = if (isEnabled) "1" else "0" + adbServer.performShell("settings", listOf("put", "global", "airplane_mode_on", state, + "&&", "am", "broadcast", "-a", "android.intent.action.AIRPLANE_MODE")) + } private fun toggleAirplaneModeAndroidSettings(isEnabled: Boolean) { AirplaneModeSettingsScreen { diff --git a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt index 3b3e85b0b..bb85339ce 100644 --- a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt +++ b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt @@ -132,12 +132,10 @@ class DeviceNetworkSampleTest : TestCase() { } private fun BaseTestContext.isDataConnectedInLowAndroid(): Boolean { - val telephonyManager: TelephonyManager? = - device.context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager? - if (telephonyManager?.simState != TelephonyManager.SIM_STATE_READY) { - return false - } - return Settings.Global.getInt(device.context.contentResolver, "mobile_data", 0) == 1 + val connectivityManager = device.context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager + val networkInfo = connectivityManager.activeNetworkInfo + + return networkInfo?.isConnected ?: false } private fun BaseTestContext.checkWifi(shouldBeEnabled: Boolean) { From 08d4b5787ade378d654fb8800a2b7af33689eb6a Mon Sep 17 00:00:00 2001 From: Nikita Evdokimov Date: Wed, 15 Apr 2026 18:16:54 +0300 Subject: [PATCH 6/6] ISSUE-712: Lint --- .../com/kaspersky/kaspresso/device/network/NetworkImpl.kt | 4 ++-- .../kaspressample/device_tests/DeviceNetworkSampleTest.kt | 2 -- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt index 4bd59ca5b..64589113a 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/network/NetworkImpl.kt @@ -253,8 +253,8 @@ class NetworkImpl( if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.M) { toggleAirplaneModeUsingAdbOnOdlerApi(enable) } else { - if (!toggleAirplaneModeUsingAdb(enable, AIRPLANE_MODE_CHANGE_CMD) - && !toggleAirplaneModeUsingAdb(enable, AIRPLANE_MODE_CHANGE_ROOT_CMD)) { + if (!toggleAirplaneModeUsingAdb(enable, AIRPLANE_MODE_CHANGE_CMD) && + !toggleAirplaneModeUsingAdb(enable, AIRPLANE_MODE_CHANGE_ROOT_CMD)) { toggleAirplaneModeAndroidSettings(enable) } } diff --git a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt index bb85339ce..66c7e50c5 100644 --- a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt +++ b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceNetworkSampleTest.kt @@ -9,8 +9,6 @@ import android.net.NetworkCapabilities import android.net.NetworkRequest import android.net.wifi.WifiManager import android.os.Build -import android.provider.Settings -import android.telephony.TelephonyManager import androidx.test.ext.junit.rules.activityScenarioRule import androidx.test.rule.GrantPermissionRule import com.kaspersky.kaspressample.device.DeviceSampleActivity