瀏覽代碼

refactor: Only handle window inset events in the main view hierarchy

...so that this isn't completely broken by some versions of Android with
the broken dispatch behavior.
Peter Cai 1 月之前
父節點
當前提交
76ef63efdf

+ 7 - 4
app-common/src/main/java/im/angry/openeuicc/ui/EuiccInfoActivity.kt

@@ -24,11 +24,12 @@ import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.util.EUICC_DEFAULT_ISDR_AID
 import im.angry.openeuicc.util.OpenEuiccContextMarker
+import im.angry.openeuicc.util.activityToolbarInsetHandler
 import im.angry.openeuicc.util.decodeHex
 import im.angry.openeuicc.util.encodeHex
 import im.angry.openeuicc.util.formatFreeSpace
-import im.angry.openeuicc.util.setupRootViewInsets
-import im.angry.openeuicc.util.setupToolbarInsets
+import im.angry.openeuicc.util.mainViewPaddingInsetHandler
+import im.angry.openeuicc.util.setupRootViewSystemBarInsets
 import im.angry.openeuicc.util.tryParseEuiccVendorInfo
 import kotlinx.coroutines.launch
 import net.typeblog.lpac_jni.impl.PKID_GSMA_LIVE_CI
@@ -64,7 +65,6 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_euicc_info)
         setSupportActionBar(requireViewById(R.id.toolbar))
-        setupToolbarInsets()
         supportActionBar!!.setDisplayHomeAsUpEnabled(true)
 
         swipeRefresh = requireViewById(R.id.swipe_refresh)
@@ -92,7 +92,10 @@ class EuiccInfoActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
 
         swipeRefresh.setOnRefreshListener { refresh() }
 
-        setupRootViewInsets(infoList)
+        setupRootViewSystemBarInsets(window.decorView.rootView, arrayOf(
+            this::activityToolbarInsetHandler,
+            mainViewPaddingInsetHandler(infoList)
+        ))
     }
 
     override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) {

+ 11 - 13
app-common/src/main/java/im/angry/openeuicc/ui/EuiccManagementFragment.kt

@@ -19,8 +19,6 @@ import android.widget.PopupMenu
 import android.widget.TextView
 import android.widget.Toast
 import androidx.appcompat.app.AlertDialog
-import androidx.core.view.ViewCompat
-import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.isVisible
 import androidx.core.view.updateLayoutParams
 import androidx.fragment.app.Fragment
@@ -43,11 +41,12 @@ import im.angry.openeuicc.util.euiccChannelManager
 import im.angry.openeuicc.util.euiccChannelManagerService
 import im.angry.openeuicc.util.isEnabled
 import im.angry.openeuicc.util.isUsb
+import im.angry.openeuicc.util.mainViewPaddingInsetHandler
 import im.angry.openeuicc.util.newInstanceEuicc
 import im.angry.openeuicc.util.operational
 import im.angry.openeuicc.util.portId
 import im.angry.openeuicc.util.seId
-import im.angry.openeuicc.util.setupRootViewInsets
+import im.angry.openeuicc.util.setupRootViewSystemBarInsets
 import im.angry.openeuicc.util.slotId
 import im.angry.openeuicc.util.withEuiccChannel
 import kotlinx.coroutines.Dispatchers
@@ -108,18 +107,17 @@ open class EuiccManagementFragment : Fragment(), EuiccProfilesChangedListener,
 
         val origFabMarginRight = (fab.layoutParams as ViewGroup.MarginLayoutParams).rightMargin
         val origFabMarginBottom = (fab.layoutParams as ViewGroup.MarginLayoutParams).bottomMargin
-        ViewCompat.setOnApplyWindowInsetsListener(fab) { v, insets ->
-            val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
 
-            v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
-                rightMargin = origFabMarginRight + bars.right
-                bottomMargin = origFabMarginBottom + bars.bottom
+        setupRootViewSystemBarInsets(
+            view, arrayOf(
+            mainViewPaddingInsetHandler(profileList),
+            { insets ->
+                fab.updateLayoutParams<ViewGroup.MarginLayoutParams> {
+                    rightMargin = origFabMarginRight + insets.right
+                    bottomMargin = origFabMarginBottom + insets.bottom
+                }
             }
-
-            WindowInsetsCompat.CONSUMED
-        }
-
-        setupRootViewInsets(profileList)
+        ))
 
         return view
     }

+ 11 - 3
app-common/src/main/java/im/angry/openeuicc/ui/IsdrAidListActivity.kt

@@ -10,8 +10,10 @@ import androidx.activity.enableEdgeToEdge
 import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.lifecycleScope
 import im.angry.openeuicc.common.R
