fix: Prevent externalAudioReader from hogging the main thread

This commit is contained in:
le-firehawk 2025-10-04 23:02:12 +09:30
parent 1357c5c062
commit 620fba0a14
12 changed files with 242 additions and 90 deletions

View file

@ -11,6 +11,7 @@ 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;
@ -25,6 +26,7 @@ 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;
@ -90,7 +92,7 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
}
};
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;
@ -99,6 +101,10 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
this.currentFilter = "";
this.album = album;
setHasStableIds(false);
if (lifecycleOwner != null) {
MappingUtil.observeExternalAudioRefresh(lifecycleOwner, this::handleExternalAudioRefresh);
}
}
@NonNull
@ -204,6 +210,12 @@ public class SongHorizontalAdapter extends RecyclerView.Adapter<SongHorizontalAd
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());

View file

@ -289,7 +289,7 @@ 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();

View file

@ -178,7 +178,7 @@ 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();

View file

@ -600,7 +600,7 @@ 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();
@ -641,7 +641,7 @@ 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();

View file

@ -264,7 +264,7 @@ 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();

View file

@ -121,7 +121,7 @@ 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();

View file

@ -201,7 +201,7 @@ 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();

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;
@ -56,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
@ -74,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();
@ -188,23 +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 -> {
removeAllTextView.setOnClickListener(v -> {
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
List<Download> downloads = currentAlbumTracks.stream().map(Download::new).collect(Collectors.toList());
DownloadUtil.getDownloadTracker(requireContext()).remove(currentAlbumMediaItems, downloads);
} else {
songs.forEach(ExternalAudioReader::delete);
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) {
@ -244,21 +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 (Preferences.getDownloadDirectoryUri() == null) {
if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
removeAll.setVisibility(View.VISIBLE);
} else {
removeAll.setVisibility(View.GONE);
}
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 {
boolean hasLocal = songs.stream().anyMatch(song -> ExternalAudioReader.getUri(song) != null);
removeAll.setVisibility(hasLocal ? View.VISIBLE : View.GONE);
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

@ -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;
@ -53,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
@ -71,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();
@ -162,8 +172,8 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
dismissBottomSheet();
});
TextView download = view.findViewById(R.id.download_text_view);
download.setOnClickListener(v -> {
downloadButton = view.findViewById(R.id.download_text_view);
downloadButton.setOnClickListener(v -> {
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownload(song),
@ -175,8 +185,8 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
dismissBottomSheet();
});
TextView remove = view.findViewById(R.id.remove_text_view);
remove.setOnClickListener(v -> {
removeButton = view.findViewById(R.id.remove_text_view);
removeButton.setOnClickListener(v -> {
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).remove(
MappingUtil.mapDownload(song),
@ -188,7 +198,7 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
dismissBottomSheet();
});
initDownloadUI(download, remove);
updateDownloadButtons();
TextView addToPlaylist = view.findViewById(R.id.add_to_playlist_text_view);
addToPlaylist.setOnClickListener(v -> {
@ -256,21 +266,19 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
dismiss();
}
private void initDownloadUI(TextView download, TextView remove) {
private void updateDownloadButtons() {
if (downloadButton == null || removeButton == null) {
return;
}
if (Preferences.getDownloadDirectoryUri() == null) {
if (DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId())) {
remove.setVisibility(View.VISIBLE);
} else {
download.setVisibility(View.VISIBLE);
remove.setVisibility(View.GONE);
}
boolean downloaded = DownloadUtil.getDownloadTracker(requireContext()).isDownloaded(song.getId());
downloadButton.setVisibility(downloaded ? View.GONE : View.VISIBLE);
removeButton.setVisibility(downloaded ? View.VISIBLE : View.GONE);
} else {
if (ExternalAudioReader.getUri(song) != null) {
remove.setVisibility(View.VISIBLE);
} 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);
}
}