Skip to content

Commit 1a7310f

Browse files
committed
TECH: added bluetooth's switcher
1 parent 16258c6 commit 1a7310f

File tree

7 files changed

+247
-0
lines changed

7 files changed

+247
-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: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
package com.kaspersky.kaspresso.device.bluetooth
2+
3+
import android.content.Context
4+
import com.kaspersky.components.kautomator.system.UiSystem
5+
import com.kaspersky.kaspresso.device.server.AdbServer
6+
import com.kaspersky.kaspresso.flakysafety.algorithm.FlakySafetyAlgorithm
7+
import com.kaspersky.kaspresso.internal.exceptions.AdbServerException
8+
import com.kaspersky.kaspresso.internal.systemscreen.NotificationsFullScreen
9+
import com.kaspersky.kaspresso.internal.systemscreen.NotificationsShortScreen
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+
logger.i("Enable bluetooth")
45+
}
46+
47+
override fun disable() {
48+
toggleBluetooth(enable = false)
49+
logger.i("Disable bluetooth")
50+
}
51+
52+
/**
53+
* Toggles Bluetooth state
54+
* Tries, first and foremost, to send ADB command. If this attempt fails,
55+
* opens Android Settings screen and tries to switch Bluetooth setting thumb.
56+
*/
57+
private fun toggleBluetooth(enable: Boolean) {
58+
if (!changeBluetoothStateUsingAdbServer(enable, BLUETOOTH_STATE_CHANGE_ROOT_CMD) &&
59+
!changeBluetoothStateUsingAdbServer(enable, BLUETOOTH_STATE_CHANGE_CMD)
60+
) {
61+
toggleBluetoothUsingAndroidSettings(enable)
62+
logger.i("Bluetooth ${if (enable) "en" else "dis"}abled")
63+
}
64+
}
65+
66+
/**
67+
* Tries to change Bluetooth state using AdbServer if it is available
68+
* @return true if Bluetooth state changed or false otherwise
69+
*/
70+
private fun changeBluetoothStateUsingAdbServer(isEnabled: Boolean, changeCommand: String): Boolean =
71+
try {
72+
val (state, expectedResult) = when (isEnabled) {
73+
true -> CMD_STATE_ENABLE to BLUETOOTH_STATE_CHECK_RESULT_ENABLED
74+
false -> CMD_STATE_DISABLE to BLUETOOTH_STATE_CHECK_RESULT_DISABLED
75+
}
76+
adbServer.performShell("$changeCommand $state")
77+
flakySafetyAlgorithm.invokeFlakySafely(flakySafetyParams) {
78+
val result = adbServer.performShell(BLUETOOTH_STATE_CHECK_CMD)
79+
if (parseAdbResponse(result)?.trim() == expectedResult) true else
80+
throw AdbServerException("Failed to change Bluetooth state using ABD")
81+
}
82+
} catch (e: AdbServerException) {
83+
false
84+
}
85+
86+
@Suppress("MagicNumber")
87+
private fun toggleBluetoothUsingAndroidSettings(enable: Boolean) {
88+
val height = targetContext.resources.displayMetrics.heightPixels
89+
val width = targetContext.resources.displayMetrics.widthPixels
90+
91+
UiSystem {
92+
drag(width / 2, 0, width / 2, (height * 0.67).toInt(), 50)
93+
}
94+
95+
UiSystem {
96+
drag(width / 2, 0, width / 2, (height * 0.67).toInt(), 50)
97+
}
98+
99+
NotificationsShortScreen {
100+
mainNotification.click()
101+
}
102+
NotificationsFullScreen {
103+
bluetoothSwitch.setChecked(enable)
104+
}
105+
UiSystem {
106+
drag(width / 2, height, width / 2, 0, 50)
107+
}
108+
}
109+
110+
private fun parseAdbResponse(response: List<String>): String? {
111+
val result = response.firstOrNull()?.lineSequence()?.first() ?: return null
112+
val match = ADB_RESULT_REGEX.find(result) ?: return null
113+
val (_, message) = match.destructured
114+
return message
115+
}
116+
}

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: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package com.kaspersky.kaspressample.device_tests
2+
3+
import android.Manifest
4+
import android.bluetooth.BluetoothManager
5+
import android.content.ActivityNotFoundException
6+
import android.content.Context
7+
import android.os.Build
8+
import androidx.test.ext.junit.rules.activityScenarioRule
9+
import androidx.test.rule.GrantPermissionRule
10+
import com.kaspersky.kaspressample.device.DeviceSampleActivity
11+
import com.kaspersky.kaspressample.utils.SafeAssert.assertFalseSafely
12+
import com.kaspersky.kaspressample.utils.SafeAssert.assertTrueSafely
13+
import com.kaspersky.kaspresso.testcases.api.testcase.TestCase
14+
import com.kaspersky.kaspresso.testcases.core.testcontext.BaseTestContext
15+
import org.junit.Rule
16+
import org.junit.Test
17+
18+
class DeviceBluetoothSampleTest : TestCase() {
19+
20+
@get:Rule
21+
val runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
22+
Manifest.permission.WRITE_EXTERNAL_STORAGE,
23+
Manifest.permission.READ_EXTERNAL_STORAGE
24+
)
25+
26+
@get:Rule
27+
val activityRule = activityScenarioRule<DeviceSampleActivity>()
28+
29+
private val currentOsVersion = Build.VERSION.SDK_INT
30+
31+
@Test
32+
fun bluetoothSampleTest() {
33+
before {
34+
tryToggleBluetooth(shouldEnable = true)
35+
}.after {
36+
tryToggleBluetooth(shouldEnable = true)
37+
}.run {
38+
39+
step("Disable bluetooth") {
40+
tryToggleBluetooth(shouldEnable = false)
41+
checkBluetooth(shouldBeEnabled = false)
42+
}
43+
44+
step("Enable bluetooth") {
45+
tryToggleBluetooth(shouldEnable = true)
46+
checkBluetooth(shouldBeEnabled = true)
47+
}
48+
}
49+
}
50+
51+
private fun tryToggleBluetooth(shouldEnable: Boolean) {
52+
try {
53+
if (shouldEnable) {
54+
device.bluetooth.enable()
55+
} else {
56+
device.bluetooth.disable()
57+
}
58+
} catch (ex: ActivityNotFoundException) { // There's no Bluetooth activity on AVD with API < 30
59+
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) return
60+
throw ex
61+
}
62+
}
63+
64+
private fun BaseTestContext.checkBluetooth(shouldBeEnabled: Boolean) {
65+
try {
66+
if (shouldBeEnabled) assertTrueSafely { isBluetoothEnabled() } else assertFalseSafely { isBluetoothEnabled() }
67+
} catch (assertionError: AssertionError) {
68+
// There is no mind to check bluetooth in Android emulators before Android 11 because
69+
// these simulators don't have a simulated bluetooth access point
70+
// that's why we just skip the bluetooth check on Android below 11
71+
if (currentOsVersion < Build.VERSION_CODES.R) return
72+
else throw assertionError
73+
}
74+
}
75+
76+
private fun BaseTestContext.isBluetoothEnabled(): Boolean =
77+
(device.targetContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter?.isEnabled
78+
?: throw IllegalStateException("BluetoothManager is unavailable")
79+
}

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)