From 5890d4a398a18feca4f809f2a0f030ce5e5421a1 Mon Sep 17 00:00:00 2001 From: Chris Witt Date: Fri, 2 Oct 2026 17:37:25 -0400 Subject: [PATCH 1/8] CW - Cleaning up non-null assertions. --- .../scri/services/GeneralKeyboardIMETest.kt | 11 + .../java/be/scri/helpers/ShiftHandler.kt | 26 +- .../be/scri/helpers/ui/KeyboardUIManager.kt | 17 +- .../be/scri/services/GeneralKeyboardIME.kt | 27 +- .../main/java/be/scri/helpers/KeyboardBase.kt | 28 +- .../main/java/be/scri/views/KeyboardView.kt | 413 ++++++++++-------- 6 files changed, 300 insertions(+), 222 deletions(-) diff --git a/app/src/androidTestKeyboards/kotlin/be/scri/services/GeneralKeyboardIMETest.kt b/app/src/androidTestKeyboards/kotlin/be/scri/services/GeneralKeyboardIMETest.kt index b75741f10..40fd103cf 100644 --- a/app/src/androidTestKeyboards/kotlin/be/scri/services/GeneralKeyboardIMETest.kt +++ b/app/src/androidTestKeyboards/kotlin/be/scri/services/GeneralKeyboardIMETest.kt @@ -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 @@ -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") + } } diff --git a/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt b/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt index 28f22141f..775f1e658 100644 --- a/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt @@ -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) } @@ -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) diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt index ff0656b56..d71e41379 100644 --- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt +++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt @@ -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() } @@ -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 diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index ca86994de..e94aa079d 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -331,7 +331,7 @@ abstract class GeneralKeyboardIME( setVibrate = getIsVibrateEnabled(applicationContext, language) setSound = getIsSoundEnabled(applicationContext, language) setHoldForAltCharacters = getHoldKeyStyle(applicationContext, language) - setKeyboard(this@GeneralKeyboardIME.keyboard!!) + this@GeneralKeyboardIME.keyboard?.let { setKeyboard(it) } mOnKeyboardActionListener = this@GeneralKeyboardIME } @@ -451,8 +451,9 @@ abstract class GeneralKeyboardIME( loadLanguageData() - keyboard = KeyboardBase(this, keyboardXml, enterKeyType, getKeyboardWidth()) - keyboardView?.setKeyboard(keyboard!!) + keyboard = + KeyboardBase(this, keyboardXml, enterKeyType, getKeyboardWidth()) + .also { keyboardView?.setKeyboard(it) } if (this::uiManager.isInitialized && keyboardXml == R.xml.keys_symbols) { uiManager.setupCurrencySymbol(language) @@ -575,7 +576,9 @@ abstract class GeneralKeyboardIME( keyboard?.setShifted(SHIFT_ON_ONE_CHAR) } } - keyboardView!!.setKeyboard(keyboard!!) + keyboard?.let { + keyboardView?.setKeyboard(it) + } switchToLetters = false } } @@ -789,9 +792,10 @@ abstract class GeneralKeyboardIME( override fun commitText(text: String) { if (currentState == ScribeState.SELECT_VERB_CONJUNCTION) { val label = text.trim() - val conjugateIndex = uiManager.getValidatedConjugateIndex(conjugateOutput) - val title = conjugateOutput?.keys?.elementAtOrNull(conjugateIndex) - val languageOutput = title?.let { conjugateOutput!![it] } + val output = conjugateOutput + val conjugateIndex = uiManager.getValidatedConjugateIndex(output) + val title = output?.keys?.elementAtOrNull(conjugateIndex) + val languageOutput = title?.let { output[it] } val matchingEntry = languageOutput?.entries?.find { (_, values) -> @@ -1248,9 +1252,12 @@ abstract class GeneralKeyboardIME( if (!this::uiManager.isInitialized) return val xmlId = getCurrentKeyboardLayoutXML() val currentShiftState = keyboard?.mShiftState ?: SHIFT_OFF - keyboard = KeyboardBase(this, xmlId, enterKeyType, getKeyboardWidth()) - keyboard?.setShifted(currentShiftState) - keyboardView?.setKeyboard(keyboard!!) + keyboard = + KeyboardBase(this, xmlId, enterKeyType, getKeyboardWidth()) + .also { + it.setShifted(currentShiftState) + keyboardView?.setKeyboard(it) + } if (xmlId == R.xml.keys_symbols) { uiManager.setupCurrencySymbol(language) diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt index 354368f71..f30fb40b1 100644 --- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt +++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt @@ -359,8 +359,12 @@ class KeyboardBase { popupResId = a.getResourceId(R.styleable.KeyboardBase_Key_popupKeyboard, 0) repeatable = a.getBoolean(R.styleable.KeyboardBase_Key_isRepeatable, false) edgeFlags = a.getInt(R.styleable.KeyboardBase_Key_keyEdgeFlags, 0) - icon = a.getDrawable(R.styleable.KeyboardBase_Key_keyIcon) - icon?.setBounds(0, 0, icon!!.intrinsicWidth, icon!!.intrinsicHeight) + icon = + a + .getDrawable(R.styleable.KeyboardBase_Key_keyIcon) + .also { + it?.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight) + } label = a.getText(R.styleable.KeyboardBase_Key_keyLabel) ?: "" topSmallNumber = a.getString(R.styleable.KeyboardBase_Key_topSmallNumber) ?: "" @@ -448,7 +452,7 @@ class KeyboardBase { row.defaultWidth = keyWidth row.defaultHorizontalGap = mDefaultHorizontalGap - characters.forEachIndexed { index, character -> + characters.forEachIndexed { _, character -> val key = Key(row) if (column >= MAX_KEYS_PER_MINI_ROW) { column = 0 @@ -464,7 +468,7 @@ class KeyboardBase { key.code = character.code column++ x += key.width + key.gap - mKeys!!.add(key) + mKeys?.add(key) row.mKeys.add(key) if (x > mMinWidth) { mMinWidth = x @@ -585,7 +589,11 @@ class KeyboardBase { TAG_KEY -> { inKey = true - key = createKeyFromXml(res, currentRow!!, x, y, parser) + if (currentRow == null) { + Log.e("KeyboardBase", "Key element found outside of a Row tag at line ${parser.lineNumber}") + continue + } + key = createKeyFromXml(res, currentRow, x, y, parser) if (hideComma && key.code == ','.code) { widthToRedistribute = key.width + key.gap @@ -595,7 +603,7 @@ class KeyboardBase { key.gap = 0 } - mKeys!!.add(key) + mKeys?.add(key) if (key.code == KEYCODE_ENTER) { val enterResourceId = when (mEnterKeyType) { @@ -628,13 +636,17 @@ class KeyboardBase { } else if (event == XmlResourceParser.END_TAG) { if (inKey) { inKey = false - x += key!!.gap + key.width + key?.run { + x += gap + width + } if (x > mMinWidth) { mMinWidth = x } } else if (inRow) { inRow = false - y += currentRow!!.defaultHeight + currentRow?.run { + y += defaultHeight + } row++ } } diff --git a/app/src/main/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt index b2cdaa57f..1a2c9f215 100644 --- a/app/src/main/java/be/scri/views/KeyboardView.kt +++ b/app/src/main/java/be/scri/views/KeyboardView.kt @@ -52,7 +52,6 @@ import be.scri.extensions.performSoundFeedback import be.scri.helpers.KeyboardBase import be.scri.helpers.KeyboardBase.Companion.KEYCODE_CAPS_LOCK import be.scri.helpers.KeyboardBase.Companion.KEYCODE_DELETE -import be.scri.helpers.KeyboardBase.Companion.KEYCODE_EMOJI import be.scri.helpers.KeyboardBase.Companion.KEYCODE_ENTER import be.scri.helpers.KeyboardBase.Companion.KEYCODE_LEFT_ARROW import be.scri.helpers.KeyboardBase.Companion.KEYCODE_MODE_CHANGE @@ -63,13 +62,13 @@ import be.scri.helpers.KeyboardBase.Companion.KEYCODE_TAB import be.scri.helpers.KeyboardBase.Companion.SHIFT_LOCKED import be.scri.helpers.KeyboardBase.MyCustomActions import be.scri.helpers.MAX_KEYS_PER_MINI_ROW -import be.scri.helpers.PreferencesHelper import be.scri.helpers.SHIFT_OFF import be.scri.helpers.SHIFT_ON_ONE_CHAR import be.scri.helpers.SHIFT_ON_PERMANENT import be.scri.models.ScribeState import java.util.Arrays import java.util.Locale +import kotlin.ranges.coerceIn /** * The base keyboard view for Scribe language keyboards application. @@ -245,8 +244,6 @@ class KeyboardView private var mEnterKeyColor: Int = 0 - private var mSpecialKeyColor: Int? = null - private var mKeyBackground: Drawable? = null private var mToolbarHolder: View? = null @@ -493,10 +490,14 @@ class KeyboardView private var keyboardBindingInternal: KeyboardViewKeyboardBinding? = null val keyboardBinding: KeyboardViewKeyboardBinding get() { - if (keyboardBindingInternal == null) { - keyboardBindingInternal = KeyboardViewKeyboardBinding.inflate(LayoutInflater.from(context)) + val existing = keyboardBindingInternal + if (existing != null) { + return existing } - return keyboardBindingInternal!! + + val binding = KeyboardViewKeyboardBinding.inflate(LayoutInflater.from(context)) + keyboardBindingInternal = binding + return binding } init { @@ -559,7 +560,10 @@ class KeyboardView object : Handler() { override fun handleMessage(msg: Message) { when (msg.what) { - MSG_REMOVE_PREVIEW -> mPreviewText!!.visibility = INVISIBLE + MSG_REMOVE_PREVIEW -> { + val previewText = mPreviewText + previewText?.visibility = INVISIBLE + } MSG_REPEAT -> if (repeatKey(false)) { val repeat = Message.obtain(this, MSG_REPEAT) @@ -651,8 +655,8 @@ class KeyboardView removeMessages() mKeyboard = keyboard - val keys = mKeyboard!!.mKeys - mKeys = keys!!.toMutableList() as ArrayList + val keys = mKeyboard?.mKeys + mKeys = keys?.toMutableList() as ArrayList requestLayout() mKeyboardChanged = true invalidateAllKeys() @@ -739,14 +743,15 @@ class KeyboardView if (mKeyboard == null) { setMeasuredDimension(0, 0) } else { - var width = mKeyboard!!.mMinWidth + var width = mKeyboard?.mMinWidth ?: 0 if (MeasureSpec.getSize(widthMeasureSpec) < width + MARGIN_ADJUSTMENT) { width = MeasureSpec.getSize(widthMeasureSpec) } val extraBottomPaddingPx = (resources.displayMetrics.density * 10).toInt() - setMeasuredDimension(width, mKeyboard!!.mHeight + extraBottomPaddingPx) + val height = mKeyboard?.mHeight ?: 0 + setMeasuredDimension(width, height + extraBottomPaddingPx) } } @@ -794,7 +799,9 @@ class KeyboardView if (mDrawPending || mBuffer == null || mKeyboardChanged) { onBufferDraw() } - canvas.drawBitmap(mBuffer!!, 0f, 0f, null) + mBuffer?.let { + canvas.drawBitmap(it, 0f, 0f, null) + } } @SuppressLint("UseCompatLoadingForDrawables") @@ -807,8 +814,7 @@ class KeyboardView // Make sure our bitmap is at least 1x1. val width = 1.coerceAtLeast(width) val height = 1.coerceAtLeast(height) - mBuffer = createBitmap(width, height) - mCanvas = Canvas(mBuffer!!) + mBuffer = createBitmap(width, height).also { mCanvas = Canvas(it) } } invalidateAllKeys() mKeyboardChanged = false @@ -818,9 +824,10 @@ class KeyboardView return } - mCanvas!!.withSave { - val canvas = mCanvas - canvas!!.clipRect(mDirtyRect) + val canvas = mCanvas ?: return + + canvas.withSave { + clipRect(mDirtyRect) val paint = mPaint val keys = mKeys val isUserDarkMode = @@ -844,12 +851,6 @@ class KeyboardView } else { Color.WHITE } - mSpecialKeyColor = - if (isUserDarkMode) { - R.color.special_key_dark - } else { - R.color.special_key_light - } val pressedColorResId = if (isUserDarkMode) { R.color.dark_key_press_color @@ -857,7 +858,13 @@ class KeyboardView R.color.light_key_press_color } val pressedColor = resources.getColor(pressedColorResId, context.theme) - val specialKeyColorValue = resources.getColor(mSpecialKeyColor!!, context.theme) + val specialKeyColorResource = + if (isUserDarkMode) { + R.color.special_key_dark + } else { + R.color.special_key_light + } + val specialKeyColorValue = resources.getColor(specialKeyColorResource, context.theme) val focusedColorResId = if (isUserDarkMode) { R.color.theme_scribe_blue @@ -890,9 +897,9 @@ class KeyboardView if (isUserDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color, context.theme, ) - canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) if (id != R.id.mini_keyboard_view) { - canvas.drawColor(mKeyboardBackgroundColor) + drawColor(mKeyboardBackgroundColor) } val keyCount = keys.size @@ -952,21 +959,21 @@ class KeyboardView (key.y + key.height - vKeyMargin + shadowOffset - padding).toFloat(), ) if (code != EXTRA_PADDING && (mPopupParent.id != R.id.mini_keyboard_view)) { - canvas.drawRoundRect(shadowRect, rectRadius, rectRadius, shadowPaint) + drawRoundRect(shadowRect, rectRadius, rectRadius, shadowPaint) } val backgroundColor = when { key.focused -> focusedColor key.pressed -> pressedColor - code == KEYCODE_SHIFT && mKeyboard!!.mShiftState == SHIFT_LOCKED -> pressedColor + code == KEYCODE_SHIFT && mKeyboard?.mShiftState == SHIFT_LOCKED -> pressedColor code in listOf(KEYCODE_DELETE, KEYCODE_SHIFT, KEYCODE_MODE_CHANGE) -> specialKeyColorValue code == KEYCODE_ENTER -> mEnterKeyColor else -> keyBackgroundColor } keyBackgroundPaint.color = backgroundColor if (code != EXTRA_PADDING) { - canvas.drawRoundRect(keyRect, rectRadius, rectRadius, keyBackgroundPaint) + drawRoundRect(keyRect, rectRadius, rectRadius, keyBackgroundPaint) } var label = adjustCase(key.label)?.toString() // Switch the character to uppercase if shift is pressed. @@ -1056,7 +1063,7 @@ class KeyboardView } } - canvas.translate(key.x.toFloat(), key.y.toFloat()) + translate(key.x.toFloat(), key.y.toFloat()) if (label?.isNotEmpty() == true) { // For characters, use large font. For labels like "Done", use small font. if (label.length > 1) { @@ -1075,7 +1082,7 @@ class KeyboardView else -> mTextColor } - canvas.drawText( + drawText( label, (key.width / 2).toFloat(), key.height / 2 + (paint.textSize - paint.descent()) / 2, @@ -1083,7 +1090,7 @@ class KeyboardView ) if (key.topSmallNumber.isNotEmpty()) { - canvas.drawText( + drawText( key.topSmallNumber, key.width - mTopSmallNumberMarginWidth - leftShiftForLabel, mTopSmallNumberMarginHeight, @@ -1096,7 +1103,7 @@ class KeyboardView } else if (key.icon != null && mKeyboard != null) { if (code == KEYCODE_SHIFT) { val drawableId = - when (mKeyboard!!.mShiftState) { + when (mKeyboard?.mShiftState) { SHIFT_OFF -> R.drawable.ic_caps_outline_vector SHIFT_ON_ONE_CHAR -> R.drawable.ic_caps_vector SHIFT_LOCKED -> R.drawable.ic_caps_underlined_vector @@ -1105,12 +1112,16 @@ class KeyboardView key.icon = resources.getDrawable(drawableId, context.theme) } else if (code == KEYCODE_CAPS_LOCK) { val drawableId = - when (mKeyboard!!.mShiftState) { + when (mKeyboard?.mShiftState) { SHIFT_LOCKED -> R.drawable.ic_caps_lock_on else -> R.drawable.ic_caps_lock_off } - key.icon = resources.getDrawable(drawableId, context.theme) - key.icon!!.applyColorFilter(mTextColor) + key.icon = + resources + .getDrawable(drawableId, context.theme) + .also { + it.applyColorFilter(mTextColor) + } } if (code == KEYCODE_LEFT_ARROW || code == KEYCODE_RIGHT_ARROW) { @@ -1121,14 +1132,18 @@ class KeyboardView else -> null } drawableId?.let { - key.icon = resources.getDrawable(it, context.theme) - key.icon!!.applyColorFilter(mTextColor) + key.icon = + resources + .getDrawable(it, context.theme) + .also { icon -> + icon.applyColorFilter(mTextColor) + } } } if (code == KEYCODE_ENTER) { val drawableId = - when (mKeyboard!!.mEnterKeyType) { + when (mKeyboard?.mEnterKeyType) { EditorInfo.IME_ACTION_SEARCH -> R.drawable.ic_search_vector @@ -1146,13 +1161,17 @@ class KeyboardView else -> R.drawable.ic_enter_vector } - key.icon = resources.getDrawable(drawableId) - key.icon!!.applyColorFilter(mTextColor) + key.icon = + resources + .getDrawable(drawableId) + .also { + it.applyColorFilter(mTextColor) + } } else { if (code == KeyboardBase.KEYCODE_FLOAT_TOGGLE) { val isFloating = (context as? KeyboardBase.KeyboardContextProvider)?.isFloatingModeActive() == true || - (mPopupParent?.context as? KeyboardBase.KeyboardContextProvider)?.isFloatingModeActive() == true + (mPopupParent.context as? KeyboardBase.KeyboardContextProvider)?.isFloatingModeActive() == true val floatIconRes = if (isFloating) { R.drawable.ic_keyboard_dismiss @@ -1169,54 +1188,54 @@ class KeyboardView code == KeyboardBase.KEYCODE_FLOAT_TOGGLE || code == KeyboardBase.KEYCODE_EMOJI if (isIconOnlyKey) { - key.icon!!.applyColorFilter(mTextColor) + key.icon?.applyColorFilter(mTextColor) } } // Controls where icons are located on their keys. - var iconWidth = key.icon!!.intrinsicWidth - var iconHeight = key.icon!!.intrinsicHeight - val isEmojiOrClipboard = - code == KeyboardBase.KEYCODE_EMOJI || - code == KeyboardBase.KEYCODE_CLIPBOARD || - code == KeyboardBase.KEYCODE_FLOAT_TOGGLE - val scaleFactor = if (isEmojiOrClipboard) 0.5f else 0.6f - val maxIconWidth = (key.width * scaleFactor).toInt() - val maxIconHeight = (key.height * scaleFactor).toInt() - if (iconWidth > maxIconWidth || iconHeight > maxIconHeight) { - val ratio = iconWidth.toFloat() / iconHeight.toFloat() - if (ratio > 1) { - iconWidth = maxIconWidth - iconHeight = (maxIconWidth / ratio).toInt() - } else { - iconHeight = maxIconHeight - iconWidth = (maxIconHeight * ratio).toInt() + key.icon?.let { icon -> + var iconWidth = icon.intrinsicWidth + var iconHeight = icon.intrinsicHeight + val isEmojiOrClipboard = + code == KeyboardBase.KEYCODE_EMOJI || + code == KeyboardBase.KEYCODE_CLIPBOARD || + code == KeyboardBase.KEYCODE_FLOAT_TOGGLE + val scaleFactor = if (isEmojiOrClipboard) 0.5f else 0.6f + val maxIconWidth = (key.width * scaleFactor).toInt() + val maxIconHeight = (key.height * scaleFactor).toInt() + if (iconWidth > maxIconWidth || iconHeight > maxIconHeight) { + val ratio = iconWidth.toFloat() / iconHeight.toFloat() + if (ratio > 1) { + iconWidth = maxIconWidth + iconHeight = (maxIconWidth / ratio).toInt() + } else { + iconHeight = maxIconHeight + iconWidth = (maxIconHeight * ratio).toInt() + } + } + val drawableX = (key.width - iconWidth) / 2 + val drawableY = (key.height - iconHeight) / 2 + translate(drawableX.toFloat(), drawableY.toFloat()) + icon.setBounds(0, 0, iconWidth, iconHeight) + icon.draw(this) + translate(-drawableX.toFloat(), -drawableY.toFloat()) + + if (code == KeyboardBase.KEYCODE_EMOJI && id != R.id.mini_keyboard_view) { + val settingsIcon = resources.getDrawable(R.drawable.ic_settings_cog_vector, context.theme) + settingsIcon.applyColorFilter(mTextColor) + val density = context.resources.displayMetrics.density + val cogSize = (12 * density).toInt() + val rightPadding = keyMargin - shadowOffset + padding + (2 * density).toInt() + val topPadding = keyMargin - shadowOffset + padding + (2 * density).toInt() + val cogX = key.width - cogSize - rightPadding + val cogY = topPadding + settingsIcon.setBounds(cogX, cogY, cogX + cogSize, cogY + cogSize) + settingsIcon.draw(this) } - } - val drawableX = (key.width - iconWidth) / 2 - val drawableY = (key.height - iconHeight) / 2 - canvas.translate(drawableX.toFloat(), drawableY.toFloat()) - key.icon!!.setBounds(0, 0, iconWidth, iconHeight) - key.icon!!.draw(canvas) - canvas.translate(-drawableX.toFloat(), -drawableY.toFloat()) - - if (code == KeyboardBase.KEYCODE_EMOJI && id != R.id.mini_keyboard_view) { - val settingsIcon = resources.getDrawable(R.drawable.ic_settings_cog_vector, context.theme) - settingsIcon.applyColorFilter(mTextColor) - val density = context.resources.displayMetrics.density - val cogSize = (12 * density).toInt() - val rightPadding = keyMargin - shadowOffset + padding + (2 * density).toInt() - val topPadding = keyMargin - shadowOffset + padding + (2 * density).toInt() - val cogX = key.width - cogSize - rightPadding - val cogY = topPadding - settingsIcon.setBounds(cogX, cogY, cogX + cogSize, cogY + cogSize) - settingsIcon.draw(canvas) } } - canvas.translate(-key.x.toFloat(), -key.y.toFloat()) + translate(-key.x.toFloat(), -key.y.toFloat()) } - - mCanvas!! } mDrawPending = false mDirtyRect.setEmpty() @@ -1239,7 +1258,7 @@ class KeyboardView if (index != NOT_A_KEY && index < mKeys.size) { val key = mKeys[index] getPressedKeyIndex(x, y) - mOnKeyboardActionListener!!.onKey(key.code) + mOnKeyboardActionListener?.onKey(key.code) mLastTapTime = eventTime } } @@ -1281,10 +1300,12 @@ class KeyboardView if (oldKeyIndex != mCurrentKeyIndex) { if (previewPopup.isShowing) { if (keyIndex == NOT_A_KEY) { - mHandler!!.sendMessageDelayed( - mHandler!!.obtainMessage(MSG_REMOVE_PREVIEW), - DELAY_AFTER_PREVIEW.toLong(), - ) + mHandler?.let { + it.sendMessageDelayed( + it.obtainMessage(MSG_REMOVE_PREVIEW), + DELAY_AFTER_PREVIEW.toLong(), + ) + } } } @@ -1296,6 +1317,7 @@ class KeyboardView private fun showKey(keyIndex: Int) { val previewPopup = mPreviewPopup + val previewText = mPreviewText ?: return val keys = mKeys if (keyIndex < 0 || keyIndex >= mKeys.size) { return @@ -1303,20 +1325,20 @@ class KeyboardView val key = keys[keyIndex] if (key.icon != null) { - mPreviewText!!.setCompoundDrawables(null, null, null, key.icon) + previewText.setCompoundDrawables(null, null, null, key.icon) } else { if (key.label.length > 1) { - mPreviewText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize.toFloat()) - mPreviewText!!.typeface = Typeface.DEFAULT_BOLD + previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize.toFloat()) + previewText.typeface = Typeface.DEFAULT_BOLD } else { - mPreviewText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge.toFloat()) - mPreviewText!!.typeface = Typeface.DEFAULT + previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge.toFloat()) + previewText.typeface = Typeface.DEFAULT } - mPreviewText!!.setCompoundDrawables(null, null, null, null) + previewText.setCompoundDrawables(null, null, null, null) try { - mPreviewText!!.text = adjustCase(key.label) - } catch (ignored: Exception) { + previewText.text = adjustCase(key.label) + } catch (_: Exception) { } } @@ -1327,7 +1349,7 @@ class KeyboardView mBackgroundColor } - val previewBackground = mPreviewText!!.background as LayerDrawable + val previewBackground = previewText.background as LayerDrawable previewBackground .findDrawableByLayerId(R.id.button_background_shape) .applyColorFilter(previewBackgroundColor) @@ -1336,29 +1358,29 @@ class KeyboardView .findDrawableByLayerId(R.id.button_background_stroke) .applyColorFilter(context.getStrokeColor()) - mPreviewText!!.background = previewBackground + previewText.background = previewBackground - mPreviewText!!.setTextColor(mTextColor) - mPreviewText!!.measure( + previewText.setTextColor(mTextColor) + previewText.measure( MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), ) - val popupWidth = Math.max(mPreviewText!!.measuredWidth, key.width) + val popupWidth = Math.max(previewText.measuredWidth, key.width) val popupHeight = mPreviewHeight - val lp = mPreviewText!!.layoutParams + val lp = previewText.layoutParams lp?.width = popupWidth lp?.height = popupHeight mPopupPreviewX = key.x mPopupPreviewY = key.y - popupHeight - mHandler!!.removeMessages(MSG_REMOVE_PREVIEW) + mHandler?.removeMessages(MSG_REMOVE_PREVIEW) getLocationInWindow(mCoordinates) mCoordinates[0] += mMiniKeyboardOffsetX // offset may be zero mCoordinates[1] += mMiniKeyboardOffsetY // offset may be zero // Set the preview background state. - mPreviewText!!.background.state = + previewText.background.state = if (key.popupResId != 0) { LONG_PRESSABLE_STATE_SET } else { @@ -1387,7 +1409,7 @@ class KeyboardView previewPopup.width = popupWidth previewPopup.height = popupHeight previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, mPopupPreviewX, mPopupPreviewY) - mPreviewText!!.visibility = VISIBLE + previewText.visibility = VISIBLE } } @@ -1490,66 +1512,68 @@ class KeyboardView val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null) mMiniKeyboard = - mMiniKeyboardContainer!! - .findViewById(R.id.mini_keyboard_view) - as KeyboardView - - mMiniKeyboard!!.mOnKeyboardActionListener = - object : OnKeyboardActionListener { - override fun onKey(code: Int) { - mOnKeyboardActionListener!!.onKey(code) - dismissPopupKeyboard() - } + ( + mMiniKeyboardContainer?.findViewById(R.id.mini_keyboard_view) + as KeyboardView + ).also { + it.mOnKeyboardActionListener = + object : OnKeyboardActionListener { + override fun onKey(code: Int) { + mOnKeyboardActionListener?.onKey(code) + dismissPopupKeyboard() + } - override fun onPress(primaryCode: Int) { - mOnKeyboardActionListener!!.onPress(primaryCode) - } + override fun onPress(primaryCode: Int) { + mOnKeyboardActionListener?.onPress(primaryCode) + } - override fun onActionUp() { - mOnKeyboardActionListener!!.onActionUp() - } + override fun onActionUp() { + mOnKeyboardActionListener?.onActionUp() + } - override fun moveCursorLeft() { - mOnKeyboardActionListener!!.moveCursorLeft() - } + override fun moveCursorLeft() { + mOnKeyboardActionListener?.moveCursorLeft() + } - override fun moveCursorRight() { - mOnKeyboardActionListener!!.moveCursorRight() - } + override fun moveCursorRight() { + mOnKeyboardActionListener?.moveCursorRight() + } - override fun onText(text: String) { - mOnKeyboardActionListener!!.onText(text) - } + override fun onText(text: String) { + mOnKeyboardActionListener?.onText(text) + } - override fun hasTextBeforeCursor(): Boolean = - mOnKeyboardActionListener!! - .hasTextBeforeCursor() + override fun hasTextBeforeCursor(): Boolean = mOnKeyboardActionListener?.hasTextBeforeCursor() ?: false - override fun commitPeriodAfterSpace() { - mOnKeyboardActionListener!!.commitPeriodAfterSpace() - } + override fun commitPeriodAfterSpace() { + mOnKeyboardActionListener?.commitPeriodAfterSpace() + } + } } + val popupCharacters = popupKey.popupCharacters val keyboard = - if (popupKey.popupCharacters != null) { - KeyboardBase(context, popupKeyboardId, popupKey.popupCharacters!!, popupKey.width) + if (popupCharacters != null) { + KeyboardBase(context, popupKeyboardId, popupCharacters, popupKey.width) } else { KeyboardBase(context, popupKeyboardId, 0) } - mMiniKeyboard!!.setKeyboard(keyboard) + mMiniKeyboard?.setKeyboard(keyboard) mPopupParent = this - mMiniKeyboardContainer!!.measure( + mMiniKeyboardContainer?.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST), ) mMiniKeyboardCache[popupKey] = mMiniKeyboardContainer } else { mMiniKeyboard = - mMiniKeyboardContainer!! - .findViewById(R.id.mini_keyboard_view) as KeyboardView + mMiniKeyboardContainer?.findViewById(R.id.mini_keyboard_view) as KeyboardView } + val miniKeyboardContainer = mMiniKeyboardContainer ?: return false + val miniKeyboard = mMiniKeyboard ?: return false + val isUserDarkMode = be.scri.helpers.PreferencesHelper .getIsDarkModeOrNot(context) @@ -1560,7 +1584,7 @@ class KeyboardView context.theme, ) - mMiniKeyboard!!.background?.let { bg -> + miniKeyboard.background?.let { bg -> if (bg is LayerDrawable) { bg .findDrawableByLayerId(R.id.button_background_shape) @@ -1575,25 +1599,25 @@ class KeyboardView mPopupX = popupKey.x mPopupY = popupKey.y - var leftX = popupKey.x + (popupKey.width - mMiniKeyboardContainer!!.measuredWidth) / 2 - leftX = leftX.coerceIn(0, (width - mMiniKeyboardContainer!!.measuredWidth).coerceAtLeast(0)) + var leftX = popupKey.x + (popupKey.width - miniKeyboardContainer.measuredWidth) / 2 + leftX = leftX.coerceIn(0, (width - miniKeyboardContainer.measuredWidth).coerceAtLeast(0)) mPopupX = leftX - mPopupY -= mMiniKeyboardContainer!!.measuredHeight + mPopupY -= miniKeyboardContainer.measuredHeight val x = mPopupX + mCoordinates[0] val y = mPopupY + mCoordinates[1] val xOffset = Math.max(0, x) - mMiniKeyboard!!.setPopupOffset(xOffset, y) + miniKeyboard.setPopupOffset(xOffset, y) // Make sure we highlight the proper key right after long pressing it, // before any ACTION_MOVE event occurs. val miniKeyboardX = - if (xOffset + mMiniKeyboard!!.measuredWidth <= measuredWidth) { + if (xOffset + miniKeyboard.measuredWidth <= measuredWidth) { xOffset } else { - measuredWidth - mMiniKeyboard!!.measuredWidth + measuredWidth - miniKeyboard.measuredWidth } - val keysCnt = mMiniKeyboard!!.mKeys.size + val keysCnt = miniKeyboard.mKeys.size var selectedKeyIndex = Math.floor((me.rawX - miniKeyboardX) / popupKey.width.toDouble()).toInt() if (keysCnt > MAX_KEYS_PER_MINI_ROW) { selectedKeyIndex += MAX_KEYS_PER_MINI_ROW @@ -1601,7 +1625,7 @@ class KeyboardView selectedKeyIndex = Math.max(0, Math.min(selectedKeyIndex, keysCnt - 1)) val isEmojiPopup = - mMiniKeyboard!!.mKeys.any { + miniKeyboard.mKeys.any { it.code == KeyboardBase.KEYCODE_EMOJI || it.code == KeyboardBase.KEYCODE_CLIPBOARD || it.code == KeyboardBase.KEYCODE_FLOAT_TOGGLE @@ -1609,27 +1633,27 @@ class KeyboardView if (isEmojiPopup) { // Emoji popup: start with no pre-selection; user slides to choose and lifts to confirm. for (i in 0 until keysCnt) { - mMiniKeyboard!!.mKeys[i].focused = false + miniKeyboard.mKeys[i].focused = false } mMiniKeyboardSelectedKeyIndex = -1 } else if (setHoldForAltCharacters) { for (i in 0 until keysCnt) { - mMiniKeyboard!!.mKeys[i].focused = i == selectedKeyIndex + miniKeyboard.mKeys[i].focused = i == selectedKeyIndex } mMiniKeyboardSelectedKeyIndex = selectedKeyIndex } else { for (i in 0 until keysCnt) { - mMiniKeyboard!!.mKeys[i].focused = false + miniKeyboard.mKeys[i].focused = false } mMiniKeyboardSelectedKeyIndex = -1 } - mMiniKeyboard!!.invalidateAllKeys() + miniKeyboard.invalidateAllKeys() val miniShiftStatus = if (isShifted()) SHIFT_ON_PERMANENT else SHIFT_OFF - mMiniKeyboard!!.setShifted(miniShiftStatus) - mPopupKeyboard.contentView = mMiniKeyboardContainer - mPopupKeyboard.width = mMiniKeyboardContainer!!.measuredWidth - mPopupKeyboard.height = mMiniKeyboardContainer!!.measuredHeight + miniKeyboard.setShifted(miniShiftStatus) + mPopupKeyboard.contentView = miniKeyboardContainer + mPopupKeyboard.width = miniKeyboardContainer.measuredWidth + mPopupKeyboard.height = miniKeyboardContainer.measuredHeight mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y) mMiniKeyboardOnScreen = true invalidateAllKeys() @@ -1711,8 +1735,9 @@ class KeyboardView mMiniKeyboardSelectedKeyIndex = -1 hoverRunnable = null dismissPopupKeyboard() + }.also { + hoverHandler?.postDelayed(it, hoverDelay) } - hoverHandler?.postDelayed(hoverRunnable!!, hoverDelay) } else { hoverRunnable = Runnable { @@ -1724,8 +1749,9 @@ class KeyboardView mMiniKeyboardSelectedKeyIndex = -1 hoverRunnable = null dismissPopupKeyboard() + }.also { + hoverHandler?.postDelayed(it, 220L) } - hoverHandler?.postDelayed(hoverRunnable!!, 220L) } } // Emoji popup: no auto-fire on hover; wait for finger lift (ACTION_UP). @@ -1738,10 +1764,12 @@ class KeyboardView // Fire whichever key is currently highlighted when the finger lifts. val idx = mMiniKeyboardSelectedKeyIndex if (idx >= 0 && idx < (mMiniKeyboard?.mKeys?.size ?: 0)) { - val key = mMiniKeyboard!!.mKeys[idx] - key.focused = false - mMiniKeyboard!!.invalidateAllKeys() - mOnKeyboardActionListener?.onKey(key.code) + val key = mMiniKeyboard?.mKeys[idx] + if (key != null) { + key.focused = false + mMiniKeyboard?.invalidateAllKeys() + mOnKeyboardActionListener?.onKey(key.code) + } } mMiniKeyboardSelectedKeyIndex = -1 dismissPopupKeyboard() @@ -1759,9 +1787,11 @@ class KeyboardView if (!isEmojiPopup && setHoldForAltCharacters) { if (mMiniKeyboardSelectedKeyIndex >= 0) { - val key = mMiniKeyboard!!.mKeys[mMiniKeyboardSelectedKeyIndex] - mOnKeyboardActionListener?.onKey(key.code) - mMiniKeyboardSelectedKeyIndex = -1 + val key = mMiniKeyboard?.mKeys[mMiniKeyboardSelectedKeyIndex] + if (key != null) { + mOnKeyboardActionListener?.onKey(key.code) + mMiniKeyboardSelectedKeyIndex = -1 + } } mMiniKeyboardSelectedKeyIndex = -1 dismissPopupKeyboard() @@ -1810,7 +1840,7 @@ class KeyboardView // type in both chars and ignore the later gestures. // Can happen at fast typing, easier to reproduce by increasing LONGPRESS_TIMEOUT. ignoreTouches = true - mHandler!!.removeMessages(MSG_LONGPRESS) + mHandler?.removeMessages(MSG_LONGPRESS) dismissPopupKeyboard() detectAndSendKey(keyIndex, touchX, touchY, eventTime) @@ -1821,7 +1851,7 @@ class KeyboardView detectAndSendKey(secondKeyIndex, newPointerX, newPointerY, eventTime) val secondKeyCode = mKeys.getOrNull(secondKeyIndex)?.code - secondKeyCode?.let { mOnKeyboardActionListener!!.onPress(it) } + secondKeyCode?.let { mOnKeyboardActionListener?.onPress(it) } showPreview(NOT_A_KEY) invalidateKey(mCurrentKey) @@ -1839,12 +1869,15 @@ class KeyboardView mLastMoveTime = eventTime val onPressKey = if (keyIndex != NOT_A_KEY) mKeys[keyIndex].code else 0 - mOnKeyboardActionListener!!.onPress(onPressKey) + mOnKeyboardActionListener?.onPress(onPressKey) if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) { mRepeatKeyIndex = mCurrentKey - val msg = mHandler!!.obtainMessage(MSG_REPEAT) - mHandler!!.sendMessageDelayed(msg, REPEAT_START_DELAY.toLong()) + val handler = mHandler + if (handler != null) { + val msg = handler.obtainMessage(MSG_REPEAT) + handler.sendMessageDelayed(msg, REPEAT_START_DELAY.toLong()) + } // If the user long presses Space, move the cursor after swipine left/right. if (mKeys[mCurrentKey].code == KEYCODE_SPACE) { mLastSpaceMoveX = -1 @@ -1866,8 +1899,11 @@ class KeyboardView } if (!handled && mCurrentKey != NOT_A_KEY) { - val msg = mHandler!!.obtainMessage(MSG_LONGPRESS, me) - mHandler!!.sendMessageDelayed(msg, LONGPRESS_TIMEOUT.toLong()) + val handler = mHandler + if (handler != null) { + val msg = handler.obtainMessage(MSG_LONGPRESS, me) + handler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT.toLong()) + } } if (mPopupParent.id != R.id.mini_keyboard_view) { @@ -1914,17 +1950,20 @@ class KeyboardView } } else if (!continueLongPress) { // Cancel old longpress. - mHandler!!.removeMessages(MSG_LONGPRESS) - // Start new longpress if key has changed. - if (keyIndex != NOT_A_KEY) { - val msg = mHandler!!.obtainMessage(MSG_LONGPRESS, me) - mHandler!!.sendMessageDelayed(msg, LONGPRESS_TIMEOUT.toLong()) - } + val handler = mHandler + if (handler != null) { + handler.removeMessages(MSG_LONGPRESS) + // Start new longpress if key has changed. + if (keyIndex != NOT_A_KEY) { + val msg = handler.obtainMessage(MSG_LONGPRESS, me) + handler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT.toLong()) + } - if (mPopupParent.id != R.id.mini_keyboard_view) { - showPreview(mCurrentKey) + if (mPopupParent.id != R.id.mini_keyboard_view) { + showPreview(mCurrentKey) + } + mLastMoveTime = eventTime } - mLastMoveTime = eventTime } } MotionEvent.ACTION_UP -> { @@ -1959,9 +1998,9 @@ class KeyboardView if (mKeys.getOrNull(mCurrentKey)?.code == KEYCODE_SPACE && !mIsLongPressingSpace) { val currentTime = System.currentTimeMillis() if (currentTime - lastSpaceBarTapTime < DOUBLE_TAP_DELAY + EXTRA_DELAY && - mOnKeyboardActionListener!!.hasTextBeforeCursor() + mOnKeyboardActionListener?.hasTextBeforeCursor() == true ) { - mOnKeyboardActionListener!!.commitPeriodAfterSpace() + mOnKeyboardActionListener?.commitPeriodAfterSpace() } else { detectAndSendKey(mCurrentKey, touchX, touchY, eventTime) } @@ -1974,7 +2013,7 @@ class KeyboardView mOnKeyboardActionListener?.setDeleteRepeating(false) } mRepeatKeyIndex = NOT_A_KEY - mOnKeyboardActionListener!!.onActionUp() + mOnKeyboardActionListener?.onActionUp() mIsLongPressingSpace = false } MotionEvent.ACTION_CANCEL -> { From 78a31bed16685cea85d3e4e609d15fa916755e0a Mon Sep 17 00:00:00 2001 From: Chris Witt Date: Fri, 2 Oct 2026 17:37:29 -0400 Subject: [PATCH 2/8] CW - Updating changelog for non-null assertion cleanup. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aaa0324b..844efc718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -105,3 +105,4 @@ Emojis for the following are chosen based on [gitmoji](https://gitmoji.dev/). - 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)). - `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. From 3fc120b8b8d6d6efaea24b09617e26dd6b21673e Mon Sep 17 00:00:00 2001 From: Prince Yadav <66916296+prince-0408@users.noreply.github.com> Date: Sat, 3 Oct 2026 14:48:06 +0530 Subject: [PATCH 3/8] refactor: extract KeyboardLayoutHandler from GeneralKeyboardIME (Part 15) - #426 (#692) * refactor: extract KeyboardLayoutHandler from GeneralKeyboardIME (Part 15) - #426 * refactor: address review comments on KeyboardLayoutHandler --- CHANGELOG.md | 1 + .../be/scri/helpers/KeyboardIMEContext.kt | 5 + .../be/scri/helpers/KeyboardLayoutHandler.kt | 135 ++++++++++++++++++ .../be/scri/services/GeneralKeyboardIME.kt | 84 ++++------- .../scri/helpers/KeyboardLayoutHandlerTest.kt | 108 ++++++++++++++ .../GeneralKeyboardIMEInputTypeTest.kt | 17 +-- 6 files changed, 283 insertions(+), 67 deletions(-) create mode 100644 app/src/keyboards/java/be/scri/helpers/KeyboardLayoutHandler.kt create mode 100644 app/src/testKeyboards/kotlin/be/scri/helpers/KeyboardLayoutHandlerTest.kt diff --git a/CHANGELOG.md b/CHANGELOG.md index 2aaa0324b..3165e79ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -104,4 +104,5 @@ 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)). diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt index 31c97cc4e..b9c2d7b21 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyboardIMEContext.kt @@ -51,6 +51,9 @@ interface KeyboardIMEContext { val keyboardSymbols: Int val keyboardSymbolShift: Int + val isFloatingMode: Boolean + val enterKeyType: Int + var lastShiftPressTS: Long val currentCommandBarHint: String @@ -79,6 +82,8 @@ interface KeyboardIMEContext { fun getKeyboardLayoutXML(): Int + fun isNumericKeyboardActive(): Boolean + fun handleDelete(isLongPress: Boolean = false) fun isDeleteRepeating(): Boolean diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardLayoutHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardLayoutHandler.kt new file mode 100644 index 000000000..ec597b572 --- /dev/null +++ b/app/src/keyboards/java/be/scri/helpers/KeyboardLayoutHandler.kt @@ -0,0 +1,135 @@ +// 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()) + ime.keyboard?.setShifted(currentShiftState) + ime.keyboardView?.setKeyboard(ime.keyboard!!) + + 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 + } + } +} diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index ca86994de..15955d0ad 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -10,9 +10,6 @@ import android.content.res.Resources import android.graphics.Rect import android.inputmethodservice.InputMethodService import android.text.InputType -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 android.view.View import android.view.inputmethod.EditorInfo @@ -37,6 +34,7 @@ import be.scri.helpers.KeyboardBase import be.scri.helpers.KeyboardDataHandler import be.scri.helpers.KeyboardIMEContext import be.scri.helpers.KeyboardLanguageMappingConstants +import be.scri.helpers.KeyboardLayoutHandler import be.scri.helpers.KeyboardStateManager import be.scri.helpers.LanguageMappingConstants.getLanguageAlias import be.scri.helpers.NativeSuggestionEngine @@ -100,7 +98,7 @@ abstract class GeneralKeyboardIME( abstract override var lastShiftPressTS: Long abstract override var keyboardMode: Int abstract var inputTypeClass: Int - abstract var enterKeyType: Int + abstract override var enterKeyType: Int abstract var switchToLetters: Boolean // Language-specific layout and behavior configurations (decoupled from base class). @@ -159,6 +157,7 @@ abstract class GeneralKeyboardIME( override lateinit var autocompletionHandler: AutocompletionHandler internal lateinit var keyHandler: KeyHandler internal val floatingKeyboardHandler by lazy { FloatingKeyboardHandler(this) } + internal val layoutHandler by lazy { KeyboardLayoutHandler(this) } internal val commandHandler by lazy { CommandHandler(this) } internal val shiftHandler by lazy { ShiftHandler(this) } internal val suggestionUIHandler by lazy { SuggestionUIHandler(this) } @@ -227,7 +226,7 @@ abstract class GeneralKeyboardIME( override var wordSuggestions: List? = null override var checkIfPluralWord: Boolean = false private var currentEnterKeyType: Int? = null - private var isNumericKeyboardActive: Boolean = false + internal var isNumericKeyboardActive: Boolean = false internal val stateManager = KeyboardStateManager() override val themeManager = KeyboardThemeManager() @@ -273,22 +272,6 @@ abstract class GeneralKeyboardIME( internal const val MAX_TEXT_LENGTH = 1000 const val COMMIT_TEXT_CURSOR_POSITION = 1 internal const val CUSTOM_CURSOR = "│" // special tall cursor character - - internal fun shouldUseNumericKeyboard(inputType: Int): Boolean = - when (inputType and TYPE_MASK_CLASS) { - TYPE_CLASS_NUMBER, TYPE_CLASS_DATETIME, TYPE_CLASS_PHONE -> true - else -> false - } - - internal fun getKeyboardLayoutXMLForInputType( - inputType: Int, - letterKeyboardLayoutXML: Int, - ): Int = - if (shouldUseNumericKeyboard(inputType)) { - R.xml.keys_numeric - } else { - letterKeyboardLayoutXML - } } // MARK: Lifecycle Methods @@ -445,9 +428,9 @@ abstract class GeneralKeyboardIME( // This setter triggers the logic in the property override if not shadowed. hasTextBeforeCursor = currentInputConnection?.getTextBeforeCursor(1, 0)?.isNotEmpty() == true - isNumericKeyboardActive = shouldUseNumericKeyboard(editorInfo.inputType) + isNumericKeyboardActive = KeyboardLayoutHandler.shouldUseNumericKeyboard(editorInfo.inputType) keyboardMode = if (isNumericKeyboardActive) keyboardSymbols else keyboardLetters - val keyboardXml = getKeyboardLayoutXMLForInputType(editorInfo.inputType, getKeyboardLayoutXML()) + val keyboardXml = KeyboardLayoutHandler.getKeyboardLayoutXMLForInputType(editorInfo.inputType, getKeyboardLayoutXML()) loadLanguageData() @@ -765,20 +748,17 @@ abstract class GeneralKeyboardIME( override fun isNumericKeyboardActive(): Boolean = isNumericKeyboardActive - override fun getCurrentKeyboardLayoutXML(): Int = - when (keyboardMode) { - keyboardSymbols -> getPrimarySymbolKeyboardLayoutXML() - keyboardSymbolShift -> R.xml.keys_symbols_shift - else -> getKeyboardLayoutXML() - } - - internal fun getPrimarySymbolKeyboardLayoutXML(): Int = + /** + * Resolves the XML resource ID for the active keyboard layout. + * Delegated to [KeyboardLayoutHandler]. + */ + override fun getCurrentKeyboardLayoutXML(): Int = layoutHandler.getCurrentKeyboardLayoutXML() - if (isNumericKeyboardActive) { - R.xml.keys_numeric - } else { - R.xml.keys_symbols - } + /** + * Resolves the primary symbol or numeric layout XML resource ID. + * Delegated to [KeyboardLayoutHandler]. + */ + internal fun getPrimarySymbolKeyboardLayoutXML(): Int = layoutHandler.getPrimarySymbolKeyboardLayoutXML() override fun onKeyboardActionListener(): KeyboardView.OnKeyboardActionListener = this @@ -1215,6 +1195,12 @@ abstract class GeneralKeyboardIME( * Returns the subsequent dataset for conjugation sub-views. * Delegated to [ConjugationHandler]. */ + internal fun getKeyboardLayoutForState( + state: ScribeState, + isSubsequentArea: Boolean = false, + dataSize: Int = 0, + ): Int = layoutHandler.getKeyboardLayoutForState(state, isSubsequentArea, dataSize) + override fun returnSubsequentData(): List> = conjugationHandler.subsequentData /** @@ -1234,31 +1220,11 @@ abstract class GeneralKeyboardIME( // MARK: Floating Keyboard Integration - override fun getKeyboardWidth(): Int = - if (isFloatingMode) { - val density = resources.displayMetrics.density - val screenWidth = resources.displayMetrics.widthPixels - val floatWidth = (320f * density).toInt() - Math.min(floatWidth, (screenWidth * 0.85f).toInt()) - } else { - resources.displayMetrics.widthPixels - } + override fun getKeyboardWidth(): Int = layoutHandler.getKeyboardWidth() - override fun recreateKeyboard() { - if (!this::uiManager.isInitialized) return - val xmlId = getCurrentKeyboardLayoutXML() - val currentShiftState = keyboard?.mShiftState ?: SHIFT_OFF - keyboard = KeyboardBase(this, xmlId, enterKeyType, getKeyboardWidth()) - keyboard?.setShifted(currentShiftState) - keyboardView?.setKeyboard(keyboard!!) - - if (xmlId == R.xml.keys_symbols) { - uiManager.setupCurrencySymbol(language) - } - keyboardView?.invalidateAllKeys() - } + override fun recreateKeyboard() = layoutHandler.recreateKeyboard() - val isFloatingMode: Boolean + override val isFloatingMode: Boolean get() = floatingKeyboardHandler.isFloatingMode fun initFloatingMode() { diff --git a/app/src/testKeyboards/kotlin/be/scri/helpers/KeyboardLayoutHandlerTest.kt b/app/src/testKeyboards/kotlin/be/scri/helpers/KeyboardLayoutHandlerTest.kt new file mode 100644 index 000000000..df56c3c4a --- /dev/null +++ b/app/src/testKeyboards/kotlin/be/scri/helpers/KeyboardLayoutHandlerTest.kt @@ -0,0 +1,108 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package be.scri.helpers + +import android.text.InputType.TYPE_CLASS_NUMBER +import android.text.InputType.TYPE_CLASS_TEXT +import be.scri.R +import be.scri.models.ScribeState +import io.mockk.every +import io.mockk.mockk +import org.junit.Assert.assertEquals +import org.junit.Assert.assertFalse +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test + +class KeyboardLayoutHandlerTest { + private lateinit var mockIme: KeyboardIMEContext + private lateinit var layoutHandler: KeyboardLayoutHandler + + @Before + fun setUp() { + mockIme = mockk(relaxed = true) + layoutHandler = KeyboardLayoutHandler(mockIme) + } + + @Test + fun testGetPrimarySymbolKeyboardLayoutXML_numericActive() { + every { mockIme.isNumericKeyboardActive() } returns true + val xmlResId = layoutHandler.getPrimarySymbolKeyboardLayoutXML() + assertEquals(R.xml.keys_numeric, xmlResId) + } + + @Test + fun testGetPrimarySymbolKeyboardLayoutXML_symbolsActive() { + every { mockIme.isNumericKeyboardActive() } returns false + val xmlResId = layoutHandler.getPrimarySymbolKeyboardLayoutXML() + assertEquals(R.xml.keys_symbols, xmlResId) + } + + @Test + fun testGetCurrentKeyboardLayoutXML_letterMode() { + // keyboardMode does not match keyboardSymbols or keyboardSymbolShift, + // so the else branch delegates to getKeyboardLayoutXML(). + // Stub with a real letter layout to verify the correct branch is taken. + every { mockIme.keyboardMode } returns 0 + every { mockIme.keyboardSymbols } returns 1 + every { mockIme.keyboardSymbolShift } returns 2 + every { mockIme.getKeyboardLayoutXML() } returns R.xml.keys_letters_english + + val xmlResId = layoutHandler.getCurrentKeyboardLayoutXML() + assertEquals(R.xml.keys_letters_english, xmlResId) + } + + @Test + fun testGetCurrentKeyboardLayoutXML_symbolMode() { + // keyboardMode matches keyboardSymbols, so symbols layout is returned. + every { mockIme.keyboardMode } returns 1 + every { mockIme.keyboardSymbols } returns 1 + every { mockIme.keyboardSymbolShift } returns 2 + every { mockIme.isNumericKeyboardActive() } returns false + + val xmlResId = layoutHandler.getCurrentKeyboardLayoutXML() + assertEquals(R.xml.keys_symbols, xmlResId) + } + + @Test + fun testGetCurrentKeyboardLayoutXML_symbolShiftMode() { + every { mockIme.keyboardMode } returns 2 + every { mockIme.keyboardSymbols } returns 1 + every { mockIme.keyboardSymbolShift } returns 2 + + val xmlResId = layoutHandler.getCurrentKeyboardLayoutXML() + assertEquals(R.xml.keys_symbols_shift, xmlResId) + } + + @Test + fun testGetKeyboardLayoutForState_verbConjunction_dataSize2() { + every { mockIme.saveConjugateModeType(any()) } returns Unit + val xmlResId = layoutHandler.getKeyboardLayoutForState(ScribeState.SELECT_VERB_CONJUNCTION, isSubsequentArea = true, dataSize = 2) + assertEquals(R.xml.conjugate_view_2x1, xmlResId) + } + + @Test + fun testGetKeyboardLayoutForState_defaultState() { + every { mockIme.getKeyboardLayoutXML() } returns R.xml.keys_letters_english + val xmlResId = layoutHandler.getKeyboardLayoutForState(ScribeState.IDLE) + assertEquals(R.xml.keys_letters_english, xmlResId) + } + + @Test + fun testShouldUseNumericKeyboard() { + assertTrue(KeyboardLayoutHandler.shouldUseNumericKeyboard(TYPE_CLASS_NUMBER)) + assertFalse(KeyboardLayoutHandler.shouldUseNumericKeyboard(TYPE_CLASS_TEXT)) + } + + @Test + fun testGetKeyboardLayoutXMLForInputType() { + assertEquals( + R.xml.keys_numeric, + KeyboardLayoutHandler.getKeyboardLayoutXMLForInputType(TYPE_CLASS_NUMBER, R.xml.keys_letters_english), + ) + assertEquals( + R.xml.keys_letters_english, + KeyboardLayoutHandler.getKeyboardLayoutXMLForInputType(TYPE_CLASS_TEXT, R.xml.keys_letters_english), + ) + } +} diff --git a/app/src/testKeyboards/kotlin/be/scri/services/GeneralKeyboardIMEInputTypeTest.kt b/app/src/testKeyboards/kotlin/be/scri/services/GeneralKeyboardIMEInputTypeTest.kt index 7ebbc9201..a7f960271 100644 --- a/app/src/testKeyboards/kotlin/be/scri/services/GeneralKeyboardIMEInputTypeTest.kt +++ b/app/src/testKeyboards/kotlin/be/scri/services/GeneralKeyboardIMEInputTypeTest.kt @@ -5,6 +5,7 @@ package be.scri.services import android.text.InputType import android.view.inputmethod.EditorInfo import be.scri.R +import be.scri.helpers.KeyboardLayoutHandler import org.junit.Assert.assertEquals import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -15,26 +16,26 @@ class GeneralKeyboardIMEInputTypeTest { fun shouldUseNumericKeyboard_returnsTrueForNumberInputs() { val inputType = InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL - assertTrue(GeneralKeyboardIME.shouldUseNumericKeyboard(inputType)) + assertTrue(KeyboardLayoutHandler.shouldUseNumericKeyboard(inputType)) } @Test fun shouldUseNumericKeyboard_returnsTrueForDateTimeInputs() { val inputType = InputType.TYPE_CLASS_DATETIME or InputType.TYPE_DATETIME_VARIATION_DATE - assertTrue(GeneralKeyboardIME.shouldUseNumericKeyboard(inputType)) + assertTrue(KeyboardLayoutHandler.shouldUseNumericKeyboard(inputType)) } @Test fun shouldUseNumericKeyboard_returnsTrueForPhoneInputs() { - assertTrue(GeneralKeyboardIME.shouldUseNumericKeyboard(InputType.TYPE_CLASS_PHONE)) + assertTrue(KeyboardLayoutHandler.shouldUseNumericKeyboard(InputType.TYPE_CLASS_PHONE)) } @Test fun shouldUseNumericKeyboard_returnsFalseForTextInputs() { val inputType = InputType.TYPE_CLASS_TEXT or InputType.TYPE_TEXT_VARIATION_EMAIL_ADDRESS - assertFalse(GeneralKeyboardIME.shouldUseNumericKeyboard(inputType)) + assertFalse(KeyboardLayoutHandler.shouldUseNumericKeyboard(inputType)) } @Test @@ -43,7 +44,7 @@ class GeneralKeyboardIMEInputTypeTest { assertEquals( R.xml.keys_numeric, - GeneralKeyboardIME.getKeyboardLayoutXMLForInputType(inputType, R.xml.keys_letters_english), + KeyboardLayoutHandler.getKeyboardLayoutXMLForInputType(inputType, R.xml.keys_letters_english), ) } @@ -53,7 +54,7 @@ class GeneralKeyboardIMEInputTypeTest { assertEquals( R.xml.keys_letters_english, - GeneralKeyboardIME.getKeyboardLayoutXMLForInputType(inputType, R.xml.keys_letters_english), + KeyboardLayoutHandler.getKeyboardLayoutXMLForInputType(inputType, R.xml.keys_letters_english), ) } @@ -61,10 +62,10 @@ class GeneralKeyboardIMEInputTypeTest { fun defaultEditorInfo_fallsBackToLetterLayout() { val fallback = EditorInfo() - assertFalse(GeneralKeyboardIME.shouldUseNumericKeyboard(fallback.inputType)) + assertFalse(KeyboardLayoutHandler.shouldUseNumericKeyboard(fallback.inputType)) assertEquals( R.xml.keys_letters_english, - GeneralKeyboardIME.getKeyboardLayoutXMLForInputType(fallback.inputType, R.xml.keys_letters_english), + KeyboardLayoutHandler.getKeyboardLayoutXMLForInputType(fallback.inputType, R.xml.keys_letters_english), ) } } From 9c4e7041f20be8c357a29e96c3011ce1690b3f85 Mon Sep 17 00:00:00 2001 From: Chris Witt Date: Sun, 4 Oct 2026 13:07:29 -0400 Subject: [PATCH 4/8] CW - Fixing merge conflicts. --- .../scri/services/GeneralKeyboardIMETest.kt | 11 ++++++++ .../be/scri/helpers/KeyboardLayoutHandler.kt | 9 ++++-- .../java/be/scri/helpers/ShiftHandler.kt | 26 ++++++++++------- .../be/scri/helpers/ui/KeyboardUIManager.kt | 7 +++-- .../be/scri/services/GeneralKeyboardIME.kt | 18 +++++++----- .../main/java/be/scri/helpers/KeyboardBase.kt | 28 +++++++++++++------ 6 files changed, 69 insertions(+), 30 deletions(-) diff --git a/app/src/androidTestKeyboards/kotlin/be/scri/services/GeneralKeyboardIMETest.kt b/app/src/androidTestKeyboards/kotlin/be/scri/services/GeneralKeyboardIMETest.kt index b75741f10..40fd103cf 100644 --- a/app/src/androidTestKeyboards/kotlin/be/scri/services/GeneralKeyboardIMETest.kt +++ b/app/src/androidTestKeyboards/kotlin/be/scri/services/GeneralKeyboardIMETest.kt @@ -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 @@ -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") + } } diff --git a/app/src/keyboards/java/be/scri/helpers/KeyboardLayoutHandler.kt b/app/src/keyboards/java/be/scri/helpers/KeyboardLayoutHandler.kt index ec597b572..0ce0636e9 100644 --- a/app/src/keyboards/java/be/scri/helpers/KeyboardLayoutHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/KeyboardLayoutHandler.kt @@ -98,9 +98,12 @@ class KeyboardLayoutHandler( val xmlId = getCurrentKeyboardLayoutXML() val currentShiftState = ime.keyboard?.mShiftState ?: SHIFT_OFF - ime.keyboard = KeyboardBase(ime.imeContext, xmlId, ime.enterKeyType, getKeyboardWidth()) - ime.keyboard?.setShifted(currentShiftState) - ime.keyboardView?.setKeyboard(ime.keyboard!!) + 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) diff --git a/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt b/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt index 28f22141f..775f1e658 100644 --- a/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt +++ b/app/src/keyboards/java/be/scri/helpers/ShiftHandler.kt @@ -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) } @@ -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) diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt index ff0656b56..b861e5682 100644 --- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt +++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt @@ -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() } diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index 15955d0ad..d4a0675ab 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -314,7 +314,7 @@ abstract class GeneralKeyboardIME( setVibrate = getIsVibrateEnabled(applicationContext, language) setSound = getIsSoundEnabled(applicationContext, language) setHoldForAltCharacters = getHoldKeyStyle(applicationContext, language) - setKeyboard(this@GeneralKeyboardIME.keyboard!!) + this@GeneralKeyboardIME.keyboard?.let { setKeyboard(it) } mOnKeyboardActionListener = this@GeneralKeyboardIME } @@ -434,8 +434,9 @@ abstract class GeneralKeyboardIME( loadLanguageData() - keyboard = KeyboardBase(this, keyboardXml, enterKeyType, getKeyboardWidth()) - keyboardView?.setKeyboard(keyboard!!) + keyboard = + KeyboardBase(this, keyboardXml, enterKeyType, getKeyboardWidth()) + .also { keyboardView?.setKeyboard(it) } if (this::uiManager.isInitialized && keyboardXml == R.xml.keys_symbols) { uiManager.setupCurrencySymbol(language) @@ -558,7 +559,9 @@ abstract class GeneralKeyboardIME( keyboard?.setShifted(SHIFT_ON_ONE_CHAR) } } - keyboardView!!.setKeyboard(keyboard!!) + keyboard?.let { + keyboardView?.setKeyboard(it) + } switchToLetters = false } } @@ -769,9 +772,10 @@ abstract class GeneralKeyboardIME( override fun commitText(text: String) { if (currentState == ScribeState.SELECT_VERB_CONJUNCTION) { val label = text.trim() - val conjugateIndex = uiManager.getValidatedConjugateIndex(conjugateOutput) - val title = conjugateOutput?.keys?.elementAtOrNull(conjugateIndex) - val languageOutput = title?.let { conjugateOutput!![it] } + val output = conjugateOutput + val conjugateIndex = uiManager.getValidatedConjugateIndex(output) + val title = output?.keys?.elementAtOrNull(conjugateIndex) + val languageOutput = title?.let { output[it] } val matchingEntry = languageOutput?.entries?.find { (_, values) -> diff --git a/app/src/main/java/be/scri/helpers/KeyboardBase.kt b/app/src/main/java/be/scri/helpers/KeyboardBase.kt index 354368f71..f30fb40b1 100644 --- a/app/src/main/java/be/scri/helpers/KeyboardBase.kt +++ b/app/src/main/java/be/scri/helpers/KeyboardBase.kt @@ -359,8 +359,12 @@ class KeyboardBase { popupResId = a.getResourceId(R.styleable.KeyboardBase_Key_popupKeyboard, 0) repeatable = a.getBoolean(R.styleable.KeyboardBase_Key_isRepeatable, false) edgeFlags = a.getInt(R.styleable.KeyboardBase_Key_keyEdgeFlags, 0) - icon = a.getDrawable(R.styleable.KeyboardBase_Key_keyIcon) - icon?.setBounds(0, 0, icon!!.intrinsicWidth, icon!!.intrinsicHeight) + icon = + a + .getDrawable(R.styleable.KeyboardBase_Key_keyIcon) + .also { + it?.setBounds(0, 0, it.intrinsicWidth, it.intrinsicHeight) + } label = a.getText(R.styleable.KeyboardBase_Key_keyLabel) ?: "" topSmallNumber = a.getString(R.styleable.KeyboardBase_Key_topSmallNumber) ?: "" @@ -448,7 +452,7 @@ class KeyboardBase { row.defaultWidth = keyWidth row.defaultHorizontalGap = mDefaultHorizontalGap - characters.forEachIndexed { index, character -> + characters.forEachIndexed { _, character -> val key = Key(row) if (column >= MAX_KEYS_PER_MINI_ROW) { column = 0 @@ -464,7 +468,7 @@ class KeyboardBase { key.code = character.code column++ x += key.width + key.gap - mKeys!!.add(key) + mKeys?.add(key) row.mKeys.add(key) if (x > mMinWidth) { mMinWidth = x @@ -585,7 +589,11 @@ class KeyboardBase { TAG_KEY -> { inKey = true - key = createKeyFromXml(res, currentRow!!, x, y, parser) + if (currentRow == null) { + Log.e("KeyboardBase", "Key element found outside of a Row tag at line ${parser.lineNumber}") + continue + } + key = createKeyFromXml(res, currentRow, x, y, parser) if (hideComma && key.code == ','.code) { widthToRedistribute = key.width + key.gap @@ -595,7 +603,7 @@ class KeyboardBase { key.gap = 0 } - mKeys!!.add(key) + mKeys?.add(key) if (key.code == KEYCODE_ENTER) { val enterResourceId = when (mEnterKeyType) { @@ -628,13 +636,17 @@ class KeyboardBase { } else if (event == XmlResourceParser.END_TAG) { if (inKey) { inKey = false - x += key!!.gap + key.width + key?.run { + x += gap + width + } if (x > mMinWidth) { mMinWidth = x } } else if (inRow) { inRow = false - y += currentRow!!.defaultHeight + currentRow?.run { + y += defaultHeight + } row++ } } From 12a1175693c89ef77ce251786bee9f7a65e5c0f6 Mon Sep 17 00:00:00 2001 From: witt Date: Wed, 30 Sep 2026 07:58:02 -0400 Subject: [PATCH 5/8] CW - Cleaning up non-null assertions. --- .../main/java/be/scri/views/KeyboardView.kt | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/app/src/main/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt index b2cdaa57f..7c60858d4 100644 --- a/app/src/main/java/be/scri/views/KeyboardView.kt +++ b/app/src/main/java/be/scri/views/KeyboardView.kt @@ -52,7 +52,6 @@ import be.scri.extensions.performSoundFeedback import be.scri.helpers.KeyboardBase import be.scri.helpers.KeyboardBase.Companion.KEYCODE_CAPS_LOCK import be.scri.helpers.KeyboardBase.Companion.KEYCODE_DELETE -import be.scri.helpers.KeyboardBase.Companion.KEYCODE_EMOJI import be.scri.helpers.KeyboardBase.Companion.KEYCODE_ENTER import be.scri.helpers.KeyboardBase.Companion.KEYCODE_LEFT_ARROW import be.scri.helpers.KeyboardBase.Companion.KEYCODE_MODE_CHANGE @@ -63,7 +62,6 @@ import be.scri.helpers.KeyboardBase.Companion.KEYCODE_TAB import be.scri.helpers.KeyboardBase.Companion.SHIFT_LOCKED import be.scri.helpers.KeyboardBase.MyCustomActions import be.scri.helpers.MAX_KEYS_PER_MINI_ROW -import be.scri.helpers.PreferencesHelper import be.scri.helpers.SHIFT_OFF import be.scri.helpers.SHIFT_ON_ONE_CHAR import be.scri.helpers.SHIFT_ON_PERMANENT @@ -493,10 +491,14 @@ class KeyboardView private var keyboardBindingInternal: KeyboardViewKeyboardBinding? = null val keyboardBinding: KeyboardViewKeyboardBinding get() { - if (keyboardBindingInternal == null) { - keyboardBindingInternal = KeyboardViewKeyboardBinding.inflate(LayoutInflater.from(context)) + val existing = keyboardBindingInternal + if (existing != null) { + return existing } - return keyboardBindingInternal!! + + val binding = KeyboardViewKeyboardBinding.inflate(LayoutInflater.from(context)) + keyboardBindingInternal = binding + return binding } init { @@ -559,7 +561,10 @@ class KeyboardView object : Handler() { override fun handleMessage(msg: Message) { when (msg.what) { - MSG_REMOVE_PREVIEW -> mPreviewText!!.visibility = INVISIBLE + MSG_REMOVE_PREVIEW -> { + val previewText = mPreviewText + previewText?.visibility = INVISIBLE + } MSG_REPEAT -> if (repeatKey(false)) { val repeat = Message.obtain(this, MSG_REPEAT) @@ -651,8 +656,8 @@ class KeyboardView removeMessages() mKeyboard = keyboard - val keys = mKeyboard!!.mKeys - mKeys = keys!!.toMutableList() as ArrayList + val keys = mKeyboard?.mKeys + mKeys = keys?.toMutableList() as ArrayList requestLayout() mKeyboardChanged = true invalidateAllKeys() @@ -1296,6 +1301,7 @@ class KeyboardView private fun showKey(keyIndex: Int) { val previewPopup = mPreviewPopup + val previewText = mPreviewText ?: return val keys = mKeys if (keyIndex < 0 || keyIndex >= mKeys.size) { return @@ -1303,19 +1309,19 @@ class KeyboardView val key = keys[keyIndex] if (key.icon != null) { - mPreviewText!!.setCompoundDrawables(null, null, null, key.icon) + previewText.setCompoundDrawables(null, null, null, key.icon) } else { if (key.label.length > 1) { - mPreviewText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize.toFloat()) - mPreviewText!!.typeface = Typeface.DEFAULT_BOLD + previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mKeyTextSize.toFloat()) + previewText.typeface = Typeface.DEFAULT_BOLD } else { - mPreviewText!!.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge.toFloat()) - mPreviewText!!.typeface = Typeface.DEFAULT + previewText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mPreviewTextSizeLarge.toFloat()) + previewText.typeface = Typeface.DEFAULT } - mPreviewText!!.setCompoundDrawables(null, null, null, null) + previewText.setCompoundDrawables(null, null, null, null) try { - mPreviewText!!.text = adjustCase(key.label) + previewText.text = adjustCase(key.label) } catch (ignored: Exception) { } } @@ -1327,7 +1333,7 @@ class KeyboardView mBackgroundColor } - val previewBackground = mPreviewText!!.background as LayerDrawable + val previewBackground = previewText.background as LayerDrawable previewBackground .findDrawableByLayerId(R.id.button_background_shape) .applyColorFilter(previewBackgroundColor) @@ -1336,16 +1342,16 @@ class KeyboardView .findDrawableByLayerId(R.id.button_background_stroke) .applyColorFilter(context.getStrokeColor()) - mPreviewText!!.background = previewBackground + previewText.background = previewBackground - mPreviewText!!.setTextColor(mTextColor) - mPreviewText!!.measure( + previewText.setTextColor(mTextColor) + previewText.measure( MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED), ) - val popupWidth = Math.max(mPreviewText!!.measuredWidth, key.width) + val popupWidth = Math.max(previewText.measuredWidth, key.width) val popupHeight = mPreviewHeight - val lp = mPreviewText!!.layoutParams + val lp = previewText.layoutParams lp?.width = popupWidth lp?.height = popupHeight @@ -1358,7 +1364,7 @@ class KeyboardView mCoordinates[1] += mMiniKeyboardOffsetY // offset may be zero // Set the preview background state. - mPreviewText!!.background.state = + previewText.background.state = if (key.popupResId != 0) { LONG_PRESSABLE_STATE_SET } else { @@ -1387,7 +1393,7 @@ class KeyboardView previewPopup.width = popupWidth previewPopup.height = popupHeight previewPopup.showAtLocation(mPopupParent, Gravity.NO_GRAVITY, mPopupPreviewX, mPopupPreviewY) - mPreviewText!!.visibility = VISIBLE + previewText.visibility = VISIBLE } } From d1c2885209812fac70fde83ed5815fe8912060cd Mon Sep 17 00:00:00 2001 From: Chris Witt Date: Fri, 2 Oct 2026 13:17:55 -0400 Subject: [PATCH 6/8] CW - Fixing non-null assertions and some light refactoring. --- .../be/scri/helpers/ui/KeyboardUIManager.kt | 10 +- .../be/scri/services/GeneralKeyboardIME.kt | 4 +- .../main/java/be/scri/views/KeyboardView.kt | 322 ++++++++++-------- 3 files changed, 184 insertions(+), 152 deletions(-) diff --git a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt index b861e5682..d71e41379 100644 --- a/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt +++ b/app/src/keyboards/java/be/scri/helpers/ui/KeyboardUIManager.kt @@ -1216,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 diff --git a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt index d4a0675ab..12039f5cf 100644 --- a/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt +++ b/app/src/keyboards/java/be/scri/services/GeneralKeyboardIME.kt @@ -436,7 +436,9 @@ abstract class GeneralKeyboardIME( keyboard = KeyboardBase(this, keyboardXml, enterKeyType, getKeyboardWidth()) - .also { keyboardView?.setKeyboard(it) } + .also { + keyboardView?.setKeyboard(it) + } if (this::uiManager.isInitialized && keyboardXml == R.xml.keys_symbols) { uiManager.setupCurrencySymbol(language) diff --git a/app/src/main/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt index 7c60858d4..668578480 100644 --- a/app/src/main/java/be/scri/views/KeyboardView.kt +++ b/app/src/main/java/be/scri/views/KeyboardView.kt @@ -68,6 +68,7 @@ import be.scri.helpers.SHIFT_ON_PERMANENT import be.scri.models.ScribeState import java.util.Arrays import java.util.Locale +import kotlin.ranges.coerceIn /** * The base keyboard view for Scribe language keyboards application. @@ -243,8 +244,6 @@ class KeyboardView private var mEnterKeyColor: Int = 0 - private var mSpecialKeyColor: Int? = null - private var mKeyBackground: Drawable? = null private var mToolbarHolder: View? = null @@ -744,14 +743,15 @@ class KeyboardView if (mKeyboard == null) { setMeasuredDimension(0, 0) } else { - var width = mKeyboard!!.mMinWidth + var width = mKeyboard?.mMinWidth ?: 0 if (MeasureSpec.getSize(widthMeasureSpec) < width + MARGIN_ADJUSTMENT) { width = MeasureSpec.getSize(widthMeasureSpec) } val extraBottomPaddingPx = (resources.displayMetrics.density * 10).toInt() - setMeasuredDimension(width, mKeyboard!!.mHeight + extraBottomPaddingPx) + val height = mKeyboard?.mHeight ?: 0 + setMeasuredDimension(width, height + extraBottomPaddingPx) } } @@ -799,7 +799,9 @@ class KeyboardView if (mDrawPending || mBuffer == null || mKeyboardChanged) { onBufferDraw() } - canvas.drawBitmap(mBuffer!!, 0f, 0f, null) + mBuffer?.let { + canvas.drawBitmap(it, 0f, 0f, null) + } } @SuppressLint("UseCompatLoadingForDrawables") @@ -812,8 +814,7 @@ class KeyboardView // Make sure our bitmap is at least 1x1. val width = 1.coerceAtLeast(width) val height = 1.coerceAtLeast(height) - mBuffer = createBitmap(width, height) - mCanvas = Canvas(mBuffer!!) + mBuffer = createBitmap(width, height).also { mCanvas = Canvas(it) } } invalidateAllKeys() mKeyboardChanged = false @@ -823,9 +824,10 @@ class KeyboardView return } - mCanvas!!.withSave { - val canvas = mCanvas - canvas!!.clipRect(mDirtyRect) + val canvas = mCanvas ?: return + + canvas.withSave { + clipRect(mDirtyRect) val paint = mPaint val keys = mKeys val isUserDarkMode = @@ -849,12 +851,6 @@ class KeyboardView } else { Color.WHITE } - mSpecialKeyColor = - if (isUserDarkMode) { - R.color.special_key_dark - } else { - R.color.special_key_light - } val pressedColorResId = if (isUserDarkMode) { R.color.dark_key_press_color @@ -862,7 +858,13 @@ class KeyboardView R.color.light_key_press_color } val pressedColor = resources.getColor(pressedColorResId, context.theme) - val specialKeyColorValue = resources.getColor(mSpecialKeyColor!!, context.theme) + val specialKeyColorResource = + if (isUserDarkMode) { + R.color.special_key_dark + } else { + R.color.special_key_light + } + val specialKeyColorValue = resources.getColor(specialKeyColorResource, context.theme) val focusedColorResId = if (isUserDarkMode) { R.color.theme_scribe_blue @@ -895,9 +897,9 @@ class KeyboardView if (isUserDarkMode) R.color.dark_keyboard_bg_color else R.color.light_keyboard_bg_color, context.theme, ) - canvas.drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) + drawColor(Color.TRANSPARENT, PorterDuff.Mode.CLEAR) if (id != R.id.mini_keyboard_view) { - canvas.drawColor(mKeyboardBackgroundColor) + drawColor(mKeyboardBackgroundColor) } val keyCount = keys.size @@ -957,21 +959,21 @@ class KeyboardView (key.y + key.height - vKeyMargin + shadowOffset - padding).toFloat(), ) if (code != EXTRA_PADDING && (mPopupParent.id != R.id.mini_keyboard_view)) { - canvas.drawRoundRect(shadowRect, rectRadius, rectRadius, shadowPaint) + drawRoundRect(shadowRect, rectRadius, rectRadius, shadowPaint) } val backgroundColor = when { key.focused -> focusedColor key.pressed -> pressedColor - code == KEYCODE_SHIFT && mKeyboard!!.mShiftState == SHIFT_LOCKED -> pressedColor + code == KEYCODE_SHIFT && mKeyboard?.mShiftState == SHIFT_LOCKED -> pressedColor code in listOf(KEYCODE_DELETE, KEYCODE_SHIFT, KEYCODE_MODE_CHANGE) -> specialKeyColorValue code == KEYCODE_ENTER -> mEnterKeyColor else -> keyBackgroundColor } keyBackgroundPaint.color = backgroundColor if (code != EXTRA_PADDING) { - canvas.drawRoundRect(keyRect, rectRadius, rectRadius, keyBackgroundPaint) + drawRoundRect(keyRect, rectRadius, rectRadius, keyBackgroundPaint) } var label = adjustCase(key.label)?.toString() // Switch the character to uppercase if shift is pressed. @@ -1061,7 +1063,7 @@ class KeyboardView } } - canvas.translate(key.x.toFloat(), key.y.toFloat()) + translate(key.x.toFloat(), key.y.toFloat()) if (label?.isNotEmpty() == true) { // For characters, use large font. For labels like "Done", use small font. if (label.length > 1) { @@ -1080,7 +1082,7 @@ class KeyboardView else -> mTextColor } - canvas.drawText( + drawText( label, (key.width / 2).toFloat(), key.height / 2 + (paint.textSize - paint.descent()) / 2, @@ -1088,7 +1090,7 @@ class KeyboardView ) if (key.topSmallNumber.isNotEmpty()) { - canvas.drawText( + drawText( key.topSmallNumber, key.width - mTopSmallNumberMarginWidth - leftShiftForLabel, mTopSmallNumberMarginHeight, @@ -1101,7 +1103,7 @@ class KeyboardView } else if (key.icon != null && mKeyboard != null) { if (code == KEYCODE_SHIFT) { val drawableId = - when (mKeyboard!!.mShiftState) { + when (mKeyboard?.mShiftState) { SHIFT_OFF -> R.drawable.ic_caps_outline_vector SHIFT_ON_ONE_CHAR -> R.drawable.ic_caps_vector SHIFT_LOCKED -> R.drawable.ic_caps_underlined_vector @@ -1110,12 +1112,16 @@ class KeyboardView key.icon = resources.getDrawable(drawableId, context.theme) } else if (code == KEYCODE_CAPS_LOCK) { val drawableId = - when (mKeyboard!!.mShiftState) { + when (mKeyboard?.mShiftState) { SHIFT_LOCKED -> R.drawable.ic_caps_lock_on else -> R.drawable.ic_caps_lock_off } - key.icon = resources.getDrawable(drawableId, context.theme) - key.icon!!.applyColorFilter(mTextColor) + key.icon = + resources + .getDrawable(drawableId, context.theme) + .also { + it.applyColorFilter(mTextColor) + } } if (code == KEYCODE_LEFT_ARROW || code == KEYCODE_RIGHT_ARROW) { @@ -1126,14 +1132,18 @@ class KeyboardView else -> null } drawableId?.let { - key.icon = resources.getDrawable(it, context.theme) - key.icon!!.applyColorFilter(mTextColor) + key.icon = + resources + .getDrawable(it, context.theme) + .also { icon -> + icon.applyColorFilter(mTextColor) + } } } if (code == KEYCODE_ENTER) { val drawableId = - when (mKeyboard!!.mEnterKeyType) { + when (mKeyboard?.mEnterKeyType) { EditorInfo.IME_ACTION_SEARCH -> R.drawable.ic_search_vector @@ -1151,8 +1161,12 @@ class KeyboardView else -> R.drawable.ic_enter_vector } - key.icon = resources.getDrawable(drawableId) - key.icon!!.applyColorFilter(mTextColor) + key.icon = + resources + .getDrawable(drawableId) + .also { + it.applyColorFilter(mTextColor) + } } else { if (code == KeyboardBase.KEYCODE_FLOAT_TOGGLE) { val isFloating = @@ -1174,54 +1188,54 @@ class KeyboardView code == KeyboardBase.KEYCODE_FLOAT_TOGGLE || code == KeyboardBase.KEYCODE_EMOJI if (isIconOnlyKey) { - key.icon!!.applyColorFilter(mTextColor) + key.icon?.applyColorFilter(mTextColor) } } // Controls where icons are located on their keys. - var iconWidth = key.icon!!.intrinsicWidth - var iconHeight = key.icon!!.intrinsicHeight - val isEmojiOrClipboard = - code == KeyboardBase.KEYCODE_EMOJI || - code == KeyboardBase.KEYCODE_CLIPBOARD || - code == KeyboardBase.KEYCODE_FLOAT_TOGGLE - val scaleFactor = if (isEmojiOrClipboard) 0.5f else 0.6f - val maxIconWidth = (key.width * scaleFactor).toInt() - val maxIconHeight = (key.height * scaleFactor).toInt() - if (iconWidth > maxIconWidth || iconHeight > maxIconHeight) { - val ratio = iconWidth.toFloat() / iconHeight.toFloat() - if (ratio > 1) { - iconWidth = maxIconWidth - iconHeight = (maxIconWidth / ratio).toInt() - } else { - iconHeight = maxIconHeight - iconWidth = (maxIconHeight * ratio).toInt() + key.icon?.let { icon -> + var iconWidth = icon.intrinsicWidth + var iconHeight = icon.intrinsicHeight + val isEmojiOrClipboard = + code == KeyboardBase.KEYCODE_EMOJI || + code == KeyboardBase.KEYCODE_CLIPBOARD || + code == KeyboardBase.KEYCODE_FLOAT_TOGGLE + val scaleFactor = if (isEmojiOrClipboard) 0.5f else 0.6f + val maxIconWidth = (key.width * scaleFactor).toInt() + val maxIconHeight = (key.height * scaleFactor).toInt() + if (iconWidth > maxIconWidth || iconHeight > maxIconHeight) { + val ratio = iconWidth.toFloat() / iconHeight.toFloat() + if (ratio > 1) { + iconWidth = maxIconWidth + iconHeight = (maxIconWidth / ratio).toInt() + } else { + iconHeight = maxIconHeight + iconWidth = (maxIconHeight * ratio).toInt() + } + } + val drawableX = (key.width - iconWidth) / 2 + val drawableY = (key.height - iconHeight) / 2 + translate(drawableX.toFloat(), drawableY.toFloat()) + icon.setBounds(0, 0, iconWidth, iconHeight) + icon.draw(this) + translate(-drawableX.toFloat(), -drawableY.toFloat()) + + if (code == KeyboardBase.KEYCODE_EMOJI && id != R.id.mini_keyboard_view) { + val settingsIcon = resources.getDrawable(R.drawable.ic_settings_cog_vector, context.theme) + settingsIcon.applyColorFilter(mTextColor) + val density = context.resources.displayMetrics.density + val cogSize = (12 * density).toInt() + val rightPadding = keyMargin - shadowOffset + padding + (2 * density).toInt() + val topPadding = keyMargin - shadowOffset + padding + (2 * density).toInt() + val cogX = key.width - cogSize - rightPadding + val cogY = topPadding + settingsIcon.setBounds(cogX, cogY, cogX + cogSize, cogY + cogSize) + settingsIcon.draw(this) } - } - val drawableX = (key.width - iconWidth) / 2 - val drawableY = (key.height - iconHeight) / 2 - canvas.translate(drawableX.toFloat(), drawableY.toFloat()) - key.icon!!.setBounds(0, 0, iconWidth, iconHeight) - key.icon!!.draw(canvas) - canvas.translate(-drawableX.toFloat(), -drawableY.toFloat()) - - if (code == KeyboardBase.KEYCODE_EMOJI && id != R.id.mini_keyboard_view) { - val settingsIcon = resources.getDrawable(R.drawable.ic_settings_cog_vector, context.theme) - settingsIcon.applyColorFilter(mTextColor) - val density = context.resources.displayMetrics.density - val cogSize = (12 * density).toInt() - val rightPadding = keyMargin - shadowOffset + padding + (2 * density).toInt() - val topPadding = keyMargin - shadowOffset + padding + (2 * density).toInt() - val cogX = key.width - cogSize - rightPadding - val cogY = topPadding - settingsIcon.setBounds(cogX, cogY, cogX + cogSize, cogY + cogSize) - settingsIcon.draw(canvas) } } - canvas.translate(-key.x.toFloat(), -key.y.toFloat()) + translate(-key.x.toFloat(), -key.y.toFloat()) } - - mCanvas!! } mDrawPending = false mDirtyRect.setEmpty() @@ -1244,7 +1258,7 @@ class KeyboardView if (index != NOT_A_KEY && index < mKeys.size) { val key = mKeys[index] getPressedKeyIndex(x, y) - mOnKeyboardActionListener!!.onKey(key.code) + mOnKeyboardActionListener?.onKey(key.code) mLastTapTime = eventTime } } @@ -1286,10 +1300,12 @@ class KeyboardView if (oldKeyIndex != mCurrentKeyIndex) { if (previewPopup.isShowing) { if (keyIndex == NOT_A_KEY) { - mHandler!!.sendMessageDelayed( - mHandler!!.obtainMessage(MSG_REMOVE_PREVIEW), - DELAY_AFTER_PREVIEW.toLong(), - ) + mHandler?.let { + it.sendMessageDelayed( + it.obtainMessage(MSG_REMOVE_PREVIEW), + DELAY_AFTER_PREVIEW.toLong(), + ) + } } } @@ -1358,7 +1374,7 @@ class KeyboardView mPopupPreviewX = key.x mPopupPreviewY = key.y - popupHeight - mHandler!!.removeMessages(MSG_REMOVE_PREVIEW) + mHandler?.removeMessages(MSG_REMOVE_PREVIEW) getLocationInWindow(mCoordinates) mCoordinates[0] += mMiniKeyboardOffsetX // offset may be zero mCoordinates[1] += mMiniKeyboardOffsetY // offset may be zero @@ -1496,64 +1512,63 @@ class KeyboardView val inflater = context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater mMiniKeyboardContainer = inflater.inflate(mPopupLayout, null) mMiniKeyboard = - mMiniKeyboardContainer!! - .findViewById(R.id.mini_keyboard_view) - as KeyboardView - - mMiniKeyboard!!.mOnKeyboardActionListener = - object : OnKeyboardActionListener { - override fun onKey(code: Int) { - mOnKeyboardActionListener!!.onKey(code) - dismissPopupKeyboard() - } + ( + mMiniKeyboardContainer?.findViewById(R.id.mini_keyboard_view) + as KeyboardView + ).also { + it.mOnKeyboardActionListener = + object : OnKeyboardActionListener { + override fun onKey(code: Int) { + mOnKeyboardActionListener?.onKey(code) + dismissPopupKeyboard() + } - override fun onPress(primaryCode: Int) { - mOnKeyboardActionListener!!.onPress(primaryCode) - } + override fun onPress(primaryCode: Int) { + mOnKeyboardActionListener?.onPress(primaryCode) + } - override fun onActionUp() { - mOnKeyboardActionListener!!.onActionUp() - } + override fun onActionUp() { + mOnKeyboardActionListener?.onActionUp() + } - override fun moveCursorLeft() { - mOnKeyboardActionListener!!.moveCursorLeft() - } + override fun moveCursorLeft() { + mOnKeyboardActionListener?.moveCursorLeft() + } - override fun moveCursorRight() { - mOnKeyboardActionListener!!.moveCursorRight() - } + override fun moveCursorRight() { + mOnKeyboardActionListener?.moveCursorRight() + } - override fun onText(text: String) { - mOnKeyboardActionListener!!.onText(text) - } + override fun onText(text: String) { + mOnKeyboardActionListener?.onText(text) + } - override fun hasTextBeforeCursor(): Boolean = - mOnKeyboardActionListener!! - .hasTextBeforeCursor() + override fun hasTextBeforeCursor(): Boolean = mOnKeyboardActionListener?.hasTextBeforeCursor() ?: false - override fun commitPeriodAfterSpace() { - mOnKeyboardActionListener!!.commitPeriodAfterSpace() - } + override fun commitPeriodAfterSpace() { + mOnKeyboardActionListener?.commitPeriodAfterSpace() + } + } } + val popupCharacters = popupKey.popupCharacters val keyboard = - if (popupKey.popupCharacters != null) { - KeyboardBase(context, popupKeyboardId, popupKey.popupCharacters!!, popupKey.width) + if (popupCharacters != null) { + KeyboardBase(context, popupKeyboardId, popupCharacters, popupKey.width) } else { KeyboardBase(context, popupKeyboardId, 0) } - mMiniKeyboard!!.setKeyboard(keyboard) + mMiniKeyboard?.setKeyboard(keyboard) mPopupParent = this - mMiniKeyboardContainer!!.measure( + mMiniKeyboardContainer?.measure( MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST), MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST), ) mMiniKeyboardCache[popupKey] = mMiniKeyboardContainer } else { mMiniKeyboard = - mMiniKeyboardContainer!! - .findViewById(R.id.mini_keyboard_view) as KeyboardView + mMiniKeyboardContainer?.findViewById(R.id.mini_keyboard_view) as KeyboardView } val isUserDarkMode = @@ -1566,7 +1581,7 @@ class KeyboardView context.theme, ) - mMiniKeyboard!!.background?.let { bg -> + mMiniKeyboard?.background?.let { bg -> if (bg is LayerDrawable) { bg .findDrawableByLayerId(R.id.button_background_shape) @@ -1717,8 +1732,9 @@ class KeyboardView mMiniKeyboardSelectedKeyIndex = -1 hoverRunnable = null dismissPopupKeyboard() + }.also { + hoverHandler?.postDelayed(it, hoverDelay) } - hoverHandler?.postDelayed(hoverRunnable!!, hoverDelay) } else { hoverRunnable = Runnable { @@ -1730,8 +1746,9 @@ class KeyboardView mMiniKeyboardSelectedKeyIndex = -1 hoverRunnable = null dismissPopupKeyboard() + }.also { + hoverHandler?.postDelayed(it, 220L) } - hoverHandler?.postDelayed(hoverRunnable!!, 220L) } } // Emoji popup: no auto-fire on hover; wait for finger lift (ACTION_UP). @@ -1744,10 +1761,12 @@ class KeyboardView // Fire whichever key is currently highlighted when the finger lifts. val idx = mMiniKeyboardSelectedKeyIndex if (idx >= 0 && idx < (mMiniKeyboard?.mKeys?.size ?: 0)) { - val key = mMiniKeyboard!!.mKeys[idx] - key.focused = false - mMiniKeyboard!!.invalidateAllKeys() - mOnKeyboardActionListener?.onKey(key.code) + val key = mMiniKeyboard?.mKeys[idx] + if (key != null) { + key.focused = false + mMiniKeyboard?.invalidateAllKeys() + mOnKeyboardActionListener?.onKey(key.code) + } } mMiniKeyboardSelectedKeyIndex = -1 dismissPopupKeyboard() @@ -1765,9 +1784,11 @@ class KeyboardView if (!isEmojiPopup && setHoldForAltCharacters) { if (mMiniKeyboardSelectedKeyIndex >= 0) { - val key = mMiniKeyboard!!.mKeys[mMiniKeyboardSelectedKeyIndex] - mOnKeyboardActionListener?.onKey(key.code) - mMiniKeyboardSelectedKeyIndex = -1 + val key = mMiniKeyboard?.mKeys[mMiniKeyboardSelectedKeyIndex] + if (key != null) { + mOnKeyboardActionListener?.onKey(key.code) + mMiniKeyboardSelectedKeyIndex = -1 + } } mMiniKeyboardSelectedKeyIndex = -1 dismissPopupKeyboard() @@ -1816,7 +1837,7 @@ class KeyboardView // type in both chars and ignore the later gestures. // Can happen at fast typing, easier to reproduce by increasing LONGPRESS_TIMEOUT. ignoreTouches = true - mHandler!!.removeMessages(MSG_LONGPRESS) + mHandler?.removeMessages(MSG_LONGPRESS) dismissPopupKeyboard() detectAndSendKey(keyIndex, touchX, touchY, eventTime) @@ -1827,7 +1848,7 @@ class KeyboardView detectAndSendKey(secondKeyIndex, newPointerX, newPointerY, eventTime) val secondKeyCode = mKeys.getOrNull(secondKeyIndex)?.code - secondKeyCode?.let { mOnKeyboardActionListener!!.onPress(it) } + secondKeyCode?.let { mOnKeyboardActionListener?.onPress(it) } showPreview(NOT_A_KEY) invalidateKey(mCurrentKey) @@ -1845,12 +1866,15 @@ class KeyboardView mLastMoveTime = eventTime val onPressKey = if (keyIndex != NOT_A_KEY) mKeys[keyIndex].code else 0 - mOnKeyboardActionListener!!.onPress(onPressKey) + mOnKeyboardActionListener?.onPress(onPressKey) if (mCurrentKey >= 0 && mKeys[mCurrentKey].repeatable) { mRepeatKeyIndex = mCurrentKey - val msg = mHandler!!.obtainMessage(MSG_REPEAT) - mHandler!!.sendMessageDelayed(msg, REPEAT_START_DELAY.toLong()) + val handler = mHandler + if (handler != null) { + val msg = handler.obtainMessage(MSG_REPEAT) + handler.sendMessageDelayed(msg, REPEAT_START_DELAY.toLong()) + } // If the user long presses Space, move the cursor after swipine left/right. if (mKeys[mCurrentKey].code == KEYCODE_SPACE) { mLastSpaceMoveX = -1 @@ -1872,8 +1896,11 @@ class KeyboardView } if (!handled && mCurrentKey != NOT_A_KEY) { - val msg = mHandler!!.obtainMessage(MSG_LONGPRESS, me) - mHandler!!.sendMessageDelayed(msg, LONGPRESS_TIMEOUT.toLong()) + val handler = mHandler + if (handler != null) { + val msg = handler.obtainMessage(MSG_LONGPRESS, me) + handler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT.toLong()) + } } if (mPopupParent.id != R.id.mini_keyboard_view) { @@ -1920,17 +1947,20 @@ class KeyboardView } } else if (!continueLongPress) { // Cancel old longpress. - mHandler!!.removeMessages(MSG_LONGPRESS) - // Start new longpress if key has changed. - if (keyIndex != NOT_A_KEY) { - val msg = mHandler!!.obtainMessage(MSG_LONGPRESS, me) - mHandler!!.sendMessageDelayed(msg, LONGPRESS_TIMEOUT.toLong()) - } + val handler = mHandler + if (handler != null) { + handler.removeMessages(MSG_LONGPRESS) + // Start new longpress if key has changed. + if (keyIndex != NOT_A_KEY) { + val msg = handler.obtainMessage(MSG_LONGPRESS, me) + handler.sendMessageDelayed(msg, LONGPRESS_TIMEOUT.toLong()) + } - if (mPopupParent.id != R.id.mini_keyboard_view) { - showPreview(mCurrentKey) + if (mPopupParent.id != R.id.mini_keyboard_view) { + showPreview(mCurrentKey) + } + mLastMoveTime = eventTime } - mLastMoveTime = eventTime } } MotionEvent.ACTION_UP -> { @@ -1965,9 +1995,9 @@ class KeyboardView if (mKeys.getOrNull(mCurrentKey)?.code == KEYCODE_SPACE && !mIsLongPressingSpace) { val currentTime = System.currentTimeMillis() if (currentTime - lastSpaceBarTapTime < DOUBLE_TAP_DELAY + EXTRA_DELAY && - mOnKeyboardActionListener!!.hasTextBeforeCursor() + mOnKeyboardActionListener?.hasTextBeforeCursor() == true ) { - mOnKeyboardActionListener!!.commitPeriodAfterSpace() + mOnKeyboardActionListener?.commitPeriodAfterSpace() } else { detectAndSendKey(mCurrentKey, touchX, touchY, eventTime) } @@ -1980,7 +2010,7 @@ class KeyboardView mOnKeyboardActionListener?.setDeleteRepeating(false) } mRepeatKeyIndex = NOT_A_KEY - mOnKeyboardActionListener!!.onActionUp() + mOnKeyboardActionListener?.onActionUp() mIsLongPressingSpace = false } MotionEvent.ACTION_CANCEL -> { From c3d0d7ccd5a785749e8b6f0161a9c2ef8ee39eaa Mon Sep 17 00:00:00 2001 From: Chris Witt Date: Fri, 2 Oct 2026 13:27:50 -0400 Subject: [PATCH 7/8] CW - Fixing non-null assertions and some light refactoring. --- .../main/java/be/scri/views/KeyboardView.kt | 41 ++++++++++--------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/app/src/main/java/be/scri/views/KeyboardView.kt b/app/src/main/java/be/scri/views/KeyboardView.kt index 668578480..1a2c9f215 100644 --- a/app/src/main/java/be/scri/views/KeyboardView.kt +++ b/app/src/main/java/be/scri/views/KeyboardView.kt @@ -1171,7 +1171,7 @@ class KeyboardView if (code == KeyboardBase.KEYCODE_FLOAT_TOGGLE) { val isFloating = (context as? KeyboardBase.KeyboardContextProvider)?.isFloatingModeActive() == true || - (mPopupParent?.context as? KeyboardBase.KeyboardContextProvider)?.isFloatingModeActive() == true + (mPopupParent.context as? KeyboardBase.KeyboardContextProvider)?.isFloatingModeActive() == true val floatIconRes = if (isFloating) { R.drawable.ic_keyboard_dismiss @@ -1338,7 +1338,7 @@ class KeyboardView previewText.setCompoundDrawables(null, null, null, null) try { previewText.text = adjustCase(key.label) - } catch (ignored: Exception) { + } catch (_: Exception) { } } @@ -1571,6 +1571,9 @@ class KeyboardView mMiniKeyboardContainer?.findViewById(R.id.mini_keyboard_view) as KeyboardView } + val miniKeyboardContainer = mMiniKeyboardContainer ?: return false + val miniKeyboard = mMiniKeyboard ?: return false + val isUserDarkMode = be.scri.helpers.PreferencesHelper .getIsDarkModeOrNot(context) @@ -1581,7 +1584,7 @@ class KeyboardView context.theme, ) - mMiniKeyboard?.background?.let { bg -> + miniKeyboard.background?.let { bg -> if (bg is LayerDrawable) { bg .findDrawableByLayerId(R.id.button_background_shape) @@ -1596,25 +1599,25 @@ class KeyboardView mPopupX = popupKey.x mPopupY = popupKey.y - var leftX = popupKey.x + (popupKey.width - mMiniKeyboardContainer!!.measuredWidth) / 2 - leftX = leftX.coerceIn(0, (width - mMiniKeyboardContainer!!.measuredWidth).coerceAtLeast(0)) + var leftX = popupKey.x + (popupKey.width - miniKeyboardContainer.measuredWidth) / 2 + leftX = leftX.coerceIn(0, (width - miniKeyboardContainer.measuredWidth).coerceAtLeast(0)) mPopupX = leftX - mPopupY -= mMiniKeyboardContainer!!.measuredHeight + mPopupY -= miniKeyboardContainer.measuredHeight val x = mPopupX + mCoordinates[0] val y = mPopupY + mCoordinates[1] val xOffset = Math.max(0, x) - mMiniKeyboard!!.setPopupOffset(xOffset, y) + miniKeyboard.setPopupOffset(xOffset, y) // Make sure we highlight the proper key right after long pressing it, // before any ACTION_MOVE event occurs. val miniKeyboardX = - if (xOffset + mMiniKeyboard!!.measuredWidth <= measuredWidth) { + if (xOffset + miniKeyboard.measuredWidth <= measuredWidth) { xOffset } else { - measuredWidth - mMiniKeyboard!!.measuredWidth + measuredWidth - miniKeyboard.measuredWidth } - val keysCnt = mMiniKeyboard!!.mKeys.size + val keysCnt = miniKeyboard.mKeys.size var selectedKeyIndex = Math.floor((me.rawX - miniKeyboardX) / popupKey.width.toDouble()).toInt() if (keysCnt > MAX_KEYS_PER_MINI_ROW) { selectedKeyIndex += MAX_KEYS_PER_MINI_ROW @@ -1622,7 +1625,7 @@ class KeyboardView selectedKeyIndex = Math.max(0, Math.min(selectedKeyIndex, keysCnt - 1)) val isEmojiPopup = - mMiniKeyboard!!.mKeys.any { + miniKeyboard.mKeys.any { it.code == KeyboardBase.KEYCODE_EMOJI || it.code == KeyboardBase.KEYCODE_CLIPBOARD || it.code == KeyboardBase.KEYCODE_FLOAT_TOGGLE @@ -1630,27 +1633,27 @@ class KeyboardView if (isEmojiPopup) { // Emoji popup: start with no pre-selection; user slides to choose and lifts to confirm. for (i in 0 until keysCnt) { - mMiniKeyboard!!.mKeys[i].focused = false + miniKeyboard.mKeys[i].focused = false } mMiniKeyboardSelectedKeyIndex = -1 } else if (setHoldForAltCharacters) { for (i in 0 until keysCnt) { - mMiniKeyboard!!.mKeys[i].focused = i == selectedKeyIndex + miniKeyboard.mKeys[i].focused = i == selectedKeyIndex } mMiniKeyboardSelectedKeyIndex = selectedKeyIndex } else { for (i in 0 until keysCnt) { - mMiniKeyboard!!.mKeys[i].focused = false + miniKeyboard.mKeys[i].focused = false } mMiniKeyboardSelectedKeyIndex = -1 } - mMiniKeyboard!!.invalidateAllKeys() + miniKeyboard.invalidateAllKeys() val miniShiftStatus = if (isShifted()) SHIFT_ON_PERMANENT else SHIFT_OFF - mMiniKeyboard!!.setShifted(miniShiftStatus) - mPopupKeyboard.contentView = mMiniKeyboardContainer - mPopupKeyboard.width = mMiniKeyboardContainer!!.measuredWidth - mPopupKeyboard.height = mMiniKeyboardContainer!!.measuredHeight + miniKeyboard.setShifted(miniShiftStatus) + mPopupKeyboard.contentView = miniKeyboardContainer + mPopupKeyboard.width = miniKeyboardContainer.measuredWidth + mPopupKeyboard.height = miniKeyboardContainer.measuredHeight mPopupKeyboard.showAtLocation(this, Gravity.NO_GRAVITY, x, y) mMiniKeyboardOnScreen = true invalidateAllKeys() From 78062f6a43a4ca4c8f6795963c08a8963a1f165d Mon Sep 17 00:00:00 2001 From: Chris Witt Date: Fri, 2 Oct 2026 15:44:54 -0400 Subject: [PATCH 8/8] CW - Updating changelog for non-null assertion cleanup. --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3165e79ba..0dc6e4cf9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -106,3 +106,4 @@ Emojis for the following are chosen based on [gitmoji](https://gitmoji.dev/). - 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.