Skip to content

Commit

Permalink
Merge branch 'master' of https://github.com/romannurik/muzei
Browse files Browse the repository at this point in the history
  • Loading branch information
romannurik committed Jan 31, 2017
2 parents 31791d4 + 0532567 commit 168b22a
Show file tree
Hide file tree
Showing 28 changed files with 872 additions and 260 deletions.
7 changes: 3 additions & 4 deletions android-client-common/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand All @@ -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"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.TimeUnit;

/**
* Update the latest artwork in the Direct Boot cache directory whenever the artwork changes
Expand All @@ -26,6 +27,7 @@
public class DirectBootCacheJobService extends JobService {
private static final String TAG = "DirectBootCacheJS";
private static final int DIRECT_BOOT_CACHE_JOB_ID = 68;
private static final long DIRECT_BOOT_CACHE_DELAY_MILLIS = TimeUnit.SECONDS.toMillis(15);
private static final String DIRECT_BOOT_CACHE_FILENAME = "current";

private AsyncTask<Void, Void, Boolean> mCacheTask = null;
Expand All @@ -37,6 +39,9 @@ static void scheduleDirectBootCacheJob(Context context) {
.addTriggerContentUri(new JobInfo.TriggerContentUri(
MuzeiContract.Artwork.CONTENT_URI,
JobInfo.TriggerContentUri.FLAG_NOTIFY_FOR_DESCENDANTS))
// Wait to avoid unnecessarily copying artwork when the user is
// quickly switching artwork
.setTriggerContentUpdateDelay(DIRECT_BOOT_CACHE_DELAY_MILLIS)
.build());
}

Expand Down Expand Up @@ -79,6 +84,8 @@ protected void onPostExecute(Boolean success) {
}
};
mCacheTask.execute();
// Schedule the job again to catch the next update to the artwork
scheduleDirectBootCacheJob(this);
return true;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
Expand All @@ -302,7 +303,8 @@ private int deleteArtwork(@NonNull final Uri uri, final String selection, final
// and manually delete each artwork file
String[] projection = new String[] {
MuzeiContract.Artwork.TABLE_NAME + "." + BaseColumns._ID,
MuzeiContract.Artwork.COLUMN_NAME_IMAGE_URI};
MuzeiContract.Artwork.COLUMN_NAME_IMAGE_URI,
MuzeiContract.Artwork.COLUMN_NAME_TOKEN};
Cursor rowsToDelete = queryArtwork(uri, projection, finalWhere, selectionArgs,
MuzeiContract.Artwork.COLUMN_NAME_IMAGE_URI);
if (rowsToDelete == null) {
Expand All @@ -325,13 +327,33 @@ private int deleteArtwork(@NonNull final Uri uri, final String selection, final
Uri artworkUri = ContentUris.withAppendedId(MuzeiContract.Artwork.CONTENT_URI,
rowsToDelete.getLong(0));
String imageUri = rowsToDelete.getString(1);
if (TextUtils.isEmpty(imageUri)) {
// An empty image URI means the artwork is unique to this specific row
String token = rowsToDelete.getString(2);
if (TextUtils.isEmpty(imageUri) && TextUtils.isEmpty(token)) {
// An empty image URI and token means the artwork is unique to this specific row
// so we can always delete it when the associated row is deleted
File artwork = getCacheFileForArtworkUri(artworkUri);
if (artwork != null && artwork.exists()) {
artwork.delete();
}
} else if (TextUtils.isEmpty(imageUri)) {
// Check if there are other rows using this same token that aren't
// in the list of ids to delete
Cursor otherArtwork = queryArtwork(MuzeiContract.Artwork.CONTENT_URI,
new String[] {MuzeiContract.Artwork.TABLE_NAME + "." + BaseColumns._ID},
MuzeiContract.Artwork.COLUMN_NAME_TOKEN + "=? AND " + notInDeleteIds,
new String[] { token }, null);
if (otherArtwork == null) {
continue;
}
if (otherArtwork.getCount() == 0) {
// There's no non-deleted rows that reference this same artwork URI
// so we can delete the artwork
File artwork = getCacheFileForArtworkUri(artworkUri);
if (artwork != null && artwork.exists()) {
artwork.delete();
}
}
otherArtwork.close();
} else {
// Check if there are other rows using this same image URI that aren't
// in the list of ids to delete
Expand Down Expand Up @@ -418,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);
Expand Down Expand Up @@ -781,7 +812,8 @@ private File getCacheFileForArtworkUri(Uri artworkUri) {
if (!directory.exists() && !directory.mkdirs()) {
return null;
}
String[] projection = { BaseColumns._ID, MuzeiContract.Artwork.COLUMN_NAME_IMAGE_URI };
String[] projection = { BaseColumns._ID, MuzeiContract.Artwork.COLUMN_NAME_IMAGE_URI,
MuzeiContract.Artwork.COLUMN_NAME_TOKEN };
Cursor data = queryArtwork(artworkUri, projection, null, null, null);
if (data == null) {
return null;
Expand All @@ -793,27 +825,32 @@ private File getCacheFileForArtworkUri(Uri artworkUri) {
// While normally we'd use data.getLong(), we later need this as a String so the automatic conversion helps here
String id = data.getString(0);
String imageUri = data.getString(1);
String token = data.getString(2);
data.close();
if (TextUtils.isEmpty(imageUri)) {
if (TextUtils.isEmpty(imageUri) && TextUtils.isEmpty(token)) {
return new File(directory, id);
}
// Otherwise, create a unique filename based on the imageUri
Uri uri = Uri.parse(imageUri);
// Otherwise, create a unique filename based on the imageUri and token
StringBuilder filename = new StringBuilder();
filename.append(uri.getScheme()).append("_")
.append(uri.getHost()).append("_");
String encodedPath = uri.getEncodedPath();
if (!TextUtils.isEmpty(encodedPath)) {
int length = encodedPath.length();
if (length > 60) {
encodedPath = encodedPath.substring(length - 60);
if (!TextUtils.isEmpty(imageUri)) {
Uri uri = Uri.parse(imageUri);
filename.append(uri.getScheme()).append("_")
.append(uri.getHost()).append("_");
String encodedPath = uri.getEncodedPath();
if (!TextUtils.isEmpty(encodedPath)) {
int length = encodedPath.length();
if (length > 60) {
encodedPath = encodedPath.substring(length - 60);
}
encodedPath = encodedPath.replace('/', '_');
filename.append(encodedPath).append("_");
}
encodedPath = encodedPath.replace('/', '_');
filename.append(encodedPath).append("_");
}
// Use the imageUri if available, otherwise use the token
String unique = !TextUtils.isEmpty(imageUri) ? imageUri : token;
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(uri.toString().getBytes("UTF-8"));
md.update(unique.getBytes("UTF-8"));
byte[] digest = md.digest();
for (byte b : digest) {
if ((0xff & b) < 0x10) {
Expand All @@ -823,7 +860,7 @@ private File getCacheFileForArtworkUri(Uri artworkUri) {
}
}
} catch (NoSuchAlgorithmException | UnsupportedEncodingException e) {
filename.append(uri.toString().hashCode());
filename.append(unique.hashCode());
}
return new File(directory, filename.toString());
}
Expand Down Expand Up @@ -860,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)) {
Expand Down
2 changes: 1 addition & 1 deletion api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ props.load(new FileInputStream(file('../local.properties')))

def android = [
sdk: props["sdk.dir"],
target: 'android-22'
target: 'android-25'
]

allprojects { ext."signing.keyId" = props["signing.keyId"] }
Expand Down
16 changes: 12 additions & 4 deletions api/src/main/java/com/google/android/apps/muzei/api/Artwork.java
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,16 @@
* <p> To create an instance, use the {@link Artwork.Builder} class.
*/
public class Artwork {
public static final String FONT_TYPE_DEFAULT = "";
public static final String FONT_TYPE_ELEGANT = "elegant";
/**
* @deprecated use {@link com.google.android.apps.muzei.api.MuzeiContract.Artwork#META_FONT_TYPE_DEFAULT}
*/
@Deprecated
public static final String FONT_TYPE_DEFAULT = MuzeiContract.Artwork.META_FONT_TYPE_DEFAULT;
/**
* @deprecated use {@link com.google.android.apps.muzei.api.MuzeiContract.Artwork#META_FONT_TYPE_ELEGANT}
*/
@Deprecated
public static final String FONT_TYPE_ELEGANT = MuzeiContract.Artwork.META_FONT_TYPE_ELEGANT;

private static final String KEY_COMPONENT_NAME = "componentName";
private static final String KEY_IMAGE_URI = "imageUri";
Expand Down Expand Up @@ -355,8 +363,8 @@ public Builder viewIntent(Intent viewIntent) {
/**
* Sets the font type to use to show metadata for the artwork.
*
* @see #FONT_TYPE_DEFAULT
* @see #FONT_TYPE_ELEGANT
* @see com.google.android.apps.muzei.api.MuzeiContract.Artwork#META_FONT_TYPE_DEFAULT
* @see com.google.android.apps.muzei.api.MuzeiContract.Artwork#META_FONT_TYPE_ELEGANT
*/
public Builder metaFont(String metaFont) {
mArtwork.mMetaFont = metaFont;
Expand Down

0 comments on commit 168b22a

Please sign in to comment.