mirror of
https://codeberg.org/OpenTracksApp/OpenTracks.git
synced 2026-10-03 01:53:05 +02:00
Show on map: everything is handled by one content provider alone.
To avoid issues forwarding a requests from (temporarily) exported content provider to internal content provider only one content provider is used.
This commit is contained in:
@@ -1,9 +1,7 @@
|
||||
package de.dennisguse.opentracks.content;
|
||||
|
||||
import android.content.ContentProvider;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.UriMatcher;
|
||||
import android.content.pm.ProviderInfo;
|
||||
import android.database.Cursor;
|
||||
import android.database.MatrixCursor;
|
||||
@@ -22,24 +20,33 @@ import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.util.Arrays;
|
||||
|
||||
import de.dennisguse.opentracks.BuildConfig;
|
||||
import de.dennisguse.opentracks.android.IContentResolver;
|
||||
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.
|
||||
* A content provider that mimics the behavior of {@link androidx.core.content.FileProvider}, which shares virtual (non-existing) KML-files.
|
||||
* The actual content of the virtual files is generated by using the functionality defined in {@link CustomContentProvider}.
|
||||
*
|
||||
* Moreover, it manages access to OpenTrack's database via {@link CustomContentProvider}.
|
||||
*
|
||||
* Explanation:
|
||||
* Although a request is handled by a {@link android.content.ContentProvider} (with temporarily granted permission), Android's security infrastructure prevents forwarding queries to non-exported {@link android.content.ContentProvider}.
|
||||
* Thus, if {@link ShareContentProvider} and {@link CustomContentProvider} would be two different instances, the data would not be accessible to external apps.
|
||||
* While handling a request {@link ShareContentProvider} could `grantPermissions()` to the calling app for {@link CustomContentProvider}'s URI.
|
||||
* However, while handling the request this would allow the calling app to actually contact {@link CustomContentProvider} directly and get access to stored data that should remain private.
|
||||
*
|
||||
*/
|
||||
public class ShareContentProvider extends ContentProvider {
|
||||
|
||||
public static final String TAG = ShareContentProvider.class.getCanonicalName();
|
||||
|
||||
public static final String SHAREPROVIDER = BuildConfig.APPLICATION_ID;
|
||||
public class ShareContentProvider extends CustomContentProvider implements IContentResolver {
|
||||
|
||||
private static final String[] COLUMNS = {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE};
|
||||
|
||||
public static final String TAG = ShareContentProvider.class.getCanonicalName();
|
||||
|
||||
public static String MIME = "application/kml+xml";
|
||||
private static final int URI_KML = 1;
|
||||
private final UriMatcher uriMatcher = new UriMatcher(UriMatcher.NO_MATCH);
|
||||
|
||||
public static Uri createURI(long[] trackIds) {
|
||||
if (trackIds.length == 0) {
|
||||
@@ -52,7 +59,31 @@ public class ShareContentProvider extends ContentProvider {
|
||||
}
|
||||
builder.deleteCharAt(builder.lastIndexOf(","));
|
||||
|
||||
return Uri.parse("content://" + SHAREPROVIDER + "/" + builder + ".kml");
|
||||
return Uri.parse("content://" + ContentProviderUtils.AUTHORITY + "/" + TracksColumns.TABLE_NAME + "/kml/" + builder + ".kml");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreate() {
|
||||
uriMatcher.addURI(ContentProviderUtils.AUTHORITY, TracksColumns.TABLE_NAME + "/kml/*", URI_KML);
|
||||
return super.onCreate();
|
||||
}
|
||||
|
||||
/**
|
||||
* Do not allow to be exported via AndroidManifest.
|
||||
* Check that caller has permissions to access {@link CustomContentProvider}.
|
||||
*/
|
||||
@Override
|
||||
public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) {
|
||||
super.attachInfo(context, info);
|
||||
|
||||
// Sanity check our security
|
||||
if (info.exported) {
|
||||
throw new UnsupportedOperationException("Provider must not be exported");
|
||||
}
|
||||
|
||||
if (!info.grantUriPermissions) {
|
||||
throw new SecurityException("Provider must grant uri permissions");
|
||||
}
|
||||
}
|
||||
|
||||
private static long[] parseURI(Uri uri) {
|
||||
@@ -69,21 +100,12 @@ public class ShareContentProvider extends ContentProvider {
|
||||
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) {
|
||||
if (uriMatcher.match(uri) != URI_KML) {
|
||||
return super.query(uri, projection, selection, selectionArgs, sortOrder);
|
||||
}
|
||||
|
||||
// ContentProvider has already checked granted permissions
|
||||
if (projection == null) {
|
||||
projection = COLUMNS;
|
||||
@@ -113,16 +135,16 @@ public class ShareContentProvider extends ContentProvider {
|
||||
@Nullable
|
||||
@Override
|
||||
public String getType(@NonNull Uri uri) {
|
||||
return MIME;
|
||||
if (uriMatcher.match(uri) == URI_KML) {
|
||||
return MIME;
|
||||
}
|
||||
return super.getType(uri);
|
||||
}
|
||||
|
||||
@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);
|
||||
ContentProviderUtils contentProviderUtils = new ContentProviderUtilsImpl(this);
|
||||
|
||||
long[] trackIds = parseURI(uri);
|
||||
final Track[] tracks = new Track[trackIds.length];
|
||||
@@ -141,35 +163,10 @@ public class ShareContentProvider extends ContentProvider {
|
||||
} 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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user