Skip to content
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,6 @@ Emojis for the following are chosen based on [gitmoji](https://gitmoji.dev/).
### ♻️ Code Refactoring

- Code quality improvements were continuously done to assure that the application is easy to maintain and meets Kotlin standards ([#426](https://github.com/scribe-org/Scribe-Android/issues/426)).
- `KeyboardLayoutHandler` was extracted from `GeneralKeyboardIME` to encapsulate layout XML resolution, symbol keyboard mapping, and width calculations ([#426](https://github.com/scribe-org/Scribe-Android/issues/426)).
- `ShiftHandler` was extracted from `GeneralKeyboardIME` to encapsulate shift state machine toggling, double-tap caps lock lock timing, and keyboard layout mode switching ([#426](https://github.com/scribe-org/Scribe-Android/issues/426)).
- Removing non-null assertions (!!) from the codebase.
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ package be.scri.services

import android.view.inputmethod.InputConnection
import androidx.test.ext.junit.runners.AndroidJUnit4
import be.scri.models.ScribeState
import io.mockk.every
import io.mockk.mockk
import org.junit.Assert.assertFalse
Expand Down Expand Up @@ -62,4 +63,14 @@ class GeneralKeyboardIMETest {
every { ime.currentInputConnection } returns null
assertFalse(ime.hasTextBeforeCursor)
}

@Test
fun commitText_inConjugationState_handlesNullConjugateOutput_withoutCrashing() {
// Arrange: state is SELECT_VERB_CONJUNCTION, but conjugateOutput is null
ime.currentState = ScribeState.SELECT_VERB_CONJUNCTION
ime.conjugateOutput = null

// Act & Assert: Should complete gracefully without NullPointerException
ime.commitText("test")
}
}
5 changes: 5 additions & 0 deletions app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,9 @@ interface KeyboardIMEContext {
val keyboardSymbols: Int
val keyboardSymbolShift: Int

val isFloatingMode: Boolean
val enterKeyType: Int

var lastShiftPressTS: Long

val currentCommandBarHint: String
Expand Down Expand Up @@ -79,6 +82,8 @@ interface KeyboardIMEContext {

fun getKeyboardLayoutXML(): Int

fun isNumericKeyboardActive(): Boolean

fun handleDelete(isLongPress: Boolean = false)

fun isDeleteRepeating(): Boolean
Expand Down
138 changes: 138 additions & 0 deletions app/src/keyboards/java/be/scri/helpers/KeyboardLayoutHandler.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
// SPDX-License-Identifier: GPL-3.0-or-later

package be.scri.helpers

import android.text.InputType.TYPE_CLASS_DATETIME
import android.text.InputType.TYPE_CLASS_NUMBER
import android.text.InputType.TYPE_CLASS_PHONE
import android.text.InputType.TYPE_MASK_CLASS
import be.scri.R
import be.scri.models.ScribeState

private const val DATA_SIZE_2 = 2
private const val DATA_SIZE_3 = 3

/**
* Encapsulates keyboard XML layout resolution, symbol layout mapping,
* keyboard width calculations, state-based layout XML selection, and view re-creation.
*/
class KeyboardLayoutHandler(
private val ime: KeyboardIMEContext,
) {
/**
* Resolves the XML resource ID for the active keyboard layout.
*
* @return The XML layout resource ID.
*/
fun getCurrentKeyboardLayoutXML(): Int =
when (ime.keyboardMode) {
ime.keyboardSymbols -> getPrimarySymbolKeyboardLayoutXML()
ime.keyboardSymbolShift -> R.xml.keys_symbols_shift
else -> ime.getKeyboardLayoutXML()
}

/**
* Resolves the primary symbol or numeric layout XML resource ID.
*
* @return The XML layout resource ID.
*/
fun getPrimarySymbolKeyboardLayoutXML(): Int =
if (ime.isNumericKeyboardActive()) {
R.xml.keys_numeric
} else {
R.xml.keys_symbols
}

/**
* Determines which keyboard layout XML to use based on the current [ScribeState].
*
* @param state The current state of the Scribe keyboard.
* @param isSubsequentArea true if this is for a secondary conjugation view.
* @param dataSize The number of items to display, used to select an appropriate layout.
* @return The resource ID of the keyboard layout XML.
*/
fun getKeyboardLayoutForState(
state: ScribeState,
isSubsequentArea: Boolean = false,
dataSize: Int = 0,
): Int =
when (state) {
ScribeState.SELECT_VERB_CONJUNCTION -> {
ime.saveConjugateModeType(ime.language)
if (!isSubsequentArea && dataSize == 0) {
ime.defaultConjugateLayoutXML
} else {
when (dataSize) {
DATA_SIZE_2 -> R.xml.conjugate_view_2x1
DATA_SIZE_3 -> R.xml.conjugate_view_1x3
else -> R.xml.conjugate_view_2x2
}
}
}

else -> {
ime.getKeyboardLayoutXML()
}
}

/**
* Calculates the width of the keyboard container.
*
* @return The keyboard width in pixels.
*/
fun getKeyboardWidth(): Int =
if (ime.isFloatingMode) {
val density = ime.getImeResources().displayMetrics.density
val screenWidth = ime.getImeResources().displayMetrics.widthPixels
val floatWidth = (320f * density).toInt()
Math.min(floatWidth, (screenWidth * 0.85f).toInt())
} else {
ime.getImeResources().displayMetrics.widthPixels
}

/**
* Re-instantiates the [KeyboardBase] and applies the updated shift state and layout.
*/
fun recreateKeyboard() {
if (!ime.isUiManagerInitialized) return

val xmlId = getCurrentKeyboardLayoutXML()
val currentShiftState = ime.keyboard?.mShiftState ?: SHIFT_OFF
ime.keyboard =
KeyboardBase(ime.imeContext, xmlId, ime.enterKeyType, getKeyboardWidth())
.also {
it.setShifted(currentShiftState)
ime.keyboardView?.setKeyboard(it)
}

if (xmlId == R.xml.keys_symbols) {
ime.uiManager.setupCurrencySymbol(ime.language)
}
ime.keyboardView?.invalidateAllKeys()
}

companion object {
/**
* Returns true if the given [inputType] requires a numeric keyboard layout.
*/
fun shouldUseNumericKeyboard(inputType: Int): Boolean =
when (inputType and TYPE_MASK_CLASS) {
TYPE_CLASS_NUMBER, TYPE_CLASS_DATETIME, TYPE_CLASS_PHONE -> true
else -> false
}

/**
* Returns the appropriate keyboard layout XML resource ID for the given [inputType].
* Falls back to [letterKeyboardLayoutXML] when a numeric layout is not required.
*/
fun getKeyboardLayoutXMLForInputType(
inputType: Int,
letterKeyboardLayoutXML: Int,
): Int =
if (shouldUseNumericKeyboard(inputType)) {
R.xml.keys_numeric
} else {
letterKeyboardLayoutXML
}
}
}
26 changes: 16 additions & 10 deletions app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ class ShiftHandler(
ime.keyboardMode = ime.keyboardSymbols
ime.getPrimarySymbolKeyboardLayoutXML()
}
ime.keyboard = KeyboardBase(ime, keyboardXml, ime.enterKeyType, ime.getKeyboardWidth())
keyboardView?.setKeyboard(ime.keyboard!!)
ime.keyboard =
KeyboardBase(ime, keyboardXml, ime.enterKeyType, ime.getKeyboardWidth())
.also {
keyboardView?.setKeyboard(it)
}
if (keyboardXml == R.xml.keys_symbols) {
handleModeChange(keyboardMode, keyboardView, ime)
}
Expand All @@ -83,14 +86,17 @@ class ShiftHandler(
ime.keyboardMode = ime.keyboardLetters
ime.getKeyboardLayoutXML()
}
ime.keyboard = KeyboardBase(context, keyboardXml, ime.enterKeyType, ime.getKeyboardWidth())
if (ime.keyboardMode == ime.keyboardLetters) {
val wasShifted = ime.keyboard?.mShiftState == SHIFT_ON_ONE_CHAR || ime.keyboard?.mShiftState == SHIFT_ON_PERMANENT
if (wasShifted) {
ime.keyboard?.setShifted(ime.keyboard?.mShiftState ?: SHIFT_OFF)
}
}
keyboardView?.setKeyboard(ime.keyboard!!)
ime.keyboard =
KeyboardBase(context, keyboardXml, ime.enterKeyType, ime.getKeyboardWidth())
.also {
if (ime.keyboardMode == ime.keyboardLetters) {
val wasShifted = it.mShiftState == SHIFT_ON_ONE_CHAR || ime.keyboard?.mShiftState == SHIFT_ON_PERMANENT
if (wasShifted) {
it.setShifted(it.mShiftState)
}
}
keyboardView?.setKeyboard(it)
}
keyboardView?.invalidateAllKeys()
if (keyboardXml == R.xml.keys_symbols) {
ime.uiManager.setupCurrencySymbol(ime.language)
Expand Down
17 changes: 10 additions & 7 deletions app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt
Original file line number Diff line number Diff line change
Expand Up @@ -653,8 +653,11 @@ class KeyboardUIManager(
fun initializeKeyboard(xmlId: Int) {
val enterKeyType = listener.getCurrentEnterKeyType()
val width = listener.getKeyboardWidth()
keyboard = KeyboardBase(context, xmlId, enterKeyType, width)
keyboardView.setKeyboard(keyboard!!)
keyboard =
KeyboardBase(context, xmlId, enterKeyType, width)
.also {
keyboardView.setKeyboard(it)
}
keyboardView.mOnKeyboardActionListener = listener.onKeyboardActionListener()
keyboardView.requestLayout()
}
Expand Down Expand Up @@ -1213,11 +1216,11 @@ class KeyboardUIManager(
binding.separator2.visibility = View.GONE
binding.separator3.visibility = View.GONE

binding.emojiBtnPhone1?.visibility = View.GONE
binding.emojiBtnPhone2?.visibility = View.GONE
binding.emojiBtnTablet1?.visibility = View.GONE
binding.emojiBtnTablet2?.visibility = View.GONE
binding.emojiBtnTablet3?.visibility = View.GONE
binding.emojiBtnPhone1.visibility = View.GONE
binding.emojiBtnPhone2.visibility = View.GONE
binding.emojiBtnTablet1.visibility = View.GONE
binding.emojiBtnTablet2.visibility = View.GONE
binding.emojiBtnTablet3.visibility = View.GONE
binding.separator4.visibility = View.GONE
binding.separator5.visibility = View.GONE
binding.separator6.visibility = View.GONE
Expand Down
Loading
Loading