Android 12: Why doesn't swiping up trigger onStop()

Viewed 294

Given foregrounded, when I swipe up from the very bottom of the screen:

  • Android 10 (Nokia 6.1), 11 (Pixel 4): onStop() is called.
  • Android 12 (Pixel 3): onStop() is not called.

Then, if you drag/swipe an app screen up to dismiss (or destroy) it:

  • Android 10 (Nokia 6.1), 11 (Pixel 4): onDestroy() is called.
  • Android 12 (Pixel 3): onStop() and onDestroy() are called consecutively.

compileSdk 31 minSdk 26 targetSdk 31

I have looked into https://developer.android.com/about/versions/12/behavior-changes-all, this behaviour is not documented there.

Is this an expected thing in Android 12? It's quite annoying as it changes the lifecycle behaviours of the app and its activities/fragments/coroutines...

1 Answers

I tried googling this question but I didn't find an answer, so after some investigation I found out that we can rely on activity focus:

class MainActivity : AppCompatActivity() {
    override fun onWindowFocusChanged(hasFocus: Boolean) {
        super.onWindowFocusChanged(hasFocus)
    }
}

onWindowFocusChanged is called when we open the recent apps view (swipe up from the very bottom of the screen). But it's also called when you show a dialog inside your app or a user interacts with notification at the top of the screen. So I came up with next solution:

class MainActivity : AppCompatActivity() {

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(binding?.root)
        binding?.root?.setOnApplyWindowInsetsListener(insetListener)
    }

    private fun onFocusLost() {
        //your code
    }

    private fun onFocusTaken() {
        //your code
    }

    //---- implementation details -----\\
    private var unnecessaryFocusTriggerTimeStamp: Long = 0
    private var recentAppsOpened = false
    //insetListener is called when a user interacts with notifications (swipes them out)
    private val insetListener = View.OnApplyWindowInsetsListener { _, insets ->
        unnecessaryFocusTriggerTimeStamp = System.currentTimeMillis()
        insets
    }
    //this method is called when a dialog is going to be shown
    override fun onCreateView(name: String, context: Context, attrs: AttributeSet): View? {
        unnecessaryFocusTriggerTimeStamp = System.currentTimeMillis()
        return super.onCreateView(name, context, attrs)
    }

    override fun onWindowFocusChanged(hasFocus: Boolean) {
        if (!hasFocus && !this.recentAppsOpened && (System.currentTimeMillis() - this.unnecessaryFocusTriggerTimeStamp) > 50) {
            this.recentAppsOpened = true
            onFocusLost()
        } else if (hasFocus && recentAppsOpened) {
            this.recentAppsOpened = false
            onFocusTaken()
        }
        super.onWindowFocusChanged(hasFocus)
    }
}

This code doesn't look good, but it works fine on Pixel 5 and Galaxy A52 (Android 12).

The lifecycle will look like this:

[swipe up]

  • onFocusLost
  • onPause
  • onStop

[back to the app]

  • onStart
  • onResume
  • onFocusTaken

Use onFocusLost and onFocusTaken to replace onStop and onStart.

Related