Skip to content

Commit

Permalink
Automatically add an extension to files when saving if it is missing
Browse files Browse the repository at this point in the history
THe Android system file picker does not allow to provide a default
extension to add to a file when creating a new one. Supposedly it should
do this for mime type to extension mappings it knows about, however that
doesn't seem to work either, and wouldn't solve the issue in any case as
it isn't possible to register additional mappings.

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: #2589
  • Loading branch information
simonpoole committed Jan 25, 2025
1 parent 1479fbf commit aae0f0d
Show file tree
Hide file tree
Showing 8 changed files with 127 additions and 28 deletions.
36 changes: 28 additions & 8 deletions src/main/java/de/blau/android/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down Expand Up @@ -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;
}
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/de/blau/android/contract/FileExtensions.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
29 changes: 18 additions & 11 deletions src/main/java/de/blau/android/contract/MimeTypes.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/de/blau/android/dialogs/ConsoleDialog.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/main/java/de/blau/android/dialogs/Layers.java
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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);
}
}
Expand Down
24 changes: 23 additions & 1 deletion src/main/java/de/blau/android/util/ContentResolverUtil.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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";
Expand Down Expand Up @@ -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.
Expand Down
37 changes: 37 additions & 0 deletions src/main/java/de/blau/android/util/SaveFile.java
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -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
*
Expand Down
22 changes: 16 additions & 6 deletions src/main/java/de/blau/android/util/SelectFile.java
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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;
Expand All @@ -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);
}

/**
Expand All @@ -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);
}

/**
Expand All @@ -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));
}
Expand Down

0 comments on commit aae0f0d

Please sign in to comment.