Merge branch 'mapping' of https://github.com/SamWhited/Conversations into SamWhited-mapping

Daniel Gultsch created

Change summary

art/marker.svg                                                     | 110 
art/render.rb                                                      |   1 
build.gradle                                                       |   1 
src/main/AndroidManifest.xml                                       |  29 
src/main/java/eu/siacs/conversations/Config.java                   |  16 
src/main/java/eu/siacs/conversations/ui/ActionBarActivity.java     |  13 
src/main/java/eu/siacs/conversations/ui/LocationActivity.java      | 314 
src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java | 248 
src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java  | 234 
src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java    |   2 
src/main/java/eu/siacs/conversations/ui/XmppActivity.java          |  15 
src/main/java/eu/siacs/conversations/ui/util/LocationHelper.java   |  72 
src/main/java/eu/siacs/conversations/ui/util/UriHelper.java        |  30 
src/main/java/eu/siacs/conversations/ui/widget/Marker.java         |  52 
src/main/java/eu/siacs/conversations/ui/widget/MyLocation.java     |  65 
src/main/res/drawable-hdpi/ic_directions_black_24dp.png            |   0 
src/main/res/drawable-hdpi/ic_directions_white_24dp.png            |   0 
src/main/res/drawable-hdpi/ic_gps_fixed_black_24dp.png             |   0 
src/main/res/drawable-hdpi/ic_gps_fixed_white_24dp.png             |   0 
src/main/res/drawable-hdpi/ic_gps_not_fixed_black_24dp.png         |   0 
src/main/res/drawable-hdpi/ic_gps_not_fixed_white_24dp.png         |   0 
src/main/res/drawable-hdpi/marker.png                              |   0 
src/main/res/drawable-mdpi/ic_directions_black_24dp.png            |   0 
src/main/res/drawable-mdpi/ic_directions_white_24dp.png            |   0 
src/main/res/drawable-mdpi/ic_gps_fixed_black_24dp.png             |   0 
src/main/res/drawable-mdpi/ic_gps_fixed_white_24dp.png             |   0 
src/main/res/drawable-mdpi/ic_gps_not_fixed_black_24dp.png         |   0 
src/main/res/drawable-mdpi/ic_gps_not_fixed_white_24dp.png         |   0 
src/main/res/drawable-mdpi/marker.png                              |   0 
src/main/res/drawable-xhdpi/ic_directions_black_24dp.png           |   0 
src/main/res/drawable-xhdpi/ic_directions_white_24dp.png           |   0 
src/main/res/drawable-xhdpi/ic_gps_fixed_black_24dp.png            |   0 
src/main/res/drawable-xhdpi/ic_gps_fixed_white_24dp.png            |   0 
src/main/res/drawable-xhdpi/ic_gps_not_fixed_black_24dp.png        |   0 
src/main/res/drawable-xhdpi/ic_gps_not_fixed_white_24dp.png        |   0 
src/main/res/drawable-xhdpi/marker.png                             |   0 
src/main/res/drawable-xxhdpi/ic_directions_black_24dp.png          |   0 
src/main/res/drawable-xxhdpi/ic_directions_white_24dp.png          |   0 
src/main/res/drawable-xxhdpi/ic_gps_fixed_black_24dp.png           |   0 
src/main/res/drawable-xxhdpi/ic_gps_fixed_white_24dp.png           |   0 
src/main/res/drawable-xxhdpi/ic_gps_not_fixed_black_24dp.png       |   0 
src/main/res/drawable-xxhdpi/ic_gps_not_fixed_white_24dp.png       |   0 
src/main/res/drawable-xxhdpi/marker.png                            |   0 
src/main/res/drawable-xxxhdpi/ic_directions_black_24dp.png         |   0 
src/main/res/drawable-xxxhdpi/ic_directions_white_24dp.png         |   0 
src/main/res/drawable-xxxhdpi/ic_gps_fixed_black_24dp.png          |   0 
src/main/res/drawable-xxxhdpi/ic_gps_fixed_white_24dp.png          |   0 
src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_black_24dp.png      |   0 
src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_white_24dp.png      |   0 
src/main/res/drawable-xxxhdpi/marker.png                           |   0 
src/main/res/drawable/ic_directions_black_24dp.xml                 |   9 
src/main/res/drawable/ic_gps_fixed_black_24dp.xml                  |   9 
src/main/res/drawable/ic_gps_not_fixed_black_24dp.xml              |   9 
src/main/res/drawable/ic_place_black_24dp.xml                      |   9 
src/main/res/layout/activity_share_location.xml                    |  66 
src/main/res/layout/activity_show_location.xml                     |  25 
src/main/res/menu/menu_show_location.xml                           |  14 
src/main/res/values/about.xml                                      |   3 
src/main/res/values/attrs.xml                                      |   5 
src/main/res/values/strings.xml                                    |   8 
src/main/res/values/themes.xml                                     |  10 
61 files changed, 1,352 insertions(+), 17 deletions(-)

Detailed changes

art/marker.svg 🔗

