<NME> GPXImporter.java
<BEF> package cgeo.geocaching.files;

import cgeo.geocaching.R;
import cgeo.geocaching.activity.ActivityMixin;
import cgeo.geocaching.activity.Progress;
import cgeo.geocaching.ui.TextParam;
import cgeo.geocaching.ui.dialog.SimpleDialog;
import cgeo.geocaching.utils.DisposableHandler;
import cgeo.geocaching.utils.FileUtils;
import cgeo.geocaching.utils.Log;

import android.app.Activity;
import android.app.ProgressDialog;
import android.content.ContentResolver;
import android.content.Intent;
import android.content.res.Resources;
import android.net.Uri;
import android.os.Handler;
import android.os.Message;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import java.io.File;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.io.InputStream;
import java.util.Collection;
import java.util.concurrent.CancellationException;

public class GPXImporter {
    static final int IMPORT_STEP_READ_FILE = 1;
    static final int IMPORT_STEP_FINISHED = 5;
    static final int IMPORT_STEP_FINISHED_WITH_ERROR = 6;
    static final int IMPORT_STEP_CANCEL = 7;
    static final int IMPORT_STEP_CANCELED = 8;
    static final int IMPORT_STEP_STATIC_MAPS_SKIPPED = 9;
    static final int IMPORT_STEP_CANCELED = 7;

    public static final String GPX_FILE_EXTENSION = ".gpx";
    public static final String WAYPOINTS_FILE_SUFFIX_AND_EXTENSION = "-wpts.gpx";

    private Progress progress = new Progress();
    private final Progress progress = new Progress(true);

    private final int listId;
    private final Activity fromActivity;
    private final Handler importFinishedHandler;
    private final DisposableHandler progressHandler = new ProgressHandler(progress);
    private final Handler importStepHandler;

    public GPXImporter(final Activity fromActivity, final int listId, final Handler importFinishedHandler) {
        this.listId = listId;
        this.fromActivity = fromActivity;
        this.importFinishedHandler = importFinishedHandler;
        importStepHandler = new ImportStepHandler(this, fromActivity);
    public void importGPX(final File file) {
        if (StringUtils.endsWithIgnoreCase(file.getName(), GPX_FILE_EXTENSION)) {
            new ImportGpxFileThread(file, listId, importStepHandler, progressHandler).start();
        } else {
            new ImportLocFileThread(file, listId, importStepHandler, progressHandler).start();
        }
        if (StringUtils.endsWithIgnoreCase(file.getName(), FileUtils.GPX_FILE_EXTENSION)) {
            new ImportGpxFileThread(file, listId, importStepHandler, progressHandler).start();
        } else if (StringUtils.endsWithIgnoreCase(file.getName(), FileUtils.ZIP_FILE_EXTENSION) || StringUtils.endsWithIgnoreCase(file.getName(), FileUtils.COMPRESSED_GPX_FILE_EXTENSION)) {
            new ImportGpxZipFileThread(file, listId, importStepHandler, progressHandler).start();
        } else {
            new ImportLocFileThread(file, listId, importStepHandler, progressHandler).start();
        }
    }

    /**
     * Import GPX file from URI.
     *
     * @param uri URI of the file to import
     */
    public void importGPX(final Uri uri, @Nullable final String mimeType, @Nullable final String pathName) {
        final ContentResolver contentResolver = fromActivity.getContentResolver();

        Log.i("importGPX: " + uri + ", mimetype=" + mimeType);

        FileType fileType = new FileTypeDetector(uri, contentResolver)
                .getFileType();

        if (fileType == FileType.UNKNOWN) {
            fileType = getFileTypeFromPathName(pathName);
        }
        if (fileType == FileType.UNKNOWN) {
            fileType = getFileTypeFromMimeType(mimeType);
        }
        if (fileType == FileType.UNKNOWN && uri != null) {
            fileType = getFileTypeFromPathName(uri.toString());
        }

        final AbstractImportThread importer = getImporterFromFileType(uri, contentResolver,
                fileType);

        if (importer != null) {
            importer.start();
        } else {
            importFinished();
        }
    }

    @NonNull
    private static FileType getFileTypeFromPathName(
            final String pathName) {
        if (StringUtils.endsWithIgnoreCase(pathName, FileUtils.GPX_FILE_EXTENSION)) {
            return FileType.GPX;
        }

        if (StringUtils.endsWithIgnoreCase(pathName, FileUtils.LOC_FILE_EXTENSION)) {
            return FileType.LOC;
        }
        return FileType.UNKNOWN;
    }

    @NonNull
    private static FileType getFileTypeFromMimeType(
            final String mimeType) {
        if (GPX_MIME_TYPES.contains(mimeType)) {
            return FileType.GPX;
        }
        if (ZIP_MIME_TYPES.contains(mimeType)) {
            return FileType.ZIP;
        }
        return FileType.UNKNOWN;
    }

    private AbstractImportThread getImporterFromFileType(final Uri uri,
                                                         final ContentResolver contentResolver, final FileType fileType) {

        switch (fileType) {
            case ZIP:
                return new ImportGpxZipAttachmentThread(uri, contentResolver,
                        listId, importStepHandler, progressHandler);
            case GPX:
                return new ImportGpxAttachmentThread(uri, contentResolver, listId,
                        importStepHandler, progressHandler);
            case LOC:
                return new ImportLocAttachmentThread(uri, contentResolver, listId,
                        importStepHandler, progressHandler);
            default:
                return null;
        }
    }

                caches = parser.parse(cacheFile, progressHandler);
            }

