Skip to content
Draft
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
36 changes: 35 additions & 1 deletion openless-all/app/android/kotlin/OpenLessImeService.kt
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,11 @@ class OpenLessImeService : InputMethodService(), OpenLessOverlayBridge.OverlaySt

private fun stopImeSession() {
if (imeSessionId == null) pendingImeStop = true
else if (!sendImeCommand("stop")) { recording = false; processing = false }
else if (!sendImeCommand("stop")) {
recording = false
processing = false
releaseDismissedEditReplacement()
}
}

private fun onImeSessionEvent(json: String) {
Expand Down Expand Up @@ -118,6 +122,10 @@ class OpenLessImeService : InputMethodService(), OpenLessOverlayBridge.OverlaySt
imeSessionId = null
pendingImeStop = false
pendingCloudArm = false
// The edit panel was already dismissed when recording stopped.
// Leaving the replacement armed makes the next successful
// dictation delete the old span by length.
releaseDismissedEditReplacement()
setState("error", ui("听写失败,请重试", "Dictation failed; please retry"))
}
}
Expand Down Expand Up @@ -311,6 +319,9 @@ class OpenLessImeService : InputMethodService(), OpenLessOverlayBridge.OverlaySt
// polish animation plays there, not on the compact edit view), while
// this flag keeps commitImeText() routing the eventual result to
// finishEditWithSpokenReplacement() instead of a normal commit.
// A failed session, or a later tap on the main mic, must release it —
// otherwise that next utterance is still treated as the correction and
// deletes original.length characters before the cursor.
private var awaitingEditReplacement = false
// The exact span being replaced by the edit flow's "speak the correct
// word" mic: either whatever the user selected in the real input field,
Expand Down Expand Up @@ -3511,6 +3522,10 @@ class OpenLessImeService : InputMethodService(), OpenLessOverlayBridge.OverlaySt
rawModeArmed = false
quickNoteArmed = false
cloudNoteArmed = false
// A tap on the main mic (edit panel already gone) is a new
// utterance. Drop a correction that failed or was superseded
// before its result arrived, so this session cannot inherit it.
releaseDismissedEditReplacement()
// The actual start of a new recording attempt — reset the
// silence watch and fire the start haptic here, not in
// onCapsuleStateChanged's "recording" branch: that branch only
Expand Down Expand Up @@ -4026,6 +4041,25 @@ class OpenLessImeService : InputMethodService(), OpenLessOverlayBridge.OverlaySt
toggleDictation()
}

/**
* Drops a spoken correction that is no longer on screen. The in-flight
* result of an open edit panel must keep its replacement; once that panel
* is gone, the next commit is an ordinary insert.
*/
private fun releaseDismissedEditReplacement() {
val next = PendingEditReplacement(
awaiting = awaitingEditReplacement,
panelVisible = editingDictationResult,
original = editingOriginalText,
replacesWhole = editingReplacesWholeResult,
addToDictionary = addToDictionaryForEdit,
).releaseIfPanelDismissed()
awaitingEditReplacement = next.awaiting
editingOriginalText = next.original
editingReplacesWholeResult = next.replacesWhole
addToDictionaryForEdit = next.addToDictionary
}

/** Backs out of the edit sub-view without applying any correction. */
private fun closeEditDictationResult() {
if (recording) {
Expand Down
26 changes: 26 additions & 0 deletions openless-all/app/android/kotlin/PendingEditReplacement.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package com.openless.app

/**
* Spoken edit replacement stays armed after the edit panel closes so the
* in-flight result can still replace the original span. That arming must not
* survive a failed session or a later tap on the main mic: both look like a
* fresh dictation, and [OpenLessImeService] would otherwise delete by the old
* span's length.
*/
internal data class PendingEditReplacement(
val awaiting: Boolean,
val panelVisible: Boolean,
val original: String?,
val replacesWhole: Boolean,
val addToDictionary: Boolean,
) {
fun releaseIfPanelDismissed(): PendingEditReplacement {
if (!awaiting || panelVisible) return this
return copy(
awaiting = false,
original = null,
replacesWhole = false,
addToDictionary = false,
)
}
}
42 changes: 42 additions & 0 deletions openless-all/app/android/kotlin/test/PendingEditReplacementTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package com.openless.app

import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertNull
import org.junit.Test

class PendingEditReplacementTest {
private fun armed(panelVisible: Boolean) = PendingEditReplacement(
awaiting = true,
panelVisible = panelVisible,
original = "hello world",
replacesWhole = true,
addToDictionary = true,
)

@Test
fun failureAfterTheEditPanelClosesDropsThePendingReplacement() {
val released = armed(panelVisible = false).releaseIfPanelDismissed()
assertFalse(released.awaiting)
assertNull(released.original)
assertFalse(released.replacesWhole)
assertFalse(released.addToDictionary)
}

@Test
fun failureWhileTheEditPanelIsOpenKeepsTheRetryArmed() {
assertEquals(armed(panelVisible = true), armed(panelVisible = true).releaseIfPanelDismissed())
}

@Test
fun idleDictationIsUnchanged() {
val idle = PendingEditReplacement(
awaiting = false,
panelVisible = false,
original = null,
replacesWhole = false,
addToDictionary = false,
)
assertEquals(idle, idle.releaseIfPanelDismissed())
}
}
2 changes: 2 additions & 0 deletions openless-all/app/scripts/copy-android-scaffolding.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ const KOTLIN_FILES = [
'OpenLessContentReader.kt',
'OpenLessContentWriter.kt',
'OpenLessImeService.kt',
'PendingEditReplacement.kt',
'ImeLearningPolicy.kt',
'StrokeInputController.kt',
'StrokeInput.kt',
Expand All @@ -81,6 +82,7 @@ const KOTLIN_FILES = [

const KOTLIN_TEST_FILES = [
'ImeLearningPolicyTest.kt',
'PendingEditReplacementTest.kt',
'OpenLessContentReaderTest.kt',
'OpenLessCredentialCipherTest.kt',
'OpenLessShizukuBridgeTest.kt',
Expand Down
Loading