From 35af1f903848c56fc4bacb21b6b369f527fd596b Mon Sep 17 00:00:00 2001 From: mucahit-kaya <54944321+mucahit-kaya@users.noreply.github.com> Date: Sat, 20 Sep 2025 21:13:23 +0200 Subject: [PATCH] fix(widget): refine layouts and progress UX across sizes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compact (4×1) - Reduce root vertical padding so the 4×1 cell yields ~56dp of content height. - Make album art a true square (50×50dp) and center vertically; keeps edges clear of rounded corners. - Tighten timing block: 2dp progress bar; 10sp labels with no extra font padding; prevents elapsed/total text from slipping below the background. - Wrap album art in a 50×50dp FrameLayout with a new 6dp-radius background drawable; soft corners while remaining visually smaller than the widget body. - Mirror the same structure in the preview layout so Studio preview matches on-device rendering. (app/src/main/res/layout/widget_layout_compact.xml, app/src/main/res/drawable/widget_album_art_bg.xml) Large Short (4×2) - Wrap album art in a fixed 90dp square container and enforce a true square crop via centerCrop. - Tighten vertical spacing: thinner progress bar, closer timing row, controls shifted down for better balance. - Keep album/timing text to the left of the controls but retune spacing so the stack stays fully inside the widget bounds. Large (4×3 and up) - Restructure to a vertical stack: header row (album art + text), full-width progress bar, timing row, primary controls, then secondary controls. - Lock album art to a 150dp square; progress bar spans the widget beneath the header to match the new visual hierarchy. Based-on: https://github.com/eddyizm/tempo/commit/cd28ee0764b35f46988fb31e4fe2aaebcdb69851 Co-authored-by: The Firehawk Co-Authored-By: Mücahit Kaya Co-Authored-By: Firehawk --- .../tempo/widget/WidgetActions.java | 17 ++ .../tempo/widget/WidgetProvider.java | 19 +- .../tempo/widget/WidgetUpdateManager.java | 86 +++++-- .../tempo/widget/WidgetViewsFactory.java | 215 +++++++++++++--- app/src/main/res/drawable/ic_repeat_one.xml | 12 + .../main/res/layout/widget_layout_compact.xml | 84 ++++++- .../main/res/layout/widget_layout_large.xml | 238 ++++++++++++------ .../res/layout/widget_layout_large_short.xml | 198 +++++++++++++++ .../main/res/layout/widget_layout_medium.xml | 216 ++++++++++++++++ .../res/layout/widget_preview_compact.xml | 6 +- app/src/main/res/values/colors_widget.xml | 1 + app/src/main/res/values/integers.xml | 4 +- app/src/main/res/values/strings.xml | 2 + .../tempo/service/MediaService.kt | 5 + .../tempo/service/MediaService.kt | 5 + .../tempo/service/MediaService.kt | 5 + notes | 0 17 files changed, 964 insertions(+), 149 deletions(-) create mode 100644 app/src/main/res/drawable/ic_repeat_one.xml create mode 100644 app/src/main/res/layout/widget_layout_large_short.xml create mode 100644 app/src/main/res/layout/widget_layout_medium.xml create mode 100644 notes diff --git a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetActions.java b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetActions.java index c035fa78..5be730f8 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetActions.java +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetActions.java @@ -4,6 +4,7 @@ import android.content.ComponentName; import android.content.Context; import android.util.Log; +import androidx.media3.common.Player; import androidx.media3.session.MediaController; import androidx.media3.session.SessionToken; @@ -34,7 +35,23 @@ public final class WidgetActions { case WidgetProvider.ACT_PREV: c.seekToPrevious(); break; + case WidgetProvider.ACT_TOGGLE_SHUFFLE: + c.setShuffleModeEnabled(!c.getShuffleModeEnabled()); + break; + case WidgetProvider.ACT_CYCLE_REPEAT: + int repeatMode = c.getRepeatMode(); + int nextMode; + if (repeatMode == Player.REPEAT_MODE_OFF) { + nextMode = Player.REPEAT_MODE_ALL; + } else if (repeatMode == Player.REPEAT_MODE_ALL) { + nextMode = Player.REPEAT_MODE_ONE; + } else { + nextMode = Player.REPEAT_MODE_OFF; + } + c.setRepeatMode(nextMode); + break; } + WidgetUpdateManager.refreshFromController(ctx); c.release(); } catch (ExecutionException | InterruptedException e) { Log.e("TempoWidget", "dispatch failed", e); diff --git a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider.java b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider.java index 2b4b1f3c..57033c20 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider.java +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetProvider.java @@ -18,6 +18,8 @@ public class WidgetProvider extends AppWidgetProvider { public static final String ACT_PLAY_PAUSE = "tempo.widget.PLAY_PAUSE"; public static final String ACT_NEXT = "tempo.widget.NEXT"; public static final String ACT_PREV = "tempo.widget.PREV"; + public static final String ACT_TOGGLE_SHUFFLE = "tempo.widget.SHUFFLE"; + public static final String ACT_CYCLE_REPEAT = "tempo.widget.REPEAT"; @Override public void onUpdate(Context ctx, AppWidgetManager mgr, int[] ids) { for (int id : ids) { @@ -31,7 +33,8 @@ public class WidgetProvider extends AppWidgetProvider { super.onReceive(ctx, intent); String a = intent.getAction(); Log.d(TAG, "onReceive action=" + a); - if (ACT_PLAY_PAUSE.equals(a) || ACT_NEXT.equals(a) || ACT_PREV.equals(a)) { + if (ACT_PLAY_PAUSE.equals(a) || ACT_NEXT.equals(a) || ACT_PREV.equals(a) + || ACT_TOGGLE_SHUFFLE.equals(a) || ACT_CYCLE_REPEAT.equals(a)) { WidgetActions.dispatchToMediaSession(ctx, a); } else if (AppWidgetManager.ACTION_APPWIDGET_UPDATE.equals(a)) { WidgetUpdateManager.refreshFromController(ctx); @@ -69,10 +72,24 @@ public class WidgetProvider extends AppWidgetProvider { new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_PREV), PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT ); + PendingIntent shuffle = PendingIntent.getBroadcast( + ctx, + requestCodeBase + 3, + new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_TOGGLE_SHUFFLE), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); + PendingIntent repeat = PendingIntent.getBroadcast( + ctx, + requestCodeBase + 4, + new Intent(ctx, WidgetProvider4x1.class).setAction(ACT_CYCLE_REPEAT), + PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT + ); rv.setOnClickPendingIntent(R.id.btn_play_pause, playPause); rv.setOnClickPendingIntent(R.id.btn_next, next); rv.setOnClickPendingIntent(R.id.btn_prev, prev); + rv.setOnClickPendingIntent(R.id.btn_shuffle, shuffle); + rv.setOnClickPendingIntent(R.id.btn_repeat, repeat); PendingIntent launch = TaskStackBuilder.create(ctx) .addNextIntentWithParentStack(new Intent(ctx, MainActivity.class)) diff --git a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java index e9515c50..dbbcc6b3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java @@ -24,20 +24,24 @@ public final class WidgetUpdateManager { public static void updateFromState(Context ctx, String title, String artist, + String album, Bitmap art, boolean playing, + boolean shuffleEnabled, + int repeatMode, long positionMs, long durationMs) { if (TextUtils.isEmpty(title)) title = ctx.getString(R.string.widget_not_playing); if (TextUtils.isEmpty(artist)) artist = ctx.getString(R.string.widget_placeholder_subtitle); + if (TextUtils.isEmpty(album)) album = ""; final TimingInfo timing = createTimingInfo(positionMs, durationMs); AppWidgetManager mgr = AppWidgetManager.getInstance(ctx); int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class)); for (int id : ids) { - android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, art, playing, - timing.elapsedText, timing.totalText, timing.progress, id); + android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, album, art, playing, + timing.elapsedText, timing.totalText, timing.progress, shuffleEnabled, repeatMode, id); WidgetProvider.attachIntents(ctx, rv, id); mgr.updateAppWidget(id, rv); } @@ -56,14 +60,20 @@ public final class WidgetUpdateManager { public static void updateFromState(Context ctx, String title, String artist, + String album, String coverArtId, boolean playing, + boolean shuffleEnabled, + int repeatMode, long positionMs, long durationMs) { final Context appCtx = ctx.getApplicationContext(); final String t = TextUtils.isEmpty(title) ? appCtx.getString(R.string.widget_not_playing) : title; final String a = TextUtils.isEmpty(artist) ? appCtx.getString(R.string.widget_placeholder_subtitle) : artist; + final String alb = !TextUtils.isEmpty(album) ? album : ""; final boolean p = playing; + final boolean sh = shuffleEnabled; + final int rep = repeatMode; final TimingInfo timing = createTimingInfo(positionMs, durationMs); if (!TextUtils.isEmpty(coverArtId)) { @@ -76,8 +86,8 @@ public final class WidgetUpdateManager { AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx); int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class)); for (int id : ids) { - android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, resource, p, - timing.elapsedText, timing.totalText, timing.progress, id); + android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, resource, p, + timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); WidgetProvider.attachIntents(appCtx, rv, id); mgr.updateAppWidget(id, rv); } @@ -87,8 +97,8 @@ public final class WidgetUpdateManager { AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx); int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class)); for (int id : ids) { - android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, null, p, - timing.elapsedText, timing.totalText, timing.progress, id); + android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p, + timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); WidgetProvider.attachIntents(appCtx, rv, id); mgr.updateAppWidget(id, rv); } @@ -99,8 +109,8 @@ public final class WidgetUpdateManager { AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx); int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class)); for (int id : ids) { - android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, null, p, - timing.elapsedText, timing.totalText, timing.progress, id); + android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p, + timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); WidgetProvider.attachIntents(appCtx, rv, id); mgr.updateAppWidget(id, rv); } @@ -116,13 +126,15 @@ public final class WidgetUpdateManager { if (!future.isDone()) return; MediaController c = future.get(); androidx.media3.common.MediaItem mi = c.getCurrentMediaItem(); - String title = null, artist = null, coverId = null; + String title = null, artist = null, album = null, coverId = null; if (mi != null && mi.mediaMetadata != null) { if (mi.mediaMetadata.title != null) title = mi.mediaMetadata.title.toString(); if (mi.mediaMetadata.artist != null) artist = mi.mediaMetadata.artist.toString(); + if (mi.mediaMetadata.albumTitle != null) album = mi.mediaMetadata.albumTitle.toString(); if (mi.mediaMetadata.extras != null) { if (title == null) title = mi.mediaMetadata.extras.getString("title"); if (artist == null) artist = mi.mediaMetadata.extras.getString("artist"); + if (album == null) album = mi.mediaMetadata.extras.getString("album"); coverId = mi.mediaMetadata.extras.getString("coverArtId"); } } @@ -133,8 +145,11 @@ public final class WidgetUpdateManager { updateFromState(appCtx, title != null ? title : appCtx.getString(R.string.widget_not_playing), artist != null ? artist : appCtx.getString(R.string.widget_placeholder_subtitle), + album, coverId, c.isPlaying(), + c.getShuffleModeEnabled(), + c.getRepeatMode(), position, duration); c.release(); @@ -174,31 +189,68 @@ public final class WidgetUpdateManager { } public static android.widget.RemoteViews chooseBuild(Context ctx, int appWidgetId) { - if (isLarge(ctx, appWidgetId)) return WidgetViewsFactory.buildLarge(ctx); - return WidgetViewsFactory.buildCompact(ctx); + LayoutSize size = resolveLayoutSize(ctx, appWidgetId); + switch (size) { + case MEDIUM: + return WidgetViewsFactory.buildMedium(ctx); + case LARGE: + return WidgetViewsFactory.buildLarge(ctx); + case EXPANDED: + return WidgetViewsFactory.buildExpanded(ctx); + case COMPACT: + default: + return WidgetViewsFactory.buildCompact(ctx); + } } private static android.widget.RemoteViews choosePopulate(Context ctx, String title, String artist, + String album, Bitmap art, boolean playing, String elapsedText, String totalText, int progress, + boolean shuffleEnabled, + int repeatMode, int appWidgetId) { - if (isLarge(ctx, appWidgetId)) { - return WidgetViewsFactory.populateLarge(ctx, title, artist, art, playing, elapsedText, totalText, progress); + LayoutSize size = resolveLayoutSize(ctx, appWidgetId); + switch (size) { + case MEDIUM: + return WidgetViewsFactory.populateMedium(ctx, title, artist, album, art, playing, + elapsedText, totalText, progress, shuffleEnabled, repeatMode); + case LARGE: + return WidgetViewsFactory.populateLarge(ctx, title, artist, album, art, playing, + elapsedText, totalText, progress, shuffleEnabled, repeatMode); + case EXPANDED: + return WidgetViewsFactory.populateExpanded(ctx, title, artist, album, art, playing, + elapsedText, totalText, progress, shuffleEnabled, repeatMode); + case COMPACT: + default: + return WidgetViewsFactory.populateCompact(ctx, title, artist, album, art, playing, + elapsedText, totalText, progress, shuffleEnabled, repeatMode); } - return WidgetViewsFactory.populate(ctx, title, artist, art, playing, elapsedText, totalText, progress); } - private static boolean isLarge(Context ctx, int appWidgetId) { + private static LayoutSize resolveLayoutSize(Context ctx, int appWidgetId) { AppWidgetManager mgr = AppWidgetManager.getInstance(ctx); android.os.Bundle opts = mgr.getAppWidgetOptions(appWidgetId); int minH = opts != null ? opts.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_HEIGHT) : 0; - int threshold = ctx.getResources().getInteger(com.cappielloantonio.tempo.R.integer.widget_large_min_height_dp); - return minH >= threshold; // dp threshold for 2-row height + int expandedThreshold = ctx.getResources().getInteger(R.integer.widget_expanded_min_height_dp); + int largeThreshold = ctx.getResources().getInteger(R.integer.widget_large_min_height_dp); + int mediumThreshold = ctx.getResources().getInteger(R.integer.widget_medium_min_height_dp); + if (minH >= expandedThreshold) return LayoutSize.EXPANDED; + if (minH >= largeThreshold) return LayoutSize.LARGE; + if (minH >= mediumThreshold) return LayoutSize.MEDIUM; + return LayoutSize.COMPACT; + } + + private enum LayoutSize { + COMPACT, + MEDIUM, + LARGE, + EXPANDED } private static final class TimingInfo { diff --git a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java index 05346eb3..ef960aef 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java @@ -2,76 +2,168 @@ package com.cappielloantonio.tempo.widget; import android.content.Context; import android.graphics.Bitmap; +import android.graphics.BitmapShader; +import android.graphics.Canvas; +import android.graphics.Paint; +import android.graphics.RectF; +import android.graphics.Shader; import android.text.TextUtils; +import android.util.TypedValue; +import android.view.View; import android.widget.RemoteViews; + +import androidx.core.content.ContextCompat; +import androidx.media3.common.Player; + import com.cappielloantonio.tempo.R; public final class WidgetViewsFactory { static final int PROGRESS_MAX = 1000; + private static final float ALBUM_ART_CORNER_RADIUS_DP = 6f; + + private WidgetViewsFactory() {} public static RemoteViews buildCompact(Context ctx) { - return build(ctx, R.layout.widget_layout_compact); + return build(ctx, R.layout.widget_layout_compact, false, false); + } + + public static RemoteViews buildMedium(Context ctx) { + return build(ctx, R.layout.widget_layout_medium, false, false); } public static RemoteViews buildLarge(Context ctx) { - return build(ctx, R.layout.widget_layout_large); + return build(ctx, R.layout.widget_layout_large_short, true, true); } + public static RemoteViews buildExpanded(Context ctx) { + return build(ctx, R.layout.widget_layout_large, true, true); + } - private static RemoteViews build(Context ctx, int layoutRes) { + private static RemoteViews build(Context ctx, + int layoutRes, + boolean showAlbum, + boolean showSecondaryControls) { RemoteViews rv = new RemoteViews(ctx.getPackageName(), layoutRes); rv.setTextViewText(R.id.title, ctx.getString(R.string.widget_not_playing)); rv.setTextViewText(R.id.subtitle, ctx.getString(R.string.widget_placeholder_subtitle)); + rv.setTextViewText(R.id.album, ""); + rv.setViewVisibility(R.id.album, showAlbum ? View.INVISIBLE : View.GONE); rv.setTextViewText(R.id.time_elapsed, ctx.getString(R.string.widget_time_elapsed_placeholder)); rv.setTextViewText(R.id.time_total, ctx.getString(R.string.widget_time_duration_placeholder)); rv.setProgressBar(R.id.progress, PROGRESS_MAX, 0, false); rv.setImageViewResource(R.id.btn_play_pause, R.drawable.ic_play); - // Show Tempo logo when nothing is playing rv.setImageViewResource(R.id.album_art, R.drawable.ic_splash_logo); + applySecondaryControlsDefaults(ctx, rv, showSecondaryControls); return rv; } - public static RemoteViews populate(Context ctx, - String title, - String subtitle, - Bitmap art, - boolean playing, - String elapsedText, - String totalText, - int progress) { - return populateWithLayout(ctx, title, subtitle, art, playing, elapsedText, totalText, progress, R.layout.widget_layout_compact); + private static void applySecondaryControlsDefaults(Context ctx, + RemoteViews rv, + boolean show) { + int visibility = show ? View.VISIBLE : View.GONE; + rv.setViewVisibility(R.id.controls_secondary, visibility); + rv.setViewVisibility(R.id.btn_shuffle, visibility); + rv.setViewVisibility(R.id.btn_repeat, visibility); + if (show) { + int defaultColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint); + rv.setImageViewResource(R.id.btn_shuffle, R.drawable.ic_shuffle); + rv.setImageViewResource(R.id.btn_repeat, R.drawable.ic_repeat); + rv.setInt(R.id.btn_shuffle, "setColorFilter", defaultColor); + rv.setInt(R.id.btn_repeat, "setColorFilter", defaultColor); + } + } + + public static RemoteViews populateCompact(Context ctx, + String title, + String subtitle, + String album, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + boolean shuffleEnabled, + int repeatMode) { + return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText, + progress, R.layout.widget_layout_compact, false, false, shuffleEnabled, repeatMode); + } + + public static RemoteViews populateMedium(Context ctx, + String title, + String subtitle, + String album, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + boolean shuffleEnabled, + int repeatMode) { + return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText, + progress, R.layout.widget_layout_medium, true, true, shuffleEnabled, repeatMode); } public static RemoteViews populateLarge(Context ctx, - String title, - String subtitle, - Bitmap art, - boolean playing, - String elapsedText, - String totalText, - int progress) { - return populateWithLayout(ctx, title, subtitle, art, playing, elapsedText, totalText, progress, R.layout.widget_layout_large); + String title, + String subtitle, + String album, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + boolean shuffleEnabled, + int repeatMode) { + return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText, + progress, R.layout.widget_layout_large_short, true, true, shuffleEnabled, repeatMode); } + public static RemoteViews populateExpanded(Context ctx, + String title, + String subtitle, + String album, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + boolean shuffleEnabled, + int repeatMode) { + return populateWithLayout(ctx, title, subtitle, album, art, playing, elapsedText, totalText, + progress, R.layout.widget_layout_large, true, true, shuffleEnabled, repeatMode); + } private static RemoteViews populateWithLayout(Context ctx, String title, - String subtitle, - Bitmap art, - boolean playing, - String elapsedText, - String totalText, - int progress, - int layoutRes) { + String subtitle, + String album, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + int layoutRes, + boolean showAlbum, + boolean showSecondaryControls, + boolean shuffleEnabled, + int repeatMode) { RemoteViews rv = new RemoteViews(ctx.getPackageName(), layoutRes); rv.setTextViewText(R.id.title, title); rv.setTextViewText(R.id.subtitle, subtitle); - if (art != null) { - rv.setImageViewBitmap(R.id.album_art, art); + if (showAlbum && !TextUtils.isEmpty(album)) { + rv.setTextViewText(R.id.album, album); + rv.setViewVisibility(R.id.album, View.VISIBLE); + } else { + rv.setTextViewText(R.id.album, ""); + rv.setViewVisibility(R.id.album, View.GONE); + } + + if (art != null) { + Bitmap rounded = maybeRoundBitmap(ctx, art); + rv.setImageViewBitmap(R.id.album_art, rounded != null ? rounded : art); } else { - // Fallback to app logo when art is missing rv.setImageViewResource(R.id.album_art, R.drawable.ic_splash_logo); } @@ -93,6 +185,67 @@ public final class WidgetViewsFactory { rv.setTextViewText(R.id.time_total, total); rv.setProgressBar(R.id.progress, PROGRESS_MAX, safeProgress, false); + applySecondaryControls(ctx, rv, showSecondaryControls, shuffleEnabled, repeatMode); + return rv; } + + private static Bitmap maybeRoundBitmap(Context ctx, Bitmap source) { + if (source == null || source.isRecycled()) { + return null; + } + + try { + int width = source.getWidth(); + int height = source.getHeight(); + if (width <= 0 || height <= 0) { + return null; + } + + Bitmap output = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(output); + + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + paint.setShader(new BitmapShader(source, Shader.TileMode.CLAMP, Shader.TileMode.CLAMP)); + + float radiusPx = TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + ALBUM_ART_CORNER_RADIUS_DP, + ctx.getResources().getDisplayMetrics()); + float maxRadius = Math.min(width, height) / 2f; + float safeRadius = Math.min(radiusPx, maxRadius); + + canvas.drawRoundRect(new RectF(0f, 0f, width, height), safeRadius, safeRadius, paint); + return output; + } catch (RuntimeException | OutOfMemoryError e) { + android.util.Log.w("TempoWidget", "Failed to round album art", e); + return null; + } + } + + private static void applySecondaryControls(Context ctx, + RemoteViews rv, + boolean show, + boolean shuffleEnabled, + int repeatMode) { + if (!show) { + rv.setViewVisibility(R.id.controls_secondary, View.GONE); + rv.setViewVisibility(R.id.btn_shuffle, View.GONE); + rv.setViewVisibility(R.id.btn_repeat, View.GONE); + return; + } + + int inactiveColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint); + int activeColor = ContextCompat.getColor(ctx, R.color.widget_icon_tint_active); + + rv.setViewVisibility(R.id.controls_secondary, View.VISIBLE); + rv.setViewVisibility(R.id.btn_shuffle, View.VISIBLE); + rv.setViewVisibility(R.id.btn_repeat, View.VISIBLE); + rv.setImageViewResource(R.id.btn_shuffle, R.drawable.ic_shuffle); + rv.setImageViewResource(R.id.btn_repeat, + repeatMode == Player.REPEAT_MODE_ONE ? R.drawable.ic_repeat_one : R.drawable.ic_repeat); + rv.setInt(R.id.btn_shuffle, "setColorFilter", shuffleEnabled ? activeColor : inactiveColor); + rv.setInt(R.id.btn_repeat, "setColorFilter", + repeatMode == Player.REPEAT_MODE_OFF ? inactiveColor : activeColor); + } } diff --git a/app/src/main/res/drawable/ic_repeat_one.xml b/app/src/main/res/drawable/ic_repeat_one.xml new file mode 100644 index 00000000..f422f79a --- /dev/null +++ b/app/src/main/res/drawable/ic_repeat_one.xml @@ -0,0 +1,12 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/widget_layout_compact.xml b/app/src/main/res/layout/widget_layout_compact.xml index 5e0108dc..78fb72fb 100644 --- a/app/src/main/res/layout/widget_layout_compact.xml +++ b/app/src/main/res/layout/widget_layout_compact.xml @@ -3,13 +3,17 @@ android:id="@+id/root" android:layout_width="match_parent" android:layout_height="64dp" - android:padding="8dp" + android:paddingStart="8dp" + android:paddingEnd="8dp" + android:paddingTop="4dp" + android:paddingBottom="4dp" android:background="@drawable/widget_bg"> @@ -32,6 +36,8 @@ android:textStyle="bold" android:textSize="14sp" android:textColor="@color/widget_title" + android:includeFontPadding="false" + android:freezesText="true" android:layout_width="match_parent" android:layout_height="wrap_content"/> @@ -41,15 +47,30 @@ android:ellipsize="end" android:textSize="12sp" android:textColor="@color/widget_subtitle" + android:includeFontPadding="false" + android:freezesText="true" android:layout_width="match_parent" android:layout_height="wrap_content"/> + + + android:textSize="10sp" + android:includeFontPadding="false"/> + android:textSize="10sp" + android:includeFontPadding="false"/> + + + + + + + @@ -93,22 +145,28 @@ android:layout_width="wrap_content" android:layout_height="match_parent"> - - - - - - + android:orientation="horizontal" + android:baselineAligned="false"> - - - - - + + android:id="@+id/text_container" + android:layout_width="0dp" + android:layout_height="match_parent" + android:layout_marginStart="16dp" + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center_vertical"> + android:singleLine="true" + android:ellipsize="marquee" + android:marqueeRepeatLimit="marquee_forever" + android:scrollHorizontally="true" + android:textStyle="bold" + android:textSize="18sp" + android:textColor="@color/widget_title" + android:includeFontPadding="false" + android:freezesText="true" /> + android:includeFontPadding="false" + android:freezesText="true" /> + + + + + + + + + + + + android:orientation="horizontal"> - + android:src="@drawable/ic_skip_previous" + android:tint="@color/widget_icon_tint" /> - + android:src="@drawable/ic_play" + android:tint="@color/widget_icon_tint" /> - + android:src="@drawable/ic_skip_next" + android:tint="@color/widget_icon_tint" /> - + + + + + + + + + diff --git a/app/src/main/res/layout/widget_layout_large_short.xml b/app/src/main/res/layout/widget_layout_large_short.xml new file mode 100644 index 00000000..6a715f6e --- /dev/null +++ b/app/src/main/res/layout/widget_layout_large_short.xml @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/widget_layout_medium.xml b/app/src/main/res/layout/widget_layout_medium.xml new file mode 100644 index 00000000..802da828 --- /dev/null +++ b/app/src/main/res/layout/widget_layout_medium.xml @@ -0,0 +1,216 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/res/layout/widget_preview_compact.xml b/app/src/main/res/layout/widget_preview_compact.xml index e369ffb6..e863603a 100644 --- a/app/src/main/res/layout/widget_preview_compact.xml +++ b/app/src/main/res/layout/widget_preview_compact.xml @@ -11,8 +11,9 @@ @@ -79,4 +80,3 @@ android:contentDescription="@string/widget_content_desc_next"/> - diff --git a/app/src/main/res/values/colors_widget.xml b/app/src/main/res/values/colors_widget.xml index 75a9f7f1..71a34138 100644 --- a/app/src/main/res/values/colors_widget.xml +++ b/app/src/main/res/values/colors_widget.xml @@ -5,4 +5,5 @@ #DE000000 #99000000 #DE000000 + #FF6750A4 diff --git a/app/src/main/res/values/integers.xml b/app/src/main/res/values/integers.xml index 7b968814..e1a1ac1c 100644 --- a/app/src/main/res/values/integers.xml +++ b/app/src/main/res/values/integers.xml @@ -1,4 +1,6 @@ - 100 + 100 + 160 + 220 diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 6a4851cd..47a6650e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -448,6 +448,8 @@ Play or pause Next track Previous track + Toggle shuffle + Change repeat mode %d album to sync %d albums to sync diff --git a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt index bdc07e96..67b02594 100644 --- a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -416,6 +416,8 @@ class MediaService : MediaLibraryService() { ?: mi?.mediaMetadata?.extras?.getString("title") val artist = mi?.mediaMetadata?.artist?.toString() ?: mi?.mediaMetadata?.extras?.getString("artist") + val album = mi?.mediaMetadata?.albumTitle?.toString() + ?: mi?.mediaMetadata?.extras?.getString("album") val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId") val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L @@ -423,8 +425,11 @@ class MediaService : MediaLibraryService() { this, title ?: "", artist ?: "", + album ?: "", coverId, player.isPlaying, + player.shuffleModeEnabled, + player.repeatMode, position, duration ) diff --git a/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt index a0af016b..5f44ed3a 100644 --- a/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -262,6 +262,8 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { ?: mi?.mediaMetadata?.extras?.getString("title") val artist = mi?.mediaMetadata?.artist?.toString() ?: mi?.mediaMetadata?.extras?.getString("artist") + val album = mi?.mediaMetadata?.albumTitle?.toString() + ?: mi?.mediaMetadata?.extras?.getString("album") val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId") val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L @@ -270,8 +272,11 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { this, title ?: "", artist ?: "", + album ?: "", coverId, player.isPlaying, + player.shuffleModeEnabled, + player.repeatMode, position, duration ) diff --git a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt index a5164e05..ac20668a 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -262,6 +262,8 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { ?: mi?.mediaMetadata?.extras?.getString("title") val artist = mi?.mediaMetadata?.artist?.toString() ?: mi?.mediaMetadata?.extras?.getString("artist") + val album = mi?.mediaMetadata?.albumTitle?.toString() + ?: mi?.mediaMetadata?.extras?.getString("album") val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId") val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L @@ -269,8 +271,11 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { this, title ?: "", artist ?: "", + album ?: "", coverId, player.isPlaying, + player.shuffleModeEnabled, + player.repeatMode, position, duration ) diff --git a/notes b/notes new file mode 100644 index 00000000..e69de29b