From 32eb9e1b7fec8b8f049097a31e2113430ed4c163 Mon Sep 17 00:00:00 2001 From: ersanin Date: Thu, 21 Nov 2024 04:11:05 +0300 Subject: [PATCH] TECH: added bluetooth's switcher --- .../com/kaspersky/kaspresso/device/Device.kt | 10 ++ .../kaspresso/device/bluetooth/Bluetooth.kt | 23 ++++ .../device/bluetooth/BluetoothImpl.kt | 120 ++++++++++++++++++ .../systemscreen/NotificationsFullScreen.kt | 5 + .../kaspresso/kaspresso/Kaspresso.kt | 13 ++ .../device_tests/DeviceBluetoothSampleTest.kt | 66 ++++++++++ .../src/main/AndroidManifest.xml | 1 + 7 files changed, 238 insertions(+) create mode 100644 kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/bluetooth/Bluetooth.kt create mode 100644 kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/bluetooth/BluetoothImpl.kt create mode 100644 samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceBluetoothSampleTest.kt diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/Device.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/Device.kt index 0ac59362b..ba6c5f85e 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/Device.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/Device.kt @@ -6,6 +6,7 @@ import androidx.test.uiautomator.UiDevice import com.kaspersky.kaspresso.device.accessibility.Accessibility import com.kaspersky.kaspresso.device.activities.Activities import com.kaspersky.kaspresso.device.apps.Apps +import com.kaspersky.kaspresso.device.bluetooth.Bluetooth import com.kaspersky.kaspresso.device.exploit.Exploit import com.kaspersky.kaspresso.device.files.Files import com.kaspersky.kaspresso.device.keyboard.Keyboard @@ -38,6 +39,15 @@ data class Device( */ val activities: Activities, + /** + * Holds the reference to the implementation of [Bluetooth] interface. + * + * Required: Started AdbServer + * 1. Download a file "kaspresso/artifacts/adbserver-desktop.jar" + * 2. Start AdbServer => input in cmd "java jar path_to_file/adbserver-desktop.jar" + */ + val bluetooth: Bluetooth, + /** * Holds the reference to the implementation of [Files] interface. * diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/bluetooth/Bluetooth.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/bluetooth/Bluetooth.kt new file mode 100644 index 000000000..08127e73e --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/bluetooth/Bluetooth.kt @@ -0,0 +1,23 @@ +package com.kaspersky.kaspresso.device.bluetooth + +/** + * The interface to work with bluetooth settings. + * + * Required: Started AdbServer + * 1. Download a file "kaspresso/artifacts/adbserver-desktop.jar" + * 2. Start AdbServer => input in cmd "java jar path_to_file/adbserver-desktop.jar" + * Methods demanding to use AdbServer in the default implementation of this interface are marked. + * But nobody can't deprecate you to write implementation that doesn't require AdbServer. + */ +interface Bluetooth { + + /** + * Enables Bluetooth on the device using adb. + */ + fun enable() + + /** + * Disables Bluetooth on the device using adb. + */ + fun disable() +} diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/bluetooth/BluetoothImpl.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/bluetooth/BluetoothImpl.kt new file mode 100644 index 000000000..e792e43a1 --- /dev/null +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/bluetooth/BluetoothImpl.kt @@ -0,0 +1,120 @@ +package com.kaspersky.kaspresso.device.bluetooth + +import android.bluetooth.BluetoothManager +import android.content.Context +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.NotificationsFullScreen + +import com.kaspersky.kaspresso.logger.UiTestLogger +import com.kaspersky.kaspresso.params.FlakySafetyParams + +/** + * The implementation of the [Bluetooth] interface. + */ +class BluetoothImpl( + private val logger: UiTestLogger, + private val targetContext: Context, + private val adbServer: AdbServer +) : Bluetooth { + + companion object { + private const val CMD_STATE_ENABLE = "enable" + private const val CMD_STATE_DISABLE = "disable" + private const val BLUETOOTH_STATE_CHANGE_CMD = "svc bluetooth" + private const val BLUETOOTH_STATE_CHANGE_ROOT_CMD = "su 0 svc bluetooth" + private const val BLUETOOTH_STATE_CHECK_CMD = "settings get global bluetooth_on" + private const val BLUETOOTH_STATE_CHECK_RESULT_ENABLED = "1" + private const val BLUETOOTH_STATE_CHECK_RESULT_DISABLED = "0" + private val ADB_RESULT_REGEX = Regex("exitCode=(\\d+), message=(.+)") + } + + private val flakySafetyAlgorithm = FlakySafetyAlgorithm(logger) + private val flakySafetyParams: FlakySafetyParams + get() = FlakySafetyParams( + timeoutMs = 1000, + intervalMs = 100, + allowedExceptions = setOf(AdbServerException::class.java) + ) + + override fun enable() { + toggleBluetooth(enable = true) + } + + override fun disable() { + toggleBluetooth(enable = false) + } + + /** + * Toggles Bluetooth state + * Tries, first and foremost, to send ADB command. If this attempt fails, + * opens Android Settings screen and tries to switch Bluetooth setting thumb. + */ + private fun toggleBluetooth(enable: Boolean) { + if (isBluetoothNotSupported()) { + logger.i("Bluetooth is not supported") + return + } + logger.i("${if (enable) "En" else "Dis"}able bluetooth") + if (!changeBluetoothStateUsingAdbServer(enable, BLUETOOTH_STATE_CHANGE_ROOT_CMD) && + !changeBluetoothStateUsingAdbServer(enable, BLUETOOTH_STATE_CHANGE_CMD) + ) { + toggleBluetoothUsingAndroidSettings(enable) + logger.i("Bluetooth ${if (enable) "en" else "dis"}abled") + } + } + + /** + * Tries to change Bluetooth state using AdbServer if it is available + * @return true if Bluetooth state changed or false otherwise + */ + private fun changeBluetoothStateUsingAdbServer(isEnabled: Boolean, changeCommand: String): Boolean = + try { + val (state, expectedResult) = when (isEnabled) { + true -> CMD_STATE_ENABLE to BLUETOOTH_STATE_CHECK_RESULT_ENABLED + false -> CMD_STATE_DISABLE to BLUETOOTH_STATE_CHECK_RESULT_DISABLED + } + adbServer.performShell("$changeCommand $state") + flakySafetyAlgorithm.invokeFlakySafely(flakySafetyParams) { + val result = adbServer.performShell(BLUETOOTH_STATE_CHECK_CMD) + if (parseAdbResponse(result)?.trim() == expectedResult) true else + throw AdbServerException("Failed to change Bluetooth state using ABD") + } + } catch (e: AdbServerException) { + false + } + + @Suppress("MagicNumber") + private fun toggleBluetoothUsingAndroidSettings(enable: Boolean) { + val height = targetContext.resources.displayMetrics.heightPixels + val width = targetContext.resources.displayMetrics.widthPixels + + UiSystem { + drag(width / 2, 0, width / 2, (height * 0.67).toInt(), 50) + } + UiSystem { + drag(width / 2, 0, width / 2, (height * 0.67).toInt(), 50) + } + NotificationsFullScreen { + bluetoothSwitch.setChecked(enable) + } + UiSystem { + drag(width / 2, height, width / 2, 0, 50) + } + UiSystem { + drag(width / 2, height, width / 2, 0, 50) + } + } + + private fun isBluetoothNotSupported(): Boolean = + (this.targetContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter == null + + private fun parseAdbResponse(response: List): String? { + val result = response.firstOrNull()?.lineSequence()?.first() ?: return null + val match = ADB_RESULT_REGEX.find(result) ?: return null + val (_, message) = match.destructured + return message + } +} diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/NotificationsFullScreen.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/NotificationsFullScreen.kt index bf1e3dee1..4fdd36fd8 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/NotificationsFullScreen.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/NotificationsFullScreen.kt @@ -1,6 +1,7 @@ package com.kaspersky.kaspresso.internal.systemscreen import com.kaspersky.components.kautomator.component.common.views.UiView +import com.kaspersky.components.kautomator.component.switch.UiSwitch import com.kaspersky.components.kautomator.screen.UiScreen import java.util.regex.Pattern @@ -11,4 +12,8 @@ object NotificationsFullScreen : UiScreen() { val mobileDataSwitch: UiView = UiView { withContentDescription(Pattern.compile(".*Mobile Phone.*")) } + + val bluetoothSwitch: UiSwitch = UiSwitch { + withContentDescription(Pattern.compile(".*Bluetooth.*")) + } } diff --git a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt index 9a17041db..d4d388cc7 100644 --- a/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt +++ b/kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt @@ -16,6 +16,8 @@ import com.kaspersky.kaspresso.device.activities.Activities import com.kaspersky.kaspresso.device.activities.ActivitiesImpl import com.kaspersky.kaspresso.device.apps.Apps import com.kaspersky.kaspresso.device.apps.AppsImpl +import com.kaspersky.kaspresso.device.bluetooth.Bluetooth +import com.kaspersky.kaspresso.device.bluetooth.BluetoothImpl import com.kaspersky.kaspresso.device.exploit.Exploit import com.kaspersky.kaspresso.device.exploit.ExploitImpl import com.kaspersky.kaspresso.device.files.Files @@ -361,6 +363,11 @@ data class Kaspresso( */ lateinit var activities: Activities + /** + * Holds an implementation of [Bluetooth] interface. If it was not specified, the default implementation is used. + */ + lateinit var bluetooth: Bluetooth + /** * Holds an implementation of [Files] interface. If it was not specified, the default implementation is used. */ @@ -715,6 +722,11 @@ data class Kaspresso( adbServer ) if (!::activities.isInitialized) activities = ActivitiesImpl(libLogger, instrumentation) + if (!::bluetooth.isInitialized) bluetooth = BluetoothImpl( + libLogger, + instrumentation.targetContext, + adbServer + ) if (!::files.isInitialized) files = FilesImpl(libLogger, adbServer) if (!::network.isInitialized) network = NetworkImpl( libLogger, @@ -953,6 +965,7 @@ data class Kaspresso( device = Device( apps = apps, activities = activities, + bluetooth = bluetooth, files = files, network = network, phone = phone, diff --git a/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceBluetoothSampleTest.kt b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceBluetoothSampleTest.kt new file mode 100644 index 000000000..acc17a391 --- /dev/null +++ b/samples/kaspresso-sample/src/androidTest/kotlin/com/kaspersky/kaspressample/device_tests/DeviceBluetoothSampleTest.kt @@ -0,0 +1,66 @@ +package com.kaspersky.kaspressample.device_tests + +import android.bluetooth.BluetoothAdapter +import android.bluetooth.BluetoothManager +import android.content.Context +import androidx.test.ext.junit.rules.activityScenarioRule +import com.kaspersky.kaspressample.device.DeviceSampleActivity +import com.kaspersky.kaspressample.utils.SafeAssert.assertFalseSafely +import com.kaspersky.kaspressample.utils.SafeAssert.assertTrueSafely +import com.kaspersky.kaspresso.device.Device +import com.kaspersky.kaspresso.testcases.api.testcase.TestCase +import com.kaspersky.kaspresso.testcases.core.testcontext.BaseTestContext +import org.junit.Rule +import org.junit.Test + +class DeviceBluetoothSampleTest : TestCase() { + + @get:Rule + val activityRule = activityScenarioRule() + + @Test + fun bluetoothSampleTest() { + before { + tryToggleBluetooth(shouldEnable = true) + }.after { + tryToggleBluetooth(shouldEnable = true) + }.run { + + step("Disable bluetooth") { + tryToggleBluetooth(shouldEnable = false) + checkBluetooth(shouldBeEnabled = false) + } + + step("Enable bluetooth") { + tryToggleBluetooth(shouldEnable = true) + checkBluetooth(shouldBeEnabled = true) + } + } + } + + private fun tryToggleBluetooth(shouldEnable: Boolean) { + if (shouldEnable) { + device.bluetooth.enable() + } else { + device.bluetooth.disable() + } + } + + private fun BaseTestContext.checkBluetooth(shouldBeEnabled: Boolean) { + try { + if (shouldBeEnabled) assertTrueSafely { isBluetoothEnabled() } else assertFalseSafely { isBluetoothEnabled() } + } catch (assertionError: AssertionError) { + if (isBluetoothNotSupported()) return + else throw assertionError + } + } + + private fun isBluetoothNotSupported(): Boolean = + device.getBluetoothAdapter() == null + + private fun isBluetoothEnabled(): Boolean = + device.getBluetoothAdapter()?.isEnabled ?: false + + private fun Device.getBluetoothAdapter(): BluetoothAdapter? = + (this.targetContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter +} diff --git a/samples/kaspresso-sample/src/main/AndroidManifest.xml b/samples/kaspresso-sample/src/main/AndroidManifest.xml index f8ed10537..e56436455 100644 --- a/samples/kaspresso-sample/src/main/AndroidManifest.xml +++ b/samples/kaspresso-sample/src/main/AndroidManifest.xml @@ -18,6 +18,7 @@ +