Skip to content

Commit 5df6518

Browse files
authored
Merge pull request #49 from WahdanZ/feature/improvemnts
Fix threading violations, crash bugs, resource leaks, and code cleanup
2 parents 36d9ae2 + 5f66f35 commit 5df6518

12 files changed

Lines changed: 111 additions & 103 deletions

CHANGELOG.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,26 @@
11
<!-- Keep a Changelog guide -> https://keepachangelog.com -->
22

33
## [Unreleased]
4+
### Fixed
5+
- **Threading**: `currentBackStack` and `currentApplicationBackStack` now run ADB on a background thread and show popups on EDT; PSI lookups wrapped in `ReadAction.compute`
6+
- **Crash**: `GetFragmentsCommand` — safe split access with `getOrNull` instead of hardcoded index, avoids `ArrayIndexOutOfBoundsException`
7+
- **Crash**: `GetApplicationBackStackCommand` — safe array bounds (`getOrNull`) and removed `!!` force-unwrap on `find {}` result
8+
- **Crash**: `Debugger` — replaced `client!!` with null-safe early return to avoid `NullPointerException` when the debug client is unavailable
9+
- **Resource leak**: `AdbControllerImp` now implements `Disposable` and removes the `AndroidDebugBridge` device-change listener in `dispose()`
10+
- **Resource leak**: `BaseAction``AdbControllerImp` is disposed immediately after the synchronous device-list read, instead of being incorrectly registered against `Project` as a long-lived disposable parent
11+
- **Resource leak**: `AdbDrawerViewer``AdbControllerImp` is now registered against `toolWindow.disposable` instead of `Project`; the controller's lifetime correctly matches the tool window, not the entire project
12+
13+
### Changed
14+
- `BaseAction` no longer uses `Disposer.register(project, controller)` — the controller is disposed explicitly after `connectedDevices()` returns, which is safe because the call is synchronous
15+
16+
### Removed
17+
- Debug `println` statements from `SpockAdbViewer` and `GetApplicationBackStackCommand`
18+
- Large commented-out dead code blocks in `SpockAdbViewer`, `ConnectDeviceOverIPCommand`, and `CheckBoxDialog`
19+
- Duplicate empty `setting.addActionListener {}` in `SpockAdbViewer`
20+
21+
### Internal
22+
- Fixed exception message `"Bazinga!!"` in `GetApplicationPermission` → professional message
23+
- Renamed `kippAppProcess``killAppProcess` (typo fix) in `ProcessDeathCommand`
424

525
## [3.0.0]
626
### Added

src/main/kotlin/spock/AdbDrawerViewer.kt

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,17 @@ import com.intellij.openapi.project.Project
44
import com.intellij.openapi.wm.ToolWindow
55
import com.intellij.openapi.wm.ToolWindowFactory
66
import org.jetbrains.android.sdk.AndroidSdkUtils
7-
import spock.adb.AdbController
87
import spock.adb.AdbControllerImp
98
import spock.adb.SpockAdbViewer
109

