mirror of
https://github.com/Helium314/HeliBoard.git
synced 2025-05-17 07:22:45 +00:00
fix some issues with url detection
This commit is contained in:
parent
3eba91e55c
commit
8abfe26411
5 changed files with 145 additions and 7 deletions
|
@ -36,6 +36,7 @@ import androidx.annotation.Nullable;
|
|||
|
||||
import org.dslul.openboard.inputmethod.latin.common.Constants;
|
||||
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.inputlogic.PrivateCommandPerformer;
|
||||
import org.dslul.openboard.inputmethod.latin.settings.SpacingAndPunctuations;
|
||||
|
@ -1001,10 +1002,18 @@ public final class RichInputConnection implements PrivateCommandPerformer {
|
|||
return StringUtils.lastPartLooksLikeURL(mCommittedTextBeforeComposingText);
|
||||
}
|
||||
|
||||
public boolean nonWordCodePointAndNoSpaceBeforeCursor(final SpacingAndPunctuations spacingAndPunctuations) {
|
||||
return StringUtilsKt.nonWordCodePointAndNoSpaceBeforeCursor(mCommittedTextBeforeComposingText, spacingAndPunctuations);
|
||||
}
|
||||
|
||||
public boolean spaceBeforeCursor() {
|
||||
return mCommittedTextBeforeComposingText.indexOf(" ") != -1;
|
||||
}
|
||||
|
||||
public boolean hasLetterBeforeLastSpaceBeforeCursor() {
|
||||
return StringUtilsKt.hasLetterBeforeLastSpaceBeforeCursor(mCommittedTextBeforeComposingText);
|
||||
}
|
||||
|
||||
public boolean wordBeforeCursorMayBeEmail() {
|
||||
return mCommittedTextBeforeComposingText.lastIndexOf(" ") < mCommittedTextBeforeComposingText.lastIndexOf("@");
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -849,7 +849,7 @@ public final class InputLogic {
|
|||
if (Character.getType(codePoint) == Character.OTHER_SYMBOL
|
||||
|| (sv.isWordSeparator(codePoint)
|
||||
&& (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
|
||||
)
|
||||
)
|
||||
|
@ -894,6 +894,8 @@ public final class InputLogic {
|
|||
&& settingsValues.mSpacingAndPunctuations.isSometimesWordConnector(mConnection.getCodePointBeforeCursor())
|
||||
// but not if there are two consecutive sometimesWordConnectors (e.g. "...bla")
|
||||
&& !settingsValues.mSpacingAndPunctuations.isSometimesWordConnector(mConnection.getCharBeforeBeforeCursor())
|
||||
// and not if there is no letter before the separator
|
||||
&& mConnection.hasLetterBeforeLastSpaceBeforeCursor()
|
||||
) {
|
||||
final CharSequence text = mConnection.textBeforeCursorUntilLastWhitespaceOrDoubleSlash();
|
||||
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) {
|
||||
if (settingsValues.shouldInsertSpacesAutomatically()
|
||||
&& 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.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();
|
||||
// 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
|
||||
// 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;
|
||||
// already contains a SometimesWordConnector -> may be URL (not so sure, only do with detection enabled
|
||||
if (settingsValues.mUrlDetectionEnabled && settingsValues.mSpacingAndPunctuations.containsSometimesWordConnector(mWordComposer.getTypedWord()))
|
||||
|
|
|
@ -26,3 +26,5 @@
|
|||
# TODO: remove once used in code.
|
||||
-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
|
||||
|
|
|
@ -12,9 +12,11 @@ import org.dslul.openboard.inputmethod.event.Event
|
|||
import org.dslul.openboard.inputmethod.keyboard.KeyboardSwitcher
|
||||
import org.dslul.openboard.inputmethod.keyboard.MainKeyboardView
|
||||
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.StringUtils
|
||||
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.utils.DeviceProtectedUtils
|
||||
import org.dslul.openboard.inputmethod.latin.utils.ScriptUtils
|
||||
|
@ -48,6 +50,8 @@ class InputLogicTest {
|
|||
private val connection: RichInputConnection get() = inputLogic.mConnection
|
||||
private val composerReader = InputLogic::class.java.getDeclaredField("mWordComposer").apply { isAccessible = true }
|
||||
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 connectionTextBeforeComposingText get() = (beforeComposingReader.get(connection) as CharSequence).toString()
|
||||
private val composingReader = RichInputConnection::class.java.getDeclaredField("mComposingText").apply { isAccessible = true }
|
||||
|
@ -425,6 +429,38 @@ class InputLogicTest {
|
|||
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 ---------
|
||||
|
||||
// should be called before every test, so the same state is guaranteed
|
||||
|
@ -451,12 +487,15 @@ class InputLogicTest {
|
|||
val oldBefore = textBeforeCursor
|
||||
val oldAfter = textAfterCursor
|
||||
val insert = StringUtils.newSingleCodePointString(codePoint)
|
||||
val phantomSpaceToInsert = if (spaceState == SpaceState.PHANTOM) " " else ""
|
||||
|
||||
latinIME.onEvent(Event.createEventForCodePointFromUnknownSource(codePoint))
|
||||
handleMessages()
|
||||
|
||||
if (!settingsValues.mAutospaceAfterPunctuationEnabled)
|
||||
assertEquals(oldBefore + insert, textBeforeCursor)
|
||||
if (phantomSpaceToInsert.isEmpty())
|
||||
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(textBeforeCursor + textAfterCursor, getText())
|
||||
checkConnectionConsistency()
|
||||
|
@ -548,6 +587,13 @@ class InputLogicTest {
|
|||
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() {
|
||||
// RichInputConnection only has composing text up to cursor, but InputConnection has full composing text
|
||||
val expectedConnectionComposingText = if (composingStart == -1 || composingEnd == -1) ""
|
||||
|
@ -730,6 +776,34 @@ private val ic = object : InputConnection {
|
|||
selectionEnd -= beforeLength
|
||||
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
|
||||
override fun getCursorCapsMode(p0: Int): Int = 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 performEditorAction(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 reportFullscreenMode(p0: Boolean): Boolean = TODO("Not yet implemented")
|
||||
override fun performPrivateCommand(p0: String?, p1: Bundle?): Boolean = TODO("Not yet implemented")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue