Skip to content

Commit d514fc6

Browse files
authored
Merge pull request #61 from superus8r/develop
Prepare release 0.9.88
2 parents 00fe65b + d4c0706 commit d514fc6

File tree

12 files changed

+704
-307
lines changed

12 files changed

+704
-307
lines changed

app/build.gradle.kts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,8 @@ android {
3737
applicationId = "org.kabiri.android.usbterminal"
3838
minSdk = 24
3939
targetSdk = 35
40-
versionCode = System.getenv("CIRCLE_BUILD_NUM")?.toIntOrNull() ?: 17
41-
versionName = "0.9.87${System.getenv("CIRCLE_BUILD_NUM") ?: ""}"
40+
versionCode = System.getenv("CIRCLE_BUILD_NUM")?.toIntOrNull() ?: 18
41+
versionName = "0.9.88${System.getenv("CIRCLE_BUILD_NUM") ?: ""}"
4242
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
4343
}
4444

@@ -120,7 +120,8 @@ tasks.register<JacocoReport>("jacocoTestReport") {
120120
)
121121
val kotlinDebugTree = fileTree(layout.buildDirectory.dir("tmp/kotlin-classes/debug")) { exclude(fileFilter) }
122122
val mainKotlinSrc = layout.projectDirectory.dir("src/main/kotlin")
123-
sourceDirectories.from(files(mainKotlinSrc))
123+
val mainJavaSrc = layout.projectDirectory.dir("src/main/java")
124+
sourceDirectories.from(files(mainKotlinSrc, mainJavaSrc))
124125
classDirectories.from(files(kotlinDebugTree))
125126
executionData.from(fileTree(layout.buildDirectory) {
126127
include(

app/src/androidTest/java/org/kabiri/android/usbterminal/MainActivityAndroidTest.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ internal class MainActivityAndroidTest {
4141
// arrange
4242
// act
4343
// assert
44-
onView(withId(R.id.tvOutput)).check(matches(isDisplayed()))
44+
onView(withId(R.id.composeOutput)).check(matches(isDisplayed()))
4545
onView(withId(R.id.btEnter)).check(matches(isDisplayed()))
4646
onView(withId(R.id.etInput)).check(matches(isDisplayed()))
4747
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
package org.kabiri.android.usbterminal.ui.terminal
2+
3+
import android.content.ClipboardManager
4+
import android.content.Context
5+
import androidx.activity.ComponentActivity
6+
import androidx.compose.runtime.mutableStateListOf
7+
import androidx.compose.ui.Modifier
8+
import androidx.compose.ui.platform.testTag
9+
import androidx.compose.ui.test.assertIsDisplayed
10+
import androidx.compose.ui.test.junit4.createAndroidComposeRule
11+
import androidx.compose.ui.test.longClick
12+
import androidx.compose.ui.test.onNodeWithTag
13+
import androidx.compose.ui.test.onNodeWithText
14+
import androidx.compose.ui.test.performTouchInput
15+
import androidx.test.ext.junit.runners.AndroidJUnit4
16+
import com.google.common.truth.Truth.assertThat
17+
import org.junit.Rule
18+
import org.junit.Test
19+
import org.junit.runner.RunWith
20+
import org.kabiri.android.usbterminal.model.OutputText
21+
import org.kabiri.android.usbterminal.ui.theme.UsbTerminalTheme
22+
23+
@RunWith(AndroidJUnit4::class)
24+
class TerminalOutputAndroidTest {
25+
@get:Rule
26+
val composeRule = createAndroidComposeRule<ComponentActivity>()
27+
28+
@Test
29+
fun terminalOutput_displaysAllLines() {
30+
// arrange
31+
val logs =
32+
mutableStateListOf(
33+
OutputText("Line 1", OutputText.OutputType.TYPE_NORMAL),
34+
OutputText("Error!", OutputText.OutputType.TYPE_ERROR),
35+
OutputText("Info", OutputText.OutputType.TYPE_INFO),
36+
)
37+
38+
// act
39+
composeRule.setContent {
40+
UsbTerminalTheme {
41+
TerminalOutput(logs = logs, autoScroll = false)
42+
}
43+
}
44+
45+
// assert
46+
composeRule.onNodeWithText("Line 1").assertIsDisplayed()
47+
composeRule.onNodeWithText("Error!").assertIsDisplayed()
48+
composeRule.onNodeWithText("Info").assertIsDisplayed()
49+
}
50+
51+
@Test
52+
fun terminalOutput_longPressCopiesAllText() {
53+
// arrange
54+
val context = composeRule.activity
55+
val logs =
56+
mutableStateListOf(
57+
OutputText("A\n", OutputText.OutputType.TYPE_NORMAL),
58+
OutputText("B", OutputText.OutputType.TYPE_NORMAL),
59+
)
60+
61+
composeRule.setContent {
62+
UsbTerminalTheme {
63+
TerminalOutput(logs = logs, autoScroll = true)
64+
}
65+
}
66+
67+
// act: long-press on one of the visible lines (parent handles the gesture)
68+
composeRule.onNodeWithText("B").performTouchInput { longClick() }
69+
composeRule.waitForIdle()
70+
71+
// assert clipboard contains concatenated text
72+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
73+
val copied =
74+
clipboard.primaryClip
75+
?.getItemAt(0)
76+
?.coerceToText(context)
77+
?.toString()
78+
assertThat(copied).isEqualTo("A\nB")
79+
}
80+
81+
@Test
82+
fun terminalOutput_appliesModifierTag() {
83+
// arrange
84+
val logs =
85+
mutableStateListOf(
86+
OutputText("Tagged line", OutputText.OutputType.TYPE_NORMAL),
87+
)
88+
89+
// act
90+
composeRule.setContent {
91+
UsbTerminalTheme {
92+
TerminalOutput(
93+
logs = logs,
94+
autoScroll = false,
95+
modifier = Modifier.testTag("terminal"),
96+
)
97+
}
98+
}
99+
100+
// assert: the tagged node exists and is visible, and the text is displayed
101+
composeRule.onNodeWithTag("terminal").assertExists().assertIsDisplayed()
102+
composeRule.onNodeWithText("Tagged line").assertIsDisplayed()
103+
}
104+
105+
@Test
106+
fun terminalOutput_handlesEmptyLogs_andLongPressCopiesEmpty() {
107+
// arrange
108+
val context = composeRule.activity
109+
val logs = mutableStateListOf<OutputText>()
110+
111+
composeRule.setContent {
112+
UsbTerminalTheme {
113+
TerminalOutput(
114+
logs = logs,
115+
autoScroll = false,
116+
modifier =
117+
Modifier.testTag("terminal"),
118+
)
119+
}
120+
}
121+
122+
// act: long-press the list itself
123+
composeRule.onNodeWithTag("terminal").performTouchInput { longClick() }
124+
composeRule.waitForIdle()
125+
126+
// assert: clipboard should contain empty string
127+
val clipboard = context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager
128+
val copied =
129+
clipboard.primaryClip
130+
?.getItemAt(0)
131+
?.coerceToText(context)
132+
?.toString()
133+
assertThat(copied).isEqualTo("")
134+
}
135+
136+
@Test
137+
fun terminalOutput_autoScrollTrue_withEmptyList_isStable() {
138+
// arrange
139+
val logs = mutableStateListOf<OutputText>()
140+
141+
// act
142+
composeRule.setContent {
143+
UsbTerminalTheme {
144+
TerminalOutput(
145+
logs = logs,
146+
autoScroll = true,
147+
modifier = Modifier.testTag("terminal"),
148+
)
149+
}
150+
}
151+
152+
// assert: composable exists but not enabled when with empty logs and autoScroll enabled
153+
composeRule.onNodeWithTag("terminal").assertExists()
154+
}
155+
156+
@Test
157+
fun terminalOutput_noAutoScroll_append_rendersNewItem() {
158+
// arrange
159+
val logs =
160+
mutableStateListOf(
161+
OutputText("Initial", OutputText.OutputType.TYPE_NORMAL),
162+
)
163+
164+
composeRule.setContent {
165+
UsbTerminalTheme {
166+
TerminalOutput(
167+
logs = logs,
168+
autoScroll = false,
169+
modifier = Modifier.testTag("terminal"),
170+
)
171+
}
172+
}
173+
174+
// act: append a new line while autoScroll is disabled
175+
composeRule.runOnUiThread {
176+
logs.add(OutputText("Next", OutputText.OutputType.TYPE_NORMAL))
177+
}
178+
composeRule.waitForIdle()
179+
180+
// assert: new item is rendered (if condition path with autoScroll=false evaluated)
181+
composeRule.onNodeWithText("Next").assertIsDisplayed()
182+
}
183+
}

app/src/main/java/org/kabiri/android/usbterminal/MainActivity.kt

Lines changed: 14 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package org.kabiri.android.usbterminal
22

33
import android.os.Bundle
4-
import android.text.method.ScrollingMovementMethod
54
import android.util.Log
65
import android.view.KeyEvent
76
import android.view.Menu
@@ -11,17 +10,17 @@ import android.view.View
1110
import android.view.inputmethod.EditorInfo
1211
import android.widget.Button
1312
import android.widget.EditText
14-
import android.widget.TextView
1513
import androidx.activity.viewModels
1614
import androidx.appcompat.app.AppCompatActivity
15+
import androidx.compose.runtime.collectAsState
16+
import androidx.compose.ui.platform.ComposeView
1717
import androidx.core.view.ViewCompat
1818
import androidx.core.view.WindowInsetsCompat
19-
import androidx.lifecycle.lifecycleScope
2019
import dagger.hilt.android.AndroidEntryPoint
21-
import kotlinx.coroutines.launch
2220
import org.kabiri.android.usbterminal.ui.setting.SettingModalBottomSheet
2321
import org.kabiri.android.usbterminal.ui.setting.SettingViewModel
24-
import org.kabiri.android.usbterminal.util.scrollToLastLine
22+
import org.kabiri.android.usbterminal.ui.terminal.TerminalOutput
23+
import org.kabiri.android.usbterminal.ui.theme.UsbTerminalTheme
2524
import org.kabiri.android.usbterminal.viewmodel.MainActivityViewModel
2625

2726
private const val TAG = "MainActivity"
@@ -34,6 +33,7 @@ class MainActivity : AppCompatActivity() {
3433
override fun onCreate(savedInstanceState: Bundle?) {
3534
super.onCreate(savedInstanceState)
3635
viewModel.startObservingUsbDevice()
36+
viewModel.startObservingTerminalOutput()
3737
setContentView(R.layout.activity_main)
3838

3939
val rootView = findViewById<View>(R.id.root_view)
@@ -56,26 +56,17 @@ class MainActivity : AppCompatActivity() {
5656
}
5757

5858
val etInput = findViewById<EditText>(R.id.etInput)
59-
val tvOutput = findViewById<TextView>(R.id.tvOutput)
59+
val composeOutput = findViewById<ComposeView>(R.id.composeOutput)
6060
val btEnter = findViewById<Button>(R.id.btEnter)
6161

62-
// make the text view scrollable:
63-
tvOutput.movementMethod = ScrollingMovementMethod()
64-
65-
var autoScrollEnabled = true
66-
lifecycleScope.launch {
67-
settingViewModel.currentAutoScroll.collect { enabled ->
68-
autoScrollEnabled = enabled
69-
}
70-
}
71-
72-
lifecycleScope.launch {
73-
viewModel.getLiveOutput()
74-
viewModel.output.collect {
75-
tvOutput.apply {
76-
text = it
77-
if (autoScrollEnabled) scrollToLastLine()
78-
}
62+
// Compose terminal output UI
63+
composeOutput.setContent {
64+
UsbTerminalTheme {
65+
val autoScrollEnabled = settingViewModel.currentAutoScroll.collectAsState(initial = true).value
66+
TerminalOutput(
67+
logs = viewModel.output,
68+
autoScroll = autoScrollEnabled,
69+
)
7970
}
8071
}
8172

0 commit comments

Comments
 (0)