@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8" standalone="no"?>
+<svg
+   xmlns:dc="http://purl.org/dc/elements/1.1/"
+   xmlns:cc="http://creativecommons.org/ns#"
+   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+   xmlns:svg="http://www.w3.org/2000/svg"
+   xmlns="http://www.w3.org/2000/svg"
+   xmlns:xlink="http://www.w3.org/1999/xlink"
+   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
+   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
+   width="48"
+   height="48"
+   viewBox="0 0 48 48"
+   id="svg2"
+   version="1.1"
+   inkscape:version="0.91 r13725"
+   sodipodi:docname="marker.svg">
+  <metadata
+     id="metadata10">
+    <rdf:RDF>
+      <cc:Work
+         rdf:about="">
+        <dc:format>image/svg+xml</dc:format>
+        <dc:type
+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
+        <dc:title />
+      </cc:Work>
+    </rdf:RDF>
+  </metadata>
+  <defs
+     id="defs8">
+    <radialGradient
+       inkscape:collect="always"
+       xlink:href="#linearGradient3913"
+       id="radialGradient3883"
+       gradientUnits="userSpaceOnUse"
+       gradientTransform="matrix(0.2039074,-0.09024614,0.07170697,0.16216229,-92.579229,-90.973095)"
+       cx="262.33273"
+       cy="945.23846"
+       fx="262.33273"
+       fy="945.23846"
+       r="185.49754" />
+    <linearGradient
+       inkscape:collect="always"
+       id="linearGradient3913">
+      <stop
+         style="stop-color:#ffffff;stop-opacity:1;"
+         offset="0"
+         id="stop3915" />
+      <stop
+         style="stop-color:#ffffff;stop-opacity:0;"
+         offset="1"
+         id="stop3917" />
+    </linearGradient>
+    <clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath4167">
+      <path
+         inkscape:connector-curvature="0"
+         d="M 24,4.0000001 C 16.27,4.0000001 10,10.27 10,18 10,28.5 24,44 24,44 24,44 38,28.5 38,18 38,10.27 31.73,4.0000001 24,4.0000001 Z M 24,23 c -2.76,0 -5,-2.24 -5,-5 0,-2.76 2.24,-5 5,-5 2.76,0 5,2.24 5,5 0,2.76 -2.24,5 -5,5 z"
+         id="path4169"
+         style="fill:#000000;fill-opacity:1" />
+    </clipPath>
+    <clipPath
+       clipPathUnits="userSpaceOnUse"
+       id="clipPath4321">
+      <path
+         inkscape:connector-curvature="0"
+         d="m 24,4.0001492 c -7.73,0 -14,6.2699998 -14,14.0000008 0,10.5 14,26 14,26 0,0 14,-15.5 14,-26 C 38,10.270149 31.73,4.0001492 24,4.0001492 Z M 24,23.00015 c -2.76,0 -5,-2.24 -5,-5 0,-2.760001 2.24,-5.000001 5,-5.000001 2.76,0 5,2.24 5,5.000001 0,2.76 -2.24,5 -5,5 z"
+         id="path4323"
+         style="fill:#000000;fill-opacity:1;stroke:#ffffff;stroke-width:3;stroke-miterlimit:4;stroke-dasharray:18, 3;stroke-dashoffset:0;stroke-opacity:0.53333285" />
+    </clipPath>
+  </defs>
+  <sodipodi:namedview
+     pagecolor="#ffffff"
+     bordercolor="#666666"
+     borderopacity="1"
+     objecttolerance="10"
+     gridtolerance="10"
+     guidetolerance="10"
+     inkscape:pageopacity="0"
+     inkscape:pageshadow="2"
+     inkscape:window-width="1920"
+     inkscape:window-height="1010"
+     id="namedview6"
+     showgrid="false"
+     inkscape:zoom="4.9166667"
+     inkscape:cx="-15.254237"
+     inkscape:cy="12.20339"
+     inkscape:window-x="0"
+     inkscape:window-y="41"
+     inkscape:window-maximized="1"
+     inkscape:current-layer="svg2" />
+  <path
+     d="M24 4c-7.73 0-14 6.27-14 14 0 10.5 14 26 14 26s14-15.5 14-26c0-7.73-6.27-14-14-14zm0 19c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5z"
+     id="path4"
+     style="fill:#00a000;fill-opacity:1;stroke:none;stroke-opacity:0.53333336;stroke-width:1.70000002;stroke-miterlimit:4;stroke-dasharray:none" />
+  <path
+     style="display:inline;opacity:0.19211821;fill:url(#radialGradient3883);fill-opacity:1;stroke:none"
+     d="m 53.884912,1.7373006 c -18.322492,0 -33.173092,14.5823714 -33.173092,32.5686504 0,3.794038 0.661899,7.436601 1.877335,10.821463 1.505391,0.209531 3.044508,0.317391 4.607513,0.317391 5.584539,0 9.890238,-1.147853 14.805425,-2.934259 l 15.611481,6.295152 a 2.0568126,2.0577227 0 0 0 2.766588,-2.403594 l -4.227888,-17.09591 c 2.717518,-4.771967 3.645449,-10.205846 3.645449,-15.810885 0,-4.0761111 -0.781533,-7.9714274 -2.20495,-11.5551094 -1.217366,-0.132888 -2.454715,-0.202899 -3.707861,-0.202899 z"
+     id="path3878"
+     inkscape:connector-curvature="0"
+     clip-path="url(#clipPath4167)" />
+  <path
+     inkscape:connector-curvature="0"
+     d="M 24,4.0000003 C 16.27,4.0000003 10,10.27 10,18 10,28.5 24,44 24,44 24,44 38,28.5 38,18 38,10.27 31.73,4.0000003 24,4.0000003 Z M 24,23 c -2.76,0 -5,-2.24 -5,-5 0,-2.76 2.24,-5 5,-5 2.76,0 5,2.24 5,5 0,2.76 -2.24,5 -5,5 z"
+     id="path4-3"
+     style="fill:none;fill-opacity:1;stroke:#ffffff;stroke-width:5;stroke-miterlimit:4;stroke-dasharray:30,5;stroke-opacity:0.53333336;stroke-dashoffset:44"
+     clip-path="url(#clipPath4321)" />
+</svg>

art/render.rb 🔗

@@ -68,6 +68,7 @@ images = {
 	'message_bubble_sent_grey.svg' => ['message_bubble_sent_grey.9', 0],
 	'date_bubble_white.svg' => ['date_bubble_white.9', 0],
 	'date_bubble_grey.svg' => ['date_bubble_grey.9', 0],
+	'marker.svg' => ['marker', 0]
 	}
 
 # Executable paths for Mac OSX

build.gradle 🔗

