3

I have some code executed on an SyncAdapter on the background thread that looks like this

Task<DocumentSnapshot> docTask = FirebaseFirestore.
          getInstance().
          collection("users_dummy").
          document("ID1").
          get();

How can I continue on the background thread when this task has been finished?

I tried to the .addOnCompleteListener after the get

 .addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>()
                {
                    @Override
                    public void onComplete(@NonNull Task<DocumentSnapshot> task)
                    {
                       // DOESN'T WORK, THIS IS THE MAIN THREAD

                    }
                });

but as noted here, the callback is done on the UI or main thread, which actually throws a

java.lang.IllegalStateException: Must not be called on the main application thread

when I perform code like Tasks.await(some task)

Full example here (ignore at will):

    private void performTheCodeThatDoesNotWork()
    {

        Task<DocumentSnapshot> docTask = FirebaseFirestore.getInstance().collection("users_dummy")
                .document("ID1").get()
                .addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>()
                {
                    @Override
                    public void onComplete(@NonNull Task<DocumentSnapshot> task)
                    {

                        try
                        {
                            // Query the second one but block this time
                            Task<DocumentSnapshot> secondId = FirebaseFirestore.getInstance().collection("users_dummy")
                                    .document("ID2").get();

/* THIS WILL THROW THE EXCEPTION ->*/ Tasks.await(secondId);
                            DocumentSnapshot result = secondId.getResult();
                        }
                        catch (Exception ex)
                        {
                            ex.printStackTrace();
                        }

                        Log.i("TAG", "Result is there but never reached");
                    }
                });

        Log.i("TAG", "Im here quickly, because async");

    }

I've tried Task.continueWith but with the same results. Do I need to facilitate an Executor and call addOnCompleteListener with it like in this doc stating leaking activities?

Or am I missing something trivial about this issue? Is it that hard to continue on some thread but perform blocking calls there?

Samuel
  • 6,126
  • 35
  • 70

2 Answers2

4

If you want your callback to be invoked on a thread other than the main thread, you will have to provide an Executor with the listener that will schedule the invocation of the listener on a non-main thread. This is your only option.

Tasks.await() is almost never the right thing to use, especially not on the main thread, since it may block. It's only for use on background threads, and only when you absolutely know you need blocking behavior.

Doug Stevenson
  • 297,357
  • 32
  • 422
  • 441
  • Could you elaborate why it is actually bad? My SyncAdapter is on the background thread and can be as blocking as he requires to, am I right? IMHO a valid solution in my above code would be to use two `Task.await` for the first and second task instead of using a continuation on the first and await on the second. If both Tasks are awaited I always stay on the bg thread. Making both tasks async just make my SyncAdapter complex because AFAIK the onPerformSync should be blocking. Without Task.await I would have to mimic this behaviour with `CountDownLatch` which looks like the more complex solution – Samuel Feb 27 '18 at 10:34
  • 1
    It's only valid off the main thread. https://medium.com/google-developers/why-are-the-firebase-apis-asynchronous-e037a6654a93 – Doug Stevenson Feb 27 '18 at 16:01
  • I understand. Do you have experience with Firebase API in a SyncAdapter? The `SyncAdapter.onPerformSync` is called on a BG thread but the method-call itself must be blocking. When I call async APIs I need to sync their finalization to `onPerformSync`. Would you recommend using Task.await all the way without addOnCompleteListener (and its processing on the main thread) or would you stay async and use `CountDownLatch` to sync the completion with `onPerformSync`. Or even something else? – Samuel Feb 27 '18 at 17:22
  • Whatever best meets your needs. – Doug Stevenson Feb 27 '18 at 17:32
2

i also had this problem but saw that firebase addOncompletion listener has a executor you can supply so you can stay on the same thread as the executor. so i created the following:

class ExecuteOnCaller: Executor {
     private val threadLocalHandler = object : ThreadLocal<Handler>() {
         override fun initialValue(): Handler {
             var looper = Looper.myLooper()
             if (looper == null)
                 looper = Looper.getMainLooper()
             return Handler(looper)
         }
     }

     private val handler = threadLocalHandler.get()
     override fun execute(command: Runnable?) {
         handler?.post(command)
     }
 }

and then you can use the executor like this:

 FirebaseAuth.getInstance().currentUser?.getIdToken(true)?.addOnCompleteListener(ExecutOnCaller(),object: OnCompleteListener<GetTokenResult> {
                override fun onComplete(task: Task<GetTokenResult>) {
                    when {
                            task.isSuccessful -> {
                                task.result.token?.let {
                                   //do whatever you want

                            }
                            else -> {
                                task.exception?.let {//some error occured}
                            }
                        }
                }

            })

notice how i passed the executor to the addoncompleteListener . if i did not do this firebase will run it on the mainThread. careful not to do UI work here though. for more info check out this post which really helped me

j2emanue
  • 60,549
  • 65
  • 286
  • 456