Merge branch 'development' into skip-duplicates

This commit is contained in:
eddyizm 2025-10-06 22:22:52 -07:00 committed by GitHub
commit 717f95a04a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
87 changed files with 6098 additions and 428 deletions

View file

@ -1,11 +1,14 @@
package com.cappielloantonio.tempo.ui.activity;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.net.ConnectivityManager;
import android.net.NetworkInfo;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.text.TextUtils;
import android.util.Log;
import android.view.View;
@ -13,7 +16,10 @@ import androidx.annotation.NonNull;
import androidx.core.splashscreen.SplashScreen;
import androidx.fragment.app.FragmentManager;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.MimeTypes;
import androidx.media3.common.util.UnstableApi;
import androidx.navigation.NavController;
import androidx.navigation.fragment.NavHostFragment;
@ -56,6 +62,7 @@ public class MainActivity extends BaseActivity {
private BottomSheetBehavior bottomSheetBehavior;
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
private Intent pendingDownloadPlaybackIntent;
@Override
protected void onCreate(Bundle savedInstanceState) {
@ -77,12 +84,16 @@ public class MainActivity extends BaseActivity {
checkConnectionType();
getOpenSubsonicExtensions();
checkTempoUpdate();
maybeSchedulePlaybackIntent(getIntent());
}
@Override
protected void onStart() {
super.onStart();
pingServer();
initService();
consumePendingPlaybackIntent();
}
@Override
@ -98,6 +109,14 @@ public class MainActivity extends BaseActivity {
bind = null;
}
@Override
protected void onNewIntent(Intent intent) {
super.onNewIntent(intent);
setIntent(intent);
maybeSchedulePlaybackIntent(intent);
consumePendingPlaybackIntent();
}
@Override
public void onBackPressed() {
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
@ -351,6 +370,7 @@ public class MainActivity extends BaseActivity {
Preferences.switchInUseServerAddress();
App.refreshSubsonicClient();
pingServer();
resetView();
} else {
Preferences.setOpenSubsonic(subsonicResponse.getOpenSubsonic() != null && subsonicResponse.getOpenSubsonic());
}
@ -361,6 +381,7 @@ public class MainActivity extends BaseActivity {
Preferences.switchInUseServerAddress();
App.refreshSubsonicClient();
pingServer();
resetView();
} else {
mainViewModel.ping().observe(this, subsonicResponse -> {
if (subsonicResponse == null) {
@ -376,6 +397,13 @@ public class MainActivity extends BaseActivity {
}
}
private void resetView() {
resetViewModel();
int id = Objects.requireNonNull(navController.getCurrentDestination()).getId();
navController.popBackStack(id, true);
navController.navigate(id);
}
private void getOpenSubsonicExtensions() {
if (Preferences.getToken() != null) {
mainViewModel.getOpenSubsonicExtensions().observe(this, openSubsonicExtensions -> {
@ -408,4 +436,68 @@ public class MainActivity extends BaseActivity {
}
}
}
private void maybeSchedulePlaybackIntent(Intent intent) {
if (intent == null) return;
if (Constants.ACTION_PLAY_EXTERNAL_DOWNLOAD.equals(intent.getAction())
|| intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) {
pendingDownloadPlaybackIntent = new Intent(intent);
}
}
private void consumePendingPlaybackIntent() {
if (pendingDownloadPlaybackIntent == null) return;
Intent intent = pendingDownloadPlaybackIntent;
pendingDownloadPlaybackIntent = null;
playDownloadedMedia(intent);
}
private void playDownloadedMedia(Intent intent) {
String uriString = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_URI);
if (TextUtils.isEmpty(uriString)) {
return;
}
Uri uri = Uri.parse(uriString);
String mediaId = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_MEDIA_ID);
if (TextUtils.isEmpty(mediaId)) {
mediaId = uri.toString();
}
String title = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_TITLE);
String artist = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ARTIST);
String album = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_ALBUM);
int duration = intent.getIntExtra(Constants.EXTRA_DOWNLOAD_DURATION, 0);
Bundle extras = new Bundle();
extras.putString("id", mediaId);
extras.putString("title", title);
extras.putString("artist", artist);
extras.putString("album", album);
extras.putString("uri", uri.toString());
extras.putString("type", Constants.MEDIA_TYPE_MUSIC);
extras.putInt("duration", duration);
MediaMetadata.Builder metadataBuilder = new MediaMetadata.Builder()
.setExtras(extras)
.setIsBrowsable(false)
.setIsPlayable(true);
if (!TextUtils.isEmpty(title)) metadataBuilder.setTitle(title);
if (!TextUtils.isEmpty(artist)) metadataBuilder.setArtist(artist);
if (!TextUtils.isEmpty(album)) metadataBuilder.setAlbumTitle(album);
MediaItem mediaItem = new MediaItem.Builder()
.setMediaId(mediaId)
.setMediaMetadata(metadataBuilder.build())
.setUri(uri)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setRequestMetadata(new MediaItem.RequestMetadata.Builder()
.setMediaUri(uri)
.setExtras(extras)
.build())
.build();
MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem);
}
}

View file

@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.adapter;
import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -23,17 +24,24 @@ import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueueAdapter.ViewHolder> {
private static final String TAG = "PlayerSongQueueAdapter";
private final ClickCallback click;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private List<Child> songs;
private String currentPlayingId;
private boolean isPlaying;
private List<Integer> currentPlayingPositions = Collections.emptyList();
public PlayerSongQueueAdapter(ClickCallback click) {
this.click = click;
this.songs = Collections.emptyList();
@ -104,6 +112,46 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
} else {
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
}
holder.itemView.setOnClickListener(v -> {
mediaBrowserListenableFuture.addListener(() -> {
try {
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
int pos = holder.getBindingAdapterPosition();
Child s = songs.get(pos);
if (currentPlayingId != null && currentPlayingId.equals(s.getId())) {
if (isPlaying) {
mediaBrowser.pause();
} else {
mediaBrowser.play();
}
} else {
mediaBrowser.seekTo(pos, 0);
mediaBrowser.play();
}
} catch (Exception e) {
Log.w(TAG, "Error obtaining MediaBrowser", e);
}
}, MoreExecutors.directExecutor());
});
bindPlaybackState(holder, song);
}
private void bindPlaybackState(@NonNull PlayerSongQueueAdapter.ViewHolder holder, @NonNull Child song) {
boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId());
if (isCurrent) {
holder.item.playPauseIcon.setVisibility(View.VISIBLE);
if (isPlaying) {
holder.item.playPauseIcon.setImageResource(R.drawable.ic_pause);
} else {
holder.item.playPauseIcon.setImageResource(R.drawable.ic_play);
}
holder.item.coverArtOverlay.setVisibility(View.VISIBLE);
} else {
holder.item.playPauseIcon.setVisibility(View.INVISIBLE);
holder.item.coverArtOverlay.setVisibility(View.INVISIBLE);
}
}
public List<Child> getItems() {
@ -132,6 +180,46 @@ public class PlayerSongQueueAdapter extends RecyclerView.Adapter<PlayerSongQueue
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
}
public void setPlaybackState(String mediaId, boolean playing) {
String oldId = this.currentPlayingId;
boolean oldPlaying = this.isPlaying;
List<Integer> oldPositions = currentPlayingPositions;
this.currentPlayingId = mediaId;
this.isPlaying = playing;
if (Objects.equals(oldId, mediaId) && oldPlaying == playing) {
List<Integer> newPositionsCheck = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
if (oldPositions.equals(newPositionsCheck)) {
return;
}
}
currentPlayingPositions = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
for (int pos : oldPositions) {
if (pos >= 0 && pos < songs.size()) {
notifyItemChanged(pos, "payload_playback");
}
}
for (int pos : currentPlayingPositions) {
if (!oldPositions.contains(pos) && pos >= 0 && pos < songs.size()) {
notifyItemChanged(pos, "payload_playback");
}
}
}
private List<Integer> findPositionsById(String id) {
if (id == null) return Collections.emptyList();
List<Integer> positions = new ArrayList<>();
for (int i = 0; i < songs.size(); i++) {
if (id.equals(songs.get(i).getId())) {
positions.add(i);
}
}
return positions;
}
public Child getItem(int id) {
return songs.get(id);
}

View file

