fix some issues with url detection

This commit is contained in:
Helium314 2023-10-16 19:52:06 +02:00
parent 3eba91e55c
commit 8abfe26411
5 changed files with 145 additions and 7 deletions

View file

@ -36,6 +36,7 @@ import androidx.annotation.Nullable;
import org.dslul.openboard.inputmethod.latin.common.Constants; import org.dslul.openboard.inputmethod.latin.common.Constants;
import org.dslul.openboard.inputmethod.latin.common.StringUtils; import org.dslul.openboard.inputmethod.latin.common.StringUtils;
import org.dslul.openboard.inputmethod.latin.common.StringUtilsKt;
import org.dslul.openboard.inputmethod.latin.common.UnicodeSurrogate; import org.dslul.openboard.inputmethod.latin.common.UnicodeSurrogate;
import org.dslul.openboard.inputmethod.latin.inputlogic.PrivateCommandPerformer; import org.dslul.openboard.inputmethod.latin.inputlogic.PrivateCommandPerformer;
import org.dslul.openboard.inputmethod.latin.settings.SpacingAndPunctuations; import org.dslul.openboard.inputmethod.latin.settings.SpacingAndPunctuations;
@ -1001,10 +1002,18 @@ public final class RichInputConnection implements PrivateCommandPerformer {
return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText); return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText);
} }
public boolean nonWordCodePointAndNoSpaceBeforeCursor(final SpacingAndPunctuations spacingAndPunctuations) {
return StringUtilsKt.nonWordCodePointAndNoSpaceBeforeCursor(mCommittedTextBeforeComposingText, spacingAndPunctuations);
}
public boolean spaceBeforeCursor() { public boolean spaceBeforeCursor() {
return mCommittedTextBeforeComposingText.indexOf(" ") != -1; return mCommittedTextBeforeComposingText.indexOf(" ") != -1;
} }
public boolean hasLetterBeforeLastSpaceBeforeCursor() {
return StringUtilsKt.hasLetterBeforeLastSpaceBeforeCursor(mCommittedTextBeforeComposingText);
}
public boolean wordBeforeCursorMayBeEmail() { public boolean wordBeforeCursorMayBeEmail() {
return mCommittedTextBeforeComposingText.lastIndexOf(" ") < mCommittedTextBeforeComposingText.lastIndexOf("@"); return mCommittedTextBeforeComposingText.lastIndexOf(" ") < mCommittedTextBeforeComposingText.lastIndexOf("@");
} }

View file

@ -0,0 +1,49 @@
package org.dslul.openboard.inputmethod.latin.common
import org.dslul.openboard.inputmethod.latin.settings.SpacingAndPunctuations
fun loopOverCodePoints(s: CharSequence, run: (Int) -> Boolean) {
val text = if (s is String) s else s.toString()
var offset = 0
while (offset < text.length) {
val codepoint = text.codePointAt(offset)
if (run(codepoint)) return
offset += Character.charCount(codepoint)
}
}
fun loopOverCodePointsBackwards(s: CharSequence, run: (Int) -> Boolean) {
val text = if (s is String) s else s.toString()
var offset = text.length
while (offset > 0) {
val codepoint = text.codePointBefore(offset)
if (run(codepoint)) return
offset -= Character.charCount(codepoint)
}
}
fun nonWordCodePointAndNoSpaceBeforeCursor(s: CharSequence, spacingAndPunctuations: SpacingAndPunctuations): Boolean {
var space = false
var nonWordCodePoint = false
loopOverCodePointsBackwards(s) {
if (!space && Character.isWhitespace(it))
space = true
if (!nonWordCodePoint && !spacingAndPunctuations.isWordCodePoint(it))
nonWordCodePoint = true
space && nonWordCodePoint
}
return space && nonWordCodePoint
}
fun hasLetterBeforeLastSpaceBeforeCursor(s: CharSequence): Boolean {
var letter = false
loopOverCodePointsBackwards(s) {
if (Character.isWhitespace(it)) true
else if (Character.isLetter(it)) {
letter = true
true
}
else false
}
return letter
}

View file