@@ -52,6 +52,7 @@ dependencies {
     implementation "com.wefika:flowlayout:0.4.1"
     implementation 'net.ypresto.androidtranscoder:android-transcoder:0.2.0'
     implementation 'rocks.xmpp:xmpp-addr:0.8.0-SNAPSHOT'
+    implementation 'org.osmdroid:osmdroid-android:6.0.1'
 }
 
 ext {

src/main/AndroidManifest.xml 🔗

@@ -13,6 +13,13 @@
     <uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
     <uses-permission android:name="android.permission.VIBRATE" />
     <uses-permission android:name="android.permission.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS" />
+    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
+    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
+
+    <uses-feature android:name="android.hardware.location" android:required="false" />
+    <uses-feature android:name="android.hardware.location.gps" android:required="false" />
+    <uses-feature android:name="android.hardware.location.network" android:required="false" />
 
     <uses-permission android:name="android.permission.CAMERA" />
 
@@ -49,7 +56,27 @@
                 <action android:name="android.media.RINGER_MODE_CHANGED" />
             </intent-filter>
         </receiver>
-
+        <activity
+            android:name=".ui.ShareLocationActivity"
+            android:label="@string/title_activity_share_location" >
+            <intent-filter>
+                <action android:name="eu.siacs.conversations.location.request" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:name=".ui.ShowLocationActivity"
+            android:label="@string/title_activity_show_location" >
+            <intent-filter>
+                <action android:name="eu.siacs.conversations.location.show" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+            <intent-filter>
+                <action android:name="android.intent.action.VIEW" />
+                <data android:scheme="geo" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
         <activity
             android:name=".ui.ConversationActivity"
             android:theme="@style/SplashTheme">

src/main/java/eu/siacs/conversations/Config.java 🔗

@@ -2,7 +2,8 @@ package eu.siacs.conversations;
 
 import android.graphics.Bitmap;
 
-import java.util.Arrays;
+import org.osmdroid.util.GeoPoint;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -10,8 +11,6 @@ import eu.siacs.conversations.xmpp.chatstate.ChatState;
 import rocks.xmpp.addr.Jid;
 
 public final class Config {
-
-
 	private static final int UNENCRYPTED = 1;
 	private static final int OPENPGP = 2;
 	private static final int OTR = 4;
@@ -160,4 +159,15 @@ public final class Config {
 
 	private Config() {
 	}
+
+	public static final class Map {
+		public final static double INITIAL_ZOOM_LEVEL = 4;
+		public final static double FINAL_ZOOM_LEVEL = 15;
+		public final static GeoPoint INITIAL_POS = new GeoPoint(33.805278, -84.171389);
+		public final static int MY_LOCATION_INDICATOR_SIZE = 10;
+		public final static int MY_LOCATION_INDICATOR_OUTLINE_SIZE = 3;
+		public final static long LOCATION_FIX_TIME_DELTA = 1000 * 10; // ms
+		public final static float LOCATION_FIX_SPACE_DELTA = 10; // m
+		public final static int LOCATION_FIX_SIGNIFICANT_TIME_DELTA = 1000 * 60 * 2; // ms
+	}
 }

src/main/java/eu/siacs/conversations/ui/ActionBarActivity.java 🔗

@@ -0,0 +1,13 @@
+package eu.siacs.conversations.ui;
+
+import android.support.v7.app.ActionBar;
+import android.support.v7.app.AppCompatActivity;
+
+public abstract class ActionBarActivity extends AppCompatActivity {
+    public static void configureActionBar(ActionBar actionBar) {
+        if (actionBar != null) {
+            actionBar.setHomeButtonEnabled(true);
+            actionBar.setDisplayHomeAsUpEnabled(true);
+        }
+    }
+}

src/main/java/eu/siacs/conversations/ui/LocationActivity.java 🔗

@@ -0,0 +1,314 @@
+package eu.siacs.conversations.ui;
+
+import android.Manifest;
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+import android.location.Location;
+import android.location.LocationListener;
+import android.location.LocationManager;
+import android.os.Build;
+import android.os.Bundle;
+import android.preference.PreferenceManager;
+import android.provider.Settings;
+import android.support.annotation.NonNull;
+import android.text.TextUtils;
+import android.util.Log;
+import android.view.MenuItem;
+
+import org.osmdroid.api.IGeoPoint;
+import org.osmdroid.api.IMapController;
+import org.osmdroid.config.Configuration;
+import org.osmdroid.config.IConfigurationProvider;
+import org.osmdroid.tileprovider.tilesource.XYTileSource;
+import org.osmdroid.util.GeoPoint;
+import org.osmdroid.views.MapView;
+import org.osmdroid.views.overlay.Overlay;
+
+import java.io.File;
+
+import eu.siacs.conversations.BuildConfig;
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.ui.util.LocationHelper;
+import eu.siacs.conversations.ui.widget.Marker;
+import eu.siacs.conversations.ui.widget.MyLocation;
+import eu.siacs.conversations.utils.ThemeHelper;
+
+public abstract class LocationActivity extends ActionBarActivity implements LocationListener {
+	protected LocationManager locationManager;
+	protected boolean hasLocationFeature;
+
+	public static final int REQUEST_CODE_CREATE = 0;
+	public static final int REQUEST_CODE_FAB_PRESSED = 1;
+	public static final int REQUEST_CODE_SNACKBAR_PRESSED = 2;
+
+	protected static final String KEY_LOCATION = "loc";
+	protected static final String KEY_ZOOM_LEVEL = "zoom";
+
+	protected Location myLoc = null;
+	protected MapView map = null;
+	protected IMapController mapController = null;
+
+	protected Bitmap marker_icon;
+
+	protected void clearMarkers() {
+		synchronized (this.map.getOverlays()) {
+			for (final Overlay overlay : this.map.getOverlays()) {
+				if (overlay instanceof Marker || overlay instanceof MyLocation) {
+					this.map.getOverlays().remove(overlay);
+				}
+			}
+		}
+	}
+
+	protected void updateLocationMarkers() {
+		clearMarkers();
+	}
+
+	protected XYTileSource tileSource() {
+		return new XYTileSource("OpenStreetMap",
+				0, 19, 256, ".png", new String[] {
+				"https://a.tile.openstreetmap.org/",
+				"https://b.tile.openstreetmap.org/",
+				"https://c.tile.openstreetmap.org/" },"© OpenStreetMap contributors");
+	}
+
+	@Override
+	protected void onCreate(final Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+		final Context ctx = getApplicationContext();
+		setTheme(ThemeHelper.find(this));
+
+		final PackageManager packageManager = ctx.getPackageManager();
+		hasLocationFeature = packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION) ||
+				packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_GPS) ||
+				packageManager.hasSystemFeature(PackageManager.FEATURE_LOCATION_NETWORK);
+		this.locationManager = (LocationManager) this.getSystemService(Context.LOCATION_SERVICE);
+		this.marker_icon = BitmapFactory.decodeResource(ctx.getResources(), R.drawable.marker);
+
+		// Ask for location permissions if location services are enabled and we're
+		// just starting the activity (we don't want to keep pestering them on every
+		// screen rotation or if there's no point because it's disabled anyways).
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && savedInstanceState == null) {
+			requestPermissions(REQUEST_CODE_CREATE);
+		}
+
+		final IConfigurationProvider config = Configuration.getInstance();
+		config.load(ctx, getPreferences());
+		config.setUserAgentValue(BuildConfig.APPLICATION_ID + "_" + BuildConfig.VERSION_CODE);
+
+		final File f = new File(ctx.getCacheDir() + "/tiles");
+		try {
+			//noinspection ResultOfMethodCallIgnored
+			f.mkdirs();
+		} catch (final SecurityException ignored) {
+		}
+		if (f.exists() && f.isDirectory() && f.canRead() && f.canWrite()) {
+			Log.d(Config.LOGTAG, "Using tile cache at: " + f.getAbsolutePath());
+			config.setOsmdroidTileCache(f.getAbsoluteFile());
+		}
+	}
+
+	@Override
+	protected void onSaveInstanceState(@NonNull final Bundle outState) {
+		super.onSaveInstanceState(outState);
+
+		final IGeoPoint center = map.getMapCenter();
+		outState.putParcelable(KEY_LOCATION, new GeoPoint(
+				center.getLatitude(),
+				center.getLongitude()
+		));
+		outState.putDouble(KEY_ZOOM_LEVEL, map.getZoomLevelDouble());
+	}
+
+	@Override
+	protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
+		super.onRestoreInstanceState(savedInstanceState);
+
+		if (savedInstanceState.containsKey(KEY_LOCATION)) {
+			mapController.setCenter(savedInstanceState.getParcelable(KEY_LOCATION));
+		}
+		if (savedInstanceState.containsKey(KEY_ZOOM_LEVEL)) {
+			mapController.setZoom(savedInstanceState.getDouble(KEY_ZOOM_LEVEL));
+		}
+	}
+
+	protected void setupMapView(final GeoPoint pos) {
+		// Get map view and configure it.
+		map = findViewById(R.id.map);
+		map.setTileSource(tileSource());
+		map.setBuiltInZoomControls(false);
+		map.setMultiTouchControls(true);
+		map.setTilesScaledToDpi(getPreferences().getBoolean("scale_tiles_for_high_dpi", false));
+		mapController = map.getController();
+		mapController.setZoom(Config.Map.INITIAL_ZOOM_LEVEL);
+		mapController.setCenter(pos);
+	}
+
+	protected void gotoLoc() {
+		gotoLoc(map.getZoomLevelDouble() == Config.Map.INITIAL_ZOOM_LEVEL);
+	}
+
+	protected abstract void gotoLoc(final boolean setZoomLevel);
+
+	protected abstract void setMyLoc(final Location location);
+
+	protected void requestLocationUpdates() {
+		if (!hasLocationFeature || locationManager == null) {
+			return;
+		}
+
+		Log.d(Config.LOGTAG, "Requesting location updates...");
+		final Location lastKnownLocationGps;
+		final Location lastKnownLocationNetwork;
+
+		try {
+			if (locationManager.getAllProviders().contains(LocationManager.GPS_PROVIDER)) {
+				lastKnownLocationGps = locationManager.getLastKnownLocation(LocationManager.GPS_PROVIDER);
+
+				if (lastKnownLocationGps != null) {
+					setMyLoc(lastKnownLocationGps);
+				}
+				locationManager.requestLocationUpdates(LocationManager.GPS_PROVIDER, Config.Map.LOCATION_FIX_TIME_DELTA,
+						Config.Map.LOCATION_FIX_SPACE_DELTA, this);
+			} else {
+				lastKnownLocationGps = null;
+			}
+
+			if (locationManager.getAllProviders().contains(LocationManager.NETWORK_PROVIDER)) {
+				lastKnownLocationNetwork = locationManager.getLastKnownLocation(LocationManager.NETWORK_PROVIDER);
+				if (lastKnownLocationNetwork != null && LocationHelper.isBetterLocation(lastKnownLocationNetwork,
+						lastKnownLocationGps)) {
+					setMyLoc(lastKnownLocationNetwork);
+				}
+				locationManager.requestLocationUpdates(LocationManager.NETWORK_PROVIDER, Config.Map.LOCATION_FIX_TIME_DELTA,
+						Config.Map.LOCATION_FIX_SPACE_DELTA, this);
+			}
+
+			// If something else is also querying for location more frequently than we are, the battery is already being
+			// drained. Go ahead and use the existing locations as often as we can get them.
+			if (locationManager.getAllProviders().contains(LocationManager.PASSIVE_PROVIDER)) {
+				locationManager.requestLocationUpdates(LocationManager.PASSIVE_PROVIDER, 0, 0, this);
+			}
+		} catch (final SecurityException ignored) {
+			// Do nothing if the users device has no location providers.
+		}
+	}
+
+	protected void pauseLocationUpdates() throws SecurityException {
+		if (locationManager != null) {
+			locationManager.removeUpdates(this);
+		}
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(final MenuItem item) {
+		switch (item.getItemId()) {
+			case android.R.id.home:
+				finish();
+				return true;
+		}
+		return super.onOptionsItemSelected(item);
+	}
+
+	@Override
+	protected void onPause() {
+		super.onPause();
+		Configuration.getInstance().save(this, getPreferences());
+		map.onPause();
+		try {
+			pauseLocationUpdates();
+		} catch (final SecurityException ignored) {
+		}
+	}
+
+	protected abstract void updateUi();
+
+	protected boolean mapAtInitialLoc() {
+		return map.getZoomLevelDouble() == Config.Map.INITIAL_ZOOM_LEVEL;
+	}
+
+	@Override
+	protected void onResume() {
+		super.onResume();
+		Configuration.getInstance().load(this, getPreferences());
+		map.onResume();
+		this.setMyLoc(null);
+		requestLocationUpdates();
+		updateLocationMarkers();
+		updateUi();
+		map.setTileSource(tileSource());
+		map.setTilesScaledToDpi(getPreferences().getBoolean("scale_tiles_for_high_dpi", false));
+
+		if (mapAtInitialLoc()) {
+			gotoLoc();
+		}
+	}
+
+	@TargetApi(Build.VERSION_CODES.M)
+	protected boolean hasLocationPermissions() {
+		return (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
+				checkSelfPermission(Manifest.permission.ACCESS_COARSE_LOCATION) == PackageManager.PERMISSION_GRANTED);
+	}
+
+	@TargetApi(Build.VERSION_CODES.M)
+	protected void requestPermissions(final int request_code) {
+		if (!hasLocationPermissions()) {
+			requestPermissions(
+					new String[]{
+							Manifest.permission.ACCESS_FINE_LOCATION,
+							Manifest.permission.ACCESS_COARSE_LOCATION,
+					},
+					request_code
+			);
+		}
+	}
+
+	@Override
+	public void onRequestPermissionsResult(final int requestCode,
+										   @NonNull final String[] permissions,
+										   @NonNull final int[] grantResults) {
+		super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+		for (int i = 0; i < grantResults.length; i++) {
+			if (Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[i]) ||
+					Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[i])) {
+				if (grantResults[i] == PackageManager.PERMISSION_GRANTED) {
+					requestLocationUpdates();
+				}
+			}
+		}
+	}
+
+	protected SharedPreferences getPreferences() {
+		return PreferenceManager.getDefaultSharedPreferences(getApplicationContext());
+	}
+
+	@TargetApi(Build.VERSION_CODES.KITKAT)
+	private boolean isLocationEnabledKitkat() {
+		try {
+			final int locationMode = Settings.Secure.getInt(getContentResolver(), Settings.Secure.LOCATION_MODE);
+			return locationMode != Settings.Secure.LOCATION_MODE_OFF;
+		} catch( final Settings.SettingNotFoundException e ){
+			return false;
+		}
+	}
+
+	@SuppressWarnings("deprecation")
+	private boolean isLocationEnabledLegacy() {
+		final String locationProviders = Settings.Secure.getString(getContentResolver(),
+				Settings.Secure.LOCATION_PROVIDERS_ALLOWED);
+		return !TextUtils.isEmpty(locationProviders);
+	}
+
+	protected boolean isLocationEnabled() {
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+			return isLocationEnabledKitkat();
+		} else {
+			return isLocationEnabledLegacy();
+		}
+	}
+}