@ -1,6 +1,8 @@
package com.cappielloantonio.tempo.ui.adapter;
import android.app.Activity;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -9,7 +11,9 @@ import android.widget.Filterable;
import androidx.annotation.NonNull;
import androidx.appcompat.content.res.AppCompatResources;
import androidx.lifecycle.LifecycleOwner;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.tempo.R;
@ -21,8 +25,11 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.DiscTitle;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Collections;
@ -30,6 +37,7 @@ import java.util.Comparator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.concurrent.ExecutionException;
@UnstableApi
public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAdapter.ViewHolder> implements Filterable {
@ -42,6 +50,11 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
private List<Child> songs;
private String currentFilter;
private String currentPlayingId;
private boolean isPlaying;
private List<Integer> currentPlayingPositions = Collections.emptyList();
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private final Filter filtering = new Filter() {
@Override
protected FilterResults performFiltering(CharSequence constraint) {
@ -70,10 +83,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
protected void publishResults(CharSequence constraint, FilterResults results) {
songs = (List<Child>) results.values;
notifyDataSetChanged();
for (int pos : currentPlayingPositions) {
if (pos >= 0 && pos < songs.size()) {
notifyItemChanged(pos, "payload_playback");
}
}
}
};
public SongHorizontalAdapter(ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) {
public SongHorizontalAdapter(LifecycleOwner lifecycleOwner, ClickCallback click, boolean showCoverArt, boolean showAlbum, AlbumID3 album) {
this.click = click;
this.showCoverArt = showCoverArt;
this.showAlbum = showAlbum;
@ -81,6 +100,11 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
this.songsFull = Collections.emptyList();
this.currentFilter = "";
this.album = album;
setHasStableIds(false);
if (lifecycleOwner != null) {
MappingUtil.observeExternalAudioRefresh(lifecycleOwner, this::handleExternalAudioRefresh);
}
}
@NonNull
@ -91,7 +115,16 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
if (!payloads.isEmpty() && payloads.contains("payload_playback")) {
bindPlaybackState(holder, songs.get(position));
} else {
super.onBindViewHolder(holder, position, payloads);
}
}
@Override
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
Child song = songs.get(position);
holder.item.searchResultSongTitleTextView.setText(song.getTitle());
@ -109,10 +142,18 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
holder.item.trackNumberTextView.setText(MusicUtil.getReadableTrackNumber(holder.itemView.getContext(), song.getTrack()));
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
if (Preferences.getDownloadDirectoryUri() == null) {
if (DownloadUtil.getDownloadTracker(holder.itemView.getContext()).isDownloaded(song.getId())) {
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
} else {
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
}
} else {
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
if (ExternalAudioReader.getUri(song) != null) {
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.VISIBLE);
} else {
holder.item.searchResultDownloadIndicatorImageView.setVisibility(View.GONE);
}
}
if (showCoverArt) CustomGlideRequest.Builder
@ -165,6 +206,39 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
} else {
holder.item.ratingIndicatorImageView.setVisibility(View.GONE);
}
bindPlaybackState(holder, song);
}
private void handleExternalAudioRefresh() {
if (Preferences.getDownloadDirectoryUri() != null) {
notifyDataSetChanged();
}
}
private void bindPlaybackState(@NonNull ViewHolder holder, @NonNull Child song) {
boolean isCurrent = currentPlayingId != null && currentPlayingId.equals(song.getId());
if (isCurrent) {
holder.item.playPauseIcon.setVisibility(View.VISIBLE);
if (isPlaying) {
holder.item.playPauseIcon.setImageResource(R.drawable.ic_pause);
} else {
holder.item.playPauseIcon.setImageResource(R.drawable.ic_play);
}
if (!showCoverArt) {
holder.item.trackNumberTextView.setVisibility(View.INVISIBLE);
} else {
holder.item.coverArtOverlay.setVisibility(View.VISIBLE);
}
} else {
holder.item.playPauseIcon.setVisibility(View.INVISIBLE);
if (!showCoverArt) {
holder.item.trackNumberTextView.setVisibility(View.VISIBLE);
} else {
holder.item.coverArtOverlay.setVisibility(View.INVISIBLE);
}
}
}
@Override
@ -188,6 +262,46 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
return position;
}
public void setPlaybackState(String mediaId, boolean playing) {
String oldId = this.currentPlayingId;
boolean oldPlaying = this.isPlaying;
List<Integer> oldPositions = currentPlayingPositions;
this.currentPlayingId = mediaId;
this.isPlaying = playing;
if (Objects.equals(oldId, mediaId) && oldPlaying == playing) {
List<Integer> newPositionsCheck = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
if (oldPositions.equals(newPositionsCheck)) {
return;
}
}
currentPlayingPositions = mediaId != null ? findPositionsById(mediaId) : Collections.emptyList();
for (int pos : oldPositions) {
if (pos >= 0 && pos < songs.size()) {
notifyItemChanged(pos, "payload_playback");
}
}
for (int pos : currentPlayingPositions) {
if (!oldPositions.contains(pos) && pos >= 0 && pos < songs.size()) {
notifyItemChanged(pos, "payload_playback");
}
}
}
private List<Integer> findPositionsById(String id) {
if (id == null) return Collections.emptyList();
List<Integer> positions = new ArrayList<>();
for (int i = 0; i < songs.size(); i++) {
if (id.equals(songs.get(i).getId())) {
positions.add(i);
}
}
return positions;
}
@Override
public Filter getFilter() {
return filtering;
@ -215,11 +329,29 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
}
public void onClick() {
int pos = getBindingAdapterPosition();
Child tappedSong = songs.get(pos);
Bundle bundle = new Bundle();
bundle.putParcelableArrayList(Constants.TRACKS_OBJECT, new ArrayList<>(MusicUtil.limitPlayableMedia(songs, getBindingAdapterPosition())));
bundle.putInt(Constants.ITEM_POSITION, MusicUtil.getPlayableMediaPosition(songs, getBindingAdapterPosition()));
click.onMediaClick(bundle);
if (tappedSong.getId().equals(currentPlayingId)) {
Log.i("SongHorizontalAdapter", "Tapping on currently playing song, toggling playback");
try{
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
Log.i("SongHorizontalAdapter", "MediaBrowser retrieved, isPlaying: " + isPlaying);
if (isPlaying) {
mediaBrowser.pause();
} else {
mediaBrowser.play();
}
} catch (ExecutionException | InterruptedException e) {
Log.e("SongHorizontalAdapter", "Error getting MediaBrowser", e);
}
} else {
click.onMediaClick(bundle);
}
}
private boolean onLongClick() {
@ -247,4 +379,8 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
notifyDataSetChanged();
}
public void setMediaBrowserListenableFuture(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture) {
this.mediaBrowserListenableFuture = mediaBrowserListenableFuture;
}
}

View file

@ -3,6 +3,9 @@ package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.os.Bundle;
import android.widget.Button;
import android.net.Uri;
import androidx.documentfile.provider.DocumentFile;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
@ -12,6 +15,9 @@ import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.util.ExternalDownloadMetadataStore;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@OptIn(markerClass = UnstableApi.class)
@ -42,7 +48,21 @@ public class DeleteDownloadStorageDialog extends DialogFragment {
if (dialog != null) {
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
positiveButton.setOnClickListener(v -> {
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
}
String uriString = Preferences.getDownloadDirectoryUri();
if (uriString != null) {
DocumentFile directory = DocumentFile.fromTreeUri(requireContext(), Uri.parse(uriString));
if (directory != null && directory.canWrite()) {
for (DocumentFile file : directory.listFiles()) {
file.delete();
}
}
ExternalAudioReader.refreshCache();
ExternalDownloadMetadataStore.clear();
}
dialog.dismiss();
});

View file

@ -0,0 +1,63 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.util.Preferences;
public class DownloadDirectoryPickerDialog extends DialogFragment {
private ActivityResultLauncher<Intent> folderPickerLauncher;
@NonNull
@Override
public android.app.Dialog onCreateDialog(Bundle savedInstanceState) {
// Register launcher *before* button triggers
folderPickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == android.app.Activity.RESULT_OK) {
Intent data = result.getData();
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
requireContext().getContentResolver().takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
);
Preferences.setDownloadDirectoryUri(uri.toString());
ExternalAudioReader.refreshCache();
Toast.makeText(requireContext(), "Download directory set:\n" + uri.toString(), Toast.LENGTH_LONG).show();
}
}
}
}
);
return new MaterialAlertDialogBuilder(requireContext())
.setTitle("Set Download Directory")
.setMessage("Choose a folder where downloaded songs will be stored.")
.setPositiveButton("Choose Folder", (dialog, which) -> {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
folderPickerLauncher.launch(intent);
})
.setNegativeButton(android.R.string.cancel, null)
.create();
}
}

View file

@ -34,6 +34,7 @@ public class DownloadStorageDialog extends DialogFragment {
.setTitle(R.string.download_storage_dialog_title)
.setPositiveButton(R.string.download_storage_external_dialog_positive_button, null)
.setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null)
.setNeutralButton(R.string.download_storage_directory_dialog_neutral_button, null)
.create();
}
@ -74,6 +75,20 @@ public class DownloadStorageDialog extends DialogFragment {
dialog.dismiss();
});
Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL);
neutralButton.setOnClickListener(v -> {
int currentPreference = Preferences.getDownloadStoragePreference();
int newPreference = 2;
if (currentPreference != newPreference) {
Preferences.setDownloadStoragePreference(newPreference);
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
dialogClickCallback.onNeutralClick();
}
dialog.dismiss();
});
}
}
}

View file

@ -0,0 +1,88 @@
package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.widget.Button;
import androidx.annotation.NonNull;
import androidx.annotation.OptIn;
import androidx.fragment.app.DialogFragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogStarredArtistSyncBinding;
import com.cappielloantonio.tempo.model.Download;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.StarredArtistsSyncViewModel;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import java.util.stream.Collectors;
@OptIn(markerClass = UnstableApi.class)
public class StarredArtistSyncDialog extends DialogFragment {
private StarredArtistsSyncViewModel starredArtistsSyncViewModel;
private Runnable onCancel;
public StarredArtistSyncDialog(Runnable onCancel) {
this.onCancel = onCancel;
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedInstanceState) {
DialogStarredArtistSyncBinding bind = DialogStarredArtistSyncBinding.inflate(getLayoutInflater());
starredArtistsSyncViewModel = new ViewModelProvider(requireActivity()).get(StarredArtistsSyncViewModel.class);
return new MaterialAlertDialogBuilder(getActivity())
.setView(bind.getRoot())
.setTitle(R.string.starred_artist_sync_dialog_title)
.setPositiveButton(R.string.starred_sync_dialog_positive_button, null)
.setNeutralButton(R.string.starred_sync_dialog_neutral_button, null)
.setNegativeButton(R.string.starred_sync_dialog_negative_button, null)
.create();
}
@Override
public void onResume() {
super.onResume();
setButtonAction(requireContext());
}
private void setButtonAction(Context context) {
androidx.appcompat.app.AlertDialog dialog = (androidx.appcompat.app.AlertDialog) getDialog();
if (dialog != null) {
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
positiveButton.setOnClickListener(v -> {
starredArtistsSyncViewModel.getStarredArtistSongs(requireActivity()).observe(this, allSongs -> {
if (allSongs != null && !allSongs.isEmpty()) {
DownloadUtil.getDownloadTracker(context).download(
MappingUtil.mapDownloads(allSongs),
allSongs.stream().map(Download::new).collect(Collectors.toList())
);
}
dialog.dismiss();
});
});
Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL);
neutralButton.setOnClickListener(v -> {
Preferences.setStarredArtistsSyncEnabled(true);
dialog.dismiss();
});
Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE);
negativeButton.setOnClickListener(v -> {
Preferences.setStarredArtistsSyncEnabled(false);
if (onCancel != null) onCancel.run();
dialog.dismiss();
});
}
}
}

View file

@ -61,7 +61,7 @@ public class StarredSyncDialog extends DialogFragment {
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
positiveButton.setOnClickListener(v -> {
starredSyncViewModel.getStarredTracks(requireActivity()).observe(requireActivity(), songs -> {
if (songs != null) {
if (songs != null && Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(context).download(
MappingUtil.mapDownloads(songs),
songs.stream().map(Download::new).collect(Collectors.toList())

View file

@ -39,7 +39,10 @@ import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
@ -52,6 +55,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
private FragmentAlbumPageBinding bind;
private MainActivity activity;
private AlbumPageViewModel albumPageViewModel;
private PlaybackViewModel playbackViewModel;
private SongHorizontalAdapter songHorizontalAdapter;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@ -74,6 +78,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
bind = FragmentAlbumPageBinding.inflate(inflater, container, false);
View view = bind.getRoot();
albumPageViewModel = new ViewModelProvider(requireActivity()).get(AlbumPageViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
init();
initAppBar();
@ -91,6 +96,14 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
super.onStart();
initializeMediaBrowser();
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
observePlayback();
}
public void onResume() {
super.onResume();
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
}
@Override
@ -119,7 +132,14 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
if (item.getItemId() == R.id.action_download_album) {
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
DownloadUtil.getDownloadTracker(requireContext()).download(MappingUtil.mapDownloads(songs), songs.stream().map(Download::new).collect(Collectors.toList()));
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownloads(songs),
songs.stream().map(Download::new).collect(Collectors.toList())
);
} else {
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
}
});
return true;
}
@ -269,10 +289,15 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.songRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, false, false, album);
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, false, false, album);
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
setMediaBrowserListenableFuture();
reapplyPlayback();
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
songHorizontalAdapter.setItems(songs);
reapplyPlayback();
});
}
});
}
@ -295,4 +320,31 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
public void onMediaLongClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
}
private void observePlayback() {
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
if (songHorizontalAdapter != null) {
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
});
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
if (songHorizontalAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
});
}
private void reapplyPlayback() {
if (songHorizontalAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
}
private void setMediaBrowserListenableFuture() {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
}
}