@ -849,7 +849,7 @@ public final class InputLogic {
if (Character.getType(codePoint) == Character.OTHER_SYMBOL if (Character.getType(codePoint) == Character.OTHER_SYMBOL
|| (sv.isWordSeparator(codePoint) || (sv.isWordSeparator(codePoint)
&& (Character.isWhitespace(codePoint) // whitespace is always a separator && (Character.isWhitespace(codePoint) // whitespace is always a separator
|| !textBeforeCursorMayBeUrlOrSimilar(sv) // if text before is not URL or similar, it's a separator || !textBeforeCursorMayBeUrlOrSimilar(sv, false) // if text before is not URL or similar, it's a separator
|| (codePoint == '/' && mWordComposer.lastChar() == '/') // break composing at 2 consecutive slashes || (codePoint == '/' && mWordComposer.lastChar() == '/') // break composing at 2 consecutive slashes
) )
) )
@ -894,6 +894,8 @@ public final class InputLogic {
&& settingsValues.mSpacingAndPunctuations.isSometimesWordConnector(mConnection.getCodePointBeforeCursor()) && settingsValues.mSpacingAndPunctuations.isSometimesWordConnector(mConnection.getCodePointBeforeCursor())
// but not if there are two consecutive sometimesWordConnectors (e.g. "...bla") // but not if there are two consecutive sometimesWordConnectors (e.g. "...bla")
&& !settingsValues.mSpacingAndPunctuations.isSometimesWordConnector(mConnection.getCharBeforeBeforeCursor()) && !settingsValues.mSpacingAndPunctuations.isSometimesWordConnector(mConnection.getCharBeforeBeforeCursor())
// and not if there is no letter before the separator
&& mConnection.hasLetterBeforeLastSpaceBeforeCursor()
) { ) {
final CharSequence text = mConnection.textBeforeCursorUntilLastWhitespaceOrDoubleSlash(); final CharSequence text = mConnection.textBeforeCursorUntilLastWhitespaceOrDoubleSlash();
final TextRange range = new TextRange(text, 0, text.length(), text.length(), false); final TextRange range = new TextRange(text, 0, text.length(), text.length(), false);
@ -2108,7 +2110,7 @@ public final class InputLogic {
private void insertAutomaticSpaceIfOptionsAndTextAllow(final SettingsValues settingsValues) { private void insertAutomaticSpaceIfOptionsAndTextAllow(final SettingsValues settingsValues) {
if (settingsValues.shouldInsertSpacesAutomatically() if (settingsValues.shouldInsertSpacesAutomatically()
&& settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces && settingsValues.mSpacingAndPunctuations.mCurrentLanguageHasSpaces
&& !textBeforeCursorMayBeUrlOrSimilar(settingsValues) && !textBeforeCursorMayBeUrlOrSimilar(settingsValues, true)
&& !mConnection.textBeforeCursorLooksLikeURL() // adding this check to textBeforeCursorMayBeUrlOrSimilar might not be wanted for word continuation (see effect on unit tests) && !mConnection.textBeforeCursorLooksLikeURL() // adding this check to textBeforeCursorMayBeUrlOrSimilar might not be wanted for word continuation (see effect on unit tests)
&& !(mConnection.getCodePointBeforeCursor() == Constants.CODE_PERIOD && mConnection.wordBeforeCursorMayBeEmail()) && !(mConnection.getCodePointBeforeCursor() == Constants.CODE_PERIOD && mConnection.wordBeforeCursorMayBeEmail())
) { ) {
@ -2116,12 +2118,15 @@ public final class InputLogic {
} }
} }
private boolean textBeforeCursorMayBeUrlOrSimilar(final SettingsValues settingsValues) { private boolean textBeforeCursorMayBeUrlOrSimilar(final SettingsValues settingsValues, final Boolean forAutoSpace) {
final EditorInfo ei = getCurrentInputEditorInfo(); final EditorInfo ei = getCurrentInputEditorInfo();
// URL field and no space -> may be URL // URL field and no space -> may be URL
// for whatever absurd reason long message, postal address and email subject have type values that return true when filtering for URI, see https://developer.android.com/reference/android/text/InputType // for whatever absurd reason long message, postal address and email subject have type values that return true when filtering for URI, see https://developer.android.com/reference/android/text/InputType
// so we really need to specifically require URI as only type variation // so we really need to specifically require URI as only type variation
if (ei != null && (ei.inputType & 0x000000f0) == 0x00000010 && !mConnection.spaceBeforeCursor()) if (ei != null && (ei.inputType & 0x000000f0) == 0x00000010 &&
// we never want to commit the first part of the url, but we want to insert autospace if text might be a normal word
(forAutoSpace ? mConnection.nonWordCodePointAndNoSpaceBeforeCursor(settingsValues.mSpacingAndPunctuations) // avoid detecting URL if it could be a word
: !mConnection.spaceBeforeCursor()))
return true; return true;
// already contains a SometimesWordConnector -> may be URL (not so sure, only do with detection enabled // already contains a SometimesWordConnector -> may be URL (not so sure, only do with detection enabled
if (settingsValues.mUrlDetectionEnabled && settingsValues.mSpacingAndPunctuations.containsSometimesWordConnector(mWordComposer.getTypedWord())) if (settingsValues.mUrlDetectionEnabled && settingsValues.mSpacingAndPunctuations.containsSometimesWordConnector(mWordComposer.getTypedWord()))

View file

@ -26,3 +26,5 @@
# TODO: remove once used in code. # TODO: remove once used in code.
-keep class org.dslul.openboard.inputmethod.keyboard.KeyboardLayout { *; } -keep class org.dslul.openboard.inputmethod.keyboard.KeyboardLayout { *; }
# after upgrading to gradle 8, stack traces contain "unknown source", which is horribly bad making them rather useless
-keepattributes SourceFile,LineNumberTable

View file

@ -12,9 +12,11 @@ import org.dslul.openboard.inputmethod.event.Event
import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher
import org.dslul.openboard.inputmethod.keyboard.MainKeyboardView import org.dslul.openboard.inputmethod.keyboard.MainKeyboardView
import org.dslul.openboard.inputmethod.latin.ShadowFacilitator2.Companion.lastAddedWord import org.dslul.openboard.inputmethod.latin.ShadowFacilitator2.Companion.lastAddedWord
import org.dslul.openboard.inputmethod.latin.SuggestedWords.SuggestedWordInfo
import org.dslul.openboard.inputmethod.latin.common.Constants import org.dslul.openboard.inputmethod.latin.common.Constants
import org.dslul.openboard.inputmethod.latin.common.StringUtils import org.dslul.openboard.inputmethod.latin.common.StringUtils
import org.dslul.openboard.inputmethod.latin.inputlogic.InputLogic import org.dslul.openboard.inputmethod.latin.inputlogic.InputLogic
import org.dslul.openboard.inputmethod.latin.inputlogic.SpaceState
import org.dslul.openboard.inputmethod.latin.settings.Settings import org.dslul.openboard.inputmethod.latin.settings.Settings
import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils import org.dslul.openboard.inputmethod.latin.utils.DeviceProtectedUtils
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils
@ -48,6 +50,8 @@ class InputLogicTest {
private val connection: RichInputConnection get() = inputLogic.mConnection private val connection: RichInputConnection get() = inputLogic.mConnection
private val composerReader = InputLogic::class.java.getDeclaredField("mWordComposer").apply { isAccessible = true } private val composerReader = InputLogic::class.java.getDeclaredField("mWordComposer").apply { isAccessible = true }
private val composer get() = composerReader.get(inputLogic) as WordComposer private val composer get() = composerReader.get(inputLogic) as WordComposer
private val spaceStateReader = InputLogic::class.java.getDeclaredField("mSpaceState").apply { isAccessible = true }
private val spaceState get() = spaceStateReader.get(inputLogic) as Int
private val beforeComposingReader = RichInputConnection::class.java.getDeclaredField("mCommittedTextBeforeComposingText").apply { isAccessible = true } private val beforeComposingReader = RichInputConnection::class.java.getDeclaredField("mCommittedTextBeforeComposingText").apply { isAccessible = true }
private val connectionTextBeforeComposingText get() = (beforeComposingReader.get(connection) as CharSequence).toString() private val connectionTextBeforeComposingText get() = (beforeComposingReader.get(connection) as CharSequence).toString()
private val composingReader = RichInputConnection::class.java.getDeclaredField("mComposingText").apply { isAccessible = true } private val composingReader = RichInputConnection::class.java.getDeclaredField("mComposingText").apply { isAccessible = true }
@ -425,6 +429,38 @@ class InputLogicTest {
assertEquals("Hey, why", text) assertEquals("Hey, why", text)
} }
@Test fun `URL detection does not trigger on non-words`() {
// first make sure it works without URL detection
reset()
chainInput("15:50-17")
assertEquals("15:50-17", text)
assertEquals("", composingText)
// then with URL detection
reset()
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
chainInput("15:50-17")
assertEquals("15:50-17", text)
assertEquals("", composingText)
}
@Test fun `autospace after selecting a suggestion`() {
reset()
pickSuggestion("this")
input('b')
assertEquals("this b", text)
assertEquals("b", composingText)
}
@Test fun `autospace works in URL field when input isn't URL`() {
reset()
DeviceProtectedUtils.getSharedPreferences(latinIME).edit { putBoolean(Settings.PREF_URL_DETECTION, true) }
setInputType(InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_URI)
pickSuggestion("this")
input('b')
assertEquals("this b", text)
assertEquals("b", composingText)
}
// ------- helper functions --------- // ------- helper functions ---------
// should be called before every test, so the same state is guaranteed // should be called before every test, so the same state is guaranteed
@ -451,12 +487,15 @@ class InputLogicTest {
val oldBefore = textBeforeCursor val oldBefore = textBeforeCursor
val oldAfter = textAfterCursor val oldAfter = textAfterCursor
val insert = StringUtils.newSingleCodePointString(codePoint) val insert = StringUtils.newSingleCodePointString(codePoint)
val phantomSpaceToInsert = if (spaceState == SpaceState.PHANTOM) " " else ""
latinIME.onEvent(Event.createEventForCodePointFromUnknownSource(codePoint)) latinIME.onEvent(Event.createEventForCodePointFromUnknownSource(codePoint))
handleMessages() handleMessages()
if (!settingsValues.mAutospaceAfterPunctuationEnabled) if (phantomSpaceToInsert.isEmpty())
assertEquals(oldBefore + insert, textBeforeCursor) assertEquals(oldBefore + phantomSpaceToInsert + insert, textBeforeCursor)
else // in some cases autospace might be suppressed
assert(oldBefore + phantomSpaceToInsert + insert == textBeforeCursor || oldBefore + insert == textBeforeCursor)
assertEquals(oldAfter, textAfterCursor) assertEquals(oldAfter, textAfterCursor)
assertEquals(textBeforeCursor + textAfterCursor, getText()) assertEquals(textBeforeCursor + textAfterCursor, getText())
checkConnectionConsistency() checkConnectionConsistency()
@ -548,6 +587,13 @@ class InputLogicTest {
checkConnectionConsistency() checkConnectionConsistency()
} }
// like selecting a suggestion from strip
private fun pickSuggestion(suggestion: String) {
val info = SuggestedWordInfo(suggestion, "", 0, 0, null, 0, 0)
latinIME.pickSuggestionManually(info)
checkConnectionConsistency()
}
private fun checkConnectionConsistency() { private fun checkConnectionConsistency() {
// RichInputConnection only has composing text up to cursor, but InputConnection has full composing text // RichInputConnection only has composing text up to cursor, but InputConnection has full composing text
val expectedConnectionComposingText = if (composingStart == -1 || composingEnd == -1) "" val expectedConnectionComposingText = if (composingStart == -1 || composingEnd == -1) ""
@ -730,6 +776,34 @@ private val ic = object : InputConnection {
selectionEnd -= beforeLength selectionEnd -= beforeLength
return true; return true;
} }
override fun sendKeyEvent(p0: KeyEvent): Boolean {
if (p0.action != KeyEvent.ACTION_DOWN) return true // only change the text on key down, like RichInputConnection does
if (p0.keyCode == KeyEvent.KEYCODE_DEL) {
if (selectionEnd == 0) return true // nothing to delete
if (selectedText.isEmpty()) {
text = text.substring(0, selectionStart - 1) + text.substring(selectionEnd)
selectionStart -= 1
} else {
text = text.substring(0, selectionStart) + text.substring(selectionEnd)
}
selectionEnd = selectionStart
return true
}
val textToAdd = when (p0.keyCode) {
KeyEvent.KEYCODE_ENTER -> "\n"
KeyEvent.KEYCODE_DEL -> null
KeyEvent.KEYCODE_UNKNOWN -> p0.characters
else -> StringUtils.newSingleCodePointString(p0.unicodeChar)
}
if (textToAdd != null) {
text = text.substring(0, selectionStart) + textToAdd + text.substring(selectionEnd)
selectionStart += textToAdd.length
selectionEnd = selectionStart
composingStart = -1
composingEnd = -1
}
return true
}
// implement only when necessary // implement only when necessary
override fun getCursorCapsMode(p0: Int): Int = TODO("Not yet implemented") override fun getCursorCapsMode(p0: Int): Int = TODO("Not yet implemented")
override fun getExtractedText(p0: ExtractedTextRequest?, p1: Int): ExtractedText = TODO("Not yet implemented") override fun getExtractedText(p0: ExtractedTextRequest?, p1: Int): ExtractedText = TODO("Not yet implemented")
@ -738,7 +812,6 @@ private val ic = object : InputConnection {
override fun commitCorrection(p0: CorrectionInfo?): Boolean = TODO("Not yet implemented") override fun commitCorrection(p0: CorrectionInfo?): Boolean = TODO("Not yet implemented")
override fun performEditorAction(p0: Int): Boolean = TODO("Not yet implemented") override fun performEditorAction(p0: Int): Boolean = TODO("Not yet implemented")
override fun performContextMenuAction(p0: Int): Boolean = TODO("Not yet implemented") override fun performContextMenuAction(p0: Int): Boolean = TODO("Not yet implemented")
override fun sendKeyEvent(p0: KeyEvent?): Boolean = TODO("Not yet implemented")
override fun clearMetaKeyStates(p0: Int): Boolean = TODO("Not yet implemented") override fun clearMetaKeyStates(p0: Int): Boolean = TODO("Not yet implemented")
override fun reportFullscreenMode(p0: Boolean): Boolean = TODO("Not yet implemented") override fun reportFullscreenMode(p0: Boolean): Boolean = TODO("Not yet implemented")
override fun performPrivateCommand(p0: String?, p1: Bundle?): Boolean = TODO("Not yet implemented") override fun performPrivateCommand(p0: String?, p1: Bundle?): Boolean = TODO("Not yet implemented")