Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TECH: added bluetooth's switcher #684

Open
wants to merge 2 commits into
base: master
Choose a base branch
from
Open
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 @@ -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,137 @@
package com.kaspersky.kaspresso.device.bluetooth

import android.bluetooth.BluetoothAdapter
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() {
logger.i("Enable bluetooth")
toggleBluetooth(enable = true)
}

override fun disable() {
logger.i("Disable bluetooth")
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
}
if (!changeBluetoothStateUsingAdbServer(enable, BLUETOOTH_STATE_CHANGE_ROOT_CMD) &&
!changeBluetoothStateUsingAdbServer(enable, BLUETOOTH_STATE_CHANGE_CMD)
) {
toggleBluetoothUsingAndroidSettings(enable)
}
if (isBluetoothEnabled()) {
logger.i("Bluetooth enabled")
} else {
logger.i("Bluetooth disabled")
}
}

/**
* 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
Copy link
Collaborator

@Nikitae57 Nikitae57 Nov 21, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Catching AdbServerException here may lead to the missing the fact that adb server is down or broken. Please, check that the test crashes when the adb server is disabled. I would throw an exception in the toggleBluetooth method if bluetooth state didn't change since we failed to set the test to the desired state

Copy link
Collaborator Author

@ersanin ersanin Nov 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why should the test fail if the adb server is disabled? In this case we can still try to switch bluetooth via quick access settings.

}

@Suppress("MagicNumber")
private fun toggleBluetoothUsingAndroidSettings(enable: Boolean) {
val height = targetContext.resources.displayMetrics.heightPixels
val width = targetContext.resources.displayMetrics.widthPixels

// Swipe down on the screen to open the Quick Access Menu
UiSystem {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Plesae, leave a comment on what's going on here

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

drag(width / 2, 0, width / 2, (height * 0.67).toInt(), 50)
}
// Swipe down again for more options. This is necessary because sometimes the bluetooth switch is located in additional settings
UiSystem {
drag(width / 2, 0, width / 2, (height * 0.67).toInt(), 50)
}
// Turn Bluetooth off or on via Quick Access Menu
NotificationsFullScreen {
bluetoothSwitch.setChecked(enable)
}
// Swipe up to close additional settings
UiSystem {
drag(width / 2, height, width / 2, 0, 50)
}
// Swipe up again to close Quick Access Menu
UiSystem {
drag(width / 2, height, width / 2, 0, 50)
}
}

private fun isBluetoothNotSupported(): Boolean =
getBluetoothAdapter() == null

private fun isBluetoothEnabled(): Boolean =
getBluetoothAdapter()?.isEnabled ?: false

private fun getBluetoothAdapter(): BluetoothAdapter? =
(this.targetContext.getSystemService(Context.BLUETOOTH_SERVICE) as? BluetoothManager)?.adapter

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,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<DeviceSampleActivity>()

@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
}
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
Loading