View file

@ -29,19 +29,16 @@ import com.cappielloantonio.tempo.service.MediaManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.AlbumArtistPageOrSimilarAdapter;
import com.cappielloantonio.tempo.ui.adapter.AlbumCatalogueAdapter;
import com.cappielloantonio.tempo.ui.adapter.ArtistCatalogueAdapter;
import com.cappielloantonio.tempo.ui.adapter.ArtistSimilarAdapter;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.ArtistPageViewModel;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
@UnstableApi
@ -49,6 +46,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
private FragmentArtistPageBinding bind;
private MainActivity activity;
private ArtistPageViewModel artistPageViewModel;
private PlaybackViewModel playbackViewModel;
private SongHorizontalAdapter songHorizontalAdapter;
private AlbumCatalogueAdapter albumCatalogueAdapter;
@ -63,6 +61,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
bind = FragmentArtistPageBinding.inflate(inflater, container, false);
View view = bind.getRoot();
artistPageViewModel = new ViewModelProvider(requireActivity()).get(ArtistPageViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
init();
initAppBar();
@ -80,6 +79,13 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
super.onStart();
initializeMediaBrowser();
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
observePlayback();
}
public void onResume() {
super.onResume();
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
}
@Override
@ -159,7 +165,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
bind.artistPageRadioButton.setOnClickListener(v -> {
artistPageViewModel.getArtistInstantMix().observe(getViewLifecycleOwner(), songs -> {
if (!songs.isEmpty()) {
if (songs != null && !songs.isEmpty()) {
MediaManager.startQueue(mediaBrowserListenableFuture, songs, 0);
activity.setBottomSheetInPeek(true);
} else {
@ -172,8 +178,10 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
private void initTopSongsView() {
bind.mostStreamedSongRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
songHorizontalAdapter = new SongHorizontalAdapter(this, true, true, null);
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, true, null);
bind.mostStreamedSongRecyclerView.setAdapter(songHorizontalAdapter);
setMediaBrowserListenableFuture();
reapplyPlayback();
artistPageViewModel.getArtistTopSongList().observe(getViewLifecycleOwner(), songs -> {
if (songs == null) {
if (bind != null) bind.artistPageTopSongsSector.setVisibility(View.GONE);
@ -183,6 +191,7 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
if (bind != null)
bind.artistPageShuffleButton.setEnabled(!songs.isEmpty());
songHorizontalAdapter.setItems(songs);
reapplyPlayback();
}
});
}
@ -273,4 +282,31 @@ public class ArtistPageFragment extends Fragment implements ClickCallback {
public void onArtistLongClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle);
}
private void observePlayback() {
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
if (songHorizontalAdapter != null) {
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
});
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
if (songHorizontalAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
});
}
private void reapplyPlayback() {
if (songHorizontalAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
}
private void setMediaBrowserListenableFuture() {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
}
}

View file

@ -33,7 +33,9 @@ import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
import com.google.common.util.concurrent.ListenableFuture;
@ -109,10 +111,14 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
if (isVisible() && getActivity() != null) {
List<Child> songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList());
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownloads(songs),
songs.stream().map(Download::new).collect(Collectors.toList())
);
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownloads(songs),
songs.stream().map(Download::new).collect(Collectors.toList())
);
} else {
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
}
}
});
}

View file

@ -28,11 +28,17 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
import com.google.android.material.appbar.MaterialToolbar;
import com.google.common.util.concurrent.ListenableFuture;
import android.content.Intent;
import android.app.Activity;
import android.net.Uri;
import android.widget.Toast;
import java.util.Collections;
import java.util.List;
import java.util.Objects;
@ -40,6 +46,7 @@ import java.util.Objects;
@UnstableApi
public class DownloadFragment extends Fragment implements ClickCallback {
private static final String TAG = "DownloadFragment";
private static final int REQUEST_CODE_PICK_DIRECTORY = 1002;
private FragmentDownloadBinding bind;
private MainActivity activity;
@ -129,8 +136,27 @@ public class DownloadFragment extends Fragment implements ClickCallback {
}
});
downloadViewModel.getRefreshResult().observe(getViewLifecycleOwner(), count -> {
if (count == null || bind == null) {
return;
}
if (count == -1) {
Toast.makeText(requireContext(), R.string.download_refresh_no_directory, Toast.LENGTH_SHORT).show();
} else if (count == 0) {
Toast.makeText(requireContext(), R.string.download_refresh_no_changes, Toast.LENGTH_SHORT).show();
} else {
Toast.makeText(
requireContext(),
getResources().getQuantityString(R.plurals.download_refresh_removed, count, count),
Toast.LENGTH_SHORT
).show();
}
});
bind.downloadedGroupByImageView.setOnClickListener(view -> showPopupMenu(view, R.menu.download_popup_menu));
bind.downloadedGoBackImageView.setOnClickListener(view -> downloadViewModel.popViewStack());
bind.downloadedRefreshImageView.setOnClickListener(view -> downloadViewModel.refreshExternalDownloads());
}
private void finishDownloadView(List<Child> songs) {
@ -216,6 +242,10 @@ public class DownloadFragment extends Fragment implements ClickCallback {
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null));
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR);
return true;
} else if (menuItem.getItemId() == R.id.menu_download_set_directory) {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
startActivityForResult(intent, REQUEST_CODE_PICK_DIRECTORY);
return true;
}
return false;
@ -267,4 +297,21 @@ public class DownloadFragment extends Fragment implements ClickCallback {
public void onDownloadGroupLongClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.downloadBottomSheetDialog, bundle);
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
if (requestCode == REQUEST_CODE_PICK_DIRECTORY && resultCode == Activity.RESULT_OK) {
Uri uri = data.getData();
if (uri != null) {
requireContext().getContentResolver().takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
);
Preferences.setDownloadDirectoryUri(uri.toString());
ExternalAudioReader.refreshCache();
Toast.makeText(requireContext(), "Download directory set", Toast.LENGTH_SHORT).show();
}
}
}
}

View file

