From 5890d4a398a18feca4f809f2a0f030ce5e5421a1 Mon Sep 17 00:00:00 2001 From: Chris Witt Date: Fri, 2 Oct 2026 17:37:25 -0400 Subject: [PATCH 1/2] 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/2] 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.