src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java 🔗

@@ -0,0 +1,248 @@
+package eu.siacs.conversations.ui;
+
+import android.Manifest;
+import android.content.Intent;
+import android.content.pm.PackageManager;
+import android.location.Location;
+import android.location.LocationListener;
+import android.os.Build;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.CoordinatorLayout;
+import android.support.design.widget.FloatingActionButton;
+import android.support.design.widget.Snackbar;
+import android.view.View;
+import android.widget.Button;
+
+import org.osmdroid.api.IGeoPoint;
+import org.osmdroid.util.GeoPoint;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.ui.util.LocationHelper;
+import eu.siacs.conversations.ui.widget.Marker;
+import eu.siacs.conversations.ui.widget.MyLocation;
+
+public class ShareLocationActivity extends LocationActivity implements LocationListener {
+
+	private Snackbar snackBar;
+	private boolean marker_fixed_to_loc = false;
+	private static final String KEY_FIXED_TO_LOC = "fixed_to_loc";
+	private Boolean noAskAgain = false;
+
+	@Override
+	protected void onSaveInstanceState(@NonNull final Bundle outState) {
+		super.onSaveInstanceState(outState);
+
+		outState.putBoolean(KEY_FIXED_TO_LOC, marker_fixed_to_loc);
+	}
+
+	@Override
+	protected void onRestoreInstanceState(@NonNull final Bundle savedInstanceState) {
+		super.onRestoreInstanceState(savedInstanceState);
+
+		if (savedInstanceState.containsKey(KEY_FIXED_TO_LOC)) {
+			this.marker_fixed_to_loc = savedInstanceState.getBoolean(KEY_FIXED_TO_LOC);
+		}
+	}
+
+	@Override
+	protected void onCreate(final Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+
+		setContentView(R.layout.activity_share_location);
+		setSupportActionBar(findViewById(R.id.toolbar));
+		configureActionBar(getSupportActionBar());
+		setupMapView(Config.Map.INITIAL_POS);
+
+		// Setup the cancel button
+		final Button cancelButton = findViewById(R.id.cancel_button);
+		cancelButton.setOnClickListener(view -> {
+			setResult(RESULT_CANCELED);
+			finish();
+		});
+
+		final CoordinatorLayout snackBarCoordinator = findViewById(R.id.snackbarCoordinator);
+		if (snackBarCoordinator != null) {
+			this.snackBar = Snackbar.make(snackBarCoordinator, R.string.location_disabled, Snackbar.LENGTH_INDEFINITE);
+			snackBar.setAction(R.string.enable, view -> {
+				if (isLocationEnabledAndAllowed()) {
+					updateUi();
+				} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && !hasLocationPermissions()) {
+					requestPermissions(REQUEST_CODE_SNACKBAR_PRESSED);
+				} else if (!isLocationEnabled()) {
+					startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
+				}
+			});
+		}
+
+		// Setup the share button
+		final Button shareButton = findViewById(R.id.share_button);
+		if (shareButton != null) {
+			shareButton.setOnClickListener(view -> {
+				final Intent result = new Intent();
+
+				if (marker_fixed_to_loc && myLoc != null) {
+					result.putExtra("latitude", myLoc.getLatitude());
+					result.putExtra("longitude", myLoc.getLongitude());
+					result.putExtra("altitude", myLoc.getAltitude());
+					result.putExtra("accuracy", (int) myLoc.getAccuracy());
+				} else {
+					final IGeoPoint markerPoint = map.getMapCenter();
+					result.putExtra("latitude", markerPoint.getLatitude());
+					result.putExtra("longitude", markerPoint.getLongitude());
+				}
+
+				setResult(RESULT_OK, result);
+				finish();
+			});
+		}
+
+		this.marker_fixed_to_loc = isLocationEnabledAndAllowed();
+
+		// Setup the fab button
+		final FloatingActionButton toggleFixedMarkerButton = findViewById(R.id.fab);
+		toggleFixedMarkerButton.setOnClickListener(view -> {
+			if (!marker_fixed_to_loc) {
+				if (!isLocationEnabled()) {
+					startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
+				} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+					requestPermissions(REQUEST_CODE_FAB_PRESSED);
+				}
+			}
+			toggleFixedLocation();
+		});
+	}
+
+	@Override
+	public void onRequestPermissionsResult(final int requestCode,
+										   @NonNull final String[] permissions,
+										   @NonNull final int[] grantResults) {
+		super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+
+		if (grantResults.length > 0 &&
+				grantResults[0] != PackageManager.PERMISSION_GRANTED &&
+				Build.VERSION.SDK_INT >= 23 &&
+				permissions.length > 0 &&
+				(
+						Manifest.permission.LOCATION_HARDWARE.equals(permissions[0]) ||
+								Manifest.permission.ACCESS_FINE_LOCATION.equals(permissions[0]) ||
+								Manifest.permission.ACCESS_COARSE_LOCATION.equals(permissions[0])
+				) &&
+				!shouldShowRequestPermissionRationale(permissions[0])) {
+			noAskAgain = true;
+		}
+
+		if (!noAskAgain && requestCode == REQUEST_CODE_SNACKBAR_PRESSED && !isLocationEnabled() && hasLocationPermissions()) {
+			startActivity(new Intent(android.provider.Settings.ACTION_LOCATION_SOURCE_SETTINGS));
+		}
+		updateUi();
+	}
+
+	@Override
+	protected void gotoLoc(final boolean setZoomLevel) {
+		if (this.myLoc != null && mapController != null) {
+			if (setZoomLevel) {
+				mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
+			}
+			mapController.animateTo(new GeoPoint(this.myLoc));
+		}
+	}
+
+	@Override
+	protected void setMyLoc(final Location location) {
+		this.myLoc = location;
+	}
+
+	@Override
+	protected void onPause() {
+		super.onPause();
+	}
+
+	@Override
+	protected void updateLocationMarkers() {
+		super.updateLocationMarkers();
+		if (this.myLoc != null) {
+			this.map.getOverlays().add(new MyLocation(this, null, this.myLoc));
+			if (this.marker_fixed_to_loc) {
+				map.getOverlays().add(new Marker(marker_icon, new GeoPoint(this.myLoc)));
+			} else {
+				map.getOverlays().add(new Marker(marker_icon));
+			}
+		} else {
+			map.getOverlays().add(new Marker(marker_icon));
+		}
+	}
+
+	@Override
+	public void onLocationChanged(final Location location) {
+		if (this.myLoc == null) {
+			this.marker_fixed_to_loc = true;
+		}
+		updateUi();
+		if (LocationHelper.isBetterLocation(location, this.myLoc)) {
+			final Location oldLoc = this.myLoc;
+			this.myLoc = location;
+
+			// Don't jump back to the users location if they're not moving (more or less).
+			if (oldLoc == null || (this.marker_fixed_to_loc && this.myLoc.distanceTo(oldLoc) > 1)) {
+				gotoLoc();
+			}
+
+			updateLocationMarkers();
+		}
+	}
+
+	@Override
+	public void onStatusChanged(final String provider, final int status, final Bundle extras) {
+
+	}
+
+	@Override
+	public void onProviderEnabled(final String provider) {
+
+	}
+
+	@Override
+	public void onProviderDisabled(final String provider) {
+
+	}
+
+	private boolean isLocationEnabledAndAllowed() {
+		return this.hasLocationFeature && (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || this.hasLocationPermissions()) && this.isLocationEnabled();
+	}
+
+	private void toggleFixedLocation() {
+		this.marker_fixed_to_loc = isLocationEnabledAndAllowed() && !this.marker_fixed_to_loc;
+		if (this.marker_fixed_to_loc) {
+			gotoLoc(false);
+		}
+		updateLocationMarkers();
+		updateUi();
+	}
+
+	@Override
+	protected void updateUi() {
+		if (!hasLocationFeature || noAskAgain || isLocationEnabledAndAllowed()) {
+			this.snackBar.dismiss();
+		} else {
+			this.snackBar.show();
+		}
+
+		// Setup the fab button
+		final FloatingActionButton fab = findViewById(R.id.fab);
+		if (isLocationEnabledAndAllowed()) {
+			fab.setVisibility(View.VISIBLE);
+			runOnUiThread(() -> {
+				fab.setImageResource(marker_fixed_to_loc ? R.drawable.ic_gps_fixed_white_24dp :
+						R.drawable.ic_gps_not_fixed_white_24dp);
+				fab.setContentDescription(getResources().getString(
+						marker_fixed_to_loc ? R.string.action_unfix_from_location : R.string.action_fix_to_location
+				));
+				fab.invalidate();
+			});
+		} else {
+			fab.setVisibility(View.GONE);
+		}
+	}
+}

