From e81e1a53568036843a0726ba44991ef3b39e01f6 Mon Sep 17 00:00:00 2001 From: le-firehawk Date: Thu, 18 Sep 2025 13:57:07 +0930 Subject: [PATCH] fix: Include song position and duration in widget MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Mücahit Kaya Co-authored-by: The Firehawk --- .../tempo/widget/WidgetUpdateManager.java | 92 ++++++++++++++++--- .../tempo/widget/WidgetViewsFactory.java | 38 +++++++- .../main/res/layout/widget_layout_compact.xml | 39 ++++++++ .../main/res/layout/widget_layout_large.xml | 39 ++++++++ app/src/main/res/values-tr/strings.xml | 2 + app/src/main/res/values/strings.xml | 2 + .../tempo/service/MediaService.kt | 6 +- .../tempo/service/MediaService.kt | 6 +- .../tempo/service/MediaService.kt | 6 +- 9 files changed, 210 insertions(+), 20 deletions(-) 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 6de08905..e9515c50 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java @@ -10,9 +10,11 @@ import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.R; +import androidx.media3.common.C; import androidx.media3.session.MediaController; import androidx.media3.session.SessionToken; import com.cappielloantonio.tempo.service.MediaService; +import com.cappielloantonio.tempo.util.MusicUtil; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; import java.util.concurrent.ExecutionException; @@ -23,14 +25,19 @@ public final class WidgetUpdateManager { String title, String artist, Bitmap art, - boolean playing) { + boolean playing, + 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); + 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, id); + android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, art, playing, + timing.elapsedText, timing.totalText, timing.progress, id); WidgetProvider.attachIntents(ctx, rv, id); mgr.updateAppWidget(id, rv); } @@ -50,11 +57,14 @@ public final class WidgetUpdateManager { String title, String artist, String coverArtId, - boolean playing) { + boolean playing, + 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 boolean p = playing; + final TimingInfo timing = createTimingInfo(positionMs, durationMs); if (!TextUtils.isEmpty(coverArtId)) { CustomGlideRequest.loadAlbumArtBitmap( @@ -66,7 +76,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, id); + android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, resource, p, + timing.elapsedText, timing.totalText, timing.progress, id); WidgetProvider.attachIntents(appCtx, rv, id); mgr.updateAppWidget(id, rv); } @@ -76,7 +87,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, id); + android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, null, p, + timing.elapsedText, timing.totalText, timing.progress, id); WidgetProvider.attachIntents(appCtx, rv, id); mgr.updateAppWidget(id, rv); } @@ -87,7 +99,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, id); + android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, null, p, + timing.elapsedText, timing.totalText, timing.progress, id); WidgetProvider.attachIntents(appCtx, rv, id); mgr.updateAppWidget(id, rv); } @@ -113,25 +126,71 @@ public final class WidgetUpdateManager { coverId = mi.mediaMetadata.extras.getString("coverArtId"); } } + long position = c.getCurrentPosition(); + long duration = c.getDuration(); + if (position == C.TIME_UNSET) position = 0; + if (duration == C.TIME_UNSET) duration = 0; updateFromState(appCtx, title != null ? title : appCtx.getString(R.string.widget_not_playing), artist != null ? artist : appCtx.getString(R.string.widget_placeholder_subtitle), coverId, - c.isPlaying()); + c.isPlaying(), + position, + duration); c.release(); } catch (ExecutionException | InterruptedException ignored) { } }, MoreExecutors.directExecutor()); } + private static TimingInfo createTimingInfo(long positionMs, long durationMs) { + long safePosition = Math.max(0L, positionMs); + long safeDuration = durationMs > 0 ? durationMs : 0L; + if (safeDuration > 0 && safePosition > safeDuration) { + safePosition = safeDuration; + } + + String elapsed = (safeDuration > 0 || safePosition > 0) + ? MusicUtil.getReadableDurationString(safePosition, true) + : null; + String total = safeDuration > 0 + ? MusicUtil.getReadableDurationString(safeDuration, true) + : null; + + int progress = 0; + if (safeDuration > 0) { + long scaled = safePosition * WidgetViewsFactory.PROGRESS_MAX; + long progressLong = scaled / safeDuration; + if (progressLong < 0) { + progress = 0; + } else if (progressLong > WidgetViewsFactory.PROGRESS_MAX) { + progress = WidgetViewsFactory.PROGRESS_MAX; + } else { + progress = (int) progressLong; + } + } + + return new TimingInfo(elapsed, total, progress); + } + public static android.widget.RemoteViews chooseBuild(Context ctx, int appWidgetId) { if (isLarge(ctx, appWidgetId)) return WidgetViewsFactory.buildLarge(ctx); return WidgetViewsFactory.buildCompact(ctx); } - private static android.widget.RemoteViews choosePopulate(Context ctx, String title, String artist, Bitmap art, boolean playing, int appWidgetId) { - if (isLarge(ctx, appWidgetId)) return WidgetViewsFactory.populateLarge(ctx, title, artist, art, playing); - return WidgetViewsFactory.populate(ctx, title, artist, art, playing); + private static android.widget.RemoteViews choosePopulate(Context ctx, + String title, + String artist, + Bitmap art, + boolean playing, + String elapsedText, + String totalText, + int progress, + int appWidgetId) { + if (isLarge(ctx, appWidgetId)) { + return WidgetViewsFactory.populateLarge(ctx, title, artist, art, playing, elapsedText, totalText, progress); + } + return WidgetViewsFactory.populate(ctx, title, artist, art, playing, elapsedText, totalText, progress); } private static boolean isLarge(Context ctx, int appWidgetId) { @@ -142,5 +201,16 @@ public final class WidgetUpdateManager { return minH >= threshold; // dp threshold for 2-row height } - + private static final class TimingInfo { + final String elapsedText; + final String totalText; + final int progress; + + TimingInfo(String elapsedText, String totalText, int progress) { + this.elapsedText = elapsedText; + this.totalText = totalText; + this.progress = progress; + } + } + } 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 9dec3d60..05346eb3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java @@ -2,11 +2,14 @@ package com.cappielloantonio.tempo.widget; import android.content.Context; import android.graphics.Bitmap; +import android.text.TextUtils; import android.widget.RemoteViews; import com.cappielloantonio.tempo.R; public final class WidgetViewsFactory { + static final int PROGRESS_MAX = 1000; + public static RemoteViews buildCompact(Context ctx) { return build(ctx, R.layout.widget_layout_compact); } @@ -20,6 +23,9 @@ public final class WidgetViewsFactory { 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.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); @@ -30,16 +36,22 @@ public final class WidgetViewsFactory { String title, String subtitle, Bitmap art, - boolean playing) { - return populateWithLayout(ctx, title, subtitle, art, playing, R.layout.widget_layout_compact); + boolean playing, + String elapsedText, + String totalText, + int progress) { + return populateWithLayout(ctx, title, subtitle, art, playing, elapsedText, totalText, progress, R.layout.widget_layout_compact); } public static RemoteViews populateLarge(Context ctx, String title, String subtitle, Bitmap art, - boolean playing) { - return populateWithLayout(ctx, title, subtitle, art, playing, R.layout.widget_layout_large); + boolean playing, + String elapsedText, + String totalText, + int progress) { + return populateWithLayout(ctx, title, subtitle, art, playing, elapsedText, totalText, progress, R.layout.widget_layout_large); } @@ -48,6 +60,9 @@ public final class WidgetViewsFactory { String subtitle, Bitmap art, boolean playing, + String elapsedText, + String totalText, + int progress, int layoutRes) { RemoteViews rv = new RemoteViews(ctx.getPackageName(), layoutRes); rv.setTextViewText(R.id.title, title); @@ -63,6 +78,21 @@ public final class WidgetViewsFactory { rv.setImageViewResource(R.id.btn_play_pause, playing ? R.drawable.ic_pause : R.drawable.ic_play); + String elapsed = !TextUtils.isEmpty(elapsedText) + ? elapsedText + : ctx.getString(R.string.widget_time_elapsed_placeholder); + String total = !TextUtils.isEmpty(totalText) + ? totalText + : ctx.getString(R.string.widget_time_duration_placeholder); + + int safeProgress = progress; + if (safeProgress < 0) safeProgress = 0; + if (safeProgress > PROGRESS_MAX) safeProgress = PROGRESS_MAX; + + rv.setTextViewText(R.id.time_elapsed, elapsed); + rv.setTextViewText(R.id.time_total, total); + rv.setProgressBar(R.id.progress, PROGRESS_MAX, safeProgress, false); + return rv; } } diff --git a/app/src/main/res/layout/widget_layout_compact.xml b/app/src/main/res/layout/widget_layout_compact.xml index 8f139917..5e0108dc 100644 --- a/app/src/main/res/layout/widget_layout_compact.xml +++ b/app/src/main/res/layout/widget_layout_compact.xml @@ -43,6 +43,45 @@ android:textColor="@color/widget_subtitle" android:layout_width="match_parent" android:layout_height="wrap_content"/> + + + + + + + + + + + + + + + + + + Tempo Widget Şu an oynatılmıyor Tempo’yu aç + 0:00 + 0:00 Albüm kapağı Çal/Duraklat Sonraki parça diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 483b4958..6a4851cd 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -442,6 +442,8 @@ Tempo Widget Not playing Open Tempo + 0:00 + 0:00 Album artwork Play or pause Next track 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 de6a6f06..8ed4ecdf 100644 --- a/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/notquitemy/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -394,12 +394,16 @@ class MediaService : MediaLibraryService() { val artist = mi?.mediaMetadata?.artist?.toString() ?: mi?.mediaMetadata?.extras?.getString("artist") 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 WidgetUpdateManager.updateFromState( this, title ?: "", artist ?: "", coverId, - player.isPlaying + player.isPlaying, + 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 dadf0d7d..69123de4 100644 --- a/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/play/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -241,14 +241,16 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { ?: mi?.mediaMetadata?.extras?.getString("artist") 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 WidgetUpdateManager.updateFromState( this, title ?: "", artist ?: "", coverId, player.isPlaying, - player.currentPosition, - player.duration + 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 4c9c77aa..b7d507b6 100644 --- a/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/tempo/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -240,14 +240,16 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { val artist = mi?.mediaMetadata?.artist?.toString() ?: mi?.mediaMetadata?.extras?.getString("artist") 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 WidgetUpdateManager.updateFromState( this, title ?: "", artist ?: "", coverId, player.isPlaying, - player.currentPosition, - player.duration + position, + duration ) }