From 014750787b6314a734f99367eb3ebfa7b95f7fd7 Mon Sep 17 00:00:00 2001 From: simonpoole Date: Sat, 25 Jan 2025 15:54:37 +0100 Subject: [PATCH] Automatically add an extension to files when saving if it is missing THe Android system file picker does not allow to provide a default extension to add to a file when creating a new one. We get around this, at the price of this being a bad UX, by renaming the file as soon as we have written it. WE only do this if there is no extension at all present. Further we attempt to save with proper mime types, however currently it seems as if these are ignored by the system. Note: currently we do nothing when saving scripts from the console modal. Resolves: https://github.com/MarcusWolschon/osmeditor4android/issues/2589 --- src/main/java/de/blau/android/Main.java | 36 ++++++++++++++----- .../blau/android/contract/FileExtensions.java | 2 ++ .../de/blau/android/contract/MimeTypes.java | 29 +++++++++------ .../blau/android/dialogs/ConsoleDialog.java | 2 +- .../java/de/blau/android/dialogs/Layers.java | 3 +- .../android/util/ContentResolverUtil.java | 24 ++++++++++++- .../java/de/blau/android/util/SaveFile.java | 34 ++++++++++++++++++ .../java/de/blau/android/util/SelectFile.java | 22 ++++++++---- 8 files changed, 124 insertions(+), 28 deletions(-) diff --git a/src/main/java/de/blau/android/Main.java b/src/main/java/de/blau/android/Main.java index 394321e87d..96ab177aed 100644 --- a/src/main/java/de/blau/android/Main.java +++ b/src/main/java/de/blau/android/Main.java @@ -1010,7 +1010,7 @@ private void processIntents() { case ACTION_IMAGE_SELECT: if (map != null) { SelectImageInterface layer = (SelectImageInterface) map - .getLayer((LayerType) intent.getSerializableExtra(NetworkImageLoader.LAYER_TYPE_KEY)); + .getLayer(Util.getSerializableExtra(intent, NetworkImageLoader.LAYER_TYPE_KEY, LayerType.class)); selectImageOnLayer(intent, layer); } break; @@ -2315,7 +2315,7 @@ protected void onPostExecute(Void result) { return true; case R.id.menu_transfer_export: descheduleAutoLock(); - SelectFile.save(this, R.string.config_osmPreferredDir_key, new SaveFile() { + SelectFile.save(this, null, R.string.config_osmPreferredDir_key, new SaveFile() { private static final long serialVersionUID = 1L; @Override @@ -2374,12 +2374,20 @@ public boolean read(FragmentActivity currentActivity, Uri fileUri) { return true; case R.id.menu_transfer_save_file: descheduleAutoLock(); - SelectFile.save(this, R.string.config_osmPreferredDir_key, new SaveFile() { + SelectFile.save(this, MimeTypes.OSMXML, R.string.config_osmPreferredDir_key, new SaveFile() { private static final long serialVersionUID = 1L; @Override public boolean save(FragmentActivity currentActivity, Uri fileUri) { - App.getLogic().writeOsmFile(currentActivity, fileUri, new PostFileWriteCallback(currentActivity, fileUri.getPath())); + + App.getLogic().writeOsmFile(currentActivity, fileUri, new PostFileWriteCallback(currentActivity, fileUri.getPath()) { + @Override + public void onSuccess() { + super.onSuccess(); + addExtensionIfNeeded(currentActivity, fileUri, FileExtensions.OSM); + } + }); + SelectFile.savePref(prefs, R.string.config_osmPreferredDir_key, fileUri); return true; } @@ -2419,13 +2427,19 @@ public boolean save(FragmentActivity currentActivity, Uri fileUri) { case R.id.menu_transfer_save_notes_all: case R.id.menu_transfer_save_notes_new_and_changed: descheduleAutoLock(); - SelectFile.save(this, R.string.config_notesPreferredDir_key, new SaveFile() { + SelectFile.save(this, MimeTypes.OSNXML, R.string.config_notesPreferredDir_key, new SaveFile() { private static final long serialVersionUID = 1L; @Override public boolean save(FragmentActivity currentActivity, Uri fileUri) { TransferTasks.writeOsnFile(currentActivity, item.getItemId() == R.id.menu_transfer_save_notes_all, fileUri, - new PostFileWriteCallback(currentActivity, fileUri.toString())); + new PostFileWriteCallback(currentActivity, fileUri.toString()) { + @Override + public void onSuccess() { + super.onSuccess(); + addExtensionIfNeeded(currentActivity, fileUri, FileExtensions.OSN); + } + }); SelectFile.savePref(prefs, R.string.config_notesPreferredDir_key, fileUri); return true; } @@ -2702,12 +2716,18 @@ public static void showJsConsole(@NonNull final Main main) { * @param listName the todo list name or null for all // NOSONAR */ private void writeTodos(@Nullable String listName) { - SelectFile.save(this, R.string.config_osmPreferredDir_key, new SaveFile() { + SelectFile.save(this, MimeTypes.TODOJSON, R.string.config_osmPreferredDir_key, new SaveFile() { private static final long serialVersionUID = 1L; @Override public boolean save(FragmentActivity currentActivity, Uri fileUri) { - TransferTasks.writeTodoFile(currentActivity, fileUri, listName, true, null); + TransferTasks.writeTodoFile(currentActivity, fileUri, listName, true, new PostFileWriteCallback(currentActivity, fileUri.getPath()) { + @Override + public void onSuccess() { + super.onSuccess(); + addExtensionIfNeeded(currentActivity, fileUri, FileExtensions.JSON); + } + }); SelectFile.savePref(prefs, R.string.config_osmPreferredDir_key, fileUri); return true; } diff --git a/src/main/java/de/blau/android/contract/FileExtensions.java b/src/main/java/de/blau/android/contract/FileExtensions.java index 56e3629694..b89050af94 100644 --- a/src/main/java/de/blau/android/contract/FileExtensions.java +++ b/src/main/java/de/blau/android/contract/FileExtensions.java @@ -8,6 +8,8 @@ public final class FileExtensions { public static final String PO = "po"; public static final String JPG = "jpg"; public static final String OSC = "osc"; + public static final String OSM = "osm"; + public static final String OSN = "osn"; // osm notes public static final String MD = "md"; public static final String MVT = "mvt"; public static final String PBF = "pbf"; diff --git a/src/main/java/de/blau/android/contract/MimeTypes.java b/src/main/java/de/blau/android/contract/MimeTypes.java index 169d4a91df..24fa6556eb 100644 --- a/src/main/java/de/blau/android/contract/MimeTypes.java +++ b/src/main/java/de/blau/android/contract/MimeTypes.java @@ -2,6 +2,17 @@ public final class MimeTypes { + // types and subtypes + public static final String IMAGE_TYPE = "image"; + public static final String PNG_SUBTYPE = "png"; + public static final String BMP_SUBTYPE = "bmp"; + public static final String APPLICATION_TYPE = "application"; + public static final String JSON_SUBTYPE = "json"; + public static final String WMS_EXCEPTION_XML_SUBTYPE = "vnd.ogc.se_xml"; + public static final String TEXT_TYPE = "text"; + public static final String MVT_SUBTYPE = "vnd.mapbox-vector-tile"; + public static final String X_PROTOBUF_SUBTYPE = "x-protobuf"; // not registered + public static final String ALL_IMAGE_FORMATS = "image/*"; public static final String JPEG = "image/jpeg"; public static final String PNG = "image/png"; @@ -14,18 +25,14 @@ public final class MimeTypes { public static final String TEXTXML = "text/xml"; public static final String TEXTCSV = "text/comma-separated-values"; - public static final String ZIP = "application/zip"; + public static final String OSMXML = "application/vnd.openstreetmap.data+xml"; // registered + public static final String OSMPBF = "application/vnd.openstreetmap.data+" + X_PROTOBUF_SUBTYPE; // not registered + public static final String OSCXML = "application/vnd.openstreetmap.osc+xml"; // not registered + public static final String OSNXML = "application/vnd.openstreetmap.osn+xml"; // not registered - // types and subtypes - public static final String IMAGE_TYPE = "image"; - public static final String PNG_SUBTYPE = "png"; - public static final String BMP_SUBTYPE = "bmp"; - public static final String APPLICATION_TYPE = "application"; - public static final String JSON_SUBTYPE = "json"; - public static final String WMS_EXCEPTION_XML_SUBTYPE = "vnd.ogc.se_xml"; - public static final String TEXT_TYPE = "text"; - public static final String MVT_SUBTYPE = "vnd.mapbox-vector-tile"; - public static final String X_PROTOBUF_SUBTYPE = "x-protobuf"; + public static final String TODOJSON = "application/vnd.vespucci.todo+" + JSON_SUBTYPE;// not registered + + public static final String ZIP = "application/zip"; /** * Private constructor diff --git a/src/main/java/de/blau/android/dialogs/ConsoleDialog.java b/src/main/java/de/blau/android/dialogs/ConsoleDialog.java index 78208b41ba..6634ce4558 100644 --- a/src/main/java/de/blau/android/dialogs/ConsoleDialog.java +++ b/src/main/java/de/blau/android/dialogs/ConsoleDialog.java @@ -254,7 +254,7 @@ private OnMenuItemClickListener getOnItemClickListener(@NonNull final Preference activity.startActivity(shareIntent); break; case R.id.console_menu_save: - SelectFile.save(activity, R.string.config_scriptsPreferredDir_key, new SaveFile() { + SelectFile.save(activity, null, R.string.config_scriptsPreferredDir_key, new SaveFile() { private static final long serialVersionUID = 1L; @Override diff --git a/src/main/java/de/blau/android/dialogs/Layers.java b/src/main/java/de/blau/android/dialogs/Layers.java index f65a20c47b..e9db7de8a7 100644 --- a/src/main/java/de/blau/android/dialogs/Layers.java +++ b/src/main/java/de/blau/android/dialogs/Layers.java @@ -1179,7 +1179,7 @@ public void onClick(View arg0) { }); item = menu.add(R.string.menu_gps_export); item.setOnMenuItemClickListener(unused -> { - SelectFile.save(activity, R.string.config_osmPreferredDir_key, new SaveFile() { + SelectFile.save(activity, MimeTypes.GPX, R.string.config_osmPreferredDir_key, new SaveFile() { private static final long serialVersionUID = 1L; @Override @@ -1188,6 +1188,7 @@ public boolean save(FragmentActivity currentActivity, Uri fileUri) { if (layer != null) { final Track track = ((de.blau.android.layer.gpx.MapOverlay) layer).getTrack(); if (track != null) { + fileUri = SaveFile.addExtensionIfNeeded(currentActivity, fileUri, FileExtensions.GPX); SavingHelper.asyncExport(currentActivity, track, fileUri); SelectFile.savePref(App.getLogic().getPrefs(), R.string.config_osmPreferredDir_key, fileUri); } diff --git a/src/main/java/de/blau/android/util/ContentResolverUtil.java b/src/main/java/de/blau/android/util/ContentResolverUtil.java index 548bd5b766..aa2da50785 100644 --- a/src/main/java/de/blau/android/util/ContentResolverUtil.java +++ b/src/main/java/de/blau/android/util/ContentResolverUtil.java @@ -1,6 +1,9 @@ package de.blau.android.util; +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + import java.io.File; +import java.io.FileNotFoundException; import java.io.IOException; import java.util.ArrayList; import java.util.List; @@ -23,7 +26,8 @@ public final class ContentResolverUtil { - private static final String DEBUG_TAG = ContentResolverUtil.class.getSimpleName().substring(0, Math.min(23, ContentResolverUtil.class.getSimpleName().length())); + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, ContentResolverUtil.class.getSimpleName().length()); + private static final String DEBUG_TAG = ContentResolverUtil.class.getSimpleName().substring(0, TAG_LEN); private static final String PRIMARY = "primary"; private static final String MY_DOWNLOADS = "content://downloads/my_downloads"; @@ -156,6 +160,24 @@ private static String getPathFromDocumentUri(@NonNull Context context, @Nullable return null; } + /** + * Rename a file + * + * @param context an Android Context + * @param uri the URI + * @param newName the new name + * @param the new uri or null + */ + @Nullable + public static Uri rename(@NonNull Context context, @NonNull Uri uri, @NonNull String newName) { + try { + return DocumentsContract.renameDocument(context.getContentResolver(), uri, newName); + } catch (FileNotFoundException e) { + Log.e(DEBUG_TAG, e.getMessage()); + } + return null; + } + /** * Get the value of the data column for this Uri. This is useful for MediaStore Uris, and other file-based * ContentProviders. diff --git a/src/main/java/de/blau/android/util/SaveFile.java b/src/main/java/de/blau/android/util/SaveFile.java index 38720bb0b8..ff7deebd7b 100644 --- a/src/main/java/de/blau/android/util/SaveFile.java +++ b/src/main/java/de/blau/android/util/SaveFile.java @@ -1,8 +1,12 @@ package de.blau.android.util; +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + import java.io.Serializable; +import android.content.Context; import android.net.Uri; +import android.util.Log; import androidx.annotation.NonNull; import androidx.fragment.app.FragmentActivity; @@ -12,6 +16,36 @@ public abstract class SaveFile implements Serializable { */ private static final long serialVersionUID = 1L; + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, SaveFile.class.getSimpleName().length()); + private static final String DEBUG_TAG = SaveFile.class.getSimpleName().substring(0, TAG_LEN); + + /** + * Add an extension to a file name if necessary + * + * @param context + * @param fileUri + * @param extension + */ + @NonNull + public static Uri addExtensionIfNeeded(@NonNull Context context, @NonNull Uri fileUri, @NonNull String extension) { + String displayName = ContentResolverUtil.getDisplaynameColumn(context, fileUri); + if (displayName.indexOf(".") < 0) { + String newName = displayName + "." + extension; + Log.i(DEBUG_TAG, "Renaming to " + newName); + try { + Uri newUri = ContentResolverUtil.rename(context, fileUri, newName); + if (newUri != null) { + return newUri; + } + } catch (Exception ex) { + // we can't trust Android + Log.e(DEBUG_TAG, "Rename to " + newName + " failed with " + ex.getMessage()); + } + + } + return fileUri; + } + /** * Save a file * diff --git a/src/main/java/de/blau/android/util/SelectFile.java b/src/main/java/de/blau/android/util/SelectFile.java index 913f03038b..6446f9e679 100644 --- a/src/main/java/de/blau/android/util/SelectFile.java +++ b/src/main/java/de/blau/android/util/SelectFile.java @@ -1,6 +1,8 @@ package de.blau.android.util; +import static de.blau.android.contract.Constants.LOG_TAG_LEN; + import java.io.File; import java.util.ArrayList; import java.util.List; @@ -43,7 +45,8 @@ */ public final class SelectFile { - private static final String DEBUG_TAG = SelectFile.class.getSimpleName().substring(0, Math.min(23, SelectFile.class.getSimpleName().length())); + private static final int TAG_LEN = Math.min(LOG_TAG_LEN, SelectFile.class.getSimpleName().length()); + private static final String DEBUG_TAG = SelectFile.class.getSimpleName().substring(0, TAG_LEN); public static final int SAVE_FILE = 7113; public static final int READ_FILE = 9340; @@ -65,15 +68,17 @@ private SelectFile() { * Save a file * * @param activity activity that called us + * @param mimeType the mime type to use, or null to not specifiy * @param directoryPrefKey string resources for shared preferences for preferred (last) directory * @param callback callback that does the actual saving, should call {@link #savePref(Preferences, int, Uri)} */ - public static void save(@NonNull FragmentActivity activity, int directoryPrefKey, @NonNull de.blau.android.util.SaveFile callback) { + public static void save(@NonNull FragmentActivity activity, @Nullable String mimeType, int directoryPrefKey, + @NonNull de.blau.android.util.SaveFile callback) { synchronized (saveCallbackLock) { saveCallback = callback; } String path = App.getPreferences(activity).getString(directoryPrefKey); - startFileSelector(activity, Intent.ACTION_CREATE_DOCUMENT, SAVE_FILE, path, false); + startFileSelector(activity, Intent.ACTION_CREATE_DOCUMENT, SAVE_FILE, path, mimeType, false); } /** @@ -98,7 +103,7 @@ public static void read(@NonNull FragmentActivity activity, int directoryPrefKey readCallback = readFile; } String path = App.getPreferences(activity).getString(directoryPrefKey); - startFileSelector(activity, Intent.ACTION_OPEN_DOCUMENT, READ_FILE, path, allowMultiple); + startFileSelector(activity, Intent.ACTION_OPEN_DOCUMENT, READ_FILE, path, null, allowMultiple); } /** @@ -108,11 +113,16 @@ public static void read(@NonNull FragmentActivity activity, int directoryPrefKey * @param intentAction the intent action we want to use * @param intentRequestCode the request code * @param path a directory path to try to start with + * @param mimeType mime type to use, null to not specify */ private static void startFileSelector(@NonNull FragmentActivity activity, @NonNull String intentAction, int intentRequestCode, @Nullable String path, - boolean allowMultiple) { + String mimeType, boolean allowMultiple) { Intent i = new Intent(intentAction); - i.setType("*/*"); + if (mimeType == null) { + i.setType("*/*"); + } else { + i.setTypeAndNormalize(mimeType); + } if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && path != null) { i.putExtra(DocumentsContract.EXTRA_INITIAL_URI, Uri.parse(path)); }