src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java 🔗

@@ -0,0 +1,234 @@
+package eu.siacs.conversations.ui;
+
+import android.app.ActionBar;
+import android.content.ActivityNotFoundException;
+import android.content.ClipData;
+import android.content.ClipboardManager;
+import android.content.ComponentName;
+import android.content.Intent;
+import android.location.Location;
+import android.location.LocationListener;
+import android.net.Uri;
+import android.os.Bundle;
+import android.support.annotation.NonNull;
+import android.support.design.widget.FloatingActionButton;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.Toast;
+
+import org.osmdroid.util.GeoPoint;
+
+import java.util.HashMap;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import eu.siacs.conversations.ui.util.LocationHelper;
+import eu.siacs.conversations.ui.util.UriHelper;
+import eu.siacs.conversations.ui.widget.Marker;
+import eu.siacs.conversations.ui.widget.MyLocation;
+
+
+public class ShowLocationActivity extends LocationActivity implements LocationListener {
+
+	private GeoPoint loc = Config.Map.INITIAL_POS;
+	private FloatingActionButton navigationButton;
+
+
+	private Uri createGeoUri() {
+		return Uri.parse("geo:" + this.loc.getLatitude() + "," + this.loc.getLongitude());
+	}
+
+	@Override
+	protected void onCreate(final Bundle savedInstanceState) {
+		super.onCreate(savedInstanceState);
+
+		final ActionBar actionBar = getActionBar();
+		if (actionBar != null) {
+			actionBar.setDisplayHomeAsUpEnabled(true);
+		}
+
+		setContentView(R.layout.activity_show_location);
+		setSupportActionBar(findViewById(R.id.toolbar));
+		configureActionBar(getSupportActionBar());
+		setupMapView(this.loc);
+
+		// Setup the fab button
+		this.navigationButton = findViewById(R.id.fab);
+		this.navigationButton.setOnClickListener(view -> startNavigation());
+
+		final Intent intent = getIntent();
+		if (intent != null) {
+			final String action = intent.getAction();
+			if (action == null) {
+				return;
+			}
+			switch (action) {
+				case "eu.siacs.conversations.location.show":
+					if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) {
+						final double longitude = intent.getDoubleExtra("longitude", 0);
+						final double latitude = intent.getDoubleExtra("latitude", 0);
+						this.loc = new GeoPoint(latitude, longitude);
+					}
+					break;
+				case Intent.ACTION_VIEW:
+					final Uri geoUri = intent.getData();
+
+					// Attempt to set zoom level if the geo URI specifies it
+					if (geoUri != null) {
+						final HashMap<String, String> query = UriHelper.parseQueryString(geoUri.getQuery());
+
+						// Check for zoom level.
+						final String z = query.get("z");
+						if (z != null) {
+							try {
+								mapController.setZoom(Double.valueOf(z));
+							} catch (final Exception ignored) {
+							}
+						}
+
+						// Check for the actual geo query.
+						boolean posInQuery = false;
+						final String q = query.get("q");
+						if (q != null) {
+							final Pattern latlng = Pattern.compile("/^([-+]?[0-9]+(\\.[0-9]+)?),([-+]?[0-9]+(\\.[0-9]+)?)(\\(.*\\))?/");
+							final Matcher m = latlng.matcher(q);
+							if (m.matches()) {
+								try {
+									this.loc = new GeoPoint(Double.valueOf(m.group(1)), Double.valueOf(m.group(3)));
+									posInQuery = true;
+								} catch (final Exception ignored) {
+								}
+							}
+						}
+
+						final String schemeSpecificPart = geoUri.getSchemeSpecificPart();
+						if (schemeSpecificPart != null && !schemeSpecificPart.isEmpty()) {
+							try {
+								final GeoPoint latlong = LocationHelper.parseLatLong(schemeSpecificPart);
+								if (latlong != null && !posInQuery) {
+									this.loc = latlong;
+								}
+							} catch (final NumberFormatException ignored) {
+							}
+						}
+					}
+
+					break;
+			}
+			updateLocationMarkers();
+		}
+	}
+
+	@Override
+	protected void gotoLoc(final boolean setZoomLevel) {
+		if (this.loc != null && mapController != null) {
+			if (setZoomLevel) {
+				mapController.setZoom(Config.Map.FINAL_ZOOM_LEVEL);
+			}
+			mapController.animateTo(new GeoPoint(this.loc));
+		}
+	}
+
+	@Override
+	public void onRequestPermissionsResult(final int requestCode,
+										   @NonNull final String[] permissions,
+										   @NonNull final int[] grantResults) {
+		super.onRequestPermissionsResult(requestCode, permissions, grantResults);
+		updateUi();
+	}
+
+	@Override
+	protected void setMyLoc(final Location location) {
+		this.myLoc = location;
+	}
+
+	@Override
+	public boolean onCreateOptionsMenu(final Menu menu) {
+		// Inflate the menu; this adds items to the action bar if it is present.
+		getMenuInflater().inflate(R.menu.menu_show_location, menu);
+		updateUi();
+		return true;
+	}
+
+	@Override
+	protected void updateLocationMarkers() {
+		super.updateLocationMarkers();
+		if (this.myLoc != null) {
+			this.map.getOverlays().add(new MyLocation(this, null, this.myLoc));
+		}
+		this.map.getOverlays().add(new Marker(this.marker_icon, this.loc));
+	}
+
+	@Override
+	protected void onPause() {
+		super.onPause();
+	}
+
+	@Override
+	public boolean onOptionsItemSelected(final MenuItem item) {
+		switch (item.getItemId()) {
+			case R.id.action_copy_location:
+				final ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE);
+				if (clipboard != null) {
+					final ClipData clip = ClipData.newPlainText("location", createGeoUri().toString());
+					clipboard.setPrimaryClip(clip);
+				}
+				return true;
+			case R.id.action_share_location:
+				final Intent shareIntent = new Intent();
+				shareIntent.setAction(Intent.ACTION_SEND);
+				shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString());
+				shareIntent.setType("text/plain");
+				try {
+					startActivity(Intent.createChooser(shareIntent, getText(R.string.share_with)));
+				} catch (final ActivityNotFoundException e) {
+					//This should happen only on faulty androids because normally chooser is always available
+					Toast.makeText(this, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT).show();
+				}
+				return true;
+		}
+		return super.onOptionsItemSelected(item);
+	}
+
+	private void startNavigation() {
+		startActivity(new Intent(Intent.ACTION_VIEW, Uri.parse(
+				"google.navigation:q=" +
+						String.valueOf(this.loc.getLatitude()) + "," + String.valueOf(this.loc.getLongitude())
+		)));
+	}
+
+	@Override
+	protected void updateUi() {
+		final Intent i = new Intent(Intent.ACTION_VIEW, Uri.parse("google.navigation:q=0,0"));
+		final ComponentName component = i.resolveActivity(getPackageManager());
+		if (this.navigationButton != null) {
+			this.navigationButton.setVisibility(component == null ? View.GONE : View.VISIBLE);
+		}
+	}
+
+	@Override
+	public void onLocationChanged(final Location location) {
+		if (LocationHelper.isBetterLocation(location, this.myLoc)) {
+			this.myLoc = location;
+			updateLocationMarkers();
+		}
+	}
+
+	@Override
+	public void onStatusChanged(final String provider, final int status, final Bundle extras) {
+
+	}
+
+	@Override
+	public void onProviderEnabled(final String provider) {
+
+	}
+
+	@Override
+	public void onProviderDisabled(final String provider) {
+
+	}
+}

