diff --git a/art/marker.svg b/art/marker.svg
new file mode 100644
index 000000000..f73c1537b
--- /dev/null
+++ b/art/marker.svg
@@ -0,0 +1,110 @@
+
+
diff --git a/art/render.rb b/art/render.rb
index 326311895..9513e5584 100755
--- a/art/render.rb
+++ b/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
diff --git a/build.gradle b/build.gradle
index aabb8f255..4a6348fa3 100644
--- a/build.gradle
+++ b/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 {
diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index a191042a2..a7eaad0bc 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -13,6 +13,13 @@
+
+
+
+
+
+
+
@@ -49,7 +56,27 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/java/eu/siacs/conversations/Config.java b/src/main/java/eu/siacs/conversations/Config.java
index 1a5ccef7c..e128ef358 100644
--- a/src/main/java/eu/siacs/conversations/Config.java
+++ b/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
+ }
}
diff --git a/src/main/java/eu/siacs/conversations/ui/ActionBarActivity.java b/src/main/java/eu/siacs/conversations/ui/ActionBarActivity.java
new file mode 100644
index 000000000..72a89ab2c
--- /dev/null
+++ b/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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/ui/LocationActivity.java b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java
new file mode 100644
index 000000000..cc089c00d
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/LocationActivity.java
@@ -0,0 +1,316 @@
+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;
+
+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();
+
+ 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);
+ final Boolean dark = PreferenceManager.getDefaultSharedPreferences(ctx)
+ .getString("theme", "light").equals("dark");
+ final int mTheme = dark ? R.style.ConversationsTheme_Dark : R.style.ConversationsTheme;
+ setTheme(mTheme);
+
+ // 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();
+ }
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java b/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java
new file mode 100644
index 000000000..7f970a2a0
--- /dev/null
+++ b/src/main/java/eu/siacs/conversations/ui/ShareLocationActivity.java
@@ -0,0 +1,247 @@
+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.FloatingActionButton;
+import android.view.View;
+import android.widget.Button;
+import android.widget.RelativeLayout;
+import android.widget.TextView;
+
+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 RelativeLayout 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();
+ });
+
+ // Setup the snackbar
+ this.snackBar = findViewById(R.id.snackbar);
+ final TextView snackbarAction = findViewById(R.id.snackbar_action);
+ snackbarAction.setOnClickListener(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.setVisibility(View.GONE);
+ } else {
+ this.snackBar.setVisibility(View.VISIBLE);
+ }
+
+ // 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);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java b/src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java
new file mode 100644
index 000000000..7e697f053
--- /dev/null
+++ b/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 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) {
+
+ }
+}
diff --git a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java
index 3e0b389a8..28755c316 100644
--- a/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java
+++ b/src/main/java/eu/siacs/conversations/ui/UriHandlerActivity.java
@@ -165,4 +165,4 @@ public class UriHandlerActivity extends AppCompatActivity {
}
finish();
}
-}
+}
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java b/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
index ec5eaead5..8684d8580 100644
--- a/src/main/java/eu/siacs/conversations/ui/XmppActivity.java
+++ b/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;
diff --git a/src/main/java/eu/siacs/conversations/ui/util/LocationHelper.java b/src/main/java/eu/siacs/conversations/ui/util/LocationHelper.java
new file mode 100644
index 000000000..27a6c0837
--- /dev/null
+++ b/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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/ui/util/UriHelper.java b/src/main/java/eu/siacs/conversations/ui/util/UriHelper.java
new file mode 100644
index 000000000..e91012ad1
--- /dev/null
+++ b/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 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 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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/ui/widget/Marker.java b/src/main/java/eu/siacs/conversations/ui/widget/Marker.java
new file mode 100644
index 000000000..0e2822270
--- /dev/null
+++ b/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);
+
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/eu/siacs/conversations/ui/widget/MyLocation.java b/src/main/java/eu/siacs/conversations/ui/widget/MyLocation.java
new file mode 100644
index 000000000..5dc771b44
--- /dev/null
+++ b/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);
+ }
+}
diff --git a/src/main/res/drawable-hdpi/ic_directions_black_24dp.png b/src/main/res/drawable-hdpi/ic_directions_black_24dp.png
new file mode 100644
index 000000000..1d429c8f7
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_directions_black_24dp.png differ
diff --git a/src/main/res/drawable-hdpi/ic_directions_white_24dp.png b/src/main/res/drawable-hdpi/ic_directions_white_24dp.png
new file mode 100644
index 000000000..e33ea5612
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_directions_white_24dp.png differ
diff --git a/src/main/res/drawable-hdpi/ic_gps_fixed_black_24dp.png b/src/main/res/drawable-hdpi/ic_gps_fixed_black_24dp.png
new file mode 100644
index 000000000..85e38726d
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_gps_fixed_black_24dp.png differ
diff --git a/src/main/res/drawable-hdpi/ic_gps_fixed_white_24dp.png b/src/main/res/drawable-hdpi/ic_gps_fixed_white_24dp.png
new file mode 100644
index 000000000..745db489b
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_gps_fixed_white_24dp.png differ
diff --git a/src/main/res/drawable-hdpi/ic_gps_not_fixed_black_24dp.png b/src/main/res/drawable-hdpi/ic_gps_not_fixed_black_24dp.png
new file mode 100644
index 000000000..7c5a19d13
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_gps_not_fixed_black_24dp.png differ
diff --git a/src/main/res/drawable-hdpi/ic_gps_not_fixed_white_24dp.png b/src/main/res/drawable-hdpi/ic_gps_not_fixed_white_24dp.png
new file mode 100644
index 000000000..76f84e2a3
Binary files /dev/null and b/src/main/res/drawable-hdpi/ic_gps_not_fixed_white_24dp.png differ
diff --git a/src/main/res/drawable-hdpi/marker.png b/src/main/res/drawable-hdpi/marker.png
new file mode 100644
index 000000000..e41741b9d
Binary files /dev/null and b/src/main/res/drawable-hdpi/marker.png differ
diff --git a/src/main/res/drawable-mdpi/ic_directions_black_24dp.png b/src/main/res/drawable-mdpi/ic_directions_black_24dp.png
new file mode 100644
index 000000000..7be13ecd1
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_directions_black_24dp.png differ
diff --git a/src/main/res/drawable-mdpi/ic_directions_white_24dp.png b/src/main/res/drawable-mdpi/ic_directions_white_24dp.png
new file mode 100644
index 000000000..b63267446
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_directions_white_24dp.png differ
diff --git a/src/main/res/drawable-mdpi/ic_gps_fixed_black_24dp.png b/src/main/res/drawable-mdpi/ic_gps_fixed_black_24dp.png
new file mode 100644
index 000000000..5684aa7dc
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_gps_fixed_black_24dp.png differ
diff --git a/src/main/res/drawable-mdpi/ic_gps_fixed_white_24dp.png b/src/main/res/drawable-mdpi/ic_gps_fixed_white_24dp.png
new file mode 100644
index 000000000..d1c563cc9
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_gps_fixed_white_24dp.png differ
diff --git a/src/main/res/drawable-mdpi/ic_gps_not_fixed_black_24dp.png b/src/main/res/drawable-mdpi/ic_gps_not_fixed_black_24dp.png
new file mode 100644
index 000000000..ffd6cd403
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_gps_not_fixed_black_24dp.png differ
diff --git a/src/main/res/drawable-mdpi/ic_gps_not_fixed_white_24dp.png b/src/main/res/drawable-mdpi/ic_gps_not_fixed_white_24dp.png
new file mode 100644
index 000000000..35404467a
Binary files /dev/null and b/src/main/res/drawable-mdpi/ic_gps_not_fixed_white_24dp.png differ
diff --git a/src/main/res/drawable-mdpi/marker.png b/src/main/res/drawable-mdpi/marker.png
new file mode 100644
index 000000000..ebfa2a29f
Binary files /dev/null and b/src/main/res/drawable-mdpi/marker.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_directions_black_24dp.png b/src/main/res/drawable-xhdpi/ic_directions_black_24dp.png
new file mode 100644
index 000000000..56284b3c4
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_directions_black_24dp.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_directions_white_24dp.png b/src/main/res/drawable-xhdpi/ic_directions_white_24dp.png
new file mode 100644
index 000000000..8b29cb4a6
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_directions_white_24dp.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_gps_fixed_black_24dp.png b/src/main/res/drawable-xhdpi/ic_gps_fixed_black_24dp.png
new file mode 100644
index 000000000..7faa3455f
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_gps_fixed_black_24dp.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_gps_fixed_white_24dp.png b/src/main/res/drawable-xhdpi/ic_gps_fixed_white_24dp.png
new file mode 100644
index 000000000..ffab865d9
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_gps_fixed_white_24dp.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_gps_not_fixed_black_24dp.png b/src/main/res/drawable-xhdpi/ic_gps_not_fixed_black_24dp.png
new file mode 100644
index 000000000..14282267e
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_gps_not_fixed_black_24dp.png differ
diff --git a/src/main/res/drawable-xhdpi/ic_gps_not_fixed_white_24dp.png b/src/main/res/drawable-xhdpi/ic_gps_not_fixed_white_24dp.png
new file mode 100644
index 000000000..c28a25012
Binary files /dev/null and b/src/main/res/drawable-xhdpi/ic_gps_not_fixed_white_24dp.png differ
diff --git a/src/main/res/drawable-xhdpi/marker.png b/src/main/res/drawable-xhdpi/marker.png
new file mode 100644
index 000000000..a3413b8e4
Binary files /dev/null and b/src/main/res/drawable-xhdpi/marker.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_directions_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_directions_black_24dp.png
new file mode 100644
index 000000000..6ebfddbeb
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_directions_black_24dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_directions_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_directions_white_24dp.png
new file mode 100644
index 000000000..ee364ee81
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_directions_white_24dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_gps_fixed_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_gps_fixed_black_24dp.png
new file mode 100644
index 000000000..d3a1ab08c
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_gps_fixed_black_24dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_gps_fixed_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_gps_fixed_white_24dp.png
new file mode 100644
index 000000000..387ecdfbc
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_gps_fixed_white_24dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_gps_not_fixed_black_24dp.png b/src/main/res/drawable-xxhdpi/ic_gps_not_fixed_black_24dp.png
new file mode 100644
index 000000000..d439f3938
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_gps_not_fixed_black_24dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/ic_gps_not_fixed_white_24dp.png b/src/main/res/drawable-xxhdpi/ic_gps_not_fixed_white_24dp.png
new file mode 100644
index 000000000..eac72e8dc
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/ic_gps_not_fixed_white_24dp.png differ
diff --git a/src/main/res/drawable-xxhdpi/marker.png b/src/main/res/drawable-xxhdpi/marker.png
new file mode 100644
index 000000000..70456d49d
Binary files /dev/null and b/src/main/res/drawable-xxhdpi/marker.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_directions_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_directions_black_24dp.png
new file mode 100644
index 000000000..0ad2fbbc7
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_directions_black_24dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_directions_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_directions_white_24dp.png
new file mode 100644
index 000000000..3e3302fbd
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_directions_white_24dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_gps_fixed_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_gps_fixed_black_24dp.png
new file mode 100644
index 000000000..0812b0e31
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_gps_fixed_black_24dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_gps_fixed_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_gps_fixed_white_24dp.png
new file mode 100644
index 000000000..c55220a5f
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_gps_fixed_white_24dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_black_24dp.png b/src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_black_24dp.png
new file mode 100644
index 000000000..faf2056e6
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_black_24dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_white_24dp.png b/src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_white_24dp.png
new file mode 100644
index 000000000..e4f719dd9
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/ic_gps_not_fixed_white_24dp.png differ
diff --git a/src/main/res/drawable-xxxhdpi/marker.png b/src/main/res/drawable-xxxhdpi/marker.png
new file mode 100644
index 000000000..c0d820b88
Binary files /dev/null and b/src/main/res/drawable-xxxhdpi/marker.png differ
diff --git a/src/main/res/drawable/ic_directions_black_24dp.xml b/src/main/res/drawable/ic_directions_black_24dp.xml
new file mode 100644
index 000000000..739dd20ee
--- /dev/null
+++ b/src/main/res/drawable/ic_directions_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/main/res/drawable/ic_gps_fixed_black_24dp.xml b/src/main/res/drawable/ic_gps_fixed_black_24dp.xml
new file mode 100644
index 000000000..07d6e4694
--- /dev/null
+++ b/src/main/res/drawable/ic_gps_fixed_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/main/res/drawable/ic_gps_not_fixed_black_24dp.xml b/src/main/res/drawable/ic_gps_not_fixed_black_24dp.xml
new file mode 100644
index 000000000..a1e7c4a27
--- /dev/null
+++ b/src/main/res/drawable/ic_gps_not_fixed_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/main/res/drawable/ic_place_black_24dp.xml b/src/main/res/drawable/ic_place_black_24dp.xml
new file mode 100644
index 000000000..e3291a943
--- /dev/null
+++ b/src/main/res/drawable/ic_place_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/src/main/res/layout/activity_share_location.xml b/src/main/res/layout/activity_share_location.xml
new file mode 100644
index 000000000..96e605aa7
--- /dev/null
+++ b/src/main/res/layout/activity_share_location.xml
@@ -0,0 +1,99 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/res/layout/activity_show_location.xml b/src/main/res/layout/activity_show_location.xml
new file mode 100644
index 000000000..5df0c87dd
--- /dev/null
+++ b/src/main/res/layout/activity_show_location.xml
@@ -0,0 +1,25 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/main/res/menu/menu_show_location.xml b/src/main/res/menu/menu_show_location.xml
new file mode 100644
index 000000000..28f80ffa7
--- /dev/null
+++ b/src/main/res/menu/menu_show_location.xml
@@ -0,0 +1,14 @@
+
\ No newline at end of file
diff --git a/src/main/res/values/about.xml b/src/main/res/values/about.xml
index cd5f84f85..68e3138ee 100644
--- a/src/main/res/values/about.xml
+++ b/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.
diff --git a/src/main/res/values/attrs.xml b/src/main/res/values/attrs.xml
index 27a227216..1e6683c12 100644
--- a/src/main/res/values/attrs.xml
+++ b/src/main/res/values/attrs.xml
@@ -66,6 +66,11 @@
+
+
+
+
+
diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml
index 00a8e049f..2f582da8e 100644
--- a/src/main/res/values/strings.xml
+++ b/src/main/res/values/strings.xml
@@ -692,4 +692,12 @@
Large
Message was not encrypted for this device.
undo
+ Location sharing is disabled
+ Fix position
+ Unfix position
+ Copy Location
+ Share Location
+ Directions
+ Share location
+ Show location
diff --git a/src/main/res/values/themes.xml b/src/main/res/values/themes.xml
index d80fb4e9d..0a06a23a3 100644
--- a/src/main/res/values/themes.xml
+++ b/src/main/res/values/themes.xml
@@ -79,6 +79,11 @@
- @drawable/ic_qr_code_scan_white_24dp
- @drawable/ic_scroll_to_end_black
+ - @drawable/ic_gps_not_fixed_black_24dp
+ - @drawable/ic_gps_fixed_black_24dp
+ - @drawable/ic_directions_black_24dp
+ - @drawable/ic_content_copy_white_24dp
+
- @drawable/ic_notifications_black_24dp
- @drawable/ic_notifications_off_black_24dp
- @drawable/ic_notifications_paused_black_24dp
@@ -164,6 +169,11 @@
- @drawable/ic_qr_code_scan_white_24dp
- @drawable/ic_scroll_to_end_white
+ - @drawable/ic_gps_not_fixed_white_24dp
+ - @drawable/ic_gps_fixed_white_24dp
+ - @drawable/ic_directions_white_24dp
+ - @drawable/ic_content_copy_white_24dp
+
- @drawable/ic_notifications_white_24dp
- @drawable/ic_notifications_off_white_24dp
- @drawable/ic_notifications_paused_white_24dp