/** * */ package com.geeksville.location; import java.io.*; import java.lang.reflect.Method; import java.net.MalformedURLException; import java.net.URL; import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.HashMap; import java.util.HashSet; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import android.app.Notification; import android.app.PendingIntent; import android.app.Service; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.location.Location; import android.location.LocationListener; import android.location.LocationManager; import android.os.AsyncTask; import android.os.Binder; import android.os.Bundle; import android.os.HandlerThread; import android.os.IBinder; import android.preference.PreferenceManager; import android.util.Log; import android.util.Pair; import android.widget.Toast; import com.flurry.android.FlurryAgent; import com.geeksville.gaggle.AudioVario; import com.geeksville.gaggle.R; import com.geeksville.gaggle.TopActivity; import com.geeksville.location.IBarometerClient.Calibration; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * Log locations to a file, db or other. * * @author kevinh * * Effectively the 'glue' between generic Java PositionWriter and the * android APIs */ public class GPSClient extends Service implements IGPSClient { /** * Debugging tag */ private static final String TAG = "GPSClient"; private MyBinder binder = new MyBinder(); private HandlerThread thread = new HandlerThread("GPSClient"); private LocationManager manager; private class UpdateFreq { public int minDist; public long minTime; long lastUpdateTime = 0; /** * * @param time * @param dist */ public UpdateFreq(long time, int dist) { minDist = dist; minTime = time; } } public static GPSClient instance; private HashMap<LocationListener, UpdateFreq> listeners = new HashMap<LocationListener, UpdateFreq>(); private long minTimePerUpdate = Long.MAX_VALUE; private float minDistPerUpdate = Float.MAX_VALUE; /** * In developer mode, this provides our data */ private TestGPSDriver simData = null; private int currentStatus = OUT_OF_SERVICE; // / Once we get a GPS altitude we will fixup the barometer private boolean hasSetBarometer = false; private IBarometerClient baro = null; private AudioVario vario; /** * Older SDKs don't define LocationProvider.AVAILABLE etc... */ private static final int AVAILABLE = 2, OUT_OF_SERVICE = 0, TEMPORARILY_UNAVAILABLE = 1; private static Map<Context, Republisher> clients = new HashMap<Context, Republisher>(); public GPSClient() { try { vario = new AudioVario(); } catch (VerifyError ex) { Log.e(TAG, "Not supported on 1.5: " + ex); } } /** * Glue to bind to this service * * @param context * the client's context * @param client * who to notify */ public static void bindTo(Context context, ServiceConnection client) { // FIXME - only ONE ServiceConnection client, the most recent one gets // to find out about changes. keep a list of my own? Or perhaps I should // rely more on the automatic relationship // between the Context's lifecycle and the service life cycle. For right // now I'll use the list of my own // FIXME - always use the application context, because that's what // really gets tracked anyway (from looking at // android source) context = context.getApplicationContext(); Republisher conns = clients.get(context); if (conns == null) { conns = new Republisher(); conns.add(client); clients.put(context, conns); // We've never heard from this context before context.bindService(new Intent(context, GPSClient.class), conns, Service.BIND_AUTO_CREATE | Service.BIND_DEBUG_UNBIND); } else conns.add(client); } /** * Unregister this client * * @param client */ public static void unbindFrom(Context context, ServiceConnection client) { // FIXME - always use the application context, because that's what // really gets tracked anyway (from looking at // android source) context = context.getApplicationContext(); Republisher conns = clients.get(context); if (conns == null) { Log.e(TAG, "missing context GPSClient.unbindFrom"); // FIXME - // figure out // why this // occurs // sometimes return; } // Done with this context? if (!conns.remove(client)) { clients.remove(context); context.unbindService(conns); } } private static class Republisher implements ServiceConnection { Set<ServiceConnection> clients = new HashSet<ServiceConnection>(); ComponentName name; IBinder service; public synchronized void add(ServiceConnection conn) { if (clients.contains(conn)) { Log.e(TAG, "redundant GPSClient.bindTo"); // FIXME - why does // this happen? return; } clients.add(conn); // If we are already connected, tell the new client now if (service != null) conn.onServiceConnected(name, service); } public synchronized boolean remove(ServiceConnection conn) { if (!clients.remove(conn)) Log.e(TAG, "missing client GPSClient.unbindFrom"); return clients.size() != 0; } @Override public synchronized void onServiceConnected(ComponentName name, IBinder service) { this.name = name; this.service = service; for (ServiceConnection c : clients) c.onServiceConnected(name, service); } @Override public synchronized void onServiceDisconnected(ComponentName name) { this.service = null; for (ServiceConnection c : clients) c.onServiceDisconnected(name); } } private class MyBinder extends Binder implements IGPSClient { @Override public void addLocationListener(long minTimeMs, float minDistMeters, LocationListener l) { GPSClient.this.addLocationListener(minTimeMs, minDistMeters, l); } @Override public Location getLastKnownLocation() { return GPSClient.this.getLastKnownLocation(); } @Override public void removeLocationListener(LocationListener l) { GPSClient.this.removeLocationListener(l); } @Override public void startForeground(String tickerMsg, String notificationText) { GPSClient.this.startForeground(tickerMsg, notificationText); } @Override public void stopForeground() { GPSClient.this.stopForeground(); } } /* * (non-Javadoc) * * @see android.app.Service#onCreate() */ @Override public void onCreate() { super.onCreate(); FlurryAgent.onStartSession(this, "XBPNNCR4T72PEBX17GKF"); instance = this; manager = (LocationManager) getSystemService(Context.LOCATION_SERVICE); // Start our looper up thread.start(); try { baro = BarometerClient.create(GPSClient.this); if (vario != null) vario.onCreate(this, thread.getLooper()); } catch (VerifyError ex) { Log.e(TAG, "Not on 1.5: " + ex); } setSimByPrefs(); // If the user changes prefs entry switch back to real hw as needed SharedPreferences.OnSharedPreferenceChangeListener listener = new SharedPreferences.OnSharedPreferenceChangeListener() { @Override public void onSharedPreferenceChanged( SharedPreferences sharedPreferences, String key) { setSimByPrefs(); } }; PreferenceManager.getDefaultSharedPreferences(this) .registerOnSharedPreferenceChangeListener(listener); } private void setSimByPrefs() { // only use sim in dev mode // if (AndroidUtil.isEmulator()) boolean cursim = simData != null; boolean newsim = PreferenceManager.getDefaultSharedPreferences(this) .getBoolean("fake_gps_pref", false); if (newsim != cursim) { if (newsim) { try { simData = new TestGPSDriver(this); } catch (RuntimeException ex) { // Tell the user what is going on Toast t = Toast.makeText(this, R.string.mock_location_required, Toast.LENGTH_LONG); t.show(); Editor e = PreferenceManager.getDefaultSharedPreferences(this).edit(); e.putBoolean("fake_gps_pref", false); e.commit(); } } else { simData.close(); simData = null; } } } /* * (non-Javadoc) * * @see android.app.Service#onDestroy() */ @Override public void onDestroy() { if (vario != null) vario.onDestroy(); // Forcibly unsubscribe anyone still using us synchronized (listeners) { if (listeners.size() != 0) { LocationListener[] tokill = listeners.keySet().toArray(null); for (LocationListener l : tokill) removeLocationListener(l); listeners.clear(); } } thread.getLooper().quit(); FlurryAgent.onEndSession(this); instance = null; super.onDestroy(); } @Override public IBinder onBind(Intent intent) { return binder; } private static final int NOTIFICATION_ID = 1; /** * Tell the OS and user we are now doing an important background operation * * @param tickerMsg * @param notificationText */ public void startForeground(String tickerMsg, String notificationText) { int icon = android.R.drawable.stat_sys_download; long when = System.currentTimeMillis(); Notification notification = new Notification(icon, tickerMsg, when); // FIXME, use real Context context = getApplicationContext(); CharSequence contentTitle = "Gaggle"; CharSequence contentText = notificationText; Intent notificationIntent = new Intent(this, TopActivity.class); PendingIntent contentIntent = PendingIntent.getActivity(this, 0, notificationIntent, 0); notification.setLatestEventInfo(context, contentTitle, contentText, contentIntent); startForegroundGlue(NOTIFICATION_ID, notification); } /** * Tell user are no longer running a critical foreground service */ public void stopForeground() { stopForegroundGlue(true); } /** * This method doesn't exist on android 1.6, so we use reflection to fallback * intelligently * * @param id * @param notification */ private void startForegroundGlue(int id, Notification notification) { try { Method m = Service.class.getMethod("startForeground", new Class[] { Integer.TYPE, Notification.class }); m.invoke(this, id, notification); } catch (NoSuchMethodException ex) { // Fall back to the old API // setForeground(true); try { Method m = Service.class.getMethod("setForeground", new Class[] { Boolean.TYPE }); m.invoke(this, true); } catch (Exception e) { throw new RuntimeException(e); } } catch (Exception e) { throw new RuntimeException(e); } } /** * This method doesn't exist on android 1.6, so we use reflection to fallback * intelligently * */ private void stopForegroundGlue(boolean removeNotification) { try { Method m = Service.class.getMethod("stopForeground", new Class[] { Boolean.TYPE, }); m.invoke(this, removeNotification); } catch (NoSuchMethodException ex) { // Fall back to the old API // setForeground(false); try { Method m = Service.class.getMethod("setForeground", new Class[] { Boolean.TYPE }); m.invoke(this, false); } catch (Exception e) { throw new RuntimeException(e); } } catch (Exception e) { throw new RuntimeException(e); } } /** * Used to request location/Status updates (once we have someone asking for * updates we will turn GPS on) * * @param l * the callback will be called from the background GPS thread * @param minTimeMs * we prefer to see updates at least this often * @param minDistMeters * if dist changes more than this wed like to find out * */ public void addLocationListener(long minTimeMs, float minDistMeters, LocationListener l) { // Log.d(TAG, "Add listener: " + l.toString()); int oldcount; synchronized (listeners) { oldcount = listeners.size(); listeners.put(l, new UpdateFreq(minTimeMs, (int) minDistMeters)); } // FIXME, we are only shrinking the range of acceptable times - someday // we should // loosen things up boolean needUpdate = false; if (minTimeMs < minTimePerUpdate) { minTimePerUpdate = minTimeMs; needUpdate = true; } if (minDistMeters < minDistPerUpdate) { minDistPerUpdate = minDistMeters; needUpdate = true; } if (oldcount == 0 || needUpdate) { // We just added the first element - // subscribe to the OS String provider = (simData != null) ? simData.getProvider() : LocationManager.GPS_PROVIDER; manager.requestLocationUpdates(provider, minTimeMs, minDistMeters, listener, thread.getLooper()); } // Provide an initial location if we know where we are if (currentStatus == AVAILABLE) { Location loc = getLastKnownLocation(); if (loc != null) l.onLocationChanged(loc); } } /** * We are no long interested in updates * * @param l */ public void removeLocationListener(LocationListener l) { // Log.d(TAG, "Remove listener: " + l.toString()); int newcount; synchronized (listeners) { listeners.remove(l); newcount = listeners.size(); } if (newcount == 0) { manager.removeUpdates(listener); } } public Location getLastKnownLocation() { String provider = (simData != null) ? simData.getProvider() : LocationManager.GPS_PROVIDER; return manager.getLastKnownLocation(provider); } private class UpdateLocationAltitudeTask extends AsyncTask<Location, Integer, Pair<Boolean, Location>> { protected Pair<Boolean, Location> doInBackground(Location... loc) { Location location = loc[0]; String result = null; Boolean success = false; try { URL url = new URL( "http://maps.googleapis.com/maps/api/elevation/" + "json?locations=" + String.valueOf(location.getLatitude()) + "," + String.valueOf(location.getLongitude()) + "&sensor=true"); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); InputStream in = new BufferedInputStream(connection.getInputStream()); BufferedReader reader = new BufferedReader(new InputStreamReader(in), 512); StringBuilder builder = new StringBuilder(); String line = null; while((line = reader.readLine()) != null) { builder.append(line); } result = builder.toString(); in.close(); } catch (MalformedURLException e) { } catch (IOException e) { } if(result != null) { try { JSONObject obj = new JSONObject(result); String status = obj.getString("status"); if(status.equals("OK")) { JSONArray results = obj.getJSONArray("results"); if(results.length() > 0) { JSONObject values = results.getJSONObject(0); double altitude = values.getDouble("elevation"); location.setAltitude(altitude); success = true; } } } catch (JSONException e) { } } return Pair.create(success, location); } protected void onPostExecute(Pair<Boolean, Location> result) { if(result.first) { Location lastLocation = new Location(getLastKnownLocation()); Location correctedLocation = result.second; //10 meters plus some accuracy tolerance double LocationTolerance = 10.0f + Math.max(lastLocation.getAccuracy(), correctedLocation.getAccuracy()); double distance = LocationUtils.LatLongToMeter( lastLocation.getLatitude(), lastLocation.getLongitude(), correctedLocation.getLatitude(), correctedLocation.getLongitude()); //Just make sure we haven't magically teleported somewhere else if(distance < LocationTolerance) { //Push in the final corrected altitude baro.setAltitude((float) correctedLocation.getAltitude(), Calibration.GOOGLE); Log.d(TAG, "Updated with correcte altitude from Google: " + correctedLocation.getAltitude()); } } } }; public void updateReferenceLocation(Location location) { baro.setAltitude((float) location.getAltitude(), Calibration.GPS); // Try to get an even more accurate altitude by asking the almighty internet new UpdateLocationAltitudeTask().execute(location); } /** * We just rebroadcast our loc updates - but from inside our shared handler * thread * */ private LocationListener listener = new LocationListener() { // Used to avoid holding the lock while running (slow) handlers private ArrayList<LocationListener> lcopy = new ArrayList<LocationListener>(); /** * Should we send an update to this listener? */ private boolean isUpdate(Entry<LocationListener, UpdateFreq> e, Location loc) { long now = loc.getTime(); UpdateFreq freq = e.getValue(); // We don't want to deliver updates too often boolean doUpdate = (now - freq.lastUpdateTime >= freq.minTime); // FIXME - check distance if (doUpdate) freq.lastUpdateTime = now; return doUpdate; } @Override public synchronized void onLocationChanged(Location location) { // If we receive a position update, assume the GPS is working currentStatus = AVAILABLE; if (baro != null) { if (!hasSetBarometer && location.hasAltitude()) { hasSetBarometer = true; updateReferenceLocation(location); } // Before forwarding the location to others, substitude the // (better) baro based altitude baro.improveLocation(location); } // Used to avoid holding the lock while running (slow) handlers synchronized (listeners) { // lcopy = listeners.keySet().toArray(lcopy); lcopy.clear(); for (Entry<LocationListener, UpdateFreq> l : listeners.entrySet()) if (isUpdate(l, location)) lcopy.add(l.getKey()); } for (LocationListener listen : lcopy) listen.onLocationChanged(location); } @Override public void onProviderDisabled(String provider) { synchronized (listeners) { for (LocationListener l : listeners.keySet()) l.onProviderDisabled(provider); } } @Override public void onProviderEnabled(String provider) { synchronized (listeners) { for (LocationListener l : listeners.keySet()) l.onProviderEnabled(provider); } } @Override public void onStatusChanged(String provider, int status, Bundle extras) { currentStatus = status; synchronized (listeners) { for (LocationListener l : listeners.keySet()) l.onStatusChanged(provider, status, extras); } } }; }