@ -0,0 +1,237 @@
package com.cappielloantonio.tempo.ui.fragment
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.ServiceConnection
import android.os.Bundle
import android.os.IBinder
import android.view.Gravity
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.*
import androidx.annotation.OptIn
import androidx.fragment.app.Fragment
import androidx.media3.common.util.UnstableApi
import com.cappielloantonio.tempo.R
import com.cappielloantonio.tempo.service.EqualizerManager
import com.cappielloantonio.tempo.service.MediaService
import com.cappielloantonio.tempo.util.Preferences
class EqualizerFragment : Fragment() {
private var equalizerManager: EqualizerManager? = null
private lateinit var eqBandsContainer: LinearLayout
private lateinit var eqSwitch: Switch
private lateinit var resetButton: Button
private lateinit var safeSpace: Space
private val bandSeekBars = mutableListOf<SeekBar>()
private val connection = object : ServiceConnection {
@OptIn(UnstableApi::class)
override fun onServiceConnected(className: ComponentName, service: IBinder) {
val binder = service as MediaService.LocalBinder
equalizerManager = binder.getEqualizerManager()
initUI()
restoreEqualizerPreferences()
}
override fun onServiceDisconnected(arg0: ComponentName) {
equalizerManager = null
}
}
@OptIn(UnstableApi::class)
override fun onStart() {
super.onStart()
Intent(requireContext(), MediaService::class.java).also { intent ->
intent.action = MediaService.ACTION_BIND_EQUALIZER
requireActivity().bindService(intent, connection, Context.BIND_AUTO_CREATE)
}
}
override fun onStop() {
super.onStop()
requireActivity().unbindService(connection)
equalizerManager = null
}
override fun onCreateView(
inflater: LayoutInflater, container: ViewGroup?,
savedInstanceState: Bundle?
): View? {
val root = inflater.inflate(R.layout.fragment_equalizer, container, false)
eqSwitch = root.findViewById(R.id.equalizer_switch)
eqSwitch.isChecked = Preferences.isEqualizerEnabled()
eqSwitch.jumpDrawablesToCurrentState()
return root
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
eqBandsContainer = view.findViewById(R.id.eq_bands_container)
resetButton = view.findViewById(R.id.equalizer_reset_button)
safeSpace = view.findViewById(R.id.equalizer_bottom_space)
}
private fun initUI() {
val manager = equalizerManager
val notSupportedView = view?.findViewById<LinearLayout>(R.id.equalizer_not_supported_container)
val switchRow = view?.findViewById<View>(R.id.equalizer_switch_row)
if (manager == null || manager.getNumberOfBands().toInt() == 0) {
switchRow?.visibility = View.GONE
resetButton.visibility = View.GONE
eqBandsContainer.visibility = View.GONE
safeSpace.visibility = View.GONE
notSupportedView?.visibility = View.VISIBLE
return
}
notSupportedView?.visibility = View.GONE
switchRow?.visibility = View.VISIBLE
resetButton.visibility = View.VISIBLE
eqBandsContainer.visibility = View.VISIBLE
safeSpace.visibility = View.VISIBLE
eqSwitch.setOnCheckedChangeListener(null)
updateUiEnabledState(eqSwitch.isChecked)
eqSwitch.setOnCheckedChangeListener { _, isChecked ->
manager.setEnabled(isChecked)
Preferences.setEqualizerEnabled(isChecked)
updateUiEnabledState(isChecked)
}
createBandSliders()
resetButton.setOnClickListener {
resetEqualizer()
saveBandLevelsToPreferences()
}
}
private fun updateUiEnabledState(isEnabled: Boolean) {
resetButton.isEnabled = isEnabled
bandSeekBars.forEach { it.isEnabled = isEnabled }
}
private fun formatDb(value: Int): String = if (value > 0) "+$value dB" else "$value dB"
private fun createBandSliders() {
val manager = equalizerManager ?: return
eqBandsContainer.removeAllViews()
bandSeekBars.clear()
val bands = manager.getNumberOfBands()
val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500)
val minLevelDb = bandLevelRange[0] / 100
val maxLevelDb = bandLevelRange[1] / 100
val savedLevels = Preferences.getEqualizerBandLevels(bands)
for (i in 0 until bands) {
val band = i.toShort()
val freq = manager.getCenterFreq(band) ?: 0
val row = LinearLayout(requireContext()).apply {
orientation = LinearLayout.HORIZONTAL
layoutParams = LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
).apply {
val topBottomMarginDp = 16
topMargin = topBottomMarginDp.dpToPx(context)
bottomMargin = topBottomMarginDp.dpToPx(context)
}
setPadding(0, 8, 0, 8)
}
val freqLabel = TextView(requireContext(), null, 0, R.style.LabelSmall).apply {
text = if (freq >= 1000) {
if (freq % 1000 == 0) {
"${freq / 1000} kHz"
} else {
String.format("%.1f kHz", freq / 1000f)
}
} else {
"$freq Hz"
}
gravity = Gravity.START
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)
}
row.addView(freqLabel)
val initialLevelDb = (savedLevels.getOrNull(i) ?: (manager.getBandLevel(band) ?: 0)) / 100
val dbLabel = TextView(requireContext(), null, 0, R.style.LabelSmall).apply {
text = formatDb(initialLevelDb)
setPadding(12, 0, 0, 0)
gravity = Gravity.END
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 2f)
}
val seekBar = SeekBar(requireContext()).apply {
max = maxLevelDb - minLevelDb
progress = initialLevelDb - minLevelDb
layoutParams = LinearLayout.LayoutParams(0, LinearLayout.LayoutParams.WRAP_CONTENT, 6f)
setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, progress: Int, fromUser: Boolean) {
val thisLevelDb = progress + minLevelDb
if (fromUser) {
manager.setBandLevel(band, (thisLevelDb * 100).toShort())
saveBandLevelsToPreferences()
}
dbLabel.text = formatDb(thisLevelDb)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
}
bandSeekBars.add(seekBar)
row.addView(seekBar)
row.addView(dbLabel)
eqBandsContainer.addView(row)
}
}
private fun resetEqualizer() {
val manager = equalizerManager ?: return
val bands = manager.getNumberOfBands()
val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500)
val minLevelDb = bandLevelRange[0] / 100
val midLevelDb = 0
for (i in 0 until bands) {
manager.setBandLevel(i.toShort(), (0).toShort())
bandSeekBars.getOrNull(i)?.progress = midLevelDb - minLevelDb
}
Preferences.setEqualizerBandLevels(ShortArray(bands.toInt()))
}
private fun saveBandLevelsToPreferences() {
val manager = equalizerManager ?: return
val bands = manager.getNumberOfBands()
val levels = ShortArray(bands.toInt()) { i -> manager.getBandLevel(i.toShort()) ?: 0 }
Preferences.setEqualizerBandLevels(levels)
}
private fun restoreEqualizerPreferences() {
val manager = equalizerManager ?: return
eqSwitch.isChecked = Preferences.isEqualizerEnabled()
updateUiEnabledState(eqSwitch.isChecked)
val bands = manager.getNumberOfBands()
val bandLevelRange = manager.getBandLevelRange() ?: shortArrayOf(-1500, 1500)
val minLevelDb = bandLevelRange[0] / 100
val savedLevels = Preferences.getEqualizerBandLevels(bands)
for (i in 0 until bands) {
val savedDb = savedLevels[i] / 100
manager.setBandLevel(i.toShort(), (savedDb * 100).toShort())
bandSeekBars.getOrNull(i)?.progress = savedDb - minLevelDb
}
}
}
private fun Int.dpToPx(context: Context): Int =
(this * context.resources.displayMetrics.density).toInt()

View file

@ -9,6 +9,7 @@ import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.PopupMenu;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -40,6 +41,7 @@ import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.Share;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
import com.cappielloantonio.tempo.ui.adapter.AlbumHorizontalAdapter;
@ -60,9 +62,12 @@ import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.UIUtil;
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.google.android.material.snackbar.Snackbar;
import com.google.common.util.concurrent.ListenableFuture;
import androidx.media3.common.MediaItem;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Collectors;
@ -74,6 +79,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
private FragmentHomeTabMusicBinding bind;
private MainActivity activity;
private HomeViewModel homeViewModel;
private PlaybackViewModel playbackViewModel;
private DiscoverSongAdapter discoverSongAdapter;
private SimilarTrackAdapter similarMusicAdapter;
@ -101,6 +107,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
bind = FragmentHomeTabMusicBinding.inflate(inflater, container, false);
View view = bind.getRoot();
homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
init();
@ -113,6 +120,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
initSyncStarredView();
initSyncStarredAlbumsView();
initSyncStarredArtistsView();
initDiscoverSongSlideView();
initSimilarSongView();
initArtistRadio();
@ -138,12 +146,18 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
super.onStart();
initializeMediaBrowser();
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
observeStarredSongsPlayback();
observeTopSongsPlayback();
}
@Override
public void onResume() {
super.onResume();
refreshSharesView();
if (topSongAdapter != null) setTopSongsMediaBrowserListenableFuture();
if (starredSongAdapter != null) setStarredSongsMediaBrowserListenableFuture();
}
@Override
@ -265,7 +279,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
}
private void initSyncStarredView() {
if (Preferences.isStarredSyncEnabled()) {
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> songs) {
@ -318,32 +332,12 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
private void initSyncStarredAlbumsView() {
if (Preferences.isStarredAlbumsSyncEnabled()) {
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observeForever(new Observer<List<AlbumID3>>() {
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer<List<AlbumID3>>() {
@Override
public void onChanged(List<AlbumID3> albums) {
if (albums != null) {
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
List<String> albumsToSync = new ArrayList<>();
int albumCount = 0;
for (AlbumID3 album : albums) {
boolean needsSync = false;
albumCount++;
albumsToSync.add(album.getName());
}
if (albumCount > 0) {
bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE);
String message = getResources().getQuantityString(
R.plurals.home_sync_starred_albums_count,
albumCount,
albumCount
);
bind.homeSyncStarredAlbumsToSync.setText(message);
}
if (albums != null && !albums.isEmpty()) {
checkIfAlbumsNeedSync(albums);
}
homeViewModel.getStarredAlbums(getViewLifecycleOwner()).removeObserver(this);
}
});
}
@ -353,26 +347,157 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
});
bind.homeSyncStarredAlbumsDownload.setOnClickListener(v -> {
homeViewModel.getAllStarredAlbumSongs().observeForever(new Observer<List<Child>>() {
homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> allSongs) {
if (allSongs != null) {
if (allSongs != null && !allSongs.isEmpty()) {
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
int songsToDownload = 0;
for (Child song : allSongs) {
if (!manager.isDownloaded(song.getId())) {
manager.download(MappingUtil.mapDownload(song), new Download(song));
songsToDownload++;
}
}
}
homeViewModel.getAllStarredAlbumSongs().removeObserver(this);
if (songsToDownload > 0) {
Toast.makeText(requireContext(),
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
Toast.LENGTH_SHORT).show();
}
}
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
}
});
});
}
private void checkIfAlbumsNeedSync(List<AlbumID3> albums) {
homeViewModel.getAllStarredAlbumSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> allSongs) {
if (allSongs != null) {
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
int songsToDownload = 0;
List<String> albumsNeedingSync = new ArrayList<>();
for (AlbumID3 album : albums) {
boolean albumNeedsSync = false;
// Check if any songs from this album need downloading
for (Child song : allSongs) {
if (song.getAlbumId() != null && song.getAlbumId().equals(album.getId()) &&
!manager.isDownloaded(song.getId())) {
songsToDownload++;
albumNeedsSync = true;
}
}
if (albumNeedsSync) {
albumsNeedingSync.add(album.getName());
}
}
if (songsToDownload > 0) {
bind.homeSyncStarredAlbumsCard.setVisibility(View.VISIBLE);
String message = getResources().getQuantityString(
R.plurals.home_sync_starred_albums_count,
albumsNeedingSync.size(),
albumsNeedingSync.size()
);
bind.homeSyncStarredAlbumsToSync.setText(message);
} else {
bind.homeSyncStarredAlbumsCard.setVisibility(View.GONE);
}
}
}
});
}
private void initSyncStarredArtistsView() {
if (Preferences.isStarredArtistsSyncEnabled()) {
homeViewModel.getStarredArtists(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), new Observer<List<ArtistID3>>() {
@Override
public void onChanged(List<ArtistID3> artists) {
if (artists != null && !artists.isEmpty()) {
checkIfArtistsNeedSync(artists);
}
}
});
}
bind.homeSyncStarredArtistsCancel.setOnClickListener(v -> {
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
});
bind.homeSyncStarredArtistsDownload.setOnClickListener(v -> {
homeViewModel.getAllStarredArtistSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> allSongs) {
if (allSongs != null && !allSongs.isEmpty()) {
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
int songsToDownload = 0;
for (Child song : allSongs) {
if (!manager.isDownloaded(song.getId())) {
manager.download(MappingUtil.mapDownload(song), new Download(song));
songsToDownload++;
}
}
if (songsToDownload > 0) {
Toast.makeText(requireContext(),
getResources().getQuantityString(R.plurals.songs_download_started, songsToDownload, songsToDownload),
Toast.LENGTH_SHORT).show();
}
}
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
}
});
});
}
private void checkIfArtistsNeedSync(List<ArtistID3> artists) {
homeViewModel.getAllStarredArtistSongs().observe(getViewLifecycleOwner(), new Observer<List<Child>>() {
@Override
public void onChanged(List<Child> allSongs) {
if (allSongs != null) {
DownloaderManager manager = DownloadUtil.getDownloadTracker(requireContext());
int songsToDownload = 0;
List<String> artistsNeedingSync = new ArrayList<>();
for (ArtistID3 artist : artists) {
boolean artistNeedsSync = false;
// Check if any songs from this artist need downloading
for (Child song : allSongs) {
if (song.getArtistId() != null && song.getArtistId().equals(artist.getId()) &&
!manager.isDownloaded(song.getId())) {
songsToDownload++;
artistNeedsSync = true;
}
}
if (artistNeedsSync) {
artistsNeedingSync.add(artist.getName());
}
}
if (songsToDownload > 0) {
bind.homeSyncStarredArtistsCard.setVisibility(View.VISIBLE);
String message = getResources().getQuantityString(
R.plurals.home_sync_starred_artists_count,
artistsNeedingSync.size(),
artistsNeedingSync.size()
);
bind.homeSyncStarredArtistsToSync.setText(message);
} else {
bind.homeSyncStarredArtistsCard.setVisibility(View.GONE);
}
}
}
});
}
private void initDiscoverSongSlideView() {
if (homeViewModel.checkHomeSectorVisibility(Constants.HOME_SECTOR_DISCOVERY)) return;
@ -475,8 +600,10 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
bind.topSongsRecyclerView.setHasFixedSize(true);
topSongAdapter = new SongHorizontalAdapter(this, true, false, null);
topSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
bind.topSongsRecyclerView.setAdapter(topSongAdapter);
setTopSongsMediaBrowserListenableFuture();
reapplyTopSongsPlayback();
homeViewModel.getChronologySample(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), chronologies -> {
if (chronologies == null || chronologies.isEmpty()) {
if (bind != null) bind.homeGridTracksSector.setVisibility(View.GONE);
@ -492,6 +619,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
.collect(Collectors.toList());
topSongAdapter.setItems(topSongs);
reapplyTopSongsPlayback();
}
});
@ -513,8 +641,10 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
bind.starredTracksRecyclerView.setHasFixedSize(true);
starredSongAdapter = new SongHorizontalAdapter(this, true, false, null);
starredSongAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
bind.starredTracksRecyclerView.setAdapter(starredSongAdapter);
setStarredSongsMediaBrowserListenableFuture();
reapplyStarredSongsPlayback();
homeViewModel.getStarredTracks(getViewLifecycleOwner()).observe(getViewLifecycleOwner(), songs -> {
if (songs == null) {
if (bind != null) bind.starredTracksSector.setVisibility(View.GONE);
@ -525,6 +655,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
bind.starredTracksRecyclerView.setLayoutManager(new GridLayoutManager(requireContext(), UIUtil.getSpanCount(songs.size(), 5), GridLayoutManager.HORIZONTAL, false));
starredSongAdapter.setItems(songs);
reapplyStarredSongsPlayback();
}
});
@ -954,6 +1085,8 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
activity.setBottomSheetInPeek(true);
}
topSongAdapter.notifyDataSetChanged();
starredSongAdapter.notifyDataSetChanged();
}
@Override
@ -1043,4 +1176,58 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
public void onShareLongClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.shareBottomSheetDialog, bundle);
}
private void observeStarredSongsPlayback() {
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
if (starredSongAdapter != null) {
Boolean playing = playbackViewModel.getIsPlaying().getValue();
starredSongAdapter.setPlaybackState(id, playing != null && playing);
}
});
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
if (starredSongAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
starredSongAdapter.setPlaybackState(id, playing != null && playing);
}
});
}
private void observeTopSongsPlayback() {
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
if (topSongAdapter != null) {
Boolean playing = playbackViewModel.getIsPlaying().getValue();
topSongAdapter.setPlaybackState(id, playing != null && playing);
}
});
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
if (topSongAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
topSongAdapter.setPlaybackState(id, playing != null && playing);
}
});
}
private void reapplyStarredSongsPlayback() {
if (starredSongAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
starredSongAdapter.setPlaybackState(id, playing != null && playing);
}
}
private void reapplyTopSongsPlayback() {
if (topSongAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
topSongAdapter.setPlaybackState(id, playing != null && playing);
}
}
private void setTopSongsMediaBrowserListenableFuture() {
topSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
}
private void setStarredSongsMediaBrowserListenableFuture() {
starredSongAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
}
}

