Merge branch 'eddyizm:development' into development

This commit is contained in:
skajmer 2025-11-02 18:15:01 +01:00 committed by GitHub
commit 8ad35ce83a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
25 changed files with 269 additions and 240 deletions

View file

@ -78,6 +78,9 @@ On the main player control screen, tapping on the artwork will reveal a small co
1. Downloads the track (there is a notification if the android screen but not a pop toast currently ) 1. Downloads the track (there is a notification if the android screen but not a pop toast currently )
2. Adds track to playlist - pops up playlist dialog. 2. Adds track to playlist - pops up playlist dialog.
3. Adds tracks to the queue via instant mix function 3. Adds tracks to the queue via instant mix function
* TBD: what is the _instant mix function_?
* Uses [getSimilarSongs](https://opensubsonic.netlify.app/docs/endpoints/getsimilarsongs/) of OpenSubsonic API.
Which tracks to be mixed depends on the server implementation. For example, Navidrome gets 15 similar artists from LastFM, then 20 top songs from each.
4. Saves play queue (if the feature is enabled in the settings) 4. Saves play queue (if the feature is enabled in the settings)
* if the setting is not enabled, it toggles a view of the lyrics if available (slides to the right) * if the setting is not enabled, it toggles a view of the lyrics if available (slides to the right)

View file

@ -110,12 +110,12 @@ dependencies {
implementation 'com.github.bumptech.glide:annotations:4.16.0' implementation 'com.github.bumptech.glide:annotations:4.16.0'
// Media3 // Media3
implementation 'androidx.media3:media3-session:1.5.1' implementation 'androidx.media3:media3-session:1.8.0'
implementation 'androidx.media3:media3-common:1.5.1' implementation 'androidx.media3:media3-common:1.8.0'
implementation 'androidx.media3:media3-exoplayer:1.5.1' implementation 'androidx.media3:media3-exoplayer:1.8.0'
implementation 'androidx.media3:media3-ui:1.5.1' implementation 'androidx.media3:media3-ui:1.8.0'
implementation 'androidx.media3:media3-exoplayer-hls:1.5.1' implementation 'androidx.media3:media3-exoplayer-hls:1.8.0'
tempusImplementation 'androidx.media3:media3-cast:1.5.1' tempusImplementation 'androidx.media3:media3-cast:1.8.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0' annotationProcessor 'com.github.bumptech.glide:compiler:4.16.0'

View file

@ -5,18 +5,20 @@ import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder import android.app.TaskStackBuilder
import android.content.Intent import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Binder import android.os.Binder
import android.os.Bundle import android.os.Bundle
import android.os.IBinder import android.os.IBinder
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import androidx.media3.common.* import androidx.media3.common.*
import androidx.media3.common.util.UnstableApi import androidx.media3.common.util.UnstableApi
import androidx.media3.exoplayer.DefaultLoadControl import androidx.media3.exoplayer.DefaultLoadControl
import androidx.media3.exoplayer.ExoPlayer import androidx.media3.exoplayer.ExoPlayer
import androidx.media3.exoplayer.source.MediaSource import androidx.media3.exoplayer.source.MediaSource
import androidx.media3.exoplayer.source.TrackGroupArray
import androidx.media3.exoplayer.trackselection.TrackSelectionArray
import androidx.media3.session.* import androidx.media3.session.*
import androidx.media3.session.MediaSession.ControllerInfo import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.R
@ -43,6 +45,7 @@ class MediaService : MediaLibraryService() {
private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var shuffleCommands: List<CommandButton> private lateinit var shuffleCommands: List<CommandButton>
private lateinit var repeatCommands: List<CommandButton> private lateinit var repeatCommands: List<CommandButton>
private lateinit var networkCallback: CustomNetworkCallback
lateinit var equalizerManager: EqualizerManager lateinit var equalizerManager: EqualizerManager
private var customLayout = ImmutableList.of<CommandButton>() private var customLayout = ImmutableList.of<CommandButton>()
@ -81,6 +84,38 @@ class MediaService : MediaLibraryService() {
const val ACTION_BIND_EQUALIZER = "com.cappielloantonio.tempo.service.BIND_EQUALIZER" 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
init {
val manager = getSystemService(ConnectivityManager::class.java)
val network = manager.activeNetwork
val capabilities = manager.getNetworkCapabilities(network)
if (capabilities != null)
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
}
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
if (isWifi != wasWifi) {
wasWifi = isWifi
widgetUpdateHandler.post(Runnable {
updateMediaItems()
})
}
}
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -90,6 +125,7 @@ class MediaService : MediaLibraryService() {
restorePlayerFromQueue() restorePlayerFromQueue()
initializePlayerListener() initializePlayerListener()
initializeEqualizerManager() initializeEqualizerManager()
initializeNetworkListener()
setPlayer(player) setPlayer(player)
} }
@ -99,6 +135,7 @@ class MediaService : MediaLibraryService() {
} }
override fun onDestroy() { override fun onDestroy() {
releaseNetworkCallback()
equalizerManager.release() equalizerManager.release()
stopWidgetUpdates() stopWidgetUpdates()
releasePlayer() releasePlayer()
@ -275,6 +312,12 @@ class MediaService : MediaLibraryService() {
} }
} }
private fun initializeNetworkListener() {
networkCallback = CustomNetworkCallback()
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback)
updateMediaItems()
}
private fun restorePlayerFromQueue() { private fun restorePlayerFromQueue() {
if (player.mediaItemCount > 0) return if (player.mediaItemCount > 0) return
@ -398,6 +441,10 @@ class MediaService : MediaLibraryService() {
mediaLibrarySession.release() mediaLibrarySession.release()
} }
private fun releaseNetworkCallback() {
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
}
@SuppressLint("PrivateResource") @SuppressLint("PrivateResource")
private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton { private fun getShuffleCommandButton(sessionCommand: SessionCommand): CommandButton {
val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON val isOn = sessionCommand.customAction == CUSTOM_COMMAND_TOGGLE_SHUFFLE_MODE_ON

View file

@ -105,16 +105,6 @@ public class AlbumCatalogueAdapter extends RecyclerView.Adapter<AlbumCatalogueAd
filtering.filter(currentFilter); filtering.filter(currentFilter);
} }
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override @Override
public Filter getFilter() { public Filter getFilter() {
return filtering; return filtering;

View file

@ -66,16 +66,6 @@ public class ArtistAdapter extends RecyclerView.Adapter<ArtistAdapter.ViewHolder
notifyDataSetChanged(); notifyDataSetChanged();
} }
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
public class ViewHolder extends RecyclerView.ViewHolder { public class ViewHolder extends RecyclerView.ViewHolder {
ItemLibraryArtistBinding item; ItemLibraryArtistBinding item;

View file

@ -97,16 +97,6 @@ public class ArtistCatalogueAdapter extends RecyclerView.Adapter<ArtistCatalogue
notifyDataSetChanged(); notifyDataSetChanged();
} }
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override @Override
public Filter getFilter() { public Filter getFilter() {
return filtering; return filtering;
@ -151,6 +141,9 @@ public class ArtistCatalogueAdapter extends RecyclerView.Adapter<ArtistCatalogue
case Constants.ARTIST_ORDER_BY_RANDOM: case Constants.ARTIST_ORDER_BY_RANDOM:
Collections.shuffle(artists); Collections.shuffle(artists);
break; break;
case Constants.ARTIST_ORDER_BY_ALBUM_COUNT:
artists.sort(Comparator.comparing(ArtistID3::getAlbumCount).reversed());
break;
} }
notifyDataSetChanged(); notifyDataSetChanged();

View file

@ -113,16 +113,6 @@ public class ArtistHorizontalAdapter extends RecyclerView.Adapter<ArtistHorizont
return artists.get(id); return artists.get(id);
} }
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
public class ViewHolder extends RecyclerView.ViewHolder { public class ViewHolder extends RecyclerView.ViewHolder {
ItemHorizontalArtistBinding item; ItemHorizontalArtistBinding item;

View file

@ -60,16 +60,6 @@ public class ArtistSimilarAdapter extends RecyclerView.Adapter<ArtistSimilarAdap
notifyDataSetChanged(); notifyDataSetChanged();
} }
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
public class ViewHolder extends RecyclerView.ViewHolder { public class ViewHolder extends RecyclerView.ViewHolder {
ItemLibrarySimilarArtistBinding item; ItemLibrarySimilarArtistBinding item;

View file

@ -96,16 +96,6 @@ public class DownloadHorizontalAdapter extends RecyclerView.Adapter<DownloadHori
return shuffling; return shuffling;
} }
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
private List<Child> groupSong(List<Child> songs) { private List<Child> groupSong(List<Child> songs) {
switch (view) { switch (view) {
case Constants.DOWNLOAD_TYPE_TRACK: case Constants.DOWNLOAD_TYPE_TRACK:

View file

@ -95,16 +95,6 @@ public class PodcastChannelCatalogueAdapter extends RecyclerView.Adapter<Podcast
notifyDataSetChanged(); notifyDataSetChanged();
} }
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
@Override @Override
public Filter getFilter() { public Filter getFilter() {
return filtering; return filtering;

View file

@ -71,16 +71,6 @@ public class PodcastEpisodeAdapter extends RecyclerView.Adapter<PodcastEpisodeAd
notifyDataSetChanged(); notifyDataSetChanged();
} }
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
public class ViewHolder extends RecyclerView.ViewHolder { public class ViewHolder extends RecyclerView.ViewHolder {
ItemHomePodcastEpisodeBinding item; ItemHomePodcastEpisodeBinding item;

View file

@ -252,16 +252,6 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
notifyDataSetChanged(); notifyDataSetChanged();
} }
@Override
public int getItemViewType(int position) {
return position;
}
@Override
public long getItemId(int position) {
return position;
}
public void setPlaybackState(String mediaId, boolean playing) { public void setPlaybackState(String mediaId, boolean playing) {
String oldId = this.currentPlayingId; String oldId = this.currentPlayingId;
boolean oldPlaying = this.isPlaying; boolean oldPlaying = this.isPlaying;

View file

@ -34,6 +34,7 @@ import com.cappielloantonio.tempo.interfaces.ClickCallback;
import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter; import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel; import com.cappielloantonio.tempo.viewmodel.ArtistCatalogueViewModel;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3; import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
@ -114,7 +115,10 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
artistAdapter = new ArtistCatalogueAdapter(this); artistAdapter = new ArtistCatalogueAdapter(this);
artistAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY); artistAdapter.setStateRestorationPolicy(RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY);
bind.artistCatalogueRecyclerView.setAdapter(artistAdapter); bind.artistCatalogueRecyclerView.setAdapter(artistAdapter);
artistCatalogueViewModel.getArtistList().observe(getViewLifecycleOwner(), artistList -> artistAdapter.setItems(artistList)); artistCatalogueViewModel.getArtistList().observe(getViewLifecycleOwner(), artistList -> {
artistAdapter.setItems(artistList);
artistAdapter.sort(Preferences.getArtistSortOrder());
});
bind.artistCatalogueRecyclerView.setOnTouchListener((v, event) -> { bind.artistCatalogueRecyclerView.setOnTouchListener((v, event) -> {
hideKeyboard(v); hideKeyboard(v);
@ -192,6 +196,9 @@ public class ArtistCatalogueFragment extends Fragment implements ClickCallback {
} else if (menuItem.getItemId() == R.id.menu_artist_sort_random) { } else if (menuItem.getItemId() == R.id.menu_artist_sort_random) {
artistAdapter.sort(Constants.ARTIST_ORDER_BY_RANDOM); artistAdapter.sort(Constants.ARTIST_ORDER_BY_RANDOM);
return true; return true;
} else if (menuItem.getItemId() == R.id.menu_artist_sort_album_count) {
artistAdapter.sort(Constants.ARTIST_ORDER_BY_ALBUM_COUNT);
return true;
} }
return false; return false;

View file

@ -117,14 +117,12 @@ public class DownloadFragment extends Fragment implements ClickCallback {
if (songs.isEmpty()) { if (songs.isEmpty()) {
if (bind != null) { if (bind != null) {
bind.emptyDownloadLayout.setVisibility(View.VISIBLE); bind.emptyDownloadLayout.setVisibility(View.VISIBLE);
bind.fragmentDownloadNestedScrollView.setVisibility(View.GONE);
bind.downloadDownloadedSector.setVisibility(View.GONE); bind.downloadDownloadedSector.setVisibility(View.GONE);
bind.downloadedGroupByImageView.setVisibility(View.GONE); bind.downloadedGroupByImageView.setVisibility(View.GONE);
} }
} else { } else {
if (bind != null) { if (bind != null) {
bind.emptyDownloadLayout.setVisibility(View.GONE); bind.emptyDownloadLayout.setVisibility(View.GONE);
bind.fragmentDownloadNestedScrollView.setVisibility(View.VISIBLE);
bind.downloadDownloadedSector.setVisibility(View.VISIBLE); bind.downloadDownloadedSector.setVisibility(View.VISIBLE);
bind.downloadedGroupByImageView.setVisibility(View.VISIBLE); bind.downloadedGroupByImageView.setVisibility(View.VISIBLE);

View file

@ -40,6 +40,7 @@ object Constants {
const val ARTIST_STARRED = "ARTIST_STARRED" const val ARTIST_STARRED = "ARTIST_STARRED"
const val ARTIST_ORDER_BY_NAME = "ARTIST_ORDER_BY_NAME" const val ARTIST_ORDER_BY_NAME = "ARTIST_ORDER_BY_NAME"
const val ARTIST_ORDER_BY_RANDOM = "ARTIST_ORDER_BY_RANDOM" const val ARTIST_ORDER_BY_RANDOM = "ARTIST_ORDER_BY_RANDOM"
const val ARTIST_ORDER_BY_ALBUM_COUNT = "ARTIST_ORDER_BY_ALBUM_COUNT"
const val ARTIST_ORDER_BY_MOST_RECENTLY_STARRED = "ARTIST_ORDER_BY_MOST_RECENTLY_STARRED" const val ARTIST_ORDER_BY_MOST_RECENTLY_STARRED = "ARTIST_ORDER_BY_MOST_RECENTLY_STARRED"
const val ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED = "ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED" const val ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED = "ARTIST_ORDER_BY_LEAST_RECENTLY_STARRED"

View file

@ -115,6 +115,22 @@ public class MappingUtil {
.build(); .build();
} }
public static MediaItem mapMediaItem(MediaItem old) {
Uri uri = old.requestMetadata.mediaUri == null ? null : MusicUtil.updateStreamUri(old.requestMetadata.mediaUri);
return new MediaItem.Builder()
.setMediaId(old.mediaId)
.setMediaMetadata(old.mediaMetadata)
.setRequestMetadata(
new MediaItem.RequestMetadata.Builder()
.setMediaUri(uri)
.setExtras(old.requestMetadata.extras)
.build()
)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setUri(uri)
.build();
}
public static List<MediaItem> mapDownloads(List<Child> items) { public static List<MediaItem> mapDownloads(List<Child> items) {
ArrayList<MediaItem> downloads = new ArrayList<>(); ArrayList<MediaItem> downloads = new ArrayList<>();

View file

@ -21,11 +21,16 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import java.util.stream.Collectors; import java.util.stream.Collectors;
public class MusicUtil { public class MusicUtil {
private static final String TAG = "MusicUtil"; private static final String TAG = "MusicUtil";
private static final Pattern BITRATE_PATTERN = Pattern.compile("&maxBitRate=\\d+");
private static final Pattern FORMAT_PATTERN = Pattern.compile("&format=\\w+");
public static Uri getStreamUri(String id) { public static Uri getStreamUri(String id) {
Map<String, String> params = App.getSubsonicClientInstance(false).getParams(); Map<String, String> params = App.getSubsonicClientInstance(false).getParams();
@ -61,6 +66,24 @@ public class MusicUtil {
return Uri.parse(uri.toString()); return Uri.parse(uri.toString());
} }
public static Uri updateStreamUri(Uri uri) {
String s = uri.toString();
Matcher m1 = BITRATE_PATTERN.matcher(s);
s = m1.replaceAll("");
Matcher m2 = FORMAT_PATTERN.matcher(s);
s = m2.replaceAll("");
s = s.replace("&estimateContentLength=true", "");
if (!Preferences.isServerPrioritized())
s += "&maxBitRate=" + getBitratePreference();
if (!Preferences.isServerPrioritized())
s += "&format=" + getTranscodingFormatPreference();
if (Preferences.askForEstimateContentLength())
s += "&estimateContentLength=true";
return Uri.parse(s);
}
public static Uri getDownloadUri(String id) { public static Uri getDownloadUri(String id) {
StringBuilder uri = new StringBuilder(); StringBuilder uri = new StringBuilder();

View file

@ -79,6 +79,7 @@ object Preferences {
private const val ALBUM_DETAIL = "album_detail" private const val ALBUM_DETAIL = "album_detail"
private const val ALBUM_SORT_ORDER = "album_sort_order" private const val ALBUM_SORT_ORDER = "album_sort_order"
private const val DEFAULT_ALBUM_SORT_ORDER = Constants.ALBUM_ORDER_BY_NAME private const val DEFAULT_ALBUM_SORT_ORDER = Constants.ALBUM_ORDER_BY_NAME
private const val ARTIST_SORT_BY_ALBUM_COUNT= "artist_sort_by_album_count"
@JvmStatic @JvmStatic
fun getServer(): String? { fun getServer(): String? {
@ -656,4 +657,14 @@ object Preferences {
fun setAlbumSortOrder(sortOrder: String) { fun setAlbumSortOrder(sortOrder: String) {
App.getInstance().preferences.edit().putString(ALBUM_SORT_ORDER, sortOrder).apply() App.getInstance().preferences.edit().putString(ALBUM_SORT_ORDER, sortOrder).apply()
} }
@JvmStatic
fun getArtistSortOrder(): String {
val sort_by_album_count = App.getInstance().preferences.getBoolean(ARTIST_SORT_BY_ALBUM_COUNT, false)
Log.d("Preferences", "getSortOrder")
if (sort_by_album_count)
return Constants.ARTIST_ORDER_BY_ALBUM_COUNT
else
return Constants.ARTIST_ORDER_BY_NAME
}
} }

View file

@ -14,22 +14,21 @@
app:layout_collapseMode="pin" app:layout_collapseMode="pin"
app:navigationIcon="@drawable/ic_arrow_back" /> app:navigationIcon="@drawable/ic_arrow_back" />
<androidx.core.widget.NestedScrollView <androidx.coordinatorlayout.widget.CoordinatorLayout
android:id="@+id/fragment_album_page_nested_scroll_view" android:layout_width="match_parent"
android:layout_height="match_parent">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/app_bar_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content"> android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/album_info_sector" android:id="@+id/album_info_sector"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipChildren="false" android:clipChildren="false"
android:paddingTop="8dp"> android:paddingTop="8dp"
app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView <ImageView
android:id="@+id/album_cover_image_view" android:id="@+id/album_cover_image_view"
@ -252,53 +251,15 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/album_page_button_layout" /> app:layout_constraintTop_toBottomOf="@+id/album_page_button_layout" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</com.google.android.material.appbar.AppBarLayout>
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/global_padding_bottom"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/song_recycler_view" android:id="@+id/song_recycler_view"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false" android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingTop="8dp" />
<LinearLayout
android:id="@+id/similar_album_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:visibility="gone">
<TextView
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="32dp"
android:paddingEnd="20dp"
android:text="@string/album_page_extra_info_button" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/similar_albums_recycler_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipToPadding="false"
android:nestedScrollingEnabled="false"
android:paddingStart="16dp"
android:paddingTop="8dp" android:paddingTop="8dp"
android:paddingEnd="8dp" android:paddingBottom="@dimen/global_padding_bottom"
android:paddingBottom="8dp" /> app:layout_behavior="@string/appbar_scrolling_view_behavior"/>
</androidx.coordinatorlayout.widget.CoordinatorLayout>
</LinearLayout>
</LinearLayout>
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout> </LinearLayout>

View file

@ -1,9 +1,10 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent"
android:orientation="vertical">
<fragment <fragment
android:id="@+id/toolbar_fragment" android:id="@+id/toolbar_fragment"
@ -26,6 +27,7 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:orientation="vertical" android:orientation="vertical"
android:paddingBottom="@dimen/global_padding_bottom"
android:visibility="gone"> android:visibility="gone">
<ImageView <ImageView
@ -57,20 +59,11 @@
android:text="@string/download_info_empty_subtitle" /> android:text="@string/download_info_empty_subtitle" />
</LinearLayout> </LinearLayout>
<androidx.core.widget.NestedScrollView
android:id="@+id/fragment_download_nested_scroll_view"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:visibility="gone"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/download_downloaded_sector" android:id="@+id/download_downloaded_sector"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:paddingHorizontal="16dp" android:paddingHorizontal="16dp"
android:paddingTop="16dp"
android:paddingBottom="@dimen/global_padding_bottom"
android:visibility="gone" android:visibility="gone"
tools:visibility="visible"> tools:visibility="visible">
@ -111,8 +104,8 @@
android:id="@+id/downloaded_go_back_image_view" android:id="@+id/downloaded_go_back_image_view"
android:layout_width="24dp" android:layout_width="24dp"
android:layout_height="24dp" android:layout_height="24dp"
android:layout_marginHorizontal="12dp"
android:layout_gravity="center" android:layout_gravity="center"
android:layout_marginHorizontal="12dp"
android:background="@drawable/ic_arrow_back" android:background="@drawable/ic_arrow_back"
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable" app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view" app:layout_constraintEnd_toStartOf="@id/downloaded_group_by_image_view"
@ -128,6 +121,7 @@
app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable" app:layout_constraintBottom_toBottomOf="@+id/downloaded_text_view_refreshable"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" /> app:layout_constraintTop_toTopOf="@+id/downloaded_text_view_refreshable" />
</androidx.constraintlayout.widget.ConstraintLayout>
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/downloaded_recycler_view" android:id="@+id/downloaded_recycler_view"
@ -135,14 +129,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:clipToPadding="false" android:clipToPadding="false"
android:nestedScrollingEnabled="false" android:nestedScrollingEnabled="false"
android:paddingHorizontal="12dp"
android:paddingTop="12dp" android:paddingTop="12dp"
android:paddingBottom="8dp" android:paddingBottom="@dimen/global_padding_bottom" />
app:layout_constraintBottom_toBottomOf="parent" </LinearLayout>
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/shuffle_downloaded_text_view_clickable" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

View file

@ -27,7 +27,7 @@
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:background="?attr/colorSurface" android:background="?attr/colorSurface"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap"> app:layout_scrollFlags="scroll|exitUntilCollapsed">
<ImageView <ImageView
android:id="@+id/playlist_cover_image_view_top_left" android:id="@+id/playlist_cover_image_view_top_left"

View file

@ -6,4 +6,7 @@
<item <item
android:id="@+id/menu_artist_sort_random" android:id="@+id/menu_artist_sort_random"
android:title="@string/menu_sort_random" /> android:title="@string/menu_sort_random" />
<item
android:id="@+id/menu_artist_sort_album_count"
android:title="@string/menu_sort_album_count" />
</menu> </menu>

View file

@ -200,6 +200,7 @@
<string name="menu_sort_artist">Artist</string> <string name="menu_sort_artist">Artist</string>
<string name="menu_sort_name">Name</string> <string name="menu_sort_name">Name</string>
<string name="menu_sort_random">Random</string> <string name="menu_sort_random">Random</string>
<string name="menu_sort_album_count">Album Count</string>
<string name="menu_sort_recently_added">Recently added</string> <string name="menu_sort_recently_added">Recently added</string>
<string name="menu_sort_recently_played">Recently played</string> <string name="menu_sort_recently_played">Recently played</string>
<string name="menu_sort_most_played">Most played</string> <string name="menu_sort_most_played">Most played</string>
@ -527,4 +528,6 @@
<string name="settings_album_detail">Show album detail</string> <string name="settings_album_detail">Show album detail</string>
<string name="settings_album_detail_summary">If enabled, show the album details like genre, song count etc. on the album page</string> <string name="settings_album_detail_summary">If enabled, show the album details like genre, song count etc. on the album page</string>
<string name="settings_artist_sort_by_album_count">Sort artists by album count</string>
<string name="settings_artist_sort_by_album_count_summary">If enabled, sort the artists by album count. Sort by name if disabled.</string>
</resources> </resources>

View file

@ -116,6 +116,12 @@
android:summary="@string/settings_album_detail_summary" android:summary="@string/settings_album_detail_summary"
android:key="album_detail" /> android:key="album_detail" />
<SwitchPreference
android:title="@string/settings_artist_sort_by_album_count"
android:defaultValue="false"
android:summary="@string/settings_artist_sort_by_album_count_summary"
android:key="artist_sort_by_album_count" />
</PreferenceCategory> </PreferenceCategory>
<PreferenceCategory app:title="@string/settings_title_playlist"> <PreferenceCategory app:title="@string/settings_title_playlist">

View file

@ -4,10 +4,14 @@ import android.app.PendingIntent.FLAG_IMMUTABLE
import android.app.PendingIntent.FLAG_UPDATE_CURRENT import android.app.PendingIntent.FLAG_UPDATE_CURRENT
import android.app.TaskStackBuilder import android.app.TaskStackBuilder
import android.content.Intent import android.content.Intent
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Binder import android.os.Binder
import android.os.IBinder import android.os.IBinder
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.util.Log
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.media3.cast.CastPlayer import androidx.media3.cast.CastPlayer
import androidx.media3.cast.SessionAvailabilityListener import androidx.media3.cast.SessionAvailabilityListener
@ -43,6 +47,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
private lateinit var castPlayer: CastPlayer private lateinit var castPlayer: CastPlayer
private lateinit var mediaLibrarySession: MediaLibrarySession private lateinit var mediaLibrarySession: MediaLibrarySession
private lateinit var librarySessionCallback: MediaLibrarySessionCallback private lateinit var librarySessionCallback: MediaLibrarySessionCallback
private lateinit var networkCallback: CustomNetworkCallback
lateinit var equalizerManager: EqualizerManager lateinit var equalizerManager: EqualizerManager
inner class LocalBinder : Binder() { inner class LocalBinder : Binder() {
@ -69,6 +74,38 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
} }
} }
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
init {
val manager = getSystemService(ConnectivityManager::class.java)
val network = manager.activeNetwork
val capabilities = manager.getNetworkCapabilities(network)
if (capabilities != null)
wasWifi = capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
}
override fun onCapabilitiesChanged(network : Network, networkCapabilities : NetworkCapabilities) {
val isWifi = networkCapabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)
if (isWifi != wasWifi) {
wasWifi = isWifi
widgetUpdateHandler.post(Runnable {
updateMediaItems()
})
}
}
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -79,6 +116,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
initializePlayerListener() initializePlayerListener()
initializeCastPlayer() initializeCastPlayer()
initializeEqualizerManager() initializeEqualizerManager()
initializeNetworkListener()
setPlayer( setPlayer(
null, null,
@ -99,6 +137,7 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
} }
override fun onDestroy() { override fun onDestroy() {
releaseNetworkCallback()
equalizerManager.release() equalizerManager.release()
stopWidgetUpdates() stopWidgetUpdates()
releasePlayer() releasePlayer()
@ -178,6 +217,12 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
.build() .build()
} }
private fun initializeNetworkListener() {
networkCallback = CustomNetworkCallback()
getSystemService(ConnectivityManager::class.java).registerDefaultNetworkCallback(networkCallback)
updateMediaItems()
}
private fun restorePlayerFromQueue() { private fun restorePlayerFromQueue() {
if (player.mediaItemCount > 0) return if (player.mediaItemCount > 0) return
@ -374,6 +419,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
automotiveRepository.deleteMetadata() automotiveRepository.deleteMetadata()
} }
private fun releaseNetworkCallback() {
getSystemService(ConnectivityManager::class.java).unregisterNetworkCallback(networkCallback)
}
private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false) private fun getRenderersFactory() = DownloadUtil.buildRenderersFactory(this, false)
override fun onCastSessionAvailable() { override fun onCastSessionAvailable() {