'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:
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 |