'Lifecycle callbacks for system activity (PickActivity / Intent.ACTION_GET_CONTENT)

I am working on an Android app that needs to perform a clean-up task (disconnect BLE devices) when the whole application goes to background. I implemented the standard ActivityLifecycleCallbacks and do my book-keeping in the Application class. All is working good as long as I use my AppCompatActivity instances within my package.

class MainApplication : Application(), Application.ActivityLifecycleCallbacks {

override fun onCreate() {
    super.onCreate()
    registerActivityLifecycleCallbacks(this)
}

/// Activity counter

var startedActivities = AtomicInteger(0)

private fun activityStarted() {
    if (startedActivities.incrementAndGet() == 1) {
        Log.d("PICKERTEST", ">>> APPLICATION STARTED")
    }
}

private fun activityStopped() {
    if (startedActivities.decrementAndGet() == 0) {
        Log.d("PICKERTEST", ">>> APPLICATION STOPPED")
    }
}

/// Activity lifecycle callbacks

override fun onActivityStarted(activity: Activity) {
    activityStarted()
}

override fun onActivityStopped(activity: Activity) {
    activityStopped()
}

...

MY PROBLEM: The trouble starts when I have to pick files (for example a Firmware Update) using the system activities. In this case, I don't get callbacks for the picker activity and my app believes it gets paused, as soon as the picker appears. This disconnects the BLE device and, once I get back from the file picker, I cannot upload the firmware file to the device.

The code to open the file picker is pretty standard:

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)

    pickButton.setOnClickListener { pickFile() }
}

private fun pickFile() {
    val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
        this.addCategory(Intent.CATEGORY_OPENABLE)
        this.type = "*/*"
    }

    startActivityForResult(intent, 0)
}

...

This opens up a system activity with a standard OS picker. According to adb shell dumpsys activity:

ActivityRecord{e05754 u0 com.android.documentsui/.picker.PickActivity t162}]

This activity comes from the android.documentsui:

PickActivity > https://android.googlesource.com/platform/packages/apps/DocumentsUI/+/android-cts-8.0_r16/src/com/android/documentsui/picker/PickActivity.java

BaseActivity > https://android.googlesource.com/platform/packages/apps/DocumentsUI/+/android-cts-8.0_r16/src/com/android/documentsui/BaseActivity.java

This package is not androidx-based and uses the standard android.app.Activity. To be fair, I am not sure if that is the issue, or if lifecycle callbacks just don't get called when I run an activity from a different package.

I can of course add a custom callback to MainApplication, to notify that we are running an external Activity

class MainApplication : Application(), Application.ActivityLifecycleCallbacks {
    
...

    /// Custom callback

    fun onStartActivityForResult() {
        Log.d("PICKERTEST", "onStartActivityForResult")
        activityStarted()
    }

    fun onActivityResult() {
        Log.d("PICKERTEST", "onActivityResult")
        activityStopped()
    }
}

and call it in my Activity

class MainActivity : AppCompatActivity() {
    
    ...

    private fun pickFile() {
        ...

        val mainApplication = applicationContext as? MainApplication
        mainApplication?.onStartActivityForResult()
    }

    override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
        super.onActivityResult(requestCode, resultCode, data)

        val mainApplication = applicationContext as? MainApplication
        mainApplication?.onActivityResult()
    }
}

This is however a hack and also does not cover the case in which my Application is paused while showing the file picker.

Does anyone know a solution for listening to lifecycle events of external activities?



Solution 1:[1]

For the benefit of those asking or checking this question, here is my approach to the problem described in my question above. I understand it is not the cleanest solution possible but it solves the purpose.

class MainApplication: Application(), Application.ActivityLifecycleCallbacks
{
    override fun onCreate() {
        super.onCreate()
        registerActivityLifecycleCallbacks(this)
    }

    /// Activity counter

    private var startedActivities = AtomicInteger(0)
    private var waitingForExternalActivity = AtomicBoolean(false)

    private fun activityIsRunning(): Boolean {
        return (startedActivities.get() > 0)
    }

    /// Activity lifecycle callbacks

    override fun onActivityStarted(activity: Activity) {
        if (waitingForExternalActivity.getAndSet(false)) {
            cancelWaitingForExternalActivityTimeout()
            startedActivities.incrementAndGet()
        } else {
            if (startedActivities.incrementAndGet() == 1) {
                onApplicationStarted()
            }
        }
    }

    override fun onActivityStopped(activity: Activity) {
        if (startedActivities.decrementAndGet() == 0) {
            if (!waitingForExternalActivity.get() && !activity.isChangingConfigurations) {
                onApplicationStopped()
            }
        }
    }

    override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { }
    override fun onActivityResumed(activity: Activity) { }
    override fun onActivityPaused(activity: Activity) { }
    override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { }
    override fun onActivityDestroyed(activity: Activity) { }

    /// Custom callback

    fun onStartExternalActivity() {
        waitingForExternalActivity.set(true)
        scheduleWaitingForExternalActivityTimeout()
    }

    private val waitingForExternalActivityHandler = Handler()
    private val waitingForExternalActivityTimeout = object : Runnable {
        override fun run() {
            waitingForExternalActivity.set(false)
            onApplicationStopped()
        }
    }

    private fun scheduleWaitingForExternalActivityTimeout() {
        waitingForExternalActivityHandler.postDelayed(waitingForExternalActivityTimeout, 60000)
    }

    private fun cancelWaitingForExternalActivityTimeout() {
        waitingForExternalActivityHandler.removeCallbacks(waitingForExternalActivityTimeout)
    }

    /// Application lifecycle - custom code

    private fun onApplicationStarted() {
        /// *** your code here
    }

    private fun onApplicationStopped() {
        /// *** your code here
    }
}

And in the rest of your code, wherever you start an external activity, you would need to first notify your application. For example:

fun pickFile(callback: ((Uri?) -> Unit)) {
    mainApplication.onStartExternalActivity()

    pickFileCallback = callback
    pickFileLauncher.launch(arrayOf("*/*"))
}

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Alessandro Mulloni