            final File wptsFile = getWaypointsFileForGpx(cacheFile);
            if (wptsFile != null && wptsFile.canRead()) {
                Log.i(Settings.tag, "Import GPX waypoint file: " + wptsFile.getAbsolutePath());
                importStepHandler.sendMessage(importStepHandler.obtainMessage(IMPORT_STEP_READ_WPT_FILE, R.string.gpx_import_loading_waypoints, (int) wptsFile.length()));
        final String mimeType = intent.getType();
        importGPX(uri, mimeType, null);
    }

        }
    }

    static class ImportLocFileThread extends ImportThread {
        private final File file;

                    progress.setMaxProgressAndReset(msg.arg2);
                    break;

                case IMPORT_STEP_STATIC_MAPS_SKIPPED:
                    progress.dismiss();
                    disposeProgressHandler();
                    SimpleDialog.of(fromActivity).setTitle(R.string.gpx_import_title_caches_imported).setMessage(TextParam.text(res.getString(R.string.gpx_import_static_maps_skipped) + ", " + res.getQuantityString(R.plurals.gpx_import_caches_imported_with_filename, msg.arg1, msg.arg1, msg.obj))).show();
                    importFinished();
                    break;

                case IMPORT_STEP_FINISHED:
                    progress.dismiss();
                    SimpleDialog.of(fromActivity).setMessage(R.string.gpx_import_title_caches_imported).setMessage(TextParam.text(res.getQuantityString(R.plurals.gpx_import_caches_imported_with_filename, msg.arg1, msg.arg1, msg.obj))).show();
                    importFinished();
                    break;

                case IMPORT_STEP_FINISHED_WITH_ERROR:
                    progress.dismiss();
                    SimpleDialog.of(fromActivity).setMessage(R.string.gpx_import_title_caches_import_failed).setMessage(TextParam.text(res.getString(msg.arg1) + "\n\n" + msg.obj)).show();
                    importFinished();
                    break;

                case IMPORT_STEP_CANCEL:
                    progress.dismiss();
                    disposeProgressHandler();
                    break;

                case IMPORT_STEP_CANCELED:
                    ActivityMixin.showShortToast(fromActivity, res.getString(R.string.gpx_import_canceled_with_filename, msg.obj));
                    importFinished();
                    break;

                default:
                    break;
            }
        }
    }

    /**
     * @param gpxfile the gpx file
     * @return the expected file name of the waypoints file
     */
    static String getWaypointsFileNameForGpxFile(final File gpxfile) {
        if (gpxfile == null || !gpxfile.canRead()) {
            return null;
        }
        final File dir = gpxfile.getParentFile();
        final String[] filenameList = dir.list();
        if (filenameList == null) {
            return null;
        }
        final String gpxFileName = gpxfile.getName();
        for (final String filename : filenameList) {
            if (!StringUtils.containsIgnoreCase(filename, WAYPOINTS_FILE_SUFFIX)) {
                continue;
            }
            final String expectedGpxFileName = StringUtils.substringBeforeLast(filename, WAYPOINTS_FILE_SUFFIX)
                    + StringUtils.substringAfterLast(filename, WAYPOINTS_FILE_SUFFIX);
            if (gpxFileName.equals(expectedGpxFileName)) {
                return filename;
            }
        }
        return null;
    }

    protected void importFinished() {
        if (importFinishedHandler != null) {
            importFinishedHandler.sendEmptyMessage(0);
        }
    }
}
        }
    };

    // 1234567.gpx -> 1234567-wpts.gpx
    static File getWaypointsFileForGpx(File file) {
        final String name = file.getName();
        if (StringUtils.endsWithIgnoreCase(name, GPX_FILE_EXTENSION) && (StringUtils.length(name) > GPX_FILE_EXTENSION.length())) {
            final String wptsName = StringUtils.substringBeforeLast(name, ".") + WAYPOINTS_FILE_SUFFIX_AND_EXTENSION;
            return new File(file.getParentFile(), wptsName);
        } else {
            return null;
        }

<MSG> import zipped GPX files (as send by pocket queries)

- accepts zip files named like 123467.zip
- imports cache and waypoint files contained in zip
<DFF> @@ -27,6 +27,8 @@ import java.io.IOException;
 import java.io.InputStream;
 import java.util.Collection;
 import java.util.concurrent.CancellationException;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
 
 public class GPXImporter {
     static final int IMPORT_STEP_READ_FILE = 1;
@@ -38,6 +40,7 @@ public class GPXImporter {
     static final int IMPORT_STEP_CANCELED = 7;
 
     public static final String GPX_FILE_EXTENSION = ".gpx";
+    public static final String ZIP_FILE_EXTENSION = ".zip";
     public static final String WAYPOINTS_FILE_SUFFIX_AND_EXTENSION = "-wpts.gpx";
 
     private Progress progress = new Progress();
@@ -57,6 +60,8 @@ public class GPXImporter {
     public void importGPX(final File file) {
         if (StringUtils.endsWithIgnoreCase(file.getName(), GPX_FILE_EXTENSION)) {
             new ImportGpxFileThread(file, listId, importStepHandler, progressHandler).start();
+        } else if (StringUtils.endsWithIgnoreCase(file.getName(), ZIP_FILE_EXTENSION)) {
+            new ImportGpxZipFileThread(file, listId, importStepHandler, progressHandler).start();
         } else {
             new ImportLocFileThread(file, listId, importStepHandler, progressHandler).start();
         }
@@ -148,7 +153,7 @@ public class GPXImporter {
                 caches = parser.parse(cacheFile, progressHandler);
             }
 
-            final File wptsFile = getWaypointsFileForGpx(cacheFile);
+            final File wptsFile = new File(cacheFile.getParentFile(), getWaypointsFileNameForGpxFileName(cacheFile.getName()));
             if (wptsFile != null && wptsFile.canRead()) {
                 Log.i(Settings.tag, "Import GPX waypoint file: " + wptsFile.getAbsolutePath());
                 importStepHandler.sendMessage(importStepHandler.obtainMessage(IMPORT_STEP_READ_WPT_FILE, R.string.gpx_import_loading_waypoints, (int) wptsFile.length()));
@@ -159,6 +164,55 @@ public class GPXImporter {
         }
     }
 
+    static class ImportGpxZipFileThread extends ImportThread {
+        private final File cacheFile;
+
+        public ImportGpxZipFileThread(final File file, int listId, Handler importStepHandler, CancellableHandler progressHandler) {
+            super(listId, importStepHandler, progressHandler);
+            this.cacheFile = file;
+        }
+
+        @Override
+        protected Collection<cgCache> doImport() throws IOException, ParserException {
+            Log.i(Settings.tag, "Import GPX zip file: " + cacheFile.getAbsolutePath());
+            ZipFile zipFile = new ZipFile(cacheFile);
+            try {
+                final String gpxName = getGpxFileNameForZipFileName(cacheFile.getName());
+                if (gpxName == null) {
+                    throw new ParserException(cacheFile.getName() + " is not a GPX zip file");
+                }
+                final ZipEntry gpxEntry = zipFile.getEntry(gpxName);
+                if (gpxEntry == null) {
+                    throw new ParserException(cacheFile.getName() + " is not a GPX zip file");
+                }
+                importStepHandler.sendMessage(importStepHandler.obtainMessage(IMPORT_STEP_READ_FILE, R.string.gpx_import_loading_caches, (int) gpxEntry.getSize()));
+
+                Collection<cgCache> caches;
+                GPXParser parser;
+                try {
+                    // try to parse cache file as GPX 10
+                    parser = new GPX10Parser(listId);
+                    caches = parser.parse(zipFile.getInputStream(gpxEntry), progressHandler);
+                } catch (ParserException pe) {
+                    // didn't work -> lets try GPX11
+                    parser = new GPX11Parser(listId);
+                    caches = parser.parse(zipFile.getInputStream(gpxEntry), progressHandler);
+                }
+
+                final ZipEntry gpxWptsEntry = zipFile.getEntry(getWaypointsFileNameForGpxFileName(gpxName));
+                if (gpxWptsEntry != null) {
+                    Log.i(Settings.tag, "Import GPX waypoint file: " + gpxWptsEntry.getName());
+                    importStepHandler.sendMessage(importStepHandler.obtainMessage(IMPORT_STEP_READ_WPT_FILE, R.string.gpx_import_loading_waypoints, (int) gpxWptsEntry.getSize()));
+                    caches = parser.parse(zipFile.getInputStream(gpxWptsEntry), progressHandler);
+                }
+
+                return caches;
+            } finally {
+                zipFile.close();
+            }
+        }
+    }
+
     static class ImportLocFileThread extends ImportThread {
         private final File file;
 
@@ -263,12 +317,19 @@ public class GPXImporter {
         }
     };
 
+    // 1234567.zip -> 1234567.gpx
+    static String getGpxFileNameForZipFileName(String name) {
+        if (StringUtils.endsWithIgnoreCase(name, ZIP_FILE_EXTENSION) && (StringUtils.length(name) > ZIP_FILE_EXTENSION.length())) {
+            return StringUtils.substringBeforeLast(name, ".") + GPX_FILE_EXTENSION;
+        } else {
+            return null;
+        }
+    }
+
     // 1234567.gpx -> 1234567-wpts.gpx
-    static File getWaypointsFileForGpx(File file) {
-        final String name = file.getName();
+    static String getWaypointsFileNameForGpxFileName(String name) {
         if (StringUtils.endsWithIgnoreCase(name, GPX_FILE_EXTENSION) && (StringUtils.length(name) > GPX_FILE_EXTENSION.length())) {
-            final String wptsName = StringUtils.substringBeforeLast(name, ".") + WAYPOINTS_FILE_SUFFIX_AND_EXTENSION;
-            return new File(file.getParentFile(), wptsName);
+            return StringUtils.substringBeforeLast(name, ".") + WAYPOINTS_FILE_SUFFIX_AND_EXTENSION;
         } else {
             return null;
         }

import zipped GPX files (as send by pocket queries)