diff --git a/src/main/java/de/blau/android/Main.java b/src/main/java/de/blau/android/Main.java index 394321e87..96ab177ae 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 56e362969..b89050af9 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 169d4a91d..24fa6556e 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 78208b41b..6634ce455 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 f65a20c47..6face4c61 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 @@ -1189,6 +1189,7 @@ public boolean save(FragmentActivity currentActivity, Uri fileUri) { final Track track = ((de.blau.android.layer.gpx.MapOverlay) layer).getTrack(); if (track != null) { SavingHelper.asyncExport(currentActivity, track, fileUri); + SaveFile.addExtensionIfNeeded(currentActivity, fileUri, FileExtensions.GPX); 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 548bd5b76..aa2da5078 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 38720bb0b..c82131cea 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,39 @@ 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 + * + * This will only work if the file has already been written + * + * @param context an Android Context + * @param fileUri the original Uri + * @param extension the extension to add + * @return a potentially new Uri + */ + @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 913f03038..6446f9e67 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)); }