improve battery consumption (#223)

This commit is contained in:
eddyizm 2025-11-03 21:20:06 -08:00 committed by GitHub
commit 24eead2d0a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 221 additions and 73 deletions

View file

@ -4,7 +4,12 @@ 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
@ -13,15 +18,21 @@ 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
@ -35,6 +46,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
@ -53,7 +65,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
}
@ -62,6 +74,29 @@ 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
@ -84,16 +119,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
@ -110,7 +135,15 @@ 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 old = player.getMediaItemAt(k)
val item = MappingUtil.mapMediaItem(old)
if (item.requestMetadata.mediaUri != old.requestMetadata.mediaUri) {
player.replaceMediaItem(k, item)
player.seekTo(pos)
}
})
}
}
@ -126,6 +159,7 @@ class MediaService : MediaLibraryService() {
initializePlayerListener()
initializeEqualizerManager()
initializeNetworkListener()
initializeScreenListener()
setPlayer(player)
}
@ -135,6 +169,7 @@ class MediaService : MediaLibraryService() {
}
override fun onDestroy() {
unregisterReceiver(broadCastReceiver)
releaseNetworkCallback()
equalizerManager.release()
stopWidgetUpdates()
@ -276,10 +311,23 @@ 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
@ -315,7 +363,6 @@ class MediaService : MediaLibraryService() {
private fun initializeNetworkListener() {
networkCallback = CustomNetworkCallback()
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback)
updateMediaItems()
}
private fun restorePlayerFromQueue() {
@ -359,15 +406,35 @@ 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)
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 (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) {
nowPlayingChanged = true
artCacheUpdated = false
artCache = null
if (!isPlaying) {
MediaManager.setPlayingPausedTimestamp(
player.currentMediaItem,
@ -381,6 +448,7 @@ class MediaService : MediaLibraryService() {
} else {
stopWidgetUpdates()
}
if (screenOn)
updateWidget()
}
@ -494,6 +562,16 @@ class MediaService : MediaLibraryService() {
.build()
}
private inner class CustomGlideTarget : CustomTarget<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
artCache = resource
}
override fun onLoadCleared(placeholder: Drawable?) {
artCache = null
}
}
private fun updateWidget() {
val mi = player.currentMediaItem
val title = mi?.mediaMetadata?.title?.toString()
@ -512,12 +590,23 @@ 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 ?: "",
coverId,
Optional.ofNullable(artCache),
player.isPlaying,
player.shuffleModeEnabled,
player.repeatMode,
@ -527,6 +616,13 @@ class MediaService : MediaLibraryService() {
albumLink,
artistLink
)
prevPlayerStates = newPlayerState
Log.d("MediaService", "fullUpdate");
} else {
WidgetUpdateManager.updateProgress(this, position, duration)
Log.d("MediaService", "updateProgress");
}
nowPlayingChanged = false
}
private fun scheduleWidgetUpdates() {

View file

@ -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<MediaBrowser> 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);
};

View file

@ -7,13 +7,16 @@ 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;
@ -23,17 +26,18 @@ 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,
String artist,
String album,
Bitmap art,
Optional<Bitmap> art,
boolean playing,
boolean shuffleEnabled,
int repeatMode,
@ -51,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));
@ -68,6 +106,7 @@ public final class WidgetUpdateManager {
}
}
public static void updateFromState(Context ctx,
String title,
String artist,
@ -82,17 +121,6 @@ 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,
@ -101,41 +129,24 @@ public final class WidgetUpdateManager {
new CustomTarget<Bitmap>() {
@Override
public void onResourceReady(Bitmap resource, Transition<? super Bitmap> 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(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.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.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));

View file

@ -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() {

View file

@ -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<Bitmap>> = Optional.empty<Optional<Bitmap>>()
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<Bitmap>() {
override fun onResourceReady(resource: Bitmap, transition: Transition<in Bitmap>?) {
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,