View file

@ -1,7 +1,11 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.Bundle;
import android.os.IBinder;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
@ -24,11 +28,14 @@ import androidx.media3.common.util.RepeatModeUtil;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import androidx.navigation.NavController;
import androidx.navigation.NavOptions;
import androidx.navigation.fragment.NavHostFragment;
import androidx.viewpager2.widget.ViewPager2;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBinding;
import com.cappielloantonio.tempo.service.EqualizerManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
@ -68,11 +75,15 @@ public class PlayerControllerFragment extends Fragment {
private ImageButton playerOpenQueueButton;
private ImageButton playerTrackInfo;
private LinearLayout ratingContainer;
private ImageButton equalizerButton;
private MainActivity activity;
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private MediaService.LocalBinder mediaServiceBinder;
private boolean isServiceBound = false;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
activity = (MainActivity) getActivity();
@ -89,6 +100,7 @@ public class PlayerControllerFragment extends Fragment {
initMediaListenable();
initMediaLabelButton();
initArtistLabelButton();
initEqualizerButton();
return view;
}
@ -126,6 +138,7 @@ public class PlayerControllerFragment extends Fragment {
playerTrackInfo = bind.getRoot().findViewById(R.id.player_info_track);
songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar);
ratingContainer = bind.getRoot().findViewById(R.id.rating_container);
equalizerButton = bind.getRoot().findViewById(R.id.player_open_equalizer_button);
checkAndSetRatingContainerVisibility();
}
@ -426,6 +439,18 @@ public class PlayerControllerFragment extends Fragment {
});
}
private void initEqualizerButton() {
equalizerButton.setOnClickListener(v -> {
NavController navController = NavHostFragment.findNavController(this);
NavOptions navOptions = new NavOptions.Builder()
.setLaunchSingleTop(true)
.setPopUpTo(R.id.equalizerFragment, true)
.build();
navController.navigate(R.id.equalizerFragment, null, navOptions);
if (activity != null) activity.collapseBottomSheetDelayed();
});
}
public void goToControllerPage() {
playerMediaCoverViewPager.setCurrentItem(0, false);
}
@ -461,4 +486,66 @@ public class PlayerControllerFragment extends Fragment {
mediaBrowser.setPlaybackParameters(new PlaybackParameters(Constants.MEDIA_PLAYBACK_SPEED_100));
// TODO Resettare lo skip del silenzio
}
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mediaServiceBinder = (MediaService.LocalBinder) service;
isServiceBound = true;
checkEqualizerBands();
}
@Override
public void onServiceDisconnected(ComponentName name) {
mediaServiceBinder = null;
isServiceBound = false;
}
};
private void bindMediaService() {
Intent intent = new Intent(requireActivity(), MediaService.class);
intent.setAction(MediaService.ACTION_BIND_EQUALIZER);
requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
isServiceBound = true;
}
private void checkEqualizerBands() {
if (mediaServiceBinder != null) {
EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager();
short numBands = eqManager.getNumberOfBands();
if (equalizerButton != null) {
if (numBands == 0) {
equalizerButton.setVisibility(View.GONE);
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) playerOpenQueueButton.getLayoutParams();
params.startToEnd = ConstraintLayout.LayoutParams.UNSET;
params.startToStart = ConstraintLayout.LayoutParams.PARENT_ID;
playerOpenQueueButton.setLayoutParams(params);
} else {
equalizerButton.setVisibility(View.VISIBLE);
ConstraintLayout.LayoutParams params = (ConstraintLayout.LayoutParams) playerOpenQueueButton.getLayoutParams();
params.startToStart = ConstraintLayout.LayoutParams.UNSET;
params.startToEnd = R.id.player_open_equalizer_button;
playerOpenQueueButton.setLayoutParams(params);
}
}
}
}
@Override
public void onResume() {
super.onResume();
bindMediaService();
}
@Override
public void onPause() {
super.onPause();
if (isServiceBound) {
requireActivity().unbindService(serviceConnection);
isServiceBound = false;
}
}
}

View file

