1

I have a Google Map implemented in the usual fashion, calling MapFragment's getMapAsync() and then waiting for onMapReady() to be called to provide the GoogleMap instance. This all works fine until I add code to wait for onMapReady() to be called, to ensure that the GoogleMap instance is available before proceeding.

There is a short stretch of time between getMapAsync() returning and onMapReady() being called (makes sense -- otherwise it wouldn't need to be asynchronous). Although, in my informal testing, the delay tends to be less than half a second, I want to handle situations where the delay might be longer.

With that goal in mind, I added a simple wait loop to block on a variable called "map_ready". map_ready is initially false and it gets set to true by onMapReady(). The waitForGoogleMap() method simply loops, testing the variable periodically and returning only when the variable becomes true. Here's the code outline.

    static boolean map_ready = false;

    private void init_gmap_fragment(Activity a) {
        [...]
        MapFragment f_new = new MapFragment();
        [...]
        f_new.getMapAsync(this);
        waitForGoogleMap();
    }

    synchronized private void waitForGoogleMap() {
        while (!map_ready) {
            Log.d(LOGTAG, "Waiting for GoogleMap...");
            try {
                wait(1000);
            } catch (Exception e) { Log.d(LOGTAG, "Exception!"); }
        }
    }

    public void onMapReady(GoogleMap gmap) {
        try {
            this.gmap = gmap;
            map_ready = true;
        }
        [...]
    }

The code as shown above prints out "Waiting for GoogleMap..." over and over -- it appears that running waitForGoogleMap() prevents onMapReady() from ever being called. If I comment out the call to waitForGoogleMap(), it runs just fine.

What am I doing wrong?


UPDATE/CLARIFICATION: I think the core issue I'm facing is that I want to listen for an event which is expected to occur on the same thread as the listener. In this case, waitForGoogleMap() is waiting for the event "onMapReady() called" and both are on the main thread.

I am making the assumption that callbacks such as onMapReady() are implemented by creating a worker thread which does the necessary legwork and then calls the main thread's Handler to add the callback (e.g., onMapReady()) to the main thread's MessageQueue. Also, that when I call wait() from the main thread that the main thread's looper will take that opportunity to excecute another item from its queue.

But apparently I'm wrong :-).

Bottom line: How can I block until onMapReady() executes?


Thanks, Barry

P.S. The above is a simplified version of my code. The containing class which implements OnMapReadyCallback (and hence the onMapReady() callback) is actually a singleton. The instance is created with an Instance() method and the instance is subsequently obtained by a separate getInstance() method. Once this is working, I will put the call to waitForGoogleMap() in the getInstance() method. That way the app won't block until it absolutely needs to.

Nikola Despotoski
  • 49,966
  • 15
  • 119
  • 148
Barry Holroyd
  • 1,005
  • 1
  • 11
  • 20

1 Answers1

2

What am I doing wrong?

waitForGoogleMap() is running on the same thread that onMapReady() is supposed to be called on, and so onMapReady() cannot be called until waitForGoogleMap() returns.

Get rid of waitForGoogleMap(). Kick off the work that needs the GoogleMap in onMapReady(). If need be, use an event bus to get the "hey, the map is ready!" event over to some other component.

CommonsWare
  • 986,068
  • 189
  • 2,389
  • 2,491
  • Thank you for your answer. Certainly I already do the necessary work in onMapReady() -- no issue there. However, imagine a scenario where the onMapReady() call back doesn't occur for a minute or two. The main UI thread will continue after the getMapAsync() call and will quickly run into code that depends on stuff that is supposed to be initialized in onMapReady(). When that happens, I want to block until onMapReady() is actually called. Although onMapReady() is called on the main thread, I assumed the work kicked off by getMapAsync() would be on a worker thread. Is that incorrect? – Barry Holroyd Nov 09 '15 at 21:06
  • @BarryHolroyd: "will quickly run into code that depends on stuff that is supposed to be initialized in onMapReady()" -- then you have fundamental architectural issues with your app. "I want to block until onMapReady() is actually called" -- that will succeed in freezing your UI and triggering an ANR in ~5 seconds. It will also make the user wonder why your app is buggy. "Is that incorrect?" -- `getMapAsync()` is called on the main application thread. Pretty much unless you're told otherwise, **everything** is called on the main application thread. – CommonsWare Nov 09 '15 at 21:14
  • @BarryHolroyd: If there are things that you cannot allow the user to do until the map is ready, do not enable those options until inside of `onMapReady()`. Use progress indicators and the like in the interim, allowing the user to get to other portions of your app that do not require the map just yet. – CommonsWare Nov 09 '15 at 21:15
  • Good comments, thanks. I'm starting to get my head around this. I think my architecture is pretty sound. The problem is that my app is 100% map-driven -- in fact, that's kind of what makes it different. The user starts with a blank map and the only interesting steps that can be taken require the map. I'm not too worried about an ANR -- I would check very second or so and update the user, not just wait interminably. I suppose I can react to each attempt of the user to do something, but I'd rather have a progress indicator (as you suggest) indicating the app is waiting on the Gmap service. – Barry Holroyd Nov 09 '15 at 22:21
  • Still not sure why my approach isn't working. Seems to me the looper should run onMapReady() whenever it finally executes; if so, I could then keep the user apprised of the status ("Waiting on Google Map service...") and eventually time out with an appropriate message if it didn't respond in a reasonable amount of time. – Barry Holroyd Nov 09 '15 at 22:26
  • @BarryHolroyd: "the only interesting steps that can be taken require the map" -- then disable the user input for those "interesting steps" until `onMapReady()` is called. "Seems to me the looper should run onMapReady() whenever it finally executes" -- you are not letting the `Looper` get control. You are in an infinite loop, and therefore the `Looper` will not get control until the heat death of the universe, give or take. "keep the user apprised of the status" -- show the message up front, then clear it in `onMapReady()`. – CommonsWare Nov 09 '15 at 22:29
  • Sorry for the delay -- I had to refactor some relevant code based on your input (thanks). Bottom line now: I guess I have to display a "waiting" screen until the map is ready, then have onMapReady() switch screens to the GoogleMap when it is called and, during that same "waiting" period disable user input. Action item for me: get a better understanding of Looper (I still don't understand when/how control is given up so that the next message in the queue can get processed). THANK YOU for your help! – Barry Holroyd Nov 13 '15 at 21:05