use internal location viewer for in text geo uris

Daniel Gultsch created

Change summary

src/main/java/eu/siacs/conversations/ui/LocationActivity.java            |   2 
src/main/java/eu/siacs/conversations/ui/ShowLocationActivity.java        | 175 
src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java |  17 
src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java           |  80 
src/main/java/eu/siacs/conversations/ui/util/UriHelper.java              |  15 
src/main/java/eu/siacs/conversations/utils/GeoHelper.java                |  12 
src/main/res/drawable/ic_open_with_24dp.xml                              |   2 
src/main/res/menu/menu_show_location.xml                                 |   8 
8 files changed, 159 insertions(+), 152 deletions(-)

Detailed changes

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

@@ -236,13 +236,11 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
 		}
 	}
 
-	@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(

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

@@ -9,6 +9,7 @@ import android.location.Location;
 import android.location.LocationListener;
 import android.net.Uri;
 import android.os.Bundle;
+import android.util.Log;
 import android.view.Menu;
 import android.view.MenuItem;
 import android.view.View;
@@ -17,11 +18,8 @@ import android.widget.Toast;
 import androidx.annotation.NonNull;
 import androidx.databinding.DataBindingUtil;
 
-import org.osmdroid.util.GeoPoint;
-
-import java.util.HashMap;
-import java.util.regex.Matcher;
-import java.util.regex.Pattern;
+import com.google.common.base.Strings;
+import com.google.common.primitives.Doubles;
 
 import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
@@ -30,8 +28,13 @@ 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;
+import eu.siacs.conversations.utils.GeoHelper;
 import eu.siacs.conversations.utils.LocationProvider;
 
+import org.osmdroid.util.GeoPoint;
+
+import java.util.Map;
+
 public class ShowLocationActivity extends LocationActivity implements LocationListener {
 
     private GeoPoint loc = LocationProvider.FALLBACK;
@@ -56,73 +59,41 @@ public class ShowLocationActivity extends LocationActivity implements LocationLi
         this.binding.fab.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);
-                    }
+        if (intent == null) {
+            return;
+        }
+        final String action = intent.getAction();
+        switch (Strings.nullToEmpty(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 uri = intent.getData();
+                if (uri == null) {
                     break;
-                case Intent.ACTION_VIEW:
-                    final Uri geoUri = intent.getData();
-
-                    // Attempt to set zoom level if the geo URI specifies it
-                    if (geoUri != null) {
-                        final HashMap<String, String> query =
-                                UriHelper.parseQueryString(geoUri.getQuery());
-
-                        // Check for zoom level.
-                        final String z = query.get("z");
-                        if (z != null) {
-                            try {
-                                mapController.setZoom(Double.valueOf(z));
-                            } catch (final Exception ignored) {
-                            }
-                        }
-
-                        // Check for the actual geo query.
-                        boolean posInQuery = false;
-                        final String q = query.get("q");
-                        if (q != null) {
-                            final Pattern latlng =
-                                    Pattern.compile(
-                                            "/^([-+]?[0-9]+(\\.[0-9]+)?),([-+]?[0-9]+(\\.[0-9]+)?)(\\(.*\\))?/");
-                            final Matcher m = latlng.matcher(q);
-                            if (m.matches()) {
-                                try {
-                                    this.loc =
-                                            new GeoPoint(
-                                                    Double.valueOf(m.group(1)),
-                                                    Double.valueOf(m.group(3)));
-                                    posInQuery = true;
-                                } catch (final Exception ignored) {
-                                }
-                            }
-                        }
-
-                        final String schemeSpecificPart = geoUri.getSchemeSpecificPart();
-                        if (schemeSpecificPart != null && !schemeSpecificPart.isEmpty()) {
-                            try {
-                                final GeoPoint latlong =
-                                        LocationHelper.parseLatLong(schemeSpecificPart);
-                                if (latlong != null && !posInQuery) {
-                                    this.loc = latlong;
-                                }
-                            } catch (final NumberFormatException ignored) {
-                            }
-                        }
-                    }
-
+                }
+                final GeoPoint point;
+                try {
+                    point = GeoHelper.parseGeoPoint(uri);
+                } catch (final Exception e) {
                     break;
-            }
-            updateLocationMarkers();
+                }
+                this.loc = point;
+                final Map<String, String> query = UriHelper.parseQueryString(uri.getQuery());
+                final String z = query.get("z");
+                final Double zoom = Strings.isNullOrEmpty(z) ? null : Doubles.tryParse(z);
+                if (zoom != null) {
+                    Log.d(Config.LOGTAG, "inferring zoom level " + zoom + " from geo uri");
+                    mapController.setZoom(zoom);
+                    gotoLoc(false);
+                }
+                break;
         }
+        updateLocationMarkers();
     }
 
     @Override
@@ -173,37 +144,43 @@ public class ShowLocationActivity extends LocationActivity implements LocationLi
 
     @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);