@ -14,6 +14,7 @@ import java.util.ArrayList;
import androidx.annotation.NonNull;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.MediaItem;
import androidx.media3.common.MediaMetadata;
import androidx.media3.common.Player;
import androidx.media3.common.util.UnstableApi;
@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.google.android.material.snackbar.Snackbar;
@ -115,10 +117,14 @@ public class PlayerCoverFragment extends Fragment {
playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), song -> {
if (song != null && bind != null) {
bind.innerButtonTopLeft.setOnClickListener(view -> {
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownload(song),
new Download(song)
);
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownload(song),
new Download(song)
);
} else {
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
}
});
bind.innerButtonTopRight.setOnClickListener(view -> {

View file

@ -4,15 +4,16 @@ import android.annotation.SuppressLint;
import android.content.ComponentName;
import android.os.Bundle;
import android.os.Handler;
import android.text.Layout;
import android.text.Spannable;
import android.text.SpannableString;
import android.text.Layout;
import android.text.TextUtils;
import android.text.style.ForegroundColorSpan;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -29,10 +30,10 @@ import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Line;
import com.cappielloantonio.tempo.subsonic.models.LyricsList;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.OpenSubsonicExtensionsUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.android.material.button.MaterialButton;
import com.google.common.util.concurrent.MoreExecutors;
import java.util.List;
@ -48,6 +49,9 @@ public class PlayerLyricsFragment extends Fragment {
private MediaBrowser mediaBrowser;
private Handler syncLyricsHandler;
private Runnable syncLyricsRunnable;
private String currentLyrics;
private LyricsList currentLyricsList;
private String currentDescription;
@Override
public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
@ -66,6 +70,7 @@ public class PlayerLyricsFragment extends Fragment {
super.onViewCreated(view, savedInstanceState);
initPanelContent();
observeDownloadState();
}
@Override
@ -101,12 +106,26 @@ public class PlayerLyricsFragment extends Fragment {
public void onDestroyView() {
super.onDestroyView();
bind = null;
currentLyrics = null;
currentLyricsList = null;
currentDescription = null;
}
private void initOverlay() {
bind.syncLyricsTapButton.setOnClickListener(view -> {
playerBottomSheetViewModel.changeSyncLyricsState();
});
bind.downloadLyricsButton.setOnClickListener(view -> {
boolean saved = playerBottomSheetViewModel.downloadCurrentLyrics();
if (getContext() != null) {
Toast.makeText(
requireContext(),
saved ? R.string.player_lyrics_download_success : R.string.player_lyrics_download_failure,
Toast.LENGTH_SHORT
).show();
}
});
}
private void initializeBrowser() {
@ -136,50 +155,91 @@ public class PlayerLyricsFragment extends Fragment {
}
private void initPanelContent() {
if (OpenSubsonicExtensionsUtil.isSongLyricsExtensionAvailable()) {
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
setPanelContent(null, lyricsList);
});
} else {
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
setPanelContent(lyrics, null);
});
}
playerBottomSheetViewModel.getLiveLyrics().observe(getViewLifecycleOwner(), lyrics -> {
currentLyrics = lyrics;
updatePanelContent();
});
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
currentLyricsList = lyricsList;
updatePanelContent();
});
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
currentDescription = description;
updatePanelContent();
});
}
private void setPanelContent(String lyrics, LyricsList lyricsList) {
playerBottomSheetViewModel.getLiveDescription().observe(getViewLifecycleOwner(), description -> {
private void observeDownloadState() {
playerBottomSheetViewModel.getLyricsCachedState().observe(getViewLifecycleOwner(), cached -> {
if (bind != null) {
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
if (lyrics != null && !lyrics.trim().equals("")) {
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(lyrics));
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
bind.syncLyricsTapButton.setVisibility(View.GONE);
} else if (lyricsList != null && lyricsList.getStructuredLyrics() != null) {
setSyncLirics(lyricsList);
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
} else if (description != null && !description.trim().equals("")) {
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(description));
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
bind.syncLyricsTapButton.setVisibility(View.GONE);
MaterialButton downloadButton = (MaterialButton) bind.downloadLyricsButton;
if (cached != null && cached) {
downloadButton.setIconResource(R.drawable.ic_done);
downloadButton.setContentDescription(getString(R.string.player_lyrics_downloaded_content_description));
} else {
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
bind.syncLyricsTapButton.setVisibility(View.GONE);
downloadButton.setIconResource(R.drawable.ic_download);
downloadButton.setContentDescription(getString(R.string.player_lyrics_download_content_description));
}
}
});
}
private void updatePanelContent() {
if (bind == null) {
return;
}
bind.nowPlayingSongLyricsSrollView.smoothScrollTo(0, 0);
if (hasStructuredLyrics(currentLyricsList)) {
setSyncLirics(currentLyricsList);
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
bind.syncLyricsTapButton.setVisibility(View.VISIBLE);
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
bind.downloadLyricsButton.setEnabled(true);
} else if (hasText(currentLyrics)) {
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentLyrics));
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
bind.syncLyricsTapButton.setVisibility(View.GONE);
bind.downloadLyricsButton.setVisibility(View.VISIBLE);
bind.downloadLyricsButton.setEnabled(true);
} else if (hasText(currentDescription)) {
bind.nowPlayingSongLyricsTextView.setText(MusicUtil.getReadableLyrics(currentDescription));
bind.nowPlayingSongLyricsTextView.setVisibility(View.VISIBLE);
bind.emptyDescriptionImageView.setVisibility(View.GONE);
bind.titleEmptyDescriptionLabel.setVisibility(View.GONE);
bind.syncLyricsTapButton.setVisibility(View.GONE);
bind.downloadLyricsButton.setVisibility(View.GONE);
bind.downloadLyricsButton.setEnabled(false);
} else {
bind.nowPlayingSongLyricsTextView.setVisibility(View.GONE);
bind.emptyDescriptionImageView.setVisibility(View.VISIBLE);
bind.titleEmptyDescriptionLabel.setVisibility(View.VISIBLE);
bind.syncLyricsTapButton.setVisibility(View.GONE);
bind.downloadLyricsButton.setVisibility(View.GONE);
bind.downloadLyricsButton.setEnabled(false);
}
}
private boolean hasText(String value) {
return value != null && !value.trim().isEmpty();
}
private boolean hasStructuredLyrics(LyricsList lyricsList) {
return lyricsList != null
&& lyricsList.getStructuredLyrics() != null
&& !lyricsList.getStructuredLyrics().isEmpty()
&& lyricsList.getStructuredLyrics().get(0) != null
&& lyricsList.getStructuredLyrics().get(0).getLine() != null
&& !lyricsList.getStructuredLyrics().get(0).getLine().isEmpty();
}
@SuppressLint("DefaultLocale")
private void setSyncLirics(LyricsList lyricsList) {
if (lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
@ -198,28 +258,28 @@ public class PlayerLyricsFragment extends Fragment {
private void defineProgressHandler() {
playerBottomSheetViewModel.getLiveLyricsList().observe(getViewLifecycleOwner(), lyricsList -> {
if (lyricsList != null) {
if (lyricsList.getStructuredLyrics() != null && lyricsList.getStructuredLyrics().get(0) != null && !lyricsList.getStructuredLyrics().get(0).getSynced()) {
releaseHandler();
return;
}
syncLyricsHandler = new Handler();
syncLyricsRunnable = () -> {
if (syncLyricsHandler != null) {
if (bind != null) {
displaySyncedLyrics();
}
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
}
};
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
} else {
if (!hasStructuredLyrics(lyricsList)) {
releaseHandler();
return;
}
if (!lyricsList.getStructuredLyrics().get(0).getSynced()) {
releaseHandler();
return;
}
syncLyricsHandler = new Handler();
syncLyricsRunnable = () -> {
if (syncLyricsHandler != null) {
if (bind != null) {
displaySyncedLyrics();
}
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
}
};
syncLyricsHandler.postDelayed(syncLyricsRunnable, 250);
});
}
@ -227,7 +287,7 @@ public class PlayerLyricsFragment extends Fragment {
LyricsList lyricsList = playerBottomSheetViewModel.getLiveLyricsList().getValue();
int timestamp = (int) (mediaBrowser.getCurrentPosition());
if (lyricsList != null && lyricsList.getStructuredLyrics() != null && !lyricsList.getStructuredLyrics().isEmpty() && lyricsList.getStructuredLyrics().get(0).getLine() != null) {
if (hasStructuredLyrics(lyricsList)) {
StringBuilder lyricsBuilder = new StringBuilder();
List<Line> lines = lyricsList.getStructuredLyrics().get(0).getLine();

View file

@ -23,6 +23,7 @@ import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.adapter.PlayerSongQueueAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -38,6 +39,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
private InnerFragmentPlayerQueueBinding bind;
private PlayerBottomSheetViewModel playerBottomSheetViewModel;
private PlaybackViewModel playbackViewModel;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
private PlayerSongQueueAdapter playerSongQueueAdapter;
@ -48,6 +50,7 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
View view = bind.getRoot();
playerBottomSheetViewModel = new ViewModelProvider(requireActivity()).get(PlayerBottomSheetViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
initQueueRecyclerView();
@ -59,6 +62,9 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
super.onStart();
initializeBrowser();
bindMediaController();
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
observePlayback();
}
@Override
@ -110,9 +116,12 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
playerSongQueueAdapter = new PlayerSongQueueAdapter(this);
bind.playerQueueRecyclerView.setAdapter(playerSongQueueAdapter);
reapplyPlayback();
playerBottomSheetViewModel.getQueueSong().observe(getViewLifecycleOwner(), queue -> {
if (queue != null) {
playerSongQueueAdapter.setItems(queue.stream().map(item -> (Child) item).collect(Collectors.toList()));
reapplyPlayback();
}
});
@ -216,4 +225,27 @@ public class PlayerQueueFragment extends Fragment implements ClickCallback {
public void onMediaClick(Bundle bundle) {
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
}
private void observePlayback() {
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
if (playerSongQueueAdapter != null) {
Boolean playing = playbackViewModel.getIsPlaying().getValue();
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
}
});
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
if (playerSongQueueAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
}
});
}
private void reapplyPlayback() {
if (playerSongQueueAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
playerSongQueueAdapter.setPlaybackState(id, playing != null && playing);
}
}
}

View file

@ -37,6 +37,9 @@ import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel;
import com.google.common.util.concurrent.ListenableFuture;
@ -49,6 +52,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
private FragmentPlaylistPageBinding bind;
private MainActivity activity;
private PlaylistPageViewModel playlistPageViewModel;
private PlaybackViewModel playbackViewModel;
private SongHorizontalAdapter songHorizontalAdapter;
@ -94,6 +98,7 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
bind = FragmentPlaylistPageBinding.inflate(inflater, container, false);
View view = bind.getRoot();
playlistPageViewModel = new ViewModelProvider(requireActivity()).get(PlaylistPageViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
init();
initAppBar();
@ -109,6 +114,15 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
super.onStart();
initializeMediaBrowser();
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
observePlayback();
}
@Override
public void onResume() {
super.onResume();
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
}
@Override
@ -128,7 +142,8 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
if (item.getItemId() == R.id.action_download_playlist) {
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
if (isVisible() && getActivity() != null) {
DownloadUtil.getDownloadTracker(requireContext()).download(
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownloads(songs),
songs.stream().map(child -> {
Download toDownload = new Download(child);
@ -136,7 +151,10 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName());
return toDownload;
}).collect(Collectors.toList())
);
);
} else {
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
}
}
});
return true;
@ -246,10 +264,15 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
bind.songRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.songRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
bind.songRecyclerView.setAdapter(songHorizontalAdapter);
setMediaBrowserListenableFuture();
reapplyPlayback();
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> songHorizontalAdapter.setItems(songs));
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
songHorizontalAdapter.setItems(songs);
reapplyPlayback();
});
}
private void initializeMediaBrowser() {
@ -270,4 +293,31 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
public void onMediaLongClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
}
private void observePlayback() {
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
if (songHorizontalAdapter != null) {
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
});
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
if (songHorizontalAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
});
}
private void reapplyPlayback() {
if (songHorizontalAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
}
private void setMediaBrowserListenableFuture() {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
}
}

View file

@ -4,14 +4,11 @@ import android.content.ComponentName;
import android.os.Bundle;
import android.text.Editable;
import android.text.TextWatcher;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.inputmethod.EditorInfo;
import android.widget.ImageView;
import android.widget.TextView;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -34,6 +31,7 @@ import com.cappielloantonio.tempo.ui.adapter.AlbumAdapter;
import com.cappielloantonio.tempo.ui.adapter.ArtistAdapter;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.cappielloantonio.tempo.viewmodel.SearchViewModel;
import com.google.common.util.concurrent.ListenableFuture;
@ -46,6 +44,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
private FragmentSearchBinding bind;
private MainActivity activity;
private SearchViewModel searchViewModel;
private PlaybackViewModel playbackViewModel;
private ArtistAdapter artistAdapter;
private AlbumAdapter albumAdapter;
@ -61,6 +60,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
bind = FragmentSearchBinding.inflate(inflater, container, false);
View view = bind.getRoot();
searchViewModel = new ViewModelProvider(requireActivity()).get(SearchViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
initSearchResultView();
initSearchView();
@ -73,6 +73,15 @@ public class SearchFragment extends Fragment implements ClickCallback {
public void onStart() {
super.onStart();
initializeMediaBrowser();
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
observePlayback();
}
@Override
public void onResume() {
super.onResume();
if (songHorizontalAdapter != null) setMediaBrowserListenableFuture();
}
@Override
@ -112,7 +121,10 @@ public class SearchFragment extends Fragment implements ClickCallback {
bind.searchResultTracksRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.searchResultTracksRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
setMediaBrowserListenableFuture();
reapplyPlayback();
bind.searchResultTracksRecyclerView.setAdapter(songHorizontalAdapter);
}
@ -242,7 +254,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
}
private boolean isQueryValid(String query) {
return !query.equals("") && query.trim().length() > 2;
return !query.equals("") && query.trim().length() > 1;
}
private void inputFocus() {
@ -260,6 +272,7 @@ public class SearchFragment extends Fragment implements ClickCallback {
@Override
public void onMediaClick(Bundle bundle) {
MediaManager.startQueue(mediaBrowserListenableFuture, bundle.getParcelableArrayList(Constants.TRACKS_OBJECT), bundle.getInt(Constants.ITEM_POSITION));
songHorizontalAdapter.notifyDataSetChanged();
activity.setBottomSheetInPeek(true);
}
@ -287,4 +300,31 @@ public class SearchFragment extends Fragment implements ClickCallback {
public void onArtistLongClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.artistBottomSheetDialog, bundle);
}
private void observePlayback() {
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
if (songHorizontalAdapter != null) {
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
});
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
if (songHorizontalAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
});
}
private void reapplyPlayback() {
if (songHorizontalAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
}
private void setMediaBrowserListenableFuture() {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
}
}