+import im.angry.openeuicc.util.activityToolbarInsetHandler
+import im.angry.openeuicc.util.mainViewPaddingInsetHandler
 import im.angry.openeuicc.util.preferenceRepository
-import im.angry.openeuicc.util.setupToolbarInsets
+import im.angry.openeuicc.util.setupRootViewSystemBarInsets
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
@@ -24,11 +26,17 @@ class IsdrAidListActivity : AppCompatActivity() {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_isdr_aid_list)
         setSupportActionBar(requireViewById(R.id.toolbar))
-        setupToolbarInsets()
         supportActionBar!!.setDisplayHomeAsUpEnabled(true)
 
         isdrAidListEditor = requireViewById(R.id.isdr_aid_list_editor)
 
+        setupRootViewSystemBarInsets(
+            window.decorView.rootView, arrayOf(
+                this::activityToolbarInsetHandler,
+                mainViewPaddingInsetHandler(isdrAidListEditor)
+            )
+        )
+
         lifecycleScope.launch {
             preferenceRepository.isdrAidListFlow.onEach {
                 isdrAidListEditor.text = Editable.Factory.getInstance().newEditable(it)
@@ -69,4 +77,4 @@ class IsdrAidListActivity : AppCompatActivity() {
 
             else -> super.onOptionsItemSelected(item)
         }
-}
+}

+ 9 - 4
app-common/src/main/java/im/angry/openeuicc/ui/LogsActivity.kt

@@ -13,11 +13,12 @@ import androidx.appcompat.app.AppCompatActivity
 import androidx.lifecycle.lifecycleScope
 import androidx.swiperefreshlayout.widget.SwipeRefreshLayout
 import im.angry.openeuicc.common.R
+import im.angry.openeuicc.util.activityToolbarInsetHandler
+import im.angry.openeuicc.util.mainViewPaddingInsetHandler
 import im.angry.openeuicc.util.readSelfLog
 import im.angry.openeuicc.util.selfAppVersion
 import im.angry.openeuicc.util.setupLogSaving
-import im.angry.openeuicc.util.setupRootViewInsets
-import im.angry.openeuicc.util.setupToolbarInsets
+import im.angry.openeuicc.util.setupRootViewSystemBarInsets
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -55,14 +56,18 @@ class LogsActivity : AppCompatActivity() {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_logs)
         setSupportActionBar(requireViewById(R.id.toolbar))
-        setupToolbarInsets()
         supportActionBar!!.setDisplayHomeAsUpEnabled(true)
 
         swipeRefresh = requireViewById(R.id.swipe_refresh)
         scrollView = requireViewById(R.id.scroll_view)
         logText = requireViewById(R.id.log_text)
 
-        setupRootViewInsets(scrollView)
+        setupRootViewSystemBarInsets(
+            window.decorView.rootView, arrayOf(
+                this::activityToolbarInsetHandler,
+                mainViewPaddingInsetHandler(scrollView)
+            )
+        )
 
         swipeRefresh.setOnRefreshListener {
             lifecycleScope.launch {

+ 8 - 2
app-common/src/main/java/im/angry/openeuicc/ui/MainActivity.kt

@@ -29,7 +29,8 @@ import im.angry.openeuicc.common.R
 import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.ui.wizard.DownloadWizardActivity
 import im.angry.openeuicc.util.OpenEuiccContextMarker
-import im.angry.openeuicc.util.setupToolbarInsets
+import im.angry.openeuicc.util.activityToolbarInsetHandler
+import im.angry.openeuicc.util.setupRootViewSystemBarInsets
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.first
@@ -83,7 +84,6 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_main)
         setSupportActionBar(requireViewById(R.id.toolbar))
-        setupToolbarInsets()
         loadingProgress = requireViewById(R.id.loading)
         tabs = requireViewById(R.id.main_tabs)
         viewPager = requireViewById(R.id.view_pager)
@@ -99,6 +99,12 @@ open class MainActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker {
             addAction(UsbManager.ACTION_USB_DEVICE_ATTACHED)
             addAction(UsbManager.ACTION_USB_DEVICE_DETACHED)
         })
