Focus restoration is done in the Activity's onRestoreInstanceState(), which is done separately from when Fragment's restore their own View's state (that would be in the Fragment's onViewStateRestored()).
As per the onRestoreInstanceState() documentation, it is called between the Activity's onStart() and onPostCreate() (which runs prior to onResume() and onPostResume() - onPostResume() is when Fragment's get their onResume() callbacks).
This means that you are correct in that there is no callback available at the Fragment level prior to onResume() where the focus is set correctly prior to that method being called.
That being said, yes, users can interact with a Fragment prior to it reaching the resumed state. For example, ViewPager2 (as well as ViewPager 1 when using BEHAVIOR_RESUME_ONLY_CURRENT_FRAGMENT) keep the non-selected fragments (i.e., those not in the middle of the screen) in the STARTED state. Through the power of multi-touch, users could drag a page over slightly and then use another finger to tap on a view that is partially visible. You'd see the same behavior if you use setMaxLifecycle() with Lifecycle.State.STARTED yourself (which is what those are doing under the hood) - the fragment is interactable, but not RESUMED.
In most general cases, however, if you're not using any of the above APIs, the Fragment lifecycle will generally match the Activity lifecycle. And an activity, as per the ActivityThread source code, does run its updates all in the same handleStartActivity() method.
It should be noted that every time the activity is destroyed, you will get a callback to your OnFocusChangeListener with hasFocus of false as the View is removed from the Activity (which always loses the View's focus). This happens after the state is saved, the View's focus state isn't actually lost, it is just something you already need to handle in your callback, usually by checking isStateSaved() and ignoring focus loss after the state is saved and checking isRemoving() if you are manually removing / replacing the Fragment (i.e., by doing a replace() operation).
Given that you already will have to have logic in your listener to avoid handling the hasFocus of false events post destruction, the 100% correct case for handling gaining focus would involve saving your own focused state (i.e., true or false for a specific view) in your saved instance state and only running your logic if the hasFocus is changing from what you already have saved. This means that you'd restore your saved instance state earlier in the Fragment's lifecycle (say, in onViewStateRestored() method that Fragments provide) and add your listener there. Then, your logic could safely ignore callbacks with the same focus:
override fun onViewStateRestored(savedInstanceState: Bundle?) {
super.onViewStateRestored(savedInstanceState)
// Restore your member variable of focus
focused = savedInstanceState?.getBoolean("HAS_FOCUS", false) ?: false
editText.setOnFocusChangeListener { _, hasFocus ->
if (focused == hasFocus) {
// ignore
return
}
focused = hasFocus
if (hasFocus) {
// We gained focus
} else if (!isStateSaved() && !isRemoving()) {
// We lost focus
}
}
}