View file

@ -1,13 +1,19 @@
package com.cappielloantonio.tempo.ui.fragment;
import android.app.Activity;
import android.content.Context;
import android.content.ComponentName;
import android.content.Intent;
import android.content.ServiceConnection;
import android.media.audiofx.AudioEffect;
import android.net.Uri;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.widget.Toast;
import androidx.activity.result.ActivityResultLauncher;
import androidx.activity.result.contract.ActivityResultContracts;
@ -18,6 +24,9 @@ import androidx.appcompat.app.AppCompatDelegate;
import androidx.core.os.LocaleListCompat;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.util.UnstableApi;
import androidx.navigation.NavController;
import androidx.navigation.NavOptions;
import androidx.navigation.fragment.NavHostFragment;
import androidx.preference.ListPreference;
import androidx.preference.Preference;
import androidx.preference.PreferenceFragmentCompat;
@ -28,15 +37,19 @@ import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.helper.ThemeHelper;
import com.cappielloantonio.tempo.interfaces.DialogClickCallback;
import com.cappielloantonio.tempo.interfaces.ScanCallback;
import com.cappielloantonio.tempo.service.EqualizerManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.dialog.DeleteDownloadStorageDialog;
import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog;
import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog;
import com.cappielloantonio.tempo.ui.dialog.StarredAlbumSyncDialog;
import com.cappielloantonio.tempo.ui.dialog.StarredArtistSyncDialog;
import com.cappielloantonio.tempo.ui.dialog.StreamingCacheStorageDialog;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.UIUtil;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.viewmodel.SettingViewModel;
import java.util.Locale;
@ -49,15 +62,41 @@ public class SettingsFragment extends PreferenceFragmentCompat {
private MainActivity activity;
private SettingViewModel settingViewModel;
private ActivityResultLauncher<Intent> someActivityResultLauncher;
private ActivityResultLauncher<Intent> equalizerResultLauncher;
private ActivityResultLauncher<Intent> directoryPickerLauncher;
private MediaService.LocalBinder mediaServiceBinder;
private boolean isServiceBound = false;
@Override
public void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
someActivityResultLauncher = registerForActivityResult(
equalizerResultLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {}
);
directoryPickerLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == Activity.RESULT_OK) {
Intent data = result.getData();
if (data != null) {
Uri uri = data.getData();
if (uri != null) {
requireContext().getContentResolver().takePersistableUriPermission(
uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
);
Preferences.setDownloadDirectoryUri(uri.toString());
ExternalAudioReader.refreshCache();
Toast.makeText(requireContext(), "Download folder set.", Toast.LENGTH_SHORT).show();
checkDownloadDirectory();
}
}
}
});
}
@ -86,9 +125,10 @@ public class SettingsFragment extends PreferenceFragmentCompat {
public void onResume() {
super.onResume();
checkEqualizer();
checkSystemEqualizer();
checkCacheStorage();
checkStorage();
checkDownloadDirectory();
setStreamingCacheSize();
setAppLanguage();
@ -98,10 +138,17 @@ public class SettingsFragment extends PreferenceFragmentCompat {
actionScan();
actionSyncStarredAlbums();
actionSyncStarredTracks();
actionSyncStarredArtists();
actionChangeStreamingCacheStorage();
actionChangeDownloadStorage();
actionSetDownloadDirectory();
actionDeleteDownloadStorage();
actionKeepScreenOn();
actionAutoDownloadLyrics();
actionMiniPlayerHeart();
bindMediaService();
actionAppEqualizer();
}
@Override
@ -124,8 +171,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
}
}
private void checkEqualizer() {
Preference equalizer = findPreference("equalizer");
private void checkSystemEqualizer() {
Preference equalizer = findPreference("system_equalizer");
if (equalizer == null) return;
@ -133,7 +180,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
equalizer.setOnPreferenceClickListener(preference -> {
someActivityResultLauncher.launch(intent);
equalizerResultLauncher.launch(intent);
return true;
});
} else {
@ -150,7 +197,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
if (requireContext().getExternalFilesDirs(null)[1] == null) {
storage.setVisible(false);
} else {
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
}
} catch (Exception exception) {
storage.setVisible(false);
@ -166,13 +213,46 @@ public class SettingsFragment extends PreferenceFragmentCompat {
if (requireContext().getExternalFilesDirs(null)[1] == null) {
storage.setVisible(false);
} else {
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
int pref = Preferences.getDownloadStoragePreference();
if (pref == 0) {
storage.setSummary(R.string.download_storage_internal_dialog_negative_button);
} else if (pref == 1) {
storage.setSummary(R.string.download_storage_external_dialog_positive_button);
} else {
storage.setSummary(R.string.download_storage_directory_dialog_neutral_button);
}
}
} catch (Exception exception) {
storage.setVisible(false);
}
}
private void checkDownloadDirectory() {
Preference storage = findPreference("download_storage");
Preference directory = findPreference("set_download_directory");
if (directory == null) return;
String current = Preferences.getDownloadDirectoryUri();
if (current != null) {
if (storage != null) storage.setVisible(false);
directory.setVisible(true);
directory.setIcon(R.drawable.ic_close);
directory.setTitle("Clear download folder");
directory.setSummary(current);
} else {
if (storage != null) storage.setVisible(true);
if (Preferences.getDownloadStoragePreference() == 2) {
directory.setVisible(true);
directory.setIcon(R.drawable.ic_folder);
directory.setTitle("Set download folder");
directory.setSummary("Choose a folder for downloaded music files");
} else {
directory.setVisible(false);
}
}
}
private void setStreamingCacheSize() {
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
@ -281,7 +361,21 @@ public class SettingsFragment extends PreferenceFragmentCompat {
return true;
});
}
private void actionSyncStarredArtists() {
findPreference("sync_starred_artists_for_offline_use").setOnPreferenceChangeListener((preference, newValue) -> {
if (newValue instanceof Boolean) {
if ((Boolean) newValue) {
StarredArtistSyncDialog dialog = new StarredArtistSyncDialog(() -> {
((SwitchPreference)preference).setChecked(false);
});
dialog.show(activity.getSupportFragmentManager(), null);
}
}
return true;
});
}
private void actionChangeStreamingCacheStorage() {
findPreference("streaming_cache_storage").setOnPreferenceClickListener(preference -> {
StreamingCacheStorageDialog dialog = new StreamingCacheStorageDialog(new DialogClickCallback() {
@ -306,11 +400,19 @@ public class SettingsFragment extends PreferenceFragmentCompat {
@Override
public void onPositiveClick() {
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
checkDownloadDirectory();
}
@Override
public void onNegativeClick() {
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
checkDownloadDirectory();
}
@Override
public void onNeutralClick() {
findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button);
checkDownloadDirectory();
}
});
dialog.show(activity.getSupportFragmentManager(), null);
@ -318,6 +420,31 @@ public class SettingsFragment extends PreferenceFragmentCompat {
});
}
private void actionSetDownloadDirectory() {
Preference pref = findPreference("set_download_directory");
if (pref != null) {
pref.setOnPreferenceClickListener(preference -> {
String current = Preferences.getDownloadDirectoryUri();
if (current != null) {
Preferences.setDownloadDirectoryUri(null);
Preferences.setDownloadStoragePreference(0);
ExternalAudioReader.refreshCache();
Toast.makeText(requireContext(), "Download folder cleared.", Toast.LENGTH_SHORT).show();
checkStorage();
checkDownloadDirectory();
} else {
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
| Intent.FLAG_GRANT_READ_URI_PERMISSION
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
directoryPickerLauncher.launch(intent);
}
return true;
});
}
}
private void actionDeleteDownloadStorage() {
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
@ -326,6 +453,36 @@ public class SettingsFragment extends PreferenceFragmentCompat {
});
}
private void actionMiniPlayerHeart() {
SwitchPreference preference = findPreference("mini_shuffle_button_visibility");
if (preference == null) {
return;
}
preference.setChecked(Preferences.showShuffleInsteadOfHeart());
preference.setOnPreferenceChangeListener((pref, newValue) -> {
if (newValue instanceof Boolean) {
Preferences.setShuffleInsteadOfHeart((Boolean) newValue);
}
return true;
});
}
private void actionAutoDownloadLyrics() {
SwitchPreference preference = findPreference("auto_download_lyrics");
if (preference == null) {
return;
}
preference.setChecked(Preferences.isAutoDownloadLyricsEnabled());
preference.setOnPreferenceChangeListener((pref, newValue) -> {
if (newValue instanceof Boolean) {
Preferences.setAutoDownloadLyricsEnabled((Boolean) newValue);
}
return true;
});
}
private void getScanStatus() {
settingViewModel.getScanStatus(new ScanCallback() {
@Override
@ -353,4 +510,63 @@ public class SettingsFragment extends PreferenceFragmentCompat {
return true;
});
}
private final ServiceConnection serviceConnection = new ServiceConnection() {
@Override
public void onServiceConnected(ComponentName name, IBinder service) {
mediaServiceBinder = (MediaService.LocalBinder) service;
isServiceBound = true;
checkEqualizerBands();
}
@Override
public void onServiceDisconnected(ComponentName name) {
mediaServiceBinder = null;
isServiceBound = false;
}
};
private void bindMediaService() {
Intent intent = new Intent(requireActivity(), MediaService.class);
intent.setAction(MediaService.ACTION_BIND_EQUALIZER);
requireActivity().bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
isServiceBound = true;
}
private void checkEqualizerBands() {
if (mediaServiceBinder != null) {
EqualizerManager eqManager = mediaServiceBinder.getEqualizerManager();
short numBands = eqManager.getNumberOfBands();
Preference appEqualizer = findPreference("app_equalizer");
if (appEqualizer != null) {
appEqualizer.setVisible(numBands > 0);
}
}
}
private void actionAppEqualizer() {
Preference appEqualizer = findPreference("app_equalizer");
if (appEqualizer != null) {
appEqualizer.setOnPreferenceClickListener(preference -> {
NavController navController = NavHostFragment.findNavController(this);
NavOptions navOptions = new NavOptions.Builder()
.setLaunchSingleTop(true)
.setPopUpTo(R.id.equalizerFragment, true)
.build();
activity.setBottomNavigationBarVisibility(true);
activity.setBottomSheetVisibility(true);
navController.navigate(R.id.equalizerFragment, null, navOptions);
return true;
});
}
}
@Override
public void onPause() {
super.onPause();
if (isServiceBound) {
requireActivity().unbindService(serviceConnection);
isServiceBound = false;
}
}
}

