Permalink
Browse files

Allow third party apps to directly insert artwork and update their so…

…urce info

Make the WRITE_PROVIDER permission available to third party apps (rather than a signature permission) to allow apps to use the new createArtwork() and updateSource() methods directly.

Hardens the MuzeiProvider to prevent unsupported operations by third party apps:
- Only Muzei can delete artwork or sources
- Apps can only insert artwork associated with one of their MuzeiArtSources
- Only Muzei can insert sources
- Apps can only update their own source(s)
  • Loading branch information...
1 parent 0d58ff5 commit 6134824615bdb68c6aa2285b2df0d6659b80e1d4 @ianhanniballake ianhanniballake committed Jan 20, 2017
@@ -17,7 +17,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="net.nurik.roman.muzei.androidclientcommon">
- <permission android:name="com.google.android.apps.muzei.WRITE_PROVIDER" android:protectionLevel="signature"/>
+ <permission android:name="com.google.android.apps.muzei.WRITE_PROVIDER" />
<uses-permission android:name="com.google.android.apps.muzei.WRITE_PROVIDER" />
<application>
@@ -26,9 +26,8 @@
android:authorities="com.google.android.apps.muzei"
android:directBootAware="true"
android:exported="true"
- android:writePermission="com.google.android.apps.muzei.WRITE_PROVIDER">
- <grant-uri-permission android:pathPattern=".*"/>
- </provider>
+ android:grantUriPermissions="true"
+ android:writePermission="com.google.android.apps.muzei.WRITE_PROVIDER" />
<service
android:name="com.google.android.apps.muzei.provider.DirectBootCacheJobService"
android:exported="true"
@@ -31,6 +31,7 @@
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.database.Cursor;
+import android.database.DatabaseUtils;
import android.database.SQLException;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
@@ -270,15 +271,15 @@ public int delete(@NonNull final Uri uri, final String selection, final String[]
Log.w(TAG, "Deletes are not supported until the user is unlocked");
return 0;
}
+ String callingPackageName = context.getPackageManager().getNameForUid(
+ Binder.getCallingUid());
+ // Only allow Muzei to delete content
+ if (!context.getPackageName().equals(callingPackageName)) {
+ throw new UnsupportedOperationException("Deletes are not supported");
+ }
if (MuzeiProvider.uriMatcher.match(uri) == MuzeiProvider.ARTWORK ||
MuzeiProvider.uriMatcher.match(uri) == MuzeiProvider.ARTWORK_ID) {
- String callingPackageName = context.getPackageManager().getNameForUid(
- Binder.getCallingUid());
- if (context.getPackageName().equals(callingPackageName)) {
- return deleteArtwork(uri, selection, selectionArgs);
- } else {
- throw new UnsupportedOperationException("Deletes are not supported");
- }
+ return deleteArtwork(uri, selection, selectionArgs);
} else if (MuzeiProvider.uriMatcher.match(uri) == MuzeiProvider.SOURCES ||
MuzeiProvider.uriMatcher.match(uri) == MuzeiProvider.SOURCE_ID) {
return deleteSource(uri, selection, selectionArgs);
@@ -439,13 +440,22 @@ public String getType(@NonNull final Uri uri) {
@Override
public Uri insert(@NonNull final Uri uri, final ContentValues values) {
- if (!UserManagerCompat.isUserUnlocked(getContext())) {
+ Context context = getContext();
+ if (context == null) {
+ return null;
+ }
+ if (!UserManagerCompat.isUserUnlocked(context)) {
Log.w(TAG, "Inserts are not supported until the user is unlocked");
return null;
}
if (MuzeiProvider.uriMatcher.match(uri) == MuzeiProvider.ARTWORK) {
return insertArtwork(uri, values);
} else if (MuzeiProvider.uriMatcher.match(uri) == MuzeiProvider.SOURCES) {
+ // Ensure the app inserting the source is Muzei
+ String callingPackageName = context.getPackageManager().getNameForUid(Binder.getCallingUid());
+ if (!context.getPackageName().equals(callingPackageName)) {
+ throw new UnsupportedOperationException("Inserting sources is not supported, use update");
+ }
return insertSource(uri, values);
} else {
throw new IllegalArgumentException("Unknown URI " + uri);
@@ -887,26 +897,23 @@ private int updateSource(@NonNull final Uri uri, final ContentValues values, fin
}
final SQLiteDatabase db = databaseHelper.getWritableDatabase();
- int count;
- switch (MuzeiProvider.uriMatcher.match(uri))
- {
- case SOURCES:
- // If the incoming URI matches the general sources pattern, does the update based on the incoming
- // data.
- count = db.update(MuzeiContract.Sources.TABLE_NAME, values, selection, selectionArgs);
- break;
- case SOURCE_ID:
- // If the incoming URI matches a single source ID, does the update based on the incoming data, but
- // modifies the where clause to restrict it to the particular source ID.
- String finalWhere = BaseColumns._ID + " = " + uri.getLastPathSegment();
- // If there were additional selection criteria, append them to the final WHERE clause
- if (selection != null)
- finalWhere = finalWhere + " AND " + selection;
- count = db.update(MuzeiContract.Sources.TABLE_NAME, values, finalWhere, selectionArgs);
- break;
- default:
- throw new IllegalArgumentException("Unknown URI " + uri);
+ String finalWhere = selection;
+ String[] finalSelectionArgs = selectionArgs;
+ if (MuzeiProvider.uriMatcher.match(uri) == SOURCE_ID) {
+ // If the incoming URI matches a single source ID, does the update based on the incoming data, but
+ // modifies the where clause to restrict it to the particular source ID.
+ finalWhere = DatabaseUtils.concatenateWhere(finalWhere,
+ BaseColumns._ID + " = " + uri.getLastPathSegment());
}
+ String callingPackageName = context.getPackageManager().getNameForUid(Binder.getCallingUid());
+ if (!context.getPackageName().equals(callingPackageName)) {
+ // Only allow other apps to update their own source
+ finalWhere = DatabaseUtils.concatenateWhere(finalWhere,
+ MuzeiContract.Sources.COLUMN_NAME_COMPONENT_NAME + " LIKE ?");
+ finalSelectionArgs = DatabaseUtils.appendSelectionArgs(finalSelectionArgs,
+ new String[] {callingPackageName +"/%"});
+ }
+ int count = db.update(MuzeiContract.Sources.TABLE_NAME, values, finalWhere, finalSelectionArgs);
if (count > 0) {
notifyChange(uri);
} else if (values.containsKey(MuzeiContract.Sources.COLUMN_NAME_COMPONENT_NAME)) {
@@ -51,6 +51,15 @@
*/
public class MuzeiContract {
/**
+ * To insert new artwork and update their source information, apps must hold this permission by declaring
+ * it in their manifest:
+ * <pre>
+ * <uses-permission android:name="com.google.android.apps.muzei.WRITE_PROVIDER" />
+ * </pre>
+ *
+ */
+ public static final String WRITE_PERMISSION = "com.google.android.apps.muzei.WRITE_PROVIDER";
+ /**
* Base authority for this content provider
*/
public static final String AUTHORITY = "com.google.android.apps.muzei";
@@ -201,6 +210,13 @@ private Artwork() {
/**
* The content:// style URL for this table. This is the main entry point for queries and for
* opening an {@link java.io.InputStream InputStream} to the current artwork's image.
+ * <p>
+ * All apps can {@link ContentResolver#query query} for artwork, but only apps holding
+ * {@link #WRITE_PERMISSION} can {@link ContentResolver#insert insert} new artwork.
+ *
+ * @see #getCurrentArtwork
+ * @see #getCurrentArtworkBitmap
+ * @see #createArtwork
*/
public static final Uri CONTENT_URI = Uri.parse(MuzeiContract.SCHEME + MuzeiContract.AUTHORITY
+ "/" + Artwork.TABLE_NAME);
@@ -605,6 +621,11 @@ private Sources() {
/**
* The content:// style URL for this table.
+ * <p>
+ * All apps can {@link ContentResolver#query query} for source info, but only apps holding
+ * {@link #WRITE_PERMISSION} can {@link ContentResolver#update update} their source info.
+ *
+ * @see #updateSource
*/
public static final Uri CONTENT_URI = Uri.parse(MuzeiContract.SCHEME + MuzeiContract.AUTHORITY
+ "/" + Sources.TABLE_NAME);

0 comments on commit 6134824

Please sign in to comment.