Skip to content
Draft
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.cappielloantonio.tempo.factory;

import androidx.annotation.NonNull;
import androidx.lifecycle.ViewModel;
import androidx.lifecycle.ViewModelProvider;

import com.cappielloantonio.tempo.repository.CoverRepository;
import com.cappielloantonio.tempo.viewmodel.CoverFlowViewModel;

public class CoverFlowViewModelFactory implements ViewModelProvider.Factory {

private final CoverRepository repository;

public CoverFlowViewModelFactory(@NonNull CoverRepository repository) {
this.repository = repository;
}

@NonNull
@Override
public <T extends ViewModel> T create(@NonNull Class<T> modelClass) {
if (modelClass.isAssignableFrom(CoverFlowViewModel.class)) {
//noinspection unchecked
return (T) new CoverFlowViewModel(repository);
}
throw new IllegalArgumentException("Unknown ViewModel class");
}
}

25 changes: 25 additions & 0 deletions app/src/main/java/com/cappielloantonio/tempo/model/Cover.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package com.cappielloantonio.tempo.model;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

public class Cover {
private final String url; // image to show in the flow
private final String coverArtId; // id used by the CustomGlideRequest (may be null)

public Cover(@NonNull String url, @Nullable String coverArtId) {
this.url = url;
this.coverArtId = coverArtId;
}

@NonNull
public String getUrl() {
return url;
}

/** Returns the id that the CustomGlideRequest needs – can be null. */
@Nullable
public String getCoverArtId() {
return coverArtId;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.cappielloantonio.tempo.repository;

import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.media3.common.MediaMetadata;
import androidx.media3.session.MediaBrowser;

import com.cappielloantonio.tempo.model.Cover;
import com.google.common.util.concurrent.ListenableFuture;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

public interface CoverRepository {
/** Returns a list of covers. Call should be made off the UI thread. */
@WorkerThread
List<Cover> getCovers() throws Exception;
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.cappielloantonio.tempo.repository;

import androidx.annotation.NonNull;
import androidx.annotation.WorkerThread;
import androidx.media3.common.MediaMetadata;
import androidx.media3.session.MediaBrowser;

import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Cover;
import com.google.common.util.concurrent.ListenableFuture;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List; /** Example implementation that extracts data from a MediaBrowser */
public class MediaBrowserCoverRepository implements CoverRepository {

private final ListenableFuture<MediaBrowser> mediaBrowserFuture;

public MediaBrowserCoverRepository(@NonNull ListenableFuture<MediaBrowser> mediaBrowserFuture) {
this.mediaBrowserFuture = mediaBrowserFuture;
}

@Override
@WorkerThread
public List<Cover> getCovers() throws Exception {
MediaBrowser mediaBrowser = mediaBrowserFuture.get(); // blocks only inside a background thread
MediaMetadata metadata = mediaBrowser.getMediaMetadata();
Comment on lines +26 to +27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Add a bounded wait when resolving mediaBrowserFuture.

Line 26 uses mediaBrowserFuture.get() with no timeout; if the future never resolves, cover loading can hang indefinitely and never post data.

Suggested fix
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
...
-        MediaBrowser mediaBrowser = mediaBrowserFuture.get();   // blocks only inside a background thread
+        MediaBrowser mediaBrowser;
+        try {
+            mediaBrowser = mediaBrowserFuture.get(5, TimeUnit.SECONDS);
+        } catch (TimeoutException e) {
+            throw new IllegalStateException("Timed out waiting for MediaBrowser", e);
+        }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
MediaBrowser mediaBrowser = mediaBrowserFuture.get(); // blocks only inside a background thread
MediaMetadata metadata = mediaBrowser.getMediaMetadata();
MediaBrowser mediaBrowser;
try {
mediaBrowser = mediaBrowserFuture.get(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
throw new IllegalStateException("Timed out waiting for MediaBrowser", e);
}
MediaMetadata metadata = mediaBrowser.getMediaMetadata();
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/cappielloantonio/tempo/repository/MediaBrowserCoverRepository.java`
around lines 26 - 27, The mediaBrowserFuture.get() call in the media browser
cover loading code has no timeout specified, which can cause the application to
hang indefinitely if the future never completes. Replace the unbounded
mediaBrowserFuture.get() call with the overloaded version that accepts a timeout
duration and time unit parameters (e.g., get with a reasonable timeout like 5 or
10 seconds) to ensure that if the future doesn't resolve within that period, a
TimeoutException is thrown and the cover loading can fail gracefully instead of
hanging.


// Inject this here, somehow, since it grabs the covertArtId
/*`
CustomGlideRequest.Builder
.from(requireContext(), metadata.extras.getString("coverArtId"), CustomGlideRequest.ResourceType.Song)
.build()
.into(bind.playerHeaderLayout.playerHeaderMediaCoverImage);

*/
// -----------------------------------------------------------------
// Replace the below with the real extraction logic from metadata.
// For demonstration we just return the three dog‑image URLs.
// -----------------------------------------------------------------
List<String> urls = Arrays.asList(
"https://images.dog.ceo/breeds/affenpinscher/n02110627_11858.jpg",
"https://images.dog.ceo/breeds/hound-english/n02089973_811.jpg",
"https://images.dog.ceo/breeds/shiba/shiba-14.jpg"
);

List<Cover> covers = new ArrayList<>();
for (String url : urls) {
covers.add(new Cover(url, null)); // coverArtId can be filled later if needed
}
return covers;
Comment on lines +27 to +51

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Repository currently bypasses the playback source, so Cover Flow cannot sync with queue state.

At Line 27 metadata is retrieved, but Lines 41-50 return a static dog URL list instead of mapping the actual playback/queue data. This breaks the intended data contract for end-to-end queue synchronization.

Direction for fix
-        MediaMetadata metadata = mediaBrowser.getMediaMetadata();
-        ...
-        List<String> urls = Arrays.asList(...);
-        List<Cover> covers = new ArrayList<>();
-        for (String url : urls) {
-            covers.add(new Cover(url, null));
-        }
-        return covers;
+        // Build covers from the active queue/media items so RecyclerView state
+        // and playback queue represent the same ordering/source of truth.
+        // (Use media item art URI and/or coverArtId extracted from metadata extras.)
+        return extractCoversFromQueue(mediaBrowser);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/cappielloantonio/tempo/repository/MediaBrowserCoverRepository.java`
around lines 27 - 51, The method retrieves metadata at the beginning but then
ignores it, instead returning a hardcoded list of dog image URLs in the covers
loop. Replace the static URL list creation (the Arrays.asList with three dog
image URLs) with logic that extracts the actual coverArtId from the metadata
object using metadata.extras.getString("coverArtId") and creates Cover objects
based on real playback data instead of placeholder URLs. This will ensure the
repository returns actual cover art data synchronized with the queue state.

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.cappielloantonio.tempo.ui.adapter;

import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ImageView;

import androidx.recyclerview.widget.RecyclerView;

import com.bumptech.glide.Glide;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.model.Cover;

import org.jspecify.annotations.NonNull;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verify whether JSpecify is explicitly declared and how broadly it's used.
set -euo pipefail

echo "== Build files mentioning jspecify =="
rg -n --iglob '*gradle*' 'jspecify|org\.jspecify'

echo
echo "== Java/Kotlin files importing org.jspecify.annotations.NonNull =="
rg -n 'import\s+org\.jspecify\.annotations\.NonNull'

echo
echo "== Java/Kotlin files importing androidx.annotation.NonNull =="
rg -n 'import\s+androidx\.annotation\.NonNull'

Repository: eddyizm/tempus

Length of output: 96


🏁 Script executed:

# Check if the file exists and read its imports
fd -n 'CoverFlowAdapter.java' --exec head -80 {}

echo "=== Checking for build files ==="
fd -t f -e gradle -e xml | grep -E '(build|pom)' | head -20

Repository: eddyizm/tempus

Length of output: 370


🏁 Script executed:

# More comprehensive search for jspecify in the entire codebase
echo "== All files with jspecify anywhere in content =="
rg 'jspecify' -i

echo
echo "== All imports of NonNull annotations =="
rg 'import.*NonNull' -A 1

Repository: eddyizm/tempus

Length of output: 45111


🏁 Script executed:

# Check main build.gradle files for dependencies
echo "=== app/build.gradle ==="
cat app/build.gradle | grep -A 100 "dependencies"

echo
echo "=== Root build.gradle ==="
head -100 build.gradle

Repository: eddyizm/tempus

Length of output: 3326


🏁 Script executed:

# Check if jspecify appears anywhere in gradle files (even if commented)
echo "=== Any mention of jspecify in all gradle files ==="
rg 'jspecify' -i app/build.gradle build.gradle gradle.properties settings.gradle 2>/dev/null || echo "No jspecify found"

# Also check if CoverFlowAdapter.java and UIUtil.java are the ONLY files with jspecify imports
echo
echo "=== All lines mentioning jspecify in source files ==="
rg 'org\.jspecify' --type java

Repository: eddyizm/tempus

Length of output: 405


Replace org.jspecify.annotations.NonNull with androidx.annotation.NonNull for consistency.

JSpecify is not declared as a dependency in the build configuration. This adapter and UIUtil.java are the only two files in the codebase using JSpecify annotations; all other files use AndroidX annotations. The inconsistency should be resolved by standardizing on androidx.annotation.NonNull to match the rest of the project and ensure the nullness checking toolchain is properly aligned.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CoverFlowAdapter.java`
at line 15, The import statement in CoverFlowAdapter.java is using
org.jspecify.annotations.NonNull, but JSpecify is not a declared dependency and
this creates inconsistency with the rest of the codebase which uses
androidx.annotation.NonNull. Replace the import statement at line 15 that
currently imports from org.jspecify.annotations with an import from
androidx.annotation.NonNull to align with the project's nullness checking
toolchain and match other files like UIUtil.java.


import java.util.List;

public class CoverFlowAdapter extends RecyclerView.Adapter<CoverFlowAdapter.CoverViewHolder> {

/** Callback that is invoked for every bound item – useful for the extra Glide request. */
public interface OnCoverBoundListener {
void onCoverBound(@NonNull Cover cover, @NonNull ImageView coverImage);
}

private final Context context;
private final List<Cover> covers;
private final OnCoverBoundListener boundListener;

public CoverFlowAdapter(@NonNull Context context,
@NonNull List<Cover> covers,
@NonNull OnCoverBoundListener boundListener) {
this.context = context;
this.covers = covers;
this.boundListener = boundListener;
}

@NonNull
@Override
public CoverViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
// Inflate a simple item layout that contains only an ImageView.
View view = LayoutInflater.from(context).inflate(R.layout.item_cover_flow, parent, false);
return new CoverViewHolder(view);
}

@Override
public void onBindViewHolder(@NonNull CoverViewHolder holder, int position) {
Cover cover = covers.get(position);
holder.bind(cover);
// Let the fragment (or caller) run the extra CustomGlideRequest if it needs the id.
boundListener.onCoverBound(cover, holder.coverImage);
}
Comment on lines +47 to +52

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Avoid issuing two image loads for the same bind target.

onBindViewHolder() loads cover.getUrl() (via bind) and then triggers a second request through boundListener for items with coverArtId. This doubles work and can cause image flicker/race effects on recycled rows.

Suggested fix
-        Cover cover = covers.get(position);
-        holder.bind(cover);
-        // Let the fragment (or caller) run the extra CustomGlideRequest if it needs the id.
-        boundListener.onCoverBound(cover, holder.coverImage);
+        Cover cover = covers.get(position);
+        // Use one source-of-truth load path per item.
+        if (cover.getCoverArtId() != null) {
+            boundListener.onCoverBound(cover, holder.coverImage);
+        } else {
+            holder.bind(cover);
+        }

Also applies to: 70-75

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/cappielloantonio/tempo/ui/adapter/CoverFlowAdapter.java`
around lines 47 - 52, The onBindViewHolder method in CoverFlowAdapter is
triggering two separate image load requests for the same ImageView: one inside
holder.bind(cover) which loads cover.getUrl(), and another through
boundListener.onCoverBound(cover, holder.coverImage) for items with coverArtId.
To fix this, refactor the code to use a single image loading path. Either pass
the necessary coverArtId information to the bind method so it can make a single
optimized request, or conditionally call boundListener only when a custom image
request is specifically needed (avoiding the automatic load in bind). Apply this
same consolidation pattern to all occurrences of this binding logic including
the location around lines 70-75.


@Override
public int getItemCount() {
return covers.size();
}

/** -----------------------------------------------------------------
* ViewHolder – each item holds a single ImageView.
* ----------------------------------------------------------------- */
class CoverViewHolder extends RecyclerView.ViewHolder {
final ImageView coverImage;

CoverViewHolder(@NonNull View itemView) {
super(itemView);
coverImage = itemView.findViewById(R.id.item_cover_image);
}

void bind(@NonNull Cover cover) {
// Normal image loading (the flow thumbnails)
Glide.with(context)
.load(cover.getUrl())
.centerCrop()
.into(coverImage);
}
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.appcompat.widget.PopupMenu;
import androidx.constraintlayout.widget.ConstraintLayout;
import androidx.fragment.app.Fragment;
Expand All @@ -36,6 +37,8 @@
import androidx.navigation.NavController;
import androidx.navigation.NavOptions;
import androidx.navigation.fragment.NavHostFragment;
import androidx.recyclerview.widget.LinearLayoutManager;
import androidx.recyclerview.widget.RecyclerView;
import androidx.transition.ChangeBounds;
import androidx.transition.Slide;
import androidx.transition.TransitionManager;
Expand All @@ -44,9 +47,15 @@

import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.InnerFragmentPlayerControllerBinding;
import com.cappielloantonio.tempo.factory.CoverFlowViewModelFactory;
import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.model.Cover;
import com.cappielloantonio.tempo.repository.CoverRepository;
import com.cappielloantonio.tempo.repository.MediaBrowserCoverRepository;
import com.cappielloantonio.tempo.equalizer.EqualizerManager;
import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.CoverFlowAdapter;
import com.cappielloantonio.tempo.ui.dialog.PlaybackSpeedDialog;
import com.cappielloantonio.tempo.ui.dialog.SleepTimerDialog;
import com.cappielloantonio.tempo.util.SleepTimerManager;
Expand All @@ -60,6 +69,8 @@
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.UIUtil;
import com.cappielloantonio.tempo.viewmodel.CoverFlowViewModel;
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
import com.cappielloantonio.tempo.viewmodel.RatingViewModel;
import com.google.android.material.chip.Chip;
Expand All @@ -71,6 +82,7 @@
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Objects;

Expand Down Expand Up @@ -99,6 +111,8 @@ public class PlayerControllerFragment extends Fragment {
private LinearLayout sleepTimerContainer;
private ImageButton sleepTimerButton;
private android.widget.TextView sleepTimerLabel;

private RecyclerView playerCoverFlow;
private ChipGroup assetLinkChipGroup;
private Chip playerSongLinkChip;
private Chip playerAlbumLinkChip;
Expand Down Expand Up @@ -136,6 +150,78 @@ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
return view;
}

private CoverFlowViewModel viewModel;

// public CoverFlowFragment() {
// super(R.layout.inner_fragment_player_controller_layout); // your layout that contains the RecyclerView
// }

@Override
public void onViewCreated(@NonNull View root, @Nullable Bundle savedInstanceState) {
super.onViewCreated(root, savedInstanceState);

// -----------------------------------------------------------------
// 1️⃣ Find the RecyclerView (the id changed to player_cover_flow)
// -----------------------------------------------------------------
playerCoverFlow = root.findViewById(R.id.player_cover_flow);

// -----------------------------------------------------------------
// 2️⃣ Set up the ViewModel (repository injection omitted for brevity)
// -----------------------------------------------------------------
// CoverRepository repository = new MediaBrowserCoverRepository(mediaBrowserListenableFuture);
// viewModel = new ViewModelProvider(this,
// new CoverFlowViewModelFactory(repository))
// .get(CoverFlowViewModel.class);
CoverRepository repository = new MediaBrowserCoverRepository(mediaBrowserListenableFuture);
CoverFlowViewModelFactory factory = new CoverFlowViewModelFactory(repository);

viewModel = new ViewModelProvider(this, factory)
.get(CoverFlowViewModel.class);
Comment on lines +175 to +179

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Initialize mediaBrowserListenableFuture before building the repository.

At Line 175, MediaBrowserCoverRepository is created before initializeBrowser() runs (Line 320), so the repository can capture a null future. When repository-backed loading is enabled, this is a crash path.

Also applies to: 319-321

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java`
around lines 175 - 179, The MediaBrowserCoverRepository is being instantiated
before mediaBrowserListenableFuture is initialized, which causes it to receive a
null value that will crash when used. Move the initialization of
mediaBrowserListenableFuture to occur before the MediaBrowserCoverRepository is
created in the constructor, or defer the CoverFlowViewModel creation (the
ViewModelProvider.get(CoverFlowViewModel.class) call) until after the
initializeBrowser() method has been called at line 320, ensuring the
mediaBrowserListenableFuture is properly initialized before being passed to the
MediaBrowserCoverRepository constructor.


// -----------------------------------------------------------------
// 3️⃣ Observe the LiveData and build the UI when data arrives
// -----------------------------------------------------------------
viewModel.getCovers().observe(getViewLifecycleOwner(), this::setupCoverFlow);
}

/** Called once the list of covers is available */
private void setupCoverFlow(@NonNull List<Cover> covers) {
if (covers.isEmpty()) return; // optionally show an empty‑state view

// -----------------------------------------------------------------
// 4️⃣ Create the adapter – the lambda receives the Cover object
// and the ImageView that just got bound.
// -----------------------------------------------------------------
CoverFlowAdapter adapter = new CoverFlowAdapter(
requireContext(),
covers,
(cover, imageView) -> {
// Run the *extra* Glide request only when we have a valid id.
String coverArtId = cover.getCoverArtId();
if (coverArtId != null) {
CustomGlideRequest.Builder
.from(requireContext(),
coverArtId,
CustomGlideRequest.ResourceType.Song)
.build()
.into(imageView); // load into the same thumbnail ImageView
}
});

// -----------------------------------------------------------------
// 5️⃣ RecyclerView basics (same as your original code)
// -----------------------------------------------------------------
playerCoverFlow.setAdapter(adapter);
playerCoverFlow.setLayoutManager(
new LinearLayoutManager(requireContext(),
LinearLayoutManager.HORIZONTAL,
false));

playerCoverFlow.addItemDecoration(UIUtil.horizontalSpacing(32));
playerCoverFlow.addOnScrollListener(UIUtil.scaleOnScroll());
UIUtil.centerAndSnapRecyclerView(playerCoverFlow);
}
Comment on lines +188 to +223

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

find . -name "PlayerControllerFragment.java" -type f

Repository: eddyizm/tempus

Length of output: 147


🏁 Script executed:

rg "setupCoverFlow" -B 5 -A 15

Repository: eddyizm/tempus

Length of output: 3535


🏁 Script executed:

rg "playerCoverFlow" --max-count 20 -B 2 -A 2

Repository: eddyizm/tempus

Length of output: 5242


🏁 Script executed:

cat -n app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java | head -250

Repository: eddyizm/tempus

Length of output: 12624


🏁 Script executed:

rg "UIUtil\.(centerAndSnapRecyclerView|horizontalSpacing|scaleOnScroll)" -A 10

Repository: eddyizm/tempus

Length of output: 1545


🏁 Script executed:

find . -name "UIUtil.java" -type f

Repository: eddyizm/tempus

Length of output: 122


🏁 Script executed:

rg "class UIUtil|fun centerAndSnapRecyclerView|fun horizontalSpacing|fun scaleOnScroll" -A 20

Repository: eddyizm/tempus

Length of output: 2162


🏁 Script executed:

rg "centerAndSnapRecyclerView|horizontalSpacing|scaleOnScroll" -B 2 -A 15 app/src/main/java/com/cappielloantonio/tempo/util/UIUtil.java

Repository: eddyizm/tempus

Length of output: 1603


🏁 Script executed:

rg "centerAndSnapRecyclerView" -A 40 app/src/main/java/com/cappielloantonio/tempo/util/UIUtil.java

Repository: eddyizm/tempus

Length of output: 1132


Make setupCoverFlow idempotent—gating decoration, listener, and snap helper setup to run only once.

Lines 220–222 execute on every LiveData emission, accumulating item decorations, scroll listeners, and reattaching the snap helper. Each call to addItemDecoration() and addOnScrollListener() stacks additional instances, and UIUtil.centerAndSnapRecyclerView() creates and reattaches a new PagerSnapHelper each time, which is wasteful and can lead to state conflicts on repeated data updates. Additionally, line 189 returns early on empty data without clearing the UI, leaving stale covers visible.

Guard the decoration, listener, and snap setup with a flag to run only on first initialization, while updating the adapter on each call:

+ private boolean coverFlowInitialized = false;

 private void setupCoverFlow(`@NonNull` List<Cover> covers) {
-    if (covers.isEmpty()) return;   // optionally show an empty‑state view
+    if (covers.isEmpty()) {
+        playerCoverFlow.setAdapter(null);
+        playerCoverFlow.setVisibility(View.GONE);
+        return;
+    }
+    playerCoverFlow.setVisibility(View.VISIBLE);

     CoverFlowAdapter adapter = new CoverFlowAdapter(
         requireContext(),
         covers,
         (cover, imageView) -> {
             String coverArtId = cover.getCoverArtId();
             if (coverArtId != null) {
                 CustomGlideRequest.Builder
                     .from(requireContext(), coverArtId, CustomGlideRequest.ResourceType.Song)
                     .build()
                     .into(imageView);
             }
         });

     playerCoverFlow.setAdapter(adapter);
-    playerCoverFlow.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
-    playerCoverFlow.addItemDecoration(UIUtil.horizontalSpacing(32));
-    playerCoverFlow.addOnScrollListener(UIUtil.scaleOnScroll());
-    UIUtil.centerAndSnapRecyclerView(playerCoverFlow);
+    if (!coverFlowInitialized) {
+        playerCoverFlow.setLayoutManager(new LinearLayoutManager(requireContext(), LinearLayoutManager.HORIZONTAL, false));
+        playerCoverFlow.addItemDecoration(UIUtil.horizontalSpacing(32));
+        playerCoverFlow.addOnScrollListener(UIUtil.scaleOnScroll());
+        UIUtil.centerAndSnapRecyclerView(playerCoverFlow);
+        coverFlowInitialized = true;
+    }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private void setupCoverFlow(@NonNull List<Cover> covers) {
if (covers.isEmpty()) return; // optionally show an empty‑state view
// -----------------------------------------------------------------
// 4️⃣ Create the adapter – the lambda receives the Cover object
// and the ImageView that just got bound.
// -----------------------------------------------------------------
CoverFlowAdapter adapter = new CoverFlowAdapter(
requireContext(),
covers,
(cover, imageView) -> {
// Run the *extra* Glide request only when we have a valid id.
String coverArtId = cover.getCoverArtId();
if (coverArtId != null) {
CustomGlideRequest.Builder
.from(requireContext(),
coverArtId,
CustomGlideRequest.ResourceType.Song)
.build()
.into(imageView); // load into the same thumbnail ImageView
}
});
// -----------------------------------------------------------------
// 5️⃣ RecyclerView basics (same as your original code)
// -----------------------------------------------------------------
playerCoverFlow.setAdapter(adapter);
playerCoverFlow.setLayoutManager(
new LinearLayoutManager(requireContext(),
LinearLayoutManager.HORIZONTAL,
false));
playerCoverFlow.addItemDecoration(UIUtil.horizontalSpacing(32));
playerCoverFlow.addOnScrollListener(UIUtil.scaleOnScroll());
UIUtil.centerAndSnapRecyclerView(playerCoverFlow);
}
private boolean coverFlowInitialized = false;
private void setupCoverFlow(`@NonNull` List<Cover> covers) {
if (covers.isEmpty()) {
playerCoverFlow.setAdapter(null);
playerCoverFlow.setVisibility(View.GONE);
return;
}
playerCoverFlow.setVisibility(View.VISIBLE);
// -----------------------------------------------------------------
// 4️⃣ Create the adapter – the lambda receives the Cover object
// and the ImageView that just got bound.
// -----------------------------------------------------------------
CoverFlowAdapter adapter = new CoverFlowAdapter(
requireContext(),
covers,
(cover, imageView) -> {
// Run the *extra* Glide request only when we have a valid id.
String coverArtId = cover.getCoverArtId();
if (coverArtId != null) {
CustomGlideRequest.Builder
.from(requireContext(),
coverArtId,
CustomGlideRequest.ResourceType.Song)
.build()
.into(imageView); // load into the same thumbnail ImageView
}
});
// -----------------------------------------------------------------
// 5️⃣ RecyclerView basics (same as your original code)
// -----------------------------------------------------------------
playerCoverFlow.setAdapter(adapter);
if (!coverFlowInitialized) {
playerCoverFlow.setLayoutManager(
new LinearLayoutManager(requireContext(),
LinearLayoutManager.HORIZONTAL,
false));
playerCoverFlow.addItemDecoration(UIUtil.horizontalSpacing(32));
playerCoverFlow.addOnScrollListener(UIUtil.scaleOnScroll());
UIUtil.centerAndSnapRecyclerView(playerCoverFlow);
coverFlowInitialized = true;
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@app/src/main/java/com/cappielloantonio/tempo/ui/fragment/PlayerControllerFragment.java`
around lines 188 - 223, The setupCoverFlow method is being called repeatedly
(likely from a LiveData observer) and accumulating item decorations, scroll
listeners, and recreating the snap helper on each call, causing state conflicts
and wasted resources. Introduce a boolean flag to guard the one-time
initialization code on lines 220-222 (the addItemDecoration,
addOnScrollListener, and centerAndSnapRecyclerView calls) so they only execute
during the first call to setupCoverFlow. On subsequent calls, only update the
adapter with the new cover data. Additionally, instead of returning early when
covers.isEmpty() on line 189, clear the adapter or remove all items from the
RecyclerView to properly update the UI state, then return.


@Override
public void onStart() {
super.onStart();
Expand Down Expand Up @@ -169,6 +255,7 @@ private void init() {
playerOpenQueueButton = bind.getRoot().findViewById(R.id.player_open_queue_button);
playerOpenLyricsButton = bind.getRoot().findViewById(R.id.player_open_lyrics_button);
playerTrackInfo = bind.getRoot().findViewById(R.id.player_info_track);
playerCoverFlow = bind.getRoot().findViewById(R.id.player_cover_flow);
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);
Expand Down Expand Up @@ -215,14 +302,20 @@ private void initQuickActionView() {
playerOpenLyricsButton.setOnClickListener(view -> {
int currentItem = playerMediaCoverViewPager.getCurrentItem();
if (currentItem == 0) {
playerMediaCoverViewPager.setCurrentItem(1, true);
playerMediaCoverViewPager.setCurrentItem(1, false);
playerCoverFlow.setVisibility(View.GONE);
playerMediaCoverViewPager.setVisibility(View.VISIBLE);
} else if (currentItem == 1) {
playerMediaCoverViewPager.setCurrentItem(0, true);;
playerMediaCoverViewPager.setCurrentItem(0, false);
playerCoverFlow.setVisibility(View.VISIBLE);
playerMediaCoverViewPager.setVisibility(View.GONE);
}

});
}



private void initializeBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
Expand Down
Loading