diff --git a/src/androidTest/java/de/blau/android/photos/PhotosTest.java b/src/androidTest/java/de/blau/android/photos/PhotosTest.java index e77ea379ae..6c1a770998 100644 --- a/src/androidTest/java/de/blau/android/photos/PhotosTest.java +++ b/src/androidTest/java/de/blau/android/photos/PhotosTest.java @@ -128,12 +128,17 @@ public void teardown() { */ // @SdkSuppress(minSdkVersion = 26) @Test - public void selectDisplayDelete() { + public void selectDisplayInfoDelete() { addLayerAndIndex(); TestUtils.unlock(device); assertEquals(2, App.getPhotoIndex().count()); TestUtils.clickAtCoordinates(device, main.getMap(), 7.5886112, 47.5519448, true); + TestUtils.clickMenuButton(device, context.getString(R.string.menu_information), false, true); + assertTrue(TestUtils.findText(device, false, context.getString(R.string.image_information_title))); + TestUtils.sleep(20000); + assertTrue(TestUtils.clickText(device, false, context.getString(R.string.done),true, false)); + TestUtils.clickMenuButton(device, context.getString(R.string.delete), false, true); assertTrue(TestUtils.clickText(device, false, context.getString(R.string.photo_viewer_delete_button), true, false)); diff --git a/src/main/java/de/blau/android/Main.java b/src/main/java/de/blau/android/Main.java index d40b860822..ffdb31bcf4 100644 --- a/src/main/java/de/blau/android/Main.java +++ b/src/main/java/de/blau/android/Main.java @@ -1029,7 +1029,7 @@ private void processIntents() { Uri uri = intent.getData(); if (!index.deletePhoto(this, uri)) { String path = ContentResolverUtil.getPath(this, uri); - if (path != null && !index.deletePhoto(this, path)) { + if (path == null || !index.deletePhoto(this, path)) { Log.e(DEBUG_TAG, "deleting " + uri + " from index failed"); } } diff --git a/src/main/java/de/blau/android/contract/Urls.java b/src/main/java/de/blau/android/contract/Urls.java index df0d051beb..7758a854bc 100644 --- a/src/main/java/de/blau/android/contract/Urls.java +++ b/src/main/java/de/blau/android/contract/Urls.java @@ -29,7 +29,7 @@ private Urls() { public static final String DEFAULT_OAM_SERVER = "https://api.openaerialmap.org/"; // these are only configurable for testing - public static final String DEFAULT_MAPILLARY_IMAGES_V4 = "https://graph.mapillary.com/%s?access_token=%s&fields=thumb_2048_url,computed_geometry"; + public static final String DEFAULT_MAPILLARY_IMAGES_V4 = "https://graph.mapillary.com/%s?access_token=%s&fields=thumb_2048_url,computed_geometry,computed_compass_angle,captured_at"; public static final String DEFAULT_MAPILLARY_SEQUENCES_URL_V4 = "https://graph.mapillary.com/image_ids?sequence_id=%s&access_token=%s&fields=id"; public static final String DEFAULT_OSM_WIKI = "https://wiki.openstreetmap.org/"; diff --git a/src/main/java/de/blau/android/dialogs/ElementInfo.java b/src/main/java/de/blau/android/dialogs/ElementInfo.java index c07a311a37..7001693863 100644 --- a/src/main/java/de/blau/android/dialogs/ElementInfo.java +++ b/src/main/java/de/blau/android/dialogs/ElementInfo.java @@ -25,7 +25,6 @@ import android.text.SpannedString; import android.text.style.StyleSpan; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ScrollView; @@ -60,7 +59,6 @@ import de.blau.android.util.DateFormatter; import de.blau.android.util.InfoDialogFragment; import de.blau.android.util.ScreenMessage; -import de.blau.android.util.ThemeUtils; import de.blau.android.util.Util; import de.blau.android.validation.Validator; @@ -264,24 +262,16 @@ public Dialog onCreateDialog(Bundle savedInstanceState) { return builder.create(); } - /** - * Create the view we want to display - * - * @param container parent view or null - * @return the View - */ @Override protected View createView(ViewGroup container) { - FragmentActivity activity = getActivity(); - LayoutInflater themedInflater = ThemeUtils.getLayoutInflater(activity); - ScrollView sv = (ScrollView) themedInflater.inflate(R.layout.element_info_view, container, false); + ScrollView sv = createEmptyView(container); TableLayout tl = (TableLayout) sv.findViewById(R.id.element_info_vertical_layout); + TableLayout.LayoutParams tp = getTableLayoutParams(); boolean compare = ue != null; - TableLayout.LayoutParams tp = new TableLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - tp.setMargins(10, 2, 10, 2); if (element != null) { + FragmentActivity activity = getActivity(); boolean deleted = element.getState() == OsmElement.STATE_DELETED; tl.setColumnStretchable(1, true); tl.setColumnStretchable(2, true); diff --git a/src/main/java/de/blau/android/dialogs/FeatureInfo.java b/src/main/java/de/blau/android/dialogs/FeatureInfo.java index 3595462ff9..2d2c351aff 100644 --- a/src/main/java/de/blau/android/dialogs/FeatureInfo.java +++ b/src/main/java/de/blau/android/dialogs/FeatureInfo.java @@ -1,7 +1,6 @@ package de.blau.android.dialogs; import java.util.List; -import java.util.Locale; import java.util.Set; import com.google.gson.JsonElement; @@ -12,9 +11,7 @@ import android.app.Dialog; import android.os.Bundle; -import android.text.Spanned; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ImageButton; @@ -167,15 +164,12 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) { @Override protected View createView(@Nullable ViewGroup container) { - FragmentActivity activity = getActivity(); - LayoutInflater inflater = ThemeUtils.getLayoutInflater(activity); - ScrollView sv = (ScrollView) inflater.inflate(R.layout.element_info_view, container, false); + ScrollView sv = createEmptyView(container); TableLayout tl = (TableLayout) sv.findViewById(R.id.element_info_vertical_layout); - - TableLayout.LayoutParams tp = new TableLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - tp.setMargins(10, 2, 10, 2); + TableLayout.LayoutParams tp = getTableLayoutParams(); if (feature != null) { + FragmentActivity activity = getActivity(); tl.setColumnShrinkable(1, true); Geometry geometry = feature.geometry(); if (geometry != null) { @@ -196,8 +190,8 @@ protected View createView(@Nullable ViewGroup container) { Util.sharePosition(getActivity(), new double[] { p.longitude(), p.latitude() }, null); }); - tl.addView(TableLayoutUtils.createRowWithButton(activity, R.string.location_lon_label, prettyPrint(p.longitude()), button, tp)); - tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lat_label, prettyPrint(p.latitude()), tp)); + tl.addView(TableLayoutUtils.createRowWithButton(activity, R.string.location_lon_label, prettyPrintCoord(p.longitude()), button, tp)); + tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lat_label, prettyPrintCoord(p.latitude()), tp)); } } tl.addView(TableLayoutUtils.divider(activity)); @@ -220,25 +214,4 @@ protected View createView(@Nullable ViewGroup container) { } return sv; } - - /** - * Get the string resource formated as an italic string - * - * @param resId String resource id - * @return a Spanned containing the string - */ - private Spanned toItalic(int resId) { - return Util.fromHtml("" + getString(resId) + ""); - } - - /** - * Pretty print a coordinate value - * - * @param coord the coordinate in WGS84 - * @return a reasonable looking string representation - */ - @NonNull - private static String prettyPrint(double coord) { - return String.format(Locale.US, "%.7f", coord) + "°"; - } } diff --git a/src/main/java/de/blau/android/dialogs/GnssPositionInfo.java b/src/main/java/de/blau/android/dialogs/GnssPositionInfo.java index 5f8fb60f82..aea5147a0e 100644 --- a/src/main/java/de/blau/android/dialogs/GnssPositionInfo.java +++ b/src/main/java/de/blau/android/dialogs/GnssPositionInfo.java @@ -9,7 +9,6 @@ import android.location.LocationManager; import android.os.Bundle; import android.util.Log; -import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; import android.widget.ScrollView; @@ -35,7 +34,6 @@ import de.blau.android.util.GeoMath; import de.blau.android.util.InfoDialogFragment; import de.blau.android.util.ScreenMessage; -import de.blau.android.util.ThemeUtils; import de.blau.android.util.Util; /** @@ -251,18 +249,13 @@ public void run() { @Override protected View createView(@Nullable ViewGroup container) { - LayoutInflater inflater; - FragmentActivity activity = getActivity(); - inflater = ThemeUtils.getLayoutInflater(activity); - ScrollView sv = (ScrollView) inflater.inflate(R.layout.element_info_view, container, false); + ScrollView sv = createEmptyView(container); tl = (TableLayout) sv.findViewById(R.id.element_info_vertical_layout); - - tp = new TableLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); - tp.setMargins(10, 2, 10, 2); + tp = getTableLayoutParams(); if (location != null) { tl.setColumnShrinkable(1, true); - updateView(activity, tl, tp); + updateView(getActivity(), tl, tp); tl.postDelayed(update, 1000); } return sv; diff --git a/src/main/java/de/blau/android/dialogs/ImageInfo.java b/src/main/java/de/blau/android/dialogs/ImageInfo.java new file mode 100644 index 0000000000..1346d4c3f5 --- /dev/null +++ b/src/main/java/de/blau/android/dialogs/ImageInfo.java @@ -0,0 +1,142 @@ +package de.blau.android.dialogs; + +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; + +import android.net.Uri; +import android.os.Bundle; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; +import android.widget.ScrollView; +import android.widget.TableLayout; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.appcompat.app.AlertDialog; +import androidx.appcompat.app.AlertDialog.Builder; +import androidx.appcompat.app.AppCompatDialog; +import androidx.fragment.app.FragmentActivity; +import androidx.fragment.app.FragmentManager; +import de.blau.android.R; +import de.blau.android.listener.DoNothingListener; +import de.blau.android.photos.Photo; +import de.blau.android.util.ContentResolverUtil; +import de.blau.android.util.DateFormatter; +import de.blau.android.util.InfoDialogFragment; + +/** + * Very simple dialog fragment to display some info on a GeoJSON element + * + * @author simon + * + */ +public class ImageInfo extends InfoDialogFragment { + + private static final String DEBUG_TAG = ImageInfo.class.getName(); + + private static final String URI_KEY = "uri"; + + private static final String TAG = "fragment_image_info"; + + private Uri uri = null; + private SimpleDateFormat dateFormat = DateFormatter.getUtcFormat("yyyy-MM-dd HH:mm:ssZ"); + + /** + * Show an info dialog for an image + * + * @param activity the calling Activity + * @param uriString the uri of the image as a string + */ + public static void showDialog(@NonNull FragmentActivity activity, @NonNull String uriString) { + dismissDialog(activity); + try { + FragmentManager fm = activity.getSupportFragmentManager(); + ImageInfo elementInfoFragment = newInstance(uriString); + elementInfoFragment.show(fm, TAG); + } catch (IllegalStateException isex) { + Log.e(DEBUG_TAG, "showDialog", isex); + } + } + + /** + * Dismiss the dialog + * + * @param activity the calling Activity + */ + private static void dismissDialog(@NonNull FragmentActivity activity) { + de.blau.android.dialogs.Util.dismissDialog(activity, TAG); + } + + /** + * Create a new instance of the FeatureInfo dialog + * + * @param feature Feature to display the info on + * + * @return an instance of ElementInfo + */ + @NonNull + private static ImageInfo newInstance(@NonNull String uriString) { + ImageInfo f = new ImageInfo(); + + Bundle args = new Bundle(); + args.putString(URI_KEY, uriString); + + f.setArguments(args); + f.setShowsDialog(true); + + return f; + } + + @Override + public AppCompatDialog onCreateDialog(Bundle savedInstanceState) { + String uriString = getArguments().getString(URI_KEY); + try { + uri = Uri.parse(uriString); + } catch (Exception e) { + Log.e(DEBUG_TAG, "Unable to parse uri " + uriString); + } + Builder builder = new AlertDialog.Builder(getActivity()); + DoNothingListener doNothingListener = new DoNothingListener(); + builder.setPositiveButton(R.string.done, doNothingListener); + builder.setTitle(R.string.image_information_title); + builder.setView(createView(null)); + return builder.create(); + } + + @Override + protected View createView(@Nullable ViewGroup container) { + ScrollView sv = createEmptyView(container); + TableLayout tl = (TableLayout) sv.findViewById(R.id.element_info_vertical_layout); + TableLayout.LayoutParams tp = getTableLayoutParams(); + + if (uri != null) { + tl.setColumnShrinkable(1, true); + try { + FragmentActivity activity = getActivity(); + Photo image = new Photo(getContext(), uri, null); + tl.addView(TableLayoutUtils.createRow(activity, ContentResolverUtil.getPath(getContext(), uri), null, tp)); + long size = ContentResolverUtil.getSizeColumn(getContext(), uri); + if (size > -1) { + tl.addView(TableLayoutUtils.createRow(activity, R.string.file_size, getString(R.string.file_size_kB, size / 1024), tp)); + } + String creator = image.getCreator(); + if (creator != null) { + tl.addView(TableLayoutUtils.createRow(activity, R.string.created_by, creator, tp)); + } + Long captureDate = image.getCaptureDate(); + if (captureDate != null) { + tl.addView(TableLayoutUtils.createRow(activity, R.string.capture_date, dateFormat.format(new Date(captureDate)), tp)); + } + tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lon_label, prettyPrintCoord(image.getLon() / 1E7D), tp)); + tl.addView(TableLayoutUtils.createRow(activity, R.string.location_lat_label, prettyPrintCoord(image.getLat() / 1E7D), tp)); + if (image.hasDirection()) { + tl.addView(TableLayoutUtils.createRow(activity, R.string.direction, String.format(Locale.US, "%3d°", image.getDirection()), tp)); + } + } catch (Exception ex) { + Log.e(DEBUG_TAG, "Exception displaying image meta data " + ex.getMessage()); + } + } + return sv; + } +} diff --git a/src/main/java/de/blau/android/dialogs/LayerInfo.java b/src/main/java/de/blau/android/dialogs/LayerInfo.java index 4f7775b2d0..9d77aa3c66 100644 --- a/src/main/java/de/blau/android/dialogs/LayerInfo.java +++ b/src/main/java/de/blau/android/dialogs/LayerInfo.java @@ -1,13 +1,8 @@ package de.blau.android.dialogs; import android.os.Bundle; -import android.text.Spanned; import android.util.Log; -import android.view.LayoutInflater; -import android.view.ViewGroup; -import android.widget.ScrollView; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; import androidx.appcompat.app.AlertDialog.Builder; import androidx.appcompat.app.AppCompatDialog; @@ -16,8 +11,6 @@ import de.blau.android.R; import de.blau.android.listener.DoNothingListener; import de.blau.android.util.InfoDialogFragment; -import de.blau.android.util.ThemeUtils; -import de.blau.android.util.Util; /** * A generic dialog fragment to display some info on layers @@ -65,27 +58,4 @@ public AppCompatDialog onCreateDialog(Bundle savedInstanceState) { builder.setView(createView(null)); return builder.create(); } - - /** - * Create the view we want to display - * - * Classes extending LayerInfo need to override this but call through to the super method to get the view - * - * @param container parent view or null - * @return the View - */ - protected ScrollView createEmptyView(@Nullable ViewGroup container) { - LayoutInflater inflater = ThemeUtils.getLayoutInflater(getActivity()); - return (ScrollView) inflater.inflate(R.layout.element_info_view, container, false); - } - - /** - * Get the string resource formated as an italic string - * - * @param resId String resource id - * @return a Spanned containing the string - */ - protected Spanned toItalic(int resId) { - return Util.fromHtml("" + getString(resId) + ""); - } } diff --git a/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java b/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java index f9511bc482..b5300c9aad 100644 --- a/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java +++ b/src/main/java/de/blau/android/layer/mapillary/MapillaryLoader.java @@ -22,21 +22,23 @@ import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonParser; +import com.google.gson.JsonPrimitive; import android.content.Context; import android.content.Intent; import android.net.Uri; import android.util.Log; import androidx.annotation.NonNull; -import androidx.annotation.Nullable; import androidx.core.content.FileProvider; import androidx.exifinterface.media.ExifInterface; +import androidx.fragment.app.FragmentActivity; import de.blau.android.App; import de.blau.android.Main; import de.blau.android.R; import de.blau.android.contract.FileExtensions; import de.blau.android.contract.MimeTypes; import de.blau.android.contract.Schemes; +import de.blau.android.dialogs.ImageInfo; import de.blau.android.osm.OsmXml; import de.blau.android.util.ExecutorTask; import de.blau.android.util.FileUtil; @@ -55,9 +57,11 @@ class MapillaryLoader extends ImageLoader { private static final int IMAGERY_LOAD_THREADS = 3; - private static final String COORDINATES_FIELD = "coordinates"; - private static final String COMPUTED_GEOMETRY_FIELD = "computed_geometry"; - private static final String THUMB_2048_URL_FIELD = "thumb_2048_url"; + private static final String COORDINATES_FIELD = "coordinates"; + private static final String COMPUTED_GEOMETRY_FIELD = "computed_geometry"; + private static final String COMPUTED_COMPASS_ANGLE_FIELD = "computed_compass_angle"; + private static final String CAPTURED_AT_FIELD = "captured_at"; + private static final String THUMB_2048_URL_FIELD = "thumb_2048_url"; private static final String JPG = "." + FileExtensions.JPG; @@ -118,22 +122,20 @@ public void load(SubsamplingScaleImageView view, String key) { throw new IOException("Download of " + key + " failed with " + mapillaryCallResponse.code() + " " + mapillaryCallResponse.message()); } try (ResponseBody responseBody = mapillaryCallResponse.body(); InputStream inputStream = responseBody.byteStream()) { - if (inputStream != null) { - JsonElement root = JsonParser.parseReader(new BufferedReader(new InputStreamReader(inputStream, Charset.forName(OsmXml.UTF_8)))); - if (root.isJsonObject() && ((JsonObject) root).has(THUMB_2048_URL_FIELD)) { - loadImage(key, imageFile, client, ((JsonObject) root).get(COMPUTED_GEOMETRY_FIELD), - ((JsonObject) root).get(THUMB_2048_URL_FIELD).getAsString()); - } else { - throw new IOException("Unexpected / missing response"); - } + if (inputStream == null) { + throw new IOException("No InputStream"); } + JsonElement root = JsonParser.parseReader(new BufferedReader(new InputStreamReader(inputStream, Charset.forName(OsmXml.UTF_8)))); + if (!root.isJsonObject() || !((JsonObject) root).has(THUMB_2048_URL_FIELD)) { + throw new IOException("Unexpected / missing response"); + } + loadImage(key, imageFile, client, (JsonObject) root, ((JsonObject) root).get(THUMB_2048_URL_FIELD).getAsString()); } + setImage(view, imageFile); + pruneCache(); } catch (IOException e) { Log.e(DEBUG_TAG, e.getMessage()); - return; } - setImage(view, imageFile); - pruneCache(); }); } catch (RejectedExecutionException rjee) { Log.e(DEBUG_TAG, "Execution rejected " + rjee.getMessage()); @@ -163,33 +165,48 @@ protected Void doInBackground(Void arg) { * @param url image url * @throws IOException if download or writing has issues */ - private void loadImage(@NonNull String key, @NonNull File imageFile, @NonNull OkHttpClient client, @Nullable JsonElement point, @NonNull String url) + private void loadImage(@NonNull String key, @NonNull File imageFile, @NonNull OkHttpClient client, JsonObject meta, @NonNull String url) throws IOException { Request request = new Request.Builder().url(url).build(); Response response = client.newCall(request).execute(); - if (response.isSuccessful()) { - try (ResponseBody responseBody = response.body(); InputStream inputStream = responseBody.byteStream()) { - if (inputStream != null) { - try (FileOutputStream fileOutput = new FileOutputStream(imageFile)) { - byte[] buffer = new byte[1024]; - int bufferLength = 0; - while ((bufferLength = inputStream.read(buffer)) > 0) { - fileOutput.write(buffer, 0, bufferLength); - } - } - if (point instanceof JsonObject && imageFile.length() > 0) { - JsonElement coords = ((JsonObject) point).get(COORDINATES_FIELD); - if (coords instanceof JsonArray && ((JsonArray) coords).size() == 2) { - ExifInterface exif = new ExifInterface(imageFile); - double lat = ((JsonArray) coords).get(1).getAsDouble(); - double lon = ((JsonArray) coords).get(0).getAsDouble(); - exif.setLatLong(lat, lon); - exif.saveAttributes(); - coordinates.put(key, new double[] { lat, lon }); - } - } + if (!response.isSuccessful()) { + throw new IOException("Download failed " + response.message()); + } + try (ResponseBody responseBody = response.body(); InputStream inputStream = responseBody.byteStream()) { + if (inputStream == null) { + throw new IOException("Download failed no InputStream"); + } + try (FileOutputStream fileOutput = new FileOutputStream(imageFile)) { + byte[] buffer = new byte[1024]; + int bufferLength = 0; + while ((bufferLength = inputStream.read(buffer)) > 0) { + fileOutput.write(buffer, 0, bufferLength); } } + JsonElement point = meta.get(COMPUTED_GEOMETRY_FIELD); + if (!(point instanceof JsonObject) || imageFile.length() == 0) { + throw new IOException("No geometry for image or image empty"); + } + JsonElement coords = ((JsonObject) point).get(COORDINATES_FIELD); + if (!(coords instanceof JsonArray) || ((JsonArray) coords).size() != 2) { + throw new IOException("No geometry for image"); + } + ExifInterface exif = new ExifInterface(imageFile); + double lat = ((JsonArray) coords).get(1).getAsDouble(); + double lon = ((JsonArray) coords).get(0).getAsDouble(); + exif.setLatLong(lat, lon); + JsonElement angleElement = meta.get(COMPUTED_COMPASS_ANGLE_FIELD); + if (angleElement instanceof JsonPrimitive) { + float angle = angleElement.getAsFloat(); + exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION, Integer.toString((int) (angle * 100)) + "/100"); + exif.setAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION_REF, ExifInterface.GPS_DIRECTION_MAGNETIC); + } + JsonElement capturedAt = meta.get(CAPTURED_AT_FIELD); + if (capturedAt instanceof JsonPrimitive) { + exif.setDateTime(capturedAt.getAsLong()); + } + exif.saveAttributes(); + coordinates.put(key, new double[] { lat, lon }); } } @@ -231,4 +248,16 @@ public void share(Context context, String key) { ScreenMessage.toastTopError(context, context.getString(R.string.toast_error_accessing_photo, key)); } } + + @Override + public boolean supportsInfo() { + return true; + } + + @Override + public void info(@NonNull FragmentActivity activity, @NonNull String uri) { + Uri f = FileProvider.getUriForFile(activity, activity.getString(R.string.content_provider), new File(cacheDir, uri + JPG)); + ImageInfo.showDialog(activity, f.toString()); + + } } diff --git a/src/main/java/de/blau/android/photos/Photo.java b/src/main/java/de/blau/android/photos/Photo.java index 5a53103e72..cf1857e50a 100644 --- a/src/main/java/de/blau/android/photos/Photo.java +++ b/src/main/java/de/blau/android/photos/Photo.java @@ -43,6 +43,9 @@ public class Photo implements BoundedObject, GeoPoint, Serializable { private int direction = 0; private String directionRef = null; // if null direction not present + private Long captureDate = null; + private String creator = null; + /** * Construct a Photo object from an Uri * @@ -139,6 +142,8 @@ private Photo(@NonNull ExifInterface exif, @NonNull String ref, @Nullable String directionRef = exif.getAttribute(ExifInterface.TAG_GPS_IMG_DIRECTION_REF); Log.d(DEBUG_TAG, "dir " + dir + " direction " + direction + " ref " + directionRef); } + captureDate = exif.getDateTime(); + creator = exif.getAttribute(ExifInterface.TAG_ARTIST); } /** @@ -263,6 +268,7 @@ public String getRef() { * * @return a name for human consumption */ + @Nullable public String getDisplayName() { return displayName != null && !"".equals(displayName) ? displayName : Uri.parse(ref).getLastPathSegment(); } @@ -285,6 +291,22 @@ public int getDirection() { return direction; } + /** + * @return the capture date + */ + @Nullable + public Long getCaptureDate() { + return captureDate; + } + + /** + * @return the creator of the image + */ + @Nullable + public String getCreator() { + return creator; + } + /** * For the BoundedObject interface */ diff --git a/src/main/java/de/blau/android/photos/PhotoViewerFragment.java b/src/main/java/de/blau/android/photos/PhotoViewerFragment.java index edfc8ed428..2953983263 100644 --- a/src/main/java/de/blau/android/photos/PhotoViewerFragment.java +++ b/src/main/java/de/blau/android/photos/PhotoViewerFragment.java @@ -33,6 +33,7 @@ import de.blau.android.Map; import de.blau.android.R; import de.blau.android.contract.Ui; +import de.blau.android.dialogs.ImageInfo; import de.blau.android.listener.DoNothingListener; import de.blau.android.util.ImageLoader; import de.blau.android.util.ImagePagerAdapter; @@ -61,8 +62,9 @@ public class PhotoViewerFragment extends SizedDynamicImmersiveDialogFragment imp private static final int MENUITEM_BACK = 0; private static final int MENUITEM_SHARE = 1; private static final int MENUITEM_GOTO = 2; - private static final int MENUITEM_DELETE = 3; - private static final int MENUITEM_FORWARD = 4; + private static final int MENUITEM_INFO = 3; + private static final int MENUITEM_DELETE = 4; + private static final int MENUITEM_FORWARD = 5; private List photoList = null; @@ -185,6 +187,78 @@ public void share(Context context, String uri) { getDialog().dismiss(); } } + + @Override + public boolean supportsInfo() { + return true; + } + + @Override + public void info(@NonNull FragmentActivity activity, @NonNull String uri) { + ImageInfo.showDialog(activity, uri); + } + + @Override + public boolean supportsDelete() { + return true; + } + + @Override + public void delete(@NonNull Context context, @NonNull String uri) { + new AlertDialog.Builder(getContext()).setTitle(R.string.photo_viewer_delete_title) + .setPositiveButton(R.string.photo_viewer_delete_button, (dialog, which) -> { + if (viewPager == null) { + return; + } + int position = getCurrentPosition(); + final int size = photoList.size(); + if (position >= 0 && position < size) { // avoid crashes from bouncing + Uri photoUri = Uri.parse(photoList.get(position)); + try { + if (getShowsDialog()) { + // delete from in memory and on device index + try (PhotoIndex index = new PhotoIndex(getContext())) { + index.deletePhoto(getContext(), photoUri); + } + Map map = (context instanceof Main) ? ((Main) context).getMap() : null; + final de.blau.android.layer.photos.MapOverlay overlay = map != null ? map.getPhotoLayer() : null; + if (overlay != null) { + // as the Photo was selected before calling this it will still have a + // reference in the layer + overlay.deselectObjects(); + overlay.invalidate(); + } + } else { + Intent intent = new Intent(getContext(), Main.class); + intent.setAction(Main.ACTION_DELETE_PHOTO); + intent.setData(photoUri); + getContext().startActivity(intent); + } + // actually delete + if (getContext().getContentResolver().delete(photoUri, null, null) >= 1) { + photoList.remove(position); + position = Math.min(position, size - 1); // this will set pos to -1 if + // empty, + // but we will exit in that case in any case + if (getShowsDialog() && photoList.isEmpty()) { + // in fragment mode we want to stay around + getDialog().dismiss(); + } else { + photoPagerAdapter.notifyDataSetChanged(); + viewPager.setCurrentItem(position); + if (size == 1 && itemBackward != null && itemForward != null) { + itemBackward.setEnabled(false); + itemForward.setEnabled(false); + } + } + } + } catch (java.lang.SecurityException sex) { + Log.e(DEBUG_TAG, "Error deleting: " + sex.getMessage() + " " + sex.getClass().getName()); + ScreenMessage.toastTopError(getContext(), getString(R.string.toast_permission_denied, sex.getMessage())); + } + } + }).setNeutralButton(R.string.cancel, null).show(); + } }; /** @@ -243,7 +317,11 @@ private View createView(@Nullable Bundle savedInstanceState) { menu.add(Menu.NONE, MENUITEM_SHARE, Menu.NONE, R.string.share).setIcon(R.drawable.ic_share_white_36dp).setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); menu.add(Menu.NONE, MENUITEM_GOTO, Menu.NONE, R.string.photo_viewer_goto).setIcon(R.drawable.ic_map_white_36dp) .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); - if (getString(R.string.content_provider).equals(Uri.parse(photoList.get(startPos)).getAuthority())) { + if (photoLoader.supportsInfo()) { + menu.add(Menu.NONE, MENUITEM_INFO, Menu.NONE, R.string.menu_information).setIcon(R.drawable.outline_info_white_48dp) + .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); + } + if (photoLoader.supportsDelete() && getString(R.string.content_provider).equals(Uri.parse(photoList.get(startPos)).getAuthority())) { // we can only delete stuff that is provided by our provider, currently this is a bit of a hack menu.add(Menu.NONE, MENUITEM_DELETE, Menu.NONE, R.string.delete).setIcon(R.drawable.ic_delete_forever_white_36dp) .setShowAsAction(MenuItem.SHOW_AS_ACTION_ALWAYS); @@ -303,93 +381,50 @@ public boolean onMenuItemClick(MenuItem item) { int size = photoList.size(); FragmentActivity caller = getActivity(); int pos = getCurrentPosition(); - if (photoList != null && !photoList.isEmpty() && pos < size) { - try { - switch (item.getItemId()) { - case MENUITEM_BACK: - pos = pos - 1; - if (pos == -1) { - pos = size - 1; - } - viewPager.setCurrentItem(pos); - break; - case MENUITEM_FORWARD: - pos = (pos + 1) % size; - viewPager.setCurrentItem(pos); - break; - case MENUITEM_GOTO: - if (photoLoader != null) { - photoLoader.showOnMap(caller, pos); - } - break; - case MENUITEM_SHARE: - if (photoLoader != null) { - photoLoader.share(caller, photoList.get(pos)); - } - break; - case MENUITEM_DELETE: - // TODO This is not generic and only works for the photo layer - new AlertDialog.Builder(getContext()).setTitle(R.string.photo_viewer_delete_title) - .setPositiveButton(R.string.photo_viewer_delete_button, (dialog, which) -> { - if (viewPager == null) { - return; - } - int position = getCurrentPosition(); - if (position >= 0 && position < photoList.size()) { // avoid crashes from bouncing - Uri photoUri = Uri.parse(photoList.get(position)); - try { - if (getShowsDialog()) { - // delete from in memory and on device index - try (PhotoIndex index = new PhotoIndex(getContext())) { - index.deletePhoto(getContext(), photoUri); - } - Map map = (caller instanceof Main) ? ((Main) caller).getMap() : null; - final de.blau.android.layer.photos.MapOverlay overlay = map != null ? map.getPhotoLayer() : null; - if (overlay != null) { - // as the Photo was selected before calling this it will still have a - // reference in the layer - overlay.deselectObjects(); - overlay.invalidate(); - } - } else { - Intent intent = new Intent(getContext(), Main.class); - intent.setAction(Main.ACTION_DELETE_PHOTO); - intent.setData(photoUri); - getContext().startActivity(intent); - } - // actually delete - if (getContext().getContentResolver().delete(photoUri, null, null) >= 1) { - photoList.remove(position); - position = Math.min(position, size - 1); // this will set pos to -1 if - // empty, - // but we will exit in that case in any case - if (getShowsDialog() && photoList.isEmpty()) { - // in fragment mode we want to stay around - getDialog().dismiss(); - } else { - photoPagerAdapter.notifyDataSetChanged(); - viewPager.setCurrentItem(position); - if (photoList.size() == 1 && itemBackward != null && itemForward != null) { - itemBackward.setEnabled(false); - itemForward.setEnabled(false); - } - } - } - } catch (java.lang.SecurityException sex) { - Log.e(DEBUG_TAG, "Error deleting: " + sex.getMessage() + " " + sex.getClass().getName()); - ScreenMessage.toastTopError(getContext(), getString(R.string.toast_permission_denied, sex.getMessage())); - } - } - }).setNeutralButton(R.string.cancel, null).show(); - break; - default: - // do nothing + if (photoList == null || photoList.isEmpty() || pos >= size) { + return false; + } + try { + final boolean loaderPresent = photoLoader != null; + switch (item.getItemId()) { + case MENUITEM_BACK: + pos = pos - 1; + if (pos == -1) { + pos = size - 1; + } + viewPager.setCurrentItem(pos); + break; + case MENUITEM_FORWARD: + pos = (pos + 1) % size; + viewPager.setCurrentItem(pos); + break; + case MENUITEM_GOTO: + if (loaderPresent) { + photoLoader.showOnMap(caller, pos); + } + break; + case MENUITEM_SHARE: + if (loaderPresent) { + photoLoader.share(caller, photoList.get(pos)); + } + break; + case MENUITEM_INFO: + if (loaderPresent) { + photoLoader.info(caller, photoList.get(pos)); + } + break; + case MENUITEM_DELETE: + if (loaderPresent) { + photoLoader.delete(caller, photoList.get(pos)); } - } finally { - prepareMenu(); + break; + default: + // do nothing } + } finally { + prepareMenu(); } - return false; + return true; } @Override diff --git a/src/main/java/de/blau/android/util/ImageLoader.java b/src/main/java/de/blau/android/util/ImageLoader.java index a8fe5f3414..c9c26c3624 100644 --- a/src/main/java/de/blau/android/util/ImageLoader.java +++ b/src/main/java/de/blau/android/util/ImageLoader.java @@ -8,6 +8,7 @@ import android.widget.TextView; import androidx.annotation.NonNull; import androidx.fragment.app.Fragment; +import androidx.fragment.app.FragmentActivity; public abstract class ImageLoader implements Serializable { @@ -50,6 +51,44 @@ public void share(@NonNull Context context, @NonNull String uri) { // empty } + /** + * Indicate if we support an image information function + * + * @return true if supported + */ + public boolean supportsInfo() { + return false; + } + + /** + * Show some information on the image + * + * @param activity Android activity + * @param uri the Uri or other reference to the photo + */ + public void info(@NonNull FragmentActivity activity, @NonNull String uri) { + // empty + } + + /** + * Indicate if we support an image deletion function + * + * @return true if supported + */ + public boolean supportsDelete() { + return false; + } + + /** + * Delete the photo + * + * @param context Android Context + * @param uri the Uri or other reference to the photo + */ + public void delete(@NonNull Context context, @NonNull String uri) { + // empty + } + /** * Set a title to display * diff --git a/src/main/java/de/blau/android/util/InfoDialogFragment.java b/src/main/java/de/blau/android/util/InfoDialogFragment.java index c122ebf637..3da25a69bb 100644 --- a/src/main/java/de/blau/android/util/InfoDialogFragment.java +++ b/src/main/java/de/blau/android/util/InfoDialogFragment.java @@ -1,12 +1,18 @@ package de.blau.android.util; +import java.util.Locale; + import android.app.Dialog; import android.os.Bundle; +import android.text.Spanned; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; +import android.widget.ScrollView; +import android.widget.TableLayout; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import de.blau.android.R; public abstract class InfoDialogFragment extends ImmersiveDialogFragment { @@ -34,4 +40,51 @@ public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup c * @return the View */ protected abstract View createView(@Nullable ViewGroup container); + + /** + * Create the view we want to display + * + * Classes extending LayerInfo need to override this but call through to the super method to get the view + * + * @param container parent view or null + * @return the View + */ + @NonNull + protected ScrollView createEmptyView(@Nullable ViewGroup container) { + LayoutInflater inflater = ThemeUtils.getLayoutInflater(getActivity()); + return (ScrollView) inflater.inflate(R.layout.element_info_view, container, false); + } + + /** + * Setup the table layout params + * + * @return an instance of TableLayout.LayoutParams + */ + @NonNull + protected TableLayout.LayoutParams getTableLayoutParams() { + TableLayout.LayoutParams tp = new TableLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT); + tp.setMargins(10, 2, 10, 2); + return tp; + } + + /** + * Get the string resource formated as an italic string + * + * @param resId String resource id + * @return a Spanned containing the string + */ + protected Spanned toItalic(int resId) { + return Util.fromHtml("" + getString(resId) + ""); + } + + /** + * Pretty print a coordinate value + * + * @param coord the coordinate in WGS84 + * @return a reasonable looking string representation + */ + @NonNull + protected static String prettyPrintCoord(double coord) { + return String.format(Locale.US, "%.7f°", coord); + } } diff --git a/src/main/res/drawable-xhdpi/outline_info_white_48dp.png b/src/main/res/drawable-xhdpi/outline_info_white_48dp.png new file mode 100644 index 0000000000..7d0740a8f1 Binary files /dev/null and b/src/main/res/drawable-xhdpi/outline_info_white_48dp.png differ diff --git a/src/main/res/values/strings.xml b/src/main/res/values/strings.xml index 452e4b125a..eb6f4a9833 100755 --- a/src/main/res/values/strings.xml +++ b/src/main/res/values/strings.xml @@ -256,6 +256,13 @@ Track points Way points + + Image info + Created by + Capture date + Direction + Size + %1$d kB Too much data loaded! You have %1$d Nodes loaded and have exceeded the configured limit. This will lead to slowdown, and if you add more data, to potential issues saving state.