package net.atomcode.bearing.location.provider; import android.content.Context; import android.location.Location; import android.os.Bundle; import android.util.Log; import com.google.android.gms.common.ConnectionResult; import com.google.android.gms.common.api.GoogleApiClient; import com.google.android.gms.location.LocationRequest; import com.google.android.gms.location.LocationServices; import net.atomcode.bearing.location.LocationListener; import net.atomcode.bearing.location.LocationProvider; import net.atomcode.bearing.location.LocationProviderRequest; import java.util.HashMap; import java.util.Map; import java.util.UUID; /** * Provide location using Google Play services */ public class GMSLocationProvider implements LocationProvider, GoogleApiClient.ConnectionCallbacks, GoogleApiClient.OnConnectionFailedListener { private static final boolean LOG = false; private static GMSLocationProvider instance; public static GMSLocationProvider getInstance() { if (instance == null) { instance = new GMSLocationProvider(); } return instance; } private GoogleApiClient apiClient; private HashMap<String, Runnable> pendingRequests; private Map<String, com.google.android.gms.location.LocationListener> runningRequests; @Override public void create(Context context) { pendingRequests = new HashMap<>(); runningRequests = new HashMap<>(); apiClient = new GoogleApiClient.Builder(context) .addConnectionCallbacks(this) .addOnConnectionFailedListener(this) .addApi(LocationServices.API) .build(); } @Override public void destroy() { pendingRequests.clear(); if (apiClient.isConnected() || apiClient.isConnecting()) { for (com.google.android.gms.location.LocationListener runningRequest : runningRequests.values()) { LocationServices.FusedLocationApi.removeLocationUpdates(apiClient, runningRequest); } runningRequests.clear(); apiClient.disconnect(); } } @Override public Location getLastKnownLocation(LocationProviderRequest request) { if (apiClient.isConnected()) { return LocationServices.FusedLocationApi.getLastLocation(apiClient); } return null; } @Override public String requestSingleLocationUpdate(final LocationProviderRequest request, final LocationListener listener) { final String requestId = UUID.randomUUID().toString(); if (!apiClient.isConnected()) { pendingRequests.put(requestId, new Runnable() { @Override public void run() { internalRequestSingleUpdate(requestId, request, listener); } }); apiClient.connect(); } else { internalRequestSingleUpdate(requestId, request, listener); } return requestId; } @Override public String requestRecurringLocationUpdates(final LocationProviderRequest request, final LocationListener listener) { final String requestId = UUID.randomUUID().toString(); if (!apiClient.isConnected()) { pendingRequests.put(requestId, new Runnable() { @Override public void run() { internalRequestRecurringUpdates(requestId, request, listener); } }); apiClient.connect(); } else { internalRequestRecurringUpdates(requestId, request, listener); } return requestId; } @Override public void cancelUpdates(String requestId) { if (pendingRequests.containsKey(requestId)) { pendingRequests.remove(requestId); } if (runningRequests.containsKey(requestId)) { LocationServices.FusedLocationApi.removeLocationUpdates(apiClient, runningRequests.get(requestId)); runningRequests.remove(requestId); if (runningRequests.size() == 0) { apiClient.disconnect(); } } } /** * Internal request for recurring updates */ private void internalRequestRecurringUpdates(final String requestId, final LocationProviderRequest request, final LocationListener listener) { final LocationRequest gmsRequest = getRecurringLocationRequestForBearingRequest(request); runningRequests.put(requestId, new com.google.android.gms.location.LocationListener() { private long lastReportedTimestamp = -1; private Location lastReportedLocation; @Override public void onLocationChanged(Location location) { long currentTimestamp = System.currentTimeMillis() / 1000; long timeSinceLastReport = currentTimestamp - lastReportedTimestamp; if (LOG) { Log.d("Bearing Location Tracker", "onLocationChanged last reported: " + timeSinceLastReport + " seconds ago (Fallback at " + request.trackingFallback / 1000 + ")"); } if (lastReportedTimestamp == -1 || timeSinceLastReport > (request.trackingFallback / 1000)) { if (LOG) { Log.d("Bearing Location Tracker", "Tracking fallback, forcing update"); } lastReportedLocation = location; lastReportedTimestamp = currentTimestamp; // Force report if (listener != null) { listener.onUpdate(location); } return; } if (request.trackingDisplacement != -1 && location.distanceTo(lastReportedLocation) > request.trackingDisplacement) { lastReportedLocation = location; lastReportedTimestamp = currentTimestamp; if (listener != null) { listener.onUpdate(location); } } } }); if (apiClient.isConnected()) { LocationServices.FusedLocationApi.requestLocationUpdates(apiClient, gmsRequest, runningRequests.get(requestId)); } else { final String connectRequestId = UUID.randomUUID().toString(); pendingRequests.put(connectRequestId, new Runnable() { @Override public void run() { LocationServices.FusedLocationApi.requestLocationUpdates(apiClient, gmsRequest, runningRequests.get(requestId)); } }); } } /** * Convert bearing request to GMS location request */ private LocationRequest getRecurringLocationRequestForBearingRequest(LocationProviderRequest request) { LocationRequest gmsRequest = new LocationRequest(); int priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY; switch (request.accuracy) { case LOW: priority = LocationRequest.PRIORITY_LOW_POWER; break; case MEDIUM: priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY; break; case HIGH: priority = LocationRequest.PRIORITY_HIGH_ACCURACY; } gmsRequest.setPriority(priority); gmsRequest.setFastestInterval(request.trackingRate); gmsRequest.setInterval(request.trackingRate); return gmsRequest; } /** * Make request for a single location update */ private void internalRequestSingleUpdate(final String requestId, final LocationProviderRequest request, final LocationListener listener) { LocationRequest gmsRequest = getSingleLocationRequestForBearingRequest(request); runningRequests.put(requestId, new com.google.android.gms.location.LocationListener() { @Override public void onLocationChanged(Location location) { if (listener != null) { listener.onUpdate(location); } runningRequests.remove(requestId); if (runningRequests.size() == 0) { apiClient.disconnect(); } } }); LocationServices.FusedLocationApi.requestLocationUpdates(apiClient, gmsRequest, runningRequests.get(requestId)); } /** * Convert bearing request to GMS Location request */ private LocationRequest getSingleLocationRequestForBearingRequest(LocationProviderRequest request) { LocationRequest gmsRequest = new LocationRequest(); gmsRequest.setNumUpdates(1); // Single update int priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY; switch (request.accuracy) { case LOW: priority = LocationRequest.PRIORITY_LOW_POWER; break; case MEDIUM: priority = LocationRequest.PRIORITY_BALANCED_POWER_ACCURACY; break; case HIGH: priority = LocationRequest.PRIORITY_HIGH_ACCURACY; } gmsRequest.setPriority(priority); return gmsRequest; } /* * ======================================================== * LocationListener and GooglePlayServicesClient callbacks * ======================================================== */ @Override public void onConnected(Bundle bundle) { // Connected. Perform pending requests for (Runnable runnable : pendingRequests.values()) { runnable.run(); } pendingRequests.clear(); } @Override public void onConnectionSuspended(int i) { } @Override public void onConnectionFailed(ConnectionResult connectionResult) { } }