Skip to content

Commit

Permalink
TECH: added bluetooth's switcher
Browse files Browse the repository at this point in the history
  • Loading branch information
ersanin committed Nov 19, 2024
1 parent 16258c6 commit 1a7310f
Show file tree
Hide file tree
Showing 7 changed files with 247 additions and 0 deletions.
10 changes: 10 additions & 0 deletions kaspresso/src/main/kotlin/com/kaspersky/kaspresso/device/Device.kt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
@@ -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()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
package com.kaspersky.kaspresso.device.bluetooth

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.internal.systemscreen.NotificationsShortScreen

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)
logger.i("Enable bluetooth")
}

override fun disable() {
toggleBluetooth(enable = false)
logger.i("Disable bluetooth")
}

/**
* 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 (!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)
}

NotificationsShortScreen {
mainNotification.click()
}
NotificationsFullScreen {
bluetoothSwitch.setChecked(enable)
}
UiSystem {
drag(width / 2, height, width / 2, 0, 50)
}
}

private fun parseAdbResponse(response: List<String>): String? {
val result = response.firstOrNull()?.lineSequence()?.first() ?: return null
val match = ADB_RESULT_REGEX.find(result) ?: return null
val (_, message) = match.destructured
return message
}
}
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -11,4 +12,8 @@ object NotificationsFullScreen : UiScreen<NotificationsFullScreen>() {
val mobileDataSwitch: UiView = UiView {
withContentDescription(Pattern.compile(".*Mobile Phone.*"))
}

val bluetoothSwitch: UiSwitch = UiSwitch {
withContentDescription(Pattern.compile(".*Bluetooth.*"))
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
*/
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -953,6 +965,7 @@ data class Kaspresso(
device = Device(
apps = apps,
activities = activities,
bluetooth = bluetooth,
files = files,
network = network,
phone = phone,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.kaspersky.kaspressample.device_tests

import android.Manifest
import android.bluetooth.BluetoothManager
import android.content.ActivityNotFoundException
import android.content.Context
import android.os.Build
import androidx.test.ext.junit.rules.activityScenarioRule
import androidx.test.rule.GrantPermissionRule
import com.kaspersky.kaspressample.device.DeviceSampleActivity
import com.kaspersky.kaspressample.utils.SafeAssert.assertFalseSafely
import com.kaspersky.kaspressample.utils.SafeAssert.assertTrueSafely
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 runtimePermissionRule: GrantPermissionRule = GrantPermissionRule.grant(
Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE
)

@get:Rule
val activityRule = activityScenarioRule<DeviceSampleActivity>()

private val currentOsVersion = Build.VERSION.SDK_INT

@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) {
try {
if (shouldEnable) {
device.bluetooth.enable()
} else {
device.bluetooth.disable()
}
} catch (ex: ActivityNotFoundException) { // There's no Bluetooth activity on AVD with API < 30
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) return
throw ex
}
}

private fun BaseTestContext.checkBluetooth(shouldBeEnabled: Boolean) {
try {
if (shouldBeEnabled) assertTrueSafely { isBluetoothEnabled() } else assertFalseSafely { isBluetoothEnabled() }
} catch (assertionError: AssertionError) {
// There is no mind to check bluetooth in Android emulators before Android 11 because
// these simulators don't have a simulated bluetooth access point
// that's why we just skip the bluetooth check on Android below 11
if (currentOsVersion < Build.VERSION_CODES.R) return
else throw assertionError
}
}

private fun BaseTestContext.isBluetoothEnabled(): Boolean =
(device.targetContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter?.isEnabled
?: throw IllegalStateException("BluetoothManager is unavailable")
}
1 change: 1 addition & 0 deletions samples/kaspresso-sample/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.CHANGE_CONFIGURATION" tools:ignore="ProtectedPermissions"/>
<uses-permission android:name="android.permission.BLUETOOTH"/>

<application
android:name="androidx.multidex.MultiDexApplication"
Expand Down

0 comments on commit 1a7310f

Please sign in to comment.