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;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
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.models.Child;
@ -54,12 +50,12 @@ public class SongRepository {
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<>();
App.getSubsonicClientInstance(false)
.getBrowsingClient()
.getSimilarSongs2(song.getId(), count)
.getSimilarSongs2(id, count)
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {

View file

@ -1,8 +1,16 @@
package com.cappielloantonio.tempo.service;
import androidx.media3.common.MediaItem;
import androidx.media3.session.MediaBrowser;
import android.content.ComponentName;
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.model.Chronology;
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) {
if (mediaItem != null) {
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 RATING_PER_ITEM = "rating_per_item"
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
@ -476,4 +478,21 @@ object Preferences {
fun setTempoUpdateReminder() {
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) {
mediaInstantMix.setValue(Collections.emptyList());
songRepository.getInstantMix(media, 20).observe(owner, mediaInstantMix::postValue);
songRepository.getInstantMix(media.getId(), 20).observe(owner, mediaInstantMix::postValue);
return mediaInstantMix;
}

View file

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

View file

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

View file

@ -109,6 +109,12 @@
app:title="@string/settings_image_size"
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
android:title="@string/settings_wifi_only_title"
android:defaultValue="false"

View file

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