feat: radio

This commit is contained in:
antonio 2023-05-07 17:11:34 +02:00
parent 1deb9ed3d7
commit a1ee70c24f
16 changed files with 478 additions and 11 deletions

View file

@ -31,4 +31,8 @@ public interface ClickCallback {
default void onPodcastClick(Bundle bundle) {}
default void onPodcastLongClick(Bundle bundle) {}
default void onInternetRadioStationClick(Bundle bundle) {}
default void onInternetRadioStationLongClick(Bundle bundle) {}
}

View file

@ -0,0 +1,39 @@
package com.cappielloantonio.play.repository;
import androidx.annotation.NonNull;
import androidx.lifecycle.MutableLiveData;
import com.cappielloantonio.play.App;
import com.cappielloantonio.play.subsonic.base.ApiResponse;
import com.cappielloantonio.play.subsonic.models.InternetRadioStation;
import java.util.List;
import retrofit2.Call;
import retrofit2.Callback;
import retrofit2.Response;
public class RadioRepository {
public MutableLiveData<List<InternetRadioStation>> getInternetRadioStations() {
MutableLiveData<List<InternetRadioStation>> radioStation = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getInternetRadioClient()
.getInternetRadioStations()
.enqueue(new Callback<ApiResponse>() {
@Override
public void onResponse(@NonNull Call<ApiResponse> call, @NonNull Response<ApiResponse> response) {
if (response.isSuccessful() && response.body() != null && response.body().getSubsonicResponse().getInternetRadioStations() != null) {
radioStation.setValue(response.body().getSubsonicResponse().getInternetRadioStations().getInternetRadioStations());
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
}
});
return radioStation;
}
}

View file

@ -9,6 +9,7 @@ import com.cappielloantonio.play.repository.ChronologyRepository;
import com.cappielloantonio.play.repository.QueueRepository;
import com.cappielloantonio.play.repository.SongRepository;
import com.cappielloantonio.play.subsonic.models.Child;
import com.cappielloantonio.play.subsonic.models.InternetRadioStation;
import com.cappielloantonio.play.util.MappingUtil;
import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors;
@ -128,6 +129,23 @@ public class MediaManager {
}
}
public static void startRadio(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, InternetRadioStation internetRadioStation) {
if (mediaBrowserListenableFuture != null) {
mediaBrowserListenableFuture.addListener(() -> {
try {
if (mediaBrowserListenableFuture.isDone()) {
mediaBrowserListenableFuture.get().clearMediaItems();
mediaBrowserListenableFuture.get().setMediaItem(MappingUtil.mapInternetRadioStation(internetRadioStation));
mediaBrowserListenableFuture.get().prepare();
mediaBrowserListenableFuture.get().play();
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
}
}, MoreExecutors.directExecutor());
}
}
public static void enqueue(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, List<Child> media, boolean playImmediatelyAfter) {
if (mediaBrowserListenableFuture != null) {
mediaBrowserListenableFuture.addListener(() -> {

View file

@ -3,6 +3,7 @@ package com.cappielloantonio.play.subsonic;
import com.cappielloantonio.play.subsonic.api.albumsonglist.AlbumSongListClient;
import com.cappielloantonio.play.subsonic.api.bookmarks.BookmarksClient;
import com.cappielloantonio.play.subsonic.api.browsing.BrowsingClient;
import com.cappielloantonio.play.subsonic.api.internetradio.InternetRadioClient;
import com.cappielloantonio.play.subsonic.api.mediaannotation.MediaAnnotationClient;
import com.cappielloantonio.play.subsonic.api.medialibraryscanning.MediaLibraryScanningClient;
import com.cappielloantonio.play.subsonic.api.mediaretrieval.MediaRetrievalClient;
@ -31,6 +32,7 @@ public class Subsonic {
private PodcastClient podcastClient;
private MediaLibraryScanningClient mediaLibraryScanningClient;
private BookmarksClient bookmarksClient;
private InternetRadioClient internetRadioClient;
public Subsonic(SubsonicPreferences preferences) {
this.preferences = preferences;
@ -110,6 +112,13 @@ public class Subsonic {
return bookmarksClient;
}
public InternetRadioClient getInternetRadioClient() {
if (internetRadioClient == null) {
internetRadioClient = new InternetRadioClient(this);
}
return internetRadioClient;
}
public String getUrl() {
String url = preferences.getServerUrl() + "/rest/";
return url.replace("//rest", "/rest");

View file

@ -0,0 +1,41 @@
package com.cappielloantonio.play.subsonic.api.internetradio;
import android.util.Log;
import com.cappielloantonio.play.subsonic.RetrofitClient;
import com.cappielloantonio.play.subsonic.Subsonic;
import com.cappielloantonio.play.subsonic.base.ApiResponse;
import retrofit2.Call;
public class InternetRadioClient {
private static final String TAG = "InternetRadioClient";
private final Subsonic subsonic;
private final InternetRadioService internetRadioService;
public InternetRadioClient(Subsonic subsonic) {
this.subsonic = subsonic;
this.internetRadioService = new RetrofitClient(subsonic).getRetrofit().create(InternetRadioService.class);
}
public Call<ApiResponse> getInternetRadioStations() {
Log.d(TAG, "getInternetRadioStations()");
return internetRadioService.getInternetRadioStations(subsonic.getParams());
}
public Call<ApiResponse> createInternetRadioStation(String streamUrl, String name, String homepageUrl) {
Log.d(TAG, "createInternetRadioStation()");
return internetRadioService.createInternetRadioStation(subsonic.getParams(), streamUrl, name, homepageUrl);
}
public Call<ApiResponse> updateInternetRadioStation(String id, String streamUrl, String name, String homepageUrl) {
Log.d(TAG, "updateInternetRadioStation()");
return internetRadioService.updateInternetRadioStation(subsonic.getParams(), id, streamUrl, name, homepageUrl);
}
public Call<ApiResponse> deleteInternetRadioStation(String id) {
Log.d(TAG, "deleteInternetRadioStation()");
return internetRadioService.deleteInternetRadioStation(subsonic.getParams(), id);
}
}

View file

@ -0,0 +1,24 @@
package com.cappielloantonio.play.subsonic.api.internetradio;
import com.cappielloantonio.play.subsonic.base.ApiResponse;
import java.util.Map;
import retrofit2.Call;
import retrofit2.http.GET;
import retrofit2.http.Query;
import retrofit2.http.QueryMap;
public interface InternetRadioService {
@GET("getInternetRadioStations")
Call<ApiResponse> getInternetRadioStations(@QueryMap Map<String, String> params);
@GET("createInternetRadioStation")
Call<ApiResponse> createInternetRadioStation(@QueryMap Map<String, String> params, @Query("streamUrl") String streamUrl, @Query("name") String name, @Query("homepageUrl") String homepageUrl);
@GET("updateInternetRadioStation")
Call<ApiResponse> updateInternetRadioStation(@QueryMap Map<String, String> params, @Query("id") String id, @Query("streamUrl") String streamUrl, @Query("name") String name, @Query("homepageUrl") String homepageUrl);
@GET("deleteInternetRadioStation")
Call<ApiResponse> deleteInternetRadioStation(@QueryMap Map<String, String> params, @Query("id") String id);
}

View file

@ -1,6 +1,10 @@
package com.cappielloantonio.play.subsonic.models
class InternetRadioStation {
import android.os.Parcelable
import kotlinx.parcelize.Parcelize
@Parcelize
class InternetRadioStation : Parcelable {
var id: String? = null
var name: String? = null
var streamUrl: String? = null

View file

@ -1,5 +1,8 @@
package com.cappielloantonio.play.subsonic.models
import com.google.gson.annotations.SerializedName
class InternetRadioStations {
@SerializedName("internetRadioStation")
var internetRadioStations: List<InternetRadioStation>? = null
}

View file

@ -0,0 +1,99 @@
package com.cappielloantonio.play.ui.adapter;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import androidx.annotation.NonNull;
import androidx.media3.common.util.UnstableApi;
import androidx.recyclerview.widget.RecyclerView;
import com.cappielloantonio.play.databinding.ItemHomeInternetRadioStationBinding;
import com.cappielloantonio.play.glide.CustomGlideRequest;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.subsonic.models.InternetRadioStation;
import com.cappielloantonio.play.util.Constants;
import java.util.Collections;
import java.util.List;
@UnstableApi
public class InternetRadioStationAdapter extends RecyclerView.Adapter<InternetRadioStationAdapter.ViewHolder> {
private final ClickCallback click;
private List<InternetRadioStation> internetRadioStations;
public InternetRadioStationAdapter(ClickCallback click) {
this.click = click;
this.internetRadioStations = Collections.emptyList();
}
@NonNull
@Override
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
ItemHomeInternetRadioStationBinding view = ItemHomeInternetRadioStationBinding.inflate(LayoutInflater.from(parent.getContext()), parent, false);
return new ViewHolder(view);
}
@Override
public void onBindViewHolder(ViewHolder holder, int position) {
InternetRadioStation internetRadioStation = internetRadioStations.get(position);
holder.item.internetRadioStationTitleTextView.setText(internetRadioStation.getName());
holder.item.internetRadioStationSubtitleTextView.setText(internetRadioStation.getStreamUrl());
CustomGlideRequest.Builder
.from(holder.itemView.getContext(), internetRadioStation.getStreamUrl())
.build()
.into(holder.item.internetRadioStationCoverImageView);
}
@Override
public int getItemCount() {
return internetRadioStations.size();
}
public void setItems(List<InternetRadioStation> internetRadioStations) {
this.internetRadioStations = internetRadioStations;
notifyDataSetChanged();
}
public InternetRadioStation getItem(int position) {
return internetRadioStations.get(position);
}
public class ViewHolder extends RecyclerView.ViewHolder {
ItemHomeInternetRadioStationBinding item;
ViewHolder(ItemHomeInternetRadioStationBinding item) {
super(item.getRoot());
this.item = item;
item.internetRadioStationTitleTextView.setSelected(true);
item.internetRadioStationSubtitleTextView.setSelected(true);
itemView.setOnClickListener(v -> onClick());
itemView.setOnLongClickListener(v -> onLongClick());
item.internetRadioStationMoreButton.setOnClickListener(v -> onLongClick());
}
public void onClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.INTERNET_RADIO_STATION_OBJECT, internetRadioStations.get(getBindingAdapterPosition()));
click.onInternetRadioStationClick(bundle);
}
private boolean onLongClick() {
Bundle bundle = new Bundle();
bundle.putParcelable(Constants.INTERNET_RADIO_STATION_OBJECT, internetRadioStations.get(getBindingAdapterPosition()));
click.onInternetRadioStationLongClick(bundle);
return false;
}
}
}

View file

@ -5,6 +5,7 @@ import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
@ -13,21 +14,27 @@ import androidx.lifecycle.ViewModelProvider;
import androidx.media3.common.util.UnstableApi;
import androidx.media3.session.MediaBrowser;
import androidx.media3.session.SessionToken;
import androidx.recyclerview.widget.LinearLayoutManager;
import com.cappielloantonio.play.databinding.FragmentHomeTabPodcastBinding;
import com.cappielloantonio.play.databinding.FragmentHomeTabRadioBinding;
import com.cappielloantonio.play.interfaces.ClickCallback;
import com.cappielloantonio.play.service.MediaManager;
import com.cappielloantonio.play.service.MediaService;
import com.cappielloantonio.play.ui.activity.MainActivity;
import com.cappielloantonio.play.viewmodel.HomeViewModel;
import com.cappielloantonio.play.ui.adapter.InternetRadioStationAdapter;
import com.cappielloantonio.play.util.Constants;
import com.cappielloantonio.play.viewmodel.RadioViewModel;
import com.google.common.util.concurrent.ListenableFuture;
@UnstableApi
public class HomeTabRadioFragment extends Fragment {
public class HomeTabRadioFragment extends Fragment implements ClickCallback {
private static final String TAG = "HomeTabRadioFragment";
private FragmentHomeTabRadioBinding bind;
private MainActivity activity;
private HomeViewModel homeViewModel;
private RadioViewModel radioViewModel;
private InternetRadioStationAdapter internetRadioStationAdapter;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@ -38,11 +45,18 @@ public class HomeTabRadioFragment extends Fragment {
bind = FragmentHomeTabRadioBinding.inflate(inflater, container, false);
View view = bind.getRoot();
homeViewModel = new ViewModelProvider(requireActivity()).get(HomeViewModel.class);
radioViewModel = new ViewModelProvider(requireActivity()).get(RadioViewModel.class);
return view;
}
@Override
public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
super.onViewCreated(view, savedInstanceState);
initRadioStationView();
}
@Override
public void onStart() {
super.onStart();
@ -62,6 +76,28 @@ public class HomeTabRadioFragment extends Fragment {
bind = null;
}
private void initRadioStationView() {
bind.internetRadioStationRecyclerView.setLayoutManager(new LinearLayoutManager(requireContext()));
bind.internetRadioStationRecyclerView.setHasFixedSize(true);
internetRadioStationAdapter = new InternetRadioStationAdapter(this);
bind.internetRadioStationRecyclerView.setAdapter(internetRadioStationAdapter);
radioViewModel.getInternetRadioStations().observe(getViewLifecycleOwner(), internetRadioStations -> {
if (internetRadioStations == null) {
if (bind != null)
bind.internetRadioStationPlaceholder.placeholder.setVisibility(View.VISIBLE);
if (bind != null) bind.internetRadioStationSector.setVisibility(View.GONE);
} else {
if (bind != null)
bind.internetRadioStationPlaceholder.placeholder.setVisibility(View.GONE);
if (bind != null)
bind.internetRadioStationSector.setVisibility(!internetRadioStations.isEmpty() ? View.VISIBLE : View.GONE);
internetRadioStationAdapter.setItems(internetRadioStations);
}
});
}
private void initializeMediaBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
}
@ -69,4 +105,15 @@ public class HomeTabRadioFragment extends Fragment {
private void releaseMediaBrowser() {
MediaBrowser.releaseFuture(mediaBrowserListenableFuture);
}
@Override
public void onInternetRadioStationClick(Bundle bundle) {
MediaManager.startRadio(mediaBrowserListenableFuture, bundle.getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT));
activity.setBottomSheetInPeek(true);
}
@Override
public void onInternetRadioStationLongClick(Bundle bundle) {
Toast.makeText(requireContext(), "Long click!", Toast.LENGTH_SHORT).show();
}
}

View file

@ -1,9 +1,7 @@
package com.cappielloantonio.play.ui.fragment;
import android.content.ComponentName;
import android.graphics.Point;
import android.os.Bundle;
import android.view.Display;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
@ -214,6 +212,17 @@ public class PlayerControllerFragment extends Fragment {
bind.getRoot().findViewById(R.id.player_skip_silence_toggle_button).setVisibility(View.VISIBLE);
setPlaybackParameters(mediaBrowser);
break;
case Constants.MEDIA_TYPE_RADIO:
bind.getRoot().setShowShuffleButton(false);
bind.getRoot().setShowRewindButton(false);
bind.getRoot().setShowPreviousButton(false);
bind.getRoot().setShowNextButton(false);
bind.getRoot().setShowFastForwardButton(false);
bind.getRoot().setRepeatToggleModes(RepeatModeUtil.REPEAT_TOGGLE_MODE_NONE);
bind.getRoot().findViewById(R.id.player_playback_speed_button).setVisibility(View.GONE);
bind.getRoot().findViewById(R.id.player_skip_silence_toggle_button).setVisibility(View.GONE);
setPlaybackParameters(mediaBrowser);
break;
case Constants.MEDIA_TYPE_MUSIC:
default:
bind.getRoot().setShowShuffleButton(true);

View file

@ -12,6 +12,7 @@ object Constants {
const val GENRE_OBJECT = "GENRE_OBJECT"
const val PLAYLIST_OBJECT = "PLAYLIST_OBJECT"
const val PODCAST_OBJECT = "PODCAST_OBJECT"
const val INTERNET_RADIO_STATION_OBJECT = "INTERNET_RADIO_STATION_OBJECT"
const val ALBUM_RECENTLY_PLAYED = "ALBUM_RECENTLY_PLAYED"
const val ALBUM_MOST_PLAYED = "ALBUM_MOST_PLAYED"
@ -42,6 +43,7 @@ object Constants {
const val MEDIA_TYPE_PODCAST = "podcast"
const val MEDIA_TYPE_AUDIOBOOK = "audiobook"
const val MEDIA_TYPE_VIDEO = "video"
const val MEDIA_TYPE_RADIO = "radio"
const val MEDIA_PLAYBACK_SPEED_080 = 0.8f
const val MEDIA_PLAYBACK_SPEED_100 = 1.0f

View file

@ -11,6 +11,7 @@ import androidx.media3.common.util.UnstableApi;
import com.cappielloantonio.play.App;
import com.cappielloantonio.play.subsonic.models.Child;
import com.cappielloantonio.play.subsonic.models.InternetRadioStation;
import java.util.ArrayList;
import java.util.List;
@ -121,6 +122,36 @@ public class MappingUtil {
.build();
}
public static MediaItem mapInternetRadioStation(InternetRadioStation internetRadioStation) {
Uri uri = Uri.parse(internetRadioStation.getStreamUrl());
Bundle bundle = new Bundle();
bundle.putString("id", internetRadioStation.getId());
bundle.putString("title", internetRadioStation.getName());
bundle.putString("artist", uri.toString());
bundle.putString("uri", uri.toString());
bundle.putString("type", Constants.MEDIA_TYPE_RADIO);
return new MediaItem.Builder()
.setMediaId(internetRadioStation.getId())
.setMediaMetadata(
new MediaMetadata.Builder()
.setTitle(internetRadioStation.getName())
.setArtist(internetRadioStation.getStreamUrl())
.setExtras(bundle)
.build()
)
.setRequestMetadata(
new MediaItem.RequestMetadata.Builder()
.setMediaUri(uri)
.setExtras(bundle)
.build()
)
.setMimeType(MimeTypes.BASE_TYPE_AUDIO)
.setUri(uri)
.build();
}
private static Uri getUri(Child media) {
return DownloadUtil.getDownloadTracker(App.getContext()).isDownloaded(media.getId())
? MusicUtil.getDownloadUri(media.getId())

View file

@ -0,0 +1,26 @@
package com.cappielloantonio.play.viewmodel;
import android.app.Application;
import androidx.annotation.NonNull;
import androidx.lifecycle.AndroidViewModel;
import androidx.lifecycle.LiveData;
import com.cappielloantonio.play.repository.RadioRepository;
import com.cappielloantonio.play.subsonic.models.InternetRadioStation;
import java.util.List;
public class RadioViewModel extends AndroidViewModel {
private final RadioRepository radioRepository;
public RadioViewModel(@NonNull Application application) {
super(application);
radioRepository = new RadioRepository();
}
public LiveData<List<InternetRadioStation>> getInternetRadioStations() {
return radioRepository.getInternetRadioStations();
}
}

View file

@ -1,7 +1,47 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
<androidx.core.widget.NestedScrollView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<LinearLayout
android:id="@+id/internet_radio_station_sector"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:paddingBottom="@dimen/global_padding_bottom">
<TextView
style="@style/TitleLarge"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="16dp"
android:paddingTop="16dp"
android:paddingEnd="16dp"
android:text="@string/home_title_internet_radio_station" />
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/internet_radio_station_recycler_view"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
android:clipToPadding="false"
android:paddingTop="8dp"
android:paddingBottom="8dp" />
</LinearLayout>
<include
android:id="@+id/internet_radio_station_placeholder"
layout="@layout/item_placeholder_horizontal"
android:visibility="gone" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,71 @@
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackground"
android:clipChildren="false"
android:orientation="horizontal"
android:paddingStart="16dp"
android:paddingTop="3dp"
android:paddingBottom="3dp">
<ImageView
android:id="@+id/internet_radio_station_cover_image_view"
android:layout_width="52dp"
android:layout_height="52dp"
android:layout_gravity="center"
android:layout_margin="2dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<View
android:id="@+id/cover_image_separator"
android:layout_width="12dp"
android:layout_height="52dp"
app:layout_constraintBottom_toBottomOf="@+id/internet_radio_station_cover_image_view"
app:layout_constraintEnd_toStartOf="@+id/internet_radio_station_title_text_view"
app:layout_constraintStart_toEndOf="@+id/internet_radio_station_cover_image_view"
app:layout_constraintTop_toTopOf="@+id/internet_radio_station_cover_image_view" />
<TextView
android:id="@+id/internet_radio_station_title_text_view"
style="@style/LabelMedium"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:paddingEnd="12dp"
android:singleLine="true"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toTopOf="@id/internet_radio_station_subtitle_text_view"
app:layout_constraintEnd_toStartOf="@+id/internet_radio_station_more_button"
app:layout_constraintStart_toEndOf="@+id/cover_image_separator"
app:layout_constraintTop_toTopOf="@+id/internet_radio_station_cover_image_view"
app:layout_constraintVertical_chainStyle="packed" />
<TextView
android:id="@+id/internet_radio_station_subtitle_text_view"
style="@style/LabelSmall"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:ellipsize="marquee"
android:paddingEnd="12dp"
android:singleLine="true"
android:text="@string/label_placeholder"
app:layout_constraintBottom_toBottomOf="@+id/internet_radio_station_cover_image_view"
app:layout_constraintEnd_toStartOf="@+id/internet_radio_station_more_button"
app:layout_constraintStart_toEndOf="@+id/cover_image_separator"
app:layout_constraintTop_toBottomOf="@+id/internet_radio_station_title_text_view" />
<ImageView
android:id="@+id/internet_radio_station_more_button"
android:layout_width="18dp"
android:layout_height="18dp"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"
android:background="@drawable/ic_more_vert"
android:foreground="?android:attr/selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="@id/internet_radio_station_subtitle_text_view"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/internet_radio_station_cover_image_view" />
</androidx.constraintlayout.widget.ConstraintLayout>