/******************************************************************************* * Code contributed to the webinos project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. * * Copyright 2011-2012 Paddy Byers * ******************************************************************************/ package org.webinos.android.impl; import java.util.HashMap; import java.util.HashSet; import java.util.Set; import org.meshpoint.anode.AndroidContext; import org.meshpoint.anode.module.IModule; import org.meshpoint.anode.module.IModuleContext; import org.webinos.api.DeviceAPIError; import org.webinos.api.geolocation.Coordinates; import org.webinos.api.geolocation.GeolocationManager; import org.webinos.api.geolocation.Position; import org.webinos.api.geolocation.PositionCallback; import org.webinos.api.geolocation.PositionError; import org.webinos.api.geolocation.PositionErrorCallback; import org.webinos.api.geolocation.PositionOptions; import android.Manifest; import android.content.Context; import android.content.pm.PackageManager; import android.content.pm.PackageManager.NameNotFoundException; import android.location.Criteria; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.location.LocationProvider; import android.os.Bundle; import android.os.Handler; import android.util.Log; public class GeolocationImpl extends GeolocationManager implements IModule, LocationListener { /***************************** * private state *****************************/ private Context androidContext; private Handler timerHandler; private Set<Request> pendingRequests; private HashMap<String, Watch> watches; private long nextWatchId = 1; private int highAccuracyCount; private LocationManager locationManager; private Criteria lowAccuracyCriteria; private Criteria highAccuracyCriteria; private String currentWatchProvider; private static final float minDistanceChange = 0; private static final long minTimeChange = 100; private static final String TAG = "org.webinos.android.impl.GeolocationImpl"; /***************************** * GeolocationManager methods *****************************/ @Override public void getCurrentPosition(PositionCallback successCallback, PositionErrorCallback errorCallback, PositionOptions options) { Log.v(TAG, "getCurrentPosition(): ent"); if(successCallback == null) { Log.e(TAG, "getCurrentPosition(): no successCallback; aborting"); throw new DeviceAPIError(DeviceAPIError.TYPE_MISMATCH_ERR); } Request req = new Request(successCallback, errorCallback, options); if(req.inError) { Log.e(TAG, "getCurrentPosition(): request inError; aborting"); return; } Criteria criteria = (req.enableHighAccuracy && highAccuracyCriteria != null) ? highAccuracyCriteria : lowAccuracyCriteria; String provider = locationManager.getBestProvider(criteria, true); if(req.tryLastKnownPosition(provider)) { Log.v(TAG, "getCurrentPosition(): responding with last known position"); return; } if(provider == null) { Log.e(TAG, "getCurrentPosition(): no available provider; responding with position unavailable"); PositionError error = new PositionError(); error.code = PositionError.POSITION_UNAVAILABLE; req.dispatch(error); return; } synchronized(this) { Log.v(TAG, "getCurrentPosition(): scheduling request"); req.schedule(); locationManager.requestSingleUpdate(criteria, req, androidContext.getMainLooper()); } } @Override public long watchPosition(PositionCallback successCallback, PositionErrorCallback errorCallback, PositionOptions options) { Log.v(TAG, "watchPosition(): ent"); if(successCallback == null) { Log.e(TAG, "watchPosition(): no successCallback; aborting"); throw new DeviceAPIError(DeviceAPIError.TYPE_MISMATCH_ERR); } Watch watch = new Watch(successCallback, errorCallback, options); if(watch.inError) { Log.e(TAG, "watchPosition(): request inError; returning 0"); return 0; } Log.v(TAG, "watchPosition(): ret " + watch.id); return watch.id; } @Override public void clearWatch(long id) { Log.v(TAG, "clearWatch(): ent id = " + id); Watch watch = getWatch(id); if(watch != null) { watch.deschedule(); removeWatch(watch); } } /***************************** * IModule methods *****************************/ @Override public Object startModule(IModuleContext ctx) { Log.v(TAG, "startModule(): ent"); try { androidContext = ((AndroidContext)ctx).getAndroidContext(); timerHandler = new Handler(androidContext.getMainLooper()); pendingRequests = new HashSet<Request>(); watches = new HashMap<String, Watch>(); locationManager = (LocationManager) androidContext.getSystemService(Context.LOCATION_SERVICE); String[] permissions = androidContext.getPackageManager().getPackageInfo(androidContext.getPackageName(), PackageManager.GET_PERMISSIONS).requestedPermissions; boolean hasCoarsePermission = false, hasFinePermission = false; for(String perm : permissions) { hasCoarsePermission |= (perm.equals(Manifest.permission.ACCESS_COARSE_LOCATION)); hasFinePermission |= (perm.equals(Manifest.permission.ACCESS_FINE_LOCATION)); if(hasCoarsePermission & hasFinePermission) break; } Log.v(TAG, "startModule(): hasCoarsePermission = " + hasCoarsePermission + "; hasFinePermission = " + hasFinePermission); if(hasCoarsePermission | hasFinePermission) { lowAccuracyCriteria = new Criteria(); lowAccuracyCriteria.setAccuracy(Criteria.ACCURACY_COARSE); lowAccuracyCriteria.setPowerRequirement(Criteria.POWER_LOW); if(hasFinePermission) { highAccuracyCriteria = new Criteria(); highAccuracyCriteria.setAccuracy(Criteria.ACCURACY_FINE); } Log.v(TAG, "startModule(): ret (success)"); return this; } Log.e(TAG, "startModule(): no permission"); } catch (NameNotFoundException e) { /* Internal error - should not happen */ Log.e(TAG, "Package manager exception: ", e); } Log.v(TAG, "startModule(): ret (failed)"); return null; } @Override public void stopModule() { Log.v(TAG, "stopModule(): ent"); PositionError error = new PositionError(); error.code = PositionError.POSITION_UNAVAILABLE; for(Request req : pendingRequests) { req.dispatch(error); } for(Watch req : watches.values()) { req.dispatch(error); } Log.v(TAG, "stopModule(): ret"); } /***************************** * Provider management *****************************/ private synchronized void resetProvider() { Log.v(TAG, "resetProvider(): ent"); if(watches.size() == 0) { locationManager.removeUpdates(this); currentWatchProvider = null; Log.v(TAG, "resetProvider(): ret (no watches)"); return; } Criteria criteria = (highAccuracyCount > 0 && highAccuracyCriteria != null) ? highAccuracyCriteria : lowAccuracyCriteria; String provider = locationManager.getBestProvider(criteria, true); if(provider == null) { currentWatchProvider = null; PositionError error = new PositionError(); error.code = PositionError.POSITION_UNAVAILABLE; for(Watch watch : watches.values()) { Log.e(TAG, "resetProvider(): cancelling watch id " + watch.id + " (no provider)"); watch.dispatch(error); watch.deschedule(); } Log.v(TAG, "resetProvider(): ret (no provider)"); return; } if(!provider.equals(currentWatchProvider)) { Log.v(TAG, "resetProvider(): changing provider to: " + provider); locationManager.removeUpdates(this); currentWatchProvider = provider; locationManager.requestLocationUpdates(provider, minTimeChange, minDistanceChange, this); } Log.v(TAG, "resetProvider(): ret"); } /***************************** * Watches *****************************/ class Request implements LocationListener, Runnable { protected PositionCallback successCallback; protected PositionErrorCallback errorCallback; protected boolean inError; public long timeout = Long.MAX_VALUE; public long maximumAge; public boolean enableHighAccuracy; private Request(PositionCallback successCallback, PositionErrorCallback errorCallback, PositionOptions options) { this.successCallback = successCallback; this.errorCallback = errorCallback; if(options != null && options.timeout != null) { timeout = options.timeout.longValue(); timeout = (timeout < 0) ? 0 : timeout; } if(options != null && options.maximumAge != null) { maximumAge = options.maximumAge.longValue(); maximumAge = (maximumAge < 0) ? 0 : maximumAge; } if (timeout == 0 && maximumAge == 0) { PositionError error = new PositionError(); error.code = PositionError.TIMEOUT; dispatch(error); return; } if(options != null && options.enableHighAccuracy != null) { enableHighAccuracy = options.enableHighAccuracy.booleanValue(); } } protected void startTimer() { if(timeout > 0) timerHandler.postDelayed(this, timeout); } protected void stopTimer() { timerHandler.removeCallbacks(this); } protected synchronized void dispatch(Position position) { stopTimer(); if(successCallback != null) { successCallback.handleEvent(position); } } protected synchronized void dispatch(PositionError error) { stopTimer(); if(errorCallback != null) { errorCallback.handleEvent(error); } inError = true; deschedule(); } protected void schedule() { startTimer(); pendingRequests.add(this); } protected void deschedule() { stopTimer(); pendingRequests.remove(this); } protected boolean tryLastKnownPosition(String provider) { if(provider == null) { /* no providers enabled; try all disabled ones */ for(String someProvider : locationManager.getProviders(false)) { if(tryLastKnownPosition(someProvider)) return true; } return false; } Location location = locationManager.getLastKnownLocation(provider); if(location != null) { long posTime = location.getTime(); long currTime = System.currentTimeMillis(); long age = currTime - posTime; if(age < maximumAge) { dispatch(toPosition(location)); return true; } } return false; } @Override public void onLocationChanged(Location location) { /* only call the callback if we haven't already * called the error callback */ if(!inError) { dispatch(toPosition(location)); deschedule(); } } @Override public void onProviderDisabled(String arg0) { PositionError error = new PositionError(); error.code = PositionError.POSITION_UNAVAILABLE; dispatch(error); deschedule(); } @Override public void onProviderEnabled(String arg0) {} @Override public void onStatusChanged(String arg0, int status, Bundle arg2) { if(status == LocationProvider.OUT_OF_SERVICE) { PositionError error = new PositionError(); error.code = PositionError.POSITION_UNAVAILABLE; dispatch(error); deschedule(); } } @Override public void run() { /* initial timeout expired */ synchronized(this) { PositionError error = new PositionError(); error.code = PositionError.TIMEOUT; dispatch(error); deschedule(); } } } class Watch extends Request { private long id; private String key; private Position lastUpdatedPosition; private Watch(PositionCallback successCallback, PositionErrorCallback errorCallback, PositionOptions options) { super(successCallback, errorCallback, options); if(!inError) { id = nextWatchId++; key = String.valueOf(id).intern(); synchronized(GeolocationImpl.this) { addWatch(this); if(!tryLastKnownPosition(currentWatchProvider)) { /* last known position didn't match the criteria; * but we can't just rely on getLocationUpdates() because * those updates are unlikely to fire, since the position * won't be that much different from a (stale) last known * position. So here we request a single update to get * the first result */ schedule(); locationManager.requestSingleUpdate(currentWatchProvider, this, androidContext.getMainLooper()); } } } } protected synchronized void dispatch(Position position) { if(lastUpdatedPosition == null || distance(lastUpdatedPosition.coords, position.coords) > minDistanceChange) { lastUpdatedPosition = position; super.dispatch(position); } } protected void deschedule() { stopTimer(); } } private synchronized void addWatch(Watch watch) { watches.put(watch.key, watch); boolean statusChange = (watches.size() == 1); if(watch.enableHighAccuracy) { statusChange |= (highAccuracyCount++ == 0); } if(statusChange) resetProvider(); } private synchronized void removeWatch(Watch watch) { watches.remove(watch.key); boolean statusChange = (watches.size() == 0); if(watch.enableHighAccuracy) { statusChange |= (--highAccuracyCount == 0); } if(statusChange) resetProvider(); } private synchronized Watch getWatch(long watchId) { return watches.get(String.valueOf(watchId).intern()); } /***************************** * Watch event listener *****************************/ @Override public void onLocationChanged(Location location) { Position position = toPosition(location); for(Watch watch : watches.values()) { watch.dispatch(position); } } @Override public void onProviderDisabled(String arg0) { resetProvider(); } @Override public void onProviderEnabled(String arg0) { resetProvider(); } @Override public void onStatusChanged(String arg0, int arg1, Bundle arg2) { resetProvider(); } /***************************** * Misc *****************************/ private static Position toPosition(Location location) { Position position = new Position(); position.coords = new Coordinates(); position.coords.accuracy = location.hasAccuracy() ? new Double(location.getAccuracy()) : null; position.coords.altitude = location.hasAltitude() ? new Double(location.getAltitude()) : null; position.coords.latitude = location.getLatitude(); position.coords.longitude = location.getLongitude(); position.coords.speed = location.hasSpeed() ? new Double(location.getSpeed()) : null; position.timestamp = location.getTime(); return position; } private double distance(Coordinates a, Coordinates b) { return Math.pow((a.latitude - b.latitude), 2) + Math.pow((a.longitude - b.longitude), 2); } }