src/main/java/eu/siacs/conversations/ui/XmppActivity.java 🔗

@@ -3,9 +3,6 @@ package eu.siacs.conversations.ui;
 import android.Manifest;
 import android.annotation.SuppressLint;
 import android.annotation.TargetApi;
-import android.support.v7.app.ActionBar;
-import android.support.v7.app.AlertDialog;
-import android.support.v7.app.AlertDialog.Builder;
 import android.app.PendingIntent;
 import android.content.ActivityNotFoundException;
 import android.content.ClipData;
@@ -37,7 +34,8 @@ import android.os.PowerManager;
 import android.os.SystemClock;
 import android.preference.PreferenceManager;
 import android.support.v4.content.ContextCompat;
-import android.support.v7.app.AppCompatActivity;
+import android.support.v7.app.AlertDialog;
+import android.support.v7.app.AlertDialog.Builder;
 import android.support.v7.app.AppCompatDelegate;
 import android.text.InputType;
 import android.util.DisplayMetrics;
@@ -76,7 +74,7 @@ import eu.siacs.conversations.xmpp.OnKeyStatusUpdated;
 import eu.siacs.conversations.xmpp.OnUpdateBlocklist;
 import rocks.xmpp.addr.Jid;
 
-public abstract class XmppActivity extends AppCompatActivity {
+public abstract class XmppActivity extends ActionBarActivity {
 
 	public static final String EXTRA_ACCOUNT = "account";
 	protected static final int REQUEST_ANNOUNCE_PGP = 0x0101;
@@ -610,13 +608,6 @@ public abstract class XmppActivity extends AppCompatActivity {
 		}
 	}
 
-	public static void configureActionBar(ActionBar actionBar) {
-		if (actionBar != null) {
-			actionBar.setHomeButtonEnabled(true);
-			actionBar.setDisplayHomeAsUpEnabled(true);
-		}
-	}
-
 	protected boolean noAccountUsesPgp() {
 		if (!hasPgp()) {
 			return true;

src/main/java/eu/siacs/conversations/ui/util/LocationHelper.java 🔗

@@ -0,0 +1,72 @@
+package eu.siacs.conversations.ui.util;
+
+import android.location.Location;
+
+import org.osmdroid.util.GeoPoint;
+
+import eu.siacs.conversations.Config;
+
+public final class LocationHelper {
+	/**
+	 * Parses a lat long string in the form "lat,long".
+	 *
+	 * @param latlong A string in the form "lat,long"
+	 * @return A GeoPoint representing the lat,long string.
+	 * @throws NumberFormatException If an invalid lat or long is specified.
+	 */
+	public static GeoPoint parseLatLong(final String latlong) throws NumberFormatException {
+		if (latlong == null || latlong.isEmpty()) {
+			return null;
+		}
+
+		final String[] parts = latlong.split(",");
+		if (parts[1].contains("?")) {
+			parts[1] = parts[1].substring(0, parts[1].indexOf("?"));
+		}
+		return new GeoPoint(Double.valueOf(parts[0]), Double.valueOf(parts[1]));
+	}
+
+	private static boolean isSameProvider(final String provider1, final String provider2) {
+		if (provider1 == null) {
+			return provider2 == null;
+		}
+		return provider1.equals(provider2);
+	}
+
+	public static boolean isBetterLocation(final Location location, final Location prevLoc) {
+		if (prevLoc == null) {
+			return true;
+		}
+
+		// Check whether the new location fix is newer or older
+		final long timeDelta = location.getTime() - prevLoc.getTime();
+		final boolean isSignificantlyNewer = timeDelta > Config.Map.LOCATION_FIX_SIGNIFICANT_TIME_DELTA;
+		final boolean isSignificantlyOlder = timeDelta < -Config.Map.LOCATION_FIX_SIGNIFICANT_TIME_DELTA;
+		final boolean isNewer = timeDelta > 0;
+
+		if (isSignificantlyNewer) {
+			return true;
+		} else if (isSignificantlyOlder) {
+			return false;
+		}
+
+		// Check whether the new location fix is more or less accurate
+		final int accuracyDelta = (int) (location.getAccuracy() - prevLoc.getAccuracy());
+		final boolean isLessAccurate = accuracyDelta > 0;
+		final boolean isMoreAccurate = accuracyDelta < 0;
+		final boolean isSignificantlyLessAccurate = accuracyDelta > 200;
+
+		// Check if the old and new location are from the same provider
+		final boolean isFromSameProvider = isSameProvider(location.getProvider(), prevLoc.getProvider());
+
+		// Determine location quality using a combination of timeliness and accuracy
+		if (isMoreAccurate) {
+			return true;
+		} else if (isNewer && !isLessAccurate) {
+			return true;
+		} else if (isNewer && !isSignificantlyLessAccurate && isFromSameProvider) {
+			return true;
+		}
+		return false;
+	}
+}

src/main/java/eu/siacs/conversations/ui/util/UriHelper.java 🔗

@@ -0,0 +1,30 @@
+package eu.siacs.conversations.ui.util;
+
+import java.util.HashMap;
+
+/**
+ * Helper methods for parsing URI's.
+ */
+public final class UriHelper {
+	/**
+	 * Parses a query string into a hashmap.
+	 *
+	 * @param q The query string to split.
+	 * @return A hashmap containing the key-value pairs from the query string.
+	 */
+	public static HashMap<String, String> parseQueryString(final String q) {
+		if (q == null || q.isEmpty()) {
+			return null;
+		}
+
+		final String[] query = q.split("&");
+		// TODO: Look up the HashMap implementation and figure out what the load factor is and make sure we're not reallocating here.
+		final HashMap<String, String> queryMap = new HashMap<>(query.length);
+		for (final String param : query) {
+			final String[] pair = param.split("=");
+			queryMap.put(pair[0], pair.length == 2 && !pair[1].isEmpty() ? pair[1] : null);
+		}
+
+		return queryMap;
+	}
+}

src/main/java/eu/siacs/conversations/ui/widget/Marker.java 🔗

@@ -0,0 +1,52 @@
+package eu.siacs.conversations.ui.widget;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Point;
+
+import org.osmdroid.util.GeoPoint;
+import org.osmdroid.views.MapView;
+import org.osmdroid.views.overlay.mylocation.SimpleLocationOverlay;
+
+/**
+ * An immutable marker overlay.
+ */
+public class Marker extends SimpleLocationOverlay {
+	private final GeoPoint position;
+	private final Bitmap icon;
+	private final Point mapPoint;
+
+	/**
+	 * Create a marker overlay which will be drawn at the current Geographical position.
+	 * @param icon A bitmap icon for the marker
+	 * @param position The geographic position where the marker will be drawn (if it is inside the view)
+	 */
+	public Marker(final Bitmap icon, final GeoPoint position) {
+		super(icon);
+		this.icon = icon;
+		this.position = position;
+		this.mapPoint = new Point();
+	}
+
+	/**
+	 * Create a marker overlay which will be drawn centered in the view.
+	 * @param icon A bitmap icon for the marker
+	 */
+	public Marker(final Bitmap icon) {
+		this(icon, null);
+	}
+
+	@Override
+	public void draw(final Canvas c, final MapView view, final boolean shadow) {
+		super.draw(c, view, shadow);
+
+		// If no position was set for the marker, draw it centered in the view.
+		view.getProjection().toPixels(this.position == null ? view.getMapCenter() : position, mapPoint);
+
+		c.drawBitmap(icon,
+				mapPoint.x - icon.getWidth() / 2,
+				mapPoint.y - icon.getHeight(),
+				null);
+
+	}
+}

src/main/java/eu/siacs/conversations/ui/widget/MyLocation.java 🔗

@@ -0,0 +1,65 @@
+package eu.siacs.conversations.ui.widget;
+
+import android.annotation.TargetApi;
+import android.content.Context;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Point;
+import android.location.Location;
+import android.os.Build;
+
+import org.osmdroid.util.GeoPoint;
+import org.osmdroid.views.MapView;
+import org.osmdroid.views.overlay.mylocation.SimpleLocationOverlay;
+
+import eu.siacs.conversations.Config;
+import eu.siacs.conversations.R;
+import microsoft.mappoint.TileSystem;
+
+public class MyLocation extends SimpleLocationOverlay {
+	private final GeoPoint position;
+	private final float accuracy;
+	private final Point mapCenterPoint;
+	private final Paint fill;
+	private final Paint outline;
+
+	@TargetApi(Build.VERSION_CODES.LOLLIPOP)
+	private int getColor(final Context ctx) {
+		final int accent;
+		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+			accent = ctx.getResources().getColor(R.color.accent, ctx.getTheme());
+		} else {
+			//noinspection deprecation
+			accent = ctx.getResources().getColor(R.color.accent);
+		}
+		return accent;
+	}
+
+	public MyLocation(final Context ctx, final Bitmap icon, final Location position) {
+		super(icon);
+		this.mapCenterPoint = new Point();
+		this.fill = new Paint(Paint.ANTI_ALIAS_FLAG);
+		final int accent = this.getColor(ctx);
+		fill.setColor(accent);
+		fill.setStyle(Paint.Style.FILL);
+		this.outline = new Paint(Paint.ANTI_ALIAS_FLAG);
+		outline.setColor(accent);
+		outline.setAlpha(50);
+		outline.setStyle(Paint.Style.FILL);
+		this.position = new GeoPoint(position);
+		this.accuracy = position.getAccuracy();
+	}
+
+	@Override
+	public void draw(final Canvas c, final MapView view, final boolean shadow) {
+		super.draw(c, view, shadow);
+
+		view.getProjection().toPixels(position, mapCenterPoint);
+		c.drawCircle(mapCenterPoint.x, mapCenterPoint.y,
+				Math.max(Config.Map.MY_LOCATION_INDICATOR_SIZE + Config.Map.MY_LOCATION_INDICATOR_OUTLINE_SIZE,
+						accuracy / (float) TileSystem.GroundResolution(position.getLatitude(), view.getZoomLevel())
+				), this.outline);
+		c.drawCircle(mapCenterPoint.x, mapCenterPoint.y, Config.Map.MY_LOCATION_INDICATOR_SIZE, this.fill);
+	}
+}

src/main/res/drawable/ic_directions_black_24dp.xml 🔗

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M21.71,11.29l-9,-9c-0.39,-0.39 -1.02,-0.39 -1.41,0l-9,9c-0.39,0.39 -0.39,1.02 0,1.41l9,9c0.39,0.39 1.02,0.39 1.41,0l9,-9c0.39,-0.38 0.39,-1.01 0,-1.41zM14,14.5V12h-4v3H8v-4c0,-0.55 0.45,-1 1,-1h5V7.5l3.5,3.5 -3.5,3.5z"/>
+</vector>

src/main/res/drawable/ic_gps_fixed_black_24dp.xml 🔗

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12,8c-2.21,0 -4,1.79 -4,4s1.79,4 4,4 4,-1.79 4,-4 -1.79,-4 -4,-4zM20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94L13,1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11L1,11v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94L11,23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94L23,13v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
+</vector>

src/main/res/drawable/ic_gps_not_fixed_black_24dp.xml 🔗

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M20.94,11c-0.46,-4.17 -3.77,-7.48 -7.94,-7.94V1h-2v2.06C6.83,3.52 3.52,6.83 3.06,11H1v2h2.06c0.46,4.17 3.77,7.48 7.94,7.94V23h2v-2.06c4.17,-0.46 7.48,-3.77 7.94,-7.94H23v-2h-2.06zM12,19c-3.87,0 -7,-3.13 -7,-7s3.13,-7 7,-7 7,3.13 7,7 -3.13,7 -7,7z"/>
+</vector>

src/main/res/drawable/ic_place_black_24dp.xml 🔗

@@ -0,0 +1,9 @@
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+        android:width="24dp"
+        android:height="24dp"
+        android:viewportWidth="24.0"
+        android:viewportHeight="24.0">
+    <path
+        android:fillColor="#FF000000"
+        android:pathData="M12,2C8.13,2 5,5.13 5,9c0,5.25 7,13 7,13s7,-7.75 7,-13c0,-3.87 -3.13,-7 -7,-7zM12,11.5c-1.38,0 -2.5,-1.12 -2.5,-2.5s1.12,-2.5 2.5,-2.5 2.5,1.12 2.5,2.5 -1.12,2.5 -2.5,2.5z"/>
+</vector>

src/main/res/layout/activity_share_location.xml 🔗

@@ -0,0 +1,66 @@
+<android.support.design.widget.CoordinatorLayout
+    android:id="@+id/snackbarCoordinator"
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent">
+    <RelativeLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        tools:context=".ui.ShareLocationActivity">
+
+        <include layout="@layout/toolbar" />
+
+        <org.osmdroid.views.MapView android:id="@+id/map"
+            android:layout_width="match_parent"
+            android:layout_height="match_parent"
+            android:layout_above="@+id/button_bar"/>
+
+        <LinearLayout
+            android:id="@+id/button_bar"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_alignParentBottom="true"
+            android:layout_alignParentLeft="true"
+            android:layout_alignParentRight="true"
+            tools:ignore="RtlHardcoded">
+
+            <Button
+                android:id="@+id/cancel_button"
+                style="?android:attr/borderlessButtonStyle"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="@string/cancel"
+                android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+
+            <View
+                android:layout_width="1dp"
+                android:layout_height="match_parent"
+                android:layout_marginBottom="7dp"
+                android:layout_marginTop="7dp"
+                android:background="@color/accent"/>
+
+            <Button
+                android:id="@+id/share_button"
+                style="?android:attr/borderlessButtonStyle"
+                android:layout_width="0dp"
+                android:layout_height="wrap_content"
+                android:layout_weight="1"
+                android:text="@string/share_with"
+                android:textAppearance="@style/TextAppearance.Conversations.Body1"/>
+        </LinearLayout>
+
+        <android.support.design.widget.FloatingActionButton
+            android:id="@+id/fab"
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="end|bottom"
+            android:src="?attr/icon_gps_fixed"
+            android:layout_alignParentEnd="true"
+            android:layout_above="@+id/button_bar"
+            android:contentDescription="@string/action_unfix_from_location"
+            android:layout_margin="16dp" />
+
+    </RelativeLayout>
+</android.support.design.widget.CoordinatorLayout>

src/main/res/layout/activity_show_location.xml 🔗

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    tools:context=".ui.ShowLocationActivity">
+
+    <include layout="@layout/toolbar" />
+
+    <org.osmdroid.views.MapView android:id="@+id/map"
+        android:layout_width="fill_parent"
+        android:layout_height="fill_parent"/>
+
+    <android.support.design.widget.FloatingActionButton
+        android:id="@+id/fab"
+        android:layout_width="wrap_content"
+        android:layout_height="wrap_content"
+        android:layout_gravity="end|bottom"
+        android:src="?attr/icon_directions"
+        android:tint="@color/white"
+        android:layout_alignParentEnd="true"
+        android:contentDescription="@string/action_unfix_from_location"
+        android:layout_margin="16dp"
+        android:layout_alignParentBottom="true"/>
+</RelativeLayout>

src/main/res/menu/menu_show_location.xml 🔗

@@ -0,0 +1,14 @@
+<menu
+    xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto">
+    <item android:id="@+id/action_share_location"
+        app:showAsAction="ifRoom"
+        android:showAsAction="ifRoom"
+        android:title="@string/action_share_location"
+        android:icon="?attr/icon_share"/>
+    <item android:id="@+id/action_copy_location"
+        android:title="@string/action_copy_location"
+        android:icon="?attr/icon_copy_bar"
+        app:showAsAction="ifRoom"
+        android:showAsAction="ifRoom"/>
+</menu>

src/main/res/values/about.xml 🔗

@@ -57,5 +57,8 @@
 			\n\nhttps://github.com/vinc3m1/RoundedImageView\n(Apache License, Version 2.0)
 			\n\nhttps://github.com/jdamcd/android-crop\n(Apache License, Version 2.0)
 			\n\nhttps://github.com/zxing/zxing\n(Apache License, Version 2.0)
+			\n\nhttps://github.com/osmdroid/osmdroid\n(Apache License, Version 2.0)
+			\n\n\nMaps
+			\n\nMaps by Open Street Map (https://www.openstreetmap.org). Copyright restrictions may apply.
 	</string>
 </resources>

src/main/res/values/attrs.xml 🔗

@@ -66,6 +66,11 @@
     <attr name="icon_enable_undecided_device" format="reference"/>
     <attr name="icon_scroll_down" format="reference"/>
 
+    <attr name="icon_gps_not_fixed" format="reference"/>
+    <attr name="icon_gps_fixed" format="reference"/>
+    <attr name="icon_directions" format="reference"/>
+    <attr name="icon_copy_bar" format="reference"/>
+
     <attr name="icon_notifications" format="reference"/>
     <attr name="icon_notifications_off" format="reference"/>
     <attr name="icon_notifications_paused" format="reference"/>

src/main/res/values/strings.xml 🔗

@@ -690,4 +690,12 @@
     <string name="large">Large</string>
     <string name="not_encrypted_for_this_device">Message was not encrypted for this device.</string>
     <string name="undo">undo</string>
+    <string name="location_disabled">Location sharing is disabled</string>
+    <string name="action_fix_to_location">Fix position</string>
+    <string name="action_unfix_from_location">Unfix position</string>
+    <string name="action_copy_location">Copy Location</string>
+    <string name="action_share_location">Share Location</string>
+    <string name="action_directions">Directions</string>
+    <string name="title_activity_share_location">Share location</string>
+    <string name="title_activity_show_location">Show location</string>
 </resources>

src/main/res/values/themes.xml 🔗

@@ -79,6 +79,11 @@
         <item type="reference" name="icon_scan_qr_code">@drawable/ic_qr_code_scan_white_24dp</item>
         <item type="reference" name="icon_scroll_down">@drawable/ic_scroll_to_end_black</item>
 
+        <item type="reference" name="icon_gps_not_fixed">@drawable/ic_gps_not_fixed_black_24dp</item>
+        <item type="reference" name="icon_gps_fixed">@drawable/ic_gps_fixed_black_24dp</item>
+        <item type="reference" name="icon_directions">@drawable/ic_directions_black_24dp</item>
+        <item type="reference" name="icon_copy_bar">@drawable/ic_content_copy_white_24dp</item>
+
         <item type="reference" name="icon_notifications">@drawable/ic_notifications_black_24dp</item>
         <item type="reference" name="icon_notifications_off">@drawable/ic_notifications_off_black_24dp</item>
         <item type="reference" name="icon_notifications_paused">@drawable/ic_notifications_paused_black_24dp</item>
@@ -164,6 +169,11 @@
         <item type="reference" name="icon_scan_qr_code">@drawable/ic_qr_code_scan_white_24dp</item>
         <item type="reference" name="icon_scroll_down">@drawable/ic_scroll_to_end_white</item>
 
+        <item type="reference" name="icon_gps_not_fixed">@drawable/ic_gps_not_fixed_white_24dp</item>
+        <item type="reference" name="icon_gps_fixed">@drawable/ic_gps_fixed_white_24dp</item>
+        <item type="reference" name="icon_directions">@drawable/ic_directions_white_24dp</item>
+        <item type="reference" name="icon_copy_bar">@drawable/ic_content_copy_white_24dp</item>
+
         <item type="reference" name="icon_notifications">@drawable/ic_notifications_white_24dp</item>
         <item type="reference" name="icon_notifications_off">@drawable/ic_notifications_off_white_24dp</item>
         <item type="reference" name="icon_notifications_paused">@drawable/ic_notifications_paused_white_24dp</item>