Skip to content

Commit 32eb9e1

Browse files
committed
TECH: added bluetooth's switcher
1 parent 557e6cf commit 32eb9e1

File tree

7 files changed

+238
-0
lines changed

7 files changed

+238
-0
lines changed

kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/Device.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import androidx.test.uiautomator.UiDevice
66
import com.kaspersky.kaspresso.device.accessibility.Accessibility
77
import com.kaspersky.kaspresso.device.activities.Activities
88
import com.kaspersky.kaspresso.device.apps.Apps
9+
import com.kaspersky.kaspresso.device.bluetooth.Bluetooth
910
import com.kaspersky.kaspresso.device.exploit.Exploit
1011
import com.kaspersky.kaspresso.device.files.Files
1112
import com.kaspersky.kaspresso.device.keyboard.Keyboard
@@ -38,6 +39,15 @@ data class Device(
3839
*/
3940
val activities: Activities,
4041

42+
/**
43+
* Holds the reference to the implementation of [Bluetooth] interface.
44+
*
45+
* Required: Started AdbServer
46+
* 1. Download a file "kaspresso/artifacts/adbserver-desktop.jar"
47+
* 2. Start AdbServer => input in cmd "java jar path_to_file/adbserver-desktop.jar"
48+
*/
49+
val bluetooth: Bluetooth,
50+
4151
/**
4252
* Holds the reference to the implementation of [Files] interface.
4353
*
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.kaspersky.kaspresso.device.bluetooth
2+
3+
/**
4+
* The interface to work with bluetooth settings.
5+
*
6+
* Required: Started AdbServer
7+
* 1. Download a file "kaspresso/artifacts/adbserver-desktop.jar"
8+
* 2. Start AdbServer => input in cmd "java jar path_to_file/adbserver-desktop.jar"
9+
* Methods demanding to use AdbServer in the default implementation of this interface are marked.
10+
* But nobody can't deprecate you to write implementation that doesn't require AdbServer.
11+
*/
12+
interface Bluetooth {
13+
14+
/**
15+
* Enables Bluetooth on the device using adb.
16+
*/
17+
fun enable()
18+
19+
/**
20+
* Disables Bluetooth on the device using adb.
21+
*/
22+
fun disable()
23+
}
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package com.kaspersky.kaspresso.device.bluetooth
2+
3+
import android.bluetooth.BluetoothManager
4+
import android.content.Context
5+
import com.kaspersky.components.kautomator.system.UiSystem
6+
import com.kaspersky.kaspresso.device.server.AdbServer
7+
import com.kaspersky.kaspresso.flakysafety.algorithm.FlakySafetyAlgorithm
8+
import com.kaspersky.kaspresso.internal.exceptions.AdbServerException
9+
import com.kaspersky.kaspresso.internal.systemscreen.NotificationsFullScreen
10+
11+
import com.kaspersky.kaspresso.logger.UiTestLogger
12+
import com.kaspersky.kaspresso.params.FlakySafetyParams
13+
14+
/**
15+
* The implementation of the [Bluetooth] interface.
16+
*/
17+
class BluetoothImpl(
18+
private val logger: UiTestLogger,
19+
private val targetContext: Context,
20+
private val adbServer: AdbServer
21+
) : Bluetooth {
22+
23+
companion object {
24+
private const val CMD_STATE_ENABLE = "enable"
25+
private const val CMD_STATE_DISABLE = "disable"
26+
private const val BLUETOOTH_STATE_CHANGE_CMD = "svc bluetooth"
27+
private const val BLUETOOTH_STATE_CHANGE_ROOT_CMD = "su 0 svc bluetooth"
28+
private const val BLUETOOTH_STATE_CHECK_CMD = "settings get global bluetooth_on"
29+
private const val BLUETOOTH_STATE_CHECK_RESULT_ENABLED = "1"
30+
private const val BLUETOOTH_STATE_CHECK_RESULT_DISABLED = "0"
31+
private val ADB_RESULT_REGEX = Regex("exitCode=(\\d+), message=(.+)")
32+
}
33+
34+
private val flakySafetyAlgorithm = FlakySafetyAlgorithm(logger)
35+
private val flakySafetyParams: FlakySafetyParams
36+
get() = FlakySafetyParams(
37+
timeoutMs = 1000,
38+
intervalMs = 100,
39+
allowedExceptions = setOf(AdbServerException::class.java)
40+
)
41+
42+
override fun enable() {
43+
toggleBluetooth(enable = true)
44+
}
45+
46+
override fun disable() {
47+
toggleBluetooth(enable = false)
48+
}
49+
50+
/**
51+
* Toggles Bluetooth state
52+
* Tries, first and foremost, to send ADB command. If this attempt fails,
53+
* opens Android Settings screen and tries to switch Bluetooth setting thumb.
54+
*/
55+
private fun toggleBluetooth(enable: Boolean) {
56+
if (isBluetoothNotSupported()) {
57+
logger.i("Bluetooth is not supported")
58+
return
59+
}
60+
logger.i("${if (enable) "En" else "Dis"}able bluetooth")
61+
if (!changeBluetoothStateUsingAdbServer(enable, BLUETOOTH_STATE_CHANGE_ROOT_CMD) &&
62+
!changeBluetoothStateUsingAdbServer(enable, BLUETOOTH_STATE_CHANGE_CMD)
63+
) {
64+
toggleBluetoothUsingAndroidSettings(enable)
65+
logger.i("Bluetooth ${if (enable) "en" else "dis"}abled")
66+
}
67+
}
68+
69+
/**
70+
* Tries to change Bluetooth state using AdbServer if it is available
71+
* @return true if Bluetooth state changed or false otherwise
72+
*/
73+
private fun changeBluetoothStateUsingAdbServer(isEnabled: Boolean, changeCommand: String): Boolean =
74+
try {
75+
val (state, expectedResult) = when (isEnabled) {
76+
true -> CMD_STATE_ENABLE to BLUETOOTH_STATE_CHECK_RESULT_ENABLED
77+
false -> CMD_STATE_DISABLE to BLUETOOTH_STATE_CHECK_RESULT_DISABLED
78+
}
79+
adbServer.performShell("$changeCommand $state")
80+
flakySafetyAlgorithm.invokeFlakySafely(flakySafetyParams) {
81+
val result = adbServer.performShell(BLUETOOTH_STATE_CHECK_CMD)
82+
if (parseAdbResponse(result)?.trim() == expectedResult) true else
83+
throw AdbServerException("Failed to change Bluetooth state using ABD")
84+
}
85+
} catch (e: AdbServerException) {
86+
false
87+
}
88+
89+
@Suppress("MagicNumber")
90+
private fun toggleBluetoothUsingAndroidSettings(enable: Boolean) {
91+
val height = targetContext.resources.displayMetrics.heightPixels
92+
val width = targetContext.resources.displayMetrics.widthPixels
93+
94+
UiSystem {
95+
drag(width / 2, 0, width / 2, (height * 0.67).toInt(), 50)
96+
}
97+
UiSystem {
98+
drag(width / 2, 0, width / 2, (height * 0.67).toInt(), 50)
99+
}
100+
NotificationsFullScreen {
101+
bluetoothSwitch.setChecked(enable)
102+
}
103+
UiSystem {
104+
drag(width / 2, height, width / 2, 0, 50)
105+
}
106+
UiSystem {
107+
drag(width / 2, height, width / 2, 0, 50)
108+
}
109+
}
110+
111+
private fun isBluetoothNotSupported(): Boolean =
112+
(this.targetContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter == null
113+
114+
private fun parseAdbResponse(response: List<String>): String? {
115+
val result = response.firstOrNull()?.lineSequence()?.first() ?: return null
116+
val match = ADB_RESULT_REGEX.find(result) ?: return null
117+
val (_, message) = match.destructured
118+
return message
119+
}
120+
}

kaspresso/src/main/kotlin/com/kaspersky/kaspresso/internal/systemscreen/NotificationsFullScreen.kt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.kaspersky.kaspresso.internal.systemscreen
22

33
import com.kaspersky.components.kautomator.component.common.views.UiView
4+
import com.kaspersky.components.kautomator.component.switch.UiSwitch
45
import com.kaspersky.components.kautomator.screen.UiScreen
56
import java.util.regex.Pattern
67

@@ -11,4 +12,8 @@ object NotificationsFullScreen : UiScreen<NotificationsFullScreen>() {
1112
val mobileDataSwitch: UiView = UiView {
1213
withContentDescription(Pattern.compile(".*Mobile Phone.*"))
1314
}
15+
16+
val bluetoothSwitch: UiSwitch = UiSwitch {
17+
withContentDescription(Pattern.compile(".*Bluetooth.*"))
18+
}
1419
}

kaspresso/src/main/kotlin/com/kaspersky/kaspresso/kaspresso/Kaspresso.kt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ import com.kaspersky.kaspresso.device.activities.Activities
1616
import com.kaspersky.kaspresso.device.activities.ActivitiesImpl
1717
import com.kaspersky.kaspresso.device.apps.Apps
1818
import com.kaspersky.kaspresso.device.apps.AppsImpl
19+
import com.kaspersky.kaspresso.device.bluetooth.Bluetooth
20+
import com.kaspersky.kaspresso.device.bluetooth.BluetoothImpl
1921
import com.kaspersky.kaspresso.device.exploit.Exploit
2022
import com.kaspersky.kaspresso.device.exploit.ExploitImpl
2123
import com.kaspersky.kaspresso.device.files.Files
@@ -361,6 +363,11 @@ data class Kaspresso(
361363
*/
362364
lateinit var activities: Activities
363365

366+
/**
367+
* Holds an implementation of [Bluetooth] interface. If it was not specified, the default implementation is used.
368+
*/
369+
lateinit var bluetooth: Bluetooth
370+
364371
/**
365372
* Holds an implementation of [Files] interface. If it was not specified, the default implementation is used.
366373
*/
@@ -715,6 +722,11 @@ data class Kaspresso(
715722
adbServer
716723
)
717724
if (!::activities.isInitialized) activities = ActivitiesImpl(libLogger, instrumentation)
725+
if (!::bluetooth.isInitialized) bluetooth = BluetoothImpl(
726+
libLogger,
727+
instrumentation.targetContext,
728+
adbServer
729+
)
718730
if (!::files.isInitialized) files = FilesImpl(libLogger, adbServer)
719731
if (!::network.isInitialized) network = NetworkImpl(
720732
libLogger,
@@ -953,6 +965,7 @@ data class Kaspresso(
953965
device = Device(
954966
apps = apps,
955967
activities = activities,
968+
bluetooth = bluetooth,
956969
files = files,
957970
network = network,
958971
phone = phone,
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.kaspersky.kaspressample.device_tests
2+
3+
import android.bluetooth.BluetoothAdapter
4+
import android.bluetooth.BluetoothManager
5+
import android.content.Context
6+
import androidx.test.ext.junit.rules.activityScenarioRule
7+
import com.kaspersky.kaspressample.device.DeviceSampleActivity
8+
import com.kaspersky.kaspressample.utils.SafeAssert.assertFalseSafely
9+
import com.kaspersky.kaspressample.utils.SafeAssert.assertTrueSafely
10+
import com.kaspersky.kaspresso.device.Device
11+
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
12+
import com.kaspersky.kaspresso.testcases.core.testcontext.BaseTestContext
13+
import org.junit.Rule
14+
import org.junit.Test
15+
16+
class DeviceBluetoothSampleTest : TestCase() {
17+
18+
@get:Rule
19+
val activityRule = activityScenarioRule<DeviceSampleActivity>()
20+
21+
@Test
22+
fun bluetoothSampleTest() {
23+
before {
24+
tryToggleBluetooth(shouldEnable = true)
25+
}.after {
26+
tryToggleBluetooth(shouldEnable = true)
27+
}.run {
28+
29+
step("Disable bluetooth") {
30+
tryToggleBluetooth(shouldEnable = false)
31+
checkBluetooth(shouldBeEnabled = false)
32+
}
33+
34+
step("Enable bluetooth") {
35+
tryToggleBluetooth(shouldEnable = true)
36+
checkBluetooth(shouldBeEnabled = true)
37+
}
38+
}
39+
}
40+
41+
private fun tryToggleBluetooth(shouldEnable: Boolean) {
42+
if (shouldEnable) {
43+
device.bluetooth.enable()
44+
} else {
45+
device.bluetooth.disable()
46+
}
47+
}
48+
49+
private fun BaseTestContext.checkBluetooth(shouldBeEnabled: Boolean) {
50+
try {
51+
if (shouldBeEnabled) assertTrueSafely { isBluetoothEnabled() } else assertFalseSafely { isBluetoothEnabled() }
52+
} catch (assertionError: AssertionError) {
53+
if (isBluetoothNotSupported()) return
54+
else throw assertionError
55+
}
56+
}
57+
58+
private fun isBluetoothNotSupported(): Boolean =
59+
device.getBluetoothAdapter() == null
60+
61+
private fun isBluetoothEnabled(): Boolean =
62+
device.getBluetoothAdapter()?.isEnabled ?: false
63+
64+
private fun Device.getBluetoothAdapter(): BluetoothAdapter? =
65+
(this.targetContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter
66+
}

samples/kaspresso-sample/src/main/AndroidManifest.xml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@
1818
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
1919
<uses-permission android:name="android.permission.READ_SMS"/>
2020
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" tools:ignore="ProtectedPermissions"/>
21+
<uses-permission android:name="android.permission.BLUETOOTH"/>
2122

2223
<application
2324
android:name="androidx.multidex.MultiDexApplication"

0 commit comments

Comments
 (0)