diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 3d3e14777..7f192c1f5 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -175,6 +175,11 @@ limitations under the License.
+
diff --git a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java
index d62e1c25c..fd450ed14 100644
--- a/src/main/java/de/dennisguse/opentracks/TrackListActivity.java
+++ b/src/main/java/de/dennisguse/opentracks/TrackListActivity.java
@@ -18,6 +18,7 @@ package de.dennisguse.opentracks;
import android.Manifest;
import android.app.SearchManager;
+import android.content.ActivityNotFoundException;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -25,9 +26,7 @@ import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
import android.content.pm.PackageManager;
import android.database.Cursor;
import android.location.LocationManager;
-import android.net.Uri;
import android.os.Bundle;
-import android.os.StrictMode;
import android.provider.Settings;
import android.util.Log;
import android.view.ContextMenu;
@@ -47,30 +46,21 @@ import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.core.app.ActivityCompat;
-import androidx.core.content.FileProvider;
import androidx.cursoradapter.widget.ResourceCursorAdapter;
import androidx.loader.app.LoaderManager;
import androidx.loader.app.LoaderManager.LoaderCallbacks;
import androidx.loader.content.CursorLoader;
import androidx.loader.content.Loader;
-import java.io.File;
-import java.io.FileNotFoundException;
-import java.io.FileOutputStream;
-import java.io.IOException;
import java.util.Locale;
import de.dennisguse.opentracks.content.ContentProviderUtils;
-import de.dennisguse.opentracks.content.Track;
+import de.dennisguse.opentracks.content.ShareContentProvider;
import de.dennisguse.opentracks.content.TracksColumns;
import de.dennisguse.opentracks.fragments.ConfirmDeleteDialogFragment;
-import de.dennisguse.opentracks.io.file.TrackFileFormat;
-import de.dennisguse.opentracks.io.file.exporter.FileTrackExporter;
-import de.dennisguse.opentracks.io.file.exporter.TrackExporter;
import de.dennisguse.opentracks.services.ITrackRecordingService;
import de.dennisguse.opentracks.services.TrackRecordingServiceConnection;
import de.dennisguse.opentracks.settings.SettingsActivity;
-import de.dennisguse.opentracks.util.FileUtils;
import de.dennisguse.opentracks.util.IntentUtils;
import de.dennisguse.opentracks.util.ListItemUtils;
import de.dennisguse.opentracks.util.PreferencesUtils;
@@ -257,17 +247,17 @@ public class TrackListActivity extends AbstractTrackActivity implements ConfirmD
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
- if (BuildConfig.DEBUG) {
- Log.d(TAG, "Enabling strict mode");
- StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
- .detectAll()
- .penaltyLog()
- .build());
- StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
- .detectAll()
- .penaltyLog()
- .build());
- }
+// if (BuildConfig.DEBUG) {
+// Log.d(TAG, "Enabling strict mode");
+// StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder()
+// .detectAll()
+// .penaltyLog()
+// .build());
+// StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder()
+// .detectAll()
+// .penaltyLog()
+// .build());
+// }
contentProviderUtils = ContentProviderUtils.Factory.get(this);
sharedPreferences = PreferencesUtils.getSharedPreferences(this);
@@ -566,8 +556,7 @@ public class TrackListActivity extends AbstractTrackActivity implements ConfirmD
}
/**
- * Save tracks in one file and send an intent to show it with a map application.
- * TODO: Exported file is not deleted automatically.
+ * Send intent to show tracks on a map (needs an another app).
*
* @param trackIds
*/
@@ -576,45 +565,14 @@ public class TrackListActivity extends AbstractTrackActivity implements ConfirmD
return;
}
- Track[] tracks = new Track[trackIds.length];
- for (int i = 0; i < trackIds.length; i++) {
- tracks[i] = contentProviderUtils.getTrack(trackIds[i]);
- }
-
- TrackFileFormat trackFileFormat = TrackFileFormat.KML;
- TrackExporter trackExporter = new FileTrackExporter(contentProviderUtils, tracks,
- trackFileFormat.newTrackWriter(this, tracks.length > 1), null);
-
- File directory = new File(FileUtils.getPath(trackFileFormat.getExtension()));
- if (!FileUtils.ensureDirectoryExists(directory)) {
- Toast.makeText(this, R.string.external_storage_not_writable, Toast.LENGTH_LONG).show();
- finish();
- return;
- }
-
- String fileName = FileUtils.buildUniqueFileName(directory, tracks[0].getName(), trackFileFormat.getExtension());
- File file = new File(directory, fileName);
-
- try (FileOutputStream fileOutputStream = new FileOutputStream(file)) {
- if (trackExporter.writeTrack(fileOutputStream)) {
- Uri fileUri = FileProvider.getUriForFile(this, FileUtils.FILEPROVIDER, file);
-
- Intent intent = new Intent();
- intent.setAction(android.content.Intent.ACTION_VIEW);
- intent.setDataAndType(fileUri, "application/vnd.google-earth.kml+xml");
-
- intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
- startActivity(intent);
- } else {
- if (!file.delete()) {
- Log.d(TAG, "Unable to delete file");
- }
- Log.e(TAG, "Unable to export track");
- }
- } catch (FileNotFoundException e) {
- Log.e(TAG, "Unable to open file " + file.getName(), e);
- } catch (IOException e) {
- Log.e(TAG, "Unable to close file output stream", e);
+ Intent intent = new Intent();
+ intent.setAction(android.content.Intent.ACTION_VIEW);
+ intent.setDataAndType(ShareContentProvider.createURI(trackIds), ShareContentProvider.MIME);
+ intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ try {
+ startActivity(intent);
+ } catch (ActivityNotFoundException e) {
+ Toast.makeText(this, "No app installed that can show the tracks on a map.", Toast.LENGTH_SHORT).show();
}
}
diff --git a/src/main/java/de/dennisguse/opentracks/content/ContentProviderUtils.java b/src/main/java/de/dennisguse/opentracks/content/ContentProviderUtils.java
index d3f48add6..8bb9ce082 100644
--- a/src/main/java/de/dennisguse/opentracks/content/ContentProviderUtils.java
+++ b/src/main/java/de/dennisguse/opentracks/content/ContentProviderUtils.java
@@ -39,7 +39,7 @@ public interface ContentProviderUtils {
* The authority (the first part of the URI) for the app's content
* provider.
*/
- String AUTHORITY = BuildConfig.APPLICATION_ID;
+ String AUTHORITY = BuildConfig.APPLICATION_ID + ".content";
/**
* The default {@link LocationFactory} which creates a location each time.
*/
diff --git a/src/main/java/de/dennisguse/opentracks/content/ShareContentProvider.java b/src/main/java/de/dennisguse/opentracks/content/ShareContentProvider.java
new file mode 100644
index 000000000..701644896
--- /dev/null
+++ b/src/main/java/de/dennisguse/opentracks/content/ShareContentProvider.java
@@ -0,0 +1,175 @@
+package de.dennisguse.opentracks.content;
+
+import android.content.ContentProvider;
+import android.content.ContentValues;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.ProviderInfo;
+import android.database.Cursor;
+import android.database.MatrixCursor;
+import android.net.Uri;
+import android.os.Bundle;
+import android.os.ParcelFileDescriptor;
+import android.provider.OpenableColumns;
+import android.util.Log;
+import android.widget.Toast;
+
+import androidx.annotation.NonNull;
+import androidx.annotation.Nullable;
+
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.util.Arrays;
+
+import de.dennisguse.opentracks.BuildConfig;
+import de.dennisguse.opentracks.io.file.TrackFileFormat;
+import de.dennisguse.opentracks.io.file.exporter.FileTrackExporter;
+import de.dennisguse.opentracks.io.file.exporter.TrackWriter;
+
+/**
+ * A content provider that mimics the behavior of {@link androidx.core.content.FileProvider}, which shares virtual (non-existing) files.
+ * The actual content is generated by accessing {@link CustomContentProvider} on request.
+ */
+public class ShareContentProvider extends ContentProvider {
+
+ public static final String TAG = ShareContentProvider.class.getCanonicalName();
+
+ public static final String SHAREPROVIDER = BuildConfig.APPLICATION_ID;
+
+ private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
+
+ public static String MIME = "application/kml+xml";
+
+ public static Uri createURI(long[] trackIds) {
+ if (trackIds.length == 0) {
+ throw new UnsupportedOperationException();
+ }
+
+ StringBuilder builder = new StringBuilder();
+ for (long trackId : trackIds) {
+ builder.append(trackId).append(",");
+ }
+ builder.deleteCharAt(builder.lastIndexOf(","));
+
+ return Uri.parse("content://" + SHAREPROVIDER + "/" + builder + ".kml");
+ }
+
+ private static long[] parseURI(Uri uri) {
+ String lastPathSegment = uri.getLastPathSegment();
+ if (lastPathSegment == null) {
+ return new long[]{};
+ }
+
+ String[] lastPathSegmentSplit = lastPathSegment.replace(".kml", "").split(",");
+ long[] trackIds = new long[lastPathSegmentSplit.length];
+ for (int i = 0; i < trackIds.length; i++) {
+ trackIds[i] = Long.valueOf(lastPathSegmentSplit[i]);
+ }
+ return trackIds;
+ }
+
+ @Override
+ public boolean onCreate() {
+ return true;
+ }
+
+ @Override
+ public void attachInfo(Context context, ProviderInfo info) {
+ super.attachInfo(context, info);
+ if (!info.grantUriPermissions) {
+ throw new SecurityException("Provider must grant uri permissions");
+ }
+ }
+
+ @Override
+ public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) {
+ // ContentProvider has already checked granted permissions
+ if (projection == null) {
+ projection = COLUMNS;
+ }
+
+ String[] cols = new String[projection.length];
+ Object[] values = new Object[projection.length];
+ int i = 0;
+ for (String col : projection) {
+ if (OpenableColumns.DISPLAY_NAME.equals(col)) {
+ cols[i] = OpenableColumns.DISPLAY_NAME;
+ values[i++] = uri.getLastPathSegment();
+ } else if (OpenableColumns.SIZE.equals(col)) {
+ cols[i] = OpenableColumns.SIZE;
+ values[i++] = -1;
+ }
+ }
+
+ cols = Arrays.copyOf(cols, i);
+ values = Arrays.copyOf(values, i);
+
+ final MatrixCursor cursor = new MatrixCursor(cols, 1);
+ cursor.addRow(values);
+ return cursor;
+ }
+
+ @Nullable
+ @Override
+ public String getType(@NonNull Uri uri) {
+ return MIME;
+ }
+
+ @Nullable
+ @Override
+ public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
+ ContentProviderUtils contentProviderUtils = ContentProviderUtils.Factory.get(getContext());
+
+ final Uri customContentProviderURI = Uri.parse("content://" + ContentProviderUtils.AUTHORITY + "/tracks");
+ getContext().grantUriPermission(getCallingPackage(), customContentProviderURI, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+
+ long[] trackIds = parseURI(uri);
+ final Track[] tracks = new Track[trackIds.length];
+ for (int i = 0; i < trackIds.length; i++) {
+ tracks[i] = contentProviderUtils.getTrack(trackIds[i]);
+ }
+
+ TrackWriter kmlTrackWriter = TrackFileFormat.KML.newTrackWriter(getContext(), false);
+ final FileTrackExporter fileTrackExporter = new FileTrackExporter(contentProviderUtils, tracks, kmlTrackWriter, null);
+
+ PipeDataWriter pipeDataWriter = new PipeDataWriter() {
+ @Override
+ public void writeDataToPipe(@NonNull ParcelFileDescriptor output, @NonNull Uri uri, @NonNull String mimeType, @Nullable Bundle opts, @Nullable String args) {
+ try (FileOutputStream fileOutputStream = new FileOutputStream(output.getFileDescriptor())) {
+ fileTrackExporter.writeTrack(fileOutputStream);
+ } catch (IOException e) {
+ Log.w(TAG, "Oops closing " + e);
+ Toast.makeText(getContext(), "", Toast.LENGTH_SHORT).show();
+ } finally {
+
+ getContext().revokeUriPermission(customContentProviderURI, Intent.FLAG_GRANT_READ_URI_PERMISSION);
+ }
+ }
+ };
+
+ return openPipeHelper(uri, getType(uri), null, null, pipeDataWriter);
+ }
+
+ @Nullable
+ @Override
+ public String[] getStreamTypes(@NonNull Uri uri, @NonNull String mimeTypeFilter) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Nullable
+ @Override
+ public Uri insert(@NonNull Uri uri, @Nullable ContentValues values) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int delete(@NonNull Uri uri, @Nullable String selection, @Nullable String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String selection, @Nullable String[] selectionArgs) {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/src/main/java/de/dennisguse/opentracks/content/TrackPointsColumns.java b/src/main/java/de/dennisguse/opentracks/content/TrackPointsColumns.java
index b32d1fb41..4a4bee190 100644
--- a/src/main/java/de/dennisguse/opentracks/content/TrackPointsColumns.java
+++ b/src/main/java/de/dennisguse/opentracks/content/TrackPointsColumns.java
@@ -19,8 +19,6 @@ package de.dennisguse.opentracks.content;
import android.net.Uri;
import android.provider.BaseColumns;
-import de.dennisguse.opentracks.BuildConfig;
-
/**
* Constants for the track points table.
*
@@ -29,7 +27,7 @@ import de.dennisguse.opentracks.BuildConfig;
public interface TrackPointsColumns extends BaseColumns {
String TABLE_NAME = "trackpoints";
- Uri CONTENT_URI = Uri.parse("content://" + BuildConfig.APPLICATION_ID + "/trackpoints");
+ Uri CONTENT_URI = Uri.parse("content://" + ContentProviderUtils.AUTHORITY + "/trackpoints");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.trackpoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.trackpoint";
String DEFAULT_SORT_ORDER = "_id";
diff --git a/src/main/java/de/dennisguse/opentracks/content/TracksColumns.java b/src/main/java/de/dennisguse/opentracks/content/TracksColumns.java
index ea3f036b2..9d21b0446 100644
--- a/src/main/java/de/dennisguse/opentracks/content/TracksColumns.java
+++ b/src/main/java/de/dennisguse/opentracks/content/TracksColumns.java
@@ -19,8 +19,6 @@ package de.dennisguse.opentracks.content;
import android.net.Uri;
import android.provider.BaseColumns;
-import de.dennisguse.opentracks.BuildConfig;
-
/**
* Constants for the tracks table.
*
@@ -33,7 +31,7 @@ public interface TracksColumns extends BaseColumns {
/**
* Tracks provider uri.
*/
- Uri CONTENT_URI = Uri.parse("content://" + BuildConfig.APPLICATION_ID + "/tracks");
+ Uri CONTENT_URI = Uri.parse("content://" + ContentProviderUtils.AUTHORITY + "/tracks");
/**
* Track content type.
diff --git a/src/main/java/de/dennisguse/opentracks/content/WaypointsColumns.java b/src/main/java/de/dennisguse/opentracks/content/WaypointsColumns.java
index d055175c6..4bd2a3dbc 100644
--- a/src/main/java/de/dennisguse/opentracks/content/WaypointsColumns.java
+++ b/src/main/java/de/dennisguse/opentracks/content/WaypointsColumns.java
@@ -19,8 +19,6 @@ package de.dennisguse.opentracks.content;
import android.net.Uri;
import android.provider.BaseColumns;
-import de.dennisguse.opentracks.BuildConfig;
-
/**
* Constants for waypoints table.
*
@@ -29,7 +27,7 @@ import de.dennisguse.opentracks.BuildConfig;
public interface WaypointsColumns extends BaseColumns {
String TABLE_NAME = "waypoints";
- Uri CONTENT_URI = Uri.parse("content://" + BuildConfig.APPLICATION_ID + "/waypoints");
+ Uri CONTENT_URI = Uri.parse("content://" + ContentProviderUtils.AUTHORITY + "/waypoints");
String CONTENT_TYPE = "vnd.android.cursor.dir/vnd.de.dennisguse.waypoint";
String CONTENT_ITEMTYPE = "vnd.android.cursor.item/vnd.de.dennisguse.waypoint";
String DEFAULT_SORT_ORDER = "_id";