0

I'm using Microsoft's msal library to have users sign in to their account. I'm using MVVM so I need to the function to return a Resource<String> but the function is returning a value before the msal callbacks are being invoked and updating the value I need to return. I've tried:

  1. Using CoroutineScope(IO).launch {} and running the sign-in function in there but that does not return asynchronously.
  2. Using CoroutineScope(IO).async {} and then return@async Resource inside the callback. I'm compiler complains since the callback is void type.
  3. Using Kotlin FLow<Resource> and emit value from inside the callback.

Can anyone help me out with any ideas of how to get this to run asynchronously or return a value from within the callbacks? Thanks!

suspend fun microsoftSignIn(activity: Activity): Resource<String> {
    var resource: Resource<String> = Resource.Error(Throwable(""))
    CoroutineScope(Dispatchers.IO).async {
            PublicClientApplication.createSingleAccountPublicClientApplication(context, R.raw.auth_config_single_account, object : IPublicClientApplication.ISingleAccountApplicationCreatedListener {
                override fun onCreated(application: ISingleAccountPublicClientApplication?) {
                    application?.signIn((activity as AllActivityBase), null, Array(1) {"api://ScopeBlahBlah"}, object : AuthenticationCallback {
                        override fun onSuccess(authenticationResult: IAuthenticationResult?) {
                            Log.i("TOKEN_MSAL ${authenticationResult?.accessToken}" )
                            authenticationResult?.accessToken?.let { storeToken(it) }
                            ///// I need this to run first before "return resource" runs!!! /////
                            resource = Resource.Success("Success")
                        }

                        override fun onCancel() {
                            resource = Resource.Error(Throwable(""))
                        }

                        override fun onError(exception: MsalException?) {=
                            resource = Resource.Error(Throwable(""))
                        }

                    })
                }

                override fun onError(exception: MsalException?) {
                    Log.i("TOKEN_MSAL_A EX ${exception?.message}")
                    resource = Resource.Error(Throwable(""))
                }

            })
    }
    return resource
}
Zain
  • 37,492
  • 7
  • 60
  • 84
Matt Casey
  • 15
  • 1

2 Answers2

1

If you need to consume a traditional asynchronous API and make it suspendable, the easiest is to use either suspendCoroutine() or CompletableDeferred. I can't easily reuse your code, so I will provide you just a generic solution, but it should be easy for you to adjust it to your needs.

suspendCoroutine():

suspend fun test1(): String {
    return withContext(Dispatchers.IO) {
        suspendCoroutine { cont ->
            doSomethingAsync(object : Callback {
                override fun onSuccess() {
                    cont.resume("success")
                }

                override fun onError() {
                    cont.resume("error")
                }
            })
        }
    }
}

CompletableDeferred:

suspend fun test2(): String {
    val result = CompletableDeferred<String>()
    withContext(Dispatchers.IO) {
        doSomethingAsync(object : Callback {
            override fun onSuccess() {
                result.complete("success")
            }

            override fun onError() {
                result.complete("error")
            }
        })
    }
    return result.await()
}

As you can see, both solutions are pretty similar. I'm not sure if there are any big differences between them. I guess not, so use whichever you like more.

Also, in both cases you can throw an exception instead of returning an error. You can use resumeWithException() for the first example and completeExceptionally() for the second.

One final note: you used Dispatchers.IO in your example, so I did the same, but I doubt it is needed here. Asynchronous operations don't block the thread by definition, so it should be ok to run them from any dispatcher/thread.

broot
  • 21,588
  • 3
  • 30
  • 35
  • 1
    @Tenfour04 Yes, you are right, I updated my answer :-) – broot Jun 22 '21 at 20:44
  • If `doSomethingAsync` actually were a blocking function, it wouldn't be valid to use `suspendCoroutine` to call it, because then `suspendCoroutine` would be blocking, which breaks the convention that suspend functions must never block. It might work with how you're wrapping it in `withContext` (which for a correctly non-blocking suspend function call is pointless), but I don't know if it could possibly tie up a thread that it shouldn't. – Tenfour04 Jun 22 '21 at 22:45
  • Thanks for this answer is exactly what I needed! Using the CompletableDeferred is what fixed it for me – Matt Casey Jun 23 '21 at 16:26
0

To convert a callback-based function into something you can use properly in coroutines, you need to use suspendCoroutine or suspendCancellableCoroutine. Assuming your code above is correct (I'm not familiar with msal), it should look something like this. You have two callbacks to convert. These are top-level utility functions that you can put anywhere in your project:

suspend fun createSingleAccountPublicClientApplication(
    context, 
    @RawRes authConfig: Int
): ISingleAccountPublicClientApplication = suspendCoroutine { continuation ->
    PublicClientApplication.createSingleAccountPublicClientApplication(context, authConfig, object : IPublicClientApplication.ISingleAccountApplicationCreatedListener {
        override fun onCreated(application: ISingleAccountPublicClientApplication) {
            continuation.resume(application)
        }

        override fun onError(exception: MsalException) {
            continuation.resumeWithException(exception)
        }
    })
}

suspend fun ISingleAccountPublicClientApplication.signInOrThrow(
    activity: AllActivityBase, // or whatever type this is in the API
    someOptionalProperty: Any?, // whatever this type is in the API
    vararg args: String
): IAuthenticationResult = suspendCancellableCoroutine { continuation ->
    signIn(activity, someOptionalProperty, args, object: : AuthenticationCallback {
        override fun onSuccess(authenticationResult: IAuthenticationResult) {
            continuation.resume(authenticationResult)
        }

        override fun onCancel() = continuation.cancel()

        override fun onError(exception: MsalException) {
            continuation.resumeWithException(exception)
        }
    })
}

Then you can use this function in any coroutine, and since it's a proper suspend function, you don't have to worry about trying to run it asynchronously or specifying Dispatchers.

Usage might look like this, based on what I think you were doing in your code:

suspend fun microsoftSignIn(activity: Activity): Resource<String>
    try {
        val application = createSingleAccountPublicClientApplication(context, R.raw.auth_config_single_account)
        val authenticationResult = application.signInOrThrow((activity as AllActivityBase), null, "api://ScopeBlahBlah")
        Log.i(TAG, "TOKEN_MSAL ${authenticationResult.accessToken}" )
        authenticationResult.accessToken?.let { storeToken(it) }
        return Resource.Success("Success")
    } catch (e: MsalException) {
        return Resource.Error(e)
    }
}
Tenfour04
  • 83,111
  • 11
  • 94
  • 154
  • Other answer is short and to-the-point. Just posting this overly detailed one because I was mostly through writing it. – Tenfour04 Jun 22 '21 at 20:48