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";