From 9f665e07fdb74ec5baafc3be906702ef62dadfcf Mon Sep 17 00:00:00 2001 From: Dennis Guse Date: Tue, 20 Aug 2019 22:45:17 +0200 Subject: [PATCH] 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. --- src/main/AndroidManifest.xml | 7 +- .../android/ContentResolverWrapper.java | 40 +++++++ .../opentracks/android/IContentResolver.java | 27 +++++ .../content/ContentProviderUtilsImpl.java | 22 ++-- .../content/CustomContentProvider.java | 2 +- .../content/ShareContentProvider.java | 107 +++++++++--------- 6 files changed, 135 insertions(+), 70 deletions(-) create mode 100644 src/main/java/de/dennisguse/opentracks/android/ContentResolverWrapper.java create mode 100644 src/main/java/de/dennisguse/opentracks/android/IContentResolver.java diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml index 7f192c1f5..9e29de023 100644 --- a/src/main/AndroidManifest.xml +++ b/src/main/AndroidManifest.xml @@ -173,14 +173,9 @@ limitations under the License. android:resource="@xml/track_widget_info" /> - diff --git a/src/main/java/de/dennisguse/opentracks/android/ContentResolverWrapper.java b/src/main/java/de/dennisguse/opentracks/android/ContentResolverWrapper.java new file mode 100644 index 000000000..cdfe66ac6 --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/android/ContentResolverWrapper.java @@ -0,0 +1,40 @@ +package de.dennisguse.opentracks.android; + +import android.content.ContentResolver; +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +public class ContentResolverWrapper implements IContentResolver { + + private ContentResolver contentResolver; + + public ContentResolverWrapper(ContentResolver contentResolver) { + this.contentResolver = contentResolver; + } + + @Nullable + public Uri insert(@NonNull Uri url, @Nullable ContentValues values) { + return contentResolver.insert(url, values); + } + + public int bulkInsert(@NonNull Uri url, @NonNull ContentValues[] values) { + return contentResolver.bulkInsert(url, values); + } + + @Nullable + public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder) { + return contentResolver.query(uri, projection, selection, selectionArgs, sortOrder); + } + + public int update(@NonNull Uri uri, @Nullable ContentValues values, @Nullable String where, @Nullable String[] selectionArgs) { + return contentResolver.update(uri, values, where, selectionArgs); + } + + public int delete(@NonNull Uri url, @Nullable String where, @Nullable String[] selectionArgs) { + return contentResolver.delete(url, where, selectionArgs); + } +} diff --git a/src/main/java/de/dennisguse/opentracks/android/IContentResolver.java b/src/main/java/de/dennisguse/opentracks/android/IContentResolver.java new file mode 100644 index 000000000..a04617f3e --- /dev/null +++ b/src/main/java/de/dennisguse/opentracks/android/IContentResolver.java @@ -0,0 +1,27 @@ +package de.dennisguse.opentracks.android; + +import android.content.ContentValues; +import android.database.Cursor; +import android.net.Uri; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresPermission; + +/** + * Defines the interface actually shared by {@link android.content.ContentProvider} and {@link android.content.ContentResolver}. + * So, both can be used interchangeably. + */ +public interface IContentResolver { + @Nullable + Uri insert(@RequiresPermission.Write @NonNull Uri url, @Nullable ContentValues values); + + int bulkInsert(@RequiresPermission.Write @NonNull Uri url, @NonNull ContentValues[] values); + + @Nullable + Cursor query(@RequiresPermission.Read @NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, @Nullable String[] selectionArgs, @Nullable String sortOrder); + + int update(@RequiresPermission.Write @NonNull Uri uri, @Nullable ContentValues values, @Nullable String where, @Nullable String[] selectionArgs); + + int delete(@RequiresPermission.Write @NonNull Uri url, @Nullable String where, @Nullable String[] selectionArgs); +} \ No newline at end of file diff --git a/src/main/java/de/dennisguse/opentracks/content/ContentProviderUtilsImpl.java b/src/main/java/de/dennisguse/opentracks/content/ContentProviderUtilsImpl.java index 31d69b8e7..f119e7c2d 100644 --- a/src/main/java/de/dennisguse/opentracks/content/ContentProviderUtilsImpl.java +++ b/src/main/java/de/dennisguse/opentracks/content/ContentProviderUtilsImpl.java @@ -24,18 +24,21 @@ import android.location.Location; import android.net.Uri; import android.util.Log; -import de.dennisguse.opentracks.content.Waypoint.WaypointType; -import de.dennisguse.opentracks.content.sensor.SensorDataSet; -import de.dennisguse.opentracks.stats.TripStatistics; -import de.dennisguse.opentracks.util.FileUtils; - import java.io.File; import java.util.ArrayList; import java.util.List; import java.util.NoSuchElementException; +import de.dennisguse.opentracks.android.ContentResolverWrapper; +import de.dennisguse.opentracks.android.IContentResolver; +import de.dennisguse.opentracks.content.Waypoint.WaypointType; +import de.dennisguse.opentracks.content.sensor.SensorDataSet; +import de.dennisguse.opentracks.stats.TripStatistics; +import de.dennisguse.opentracks.util.FileUtils; + /** * {@link ContentProviderUtils} implementation. + * Allows to use {@link ContentResolver} and {@link android.content.ContentProvider} interchangeably via {@link IContentResolver}. * * @author Leif Hendrik Wilden */ @@ -45,10 +48,14 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils { private static final int MAX_LATITUDE = 90000000; - private final ContentResolver contentResolver; + private final IContentResolver contentResolver; private int defaultCursorBatchSize = 2000; public ContentProviderUtilsImpl(ContentResolver contentResolver) { + this.contentResolver = new ContentResolverWrapper(contentResolver); + } + + public ContentProviderUtilsImpl(IContentResolver contentResolver) { this.contentResolver = contentResolver; } @@ -716,8 +723,7 @@ public class ContentProviderUtilsImpl implements ContentProviderUtils { if (maxWaypoints >= 0) { sortOrder += " LIMIT " + maxWaypoints; } - return contentResolver.query( - WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder); + return contentResolver.query(WaypointsColumns.CONTENT_URI, projection, selection, selectionArgs, sortOrder); } @Override diff --git a/src/main/java/de/dennisguse/opentracks/content/CustomContentProvider.java b/src/main/java/de/dennisguse/opentracks/content/CustomContentProvider.java index 792927554..f599c3cc0 100644 --- a/src/main/java/de/dennisguse/opentracks/content/CustomContentProvider.java +++ b/src/main/java/de/dennisguse/opentracks/content/CustomContentProvider.java @@ -40,7 +40,7 @@ import androidx.annotation.VisibleForTesting; * * @author Leif Hendrik Wilden */ -public class CustomContentProvider extends ContentProvider { +public abstract class CustomContentProvider extends ContentProvider { @VisibleForTesting static final int DATABASE_VERSION = 23; diff --git a/src/main/java/de/dennisguse/opentracks/content/ShareContentProvider.java b/src/main/java/de/dennisguse/opentracks/content/ShareContentProvider.java index 701644896..a931dcb33 100644 --- a/src/main/java/de/dennisguse/opentracks/content/ShareContentProvider.java +++ b/src/main/java/de/dennisguse/opentracks/content/ShareContentProvider.java @@ -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(); - } }