Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,38 @@ 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 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 <enable|disable>`, 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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -49,16 +50,24 @@ 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"
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=(.+)")
}

Expand Down Expand Up @@ -102,8 +111,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 =
Expand Down Expand Up @@ -176,8 +185,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")
}

/**
Expand Down Expand Up @@ -240,6 +249,54 @@ class NetworkImpl(
}
}

override fun toggleAirplaneMode(enable: Boolean) {
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")
}
}

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
}
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 (_: 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 {
open(targetContext)
airplaneModeSwitch.setChecked(isEnabled)
close(targetContext)
}
}

private fun parseAdbResponse(response: List<String>): String? {
val result = response.firstOrNull()?.lineSequence()?.first() ?: return null
val match = ADB_RESULT_REGEX.find(result) ?: return null
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
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<AirplaneModeSettingsScreen>() {

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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -88,6 +86,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() }
}
}
}

Expand Down Expand Up @@ -125,12 +130,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) {
Expand Down
Loading