From 65156be27a9343113cdfc47d973a94254b556dc2 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Tue, 1 Sep 2026 01:21:03 +0100 Subject: [PATCH 1/5] refactor: move showSoftInput to Compat Prep for compileSdk 37: `SHOW_IMPLICIT` is deprecated in API 37. The flag is a no-op from Android 16, so drop it on API 36+ Part of 21672 Assisted-by: Claude Fable 5 --- .../java/com/ichi2/anki/dialogs/FlagAdapter.kt | 6 ++---- .../main/java/com/ichi2/utils/AndroidUiUtils.kt | 7 +++---- .../java/com/ichi2/anki/compat/BaseCompat.kt | 7 +++++++ .../main/java/com/ichi2/anki/compat/Compat.kt | 17 +++++++++++++++++ .../java/com/ichi2/anki/compat/CompatHelper.kt | 1 + .../java/com/ichi2/anki/compat/CompatV36.kt | 17 +++++++++++++++++ 6 files changed, 47 insertions(+), 8 deletions(-) create mode 100644 compat/src/main/java/com/ichi2/anki/compat/CompatV36.kt diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/FlagAdapter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/FlagAdapter.kt index f6fc8f9e98f7..1b9fe8b1a904 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/FlagAdapter.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/FlagAdapter.kt @@ -17,11 +17,9 @@ package com.ichi2.anki.dialogs -import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.view.inputmethod.InputMethodManager import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView @@ -31,6 +29,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.google.android.material.textfield.TextInputEditText import com.ichi2.anki.Flag +import com.ichi2.anki.compat.CompatHelper import com.ichi2.anki.databinding.ItemEditFlagBinding import com.ichi2.utils.moveCursorToEnd import kotlinx.coroutines.CoroutineScope @@ -86,8 +85,7 @@ class FlagAdapter( holder.flagNameEditLayout.visibility = View.VISIBLE holder.flagNameEdit.requestFocus() holder.flagNameEdit.moveCursorToEnd() - val inputMethodManager = holder.flagNameEdit.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.showSoftInput(holder.flagNameEdit, InputMethodManager.SHOW_IMPLICIT) + CompatHelper.compat.showSoftInput(holder.flagNameEdit) } holder.saveButton.setOnClickListener { diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/AndroidUiUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/AndroidUiUtils.kt index 884b5a264948..82abedea9663 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/AndroidUiUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/AndroidUiUtils.kt @@ -21,6 +21,7 @@ import android.view.View import android.view.Window import android.view.WindowManager import android.view.inputmethod.InputMethodManager +import com.ichi2.anki.compat.CompatHelper object AndroidUiUtils { /** @@ -31,8 +32,7 @@ object AndroidUiUtils { */ fun Activity?.showSoftInput() { val currentFocus = this?.currentFocus ?: return - val imm = this.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(currentFocus, InputMethodManager.SHOW_IMPLICIT) + CompatHelper.compat.showSoftInput(currentFocus) } /** @@ -61,8 +61,7 @@ object AndroidUiUtils { // Required on some Android 9, 10 devices to show keyboard: https://stackoverflow.com/a/7784904 view.postDelayed({ view.requestFocus() - val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) + CompatHelper.compat.showSoftInput(view) runnable?.run() }, 200) } diff --git a/compat/src/main/java/com/ichi2/anki/compat/BaseCompat.kt b/compat/src/main/java/com/ichi2/anki/compat/BaseCompat.kt index f8e1b07a9dd2..2802679fe083 100644 --- a/compat/src/main/java/com/ichi2/anki/compat/BaseCompat.kt +++ b/compat/src/main/java/com/ichi2/anki/compat/BaseCompat.kt @@ -23,6 +23,7 @@ import android.provider.MediaStore import android.view.View import android.view.Window import android.view.WindowManager +import android.view.inputmethod.InputMethodManager import androidx.annotation.AnimRes import androidx.appcompat.widget.TooltipCompat import com.ichi2.anki.common.utils.annotation.KotlinCleanup @@ -54,6 +55,12 @@ open class BaseCompat : Compat { ) } + // Until API36, SHOW_IMPLICIT marks the request as one the system may ignore + override fun showSoftInput(view: View): Boolean { + val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + return imm.showSoftInput(view, InputMethodManager.SHOW_IMPLICIT) + } + override fun overrideTransition( activity: Activity, @AnimRes enter: Int, diff --git a/compat/src/main/java/com/ichi2/anki/compat/Compat.kt b/compat/src/main/java/com/ichi2/anki/compat/Compat.kt index 8746393d843d..21f32fe5e622 100644 --- a/compat/src/main/java/com/ichi2/anki/compat/Compat.kt +++ b/compat/src/main/java/com/ichi2/anki/compat/Compat.kt @@ -80,6 +80,23 @@ interface Compat { fun hideStatusBar(window: Window) + /** + * Request that the current input method's soft input area be shown to the + * user, if needed. + * + * Below Android 16, this is an implicit request: the window may not be + * shown, for example if a hardware keyboard is connected. + * + * @param view The currently focused view, which would like to receive soft keyboard input. + * Note that this view is only considered focused here if both it itself has + * [view focus][View.isFocused], and its containing window has + * [window focus][View.hasWindowFocus]. Otherwise the call fails and returns `false`. + * @return `true` if a request was sent to system_server, `false` otherwise. Note: + * this does not return result of the request. + * @see android.view.inputmethod.InputMethodManager.showSoftInput + */ + fun showSoftInput(view: View): Boolean + fun overrideTransition( activity: Activity, @AnimRes enter: Int, diff --git a/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt b/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt index 6ba97f4429df..2e65b1e09799 100644 --- a/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt +++ b/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt @@ -40,6 +40,7 @@ class CompatHelper private constructor() { @SuppressLint("NewApi") private val compatValue: Compat = when { + sdkVersion >= Build.VERSION_CODES.BAKLAVA -> CompatV36() sdkVersion >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE -> CompatV34() sdkVersion >= Build.VERSION_CODES.TIRAMISU -> CompatV33() sdkVersion >= Build.VERSION_CODES.S -> CompatV31() diff --git a/compat/src/main/java/com/ichi2/anki/compat/CompatV36.kt b/compat/src/main/java/com/ichi2/anki/compat/CompatV36.kt new file mode 100644 index 000000000000..b839d57d3698 --- /dev/null +++ b/compat/src/main/java/com/ichi2/anki/compat/CompatV36.kt @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0-or-later + +package com.ichi2.anki.compat + +import android.content.Context +import android.view.View +import android.view.inputmethod.InputMethodManager +import androidx.annotation.RequiresApi + +@RequiresApi(36) +open class CompatV36 : CompatV34() { + // From API36, SHOW_IMPLICIT is a no-op (and deprecated in API37) + override fun showSoftInput(view: View): Boolean { + val imm = view.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + return imm.showSoftInput(view, 0) + } +} From ab877ac2fa09ed6abb80c94e72b9dbb97ee94618 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Tue, 1 Sep 2026 01:35:18 +0100 Subject: [PATCH 2/5] refactor: expose View.showSoftInput Simplifies call sites Part of 21672 Assisted-by: Claude Fable 5 --- .../src/main/java/com/ichi2/anki/dialogs/FlagAdapter.kt | 4 ++-- AnkiDroid/src/main/java/com/ichi2/utils/AndroidUiUtils.kt | 6 +++--- compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt | 7 +++++++ 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/FlagAdapter.kt b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/FlagAdapter.kt index 1b9fe8b1a904..db74b7083326 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/FlagAdapter.kt +++ b/AnkiDroid/src/main/java/com/ichi2/anki/dialogs/FlagAdapter.kt @@ -29,7 +29,7 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.button.MaterialButton import com.google.android.material.textfield.TextInputEditText import com.ichi2.anki.Flag -import com.ichi2.anki.compat.CompatHelper +import com.ichi2.anki.compat.showSoftInput import com.ichi2.anki.databinding.ItemEditFlagBinding import com.ichi2.utils.moveCursorToEnd import kotlinx.coroutines.CoroutineScope @@ -85,7 +85,7 @@ class FlagAdapter( holder.flagNameEditLayout.visibility = View.VISIBLE holder.flagNameEdit.requestFocus() holder.flagNameEdit.moveCursorToEnd() - CompatHelper.compat.showSoftInput(holder.flagNameEdit) + holder.flagNameEdit.showSoftInput() } holder.saveButton.setOnClickListener { diff --git a/AnkiDroid/src/main/java/com/ichi2/utils/AndroidUiUtils.kt b/AnkiDroid/src/main/java/com/ichi2/utils/AndroidUiUtils.kt index 82abedea9663..0ea5bbaf6f3a 100644 --- a/AnkiDroid/src/main/java/com/ichi2/utils/AndroidUiUtils.kt +++ b/AnkiDroid/src/main/java/com/ichi2/utils/AndroidUiUtils.kt @@ -21,7 +21,7 @@ import android.view.View import android.view.Window import android.view.WindowManager import android.view.inputmethod.InputMethodManager -import com.ichi2.anki.compat.CompatHelper +import com.ichi2.anki.compat.showSoftInput object AndroidUiUtils { /** @@ -32,7 +32,7 @@ object AndroidUiUtils { */ fun Activity?.showSoftInput() { val currentFocus = this?.currentFocus ?: return - CompatHelper.compat.showSoftInput(currentFocus) + currentFocus.showSoftInput() } /** @@ -61,7 +61,7 @@ object AndroidUiUtils { // Required on some Android 9, 10 devices to show keyboard: https://stackoverflow.com/a/7784904 view.postDelayed({ view.requestFocus() - CompatHelper.compat.showSoftInput(view) + view.showSoftInput() runnable?.run() }, 200) } diff --git a/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt b/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt index 2e65b1e09799..a24c81594c4a 100644 --- a/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt +++ b/compat/src/main/java/com/ichi2/anki/compat/CompatHelper.kt @@ -210,6 +210,13 @@ class CompatHelper private constructor() { */ fun View.setTooltipTextCompat(tooltipText: CharSequence?) = TooltipCompat.setTooltipText(this, tooltipText) +/** + * Request that the current input method's soft input area be shown to the user, if needed. + * + * @see Compat.showSoftInput + */ +fun View.showSoftInput(): Boolean = compat.showSoftInput(this) + inline fun Bundle.requireSerializableCompat(key: String): T = requireNotNull(compat.getSerializable(this, key, T::class.java)) { "key: '$key' not found or null" From b9762e89883237c709a03e7aa2ce927408234f13 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Tue, 1 Sep 2026 01:34:00 +0100 Subject: [PATCH 3/5] refactor: use WindowInsetsControllerCompat to hide status bar Prep for compileSdk 37: its annotation metadata declares an empty IntDef for the platform `InsetsType` typedef, so lint reports a `WrongConstant` false positive on `WindowInsetsController.hide`. The androidx annotations are unaffected Part of 21672 Assisted-by: Claude Fable 5 --- compat/src/main/java/com/ichi2/anki/compat/CompatV30.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compat/src/main/java/com/ichi2/anki/compat/CompatV30.kt b/compat/src/main/java/com/ichi2/anki/compat/CompatV30.kt index f45e08302d6b..c0b949f9012f 100644 --- a/compat/src/main/java/com/ichi2/anki/compat/CompatV30.kt +++ b/compat/src/main/java/com/ichi2/anki/compat/CompatV30.kt @@ -5,7 +5,8 @@ package com.ichi2.anki.compat import android.view.Window import androidx.annotation.RequiresApi -import androidx.core.view.WindowInsetsCompat +import androidx.core.view.WindowCompat +import androidx.core.view.WindowInsetsCompat.Type.statusBars @RequiresApi(30) @Suppress("ktlint:standard:property-naming") @@ -21,7 +22,6 @@ open class CompatV30 : CompatV29() { if (view == null) { return } - val controller = view.windowInsetsController - controller?.hide(WindowInsetsCompat.Type.statusBars()) + WindowCompat.getInsetsController(window, view).hide(statusBars()) } } From 138aafeda042deef289bedb2a43db95a79a28f60 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Tue, 1 Sep 2026 02:38:46 +0100 Subject: [PATCH 4/5] test: pin Robolectric SDK Robolectric 4.16.1 supports a max of SDK 36 Assisted-by: Claude Fable 5 --- api/src/test/java/com/ichi2/anki/api/ApiUtilsTest.kt | 2 ++ .../java/com/ichi2/anki/common/utils/android/ColorUtilsTest.kt | 2 ++ .../java/com/ichi2/anki/common/utils/android/KeyUtilsTest.kt | 2 ++ 3 files changed, 6 insertions(+) diff --git a/api/src/test/java/com/ichi2/anki/api/ApiUtilsTest.kt b/api/src/test/java/com/ichi2/anki/api/ApiUtilsTest.kt index d9ccf9abff6d..209aa69567d3 100644 --- a/api/src/test/java/com/ichi2/anki/api/ApiUtilsTest.kt +++ b/api/src/test/java/com/ichi2/anki/api/ApiUtilsTest.kt @@ -5,6 +5,7 @@ package com.ichi2.anki.api import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import org.robolectric.annotation.Config import kotlin.test.assertEquals import kotlin.test.assertNull @@ -19,6 +20,7 @@ import kotlin.test.assertNull * Slack: bresan */ @RunWith(RobolectricTestRunner::class) +@Config(sdk = [35]) internal class ApiUtilsTest { @Test fun joinFieldsShouldJoinWhenListIsValid() { diff --git a/common/android/src/test/java/com/ichi2/anki/common/utils/android/ColorUtilsTest.kt b/common/android/src/test/java/com/ichi2/anki/common/utils/android/ColorUtilsTest.kt index bda082c6258c..e8e02ecfc5e8 100644 --- a/common/android/src/test/java/com/ichi2/anki/common/utils/android/ColorUtilsTest.kt +++ b/common/android/src/test/java/com/ichi2/anki/common/utils/android/ColorUtilsTest.kt @@ -6,10 +6,12 @@ import android.graphics.Color import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith +import org.robolectric.annotation.Config import kotlin.test.assertEquals /** Tests for [darkenColor] and [lightenColorAbsolute] */ @RunWith(AndroidJUnit4::class) +@Config(sdk = [35]) class ColorUtilsTest { @Test fun darkenColor_withNoChange_returnsSameColor() { diff --git a/common/android/src/test/java/com/ichi2/anki/common/utils/android/KeyUtilsTest.kt b/common/android/src/test/java/com/ichi2/anki/common/utils/android/KeyUtilsTest.kt index ebf46e7b23cb..cb6c9f6c30d0 100644 --- a/common/android/src/test/java/com/ichi2/anki/common/utils/android/KeyUtilsTest.kt +++ b/common/android/src/test/java/com/ichi2/anki/common/utils/android/KeyUtilsTest.kt @@ -14,11 +14,13 @@ import android.view.KeyEvent.KEYCODE_STAR import androidx.test.ext.junit.runners.AndroidJUnit4 import org.junit.Test import org.junit.runner.RunWith +import org.robolectric.annotation.Config import kotlin.test.assertEquals import kotlin.test.assertNull /** Tests for [KeyEvent.digit]. */ @RunWith(AndroidJUnit4::class) +@Config(sdk = [35]) class KeyUtilsTest { @Test fun `digit returns the digit for 0-9 keys`() { From cbbbd9217014f224167402a4d10b80a2d0b748d2 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison@users.noreply.github.com> Date: Tue, 1 Sep 2026 01:34:38 +0100 Subject: [PATCH 5/5] build(deps): bump compileSdk to 37 SDK 37 is the first release under 'major.minor' SDK versioning: the platform ships as `android-37.0`, so `compileSdkMinor` must be set for the target to resolve Fixes 21672 Assisted-by: Claude Fable 5 --- AnkiDroid/build.gradle | 1 + baselineprofile/build.gradle.kts | 4 ++++ buildSrc/src/main/kotlin/ankidroid.android.api.gradle.kts | 1 + buildSrc/src/main/kotlin/ankidroid.android.library.gradle.kts | 1 + gradle/libs.versions.toml | 3 ++- 5 files changed, 9 insertions(+), 1 deletion(-) diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle index 4bca935bb5e2..5f4ea1642a69 100644 --- a/AnkiDroid/build.gradle +++ b/AnkiDroid/build.gradle @@ -65,6 +65,7 @@ android { namespace = "com.ichi2.anki" compileSdk = libs.versions.compileSdk.get().toInteger() + compileSdkMinor = libs.versions.compileSdkMinor.get().toInteger() buildFeatures { buildConfig = true diff --git a/baselineprofile/build.gradle.kts b/baselineprofile/build.gradle.kts index af8215419e79..47de721bcc3b 100644 --- a/baselineprofile/build.gradle.kts +++ b/baselineprofile/build.gradle.kts @@ -13,6 +13,10 @@ configure { libs.versions.compileSdk .get() .toInt() + compileSdkMinor = + libs.versions.compileSdkMinor + .get() + .toInt() compileOptions { sourceCompatibility = JavaVersion.VERSION_17 diff --git a/buildSrc/src/main/kotlin/ankidroid.android.api.gradle.kts b/buildSrc/src/main/kotlin/ankidroid.android.api.gradle.kts index 7d51e98bdf54..ae67c58407f3 100644 --- a/buildSrc/src/main/kotlin/ankidroid.android.api.gradle.kts +++ b/buildSrc/src/main/kotlin/ankidroid.android.api.gradle.kts @@ -20,6 +20,7 @@ plugins { extensions.configure { compileSdk = libsVersionFor("compileSdk").toInt() + compileSdkMinor = libsVersionFor("compileSdkMinor").toInt() compileOptions { // API remains on VERSION_11 for consumer compatibility. diff --git a/buildSrc/src/main/kotlin/ankidroid.android.library.gradle.kts b/buildSrc/src/main/kotlin/ankidroid.android.library.gradle.kts index 2081d11469c5..8ae226722ae3 100644 --- a/buildSrc/src/main/kotlin/ankidroid.android.library.gradle.kts +++ b/buildSrc/src/main/kotlin/ankidroid.android.library.gradle.kts @@ -16,6 +16,7 @@ plugins { extensions.configure { compileSdk = libsVersionFor("compileSdk").toInt() + compileSdkMinor = libsVersionFor("compileSdkMinor").toInt() defaultConfig { minSdk = libsVersionFor("minSdk").toInt() diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 1343513c698b..fcd691704c78 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -1,5 +1,6 @@ [versions] -compileSdk = "36" +compileSdk = "37" +compileSdkMinor = "0" # Changing minSdk means newer AnkiDroid versions will not support older devices. # However the Play Store will keep old AnkiDroid versions available for older