-                    Toast.makeText(this, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT)
-                            .show();
-                }
-                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;
+        final var itemId = item.getItemId();
+        if (itemId == R.id.action_copy_location) {
+            final ClipboardManager clipboard = getSystemService(ClipboardManager.class);
+            final ClipData clip = ClipData.newPlainText("location", createGeoUri().toString());
+            clipboard.setPrimaryClip(clip);
+            Toast.makeText(this, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show();
+            return true;
+        } else if (itemId == 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;
+        } else if (itemId == R.id.action_open_with) {
+            final Intent intent = new Intent(Intent.ACTION_VIEW);
+            intent.setData(createGeoUri());
+            try {
+                startActivity(Intent.createChooser(intent, getText(R.string.open_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;
+
+        } else {
+            return super.onOptionsItemSelected(item);
         }
-        return super.onOptionsItemSelected(item);
     }
 
     private void startNavigation() {

src/main/java/eu/siacs/conversations/ui/adapter/MediaPreviewAdapter.java 🔗

@@ -9,6 +9,7 @@ import android.graphics.drawable.BitmapDrawable;
 import android.graphics.drawable.Drawable;
 import android.net.Uri;
 import android.os.AsyncTask;
+import android.util.Log;
 import android.view.LayoutInflater;
 import android.view.ViewGroup;
 import android.widget.ImageView;
@@ -20,16 +21,19 @@ import androidx.core.widget.ImageViewCompat;
 import androidx.databinding.DataBindingUtil;
 import androidx.recyclerview.widget.RecyclerView;
 
+import eu.siacs.conversations.Config;
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.databinding.ItemMediaPreviewBinding;
 import eu.siacs.conversations.persistance.FileBackend;
 import eu.siacs.conversations.ui.ConversationFragment;
+import eu.siacs.conversations.ui.ShowLocationActivity;
 import eu.siacs.conversations.ui.XmppActivity;
 import eu.siacs.conversations.ui.util.Attachment;
 
 import java.lang.ref.WeakReference;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.Locale;
 import java.util.concurrent.RejectedExecutionException;
 
 public class MediaPreviewAdapter
@@ -73,11 +77,16 @@ public class MediaPreviewAdapter
         holder.binding.mediaPreview.setOnClickListener(v -> view(context, attachment));
     }
 
-    private static void view(final Context context, Attachment attachment) {
+    private static void view(final Context context, final Attachment attachment) {
         final Intent view = new Intent(Intent.ACTION_VIEW);
-        final Uri uri = FileBackend.getUriForUri(context, attachment.getUri());
-        view.setDataAndType(uri, attachment.getMime());
-        view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        if (attachment.getType() == Attachment.Type.LOCATION) {
+            view.setClass(context, ShowLocationActivity.class);
+            view.setData(attachment.getUri());
+        } else {
+            final Uri uri = FileBackend.getUriForUri(context, attachment.getUri());
+            view.setDataAndType(uri, attachment.getMime());
+            view.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+        }
         try {
             context.startActivity(view);
         } catch (final ActivityNotFoundException e) {

src/main/java/eu/siacs/conversations/ui/text/FixedURLSpan.java 🔗

@@ -38,6 +38,7 @@ import android.os.Build;
 import android.text.Editable;
 import android.text.Spanned;
 import android.text.style.URLSpan;
+import android.view.SoundEffectConstants;
 import android.view.View;
 import android.widget.Toast;
 
@@ -45,43 +46,56 @@ import java.util.Arrays;
 
 import eu.siacs.conversations.R;
 import eu.siacs.conversations.ui.ConversationsActivity;
-
+import eu.siacs.conversations.ui.ShowLocationActivity;
 
 @SuppressLint("ParcelCreator")
 public class FixedURLSpan extends URLSpan {
 
-	private FixedURLSpan(String url) {
-		super(url);
-	}
+    private FixedURLSpan(String url) {
+        super(url);
+    }
 
-	public static void fix(final Editable editable) {
-		for (final URLSpan urlspan : editable.getSpans(0, editable.length() - 1, URLSpan.class)) {
-			final int start = editable.getSpanStart(urlspan);
-			final int end = editable.getSpanEnd(urlspan);
-			editable.removeSpan(urlspan);
-			editable.setSpan(new FixedURLSpan(urlspan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
-		}
-	}
+    public static void fix(final Editable editable) {
+        for (final URLSpan urlspan : editable.getSpans(0, editable.length() - 1, URLSpan.class)) {
+            final int start = editable.getSpanStart(urlspan);
+            final int end = editable.getSpanEnd(urlspan);
+            editable.removeSpan(urlspan);
+            editable.setSpan(
+                    new FixedURLSpan(urlspan.getURL()),
+                    start,
+                    end,
+                    Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
+        }
+    }
 
-	@Override
-	public void onClick(View widget) {
-		final Uri uri = Uri.parse(getURL());
-		final Context context = widget.getContext();
-		final boolean candidateToProcessDirectly = "xmpp".equals(uri.getScheme()) || ("https".equals(uri.getScheme()) && "conversations.im".equals(uri.getHost()) && uri.getPathSegments().size() > 1 && Arrays.asList("j","i").contains(uri.getPathSegments().get(0)));
-		if (candidateToProcessDirectly && context instanceof ConversationsActivity) {
-			if (((ConversationsActivity) context).onXmppUriClicked(uri)) {
-				widget.playSoundEffect(0);
-				return;
-			}
-		}
-		final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
-		intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
-		//intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName());
-		try {
-			context.startActivity(intent);
-			widget.playSoundEffect(0);
-		} catch (ActivityNotFoundException e) {
-			Toast.makeText(context, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT).show();
-		}
-	}
+    @Override
+    public void onClick(final View widget) {
+        final Uri uri = Uri.parse(getURL());
+        final Context context = widget.getContext();
+        final boolean candidateToProcessDirectly =
+                "xmpp".equals(uri.getScheme())
+                        || ("https".equals(uri.getScheme())
+                                && "conversations.im".equals(uri.getHost())
+                                && uri.getPathSegments().size() > 1
+                                && Arrays.asList("j", "i").contains(uri.getPathSegments().get(0)));
+        if (candidateToProcessDirectly && context instanceof ConversationsActivity) {
+            if (((ConversationsActivity) context).onXmppUriClicked(uri)) {
+                widget.playSoundEffect(SoundEffectConstants.CLICK);
+                return;
+            }
+        }
+        final Intent intent = new Intent(Intent.ACTION_VIEW, uri);
+        if ("geo".equalsIgnoreCase(uri.getScheme())) {
+            intent.setClass(context, ShowLocationActivity.class);
+        } else {
+            intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
+        }
+        try {
+            context.startActivity(intent);
+            widget.playSoundEffect(SoundEffectConstants.CLICK);
+        } catch (ActivityNotFoundException e) {
+            Toast.makeText(context, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT)
+                    .show();
+        }
+    }
 }

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

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

src/main/java/eu/siacs/conversations/utils/GeoHelper.java 🔗

@@ -46,13 +46,17 @@ public class GeoHelper {
 		}
 	}
 
+	public static GeoPoint parseGeoPoint(final Uri uri) {
+		return parseGeoPoint(uri.toString());
+	}
+
 	private static GeoPoint parseGeoPoint(String body) throws IllegalArgumentException {
-		Matcher matcher = GEO_URI.matcher(body);
+		final Matcher matcher = GEO_URI.matcher(body);
 		if (!matcher.matches()) {
 			throw new IllegalArgumentException("Invalid geo uri");
 		}
-		double latitude;
-		double longitude;
+		final double latitude;
+		final double longitude;
 		try {
 			latitude = Double.parseDouble(matcher.group(1));
 			if (latitude > 90.0 || latitude < -90.0) {
@@ -62,7 +66,7 @@ public class GeoHelper {
 			if (longitude > 180.0 || longitude < -180.0) {
 				throw new IllegalArgumentException("Invalid geo uri");
 			}
-		} catch (NumberFormatException e) {
+		} catch (final NumberFormatException e) {
 			throw new IllegalArgumentException("Invalid geo uri",e);
 		}
 		return new GeoPoint(latitude, longitude);

src/main/res/drawable/ic_content_copy_24dp.xml → src/main/res/drawable/ic_open_with_24dp.xml 🔗

@@ -7,6 +7,6 @@
 
     <path
         android:fillColor="@android:color/white"
-        android:pathData="M16,1L4,1c-1.1,0 -2,0.9 -2,2v14h2L4,3h12L16,1zM19,5L8,5c-1.1,0 -2,0.9 -2,2v14c0,1.1 0.9,2 2,2h11c1.1,0 2,-0.9 2,-2L21,7c0,-1.1 -0.9,-2 -2,-2zM19,21L8,21L8,7h11v14z" />
+        android:pathData="M10,9h4L14,6h3l-5,-5 -5,5h3v3zM9,10L6,10L6,7l-5,5 5,5v-3h3v-4zM23,12l-5,-5v3h-3v4h3v3l5,-5zM14,15h-4v3L7,18l5,5 5,-5h-3v-3z" />
 
 </vector>

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

@@ -6,9 +6,13 @@
         android:showAsAction="ifRoom"
         android:title="@string/action_share_location"
         android:icon="@drawable/ic_share_24dp"/>
+    <item android:id="@+id/action_open_with"
+        app:showAsAction="ifRoom"
+        android:showAsAction="ifRoom"
+        android:title="@string/open_with"
+        android:icon="@drawable/ic_open_with_24dp"/>
     <item android:id="@+id/action_copy_location"
         android:title="@string/action_copy_location"
-        android:icon="@drawable/ic_content_copy_24dp"
-        app:showAsAction="ifRoom"
+        app:showAsAction="never"
         android:showAsAction="ifRoom"/>
 </menu>