androidkotlinkotlin-coroutinesktorregisterforactivityresult

How to get result using registerForActivityResult from within ktor's Routing call running in a non-activity class?


How to get result from another activity (registerForActivity) from with in ktor's Routing API call (eg. /POST) running in a non-activity class?

Background: For an Android app, I run ktor server engine 'netty' in a non-activity class HttpServer.kt. I need to call another app's activity from with in ktor's Routing' POST handler, so I pass 'appCompatActivity' from MainActivity.kt. That's done, just because, I assume, registerForActivityResult() has dependency on UI/life cycle class.

Problem arises when running this as below, as registerForActivityResult() requires to be run earlier (like onCreate() ?), and I don't have such a class in this non-activity class. Moreover, the callback to run when ActivityResult is returned needs to call ktor ApplicationCall's respond which is also a suspend function.

class HttpServer(
    private val applicationContext: AppCompatActivity
) {
    private val logger = LoggerFactory.getLogger(HttpServer::class.java.simpleName)
    private val server = createServer()

    private fun ApplicationCall.startSaleActivityForResult() {    //    <======   *
        val activityLauncherCustom =
            applicationContext.registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result: ActivityResult ->
                if (result.resultCode == Activity.RESULT_OK || result.resultCode == Activity.RESULT_CANCELED) {
                    val transactionResultReturned = result.data
                    // Handle the returned result properly using transactionResultReturned
                    GlobalScope.launch {
                        respond(status = HttpStatusCode.OK, TransactionResponse())
                    }
                }
            }
        val intent = Intent()    
        // Ignoring statements to create proper action/data intent
        activityLauncherCustom.launch(intent)    //    <======   *
    }

    fun start() = server.start()

    fun stop() = server.stop(0, 0)

    private fun createServer(): NettyApplicationEngine {
        return GlobalScope.embeddedServer(Netty) {
            install(CallLogging)
            install(ContentNegotiation) {
                gson {
                    setPrettyPrinting()
                }
            }
            routing {
                route("/") {
                    post {
                        call.startSaleActivityForResult()    //    <======   *
                    }
                }
            }
        }
    }

    private fun <TEngine : ApplicationEngine, TConfiguration : ApplicationEngine.Configuration>
            CoroutineScope.embeddedServer(
        factory: ApplicationEngineFactory<TEngine, TConfiguration>,
        module: Application.() -> Unit
    ): TEngine {
        val environment = applicationEngineEnvironment {
            this.parentCoroutineContext = coroutineContext + parentCoroutineContext
            this.log = logger
            this.module(module)

            connector {
                this.port = 8081
            }
        }
        return embeddedServer(factory, environment)
    }
}

Above is what I tried, but gives below error. And I don't have onCreate on this non-activity class.

java.lang.IllegalStateException: LifecycleOwner com.youtap.upti.MainActivity@38dcf06 is attempting to register while current state is RESUMED. LifecycleOwners must call register before they are STARTED.

Any suggestions to resolve this problem would be grateful.

Below same above snippet as a screenshot to display helper text on declaration/param types from Android Studio: enter image description here

And I invoke this server class from onCreate() of MainActivity: enter image description here


Solution

  • To solve your problem and to hide the complexity you can create an intermediate class for launching activity and waiting for a result to come:

    import kotlinx.coroutines.channels.Channel
    
    class Repository(private val activity: MainActivity) {
        private val channel = Channel<Int>(1)
    
        suspend fun get(input: String): Int {
            activity.activityLauncher.launch(input)
            return channel.receive()
        }
    
        suspend fun callback(result: Int) {
            channel.send(result)
        }
    }
    

    You can store a reference to a repository and an activity launcher in the MainActivity class:

    class MainActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_main)
    
            CoroutineScope(Dispatchers.IO).launch {
                HttpServer(this@MainActivity).also { it.start() }
            }
        }
    
        val activityLauncher = registerForActivityResult(MySecondActivityContract()) { result ->
            GlobalScope.launch {
                repository.callback(result!!)
            }
        }
    
        val repository = Repository(this)
    }
    

    My second activity and a contract looks like the following:

    class ChildActivity : AppCompatActivity() {
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(R.layout.activity_child)
    
            val result = Intent()
            result.putExtra("name", 6666)
            result.data = Uri.parse("http://mydata")
            setResult(Activity.RESULT_OK, result)
            finish()
        }
    }
    
    class MySecondActivityContract : ActivityResultContract<String, Int?>() {
    
        override fun createIntent(context: Context, input: String?): Intent {
            return Intent(context, ChildActivity::class.java)
                .putExtra("my_input_key", input)
        }
    
        override fun parseResult(resultCode: Int, intent: Intent?): Int? = when {
            resultCode != Activity.RESULT_OK -> null
            else -> intent?.getIntExtra("name", 42)
        }
    
        override fun getSynchronousResult(context: Context, input: String?): SynchronousResult<Int?>? {
            return if (input.isNullOrEmpty()) SynchronousResult(42) else null
        }
    }
    

    The most simplest part is routing handler:

    routing {
        route("/") {
            post {
                val result = (applicationContext as MainActivity).repository.get("input")
                call.respondText { result.toString() }
            }
        }
    }
    

    This solution works but only one request is processed at the same time and it's not robust because Activity may be destroyed before HTTP server or repository objects.