package eu.faircode.finegeotag; import android.app.AlarmManager; import android.app.IntentService; import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.location.Address; import android.location.Geocoder; import android.location.Location; import android.location.LocationManager; import android.net.Uri; import android.os.Handler; import android.os.Looper; import android.os.SystemClock; import android.preference.PreferenceManager; import android.text.TextUtils; import android.util.Log; import android.view.Gravity; import android.view.LayoutInflater; import android.view.View; import android.widget.ImageView; import android.widget.TextView; import android.widget.Toast; import com.google.gson.Gson; import com.google.gson.GsonBuilder; import com.google.gson.JsonDeserializationContext; import com.google.gson.JsonDeserializer; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParseException; import com.google.gson.JsonSerializationContext; import com.google.gson.JsonSerializer; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.lang.reflect.Type; import java.nio.ByteBuffer; import java.nio.ByteOrder; import java.util.ArrayList; import java.util.List; public class LocationService extends IntentService { private static final String TAG = "FineGeotag.Service"; public static final String ACTION_LOCATION_FINE = "LocationFine"; public static final String ACTION_LOCATION_COARSE = "LocationCoarse"; public static final String ACTION_TIMEOUT = "TimeOut"; private static final int LOCATION_MIN_TIME = 1000; // milliseconds private static final int LOCATION_MIN_DISTANCE = 1; // meters private static final String PREFIX_LOCATION = "location_"; private static final String ACTION_GEOTAGGED = "eu.faircode.action.GEOTAGGED"; private static int mEGM96Pointer = -1; private static int mEGM96Offset; public LocationService() { super(TAG); } @Override protected void onHandleIntent(Intent intent) { Log.w(TAG, "Intent=" + intent); String image_filename = intent.getData().getPath(); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); if (ACTION_LOCATION_FINE.equals(intent.getAction()) || ACTION_LOCATION_COARSE.equals(intent.getAction())) { // Process location update Location location = (Location) intent.getExtras().get(LocationManager.KEY_LOCATION_CHANGED); Log.w(TAG, "Update location=" + location + " image=" + image_filename); if (location == null || (location.getLatitude() == 0.0 && location.getLongitude() == 0.0)) return; // Correct altitude if (LocationManager.GPS_PROVIDER.equals(location.getProvider())) try { double offset = getEGM96Offset(location, this); Log.w(TAG, "Offset=" + offset); location.setAltitude(location.getAltitude() - offset); Log.w(TAG, "Corrected location=" + location); } catch (IOException ex) { Log.w(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } // Get location preferences boolean pref_altitude = prefs.getBoolean(ActivitySettings.PREF_ALTITUDE, ActivitySettings.DEFAULT_ALTITUDE); float pref_accuracy = Float.parseFloat(prefs.getString(ActivitySettings.PREF_ACCURACY, ActivitySettings.DEFAULT_ACCURACY)); Log.w(TAG, "Prefer altitude=" + pref_altitude + " accuracy=" + pref_accuracy); // Persist better location Location bestLocation = LocationDeserializer.deserialize(prefs.getString(PREFIX_LOCATION + image_filename, null)); if (isBetterLocation(bestLocation, location)) { Log.w(TAG, "Better location=" + location + " image=" + image_filename); prefs.edit().putString(PREFIX_LOCATION + image_filename, LocationSerializer.serialize(location)).apply(); } // Check altitude if (!location.hasAltitude() && pref_altitude) { Log.w(TAG, "No altitude image=" + image_filename); return; } // Check accuracy if (!location.hasAccuracy() || location.getAccuracy() > pref_accuracy) { Log.w(TAG, "Inaccurate image=" + image_filename); return; } stopLocating(image_filename); // Process location handleLocation(image_filename, location); } else if (ACTION_TIMEOUT.equals(intent.getAction())) { // Process location time-out Log.w(TAG, "Timeout image=" + image_filename); // Process best location Location bestLocation = LocationDeserializer.deserialize(prefs.getString(PREFIX_LOCATION + image_filename, null)); if (bestLocation == null) { int known = Integer.parseInt(prefs.getString(ActivitySettings.PREF_KNOWN, ActivitySettings.DEFAULT_KNOWN)); LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE); for (String provider : lm.getProviders(false)) { Location lastKnownLocation = lm.getLastKnownLocation(provider); Log.w(TAG, "Last known location=" + lastKnownLocation + " provider=" + provider); if (lastKnownLocation != null && lastKnownLocation.getTime() > System.currentTimeMillis() - known * 60 * 1000 && isBetterLocation(bestLocation, lastKnownLocation)) bestLocation = lastKnownLocation; } } stopLocating(image_filename); Log.w(TAG, "Best location=" + bestLocation + " image=" + image_filename); if (bestLocation != null) handleLocation(image_filename, bestLocation); } } public static void startLocating(String image_filename, Context context) { LocationManager lm = (LocationManager) context.getSystemService(Context.LOCATION_SERVICE); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); // Request coarse location if (lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER)) { Intent locationIntent = new Intent(context, LocationService.class); locationIntent.setAction(LocationService.ACTION_LOCATION_COARSE); locationIntent.setData(Uri.fromFile(new File(image_filename))); PendingIntent pi = PendingIntent.getService(context, 0, locationIntent, PendingIntent.FLAG_UPDATE_CURRENT); lm.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, LOCATION_MIN_TIME, LOCATION_MIN_DISTANCE, pi); Log.w(TAG, "Requested network locations image=" + image_filename); } // Request fine location if (lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) { Intent locationIntent = new Intent(context, LocationService.class); locationIntent.setAction(LocationService.ACTION_LOCATION_FINE); locationIntent.setData(Uri.fromFile(new File(image_filename))); PendingIntent pi = PendingIntent.getService(context, 0, locationIntent, PendingIntent.FLAG_UPDATE_CURRENT); lm.requestLocationUpdates(LocationManager.GPS_PROVIDER, LOCATION_MIN_TIME, LOCATION_MIN_DISTANCE, pi); Log.w(TAG, "Requested GPS locations image=" + image_filename); } // Set location timeout int timeout = Integer.parseInt(prefs.getString(ActivitySettings.PREF_TIMEOUT, ActivitySettings.DEFAULT_TIMEOUT)); if (!lm.isProviderEnabled(LocationManager.NETWORK_PROVIDER) && !lm.isProviderEnabled(LocationManager.GPS_PROVIDER)) timeout = 1; Intent alarmIntent = new Intent(context, LocationService.class); alarmIntent.setAction(LocationService.ACTION_TIMEOUT); alarmIntent.setData(Uri.fromFile(new File(image_filename))); PendingIntent pia = PendingIntent.getService(context, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); am.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, SystemClock.elapsedRealtime() + timeout * 1000, pia); Log.w(TAG, "Set timeout=" + timeout + "s image=" + image_filename); } private static double getEGM96Offset(Location location, Context context) throws IOException { InputStream is = null; try { double lat = location.getLatitude(); double lon = location.getLongitude(); int y = (int) Math.floor((90 - lat) * 4); int x = (int) Math.floor((lon >= 0 ? lon : lon + 360) * 4); int p = ((y * 1440) + x) * 2; int o; if (mEGM96Pointer >= 0 && p == mEGM96Pointer) o = mEGM96Offset; else { is = context.getAssets().open("WW15MGH.DAC"); is.skip(p); ByteBuffer bb = ByteBuffer.allocate(2); bb.order(ByteOrder.BIG_ENDIAN); bb.put((byte) is.read()); bb.put((byte) is.read()); o = bb.getShort(0); mEGM96Pointer = p; mEGM96Offset = o; } return o / 100d; } finally { if (is != null) { try { is.close(); } catch (IOException ignored) { } } } } private boolean isBetterLocation(Location prev, Location current) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); boolean pref_altitude = prefs.getBoolean(ActivitySettings.PREF_ALTITUDE, ActivitySettings.DEFAULT_ALTITUDE); return (prev == null || ((!pref_altitude || !prev.hasAltitude() || current.hasAltitude()) && (current.hasAccuracy() ? current.getAccuracy() : Float.MAX_VALUE) < (prev.hasAccuracy() ? prev.getAccuracy() : Float.MAX_VALUE))); } private void handleLocation(String image_filename, Location location) { try { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); // Check if image still exists if (!new File(image_filename).exists()) { Log.w(TAG, "File deleted image=" + image_filename); return; } // Write Exif ExifInterfaceEx exif = new ExifInterfaceEx(image_filename); exif.setLocation(location); exif.saveAttributes(); Log.w(TAG, "Exif updated location=" + location + " image=" + image_filename); // Reverse geocode if (prefs.getBoolean(ActivitySettings.PREF_TOAST, ActivitySettings.DEFAULT_TOAST)) { String address = TextUtils.join("\n", reverseGeocode(location, this)); Log.w(TAG, "Address=" + address + " image=" + image_filename); address = getString(R.string.msg_geotagged) + (address == null ? "" : "\n" + address); notify(image_filename, address); } // Broadcast geotagged intent Intent intent = new Intent(ACTION_GEOTAGGED); intent.setData(Uri.fromFile(new File(image_filename))); intent.putExtra(LocationManager.KEY_LOCATION_CHANGED, location); Log.w(TAG, "Broadcasting " + intent); sendBroadcast(intent); } catch (IOException ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } catch (Throwable ex) { Log.e(TAG, ex.toString() + "\n" + Log.getStackTraceString(ex)); } } private void stopLocating(String image_filename) { SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(this); LocationManager lm = (LocationManager) getSystemService(Context.LOCATION_SERVICE); // Cancel coarse location updates { Intent locationIntent = new Intent(this, LocationService.class); locationIntent.setAction(LocationService.ACTION_LOCATION_COARSE); locationIntent.setData(Uri.fromFile(new File(image_filename))); PendingIntent pi = PendingIntent.getService(this, 0, locationIntent, PendingIntent.FLAG_UPDATE_CURRENT); lm.removeUpdates(pi); } // Cancel fine location updates { Intent locationIntent = new Intent(this, LocationService.class); locationIntent.setAction(LocationService.ACTION_LOCATION_FINE); locationIntent.setData(Uri.fromFile(new File(image_filename))); PendingIntent pi = PendingIntent.getService(this, 0, locationIntent, PendingIntent.FLAG_UPDATE_CURRENT); lm.removeUpdates(pi); } // Cancel alarm { Intent alarmIntent = new Intent(this, LocationService.class); alarmIntent.setAction(LocationService.ACTION_TIMEOUT); alarmIntent.setData(Uri.fromFile(new File(image_filename))); PendingIntent pi = PendingIntent.getService(this, 0, alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT); AlarmManager am = (AlarmManager) getSystemService(Context.ALARM_SERVICE); am.cancel(pi); } prefs.edit().remove(PREFIX_LOCATION + image_filename).apply(); } private static List<String> reverseGeocode(Location location, Context context) { List<String> listLine = new ArrayList<>(); if (location != null && Geocoder.isPresent()) try { Geocoder geocoder = new Geocoder(context); List<Address> listPlace = geocoder.getFromLocation(location.getLatitude(), location.getLongitude(), 1); if (listPlace != null && listPlace.size() > 0) { for (int l = 0; l < listPlace.get(0).getMaxAddressLineIndex(); l++) listLine.add(listPlace.get(0).getAddressLine(l)); } } catch (IOException ignored) { } return listLine; } private void notify(final String image_filename, final String text) { new Handler(Looper.getMainLooper()).post(new Runnable() { @Override public void run() { LayoutInflater inflater = LayoutInflater.from(LocationService.this); View layout = inflater.inflate(R.layout.geotagged, null); ImageView iv = (ImageView) layout.findViewById(R.id.image); iv.setImageURI(Uri.fromFile(new File(image_filename))); TextView tv = (TextView) layout.findViewById(R.id.text); tv.setText(text); Toast toast = new Toast(getApplicationContext()); toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0); toast.setDuration(Toast.LENGTH_LONG); toast.setView(layout); toast.show(); } }); } // Serialization private static class LocationSerializer implements JsonSerializer<Location> { public JsonElement serialize(Location src, Type typeOfSrc, JsonSerializationContext context) { JsonObject jObject = new JsonObject(); jObject.addProperty("Provider", src.getProvider()); jObject.addProperty("Time", src.getTime()); jObject.addProperty("Latitude", src.getLatitude()); jObject.addProperty("Longitude", src.getLongitude()); if (src.hasAltitude()) jObject.addProperty("Altitude", src.getAltitude()); if (src.hasSpeed()) jObject.addProperty("Speed", src.getSpeed()); if (src.hasAccuracy()) jObject.addProperty("Accuracy", src.getAccuracy()); if (src.hasBearing()) jObject.addProperty("Bearing", src.getBearing()); return jObject; } public static String serialize(Location location) { GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(Location.class, new LocationSerializer()); Gson gson = builder.create(); String json = gson.toJson(location); return json; } } private static class LocationDeserializer implements JsonDeserializer<Location> { public Location deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { JsonObject jObject = (JsonObject) json; Location location = new Location(jObject.get("Provider").getAsString()); location.setTime(jObject.get("Time").getAsLong()); location.setLatitude(jObject.get("Latitude").getAsDouble()); location.setLongitude(jObject.get("Longitude").getAsDouble()); if (jObject.has("Altitude")) location.setAltitude(jObject.get("Altitude").getAsDouble()); if (jObject.has("Speed")) location.setSpeed(jObject.get("Speed").getAsFloat()); if (jObject.has("Bearing")) location.setBearing(jObject.get("Bearing").getAsFloat()); if (jObject.has("Accuracy")) location.setAccuracy(jObject.get("Accuracy").getAsFloat()); return location; } public static Location deserialize(String json) { GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(Location.class, new LocationDeserializer()); Gson gson = builder.create(); Location location = gson.fromJson(json, Location.class); return location; } } }