1110
class AdbDrawerViewer : ToolWindowFactory {
1211

1312
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
14-
val adbController: AdbController = AdbControllerImp(project, AndroidSdkUtils.getDebugBridge(project))
13+
val adbController = AdbControllerImp(project, AndroidSdkUtils.getDebugBridge(project))
14+
// Use toolWindow.disposable, not project, as the parent — it is disposed when the
15+
// tool window is torn down (plugin unload / project close), which is the correct
16+
// lifetime for the device-change listener registered inside AdbControllerImp.
17+
com.intellij.openapi.util.Disposer.register(toolWindow.disposable, adbController)
1518
val contentManager = toolWindow.contentManager
1619

1720
with(SpockAdbViewer(project)) {

src/main/kotlin/spock/adb/AdbControllerImp.kt

Lines changed: 69 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import spock.adb.premission.ListItem
2020
class AdbControllerImp(
2121
private val project: Project,
2222
private val debugBridge: AndroidDebugBridge?
23-
) : AdbController, AndroidDebugBridge.IDeviceChangeListener {
23+
) : AdbController, AndroidDebugBridge.IDeviceChangeListener, com.intellij.openapi.Disposable {
2424

2525
private var updateDeviceList: ((List<IDevice>) -> Unit)? = null
2626

@@ -55,52 +55,72 @@ class AdbControllerImp(
5555
device: IDevice
5656

5757
) {
58-
val activitiesList = mutableListOf<String>()
59-
val activitiesClass: List<BackStackData> = GetBackStackCommand().execute(Any(), project, device)
58+
// ADB must run on a background thread — wrap everything in execute {}
59+
execute {
60+
val activitiesList = mutableListOf<String>()
61+
val activitiesClass: List<BackStackData> = GetBackStackCommand().execute(Any(), project, device)
6062

61-
activitiesClass.forEachIndexed { index, activityData ->
62-
activitiesList.add("\t$index-${activityData.appPackage}")
63+
activitiesClass.forEachIndexed { index, activityData ->
64+
activitiesList.add("\t$index-${activityData.appPackage}")
65+
activityData.activitiesList.forEachIndexed { activityIndex, activity ->
66+
activitiesList.add("\t\t\t\t$activityIndex-${activity}")
67+
}
68+
}
6369

64-
activityData.activitiesList.forEachIndexed { activityIndex, activity ->
65-
activitiesList.add("\t\t\t\t$activityIndex-${activity}")
70+
// PSI lookups require a ReadAction when called from a background thread
71+
val classes = com.intellij.openapi.application.ReadAction.compute<List<PsiClass?>, RuntimeException> {
72+
activitiesList.map { it.trim().substringAfter("-").psiClassByNameFromProjct(project) }
6673
}
67-
}
6874

69-
showClassPopup(
70-
title = "Activities",
71-
items = activitiesList,
72-
classes = activitiesList.map { it.trim().substringAfter("-").psiClassByNameFromProjct(project) }
73-
)
75+
// Popup creation and display must happen on the EDT
76+
ApplicationManager.getApplication().invokeLater {
77+
showClassPopup(title = "Activities", items = activitiesList, classes = classes)
78+
}
79+
}
7480
}
7581

7682
override fun currentApplicationBackStack(device: IDevice) {
77-
val applicationID = getApplicationID(device)
78-
val activitiesList: MutableList<String>
79-
val activitiesClass: List<ActivityData> =
80-
GetApplicationBackStackCommand().execute(applicationID, project, device)
81-
activitiesList = activitiesClass.map { listOf(it.activity) + it.fragment }.flatten().toMutableList()
82-
JBPopupFactory.getInstance()
83-
.createPopupChooserBuilder(activitiesList)
84-
.setTitle("Activities")
85-
.setRenderer(javax.swing.ListCellRenderer<String> { _, value, _, _, _ ->
86-
var title = value.toString()
87-
title = if (!value.toString().contains('.'))
88-
" |--$title (Fragment)"
89-
else
90-
(title.split('.').lastOrNull() ?: "") + "(Activity)"
91-
val label = JBLabel(title)
92-
label.border = JBUI.Borders.empty(5, 10, 5, 20)
93-
label
94-
})
95-
.setItemChosenCallback { current ->
96-
if (current.contains('.'))
97-
current.psiClassByNameFromProjct(project)?.openIn(project)
98-
else
99-
current.psiClassByNameFromCache(project)?.openIn(project)
100-
}
101-
.createPopup()
102-
.showCenteredInCurrentWindow(project)
83+
// ADB must run on a background thread — wrap everything in execute {}
84+
execute {
85+
val applicationID = getApplicationID(device)
86+
val activitiesClass: List<ActivityData> =
87+
GetApplicationBackStackCommand().execute(applicationID, project, device)
88+
val activitiesList = activitiesClass.map { listOf(it.activity) + it.fragment }.flatten().toMutableList()
10389

90+
// Popup creation and display must happen on the EDT
91+
ApplicationManager.getApplication().invokeLater {
92+
JBPopupFactory.getInstance()
93+
.createPopupChooserBuilder(activitiesList)
94+
.setTitle("Activities")
95+
.setRenderer(javax.swing.ListCellRenderer<String> { _, value, _, _, _ ->
96+
var title = value.toString()
97+
title = if (!value.toString().contains('.'))
98+
" |--$title (Fragment)"
99+
else
100+
(title.split('.').lastOrNull() ?: "") + "(Activity)"
101+
val label = JBLabel(title)
102+
label.border = JBUI.Borders.empty(5, 10, 5, 20)
103+
label
104+
})
105+
.setItemChosenCallback { current ->
106+
// Item chosen callback runs on EDT; dispatch PSI lookup to background
107+
execute {
108+
val psiClass = com.intellij.openapi.application.ReadAction.compute<PsiClass?, RuntimeException> {
109+
if (current.contains('.'))
110+
current.psiClassByNameFromProjct(project)
111+
else
112+
current.psiClassByNameFromCache(project)
113+
}
114+
ApplicationManager.getApplication().invokeLater {
115+
psiClass?.openIn(project)
116+
?: showError("class $current Not Found")
117+
}
118+
}
119+
}
120+
.createPopup()
121+
.showCenteredInCurrentWindow(project)
122+
}
123+
}
104124
}
105125

106126
override fun currentActivity(
@@ -110,9 +130,12 @@ class AdbControllerImp(
110130
execute {
111131
val activity =
112132
GetActivityCommand().execute(Any(), project, device) ?: throw Exception("No activities found")
133+
// Resolve PSI on background thread inside ReadAction, then open on EDT
134+
val psiClass = com.intellij.openapi.application.ReadAction.compute<PsiClass?, RuntimeException> {
135+
activity.psiClassByNameFromProjct(project)
136+
}
113137
ApplicationManager.getApplication().invokeLater {
114-
activity.psiClassByNameFromProjct(project)?.openIn(project)
115-
?: showError("class $activity Not Found")
138+
psiClass?.openIn(project) ?: showError("class $activity Not Found")
116139
}
117140
}
118141
}
@@ -444,4 +467,9 @@ class AdbControllerImp(
444467
showSuccess(result)
445468
}
446469
}
470+
471+
override fun dispose() {
472+
AndroidDebugBridge.removeDeviceChangeListener(this)
473+
updateDeviceList = null
474+
}
447475
}

src/main/kotlin/spock/adb/SpockAdbViewer.kt

Lines changed: 1 addition & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,6 @@ class SpockAdbViewer(
117117
AppSettingService.getInstance().run {
118118
state.let {
119119
val dialog = CheckBoxDialog(it.list) { selectedItem ->
120-
println(selectedItem)
121120
this.loadState(it.copy(list = it.list.map { item ->
122121
if (item.name == selectedItem.name)
123122
item.copy(isSelected = selectedItem.isSelected)
@@ -133,29 +132,10 @@ class SpockAdbViewer(
133132
}
134133

135134
}
136-
adbWifi.isVisible = false
137-
// adbWifi.addActionListener {
138-
// val ip = Messages.showInputDialog(
139-
// "Enter You Android Device IP address",
140-
// "Spock Adb- Device connect over Wifi",
141-
// null,
142-
// "192.168.1.20",
143-
// IPAddressInputValidator()
144-
// )
145-
// ip?.let { adbController.connectDeviceOverIp(ip = ip) }
146-
147-
// }
148-
149-
// refresh.addActionListener {
150-
// adbController.refresh()
151-
// updateDevicesList()
152-
// }
135+
adbWifi.isVisible = false
153136
devicesListComboBox.addItemListener {
154137
selectedIDevice = devices[devicesListComboBox.selectedIndex]
155138

156-
}
157-
setting.addActionListener {
158-
159139
}
160140
activitiesBackStackButton.addActionListener {
161141
selectedIDevice?.let { device ->

src/main/kotlin/spock/adb/actions/BaseAction.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,21 +11,25 @@ import spock.adb.AdbController
1111
import spock.adb.AdbControllerImp
1212
import spock.adb.notification.CommonNotifier.Companion.showNotifier
1313

14+
1415
abstract class BaseAction : AnAction() {
1516
override fun actionPerformed(event: AnActionEvent) = event.project?.run {
1617
val controller = AdbControllerImp(this, AndroidSdkUtils.getDebugBridge(this))
18+
// connectedDevices() is synchronous — it immediately invokes the callback with the
19+
// current bridge.devices list, so we can dispose right after to remove the
20+
// AndroidDebugBridge device-change listener that init() registered.
1721
controller.connectedDevices { list ->
1822
if (list.isNotEmpty())
19-
if(list.size > 1)
23+
if (list.size > 1)
2024
showDeviceList(project = this, devices = list) {
2125
performAction(controller, it)
2226
}
2327
else
2428
performAction(controller, list[0])
2529
else
2630
showNotifier(project = this, content = "No Devices", type = NotificationType.ERROR)
27-
2831
}
32+
controller.dispose() // remove the device-change listener; async ADB work is unaffected
2933
} ?: cancelAction()
3034

3135
private fun showDeviceList(project: Project, devices: List<IDevice>, block: (device: IDevice) -> Unit) {
Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,9 @@
11
package spock.adb.command
22

33
import com.intellij.openapi.project.Project
4-
import java.nio.charset.Charset
5-
import java.util.concurrent.TimeUnit
6-
import org.jetbrains.android.sdk.AndroidSdkUtils
74

85
class ConnectDeviceOverIPCommand : AdbCommand<String, Any> {
96
override fun execute(p: String, project: Project): Any {
10-
// val adbPath = AndroidSdkUtils.getAdb(project)?.absolutePath
11-
// var process: Process? = null
12-
// print("$adbPath connect $p:5555")
13-
// try {
14-
// process = Runtime.getRuntime().exec("$adbPath connect $p:5555")
15-
// if (!process.waitFor(30, TimeUnit.SECONDS)) {
16-
// process.run { destroy() }
17-
// }
18-
// val content = process.errorStream.readBytes().toString(Charset.defaultCharset())
19-
// print(content)
20-
// process.run {
21-
// print(content)
22-
// destroy()
23-
// }
24-
// if (content.isNotEmpty()) throw Exception("enable to connect to $p")
25-
// return ""
26-
// } catch (e: Exception) {
27-
// print(e)
28-
// process?.destroy()
29-
// throw Exception("enable to connect to $p")
30-
// }
317
return ""
328
}
339
}

src/main/kotlin/spock/adb/command/GetApplicationBackStackCommand.kt

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,9 @@ import java.util.concurrent.TimeUnit
99
class GetApplicationBackStackCommand : Command<String, List<ActivityData>> {
1010

1111
companion object {
12-
val systemFragments = listOf("ReportFragment", "FragmentManager", "NavHostFragment", "BackStackEntry")
1312
val currentActiveActivity = Regex("([A-Z])\\w+=true")
1413
val activityRegex = Regex(" {2}ACTIVITY.*")
1514
val fragmentRegex = Regex("[a-zA-Z1-9]+\\{[a-z0-9}]")
16-
val addedFragmentRegex = Regex("#2: ADD [a-zA-Z1-9]+\\{[a-z0-9}]")
1715
val removedFragmentRegex = Regex("#1: REMOVE [a-zA-Z1-9]+\\{[a-z0-9}]")
1816
}
1917

@@ -33,9 +31,9 @@ class GetApplicationBackStackCommand : Command<String, List<ActivityData>> {
3331
val lines = bulkActivitiesData.lines()
3432
lines.mapIndexed { index, s ->
3533
if (s.contains(activityRegex)) {
36-
val status = currentActiveActivity.find(lines[index + 2])?.value?.split("=")?.firstOrNull()
34+
val status = currentActiveActivity.find(lines.getOrNull(index + 2) ?: "")?.value?.split("=")?.firstOrNull()
3735
?: ""
38-
val getActivityName = s.split(" ").find { it.contains("/") }!!.replace("/", "")
36+
val getActivityName = s.split(" ").find { it.contains("/") }?.replace("/", "") ?: return@mapIndexed
3937
tasks.add(
4038
ActivityData(
4139
activity = getActivityName,
@@ -53,7 +51,6 @@ class GetApplicationBackStackCommand : Command<String, List<ActivityData>> {
5351
val removedFragment =
5452
removedFragmentRegex.find(s)?.value?.split("{")?.firstOrNull()?.split("REMOVE ")?.lastOrNull()
5553
if (removedFragment != null) {
56-
println(removedFragment)
5754
tasks[tasks.size - 1] = task.copy(fragment = task.fragment + listOf(removedFragment))
5855
}
5956
}

src/main/kotlin/spock/adb/command/GetApplicationPermission.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ class GetApplicationPermission : Command<String, List<ListItem>> {
3838
} else
3939
throw Exception("Application $p not installed")
4040
} else
41-
throw Exception("Bazinga!! Your Device is before Marshmallow. No need to explicitly handle permissions")
41+
throw Exception("Device API level is below Marshmallow. Runtime permissions are not supported on this device.")
4242

4343
}
4444

src/main/kotlin/spock/adb/command/GetFragmentsCommand.kt

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,9 @@ class GetFragmentsCommand : Command<String, List<FragmentData>> {
2929
}
3030

3131
private fun getFragmentsUsingOldMethod(bulkTaskDetails: String): List<FragmentData> {
32-
return bulkTaskDetails
33-
.split("Added Fragments:")[2]
32+
val parts = bulkTaskDetails.split("Added Fragments:")
33+
val section = parts.getOrNull(2) ?: return mutableListOf()
34+
return section
3435
.lines()
3536
.map { it.trim() }
3637
.filter { (it.startsWith("#") && !it.contains("BackStackEntry")) }

src/main/kotlin/spock/adb/command/ProcessDeathCommand.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ class ProcessDeathCommand : Command<String, Unit> {
1414
Thread.sleep(2500L) //If we don't add this delay, the following commands executes without the app
1515
// being on the background thus not working.
1616

17-
kippAppProcess(device, p)
17+
killAppProcess(device, p)
1818

1919
startApplication(device, p)
2020
} else {
@@ -28,7 +28,7 @@ class ProcessDeathCommand : Command<String, Unit> {
2828
}
2929
}
3030

31-
private fun kippAppProcess(device: IDevice, p: String) =
31+
private fun killAppProcess(device: IDevice, p: String) =
3232
device.executeShellCommand("am kill $p", ShellOutputReceiver(), 15L, TimeUnit.SECONDS)
3333

3434
private fun startApplication(device: IDevice, p: String) {

0 commit comments

Comments
 (0)