+
+        setupRootViewSystemBarInsets(
+            window.decorView.rootView, arrayOf(
+                this::activityToolbarInsetHandler
+            ), consume = false
+        )
     }
 
     override fun onDestroy() {

+ 9 - 4
app-common/src/main/java/im/angry/openeuicc/ui/NotificationsActivity.kt

@@ -24,9 +24,10 @@ import im.angry.openeuicc.common.R
 import im.angry.openeuicc.core.EuiccChannel
 import im.angry.openeuicc.core.EuiccChannelManager
 import im.angry.openeuicc.util.OpenEuiccContextMarker
+import im.angry.openeuicc.util.activityToolbarInsetHandler
 import im.angry.openeuicc.util.displayName
-import im.angry.openeuicc.util.setupRootViewInsets
-import im.angry.openeuicc.util.setupToolbarInsets
+import im.angry.openeuicc.util.mainViewPaddingInsetHandler
+import im.angry.openeuicc.util.setupRootViewSystemBarInsets
 import kotlinx.coroutines.Dispatchers
 import kotlinx.coroutines.launch
 import kotlinx.coroutines.withContext
@@ -45,13 +46,17 @@ class NotificationsActivity : BaseEuiccAccessActivity(), OpenEuiccContextMarker
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_notifications)
         setSupportActionBar(requireViewById(R.id.toolbar))
-        setupToolbarInsets()
         supportActionBar!!.setDisplayHomeAsUpEnabled(true)
 
         swipeRefresh = requireViewById(R.id.swipe_refresh)
         notificationList = requireViewById(R.id.recycler_view)
 
