feat: Implemented continuous playing

This commit is contained in:
CappielloAntonio 2024-06-02 19:18:16 +02:00
parent 2c3aebc83b
commit 176db09662
9 changed files with 101 additions and 44 deletions

View file

@ -1,13 +1,9 @@
package com.cappielloantonio.tempo.repository; package com.cappielloantonio.tempo.repository;
import android.util.Log;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData; import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.database.AppDatabase;
import com.cappielloantonio.tempo.database.dao.FavoriteDao;
import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.base.ApiResponse;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
@ -54,12 +50,12 @@ public class SongRepository {
return starredSongs; return starredSongs;
} }
public MutableLiveData<List<Child>> getInstantMix(Child song, int count) { public MutableLiveData<List<Child>> getInstantMix(String id, int count) {
MutableLiveData<List<Child>> instantMix = new MutableLiveData<>(); MutableLiveData<List<Child>> instantMix = new MutableLiveData<>();
App.getSubsonicClientInstance(false) App.getSubsonicClientInstance(false)
.getBrowsingClient() .getBrowsingClient()
.getSimilarSongs2(song.getId(), count) .getSimilarSongs2(id, count)
.enqueue(new Callback<ApiResponse>() { .enqueue(new Callback<ApiResponse>() {
@Override @Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) { public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {

View file

@ -1,8 +1,16 @@
package com.cappielloantonio.tempo.service; package com.cappielloantonio.tempo.service;
import androidx.media3.common.MediaItem; import android.content.ComponentName;
import androidx.media3.session.MediaBrowser;
import androidx.annotation.OptIn;
import androidx.lifecycle.LiveData;
import androidx.lifecycle.Observer;
import androidx.media3.common.MediaItem;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import com.cappielloantonio.tempo.App;
import com.cappielloantonio.tempo.interfaces.MediaIndexCallback; import com.cappielloantonio.tempo.interfaces.MediaIndexCallback;
import com.cappielloantonio.tempo.model.Chronology; import com.cappielloantonio.tempo.model.Chronology;
import com.cappielloantonio.tempo.repository.ChronologyRepository; import com.cappielloantonio.tempo.repository.ChronologyRepository;
@ -299,6 +307,30 @@ public class MediaManager {
} }
} }
@OptIn(markerClass = UnstableApi.class)
public static void continuousPlay(MediaItem mediaItem) {
if (mediaItem != null && Preferences.isContinuousPlayEnabled() && Preferences.isInstantMixUsable()) {
Preferences.setLastInstantMix();
LiveData<List<Child>> instantMix = getSongRepository().getInstantMix(mediaItem.mediaId, 10);
instantMix.observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> media) {
if (media != null) {
ListenableFuture<MediaBrowser> mediaBrowserListenableFuture = new MediaBrowser.Builder(
App.getContext(),
new SessionToken(App.getContext(), new ComponentName(App.getContext(), MediaService.class))
).buildAsync();
enqueue(mediaBrowserListenableFuture, media, true);
}
instantMix.removeObserver(this);
}
});
}
}
public static void saveChronology(MediaItem mediaItem) { public static void saveChronology(MediaItem mediaItem) {
if (mediaItem != null) { if (mediaItem != null) {
getChronologyRepository().insert(new Chronology(mediaItem)); getChronologyRepository().insert(new Chronology(mediaItem));

View file

@ -62,6 +62,8 @@ object Preferences {
private const val HOME_SECTOR_LIST = "home_sector_list" private const val HOME_SECTOR_LIST = "home_sector_list"
private const val RATING_PER_ITEM = "rating_per_item" private const val RATING_PER_ITEM = "rating_per_item"
private const val NEXT_UPDATE_CHECK = "next_update_check" private const val NEXT_UPDATE_CHECK = "next_update_check"
private const val CONTINUOUS_PLAY = "continuous_play"
private const val LAST_INSTANT_MIX = "last_instant_mix"
@JvmStatic @JvmStatic
@ -476,4 +478,21 @@ object Preferences {
fun setTempoUpdateReminder() { fun setTempoUpdateReminder() {
App.getInstance().preferences.edit().putLong(NEXT_UPDATE_CHECK, System.currentTimeMillis()).apply() App.getInstance().preferences.edit().putLong(NEXT_UPDATE_CHECK, System.currentTimeMillis()).apply()
} }
@JvmStatic
fun isContinuousPlayEnabled(): Boolean {
return App.getInstance().preferences.getBoolean(CONTINUOUS_PLAY, true)
}
@JvmStatic
fun setLastInstantMix() {
App.getInstance().preferences.edit().putLong(LAST_INSTANT_MIX, System.currentTimeMillis()).apply()
}
@JvmStatic
fun isInstantMixUsable(): Boolean {
return App.getInstance().preferences.getLong(
LAST_INSTANT_MIX, 0
) + 5000 < System.currentTimeMillis()
}
} }

View file

@ -203,7 +203,7 @@ public class HomeViewModel extends AndroidViewModel {
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) { public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
mediaInstantMix.setValue(Collections.emptyList()); mediaInstantMix.setValue(Collections.emptyList());
songRepository.getInstantMix(media, 20).observe(owner, mediaInstantMix::postValue); songRepository.getInstantMix(media.getId(), 20).observe(owner, mediaInstantMix::postValue);
return mediaInstantMix; return mediaInstantMix;
} }

View file

@ -190,7 +190,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) { public LiveData<List<Child>> getMediaInstantMix(LifecycleOwner owner, Child media) {
instantMix.setValue(Collections.emptyList()); instantMix.setValue(Collections.emptyList());
songRepository.getInstantMix(media, 20).observe(owner, instantMix::postValue); songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue);
return instantMix; return instantMix;
} }

View file

@ -128,7 +128,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
public LiveData<List<Child>> getInstantMix(LifecycleOwner owner, Child media) { public LiveData<List<Child>> getInstantMix(LifecycleOwner owner, Child media) {
instantMix.setValue(Collections.emptyList()); instantMix.setValue(Collections.emptyList());
songRepository.getInstantMix(media, 20).observe(owner, instantMix::postValue); songRepository.getInstantMix(media.getId(), 20).observe(owner, instantMix::postValue);
return instantMix; return instantMix;
} }

View file

@ -262,6 +262,8 @@
<string name="settings_audio_transcode_priority_toast">Priority on transcoding of track given to server</string> <string name="settings_audio_transcode_priority_toast">Priority on transcoding of track given to server</string>
<string name="settings_buffering_strategy">Buffering strategy</string> <string name="settings_buffering_strategy">Buffering strategy</string>
<string name="settings_buffering_strategy_summary">For the change to take effect you must manually restart the app.</string> <string name="settings_buffering_strategy_summary">For the change to take effect you must manually restart the app.</string>
<string name="settings_continuous_play_summary">Allows music to keep playing after a playlist has ended, playing similar songs</string>
<string name="settings_continuous_play_title">Continuous play</string>
<string name="settings_covers_cache">Size of artwork cache</string> <string name="settings_covers_cache">Size of artwork cache</string>
<string name="settings_data_saving_mode_summary">In order to reduce data consumption, avoid downloading covers.</string> <string name="settings_data_saving_mode_summary">In order to reduce data consumption, avoid downloading covers.</string>
<string name="settings_data_saving_mode_title">Limit mobile data usage</string> <string name="settings_data_saving_mode_title">Limit mobile data usage</string>

View file

@ -109,6 +109,12 @@
app:title="@string/settings_image_size" app:title="@string/settings_image_size"
app:useSimpleSummaryProvider="true" /> app:useSimpleSummaryProvider="true" />
<SwitchPreference
android:title="@string/settings_continuous_play_title"
android:defaultValue="true"
android:summary="@string/settings_continuous_play_summary"
android:key="continuous_play" />
<SwitchPreference <SwitchPreference
android:title="@string/settings_wifi_only_title" android:title="@string/settings_wifi_only_title"
android:defaultValue="false" android:defaultValue="false"

View file

@ -29,7 +29,6 @@ import com.google.android.gms.common.GoogleApiAvailability
@UnstableApi @UnstableApi
class MediaService : MediaLibraryService(), SessionAvailabilityListener { class MediaService : MediaLibraryService(), SessionAvailabilityListener {
private lateinit var automotiveRepository: AutomotiveRepository private lateinit var automotiveRepository: AutomotiveRepository
private lateinit var player: ExoPlayer private lateinit var player: ExoPlayer
private lateinit var castPlayer: CastPlayer private lateinit var castPlayer: CastPlayer
@ -45,8 +44,8 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
initializePlayerListener() initializePlayerListener()
setPlayer( setPlayer(
null, null,
if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable) castPlayer else player if (this::castPlayer.isInitialized && castPlayer.isCastSessionAvailable) castPlayer else player
) )
} }
@ -73,18 +72,18 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
private fun initializePlayer() { private fun initializePlayer() {
player = ExoPlayer.Builder(this) player = ExoPlayer.Builder(this)
.setRenderersFactory(getRenderersFactory()) .setRenderersFactory(getRenderersFactory())
.setMediaSourceFactory(getMediaSourceFactory()) .setMediaSourceFactory(getMediaSourceFactory())
.setAudioAttributes(AudioAttributes.DEFAULT, true) .setAudioAttributes(AudioAttributes.DEFAULT, true)
.setHandleAudioBecomingNoisy(true) .setHandleAudioBecomingNoisy(true)
.setWakeMode(C.WAKE_MODE_NETWORK) .setWakeMode(C.WAKE_MODE_NETWORK)
.setLoadControl(initializeLoadControl()) .setLoadControl(initializeLoadControl())
.build() .build()
} }
private fun initializeCastPlayer() { private fun initializeCastPlayer() {
if (GoogleApiAvailability.getInstance() if (GoogleApiAvailability.getInstance()
.isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS .isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
) { ) {
castPlayer = CastPlayer(CastContext.getSharedInstance(this)) castPlayer = CastPlayer(CastContext.getSharedInstance(this))
castPlayer.setSessionAvailabilityListener(this) castPlayer.setSessionAvailabilityListener(this)
@ -93,15 +92,15 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
private fun initializeMediaLibrarySession() { private fun initializeMediaLibrarySession() {
val sessionActivityPendingIntent = val sessionActivityPendingIntent =
TaskStackBuilder.create(this).run { TaskStackBuilder.create(this).run {
addNextIntent(Intent(this@MediaService, MainActivity::class.java)) addNextIntent(Intent(this@MediaService, MainActivity::class.java))
getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT) getPendingIntent(0, FLAG_IMMUTABLE or FLAG_UPDATE_CURRENT)
} }
mediaLibrarySession = mediaLibrarySession =
MediaLibrarySession.Builder(this, player, createLibrarySessionCallback()) MediaLibrarySession.Builder(this, player, createLibrarySessionCallback())
.setSessionActivity(sessionActivityPendingIntent) .setSessionActivity(sessionActivityPendingIntent)
.build() .build()
} }
private fun createLibrarySessionCallback(): MediaLibrarySession.Callback { private fun createLibrarySessionCallback(): MediaLibrarySession.Callback {
@ -121,13 +120,16 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
override fun onTracksChanged(tracks: Tracks) { override fun onTracksChanged(tracks: Tracks) {
ReplayGainUtil.setReplayGain(player, tracks) ReplayGainUtil.setReplayGain(player, tracks)
MediaManager.scrobble(player.currentMediaItem, false) MediaManager.scrobble(player.currentMediaItem, false)
if (player.currentMediaItemIndex + 1 == player.mediaItemCount)
MediaManager.continuousPlay(player.currentMediaItem)
} }
override fun onIsPlayingChanged(isPlaying: Boolean) { override fun onIsPlayingChanged(isPlaying: Boolean) {
if (!isPlaying) { if (!isPlaying) {
MediaManager.setPlayingPausedTimestamp( MediaManager.setPlayingPausedTimestamp(
player.currentMediaItem, player.currentMediaItem,
player.currentPosition player.currentPosition
) )
} else { } else {
MediaManager.scrobble(player.currentMediaItem, false) MediaManager.scrobble(player.currentMediaItem, false)
@ -138,8 +140,8 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
super.onPlaybackStateChanged(playbackState) super.onPlaybackStateChanged(playbackState)
if (!player.hasNextMediaItem() && if (!player.hasNextMediaItem() &&
playbackState == Player.STATE_ENDED && playbackState == Player.STATE_ENDED &&
player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC player.mediaMetadata.extras?.getString("type") == Constants.MEDIA_TYPE_MUSIC
) { ) {
MediaManager.scrobble(player.currentMediaItem, true) MediaManager.scrobble(player.currentMediaItem, true)
MediaManager.saveChronology(player.currentMediaItem) MediaManager.saveChronology(player.currentMediaItem)
@ -147,9 +149,9 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
} }
override fun onPositionDiscontinuity( override fun onPositionDiscontinuity(
oldPosition: Player.PositionInfo, oldPosition: Player.PositionInfo,
newPosition: Player.PositionInfo, newPosition: Player.PositionInfo,
reason: Int reason: Int
) { ) {
super.onPositionDiscontinuity(oldPosition, newPosition, reason) super.onPositionDiscontinuity(oldPosition, newPosition, reason)
@ -169,13 +171,13 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
private fun initializeLoadControl(): DefaultLoadControl { private fun initializeLoadControl(): DefaultLoadControl {
return DefaultLoadControl.Builder() return DefaultLoadControl.Builder()
.setBufferDurationsMs( .setBufferDurationsMs(
(DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(), (DefaultLoadControl.DEFAULT_MIN_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
(DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(), (DefaultLoadControl.DEFAULT_MAX_BUFFER_MS * Preferences.getBufferingStrategy()).toInt(),
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS, DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_MS,
DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS DefaultLoadControl.DEFAULT_BUFFER_FOR_PLAYBACK_AFTER_REBUFFER_MS
) )
.build() .build()
} }
private fun setPlayer(oldPlayer: Player?, newPlayer: Player) { private fun setPlayer(oldPlayer: Player?, newPlayer: Player) {
@ -196,7 +198,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
private fun getMediaSourceFactory() = private fun getMediaSourceFactory() =
DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this)) DefaultMediaSourceFactory(this).setDataSourceFactory(DownloadUtil.getDataSourceFactory(this))
override fun onCastSessionAvailable() { override fun onCastSessionAvailable() {
setPlayer(player, castPlayer) setPlayer(player, castPlayer)