use internal location viewer for in text geo uris

This commit is contained in:
Daniel Gultsch 2024-04-15 10:13:04 +02:00
parent a3cc91dc11
commit e950d431bc
No known key found for this signature in database
GPG key ID: F43D18AD2A0982C2
8 changed files with 161 additions and 154 deletions

View file

@ -236,13 +236,11 @@ public abstract class LocationActivity extends ActionBarActivity implements Loca
} }
} }
@TargetApi(Build.VERSION_CODES.M)
protected boolean hasLocationPermissions() { protected boolean hasLocationPermissions() {
return (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED || return (checkSelfPermission(Manifest.permission.ACCESS_FINE_LOCATION) == PackageManager.PERMISSION_GRANTED ||
checkSelfPermission(Manifest.permission.ACCESS_COARSE_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) { protected void requestPermissions(final int request_code) {
if (!hasLocationPermissions()) { if (!hasLocationPermissions()) {
requestPermissions( requestPermissions(

View file

@ -9,6 +9,7 @@ import android.location.Location;
import android.location.LocationListener; import android.location.LocationListener;
import android.net.Uri; import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
@ -17,11 +18,8 @@ import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.databinding.DataBindingUtil; import androidx.databinding.DataBindingUtil;
import org.osmdroid.util.GeoPoint; import com.google.common.base.Strings;
import com.google.common.primitives.Doubles;
import java.util.HashMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import eu.siacs.conversations.Config; import eu.siacs.conversations.Config;
import eu.siacs.conversations.R; 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.util.UriHelper;
import eu.siacs.conversations.ui.widget.Marker; import eu.siacs.conversations.ui.widget.Marker;
import eu.siacs.conversations.ui.widget.MyLocation; import eu.siacs.conversations.ui.widget.MyLocation;
import eu.siacs.conversations.utils.GeoHelper;
import eu.siacs.conversations.utils.LocationProvider; import eu.siacs.conversations.utils.LocationProvider;
import org.osmdroid.util.GeoPoint;
import java.util.Map;
public class ShowLocationActivity extends LocationActivity implements LocationListener { public class ShowLocationActivity extends LocationActivity implements LocationListener {
private GeoPoint loc = LocationProvider.FALLBACK; private GeoPoint loc = LocationProvider.FALLBACK;
@ -56,12 +59,11 @@ public class ShowLocationActivity extends LocationActivity implements LocationLi
this.binding.fab.setOnClickListener(view -> startNavigation()); this.binding.fab.setOnClickListener(view -> startNavigation());
final Intent intent = getIntent(); final Intent intent = getIntent();
if (intent != null) { if (intent == null) {
final String action = intent.getAction();
if (action == null) {
return; return;
} }
switch (action) { final String action = intent.getAction();
switch (Strings.nullToEmpty(action)) {
case "eu.siacs.conversations.location.show": case "eu.siacs.conversations.location.show":
if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) { if (intent.hasExtra("longitude") && intent.hasExtra("latitude")) {
final double longitude = intent.getDoubleExtra("longitude", 0); final double longitude = intent.getDoubleExtra("longitude", 0);
@ -70,60 +72,29 @@ public class ShowLocationActivity extends LocationActivity implements LocationLi
} }
break; break;
case Intent.ACTION_VIEW: case Intent.ACTION_VIEW:
final Uri geoUri = intent.getData(); final Uri uri = intent.getData();
if (uri == null) {
// Attempt to set zoom level if the geo URI specifies it break;
if (geoUri != null) { }
final HashMap<String, String> query = final GeoPoint point;
UriHelper.parseQueryString(geoUri.getQuery()); try {
point = GeoHelper.parseGeoPoint(uri);
// Check for zoom level. } catch (final Exception e) {
break;
}
this.loc = point;
final Map<String, String> query = UriHelper.parseQueryString(uri.getQuery());
final String z = query.get("z"); final String z = query.get("z");
if (z != null) { final Double zoom = Strings.isNullOrEmpty(z) ? null : Doubles.tryParse(z);
try { if (zoom != null) {
mapController.setZoom(Double.valueOf(z)); Log.d(Config.LOGTAG, "inferring zoom level " + zoom + " from geo uri");
} catch (final Exception ignored) { mapController.setZoom(zoom);
gotoLoc(false);
} }
}
// 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; break;
} }
updateLocationMarkers(); updateLocationMarkers();
} }
}
@Override @Override
protected void gotoLoc(final boolean setZoomLevel) { protected void gotoLoc(final boolean setZoomLevel) {
@ -173,19 +144,14 @@ public class ShowLocationActivity extends LocationActivity implements LocationLi
@Override @Override
public boolean onOptionsItemSelected(final MenuItem item) { public boolean onOptionsItemSelected(final MenuItem item) {
switch (item.getItemId()) { final var itemId = item.getItemId();
case R.id.action_copy_location: if (itemId == R.id.action_copy_location) {
final ClipboardManager clipboard = final ClipboardManager clipboard = getSystemService(ClipboardManager.class);
(ClipboardManager) getSystemService(CLIPBOARD_SERVICE); final ClipData clip = ClipData.newPlainText("location", createGeoUri().toString());
if (clipboard != null) {
final ClipData clip =
ClipData.newPlainText("location", createGeoUri().toString());
clipboard.setPrimaryClip(clip); clipboard.setPrimaryClip(clip);
Toast.makeText(this, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT) Toast.makeText(this, R.string.url_copied_to_clipboard, Toast.LENGTH_SHORT).show();
.show();
}
return true; return true;
case R.id.action_share_location: } else if (itemId == R.id.action_share_location) {
final Intent shareIntent = new Intent(); final Intent shareIntent = new Intent();
shareIntent.setAction(Intent.ACTION_SEND); shareIntent.setAction(Intent.ACTION_SEND);
shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString()); shareIntent.putExtra(Intent.EXTRA_TEXT, createGeoUri().toString());
@ -195,16 +161,27 @@ public class ShowLocationActivity extends LocationActivity implements LocationLi
} catch (final ActivityNotFoundException e) { } catch (final ActivityNotFoundException e) {
// This should happen only on faulty androids because normally chooser is always // This should happen only on faulty androids because normally chooser is always
// available // available
Toast.makeText( Toast.makeText(this, R.string.no_application_found_to_open_file, Toast.LENGTH_SHORT)
this,
R.string.no_application_found_to_open_file,
Toast.LENGTH_SHORT)
.show(); .show();
} }
return true; 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() { private void startNavigation() {
final Intent intent = getStartNavigationIntent(); final Intent intent = getStartNavigationIntent();

View file

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

View file

@ -38,6 +38,7 @@ import android.os.Build;
import android.text.Editable; import android.text.Editable;
import android.text.Spanned; import android.text.Spanned;
import android.text.style.URLSpan; import android.text.style.URLSpan;
import android.view.SoundEffectConstants;
import android.view.View; import android.view.View;
import android.widget.Toast; import android.widget.Toast;
@ -45,7 +46,7 @@ import java.util.Arrays;
import eu.siacs.conversations.R; import eu.siacs.conversations.R;
import eu.siacs.conversations.ui.ConversationsActivity; import eu.siacs.conversations.ui.ConversationsActivity;
import eu.siacs.conversations.ui.ShowLocationActivity;
@SuppressLint("ParcelCreator") @SuppressLint("ParcelCreator")
public class FixedURLSpan extends URLSpan { public class FixedURLSpan extends URLSpan {
@ -59,29 +60,42 @@ public class FixedURLSpan extends URLSpan {
final int start = editable.getSpanStart(urlspan); final int start = editable.getSpanStart(urlspan);
final int end = editable.getSpanEnd(urlspan); final int end = editable.getSpanEnd(urlspan);
editable.removeSpan(urlspan); editable.removeSpan(urlspan);
editable.setSpan(new FixedURLSpan(urlspan.getURL()), start, end, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); editable.setSpan(
new FixedURLSpan(urlspan.getURL()),
start,
end,
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
} }
} }
@Override @Override
public void onClick(View widget) { public void onClick(final View widget) {
final Uri uri = Uri.parse(getURL()); final Uri uri = Uri.parse(getURL());
final Context context = widget.getContext(); 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))); 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 (candidateToProcessDirectly && context instanceof ConversationsActivity) {
if (((ConversationsActivity) context).onXmppUriClicked(uri)) { if (((ConversationsActivity) context).onXmppUriClicked(uri)) {
widget.playSoundEffect(0); widget.playSoundEffect(SoundEffectConstants.CLICK);
return; return;
} }
} }
final Intent intent = new Intent(Intent.ACTION_VIEW, uri); 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); intent.setFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT);
//intent.putExtra(Browser.EXTRA_APPLICATION_ID, context.getPackageName()); }
try { try {
context.startActivity(intent); context.startActivity(intent);
widget.playSoundEffect(0); widget.playSoundEffect(SoundEffectConstants.CLICK);
} catch (ActivityNotFoundException e) { } catch (ActivityNotFoundException e) {
Toast.makeText(context, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT).show(); Toast.makeText(context, R.string.no_application_found_to_open_link, Toast.LENGTH_SHORT)
.show();
} }
} }
} }

View file

@ -1,6 +1,8 @@
package eu.siacs.conversations.ui.util; 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. * Helper methods for parsing URI's.
@ -12,19 +14,18 @@ public final class UriHelper {
* @param q The query string to split. * @param q The query string to split.
* @return A hashmap containing the key-value pairs from the query string. * @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()) { if (q == null || q.isEmpty()) {
return null; return ImmutableMap.of();
} }
final ImmutableMap.Builder<String,String> queryMapBuilder = new ImmutableMap.Builder<>();
final String[] query = q.split("&"); 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) { for (final String param : query) {
final String[] pair = param.split("="); 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();
} }
} }

View file

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

View file

@ -7,6 +7,6 @@
<path <path
android:fillColor="@android:color/white" 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> </vector>

View file

@ -6,9 +6,13 @@
android:showAsAction="ifRoom" android:showAsAction="ifRoom"
android:title="@string/action_share_location" android:title="@string/action_share_location"
android:icon="@drawable/ic_share_24dp"/> 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" <item android:id="@+id/action_copy_location"
android:title="@string/action_copy_location" android:title="@string/action_copy_location"
android:icon="@drawable/ic_content_copy_24dp" app:showAsAction="never"
app:showAsAction="ifRoom"
android:showAsAction="ifRoom"/> android:showAsAction="ifRoom"/>
</menu> </menu>