From b335ddec01c050c76a707480e7d5e748b8047995 Mon Sep 17 00:00:00 2001 From: pca006132 Date: Sun, 2 Nov 2025 17:22:41 +0800 Subject: [PATCH 01/12] cache artwork bitmap --- .../tempo/service/MediaService.kt | 30 ++++++++- .../tempo/widget/WidgetUpdateManager.java | 65 +++++++++++-------- .../tempo/service/MediaService.kt | 30 ++++++++- 3 files changed, 97 insertions(+), 28 deletions(-) diff --git a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt index bb237c62..f1ae6de7 100644 --- a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -5,6 +5,8 @@ import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.TaskStackBuilder import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Drawable import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities @@ -13,6 +15,7 @@ import android.os.Bundle import android.os.IBinder import android.os.Handler import android.os.Looper +import android.text.TextUtils import android.util.Log import androidx.media3.common.* import androidx.media3.common.util.UnstableApi @@ -21,7 +24,10 @@ import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.MediaSource import androidx.media3.session.* import androidx.media3.session.MediaSession.ControllerInfo +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition import com.cappielloantonio.tempo.R +import com.cappielloantonio.tempo.glide.CustomGlideRequest import com.cappielloantonio.tempo.repository.QueueRepository import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.AssetLinkUtil @@ -35,6 +41,7 @@ import com.cappielloantonio.tempo.widget.WidgetUpdateManager import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture +import java.util.Optional @UnstableApi @@ -61,6 +68,7 @@ class MediaService : MediaLibraryService() { widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS) } } + @Volatile private var artCache : Optional> = Optional.empty>() inner class LocalBinder : Binder() { fun getEqualizerManager(): EqualizerManager { @@ -368,6 +376,7 @@ class MediaService : MediaLibraryService() { } override fun onIsPlayingChanged(isPlaying: Boolean) { + artCache = Optional.empty() if (!isPlaying) { MediaManager.setPlayingPausedTimestamp( player.currentMediaItem, @@ -494,6 +503,16 @@ class MediaService : MediaLibraryService() { .build() } + private inner class CustomGlideTarget : CustomTarget() { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + artCache = Optional.of(Optional.of(resource)) + } + + override fun onLoadCleared(placeholder: Drawable?) { + artCache = Optional.of(Optional.empty()) + } + } + private fun updateWidget() { val mi = player.currentMediaItem val title = mi?.mediaMetadata?.title?.toString() @@ -512,12 +531,21 @@ class MediaService : MediaLibraryService() { ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId")) val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L + + if (!TextUtils.isEmpty(coverId) && artCache.isEmpty) { + CustomGlideRequest.loadAlbumArtBitmap( + applicationContext, + coverId, + WidgetUpdateManager.WIDGET_SAFE_ART_SIZE, + CustomGlideTarget()) + } + WidgetUpdateManager.updateFromState( this, title ?: "", artist ?: "", album ?: "", - coverId, + artCache, player.isPlaying, player.shuffleModeEnabled, player.repeatMode, 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 f159c526..6bf52f95 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java @@ -13,7 +13,9 @@ import com.bumptech.glide.request.transition.Transition; import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.R; +import androidx.annotation.OptIn; import androidx.media3.common.C; +import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaController; import androidx.media3.session.SessionToken; @@ -23,11 +25,12 @@ import com.cappielloantonio.tempo.util.MusicUtil; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; +import java.util.Optional; import java.util.concurrent.ExecutionException; public final class WidgetUpdateManager { - private static final int WIDGET_SAFE_ART_SIZE = 512; + public static final int WIDGET_SAFE_ART_SIZE = 512; public static void updateFromState(Context ctx, String title, @@ -68,11 +71,12 @@ public final class WidgetUpdateManager { } } + public static void updateFromState(Context ctx, String title, String artist, String album, - String coverArtId, + Optional> coverArt, boolean playing, boolean shuffleEnabled, int repeatMode, @@ -93,6 +97,32 @@ public final class WidgetUpdateManager { final String albumLinkFinal = albumLink; final String artistLinkFinal = artistLink; + AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx); + int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class)); + Bitmap resource = coverArt.filter(Optional::isPresent).map(Optional::get).orElse(null); + + for (int id : ids) { + 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, songLinkFinal, albumLinkFinal, artistLinkFinal); + mgr.updateAppWidget(id, rv); + } + } + + public static void updateFromState(Context ctx, + String title, + String artist, + String album, + String coverArtId, + boolean playing, + boolean shuffleEnabled, + int repeatMode, + long positionMs, + long durationMs, + String songLink, + String albumLink, + String artistLink) { + final Context appCtx = ctx.getApplicationContext(); if (!TextUtils.isEmpty(coverArtId)) { CustomGlideRequest.loadAlbumArtBitmap( appCtx, @@ -101,41 +131,24 @@ public final class WidgetUpdateManager { new CustomTarget() { @Override public void onResourceReady(Bitmap resource, Transition transition) { - 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, alb, resource, p, - timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); - WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal); - mgr.updateAppWidget(id, rv); - } + updateFromState(ctx, title, artist, album, Optional.of(Optional.of(resource)), + playing, shuffleEnabled, repeatMode, positionMs, durationMs, songLink, albumLink, artistLink); } @Override public void onLoadCleared(Drawable placeholder) { - 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, alb, null, p, - timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); - WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal); - mgr.updateAppWidget(id, rv); - } + updateFromState(ctx, title, artist, album, Optional.of(Optional.empty()), + playing, shuffleEnabled, repeatMode, positionMs, durationMs, songLink, albumLink, artistLink); } } ); } else { - 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, alb, null, p, - timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); - WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal); - mgr.updateAppWidget(id, rv); - } + updateFromState(ctx, title, artist, album, Optional.of(Optional.empty()), + playing, shuffleEnabled, repeatMode, positionMs, durationMs, songLink, albumLink, artistLink); } } + @OptIn(markerClass = UnstableApi.class) public static void refreshFromController(Context ctx) { final Context appCtx = ctx.getApplicationContext(); SessionToken token = new SessionToken(appCtx, new ComponentName(appCtx, MediaService.class)); diff --git a/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaService.kt index 36ea5b26..62b61255 100644 --- a/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -4,6 +4,8 @@ import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.TaskStackBuilder import android.content.Intent +import android.graphics.Bitmap +import android.graphics.drawable.Drawable import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities @@ -11,6 +13,7 @@ import android.os.Binder import android.os.IBinder import android.os.Handler import android.os.Looper +import android.text.TextUtils import android.util.Log import androidx.core.content.ContextCompat import androidx.media3.cast.CastPlayer @@ -25,7 +28,10 @@ import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession.ControllerInfo +import com.bumptech.glide.request.target.CustomTarget +import com.bumptech.glide.request.transition.Transition import com.cappielloantonio.tempo.repository.AutomotiveRepository +import com.cappielloantonio.tempo.glide.CustomGlideRequest import com.cappielloantonio.tempo.repository.QueueRepository import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.AssetLinkUtil @@ -39,6 +45,7 @@ import com.cappielloantonio.tempo.widget.WidgetUpdateManager import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability +import java.util.Optional @UnstableApi class MediaService : MediaLibraryService(), SessionAvailabilityListener { @@ -49,6 +56,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { private lateinit var librarySessionCallback: MediaLibrarySessionCallback private lateinit var networkCallback: CustomNetworkCallback lateinit var equalizerManager: EqualizerManager + @Volatile private var artCache : Optional> = Optional.empty>() inner class LocalBinder : Binder() { fun getEqualizerManager(): EqualizerManager { @@ -278,6 +286,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { } override fun onIsPlayingChanged(isPlaying: Boolean) { + artCache = Optional.empty() if (!isPlaying) { MediaManager.setPlayingPausedTimestamp( player.currentMediaItem, @@ -339,6 +348,16 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { } } + private inner class CustomGlideTarget : CustomTarget() { + override fun onResourceReady(resource: Bitmap, transition: Transition?) { + artCache = Optional.of(Optional.of(resource)) + } + + override fun onLoadCleared(placeholder: Drawable?) { + artCache = Optional.of(Optional.empty()) + } + } + private fun updateWidget() { val mi = player.currentMediaItem val title = mi?.mediaMetadata?.title?.toString() @@ -357,12 +376,21 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId")) val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L + + if (!TextUtils.isEmpty(coverId) && artCache.isEmpty) { + CustomGlideRequest.loadAlbumArtBitmap( + applicationContext, + coverId, + WidgetUpdateManager.WIDGET_SAFE_ART_SIZE, + CustomGlideTarget()) + } + WidgetUpdateManager.updateFromState( this, title ?: "", artist ?: "", album ?: "", - coverId, + artCache, player.isPlaying, player.shuffleModeEnabled, player.repeatMode, From be9eec625ab191a3e7bf99a94457bfa62575659e Mon Sep 17 00:00:00 2001 From: pca006132 Date: Mon, 3 Nov 2025 14:46:10 +0800 Subject: [PATCH 02/12] avoid full updates --- .../tempo/service/MediaService.kt | 89 ++++++++++++++----- .../tempo/widget/WidgetUpdateManager.java | 82 +++++++++-------- .../tempo/widget/WidgetViewsFactory.java | 2 +- 3 files changed, 108 insertions(+), 65 deletions(-) diff --git a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt index f1ae6de7..b7ea7773 100644 --- a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -4,7 +4,10 @@ import android.annotation.SuppressLint import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.TaskStackBuilder +import android.content.BroadcastReceiver +import android.content.Context import android.content.Intent +import android.content.IntentFilter import android.graphics.Bitmap import android.graphics.drawable.Drawable import android.net.ConnectivityManager @@ -60,7 +63,7 @@ class MediaService : MediaLibraryService() { private var widgetUpdateScheduled = false private val widgetUpdateRunnable = object : Runnable { override fun run() { - if (!player.isPlaying) { + if (!player.isPlaying || !screenOn) { widgetUpdateScheduled = false return } @@ -68,7 +71,29 @@ class MediaService : MediaLibraryService() { widgetUpdateHandler.postDelayed(this, WIDGET_UPDATE_INTERVAL_MS) } } - @Volatile private var artCache : Optional> = Optional.empty>() + + private var prevPlayerStates = Triple(false, false, -1) + @Volatile private var nowPlayingChanged = false + @Volatile private var artCacheUpdated = false + @Volatile private var artCache : Bitmap? = null + @Volatile private var screenOn = true + + val broadCastReceiver = object : BroadcastReceiver() { + override fun onReceive(contxt: Context?, intent: Intent?) { + when (intent?.action) { + Intent.ACTION_SCREEN_ON -> { + Log.d("MediaService", "screenOn"); + screenOn = true + widgetUpdateHandler.post(widgetUpdateRunnable) + } + Intent.ACTION_SCREEN_OFF -> { + Log.d("MediaService", "screenOff"); + screenOn = false + } + } + } + } + inner class LocalBinder : Binder() { fun getEqualizerManager(): EqualizerManager { @@ -134,6 +159,7 @@ class MediaService : MediaLibraryService() { initializePlayerListener() initializeEqualizerManager() initializeNetworkListener() + initializeScreenListener() setPlayer(player) } @@ -143,6 +169,7 @@ class MediaService : MediaLibraryService() { } override fun onDestroy() { + unregisterReceiver(broadCastReceiver) releaseNetworkCallback() equalizerManager.release() stopWidgetUpdates() @@ -288,6 +315,12 @@ class MediaService : MediaLibraryService() { player.repeatMode = Preferences.getRepeatMode() } + private fun initializeScreenListener() { + val filter = IntentFilter(Intent.ACTION_SCREEN_ON) + filter.addAction(Intent.ACTION_SCREEN_OFF) + registerReceiver(broadCastReceiver, filter) + } + private fun initializeEqualizerManager() { equalizerManager = EqualizerManager() val audioSessionId = player.audioSessionId @@ -376,7 +409,9 @@ class MediaService : MediaLibraryService() { } override fun onIsPlayingChanged(isPlaying: Boolean) { - artCache = Optional.empty() + nowPlayingChanged = true + artCacheUpdated = false + artCache = null if (!isPlaying) { MediaManager.setPlayingPausedTimestamp( player.currentMediaItem, @@ -390,7 +425,8 @@ class MediaService : MediaLibraryService() { } else { stopWidgetUpdates() } - updateWidget() + if (screenOn) + updateWidget() } override fun onPlaybackStateChanged(playbackState: Int) { @@ -505,11 +541,11 @@ class MediaService : MediaLibraryService() { private inner class CustomGlideTarget : CustomTarget() { override fun onResourceReady(resource: Bitmap, transition: Transition?) { - artCache = Optional.of(Optional.of(resource)) + artCache = resource } override fun onLoadCleared(placeholder: Drawable?) { - artCache = Optional.of(Optional.empty()) + artCache = null } } @@ -532,7 +568,7 @@ class MediaService : MediaLibraryService() { val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L - if (!TextUtils.isEmpty(coverId) && artCache.isEmpty) { + if (!TextUtils.isEmpty(coverId) && nowPlayingChanged) { CustomGlideRequest.loadAlbumArtBitmap( applicationContext, coverId, @@ -540,21 +576,30 @@ class MediaService : MediaLibraryService() { CustomGlideTarget()) } - WidgetUpdateManager.updateFromState( - this, - title ?: "", - artist ?: "", - album ?: "", - artCache, - player.isPlaying, - player.shuffleModeEnabled, - player.repeatMode, - position, - duration, - songLink, - albumLink, - artistLink - ) + val newPlayerState = Triple(player.isPlaying, player.shuffleModeEnabled, player.repeatMode) + if (nowPlayingChanged || prevPlayerStates != newPlayerState) { + WidgetUpdateManager.updateFromState( + this, + title ?: "", + artist ?: "", + album ?: "", + Optional.ofNullable(artCache), + player.isPlaying, + player.shuffleModeEnabled, + player.repeatMode, + position, + duration, + songLink, + albumLink, + artistLink + ) + prevPlayerStates = newPlayerState + Log.d("MediaService", "fullUpdate"); + } else { + WidgetUpdateManager.updateProgress(this, position, duration) + Log.d("MediaService", "updateProgress"); + } + nowPlayingChanged = false } private fun scheduleWidgetUpdates() { 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 6bf52f95..c4ade7c1 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java @@ -7,6 +7,7 @@ import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.TextUtils; +import android.widget.RemoteViews; import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.transition.Transition; @@ -36,7 +37,7 @@ public final class WidgetUpdateManager { String title, String artist, String album, - Bitmap art, + Optional art, boolean playing, boolean shuffleEnabled, int repeatMode, @@ -54,13 +55,47 @@ public final class WidgetUpdateManager { 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, album, art, playing, + android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, album, art.orElse(null), playing, timing.elapsedText, timing.totalText, timing.progress, shuffleEnabled, repeatMode, id); WidgetProvider.attachIntents(ctx, rv, id, songLink, albumLink, artistLink); mgr.updateAppWidget(id, rv); } } + public static void updateProgress(Context ctx, + long positionMs, + long durationMs) { + final TimingInfo timing = createTimingInfo(positionMs, durationMs); + AppWidgetManager mgr = AppWidgetManager.getInstance(ctx); + int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class)); + for (int id : ids) { + LayoutSize size = resolveLayoutSize(ctx, id); + int layoutRes = 0; + switch (size) { + case MEDIUM: + layoutRes = R.layout.widget_layout_medium; + break; + case LARGE: + layoutRes = R.layout.widget_layout_large_short; + break; + case EXPANDED: + layoutRes = R.layout.widget_layout_large; + break; + case COMPACT: + default: + layoutRes = R.layout.widget_layout_compact; + break; + } + + RemoteViews rv = new RemoteViews(ctx.getPackageName(), layoutRes); + int safeProgress = Math.max(0, Math.min(timing.progress, WidgetViewsFactory.PROGRESS_MAX)); + rv.setTextViewText(R.id.time_elapsed, timing.elapsedText); + rv.setTextViewText(R.id.time_total, timing.totalText); + rv.setProgressBar(R.id.progress, WidgetViewsFactory.PROGRESS_MAX, safeProgress, false); + mgr.updateAppWidget(id, rv); + } + } + public static void pushNow(Context ctx) { AppWidgetManager mgr = AppWidgetManager.getInstance(ctx); int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class)); @@ -72,43 +107,6 @@ public final class WidgetUpdateManager { } - public static void updateFromState(Context ctx, - String title, - String artist, - String album, - Optional> coverArt, - boolean playing, - boolean shuffleEnabled, - int repeatMode, - long positionMs, - long durationMs, - String songLink, - String albumLink, - String artistLink) { - 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); - final String songLinkFinal = songLink; - final String albumLinkFinal = albumLink; - final String artistLinkFinal = artistLink; - - AppWidgetManager mgr = AppWidgetManager.getInstance(appCtx); - int[] ids = mgr.getAppWidgetIds(new ComponentName(appCtx, WidgetProvider4x1.class)); - Bitmap resource = coverArt.filter(Optional::isPresent).map(Optional::get).orElse(null); - - for (int id : ids) { - 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, songLinkFinal, albumLinkFinal, artistLinkFinal); - mgr.updateAppWidget(id, rv); - } - } - public static void updateFromState(Context ctx, String title, String artist, @@ -131,19 +129,19 @@ public final class WidgetUpdateManager { new CustomTarget() { @Override public void onResourceReady(Bitmap resource, Transition transition) { - updateFromState(ctx, title, artist, album, Optional.of(Optional.of(resource)), + updateFromState(ctx, title, artist, album, Optional.of(resource), playing, shuffleEnabled, repeatMode, positionMs, durationMs, songLink, albumLink, artistLink); } @Override public void onLoadCleared(Drawable placeholder) { - updateFromState(ctx, title, artist, album, Optional.of(Optional.empty()), + updateFromState(ctx, title, artist, album, Optional.empty(), playing, shuffleEnabled, repeatMode, positionMs, durationMs, songLink, albumLink, artistLink); } } ); } else { - updateFromState(ctx, title, artist, album, Optional.of(Optional.empty()), + updateFromState(ctx, title, artist, album, Optional.empty(), playing, shuffleEnabled, repeatMode, positionMs, durationMs, songLink, albumLink, artistLink); } } 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 c66fd1cb..3cc53dc8 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java @@ -19,7 +19,7 @@ import com.cappielloantonio.tempo.R; public final class WidgetViewsFactory { - static final int PROGRESS_MAX = 1000; + public static final int PROGRESS_MAX = 1000; private static final float ALBUM_ART_CORNER_RADIUS_DP = 6f; private WidgetViewsFactory() { From 0028872e3fbba23a6c15daa70233e68bf6d9831c Mon Sep 17 00:00:00 2001 From: pca006132 Date: Mon, 3 Nov 2025 15:07:29 +0800 Subject: [PATCH 03/12] update one media item only --- .../tempo/service/MediaService.kt | 26 +++++++++---------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt index b7ea7773..bc9bda13 100644 --- a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -117,16 +117,6 @@ class MediaService : MediaLibraryService() { const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER" } - fun updateMediaItems() { - Log.d("MediaService", "update items"); - val n = player.mediaItemCount - val k = player.currentMediaItemIndex - val current = player.currentPosition - val items = (0 .. n-1).map{i -> MappingUtil.mapMediaItem(player.getMediaItemAt(i))} - player.clearMediaItems() - player.setMediaItems(items, k, current) - } - inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() { var wasWifi = false @@ -143,7 +133,12 @@ class MediaService : MediaLibraryService() { if (isWifi != wasWifi) { wasWifi = isWifi widgetUpdateHandler.post(Runnable { - updateMediaItems() + Log.d("MediaService", "update item due to network change"); + val pos = player.currentPosition + val k = player.currentMediaItemIndex + val item = MappingUtil.mapMediaItem(player.getMediaItemAt(k)) + player.replaceMediaItem(k, item) + player.seekTo(pos) }) } } @@ -356,7 +351,6 @@ class MediaService : MediaLibraryService() { private fun initializeNetworkListener() { networkCallback = CustomNetworkCallback() getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback) - updateMediaItems() } private fun restorePlayerFromQueue() { @@ -400,8 +394,12 @@ class MediaService : MediaLibraryService() { override fun onTracksChanged(tracks: Tracks) { ReplayGainUtil.setReplayGain(player, tracks) val currentMediaItem = player.currentMediaItem - if (currentMediaItem != null && currentMediaItem.mediaMetadata.extras != null) { - MediaManager.scrobble(currentMediaItem, false) + + val item = MappingUtil.mapMediaItem(currentMediaItem) + player.replaceMediaItem(player.currentMediaItemIndex, item) + + if (item != null && item.mediaMetadata.extras != null) { + MediaManager.scrobble(item, false) } if (player.currentMediaItemIndex + 1 == player.mediaItemCount) From ba94d7e5cc6786aa3ecaca070696afa0559dadda Mon Sep 17 00:00:00 2001 From: pca006132 Date: Mon, 3 Nov 2025 16:01:15 +0800 Subject: [PATCH 04/12] fix null --- .../com/cappielloantonio/tempo/service/MediaService.kt | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt index bc9bda13..53db697c 100644 --- a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -395,11 +395,13 @@ class MediaService : MediaLibraryService() { ReplayGainUtil.setReplayGain(player, tracks) val currentMediaItem = player.currentMediaItem - val item = MappingUtil.mapMediaItem(currentMediaItem) - player.replaceMediaItem(player.currentMediaItemIndex, item) + if (currentMediaItem != null) { + val item = MappingUtil.mapMediaItem(currentMediaItem) + player.replaceMediaItem(player.currentMediaItemIndex, item) - if (item != null && item.mediaMetadata.extras != null) { - MediaManager.scrobble(item, false) + if (item.mediaMetadata.extras != null) { + MediaManager.scrobble(item, false) + } } if (player.currentMediaItemIndex + 1 == player.mediaItemCount) From 1dca1ef68df644d734c0a229c9d0fc16da96ba8b Mon Sep 17 00:00:00 2001 From: pca006132 Date: Mon, 3 Nov 2025 16:16:06 +0800 Subject: [PATCH 05/12] avoid updating player bottom sheet when not visible --- .../ui/fragment/PlayerBottomSheetFragment.java | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java index e2bca343..20774b75 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java @@ -3,6 +3,7 @@ package com.cappielloantonio.tempo.ui.fragment; import android.content.ComponentName; import android.os.Bundle; import android.os.Handler; +import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -47,7 +48,7 @@ public class PlayerBottomSheetFragment extends Fragment { private PlayerBottomSheetViewModel playerBottomSheetViewModel; private ListenableFuture mediaBrowserListenableFuture; - private Handler progressBarHandler; + private Handler progressBarHandler = null; private Runnable progressBarRunnable; @Nullable @@ -66,6 +67,14 @@ public class PlayerBottomSheetFragment extends Fragment { return view; } + @Override + public void onResume() { + super.onResume(); + if (progressBarHandler != null) + progressBarHandler.post(progressBarRunnable); + Log.d("Player", "resumed"); + } + @Override public void onStart() { super.onStart(); @@ -281,6 +290,10 @@ public class PlayerBottomSheetFragment extends Fragment { private void defineProgressBarHandler(MediaBrowser mediaBrowser) { progressBarHandler = new Handler(); progressBarRunnable = () -> { + if (!isResumed()) { + Log.d("Player", "not resumed"); + return; + } setProgress(mediaBrowser); progressBarHandler.postDelayed(progressBarRunnable, 1000); }; From 38c144c0732ed2727fe66ab6d9a409e50d1b651f Mon Sep 17 00:00:00 2001 From: pca006132 Date: Mon, 3 Nov 2025 22:33:21 +0800 Subject: [PATCH 06/12] avoid rebuffering after track change --- .../tempo/service/MediaService.kt | 26 +++++++++++++++---- 1 file changed, 21 insertions(+), 5 deletions(-) diff --git a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt index 53db697c..c332d008 100644 --- a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -21,6 +21,8 @@ import android.os.Looper import android.text.TextUtils import android.util.Log import androidx.media3.common.* +import androidx.media3.common.Player.REPEAT_MODE_ALL +import androidx.media3.common.Player.RepeatMode import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer @@ -136,9 +138,12 @@ class MediaService : MediaLibraryService() { Log.d("MediaService", "update item due to network change"); val pos = player.currentPosition val k = player.currentMediaItemIndex - val item = MappingUtil.mapMediaItem(player.getMediaItemAt(k)) - player.replaceMediaItem(k, item) - player.seekTo(pos) + val old = player.getMediaItemAt(k) + val item = MappingUtil.mapMediaItem(old) + if (item.requestMetadata.mediaUri != old.requestMetadata.mediaUri) { + player.replaceMediaItem(k, item) + player.seekTo(pos) + } }) } } @@ -397,15 +402,26 @@ class MediaService : MediaLibraryService() { if (currentMediaItem != null) { val item = MappingUtil.mapMediaItem(currentMediaItem) - player.replaceMediaItem(player.currentMediaItemIndex, item) + if (item.requestMetadata.mediaUri != currentMediaItem.requestMetadata.mediaUri) + player.replaceMediaItem(player.currentMediaItemIndex, item) if (item.mediaMetadata.extras != null) { MediaManager.scrobble(item, false) } } - if (player.currentMediaItemIndex + 1 == player.mediaItemCount) + if (player.currentMediaItemIndex + 1 < player.mediaItemCount) + player.replaceMediaItem( + player.currentMediaItemIndex + 1, + MappingUtil.mapMediaItem(player.getMediaItemAt(player.currentMediaItemIndex + 1))) + + if (player.currentMediaItemIndex + 1 == player.mediaItemCount) { + if (player.repeatMode == REPEAT_MODE_ALL && player.mediaItemCount > 1) + player.replaceMediaItem( + 0, + MappingUtil.mapMediaItem(player.getMediaItemAt(0))) MediaManager.continuousPlay(player.currentMediaItem) + } } override fun onIsPlayingChanged(isPlaying: Boolean) { From 2644fa52b6f45e269771e23ba6e779f0d6b8a2d9 Mon Sep 17 00:00:00 2001 From: pca006132 Date: Mon, 3 Nov 2025 22:33:42 +0800 Subject: [PATCH 07/12] enable offload --- .../com/cappielloantonio/tempo/service/MediaService.kt | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt index c332d008..b1585e42 100644 --- a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -311,6 +311,13 @@ class MediaService : MediaLibraryService() { .setLoadControl(initializeLoadControl()) .build() + val params = player.trackSelectionParameters.buildUpon() + .setAudioOffloadPreferences( + TrackSelectionParameters.AudioOffloadPreferences.Builder().setAudioOffloadMode( + TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED + ).build() + ).build() + player.trackSelectionParameters = params player.shuffleModeEnabled = Preferences.isShuffleModeEnabled() player.repeatMode = Preferences.getRepeatMode() } From d5d504fc64cf9057def98ecb1af5590f0cfd5658 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Tue, 4 Nov 2025 07:02:19 -0800 Subject: [PATCH 08/12] Revert "improve battery consumption" --- .../tempo/service/MediaService.kt | 160 ++++-------------- .../fragment/PlayerBottomSheetFragment.java | 15 +- .../tempo/widget/WidgetUpdateManager.java | 87 +++++----- .../tempo/widget/WidgetViewsFactory.java | 2 +- .../tempo/service/MediaService.kt | 30 +--- 5 files changed, 73 insertions(+), 221 deletions(-) diff --git a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt index b1585e42..bb237c62 100644 --- a/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/degoogled/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -4,12 +4,7 @@ import android.annotation.SuppressLint import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.TaskStackBuilder -import android.content.BroadcastReceiver -import android.content.Context import android.content.Intent -import android.content.IntentFilter -import android.graphics.Bitmap -import android.graphics.drawable.Drawable import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities @@ -18,21 +13,15 @@ import android.os.Bundle import android.os.IBinder import android.os.Handler import android.os.Looper -import android.text.TextUtils import android.util.Log import androidx.media3.common.* -import androidx.media3.common.Player.REPEAT_MODE_ALL -import androidx.media3.common.Player.RepeatMode import androidx.media3.common.util.UnstableApi import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.source.MediaSource import androidx.media3.session.* import androidx.media3.session.MediaSession.ControllerInfo -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition import com.cappielloantonio.tempo.R -import com.cappielloantonio.tempo.glide.CustomGlideRequest import com.cappielloantonio.tempo.repository.QueueRepository import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.AssetLinkUtil @@ -46,7 +35,6 @@ import com.cappielloantonio.tempo.widget.WidgetUpdateManager import com.google.common.collect.ImmutableList import com.google.common.util.concurrent.Futures import com.google.common.util.concurrent.ListenableFuture -import java.util.Optional @UnstableApi @@ -65,7 +53,7 @@ class MediaService : MediaLibraryService() { private var widgetUpdateScheduled = false private val widgetUpdateRunnable = object : Runnable { override fun run() { - if (!player.isPlaying || !screenOn) { + if (!player.isPlaying) { widgetUpdateScheduled = false return } @@ -74,29 +62,6 @@ class MediaService : MediaLibraryService() { } } - private var prevPlayerStates = Triple(false, false, -1) - @Volatile private var nowPlayingChanged = false - @Volatile private var artCacheUpdated = false - @Volatile private var artCache : Bitmap? = null - @Volatile private var screenOn = true - - val broadCastReceiver = object : BroadcastReceiver() { - override fun onReceive(contxt: Context?, intent: Intent?) { - when (intent?.action) { - Intent.ACTION_SCREEN_ON -> { - Log.d("MediaService", "screenOn"); - screenOn = true - widgetUpdateHandler.post(widgetUpdateRunnable) - } - Intent.ACTION_SCREEN_OFF -> { - Log.d("MediaService", "screenOff"); - screenOn = false - } - } - } - } - - inner class LocalBinder : Binder() { fun getEqualizerManager(): EqualizerManager { return this@MediaService.equalizerManager @@ -119,6 +84,16 @@ class MediaService : MediaLibraryService() { const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER" } + fun updateMediaItems() { + Log.d("MediaService", "update items"); + val n = player.mediaItemCount + val k = player.currentMediaItemIndex + val current = player.currentPosition + val items = (0 .. n-1).map{i -> MappingUtil.mapMediaItem(player.getMediaItemAt(i))} + player.clearMediaItems() + player.setMediaItems(items, k, current) + } + inner class CustomNetworkCallback : ConnectivityManager.NetworkCallback() { var wasWifi = false @@ -135,15 +110,7 @@ class MediaService : MediaLibraryService() { if (isWifi != wasWifi) { wasWifi = isWifi widgetUpdateHandler.post(Runnable { - Log.d("MediaService", "update item due to network change"); - val pos = player.currentPosition - val k = player.currentMediaItemIndex - val old = player.getMediaItemAt(k) - val item = MappingUtil.mapMediaItem(old) - if (item.requestMetadata.mediaUri != old.requestMetadata.mediaUri) { - player.replaceMediaItem(k, item) - player.seekTo(pos) - } + updateMediaItems() }) } } @@ -159,7 +126,6 @@ class MediaService : MediaLibraryService() { initializePlayerListener() initializeEqualizerManager() initializeNetworkListener() - initializeScreenListener() setPlayer(player) } @@ -169,7 +135,6 @@ class MediaService : MediaLibraryService() { } override fun onDestroy() { - unregisterReceiver(broadCastReceiver) releaseNetworkCallback() equalizerManager.release() stopWidgetUpdates() @@ -311,23 +276,10 @@ class MediaService : MediaLibraryService() { .setLoadControl(initializeLoadControl()) .build() - val params = player.trackSelectionParameters.buildUpon() - .setAudioOffloadPreferences( - TrackSelectionParameters.AudioOffloadPreferences.Builder().setAudioOffloadMode( - TrackSelectionParameters.AudioOffloadPreferences.AUDIO_OFFLOAD_MODE_ENABLED - ).build() - ).build() - player.trackSelectionParameters = params player.shuffleModeEnabled = Preferences.isShuffleModeEnabled() player.repeatMode = Preferences.getRepeatMode() } - private fun initializeScreenListener() { - val filter = IntentFilter(Intent.ACTION_SCREEN_ON) - filter.addAction(Intent.ACTION_SCREEN_OFF) - registerReceiver(broadCastReceiver, filter) - } - private fun initializeEqualizerManager() { equalizerManager = EqualizerManager() val audioSessionId = player.audioSessionId @@ -363,6 +315,7 @@ class MediaService : MediaLibraryService() { private fun initializeNetworkListener() { networkCallback = CustomNetworkCallback() getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback) + updateMediaItems() } private fun restorePlayerFromQueue() { @@ -406,35 +359,15 @@ class MediaService : MediaLibraryService() { override fun onTracksChanged(tracks: Tracks) { ReplayGainUtil.setReplayGain(player, tracks) val currentMediaItem = player.currentMediaItem - - if (currentMediaItem != null) { - val item = MappingUtil.mapMediaItem(currentMediaItem) - if (item.requestMetadata.mediaUri != currentMediaItem.requestMetadata.mediaUri) - player.replaceMediaItem(player.currentMediaItemIndex, item) - - if (item.mediaMetadata.extras != null) { - MediaManager.scrobble(item, false) - } + if (currentMediaItem != null && currentMediaItem.mediaMetadata.extras != null) { + MediaManager.scrobble(currentMediaItem, false) } - if (player.currentMediaItemIndex + 1 < player.mediaItemCount) - player.replaceMediaItem( - player.currentMediaItemIndex + 1, - MappingUtil.mapMediaItem(player.getMediaItemAt(player.currentMediaItemIndex + 1))) - - if (player.currentMediaItemIndex + 1 == player.mediaItemCount) { - if (player.repeatMode == REPEAT_MODE_ALL && player.mediaItemCount > 1) - player.replaceMediaItem( - 0, - MappingUtil.mapMediaItem(player.getMediaItemAt(0))) + if (player.currentMediaItemIndex + 1 == player.mediaItemCount) MediaManager.continuousPlay(player.currentMediaItem) - } } override fun onIsPlayingChanged(isPlaying: Boolean) { - nowPlayingChanged = true - artCacheUpdated = false - artCache = null if (!isPlaying) { MediaManager.setPlayingPausedTimestamp( player.currentMediaItem, @@ -448,8 +381,7 @@ class MediaService : MediaLibraryService() { } else { stopWidgetUpdates() } - if (screenOn) - updateWidget() + updateWidget() } override fun onPlaybackStateChanged(playbackState: Int) { @@ -562,16 +494,6 @@ class MediaService : MediaLibraryService() { .build() } - private inner class CustomGlideTarget : CustomTarget() { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - artCache = resource - } - - override fun onLoadCleared(placeholder: Drawable?) { - artCache = null - } - } - private fun updateWidget() { val mi = player.currentMediaItem val title = mi?.mediaMetadata?.title?.toString() @@ -590,39 +512,21 @@ class MediaService : MediaLibraryService() { ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId")) val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L - - if (!TextUtils.isEmpty(coverId) && nowPlayingChanged) { - CustomGlideRequest.loadAlbumArtBitmap( - applicationContext, - coverId, - WidgetUpdateManager.WIDGET_SAFE_ART_SIZE, - CustomGlideTarget()) - } - - val newPlayerState = Triple(player.isPlaying, player.shuffleModeEnabled, player.repeatMode) - if (nowPlayingChanged || prevPlayerStates != newPlayerState) { - WidgetUpdateManager.updateFromState( - this, - title ?: "", - artist ?: "", - album ?: "", - Optional.ofNullable(artCache), - player.isPlaying, - player.shuffleModeEnabled, - player.repeatMode, - position, - duration, - songLink, - albumLink, - artistLink - ) - prevPlayerStates = newPlayerState - Log.d("MediaService", "fullUpdate"); - } else { - WidgetUpdateManager.updateProgress(this, position, duration) - Log.d("MediaService", "updateProgress"); - } - nowPlayingChanged = false + WidgetUpdateManager.updateFromState( + this, + title ?: "", + artist ?: "", + album ?: "", + coverId, + player.isPlaying, + player.shuffleModeEnabled, + player.repeatMode, + position, + duration, + songLink, + albumLink, + artistLink + ) } private fun scheduleWidgetUpdates() { diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java index 20774b75..e2bca343 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerBottomSheetFragment.java @@ -3,7 +3,6 @@ package com.cappielloantonio.tempo.ui.fragment; import android.content.ComponentName; import android.os.Bundle; import android.os.Handler; -import android.util.Log; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; @@ -48,7 +47,7 @@ public class PlayerBottomSheetFragment extends Fragment { private PlayerBottomSheetViewModel playerBottomSheetViewModel; private ListenableFuture mediaBrowserListenableFuture; - private Handler progressBarHandler = null; + private Handler progressBarHandler; private Runnable progressBarRunnable; @Nullable @@ -67,14 +66,6 @@ public class PlayerBottomSheetFragment extends Fragment { return view; } - @Override - public void onResume() { - super.onResume(); - if (progressBarHandler != null) - progressBarHandler.post(progressBarRunnable); - Log.d("Player", "resumed"); - } - @Override public void onStart() { super.onStart(); @@ -290,10 +281,6 @@ public class PlayerBottomSheetFragment extends Fragment { private void defineProgressBarHandler(MediaBrowser mediaBrowser) { progressBarHandler = new Handler(); progressBarRunnable = () -> { - if (!isResumed()) { - Log.d("Player", "not resumed"); - return; - } setProgress(mediaBrowser); progressBarHandler.postDelayed(progressBarRunnable, 1000); }; 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 c4ade7c1..f159c526 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetUpdateManager.java @@ -7,16 +7,13 @@ import android.graphics.Bitmap; import android.graphics.drawable.Drawable; import android.os.Bundle; import android.text.TextUtils; -import android.widget.RemoteViews; 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.annotation.OptIn; import androidx.media3.common.C; -import androidx.media3.common.util.UnstableApi; import androidx.media3.session.MediaController; import androidx.media3.session.SessionToken; @@ -26,18 +23,17 @@ import com.cappielloantonio.tempo.util.MusicUtil; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.MoreExecutors; -import java.util.Optional; import java.util.concurrent.ExecutionException; public final class WidgetUpdateManager { - public static final int WIDGET_SAFE_ART_SIZE = 512; + private static final int WIDGET_SAFE_ART_SIZE = 512; public static void updateFromState(Context ctx, String title, String artist, String album, - Optional art, + Bitmap art, boolean playing, boolean shuffleEnabled, int repeatMode, @@ -55,47 +51,13 @@ public final class WidgetUpdateManager { 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, album, art.orElse(null), playing, + 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, songLink, albumLink, artistLink); mgr.updateAppWidget(id, rv); } } - public static void updateProgress(Context ctx, - long positionMs, - long durationMs) { - final TimingInfo timing = createTimingInfo(positionMs, durationMs); - AppWidgetManager mgr = AppWidgetManager.getInstance(ctx); - int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class)); - for (int id : ids) { - LayoutSize size = resolveLayoutSize(ctx, id); - int layoutRes = 0; - switch (size) { - case MEDIUM: - layoutRes = R.layout.widget_layout_medium; - break; - case LARGE: - layoutRes = R.layout.widget_layout_large_short; - break; - case EXPANDED: - layoutRes = R.layout.widget_layout_large; - break; - case COMPACT: - default: - layoutRes = R.layout.widget_layout_compact; - break; - } - - RemoteViews rv = new RemoteViews(ctx.getPackageName(), layoutRes); - int safeProgress = Math.max(0, Math.min(timing.progress, WidgetViewsFactory.PROGRESS_MAX)); - rv.setTextViewText(R.id.time_elapsed, timing.elapsedText); - rv.setTextViewText(R.id.time_total, timing.totalText); - rv.setProgressBar(R.id.progress, WidgetViewsFactory.PROGRESS_MAX, safeProgress, false); - mgr.updateAppWidget(id, rv); - } - } - public static void pushNow(Context ctx) { AppWidgetManager mgr = AppWidgetManager.getInstance(ctx); int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class)); @@ -106,7 +68,6 @@ public final class WidgetUpdateManager { } } - public static void updateFromState(Context ctx, String title, String artist, @@ -121,6 +82,17 @@ public final class WidgetUpdateManager { String albumLink, String artistLink) { 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); + final String songLinkFinal = songLink; + final String albumLinkFinal = albumLink; + final String artistLinkFinal = artistLink; + if (!TextUtils.isEmpty(coverArtId)) { CustomGlideRequest.loadAlbumArtBitmap( appCtx, @@ -129,24 +101,41 @@ public final class WidgetUpdateManager { new CustomTarget() { @Override public void onResourceReady(Bitmap resource, Transition transition) { - updateFromState(ctx, title, artist, album, Optional.of(resource), - playing, shuffleEnabled, repeatMode, positionMs, durationMs, songLink, albumLink, artistLink); + 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, alb, resource, p, + timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); + WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal); + mgr.updateAppWidget(id, rv); + } } @Override public void onLoadCleared(Drawable placeholder) { - updateFromState(ctx, title, artist, album, Optional.empty(), - playing, shuffleEnabled, repeatMode, positionMs, durationMs, songLink, albumLink, artistLink); + 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, alb, null, p, + timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); + WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal); + mgr.updateAppWidget(id, rv); + } } } ); } else { - updateFromState(ctx, title, artist, album, Optional.empty(), - playing, shuffleEnabled, repeatMode, positionMs, durationMs, songLink, albumLink, artistLink); + 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, alb, null, p, + timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); + WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal); + mgr.updateAppWidget(id, rv); + } } } - @OptIn(markerClass = UnstableApi.class) public static void refreshFromController(Context ctx) { final Context appCtx = ctx.getApplicationContext(); SessionToken token = new SessionToken(appCtx, new ComponentName(appCtx, MediaService.class)); 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 3cc53dc8..c66fd1cb 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java +++ b/app/src/main/java/com/cappielloantonio/tempo/widget/WidgetViewsFactory.java @@ -19,7 +19,7 @@ import com.cappielloantonio.tempo.R; public final class WidgetViewsFactory { - public static final int PROGRESS_MAX = 1000; + static final int PROGRESS_MAX = 1000; private static final float ALBUM_ART_CORNER_RADIUS_DP = 6f; private WidgetViewsFactory() { diff --git a/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaService.kt b/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaService.kt index 62b61255..36ea5b26 100644 --- a/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaService.kt +++ b/app/src/tempus/java/com/cappielloantonio/tempo/service/MediaService.kt @@ -4,8 +4,6 @@ import android.app.PendingIntent.FLAG_IMMUTABLE import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.TaskStackBuilder import android.content.Intent -import android.graphics.Bitmap -import android.graphics.drawable.Drawable import android.net.ConnectivityManager import android.net.Network import android.net.NetworkCapabilities @@ -13,7 +11,6 @@ import android.os.Binder import android.os.IBinder import android.os.Handler import android.os.Looper -import android.text.TextUtils import android.util.Log import androidx.core.content.ContextCompat import androidx.media3.cast.CastPlayer @@ -28,10 +25,7 @@ import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.ExoPlayer import androidx.media3.session.MediaLibraryService import androidx.media3.session.MediaSession.ControllerInfo -import com.bumptech.glide.request.target.CustomTarget -import com.bumptech.glide.request.transition.Transition import com.cappielloantonio.tempo.repository.AutomotiveRepository -import com.cappielloantonio.tempo.glide.CustomGlideRequest import com.cappielloantonio.tempo.repository.QueueRepository import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.util.AssetLinkUtil @@ -45,7 +39,6 @@ import com.cappielloantonio.tempo.widget.WidgetUpdateManager import com.google.android.gms.cast.framework.CastContext import com.google.android.gms.common.ConnectionResult import com.google.android.gms.common.GoogleApiAvailability -import java.util.Optional @UnstableApi class MediaService : MediaLibraryService(), SessionAvailabilityListener { @@ -56,7 +49,6 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { private lateinit var librarySessionCallback: MediaLibrarySessionCallback private lateinit var networkCallback: CustomNetworkCallback lateinit var equalizerManager: EqualizerManager - @Volatile private var artCache : Optional> = Optional.empty>() inner class LocalBinder : Binder() { fun getEqualizerManager(): EqualizerManager { @@ -286,7 +278,6 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { } override fun onIsPlayingChanged(isPlaying: Boolean) { - artCache = Optional.empty() if (!isPlaying) { MediaManager.setPlayingPausedTimestamp( player.currentMediaItem, @@ -348,16 +339,6 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { } } - private inner class CustomGlideTarget : CustomTarget() { - override fun onResourceReady(resource: Bitmap, transition: Transition?) { - artCache = Optional.of(Optional.of(resource)) - } - - override fun onLoadCleared(placeholder: Drawable?) { - artCache = Optional.of(Optional.empty()) - } - } - private fun updateWidget() { val mi = player.currentMediaItem val title = mi?.mediaMetadata?.title?.toString() @@ -376,21 +357,12 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener { ?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId")) val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L - - if (!TextUtils.isEmpty(coverId) && artCache.isEmpty) { - CustomGlideRequest.loadAlbumArtBitmap( - applicationContext, - coverId, - WidgetUpdateManager.WIDGET_SAFE_ART_SIZE, - CustomGlideTarget()) - } - WidgetUpdateManager.updateFromState( this, title ?: "", artist ?: "", album ?: "", - artCache, + coverId, player.isPlaying, player.shuffleModeEnabled, player.repeatMode, From e5a928ec0f8bd1f3a53b6a4bf94a7cb60e36ee27 Mon Sep 17 00:00:00 2001 From: pca006132 Date: Wed, 5 Nov 2025 00:34:30 +0800 Subject: [PATCH 09/12] fix: skip mapping downloaded item --- .../java/com/cappielloantonio/tempo/util/MappingUtil.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java index 1222804b..0a6f33e3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/MappingUtil.java @@ -116,6 +116,13 @@ public class MappingUtil { } public static MediaItem mapMediaItem(MediaItem old) { + String mediaId = null; + if (old.requestMetadata.extras != null) + mediaId = old.requestMetadata.extras.getString("id"); + + if (mediaId != null && DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(mediaId)) { + return old; + } Uri uri = old.requestMetadata.mediaUri == null ? null : MusicUtil.updateStreamUri(old.requestMetadata.mediaUri); return new MediaItem.Builder() .setMediaId(old.mediaId) From 20900fb557b8fe2e50a7c3e80945838a917ce0c7 Mon Sep 17 00:00:00 2001 From: eddyizm Date: Tue, 4 Nov 2025 22:10:39 -0800 Subject: [PATCH 10/12] chore: pre release prep --- CHANGELOG.md | 15 +++++++++++++++ README.md | 8 +++++--- fastlane/metadata/android/en-US/changelogs/3.txt | 10 ++++++++++ 3 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/3.txt diff --git a/CHANGELOG.md b/CHANGELOG.md index 4e2788de..46e17505 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,20 @@ # Changelog + +## Pending release.. +* chore(i18n): Update Spanish (es-ES) translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/205 +* feat: shuffle for artists without using `getTopSongs` by @pca006132 in https://github.com/eddyizm/tempus/pull/207 +* chore: Update USAGE.md with instant mix details by @zc-devs in https://github.com/eddyizm/tempus/pull/220 +* feat: sort artists by album count by @pca006132 in https://github.com/eddyizm/tempus/pull/206 +* Fix downloaded tab performance by @pca006132 in https://github.com/eddyizm/tempus/pull/210 +* fix: remove NestedScrollViews for fragment_album_page by @pca006132 in https://github.com/eddyizm/tempus/pull/216 +* fix: playlist page should not snap by @pca006132 in https://github.com/eddyizm/tempus/pull/218 +* fix: do not override getItemViewType and getItemId by @pca006132 in https://github.com/eddyizm/tempus/pull/221 +* chore: update media3 dependencies by @pca006132 in https://github.com/eddyizm/tempus/pull/217 +* fix: update MediaItems after network change by @pca006132 in https://github.com/eddyizm/tempus/pull/222 + +--- + ## [4.0.7](https://github.com/eddyizm/tempo/releases/tag/v4.0.7) (2025-10-28) ## What's Changed * chore: updated tempo references to tempus including github check by @eddyizm in https://github.com/eddyizm/tempus/pull/197 diff --git a/README.md b/README.md index f0e76339..874a718e 100644 --- a/README.md +++ b/README.md @@ -47,9 +47,6 @@ Please note the two variants in the release assets include release/debug and 32/ [CHANGELOG.md](CHANGELOG.md) -[**Buy me a coffee**](https://ko-fi.com/eddyizm) - - ## Usage [Documentation](USAGE.md) (work in progress) @@ -114,6 +111,11 @@ Currently there are no tests but I would love to start on some unit tests. Not a hard requirement but any new feature/change should ideally include an update to the nacent documention. +## Support + +[**Buy me a coffee**](https://ko-fi.com/eddyizm) +bitcoin: `3QVHSSCJvn6yXEcJ3A3cxYLMmbvFsrnUs5` + ## License Tempus is released under the [GNU General Public License v3.0](LICENSE). Feel free to modify, distribute, and use the app in accordance with the terms of the license. Contributions to the project are also welcome. diff --git a/fastlane/metadata/android/en-US/changelogs/3.txt b/fastlane/metadata/android/en-US/changelogs/3.txt new file mode 100644 index 00000000..b9cf7455 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/3.txt @@ -0,0 +1,10 @@ +Update Spanish (es-ES) translation +Shuffle for artists without using `getTopSongs` +Update USAGE.md with instant mix detils +Sort artists by album count +Fix downloaded tab performance +Remove NestedScrollViews for fragment_album_page +Playlist page should not snap +Do not override getItemViewType and getItemId +Update media3 dependencies +Update MediaItems after network change From 9ab22bfede02144e2ddf53c9bd5ff8b1a657026e Mon Sep 17 00:00:00 2001 From: eddyizm Date: Wed, 5 Nov 2025 07:51:55 -0800 Subject: [PATCH 11/12] chore: bump version --- app/build.gradle | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 53758756..8cd604ba 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -10,8 +10,8 @@ android { minSdkVersion 24 targetSdk 35 - versionCode 2 - versionName '4.0.7' + versionCode 3 + versionName '4.1.0' testInstrumentationRunner 'androidx.test.runner.AndroidJUnitRunner' javaCompileOptions { From 28c2f87b26dd461a59bb607a6944f2b035e0f8aa Mon Sep 17 00:00:00 2001 From: eddyizm Date: Wed, 5 Nov 2025 09:15:11 -0800 Subject: [PATCH 12/12] chore: updated change log --- CHANGELOG.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 46e17505..429c41cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,12 @@ # Changelog - ## Pending release.. + +## [4.1.0](https://github.com/eddyizm/tempo/releases/tag/v4.1.0) (2025-11-05) +## What's Changed * chore(i18n): Update Spanish (es-ES) translation by @jaime-grj in https://github.com/eddyizm/tempus/pull/205 -* feat: shuffle for artists without using `getTopSongs` by @pca006132 in https://github.com/eddyizm/tempus/pull/207 -* chore: Update USAGE.md with instant mix details by @zc-devs in https://github.com/eddyizm/tempus/pull/220 +* shuffle for artists without using `getTopSongs` by @pca006132 in https://github.com/eddyizm/tempus/pull/207 +* Update USAGE.md with instant mix details by @zc-devs in https://github.com/eddyizm/tempus/pull/220 * feat: sort artists by album count by @pca006132 in https://github.com/eddyizm/tempus/pull/206 * Fix downloaded tab performance by @pca006132 in https://github.com/eddyizm/tempus/pull/210 * fix: remove NestedScrollViews for fragment_album_page by @pca006132 in https://github.com/eddyizm/tempus/pull/216 @@ -12,8 +14,12 @@ * fix: do not override getItemViewType and getItemId by @pca006132 in https://github.com/eddyizm/tempus/pull/221 * chore: update media3 dependencies by @pca006132 in https://github.com/eddyizm/tempus/pull/217 * fix: update MediaItems after network change by @pca006132 in https://github.com/eddyizm/tempus/pull/222 +* fix: skip mapping downloaded item by @pca006132 in https://github.com/eddyizm/tempus/pull/228 ---- +## New Contributors +* @pca006132 made their first contribution in https://github.com/eddyizm/tempus/pull/207 + +**Full Changelog**: https://github.com/eddyizm/tempus/compare/v4.0.7...v4.1.0 ## [4.0.7](https://github.com/eddyizm/tempo/releases/tag/v4.0.7) (2025-10-28) ## What's Changed