diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 1ee61abb..d0c73348 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -190,6 +190,18 @@
android:name="android.appwidget.provider"
android:resource="@xml/app_widget_small_info" />
+
+
+
+
+
+
+
diff --git a/app/src/main/java/com/kabouzeid/gramophone/appwidgets/AppWidgetCard.java b/app/src/main/java/com/kabouzeid/gramophone/appwidgets/AppWidgetCard.java
new file mode 100644
index 00000000..5504e8b4
--- /dev/null
+++ b/app/src/main/java/com/kabouzeid/gramophone/appwidgets/AppWidgetCard.java
@@ -0,0 +1,229 @@
+package com.kabouzeid.gramophone.appwidgets;
+
+import android.app.PendingIntent;
+import android.appwidget.AppWidgetManager;
+import android.content.ComponentName;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Bitmap;
+import android.graphics.drawable.BitmapDrawable;
+import android.graphics.drawable.Drawable;
+import android.support.annotation.Nullable;
+import android.support.v7.graphics.Palette;
+import android.text.TextUtils;
+import android.view.View;
+import android.widget.RemoteViews;
+
+import com.bumptech.glide.Glide;
+import com.bumptech.glide.request.animation.GlideAnimation;
+import com.bumptech.glide.request.target.SimpleTarget;
+import com.bumptech.glide.request.target.Target;
+import com.kabouzeid.appthemehelper.util.MaterialValueHelper;
+import com.kabouzeid.gramophone.R;
+import com.kabouzeid.gramophone.glide.SongGlideRequest;
+import com.kabouzeid.gramophone.glide.palette.BitmapPaletteWrapper;
+import com.kabouzeid.gramophone.model.Song;
+import com.kabouzeid.gramophone.service.MusicService;
+import com.kabouzeid.gramophone.ui.activities.MainActivity;
+import com.kabouzeid.gramophone.util.Util;
+
+public class AppWidgetCard extends BaseAppWidget {
+ public static final String NAME = "app_widget_card";
+
+ private static AppWidgetCard mInstance;
+ private Target target; // for cancellation
+
+ private static int imageSize = 0;
+ private static float cardRadius = 0f;
+
+ public static synchronized AppWidgetCard getInstance() {
+ if (mInstance == null) {
+ mInstance = new AppWidgetCard();
+ }
+ return mInstance;
+ }
+
+ /**
+ * {@inheritDoc}
+ */
+ @Override
+ public void onUpdate(final Context context, final AppWidgetManager appWidgetManager,
+ final int[] appWidgetIds) {
+ defaultAppWidget(context, appWidgetIds);
+ final Intent updateIntent = new Intent(MusicService.APP_WIDGET_UPDATE);
+ updateIntent.putExtra(MusicService.EXTRA_APP_WIDGET_NAME, NAME);
+ updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);
+ updateIntent.addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY);
+ context.sendBroadcast(updateIntent);
+ }
+
+ /**
+ * Initialize given widgets to default state, where we launch Music on
+ * default click and hide actions if service not running.
+ */
+ private void defaultAppWidget(final Context context, final int[] appWidgetIds) {
+ final RemoteViews appWidgetView = new RemoteViews(context.getPackageName(), R.layout.app_widget_card);
+
+ appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE);
+ appWidgetView.setViewVisibility(R.id.image, View.INVISIBLE);
+ appWidgetView.setImageViewBitmap(R.id.button_next, createBitmap(Util.getTintedVectorDrawable(context, R.drawable.ic_skip_next_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true)), 1f));
+ appWidgetView.setImageViewBitmap(R.id.button_prev, createBitmap(Util.getTintedVectorDrawable(context, R.drawable.ic_skip_previous_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true)), 1f));
+ appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, createBitmap(Util.getTintedVectorDrawable(context, R.drawable.ic_play_arrow_white_24dp, MaterialValueHelper.getSecondaryTextColor(context, true)), 1f));
+
+ linkButtons(context, appWidgetView);
+ pushUpdate(context, appWidgetIds, appWidgetView);
+ }
+
+ private void pushUpdate(final Context context, final int[] appWidgetIds, final RemoteViews views) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ if (appWidgetIds != null) {
+ appWidgetManager.updateAppWidget(appWidgetIds, views);
+ } else {
+ appWidgetManager.updateAppWidget(new ComponentName(context, getClass()), views);
+ }
+ }
+
+ /**
+ * Check against {@link AppWidgetManager} if there are any instances of this
+ * widget.
+ */
+ private boolean hasInstances(final Context context) {
+ final AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
+ final int[] mAppWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(context,
+ getClass()));
+ return mAppWidgetIds.length > 0;
+ }
+
+ /**
+ * Handle a change notification coming over from
+ * {@link MusicService}
+ */
+ public void notifyChange(final MusicService service, final String what) {
+ if (hasInstances(service)) {
+ if (MusicService.META_CHANGED.equals(what) || MusicService.PLAY_STATE_CHANGED.equals(what)) {
+ performUpdate(service, null);
+ }
+ }
+ }
+
+ /**
+ * Update all active widget instances by pushing changes
+ */
+ public void performUpdate(final MusicService service, final int[] appWidgetIds) {
+ final RemoteViews appWidgetView = new RemoteViews(service.getPackageName(), R.layout.app_widget_card);
+
+ final boolean isPlaying = service.isPlaying();
+ final Song song = service.getCurrentSong();
+
+ // Set the titles and artwork
+ if (TextUtils.isEmpty(song.title) && TextUtils.isEmpty(song.artistName)) {
+ appWidgetView.setViewVisibility(R.id.media_titles, View.INVISIBLE);
+ } else {
+ if (TextUtils.isEmpty(song.artistName) || TextUtils.isEmpty(song.albumName)) {
+ appWidgetView.setTextViewText(R.id.text_separator, "");
+ } else {
+ appWidgetView.setTextViewText(R.id.text_separator, "•");
+ }
+
+ appWidgetView.setViewVisibility(R.id.media_titles, View.VISIBLE);
+ appWidgetView.setTextViewText(R.id.title, song.title);
+ appWidgetView.setTextViewText(R.id.artist, song.artistName);
+ appWidgetView.setTextViewText(R.id.album, song.albumName);
+ }
+
+ // Set correct drawable for pause state
+ int playPauseRes = isPlaying ? R.drawable.ic_pause_white_24dp : R.drawable.ic_play_arrow_white_24dp;
+ appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, createBitmap(Util.getTintedVectorDrawable(service, playPauseRes, MaterialValueHelper.getSecondaryTextColor(service, true)), 1f));
+
+ // Set prev/next button drawables
+ appWidgetView.setImageViewBitmap(R.id.button_next, createBitmap(Util.getTintedVectorDrawable(service, R.drawable.ic_skip_next_white_24dp, MaterialValueHelper.getSecondaryTextColor(service, true)), 1f));
+ appWidgetView.setImageViewBitmap(R.id.button_prev, createBitmap(Util.getTintedVectorDrawable(service, R.drawable.ic_skip_previous_white_24dp, MaterialValueHelper.getSecondaryTextColor(service, true)), 1f));
+
+ // Link actions buttons to intents
+ linkButtons(service, appWidgetView);
+
+ if (imageSize == 0)
+ imageSize = service.getResources().getDimensionPixelSize(R.dimen.app_widget_card_image_size);
+ if (cardRadius == 0f)
+ cardRadius = service.getResources().getDimension(R.dimen.app_widget_card_radius);
+
+ // Load the album cover async and push the update on completion
+ service.runOnUiThread(new Runnable() {
+ @Override
+ public void run() {
+ if (target != null) {
+ Glide.clear(target);
+ }
+ target = SongGlideRequest.Builder.from(Glide.with(service), song)
+ .checkIgnoreMediaStore(service)
+ .generatePalette(service).build()
+ .into(new SimpleTarget(imageSize, imageSize) {
+ @Override
+ public void onResourceReady(BitmapPaletteWrapper resource, GlideAnimation super BitmapPaletteWrapper> glideAnimation) {
+ Palette palette = resource.getPalette();
+ update(resource.getBitmap(), palette.getVibrantColor(palette.getMutedColor(MaterialValueHelper.getSecondaryTextColor(service, true))));
+ }
+
+ @Override
+ public void onLoadFailed(Exception e, Drawable errorDrawable) {
+ super.onLoadFailed(e, errorDrawable);
+ update(null, MaterialValueHelper.getSecondaryTextColor(service, true));
+ }
+
+ private void update(@Nullable Bitmap bitmap, int color) {
+ appWidgetView.setViewVisibility(R.id.image, View.VISIBLE);
+
+ // Set correct drawable for pause state
+ int playPauseRes = isPlaying ? R.drawable.ic_pause_white_24dp : R.drawable.ic_play_arrow_white_24dp;
+ appWidgetView.setImageViewBitmap(R.id.button_toggle_play_pause, createBitmap(Util.getTintedVectorDrawable(service, playPauseRes, color), 1f));
+
+ // Set prev/next button drawables
+ appWidgetView.setImageViewBitmap(R.id.button_next, createBitmap(Util.getTintedVectorDrawable(service, R.drawable.ic_skip_next_white_24dp, color), 1f));
+ appWidgetView.setImageViewBitmap(R.id.button_prev, createBitmap(Util.getTintedVectorDrawable(service, R.drawable.ic_skip_previous_white_24dp, color), 1f));
+
+ Drawable image;
+
+ if (bitmap == null) {
+ image = service.getResources().getDrawable(R.drawable.default_album_art);
+ } else {
+ image = new BitmapDrawable(bitmap);
+ }
+
+ appWidgetView.setImageViewBitmap(R.id.image, createRoundedBitmap(image, imageSize, imageSize, cardRadius, 0, cardRadius, 0));
+
+ pushUpdate(service, appWidgetIds, appWidgetView);
+ }
+ });
+ }
+ });
+ }
+
+ /**
+ * Link up various button actions using {@link PendingIntent}.
+ */
+ private void linkButtons(final Context context, final RemoteViews views) {
+ Intent action;
+ PendingIntent pendingIntent;
+
+ final ComponentName serviceName = new ComponentName(context, MusicService.class);
+
+ // Home
+ action = new Intent(context, MainActivity.class);
+ action.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_CLEAR_TOP);
+ pendingIntent = PendingIntent.getActivity(context, 0, action, 0);
+ views.setOnClickPendingIntent(R.id.image, pendingIntent);
+ views.setOnClickPendingIntent(R.id.media_titles, pendingIntent);
+
+ // Previous track
+ pendingIntent = buildPendingIntent(context, MusicService.ACTION_REWIND, serviceName);
+ views.setOnClickPendingIntent(R.id.button_prev, pendingIntent);
+
+ // Play and pause
+ pendingIntent = buildPendingIntent(context, MusicService.ACTION_TOGGLE_PAUSE, serviceName);
+ views.setOnClickPendingIntent(R.id.button_toggle_play_pause, pendingIntent);
+
+ // Next track
+ pendingIntent = buildPendingIntent(context, MusicService.ACTION_SKIP, serviceName);
+ views.setOnClickPendingIntent(R.id.button_next, pendingIntent);
+ }
+}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/appwidgets/BaseAppWidget.java b/app/src/main/java/com/kabouzeid/gramophone/appwidgets/BaseAppWidget.java
index 1ba0e4b9..4de0b811 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/appwidgets/BaseAppWidget.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/appwidgets/BaseAppWidget.java
@@ -6,7 +6,11 @@ import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.graphics.Bitmap;
+import android.graphics.BitmapShader;
import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.Path;
+import android.graphics.RectF;
import android.graphics.drawable.Drawable;
/**
@@ -22,6 +26,46 @@ public class BaseAppWidget extends AppWidgetProvider {
return bitmap;
}
+ protected static Bitmap createRoundedBitmap(Drawable drawable, int width, int height, float tl, float tr, float bl, float br) {
+ if (drawable == null) return null;
+
+ Bitmap bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+ Canvas c = new Canvas(bitmap);
+ drawable.setBounds(0, 0, width, height);
+ drawable.draw(c);
+
+ Bitmap rounded = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888);
+
+ Canvas canvas = new Canvas(rounded);
+ Paint paint = new Paint();
+ paint.setShader(new BitmapShader(bitmap, BitmapShader.TileMode.CLAMP, BitmapShader.TileMode.CLAMP));
+ paint.setAntiAlias(true);
+ canvas.drawPath(composeRoundedRectPath(new RectF(0, 0, width, height), tl, tr, bl, br), paint);
+
+ return rounded;
+ }
+
+ protected static Path composeRoundedRectPath(RectF rect, float tl, float tr, float bl, float br) {
+ Path path = new Path();
+ tl = tl < 0 ? 0 : tl;
+ tr = tr < 0 ? 0 : tr;
+ bl = bl < 0 ? 0 : bl;
+ br = br < 0 ? 0 : br;
+
+ path.moveTo(rect.left + tl, rect.top);
+ path.lineTo(rect.right - tr, rect.top);
+ path.quadTo(rect.right, rect.top, rect.right, rect.top + tr);
+ path.lineTo(rect.right, rect.bottom - br);
+ path.quadTo(rect.right, rect.bottom, rect.right - br, rect.bottom);
+ path.lineTo(rect.left + bl, rect.bottom);
+ path.quadTo(rect.left, rect.bottom, rect.left, rect.bottom - bl);
+ path.lineTo(rect.left, rect.top + tl);
+ path.quadTo(rect.left, rect.top, rect.left + tl, rect.top);
+ path.close();
+
+ return path;
+ }
+
protected PendingIntent buildPendingIntent(Context context, final String action, final ComponentName serviceName) {
Intent intent = new Intent(action);
intent.setComponent(serviceName);
diff --git a/app/src/main/java/com/kabouzeid/gramophone/appwidgets/BootReceiver.java b/app/src/main/java/com/kabouzeid/gramophone/appwidgets/BootReceiver.java
index 0773efce..c80a77fe 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/appwidgets/BootReceiver.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/appwidgets/BootReceiver.java
@@ -19,7 +19,8 @@ public class BootReceiver extends BroadcastReceiver {
// Start music service if there are any existing widgets
if (widgetManager.getAppWidgetIds(new ComponentName(context, AppWidgetBig.class)).length > 0 ||
widgetManager.getAppWidgetIds(new ComponentName(context, AppWidgetClassic.class)).length > 0 ||
- widgetManager.getAppWidgetIds(new ComponentName(context, AppWidgetSmall.class)).length > 0) {
+ widgetManager.getAppWidgetIds(new ComponentName(context, AppWidgetSmall.class)).length > 0 ||
+ widgetManager.getAppWidgetIds(new ComponentName(context, AppWidgetCard.class)).length > 0) {
final Intent serviceIntent = new Intent(context, MusicService.class);
context.startService(serviceIntent);
}
diff --git a/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java b/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java
index de5e1999..4e389887 100644
--- a/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java
+++ b/app/src/main/java/com/kabouzeid/gramophone/service/MusicService.java
@@ -40,6 +40,7 @@ import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.kabouzeid.gramophone.R;
import com.kabouzeid.gramophone.appwidgets.AppWidgetBig;
+import com.kabouzeid.gramophone.appwidgets.AppWidgetCard;
import com.kabouzeid.gramophone.appwidgets.AppWidgetClassic;
import com.kabouzeid.gramophone.appwidgets.AppWidgetSmall;
import com.kabouzeid.gramophone.glide.BlurTransformation;
@@ -128,6 +129,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
private AppWidgetBig appWidgetBig = AppWidgetBig.getInstance();
private AppWidgetClassic appWidgetClassic = AppWidgetClassic.getInstance();
private AppWidgetSmall appWidgetSmall = AppWidgetSmall.getInstance();
+ private AppWidgetCard appWidgetCard = AppWidgetCard.getInstance();
private Playback playback;
private ArrayList playingQueue = new ArrayList<>();
@@ -1055,6 +1057,7 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
appWidgetBig.notifyChange(this, what);
appWidgetClassic.notifyChange(this, what);
appWidgetSmall.notifyChange(this, what);
+ appWidgetCard.notifyChange(this, what);
}
private static final long MEDIA_SESSION_ACTIONS = PlaybackStateCompat.ACTION_PLAY
@@ -1295,6 +1298,9 @@ public class MusicService extends Service implements SharedPreferences.OnSharedP
} else if (AppWidgetBig.NAME.equals(command)) {
final int[] ids = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
appWidgetBig.performUpdate(MusicService.this, ids);
+ } else if (AppWidgetCard.NAME.equals(command)) {
+ final int[] ids = intent.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS);
+ appWidgetCard.performUpdate(MusicService.this, ids);
}
}
};
diff --git a/app/src/main/res/drawable-v21/widget_selector_light.xml b/app/src/main/res/drawable-v21/widget_selector_light.xml
new file mode 100644
index 00000000..86c53d73
--- /dev/null
+++ b/app/src/main/res/drawable-v21/widget_selector_light.xml
@@ -0,0 +1,3 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/card.xml b/app/src/main/res/drawable/card.xml
new file mode 100644
index 00000000..69e9986d
--- /dev/null
+++ b/app/src/main/res/drawable/card.xml
@@ -0,0 +1,45 @@
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
+ -
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/drawable/widget_selector_light.xml b/app/src/main/res/drawable/widget_selector_light.xml
new file mode 100644
index 00000000..b03bcafa
--- /dev/null
+++ b/app/src/main/res/drawable/widget_selector_light.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/app_widget_card.xml b/app/src/main/res/layout/app_widget_card.xml
new file mode 100644
index 00000000..53777743
--- /dev/null
+++ b/app/src/main/res/layout/app_widget_card.xml
@@ -0,0 +1,130 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
index 63fc626a..a40a4e4f 100644
--- a/app/src/main/res/values/dimens.xml
+++ b/app/src/main/res/values/dimens.xml
@@ -73,4 +73,10 @@ http://developer.android.com/guide/topics/appwidgets/index.html#CreatingLayout
250dp
110dp
+ 56dp
+ 250dp
+ 64dp
+ 128dp
+ 2dp
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index b4a69299..f7b4c0cf 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -268,6 +268,7 @@
Phonograph - Big
Phonograph - Classic
Phonograph - Small
+ Phonograph - Card
Report an issue
Issue
Login
diff --git a/app/src/main/res/xml/app_widget_card_info.xml b/app/src/main/res/xml/app_widget_card_info.xml
new file mode 100644
index 00000000..18ac28cd
--- /dev/null
+++ b/app/src/main/res/xml/app_widget_card_info.xml
@@ -0,0 +1,12 @@
+
+
+
+
\ No newline at end of file