NSouth
NSouth

Reputation: 5276

How to use async/await with an SDK that expects call-back based asynchronous activity

I am implementing native Android Facebook authentication in Xamarin Forms and am struggling with the Facebook SDK breaking my await/async chain. The SDK requires passing implementations of IFacebookCallback and GraphRequest.IGraphJSONObjectCallback which define callbacks to process results. I would like to do the following in code that requests a login:

public async Task LogInFacebookAsync()
{
    var loginResult = await _facebookManager.Login();  
    // do stuff with result
}

However, here's what I'm currently doing.
Code that requests a login:

public void LogInFacebookAsync()
{
    _facebookManager.Login(onFacebookLoginComplete);
}
private async Task<string> onFacebookLoginComplete(FacebookUser fbUser, string errorMessage) 
{
    // do stuff with result
}

Here is my FacebookManager class which implements the necessary interfaces. Notice that results can come from multiple callback methods: OnCancel(), OnError(), OnCompleted(), each of which invokes _onLoginComplete as passed in by the code above.

public class FacebookManager : Java.Lang.Object, IFacebookManager, IFacebookCallback, GraphRequest.IGraphJSONObjectCallback
{
    public Func<FacebookUser, string, Task> _onLoginComplete;
    public ICallbackManager _callbackManager;
    private Activity _context;

    public FacebookManager()
    {
        this._context = CrossCurrentActivity.Current.Activity;
        _callbackManager = CallbackManagerFactory.Create();
        LoginManager.Instance.RegisterCallback(_callbackManager, this);
    }

    public async Task Login(Func<FacebookUser, string, Task> onLoginComplete)
    {
        _onLoginComplete = onLoginComplete;
        LoginManager.Instance.SetLoginBehavior(LoginBehavior.NativeWithFallback);
        LoginManager.Instance.LogInWithReadPermissions(_context, new List<string> { "public_profile", "email" });
        await Task.CompletedTask;
    }

    #region IFacebookCallback   
    public void OnCancel()
    {
        _onLoginComplete?.Invoke(null, "Canceled!");
    }
    public void OnError(FacebookException error)
    {
        _onLoginComplete?.Invoke(null, error.Message);
    }       
    public void OnSuccess(Java.Lang.Object result)
    {
        var n = result as LoginResult;
        if (n != null)
        {
            var request = GraphRequest.NewMeRequest(n.AccessToken, this);
            var bundle = new Android.OS.Bundle();
            bundle.PutString("fields", "id, first_name, email, last_name, picture.width(500).height(500)");
            request.Parameters = bundle;
            request.ExecuteAsync();
        }
    }
    #endregion

    #region GraphRequest.IGraphJSONObjectCallback
    public void OnCompleted(JSONObject p0, GraphResponse p1)
    {       
        // extract user
        _onLoginComplete?.Invoke(fbUser, string.Empty);   
    }
    #endregion
}

Is there any way I can wrap these operations so that I am "async all the way down"?

Update:

Thank you Paulo for the great answer. I was already thinking along those lines and you really crystallized the solution. My only concern with the answer was about it being thread-safe. What if a button is double-clicked or two API calls are made at the same time that require Facebook login? The following is my attempt to make the LoginAsync() method safer. However, I'm no expert at thread safety and I welcome all criticism.

private TaskCompletionSource<FacebookUser> _loginTcs;
public async Task<FacebookUser> LoginAsync()
{
    Task<FacebookUser> loginTask = _loginTcs?.Task;
    // Don't want to create a new login request to the Facebook API if one is already being made.
    if (loginTask != null && (loginTask.Status == TaskStatus.Running || loginTask.Status == TaskStatus.WaitingForActivation 
        || loginTask.Status == TaskStatus.WaitingToRun || loginTask.Status == TaskStatus.WaitingForChildrenToComplete))
    {
        return await loginTask.ConfigureAwait(false);
    }

    this._loginTcs = new TaskCompletionSource<FacebookUser>();
    LoginManager.Instance.SetLoginBehavior(LoginBehavior.NativeWithFallback);
    LoginManager.Instance.LogInWithReadPermissions(_context, new List<string> { "public_profile", "email" });
    var user = await this._loginTcs.Task.ConfigureAwait(false);
    return user;
}

Upvotes: 0

Views: 291

Answers (1)

Paulo Morgado
Paulo Morgado

Reputation: 14856

Use a TaskCompletionSource. No need for anything else.

I don't know a lot about that API (and I'm surprised Xamarin does not provide na idiomatic .NET version of it) but, roughly, what you need to do is:

Add a task completion source to your class instead of the _onLoginComplete delegate:

// public Func<FacebookUser, string, Task> _onLoginComplete;
private TaskCompletionSource<FacebookUser> completion;

Change your OnCancel method to cancel the task:

public void OnCancel()
{
    this.completion.TrySetCanceled();
}

Change your OnError method to complete the task with error:

public void OnError(FacebookException error)
{
    this.completion.TrySetException(error);
}

Change your OnCompleted to set the task result:

public void OnCompleted(JSONObject p0, GraphResponse p1)
{       
    // extract user
    this.completion.TrySetResult(fbUser);   
}

Change your login method to create the task completion source and return the task completion source's task:

public async Task<FacebookUser> LoginAsync()
{
    this.completion = new TaskCompletionSource<FacebookUser>();
    LoginManager.Instance.SetLoginBehavior(LoginBehavior.NativeWithFallback);
    LoginManager.Instance.LogInWithReadPermissions(_context, new List<string> { "public_profile", "email" });
    await this.completion.Task;
}

Upvotes: 3

Related Questions