View file

@ -36,6 +36,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
import com.cappielloantonio.tempo.viewmodel.SongListPageViewModel;
import com.google.common.util.concurrent.ListenableFuture;
@ -49,6 +50,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
private FragmentSongListPageBinding bind;
private MainActivity activity;
private SongListPageViewModel songListPageViewModel;
private PlaybackViewModel playbackViewModel;
private SongHorizontalAdapter songHorizontalAdapter;
@ -69,6 +71,7 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
bind = FragmentSongListPageBinding.inflate(inflater, container, false);
View view = bind.getRoot();
songListPageViewModel = new ViewModelProvider(requireActivity()).get(SongListPageViewModel.class);
playbackViewModel = new ViewModelProvider(requireActivity()).get(PlaybackViewModel.class);
init();
initAppBar();
@ -82,6 +85,15 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
public void onStart() {
super.onStart();
initializeMediaBrowser();
MediaManager.registerPlaybackObserver(mediaBrowserListenableFuture, playbackViewModel);
observePlayback();
}
@Override
public void onResume() {
super.onResume();
setMediaBrowserListenableFuture();
}
@Override
@ -189,11 +201,14 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
bind.songListRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.songListRecyclerView.setHasFixedSize(true);
songHorizontalAdapter = new SongHorizontalAdapter(this, true, false, null);
songHorizontalAdapter = new SongHorizontalAdapter(getViewLifecycleOwner(), this, true, false, null);
bind.songListRecyclerView.setAdapter(songHorizontalAdapter);
setMediaBrowserListenableFuture();
reapplyPlayback();
songListPageViewModel.getSongList().observe(getViewLifecycleOwner(), songs -> {
isLoading = false;
songHorizontalAdapter.setItems(songs);
reapplyPlayback();
setSongListPageSubtitle(songs);
});
@ -325,4 +340,31 @@ public class SongListPageFragment extends Fragment implements ClickCallback {
public void onMediaLongClick(Bundle bundle) {
Navigation.findNavController(requireView()).navigate(R.id.songBottomSheetDialog, bundle);
}
private void observePlayback() {
playbackViewModel.getCurrentSongId().observe(getViewLifecycleOwner(), id -> {
if (songHorizontalAdapter != null) {
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
});
playbackViewModel.getIsPlaying().observe(getViewLifecycleOwner(), playing -> {
if (songHorizontalAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
});
}
private void reapplyPlayback() {
if (songHorizontalAdapter != null) {
String id = playbackViewModel.getCurrentSongId().getValue();
Boolean playing = playbackViewModel.getIsPlaying().getValue();
songHorizontalAdapter.setPlaybackState(id, playing != null && playing);
}
}
private void setMediaBrowserListenableFuture() {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
}
}

View file

@ -13,6 +13,7 @@ import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.MediaItem;
@ -37,6 +38,8 @@ import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel;
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
@ -54,6 +57,10 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
private AlbumBottomSheetViewModel albumBottomSheetViewModel;
private AlbumID3 album;
private TextView removeAllTextView;
private List<Child> currentAlbumTracks = Collections.emptyList();
private List<MediaItem> currentAlbumMediaItems = Collections.emptyList();
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@Nullable
@ -72,6 +79,12 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateRemoveAllVisibility);
}
@Override
public void onStart() {
super.onStart();
@ -163,7 +176,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
downloadAll.setOnClickListener(v -> {
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
} else {
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
}
dismissBottomSheet();
});
});
@ -182,19 +199,23 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
});
});
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
removeAllTextView = view.findViewById(R.id.remove_all_text_view);
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
currentAlbumTracks = songs != null ? songs : Collections.emptyList();
currentAlbumMediaItems = MappingUtil.mapDownloads(currentAlbumTracks);
removeAll.setOnClickListener(v -> {
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
removeAllTextView.setOnClickListener(v -> {
if (Preferences.getDownloadDirectoryUri() == null) {
List<Download> downloads = currentAlbumTracks.stream().map(Download::new).collect(Collectors.toList());
DownloadUtil.getDownloadTracker(requireContext()).remove(currentAlbumMediaItems, downloads);
} else {
currentAlbumTracks.forEach(ExternalAudioReader::delete);
}
dismissBottomSheet();
});
updateRemoveAllVisibility();
});
initDownloadUI(removeAll);
TextView goToArtist = view.findViewById(R.id.go_to_artist_text_view);
goToArtist.setOnClickListener(v -> albumBottomSheetViewModel.getArtist().observe(getViewLifecycleOwner(), artist -> {
if (artist != null) {
@ -234,14 +255,29 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
dismiss();
}
private void initDownloadUI(TextView removeAll) {
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
private void updateRemoveAllVisibility() {
if (removeAllTextView == null) {
return;
}
if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
removeAll.setVisibility(View.VISIBLE);
if (currentAlbumTracks == null || currentAlbumTracks.isEmpty()) {
removeAllTextView.setVisibility(View.GONE);
return;
}
if (Preferences.getDownloadDirectoryUri() == null) {
List<MediaItem> mediaItems = currentAlbumMediaItems;
if (mediaItems == null || mediaItems.isEmpty()) {
removeAllTextView.setVisibility(View.GONE);
} else if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
removeAllTextView.setVisibility(View.VISIBLE);
} else {
removeAllTextView.setVisibility(View.GONE);
}
});
} else {
boolean hasLocal = currentAlbumTracks.stream().anyMatch(song -> ExternalAudioReader.getUri(song) != null);
removeAllTextView.setVisibility(hasLocal ? View.VISIBLE : View.GONE);
}
}
private void initializeMediaBrowser() {

View file

@ -66,7 +66,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
super.onStop();
}
// TODO Utilizzare il viewmodel come tramite ed evitare le chiamate dirette
// TODO Use the viewmodel as a conduit and avoid direct calls
private void init(View view) {
ImageView coverArtist = view.findViewById(R.id.artist_cover_image_view);
CustomGlideRequest.Builder
@ -81,7 +81,7 @@ public class ArtistBottomSheetDialog extends BottomSheetDialogFragment implement
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
favoriteToggle.setChecked(artistBottomSheetViewModel.getArtist().getStarred() != null);
favoriteToggle.setOnClickListener(v -> {
artistBottomSheetViewModel.setFavorite();
artistBottomSheetViewModel.setFavorite(requireContext());
});
TextView playRadio = view.findViewById(R.id.play_radio_text_view);

View file

@ -25,6 +25,8 @@ import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.common.util.concurrent.ListenableFuture;
@ -117,10 +119,13 @@ public class DownloadedBottomSheetDialog extends BottomSheetDialogFragment imple
TextView removeAll = view.findViewById(R.id.remove_all_text_view);
removeAll.setOnClickListener(v -> {
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
if (Preferences.getDownloadDirectoryUri() == null) {
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
} else {
songs.forEach(ExternalAudioReader::delete);
}
dismissBottomSheet();
});

View file

@ -13,6 +13,7 @@ import android.widget.TextView;
import android.widget.Toast;
import android.widget.ToggleButton;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.util.UnstableApi;
@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
@ -39,6 +41,10 @@ import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.common.util.concurrent.ListenableFuture;
import android.content.Intent;
import androidx.media3.common.MediaItem;
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
import java.util.ArrayList;
import java.util.Collections;
@ -48,6 +54,9 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
private SongBottomSheetViewModel songBottomSheetViewModel;
private Child song;
private TextView downloadButton;
private TextView removeButton;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@Nullable
@ -66,6 +75,12 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
MappingUtil.observeExternalAudioRefresh(getViewLifecycleOwner(), this::updateDownloadButtons);
}
@Override
public void onStart() {
super.onStart();
@ -157,25 +172,33 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
dismissBottomSheet();
});
TextView download = view.findViewById(R.id.download_text_view);
download.setOnClickListener(v -> {
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownload(song),
new Download(song)
);
downloadButton = view.findViewById(R.id.download_text_view);
downloadButton.setOnClickListener(v -> {
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownload(song),
new Download(song)
);
} else {
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
}
dismissBottomSheet();
});
TextView remove = view.findViewById(R.id.remove_text_view);
remove.setOnClickListener(v -> {
DownloadUtil.getDownloadTracker(requireContext()).remove(
MappingUtil.mapDownload(song),
new Download(song)
);
removeButton = view.findViewById(R.id.remove_text_view);
removeButton.setOnClickListener(v -> {
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).remove(
MappingUtil.mapDownload(song),
new Download(song)
);
} else {
ExternalAudioReader.delete(song);
}
dismissBottomSheet();
});
initDownloadUI(download, remove);
updateDownloadButtons();
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
addToPlaylist.setOnClickListener(v -> {
@ -243,12 +266,19 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
dismiss();
}
private void initDownloadUI(TextView download, TextView remove) {
if (DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId())) {
remove.setVisibility(View.VISIBLE);
private void updateDownloadButtons() {
if (downloadButton == null || removeButton == null) {
return;
}
if (Preferences.getDownloadDirectoryUri() == null) {
boolean downloaded = DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId());
downloadButton.setVisibility(downloaded ? View.GONE : View.VISIBLE);
removeButton.setVisibility(downloaded ? View.VISIBLE : View.GONE);
} else {
download.setVisibility(View.VISIBLE);
remove.setVisibility(View.GONE);
boolean hasLocal = ExternalAudioReader.getUri(song) != null;
downloadButton.setVisibility(hasLocal ? View.GONE : View.VISIBLE);
removeButton.setVisibility(hasLocal ? View.VISIBLE : View.GONE);
}
}