From 5f42c88b5c258a051a4f96baacccf8693adf6d14 Mon Sep 17 00:00:00 2001 From: mozhganpeivandiansharbaf Date: Wed, 27 Aug 2025 13:15:02 +0200 Subject: [PATCH 1/6] feat(health-sdk): Keypad is not retaining on the screen while rotation on Android 10 IPC-756 --- .../core/api/internal/GiniCoreAPIBuilder.kt | 2 +- .../health/sdk/review/ReviewFragment.kt | 69 +++++++++++++++- .../reviewBottomSheet/ReviewBottomSheet.kt | 81 +++++++++++++++++++ .../review/reviewComponent/ReviewView.kt | 2 + .../internal/payment/utils/Extensions.kt | 17 ++++ 5 files changed, 168 insertions(+), 3 deletions(-) diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/internal/GiniCoreAPIBuilder.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/internal/GiniCoreAPIBuilder.kt index 700ebc97b..b1988a659 100644 --- a/core-api-library/library/src/main/java/net/gini/android/core/api/internal/GiniCoreAPIBuilder.kt +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/internal/GiniCoreAPIBuilder.kt @@ -70,7 +70,7 @@ abstract class GiniCoreAPIBuilder, G : GiniCoreAPI(focusedId) + if (et?.isShown == true && et.isEnabled && et.isFocusable) { + viewLifecycleOwner.lifecycleScope.launch { + delay(VIEW_SETTLE_DELAY_MS) + et.showKeyboard() // your helper already requests focus + } + } + } } + private fun GhsFragmentReviewBinding.setStateListeners() { viewLifecycleOwner.lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.RESUMED) { @@ -376,6 +416,18 @@ class ReviewFragment private constructor( }) } + private fun startPreRKeyboardTracker(root: View) { + val listener = ViewTreeObserver.OnGlobalLayoutListener { + val r = android.graphics.Rect() + root.getWindowVisibleDisplayFrame(r) + val visible = r.height() + val heightDiff = root.rootView.height - visible + imeVisibleNow = heightDiff > root.rootView.height * KEYBOARD_VISIBILITY_RATIO // ~keyboard threshold + } + root.viewTreeObserver.addOnGlobalLayoutListener(listener) + preRKeyboardTracker = listener + } + private fun GhsFragmentReviewBinding.showInfoBar() { root.doOnLayout { if (resources.isLandscapeOrientation()) { @@ -477,10 +529,23 @@ class ReviewFragment private constructor( override fun onSaveInstanceState(outState: Bundle) { val height = view?.findViewById(HealthR.id.pager)?.layoutParams?.height ?: -1 outState.putInt(PAGER_HEIGHT, height) + if (preQ()) { + val focusedId = view?.findFocus()?.id ?: View.NO_ID + outState.putInt(KEY_FOCUSED_ID, focusedId) + outState.putBoolean(KEY_IME_WAS_VISIBLE, imeVisibleNow) + } super.onSaveInstanceState(outState) - } + } + private fun preQ() = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q + override fun onDestroyView() { + preRKeyboardTracker?.let { l -> + view?.viewTreeObserver?.removeOnGlobalLayoutListener(l) + } + preRKeyboardTracker = null + super.onDestroyView() + } internal companion object { private const val PAGER_HEIGHT = "pager_height" diff --git a/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt b/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt index 743871958..cada54779 100644 --- a/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt +++ b/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt @@ -2,15 +2,21 @@ package net.gini.android.internal.payment.review.reviewBottomSheet import android.app.Dialog import android.content.DialogInterface +import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.view.ViewTreeObserver +import android.widget.EditText import android.widget.FrameLayout import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider +import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomsheet.BottomSheetBehavior import com.google.android.material.bottomsheet.BottomSheetDialog +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import net.gini.android.internal.payment.GiniInternalPaymentModule import net.gini.android.internal.payment.api.model.PaymentDetails import net.gini.android.internal.payment.databinding.GpsBottomSheetReviewBinding @@ -24,13 +30,23 @@ import net.gini.android.internal.payment.utils.extensions.isLandscapeOrientation import net.gini.android.internal.payment.utils.extensions.isViewModelInitialized import net.gini.android.internal.payment.utils.extensions.onKeyboardAction import net.gini.android.internal.payment.utils.extensions.setBackListener +import net.gini.android.internal.payment.utils.showKeyboard +private const val VIEW_SETTLE_DELAY_MS = 200L +private const val KEY_IME_WAS_VISIBLE = "ime_was_visible" +private const val KEY_FOCUSED_ID = "focused_view_id" +private const val KEYBOARD_VISIBILITY_RATIO = 0.25f +private var lastFocusedId: Int = View.NO_ID +private var focusTracker: ViewTreeObserver.OnGlobalFocusChangeListener? = null class ReviewBottomSheet private constructor( private val viewModelFactory: ViewModelProvider.Factory? ) : GpsBottomSheetDialogFragment() { constructor() : this(null) + private var imeVisibleNow: Boolean = false + private var preRKeyboardTracker: ViewTreeObserver.OnGlobalLayoutListener? = null + private val viewModel: ReviewBottomSheetViewModel by viewModels { viewModelFactory ?: object : ViewModelProvider.Factory {} } @@ -72,6 +88,43 @@ class ReviewBottomSheet private constructor( return dialog } + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + // Track the currently focused view’s id at all times + focusTracker = ViewTreeObserver.OnGlobalFocusChangeListener { _, new -> + val id = new?.id ?: View.NO_ID + if (id != View.NO_ID) lastFocusedId = id + } + view.viewTreeObserver.addOnGlobalFocusChangeListener(focusTracker) + + if (preQ()) { + startPreRKeyboardTracker(view) + restoreFocusAndImeIfNeeded(view, savedInstanceState) + } + } + + private fun preQ() = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q + + private fun restoreFocusAndImeIfNeeded(root: View, savedInstanceState: Bundle?) { + val focusedId = savedInstanceState?.getInt(KEY_FOCUSED_ID) ?: View.NO_ID + val imeWasVisible = savedInstanceState?.getBoolean(KEY_IME_WAS_VISIBLE) ?: false + if (focusedId == View.NO_ID) return + + root.post { + val v = root.findViewById(focusedId) + if (v != null && v.isShown && v.isEnabled && v.isFocusable) { + v.requestFocus() + // if you specifically want to re-open the IME: + if (imeWasVisible && v is EditText) { + viewLifecycleOwner.lifecycleScope.launch { + delay(VIEW_SETTLE_DELAY_MS) + v.showKeyboard() // your helper + } + } + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) if (viewModelFactory == null && !isViewModelInitialized(ReviewBottomSheetViewModel::class)) { @@ -101,11 +154,39 @@ class ReviewBottomSheet private constructor( return binding.root } + private fun startPreRKeyboardTracker(root: View) { + val listener = ViewTreeObserver.OnGlobalLayoutListener { + val r = android.graphics.Rect() + root.getWindowVisibleDisplayFrame(r) + val visible = r.height() + val heightDiff = root.rootView.height - visible + imeVisibleNow = heightDiff > root.rootView.height * KEYBOARD_VISIBILITY_RATIO // ~keyboard threshold + } + root.viewTreeObserver.addOnGlobalLayoutListener(listener) + preRKeyboardTracker = listener + } + + override fun onDestroyView() { + preRKeyboardTracker?.let { l -> + view?.viewTreeObserver?.removeOnGlobalLayoutListener(l) + } + preRKeyboardTracker = null + super.onDestroyView() + } + override fun onCancel(dialog: DialogInterface) { viewModel.backListener?.backCalled() super.onCancel(dialog) } + override fun onSaveInstanceState(outState: Bundle) { + if (preQ()) { + outState.putInt(KEY_FOCUSED_ID, lastFocusedId) + outState.putBoolean(KEY_IME_WAS_VISIBLE, imeVisibleNow) + } + super.onSaveInstanceState(outState) + + } companion object { fun newInstance( configuration: ReviewConfiguration = ReviewConfiguration(), diff --git a/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewComponent/ReviewView.kt b/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewComponent/ReviewView.kt index 8cbbd3605..0347273a9 100644 --- a/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewComponent/ReviewView.kt +++ b/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewComponent/ReviewView.kt @@ -12,6 +12,8 @@ import android.util.AttributeSet import android.util.TypedValue import android.view.View import android.view.ViewGroup +import android.view.WindowInsets +import android.view.inputmethod.InputMethodManager import android.widget.EditText import android.widget.ScrollView import androidx.constraintlayout.widget.ConstraintLayout diff --git a/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/utils/Extensions.kt b/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/utils/Extensions.kt index a118fce19..56b3edc2b 100644 --- a/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/utils/Extensions.kt +++ b/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/utils/Extensions.kt @@ -1,9 +1,14 @@ package net.gini.android.internal.payment.utils import android.R +import android.content.Context import android.content.res.ColorStateList +import android.os.Build import android.text.Editable import android.text.TextWatcher +import android.view.View +import android.view.WindowInsets +import android.view.inputmethod.InputMethodManager import android.widget.Button import androidx.annotation.ColorInt import androidx.annotation.IntRange @@ -85,3 +90,15 @@ internal suspend fun Flow.withPrev() = flow { prev = it } } + + +fun View.showKeyboard() { + requestFocus() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { + windowInsetsController?.show(WindowInsets.Type.ime()) + } else { + val imm = context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager + imm.showSoftInput(this, InputMethodManager.SHOW_IMPLICIT) + } +} + From ff57331ebba78585c104e80e0ca094472a11a14c Mon Sep 17 00:00:00 2001 From: mozhganpeivandiansharbaf Date: Wed, 27 Aug 2025 14:17:27 +0200 Subject: [PATCH 2/6] fix(health-sdk): Disable debugging mode and clean up unused imports in ReviewView IPC-756 --- .../net/gini/android/core/api/internal/GiniCoreAPIBuilder.kt | 2 +- .../internal/payment/review/reviewComponent/ReviewView.kt | 2 -- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/core-api-library/library/src/main/java/net/gini/android/core/api/internal/GiniCoreAPIBuilder.kt b/core-api-library/library/src/main/java/net/gini/android/core/api/internal/GiniCoreAPIBuilder.kt index b1988a659..700ebc97b 100644 --- a/core-api-library/library/src/main/java/net/gini/android/core/api/internal/GiniCoreAPIBuilder.kt +++ b/core-api-library/library/src/main/java/net/gini/android/core/api/internal/GiniCoreAPIBuilder.kt @@ -70,7 +70,7 @@ abstract class GiniCoreAPIBuilder, G : GiniCoreAPI Date: Wed, 27 Aug 2025 16:15:03 +0200 Subject: [PATCH 3/6] fix(health-sdk): Simplify focus tracking and keyboard handling in ReviewBottomSheet IPC-756 --- .../reviewBottomSheet/ReviewBottomSheet.kt | 54 +++++++++---------- 1 file changed, 25 insertions(+), 29 deletions(-) diff --git a/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt b/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt index cada54779..2799d565c 100644 --- a/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt +++ b/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt @@ -2,14 +2,13 @@ package net.gini.android.internal.payment.review.reviewBottomSheet import android.app.Dialog import android.content.DialogInterface -import android.os.Build import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.view.ViewTreeObserver -import android.widget.EditText import android.widget.FrameLayout +import android.widget.TextView import androidx.fragment.app.viewModels import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.lifecycleScope @@ -36,12 +35,14 @@ private const val VIEW_SETTLE_DELAY_MS = 200L private const val KEY_IME_WAS_VISIBLE = "ime_was_visible" private const val KEY_FOCUSED_ID = "focused_view_id" private const val KEYBOARD_VISIBILITY_RATIO = 0.25f -private var lastFocusedId: Int = View.NO_ID -private var focusTracker: ViewTreeObserver.OnGlobalFocusChangeListener? = null + class ReviewBottomSheet private constructor( private val viewModelFactory: ViewModelProvider.Factory? ) : GpsBottomSheetDialogFragment() { + private var lastFocusedId: Int = View.NO_ID + private var focusTracker: ViewTreeObserver.OnGlobalFocusChangeListener? = null + constructor() : this(null) private var imeVisibleNow: Boolean = false @@ -90,36 +91,27 @@ class ReviewBottomSheet private constructor( override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - // Track the currently focused view’s id at all times + focusTracker = ViewTreeObserver.OnGlobalFocusChangeListener { _, new -> val id = new?.id ?: View.NO_ID if (id != View.NO_ID) lastFocusedId = id } view.viewTreeObserver.addOnGlobalFocusChangeListener(focusTracker) - if (preQ()) { - startPreRKeyboardTracker(view) - restoreFocusAndImeIfNeeded(view, savedInstanceState) - } + startPreRKeyboardTracker(view) + restoreFocusAndImeIfNeeded(view, savedInstanceState) } - private fun preQ() = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q - private fun restoreFocusAndImeIfNeeded(root: View, savedInstanceState: Bundle?) { val focusedId = savedInstanceState?.getInt(KEY_FOCUSED_ID) ?: View.NO_ID val imeWasVisible = savedInstanceState?.getBoolean(KEY_IME_WAS_VISIBLE) ?: false - if (focusedId == View.NO_ID) return - + if (focusedId == View.NO_ID || !imeWasVisible) return root.post { - val v = root.findViewById(focusedId) - if (v != null && v.isShown && v.isEnabled && v.isFocusable) { - v.requestFocus() - // if you specifically want to re-open the IME: - if (imeWasVisible && v is EditText) { - viewLifecycleOwner.lifecycleScope.launch { - delay(VIEW_SETTLE_DELAY_MS) - v.showKeyboard() // your helper - } + val et = root.findViewById(focusedId) + if (et?.isShown == true && et.isEnabled && et.isFocusable) { + viewLifecycleOwner.lifecycleScope.launch { + delay(VIEW_SETTLE_DELAY_MS) + et.showKeyboard() } } } @@ -160,17 +152,22 @@ class ReviewBottomSheet private constructor( root.getWindowVisibleDisplayFrame(r) val visible = r.height() val heightDiff = root.rootView.height - visible - imeVisibleNow = heightDiff > root.rootView.height * KEYBOARD_VISIBILITY_RATIO // ~keyboard threshold + imeVisibleNow = + heightDiff > root.rootView.height * KEYBOARD_VISIBILITY_RATIO // ~keyboard threshold } root.viewTreeObserver.addOnGlobalLayoutListener(listener) preRKeyboardTracker = listener } override fun onDestroyView() { - preRKeyboardTracker?.let { l -> - view?.viewTreeObserver?.removeOnGlobalLayoutListener(l) + preRKeyboardTracker?.let { + view?.viewTreeObserver?.removeOnGlobalLayoutListener(it) + } + focusTracker?.let { + view?.viewTreeObserver?.removeOnGlobalFocusChangeListener(it) } preRKeyboardTracker = null + focusTracker = null super.onDestroyView() } @@ -180,13 +177,12 @@ class ReviewBottomSheet private constructor( } override fun onSaveInstanceState(outState: Bundle) { - if (preQ()) { - outState.putInt(KEY_FOCUSED_ID, lastFocusedId) - outState.putBoolean(KEY_IME_WAS_VISIBLE, imeVisibleNow) - } + outState.putInt(KEY_FOCUSED_ID, lastFocusedId) + outState.putBoolean(KEY_IME_WAS_VISIBLE, imeVisibleNow) super.onSaveInstanceState(outState) } + companion object { fun newInstance( configuration: ReviewConfiguration = ReviewConfiguration(), From 8f592a848eb5c79efefd6f9fd7b2099aa8c986be Mon Sep 17 00:00:00 2001 From: mozhganpeivandiansharbaf Date: Wed, 27 Aug 2025 16:15:17 +0200 Subject: [PATCH 4/6] fix(health-sdk): Simplify focus tracking and keyboard handling in ReviewBottomSheet IPC-756 --- .../net/gini/android/health/sdk/review/ReviewFragment.kt | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt b/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt index 9c2e49329..a22ff9a6f 100644 --- a/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt +++ b/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt @@ -84,7 +84,7 @@ internal interface ReviewFragmentListener { /** * Delay duration (in milliseconds) used to allow the view to settle down before requesting focus. * - * A value of 500ms was chosen based on observed behaviour on Android 10 devices and below, where + * A value of 200ms was chosen based on observed behaviour on Android 10 devices and below, where * immediately requesting keyboard focus after view creation can result in the keyboard not * appearing. * This delay helps ensure that the keyboard is reliably shown when the field requests focus. @@ -540,8 +540,8 @@ class ReviewFragment private constructor( private fun preQ() = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q override fun onDestroyView() { - preRKeyboardTracker?.let { l -> - view?.viewTreeObserver?.removeOnGlobalLayoutListener(l) + preRKeyboardTracker?.let { + view?.viewTreeObserver?.removeOnGlobalLayoutListener(it) } preRKeyboardTracker = null super.onDestroyView() From 02a97514b82e508f8e4cde5430238343b0795cd2 Mon Sep 17 00:00:00 2001 From: mozhganpeivandiansharbaf Date: Mon, 1 Sep 2025 10:55:46 +0200 Subject: [PATCH 5/6] fix(health-sdk): Improve comments for keyboard handling logic in ReviewFragment IPC-756 --- .../java/net/gini/android/health/sdk/review/ReviewFragment.kt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt b/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt index a22ff9a6f..39fcea31d 100644 --- a/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt +++ b/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt @@ -204,7 +204,7 @@ class ReviewFragment private constructor( if (et?.isShown == true && et.isEnabled && et.isFocusable) { viewLifecycleOwner.lifecycleScope.launch { delay(VIEW_SETTLE_DELAY_MS) - et.showKeyboard() // your helper already requests focus + et.showKeyboard() // Helper already requests focus } } } @@ -422,7 +422,7 @@ class ReviewFragment private constructor( root.getWindowVisibleDisplayFrame(r) val visible = r.height() val heightDiff = root.rootView.height - visible - imeVisibleNow = heightDiff > root.rootView.height * KEYBOARD_VISIBILITY_RATIO // ~keyboard threshold + imeVisibleNow = heightDiff > root.rootView.height * KEYBOARD_VISIBILITY_RATIO // keyboard threshold } root.viewTreeObserver.addOnGlobalLayoutListener(listener) preRKeyboardTracker = listener From c677cf141a564ae3b3be704195561f2d196c8182 Mon Sep 17 00:00:00 2001 From: mozhganpeivandiansharbaf Date: Tue, 21 Oct 2025 14:58:53 +0200 Subject: [PATCH 6/6] fix(health-sdk): remove blank lines in onSaveInstanceState IPC-176 --- .../java/net/gini/android/health/sdk/review/ReviewFragment.kt | 1 - .../payment/review/reviewBottomSheet/ReviewBottomSheet.kt | 1 - 2 files changed, 2 deletions(-) diff --git a/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt b/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt index 39fcea31d..4848ad6f1 100644 --- a/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt +++ b/health-sdk/sdk/src/main/java/net/gini/android/health/sdk/review/ReviewFragment.kt @@ -535,7 +535,6 @@ class ReviewFragment private constructor( outState.putBoolean(KEY_IME_WAS_VISIBLE, imeVisibleNow) } super.onSaveInstanceState(outState) - } private fun preQ() = Build.VERSION.SDK_INT <= Build.VERSION_CODES.Q diff --git a/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt b/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt index 2799d565c..25622d863 100644 --- a/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt +++ b/internal-payment-sdk/sdk/src/main/java/net/gini/android/internal/payment/review/reviewBottomSheet/ReviewBottomSheet.kt @@ -180,7 +180,6 @@ class ReviewBottomSheet private constructor( outState.putInt(KEY_FOCUSED_ID, lastFocusedId) outState.putBoolean(KEY_IME_WAS_VISIBLE, imeVisibleNow) super.onSaveInstanceState(outState) - } companion object {