0

I am trying to use the non-blocking FusedLocationProvider API as a blocking one. Here's what I am doing (roughly):

  1. Start an AsyncTask
  2. In the background thread, connect to the PlayServices/FusedProvider API. There is a blocking method with a timeout available for this.
  3. Using the above API, request a location update. The API will calculate a the location using the device and trigger a callback which executes on the caller thread.
  4. Wait for the callback using countDownLatch.await(timeOut,timeUnit)
  5. When the Fused API triggers the callback, retrieve the location and do a countDownLatch.countdown(), this will release the latch.
  6. Return the location.

I need a way to block since I may need to calculate the location and work with it, at any point of time. The above approach is working but I am seeking a better way since the CountDownLatch is being triggered from the same thread it is blocking. I don't know whether I am committing sacrilege/sheer dumbery by doing this.

Here's my code:

public class BlockingLocationProvider implements ConnectionCallbacks,
        OnConnectionFailedListener, LocationListener {

    private static final String TAG = "BlockingLocationProvider";

    private LocationInfo mLocInfoRow;
    private long mCallTime;
    private Double mDel;
    private GoogleApiClient mGoogleApiClient;
    private Location mLocation;
    private LocationRequest locationRequest;
    private boolean mLocationCalculated = false;
    private CountDownLatch latch;

    private Context context;

    private ConnectionResult gApiConnResult;

    @Override
    public void onConnectionFailed(ConnectionResult arg0) {
        Logger.error(TAG, "Google play services error while getting location.");
        mLocInfoRow = new LocationInfo().timestamp(System.currentTimeMillis())
                .status(LocStatus.ERR_G_PLAY).statusCode(arg0.getErrorCode());
        mLocation = null;
        mLocationCalculated = true;
        latch.countDown();
    }

    @Override
    public void onConnected(Bundle arg0) {
        locationRequest = new LocationRequest();
        locationRequest.setInterval(0).setNumUpdates(1)
                .setPriority(LocationRequest.PRIORITY_HIGH_ACCURACY)
                .setExpirationDuration(2000);
        LocationServices.FusedLocationApi.requestLocationUpdates(
                mGoogleApiClient, locationRequest, this);
    }

    @Override
    public void onConnectionSuspended(int arg0) {
        Logger.warning(TAG, "Can't handle suspension.");
        mLocInfoRow = new LocationInfo().timestamp(System.currentTimeMillis())
                .status(LocStatus.ERR_UNKNOWN).statusCode(-200);
        latch.countDown();
    }

    @Override
    public void onLocationChanged(Location arg0) {
        mLocation = arg0;
        if (mLocation == null) {
            Logger.debug(TAG, "Fused provider returned null");
            mLocInfoRow = new LocationInfo()
                    .timestamp(System.currentTimeMillis())
                    .status(LocStatus.ERR_FUSED_NULL).statusCode(-3);
        } else {
            Logger.debug(TAG, "Got a location from fused provider.");
            mLocInfoRow = new LocationInfo().timestamp(mLocation.getTime())
                    .lat(mLocation.getLatitude()).lng(mLocation.getLongitude())
                    .travelSpeed(mLocation.getSpeed())
                    .altitude(mLocation.getAltitude())
                    .acc(mLocation.getAccuracy()).provider("fused")
                    .status("OK").statusCode(0);

        }
        mLocationCalculated = true;
        latch.countDown();
    }

    public LocationInfo getLocationInfo(Context ctx) {
        this.context = ctx;
        calcLocation();
        return mLocInfoRow;
    }

    public Location getLocation(Context ctx) {
        this.context = ctx;
        calcLocation();
        return mLocation;
    }

    // This should block
    public void calcLocation() {

        if (Thread.currentThread().getName().equals("main")) {
            throw new RuntimeException("Cannot run " + TAG + " on UI thread.");
        }

        latch = new CountDownLatch(1);

        /* To figure how long it takes to calc location */
        mCallTime = System.currentTimeMillis();

        Logger.verbose(TAG, "Checking play services.");
        // First check play services
        int playServicesAvailable = GooglePlayServicesUtil
                .isGooglePlayServicesAvailable(context);
        // whoopsie!
        if (playServicesAvailable != ConnectionResult.SUCCESS) {
            Logger.error(TAG,
                    "Google play services error while getting location.");
            mLocInfoRow = new LocationInfo()
                    .timestamp(System.currentTimeMillis())
                    .status(LocStatus.ERR_G_PLAY)
                    .statusCode(playServicesAvailable);
            mLocation = null;
            return;
        }

        Logger.verbose(TAG, "Checking GPS.");
        // Then check GPS enabled or not
        if (!isGpsEnabled(context)) {
            Logger.error(TAG,
                    "User has disabled GPS. Unable to provide location updates.");
            mLocInfoRow = new LocationInfo()
                    .timestamp(System.currentTimeMillis())
                    .status(LocStatus.ERR_NO_GPS).statusCode(-2);
            mLocation = null;
            return;
        }

        Logger.verbose(TAG, "Connecting to play services.");
        // Then connect to play services client
        mGoogleApiClient = new GoogleApiClient.Builder(context)
                .addConnectionCallbacks(this)
                .addOnConnectionFailedListener(this)
                .addApi(LocationServices.API).build();

        if (!(mGoogleApiClient.isConnected() || mGoogleApiClient.isConnecting())) {
            gApiConnResult = mGoogleApiClient.blockingConnect(5,
                    TimeUnit.SECONDS);

        }

        boolean timeout = false;

        if (gApiConnResult.isSuccess()) {
            try {
                Logger.warning(TAG, "Waiting latch on location request...");
                timeout = !latch.await(5, TimeUnit.SECONDS);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            if (timeout) {
                Logger.warning(TAG, "Latch timeout!");
                mLocInfoRow = new LocationInfo()
                        .timestamp(System.currentTimeMillis())
                        .status(LocStatus.ERR_TIMEOUT).statusCode(-100);
            } else {
                Logger.debug(TAG, "Latch triggered!");
            }

        } else {
            Logger.warning(TAG, "gApi connect timeout!");
            mLocInfoRow = new LocationInfo()
                    .timestamp(System.currentTimeMillis())
                    .status(LocStatus.ERR_TIMEOUT).statusCode(-300);
        }

        mDel = (Double.valueOf(System.currentTimeMillis() - mCallTime) / 1000);
        Logger.debug(TAG, "Took " + mDel + " seconds for location update.");
        LocationServices.FusedLocationApi.removeLocationUpdates(
                mGoogleApiClient, this);
        mGoogleApiClient.disconnect();
    }

    private static boolean isGpsEnabled(final Context ctx) {
        LocationManager lm = (LocationManager) ctx
                .getSystemService(Context.LOCATION_SERVICE);
        return lm.isProviderEnabled(LocationManager.GPS_PROVIDER);
    }

}

Is there any strategy apart from a latch on an infinite loop & flags to block in this case? Thanks.

iceman
  • 826
  • 13
  • 25
  • Is there any real reason for blocking at all? You have all those callbacks from the api which deliver you data as it becomes available, but you put them essentially on a leash in an extra `Thread`? Why? Just use the callbacks. No `Threads` or asynchronous operations needed. – Xaver Kapeller Mar 16 '15 at 10:11
  • I am using a framework that sends the user's location as part of some network communications. The call to plug in location needs to block since the user is waiting for the network update to complete (they see an 'updating' dialog). This is certainly not an ideal approach and I would like to re-engineer the solution to use callbacks, but I'm looking for a better alternative to callbacks (than this) as a stop gap till then. – iceman Mar 16 '15 at 10:29
  • What is bothering me is that the `CountDownLatch` being triggered from the same thread that it is waiting in. I clearly don't understand what `await()` is doing if a callback can be executed in the same thread. – iceman Mar 16 '15 at 10:37
  • Also, is there a very valid reason for `FusedLocationProvider` to expose only callbacks? They could have exposed a blocking api with a timeout. Something like `getCurrentBestLocation(10,TimeUnit.SECONDS)` – iceman Mar 16 '15 at 10:40
  • 1
    Most Google API's are completely centered around callbacks. It can be a mess sometimes, but the key to using them optimally is just to play into this callback strategy. Don't try to block until the callback is called. Whatever you are doing in the `Thread` which is being blocked you can do just as well in the callback itself. What you are trying to do will just cause problems in the long run. Both from a technical and a design perspective. – Xaver Kapeller Mar 16 '15 at 10:49
  • I have a http message sender class which wraps the data being sent in various info e.g. username, app version inside an `AsyncTask`. This API is used all over the place. If I'm not mistaken I'll have to extract the location out of the message building part and start the `AsyncTask` in the callback. Apparently cleaner (because does not block), but what's the harm in blocking for a few seconds and timing out? Thanks BTW! – iceman Mar 16 '15 at 10:56
  • Also the play services client `GoogleApiClient` has blocking connect method with a timeout. – iceman Mar 16 '15 at 10:56
  • 1
    It can have all sorts of side effects aside from the fact that you are creating a `Thread` basically for no reason at all, just to block it until you receive a callback? What do you think happens internally in the Location API? The requests are already handled asynchronously indicated by the callback you receive some time later. What you are trying to do forces an asynchronous API to behave as if it were synchronous while simultaneously slapping your own asynchronous layer on top of it. It is just bad design, and you are not supposed to do it that way. – Xaver Kapeller Mar 16 '15 at 11:02
  • I was blocking to avoid a lot of rewrite of an already shaky code which I inherited at my workplace. Thanks for your advice :) – iceman Mar 16 '15 at 11:08

0 Answers0