-        setupRootViewInsets(notificationList)
+        setupRootViewSystemBarInsets(
+            window.decorView.rootView, arrayOf(
+                this::activityToolbarInsetHandler,
+                mainViewPaddingInsetHandler(notificationList)
+            )
+        )
     }
 
     override fun onInit() {

+ 9 - 2
app-common/src/main/java/im/angry/openeuicc/ui/SettingsActivity.kt

@@ -6,7 +6,8 @@ import androidx.activity.enableEdgeToEdge
 import androidx.appcompat.app.AppCompatActivity
 import im.angry.openeuicc.OpenEuiccApplication
 import im.angry.openeuicc.common.R
-import im.angry.openeuicc.util.setupToolbarInsets
+import im.angry.openeuicc.util.activityToolbarInsetHandler
+import im.angry.openeuicc.util.setupRootViewSystemBarInsets
 
 class SettingsActivity : AppCompatActivity() {
     private val appContainer
@@ -17,8 +18,14 @@ class SettingsActivity : AppCompatActivity() {
         super.onCreate(savedInstanceState)
         setContentView(R.layout.activity_settings)
         setSupportActionBar(requireViewById(R.id.toolbar))
-        setupToolbarInsets()
         supportActionBar!!.setDisplayHomeAsUpEnabled(true)
+
+        setupRootViewSystemBarInsets(
+            window.decorView.rootView, arrayOf(
+                this::activityToolbarInsetHandler
+            ), consume = false
+        )
+
         val settingsFragment = appContainer.uiComponentFactory.createSettingsFragment()
         supportFragmentManager.beginTransaction()
             .replace(R.id.settings_container, settingsFragment)

+ 5 - 2
app-common/src/main/java/im/angry/openeuicc/ui/SettingsFragment.kt

@@ -14,9 +14,10 @@ import androidx.preference.PreferenceCategory
 import androidx.preference.PreferenceFragmentCompat
 import im.angry.openeuicc.common.R
 import im.angry.openeuicc.util.PreferenceFlowWrapper
+import im.angry.openeuicc.util.mainViewPaddingInsetHandler
 import im.angry.openeuicc.util.preferenceRepository
 import im.angry.openeuicc.util.selfAppVersion
-import im.angry.openeuicc.util.setupRootViewInsets
+import im.angry.openeuicc.util.setupRootViewSystemBarInsets
 import kotlinx.coroutines.flow.collect
 import kotlinx.coroutines.flow.onEach
 import kotlinx.coroutines.launch
@@ -97,7 +98,9 @@ open class SettingsFragment : PreferenceFragmentCompat() {
 
     override fun onStart() {
         super.onStart()
-        setupRootViewInsets(requireView().requireViewById(R.id.recycler_view))
+        setupRootViewSystemBarInsets(requireView(), arrayOf(
+            mainViewPaddingInsetHandler(requireView().requireViewById(R.id.recycler_view))
+        ))
     }
 
     @Suppress("UNUSED_PARAMETER")

+ 53 - 27
app-common/src/main/java/im/angry/openeuicc/util/UiUtils.kt

@@ -11,6 +11,7 @@ import androidx.activity.result.ActivityResultCaller
 import androidx.activity.result.contract.ActivityResultContracts
 import androidx.appcompat.app.AlertDialog
 import androidx.appcompat.app.AppCompatActivity
+import androidx.core.graphics.Insets
 import androidx.core.view.ViewCompat
 import androidx.core.view.WindowInsetsCompat
 import androidx.core.view.updateLayoutParams
@@ -35,46 +36,71 @@ fun DialogFragment.setWidthPercent(percentage: Int) {
 }
 
 /**
- * Call this method (in onActivityCreated or later)
- * to make the dialog near-full screen.
+ * A handler function for `setupRootViewSystemBarInsets`, which is intended to set up
+ * insets for the top toolbar, in the case where the activity contains a toolbar with the default
+ * ID `R.id.toolbar`, and a spacer `R.id.toolbar_spacer` for status bar background.
  */
-fun DialogFragment.setFullScreen() {
-    dialog?.window?.setLayout(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT)
-}
-
-fun AppCompatActivity.setupToolbarInsets() {
-    val spacer = requireViewById<View>(R.id.toolbar_spacer)
-    ViewCompat.setOnApplyWindowInsetsListener(requireViewById(R.id.toolbar)) { v, insets ->
-        val bars = insets.getInsets(
-            WindowInsetsCompat.Type.systemBars()
-                or WindowInsetsCompat.Type.displayCutout()
-        )
-
-        v.updateLayoutParams<ViewGroup.MarginLayoutParams> {
-            topMargin = bars.top
-        }
-        v.updatePadding(bars.left, v.paddingTop, bars.right, v.paddingBottom)
-
-        spacer.updateLayoutParams {
-            height = v.top
+fun AppCompatActivity.activityToolbarInsetHandler(insets: Insets) {
+    val toolbar = requireViewById<View>(R.id.toolbar)
+    toolbar.apply {
+        updateLayoutParams<ViewGroup.MarginLayoutParams> {
+            topMargin = insets.top
         }
+        updatePadding(insets.left, paddingTop, insets.right, paddingBottom)
+    }
 
-        WindowInsetsCompat.CONSUMED
+    requireViewById<View>(R.id.toolbar_spacer).updateLayoutParams {
+        height = toolbar.top
     }
 }
 
-fun setupRootViewInsets(view: ViewGroup) {
+/**
+ * A handler function for `setupRootViewSystemBarInsets`, which is intended to set up
+ * left, right, and bottom padding for a "main view", usually a RecyclerView or a ScrollView.
+ *
+ * It ignores top paddings because that should be handled by the toolbar handler for the activity.
+ * See above.
+ */
+fun mainViewPaddingInsetHandler(v: View): (Insets) -> Unit = { insets ->
     // Disable clipToPadding to make sure content actually display
-    view.clipToPadding = false
-    ViewCompat.setOnApplyWindowInsetsListener(view) { v, insets ->
+    if (v is ViewGroup) {
+        v.clipToPadding = false
+    }
+    v.updatePadding(insets.left, v.paddingTop, insets.right, insets.bottom)
+}
+
+/**
+ * A wrapper for `ViewCompat.setOnApplyWindowInsetsListener`, which should only be called
+ * on a root view of a certain component. For activities, this should usually be `window.decorView.rootView`,
+ * and for Fragments this should be the outermost layer of view it inflated during creation.
+ *
+ * This function takes in an array of handler functions, and is expected to only ever be called
+ * on views belonging to the same hierarchy. All sibling views should be handled from the array of
+ * handler functions, rather than a separate call to this function OR `ViewCompat.setOnApplyWindowInsetsListener`.
+ *
+ * The reason this function exists is that on some versions of Android, the dispatch of window inset
+ * events is completely broken. If an inset event is handled by a view, it will never be seen by any of
+ * its siblings. By wrapping this function and restricting its use to only the "main" view hierarchy and
+ * handling all sibling views using our own handler functions, we work around that issue.
+ *
+ * Note that this function by default returns `WindowInsetCompat.CONSUME`, which will prevent the event from
+ * being dispatched further to child views. This may be a problem for activities that act as fragment hosts.
+ * In that case, please set `consume = false` in order for the event to propagate.
+ */
+fun setupRootViewSystemBarInsets(rootView: View, handlers: Array<(Insets) -> Unit>, consume: Boolean = true) {
+    ViewCompat.setOnApplyWindowInsetsListener(rootView) { _, insets ->
         val bars = insets.getInsets(
             WindowInsetsCompat.Type.systemBars()
                 or WindowInsetsCompat.Type.displayCutout()
         )
 
-        v.updatePadding(bars.left, v.paddingTop, bars.right, bars.bottom)
+        handlers.forEach { it(bars) }
 
-        WindowInsetsCompat.CONSUMED
+        if (consume) {
+            WindowInsetsCompat.CONSUMED
+        } else {
+            insets
+        }
     }
 }