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 @@ + + + + + + image/svg+xml + + + + + + + + + + + + + + + + + + + + + + + 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 @@ + + + + + + + + + + + + + + +