feat: Make all objects in Tempo references for quick access (#158)

This commit is contained in:
eddyizm 2025-10-09 22:18:11 -07:00 committed by GitHub
commit 9c088a7e88
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 1119 additions and 53 deletions

View file

@ -42,6 +42,16 @@
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" /> <category android:name="android.intent.category.LAUNCHER" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="asset"
android:scheme="tempo" />
</intent-filter>
</activity> </activity>
<service <service

View file

@ -80,6 +80,33 @@ public class PlaylistRepository {
return listLivePlaylistSongs; return listLivePlaylistSongs;
} }
public MutableLiveData<Playlist> getPlaylist(String id) {
MutableLiveData<Playlist> playlistLiveData = new MutableLiveData<>();
App.getSubsonicClientInstance(false)
.getPlaylistClient()
.getPlaylist(id)
.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().getPlaylist() != null) {
playlistLiveData.setValue(response.body().getSubsonicResponse().getPlaylist());
} else {
playlistLiveData.setValue(null);
}
}
@Override
public void onFailure(@NonNull Call<ApiResponse> call, @NonNull Throwable t) {
playlistLiveData.setValue(null);
}
});
return playlistLiveData;
}
public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) { public void addSongToPlaylist(String playlistId, ArrayList<String> songsId) {
if (songsId.isEmpty()) { if (songsId.isEmpty()) {
Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show(); Toast.makeText(App.getContext(), App.getContext().getString(R.string.playlist_chooser_dialog_toast_all_skipped), Toast.LENGTH_SHORT).show();

View file

@ -37,6 +37,8 @@ import com.cappielloantonio.tempo.ui.dialog.ConnectionAlertDialog;
import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog; import com.cappielloantonio.tempo.ui.dialog.GithubTempoUpdateDialog;
import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog; import com.cappielloantonio.tempo.ui.dialog.ServerUnreachableDialog;
import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment; import com.cappielloantonio.tempo.ui.fragment.PlayerBottomSheetFragment;
import com.cappielloantonio.tempo.util.AssetLinkNavigator;
import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.MainViewModel; import com.cappielloantonio.tempo.viewmodel.MainViewModel;
@ -60,6 +62,8 @@ public class MainActivity extends BaseActivity {
private BottomNavigationView bottomNavigationView; private BottomNavigationView bottomNavigationView;
public NavController navController; public NavController navController;
private BottomSheetBehavior bottomSheetBehavior; private BottomSheetBehavior bottomSheetBehavior;
private AssetLinkNavigator assetLinkNavigator;
private AssetLinkUtil.AssetLink pendingAssetLink;
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver; ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
private Intent pendingDownloadPlaybackIntent; private Intent pendingDownloadPlaybackIntent;
@ -76,6 +80,7 @@ public class MainActivity extends BaseActivity {
setContentView(view); setContentView(view);
mainViewModel = new ViewModelProvider(this).get(MainViewModel.class); mainViewModel = new ViewModelProvider(this).get(MainViewModel.class);
assetLinkNavigator = new AssetLinkNavigator(this);
connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this); connectivityStatusBroadcastReceiver = new ConnectivityStatusBroadcastReceiver(this);
connectivityStatusReceiverManager(true); connectivityStatusReceiverManager(true);
@ -311,6 +316,24 @@ public class MainActivity extends BaseActivity {
public void goFromLogin() { public void goFromLogin() {
setBottomSheetInPeek(mainViewModel.isQueueLoaded()); setBottomSheetInPeek(mainViewModel.isQueueLoaded());
goToHome(); goToHome();
consumePendingAssetLink();
}
public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink) {
openAssetLink(assetLink, true);
}
public void openAssetLink(@NonNull AssetLinkUtil.AssetLink assetLink, boolean collapsePlayer) {
if (!isUserAuthenticated()) {
pendingAssetLink = assetLink;
return;
}
if (collapsePlayer) {
setBottomSheetInPeek(true);
}
if (assetLinkNavigator != null) {
assetLinkNavigator.open(assetLink);
}
} }
public void quit() { public void quit() {
@ -443,6 +466,7 @@ public class MainActivity extends BaseActivity {
|| intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) { || intent.hasExtra(Constants.EXTRA_DOWNLOAD_URI)) {
pendingDownloadPlaybackIntent = new Intent(intent); pendingDownloadPlaybackIntent = new Intent(intent);
} }
handleAssetLinkIntent(intent);
} }
private void consumePendingPlaybackIntent() { private void consumePendingPlaybackIntent() {
@ -452,6 +476,35 @@ public class MainActivity extends BaseActivity {
playDownloadedMedia(intent); playDownloadedMedia(intent);
} }
private void handleAssetLinkIntent(Intent intent) {
AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.parse(intent);
if (assetLink == null) {
return;
}
if (!isUserAuthenticated()) {
pendingAssetLink = assetLink;
intent.setData(null);
return;
}
if (assetLinkNavigator != null) {
assetLinkNavigator.open(assetLink);
}
intent.setData(null);
}
private boolean isUserAuthenticated() {
return Preferences.getPassword() != null
|| (Preferences.getToken() != null && Preferences.getSalt() != null);
}
private void consumePendingAssetLink() {
if (pendingAssetLink == null || assetLinkNavigator == null) {
return;
}
assetLinkNavigator.open(pendingAssetLink);
pendingAssetLink = null;
}
private void playDownloadedMedia(Intent intent) { private void playDownloadedMedia(Intent intent) {
String uriString = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_URI); String uriString = intent.getStringExtra(Constants.EXTRA_DOWNLOAD_URI);
if (TextUtils.isEmpty(uriString)) { if (TextUtils.isEmpty(uriString)) {
@ -500,4 +553,4 @@ public class MainActivity extends BaseActivity {
MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem); MediaManager.playDownloadedMediaItem(getMediaBrowserListenableFuture(), mediaItem);
} }
} }

View file

@ -2,6 +2,7 @@ package com.cappielloantonio.tempo.ui.dialog;
import android.app.Dialog; import android.app.Dialog;
import android.os.Bundle; import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.fragment.app.DialogFragment; import androidx.fragment.app.DialogFragment;
@ -10,6 +11,7 @@ import androidx.media3.common.MediaMetadata;
import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogTrackInfoBinding; import com.cappielloantonio.tempo.databinding.DialogTrackInfoBinding;
import com.cappielloantonio.tempo.glide.CustomGlideRequest; import com.cappielloantonio.tempo.glide.CustomGlideRequest;
import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
@ -21,6 +23,11 @@ public class TrackInfoDialog extends DialogFragment {
private DialogTrackInfoBinding bind; private DialogTrackInfoBinding bind;
private final MediaMetadata mediaMetadata; private final MediaMetadata mediaMetadata;
private AssetLinkUtil.AssetLink songLink;
private AssetLinkUtil.AssetLink albumLink;
private AssetLinkUtil.AssetLink artistLink;
private AssetLinkUtil.AssetLink genreLink;
private AssetLinkUtil.AssetLink yearLink;
public TrackInfoDialog(MediaMetadata mediaMetadata) { public TrackInfoDialog(MediaMetadata mediaMetadata) {
this.mediaMetadata = mediaMetadata; this.mediaMetadata = mediaMetadata;
@ -52,6 +59,8 @@ public class TrackInfoDialog extends DialogFragment {
} }
private void setTrackInfo() { private void setTrackInfo() {
genreLink = null;
yearLink = null;
bind.trakTitleInfoTextView.setText(mediaMetadata.title); bind.trakTitleInfoTextView.setText(mediaMetadata.title);
bind.trakArtistInfoTextView.setText( bind.trakArtistInfoTextView.setText(
mediaMetadata.artist != null mediaMetadata.artist != null
@ -61,17 +70,41 @@ public class TrackInfoDialog extends DialogFragment {
: ""); : "");
if (mediaMetadata.extras != null) { if (mediaMetadata.extras != null) {
songLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_SONG, mediaMetadata.extras.getString("id"));
albumLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ALBUM, mediaMetadata.extras.getString("albumId"));
artistLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ARTIST, mediaMetadata.extras.getString("artistId"));
genreLink = AssetLinkUtil.parseLinkString(mediaMetadata.extras.getString("assetLinkGenre"));
yearLink = AssetLinkUtil.parseLinkString(mediaMetadata.extras.getString("assetLinkYear"));
CustomGlideRequest.Builder CustomGlideRequest.Builder
.from(requireContext(), mediaMetadata.extras.getString("coverArtId", ""), CustomGlideRequest.ResourceType.Song) .from(requireContext(), mediaMetadata.extras.getString("coverArtId", ""), CustomGlideRequest.ResourceType.Song)
.build() .build()
.into(bind.trackCoverInfoImageView); .into(bind.trackCoverInfoImageView);
bind.titleValueSector.setText(mediaMetadata.extras.getString("title", getString(R.string.label_placeholder))); bindAssetLink(bind.trackCoverInfoImageView, albumLink != null ? albumLink : songLink);
bind.albumValueSector.setText(mediaMetadata.extras.getString("album", getString(R.string.label_placeholder))); bindAssetLink(bind.trakTitleInfoTextView, songLink);
bind.artistValueSector.setText(mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder))); bindAssetLink(bind.trakArtistInfoTextView, artistLink != null ? artistLink : songLink);
String titleValue = mediaMetadata.extras.getString("title", getString(R.string.label_placeholder));
String albumValue = mediaMetadata.extras.getString("album", getString(R.string.label_placeholder));
String artistValue = mediaMetadata.extras.getString("artist", getString(R.string.label_placeholder));
String genreValue = mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder));
int yearValue = mediaMetadata.extras.getInt("year", 0);
if (genreLink == null && genreValue != null && !genreValue.isEmpty() && !getString(R.string.label_placeholder).contentEquals(genreValue)) {
genreLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_GENRE, genreValue);
}
if (yearLink == null && yearValue != 0) {
yearLink = AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(yearValue));
}
bind.titleValueSector.setText(titleValue);
bind.albumValueSector.setText(albumValue);
bind.artistValueSector.setText(artistValue);
bind.trackNumberValueSector.setText(mediaMetadata.extras.getInt("track", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("track", 0)) : getString(R.string.label_placeholder)); bind.trackNumberValueSector.setText(mediaMetadata.extras.getInt("track", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("track", 0)) : getString(R.string.label_placeholder));
bind.yearValueSector.setText(mediaMetadata.extras.getInt("year", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("year", 0)) : getString(R.string.label_placeholder)); bind.yearValueSector.setText(yearValue != 0 ? String.valueOf(yearValue) : getString(R.string.label_placeholder));
bind.genreValueSector.setText(mediaMetadata.extras.getString("genre", getString(R.string.label_placeholder))); bind.genreValueSector.setText(genreValue);
bind.sizeValueSector.setText(mediaMetadata.extras.getLong("size", 0) != 0 ? MusicUtil.getReadableByteCount(mediaMetadata.extras.getLong("size", 0)) : getString(R.string.label_placeholder)); bind.sizeValueSector.setText(mediaMetadata.extras.getLong("size", 0) != 0 ? MusicUtil.getReadableByteCount(mediaMetadata.extras.getLong("size", 0)) : getString(R.string.label_placeholder));
bind.contentTypeValueSector.setText(mediaMetadata.extras.getString("contentType", getString(R.string.label_placeholder))); bind.contentTypeValueSector.setText(mediaMetadata.extras.getString("contentType", getString(R.string.label_placeholder)));
bind.suffixValueSector.setText(mediaMetadata.extras.getString("suffix", getString(R.string.label_placeholder))); bind.suffixValueSector.setText(mediaMetadata.extras.getString("suffix", getString(R.string.label_placeholder)));
@ -83,6 +116,12 @@ public class TrackInfoDialog extends DialogFragment {
bind.bitDepthValueSector.setText(mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + " bits" : getString(R.string.label_placeholder)); bind.bitDepthValueSector.setText(mediaMetadata.extras.getInt("bitDepth", 0) != 0 ? mediaMetadata.extras.getInt("bitDepth", 0) + " bits" : getString(R.string.label_placeholder));
bind.pathValueSector.setText(mediaMetadata.extras.getString("path", getString(R.string.label_placeholder))); bind.pathValueSector.setText(mediaMetadata.extras.getString("path", getString(R.string.label_placeholder)));
bind.discNumberValueSector.setText(mediaMetadata.extras.getInt("discNumber", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("discNumber", 0)) : getString(R.string.label_placeholder)); bind.discNumberValueSector.setText(mediaMetadata.extras.getInt("discNumber", 0) != 0 ? String.valueOf(mediaMetadata.extras.getInt("discNumber", 0)) : getString(R.string.label_placeholder));
bindAssetLink(bind.titleValueSector, songLink);
bindAssetLink(bind.albumValueSector, albumLink);
bindAssetLink(bind.artistValueSector, artistLink);
bindAssetLink(bind.genreValueSector, genreLink);
bindAssetLink(bind.yearValueSector, yearLink);
} }
} }
@ -135,4 +174,31 @@ public class TrackInfoDialog extends DialogFragment {
bind.trakTranscodingInfoTextView.setText(info); bind.trakTranscodingInfoTextView.setText(info);
} }
} }
private void bindAssetLink(android.view.View view, AssetLinkUtil.AssetLink assetLink) {
if (view == null) return;
if (assetLink == null) {
AssetLinkUtil.clearLinkAppearance(view);
view.setOnClickListener(null);
view.setOnLongClickListener(null);
view.setClickable(false);
view.setLongClickable(false);
return;
}
view.setClickable(true);
view.setLongClickable(true);
AssetLinkUtil.applyLinkAppearance(view);
view.setOnClickListener(v -> {
dismissAllowingStateLoss();
boolean collapse = !AssetLinkUtil.TYPE_SONG.equals(assetLink.type);
((com.cappielloantonio.tempo.ui.activity.MainActivity) requireActivity()).openAssetLink(assetLink, collapse);
});
view.setOnLongClickListener(v -> {
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show();
return true;
});
}
} }

View file

@ -35,6 +35,7 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter; import com.cappielloantonio.tempo.ui.adapter.SongHorizontalAdapter;
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog; import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
import com.cappielloantonio.tempo.ui.dialog.RatingDialog; import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.MappingUtil; import com.cappielloantonio.tempo.util.MappingUtil;
@ -177,8 +178,35 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
bind.albumNameLabel.setText(album.getName()); bind.albumNameLabel.setText(album.getName());
bind.albumArtistLabel.setText(album.getArtist()); bind.albumArtistLabel.setText(album.getArtist());
AssetLinkUtil.applyLinkAppearance(bind.albumArtistLabel);
AssetLinkUtil.AssetLink artistLink = buildArtistLink(album);
bind.albumArtistLabel.setOnLongClickListener(v -> {
if (artistLink != null) {
AssetLinkUtil.copyToClipboard(requireContext(), artistLink);
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, artistLink.id), Toast.LENGTH_SHORT).show();
return true;
}
return false;
});
bind.albumReleaseYearLabel.setText(album.getYear() != 0 ? String.valueOf(album.getYear()) : ""); bind.albumReleaseYearLabel.setText(album.getYear() != 0 ? String.valueOf(album.getYear()) : "");
bind.albumReleaseYearLabel.setVisibility(album.getYear() != 0 ? View.VISIBLE : View.GONE); if (album.getYear() != 0) {
bind.albumReleaseYearLabel.setVisibility(View.VISIBLE);
AssetLinkUtil.applyLinkAppearance(bind.albumReleaseYearLabel);
bind.albumReleaseYearLabel.setOnClickListener(v -> openYearLink(album.getYear()));
bind.albumReleaseYearLabel.setOnLongClickListener(v -> {
AssetLinkUtil.AssetLink yearLink = buildYearLink(album.getYear());
if (yearLink != null) {
AssetLinkUtil.copyToClipboard(requireContext(), yearLink);
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, yearLink.id), Toast.LENGTH_SHORT).show();
}
return true;
});
} else {
bind.albumReleaseYearLabel.setVisibility(View.GONE);
bind.albumReleaseYearLabel.setOnClickListener(null);
bind.albumReleaseYearLabel.setOnLongClickListener(null);
AssetLinkUtil.clearLinkAppearance(bind.albumReleaseYearLabel);
}
bind.albumSongCountDurationTextview.setText(getString(R.string.album_page_tracks_count_and_duration, album.getSongCount(), album.getDuration() != null ? album.getDuration() / 60 : 0)); bind.albumSongCountDurationTextview.setText(getString(R.string.album_page_tracks_count_and_duration, album.getSongCount(), album.getDuration() != null ? album.getDuration() / 60 : 0));
if (album.getGenre() != null && !album.getGenre().isEmpty()) { if (album.getGenre() != null && !album.getGenre().isEmpty()) {
bind.albumGenresTextview.setText(album.getGenre()); bind.albumGenresTextview.setText(album.getGenre());
@ -347,4 +375,23 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
private void setMediaBrowserListenableFuture() { private void setMediaBrowserListenableFuture() {
songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture); songHorizontalAdapter.setMediaBrowserListenableFuture(mediaBrowserListenableFuture);
} }
}
private void openYearLink(int year) {
AssetLinkUtil.AssetLink link = buildYearLink(year);
if (link != null) {
activity.openAssetLink(link);
}
}
private AssetLinkUtil.AssetLink buildYearLink(int year) {
if (year <= 0) return null;
return AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year));
}
private AssetLinkUtil.AssetLink buildArtistLink(AlbumID3 album) {
if (album == null || album.getArtistId() == null || album.getArtistId().isEmpty()) {
return null;
}
return AssetLinkUtil.buildAssetLink(AssetLinkUtil.TYPE_ARTIST, album.getArtistId());
}
}

View file

@ -195,6 +195,7 @@ public class PlayerBottomSheetFragment extends Fragment {
} }
} }
private void setMediaControllerUI(MediaBrowser mediaBrowser) { private void setMediaControllerUI(MediaBrowser mediaBrowser) {
if (mediaBrowser.getMediaMetadata().extras != null) { if (mediaBrowser.getMediaMetadata().extras != null) {
switch (mediaBrowser.getMediaMetadata().extras.getString("type", Constants.MEDIA_TYPE_MUSIC)) { switch (mediaBrowser.getMediaMetadata().extras.getString("type", Constants.MEDIA_TYPE_MUSIC)) {

View file

@ -13,9 +13,10 @@ import android.view.ViewGroup;
import android.widget.Button; import android.widget.Button;
import android.widget.ImageButton; import android.widget.ImageButton;
import android.widget.LinearLayout; import android.widget.LinearLayout;
import android.widget.RatingBar;
import android.widget.TextView; import android.widget.TextView;
import android.widget.ToggleButton; import android.widget.ToggleButton;
import android.widget.RatingBar; import android.widget.Toast;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.constraintlayout.widget.ConstraintLayout; import androidx.constraintlayout.widget.ConstraintLayout;
@ -41,12 +42,14 @@ import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.dialog.RatingDialog; import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog; import com.cappielloantonio.tempo.ui.dialog.TrackInfoDialog;
import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager; import com.cappielloantonio.tempo.ui.fragment.pager.PlayerControllerHorizontalPager;
import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel; import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
import com.cappielloantonio.tempo.viewmodel.RatingViewModel; import com.cappielloantonio.tempo.viewmodel.RatingViewModel;
import com.google.android.material.chip.Chip; import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.android.material.elevation.SurfaceColors; import com.google.android.material.elevation.SurfaceColors;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -76,6 +79,10 @@ public class PlayerControllerFragment extends Fragment {
private ImageButton playerTrackInfo; private ImageButton playerTrackInfo;
private LinearLayout ratingContainer; private LinearLayout ratingContainer;
private ImageButton equalizerButton; private ImageButton equalizerButton;
private ChipGroup assetLinkChipGroup;
private Chip playerSongLinkChip;
private Chip playerAlbumLinkChip;
private Chip playerArtistLinkChip;
private MainActivity activity; private MainActivity activity;
private PlayerBottomSheetViewModel playerBottomSheetViewModel; private PlayerBottomSheetViewModel playerBottomSheetViewModel;
@ -139,6 +146,10 @@ public class PlayerControllerFragment extends Fragment {
songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar); songRatingBar = bind.getRoot().findViewById(R.id.song_rating_bar);
ratingContainer = bind.getRoot().findViewById(R.id.rating_container); ratingContainer = bind.getRoot().findViewById(R.id.rating_container);
equalizerButton = bind.getRoot().findViewById(R.id.player_open_equalizer_button); equalizerButton = bind.getRoot().findViewById(R.id.player_open_equalizer_button);
assetLinkChipGroup = bind.getRoot().findViewById(R.id.asset_link_chip_group);
playerSongLinkChip = bind.getRoot().findViewById(R.id.asset_link_song_chip);
playerAlbumLinkChip = bind.getRoot().findViewById(R.id.asset_link_album_chip);
playerArtistLinkChip = bind.getRoot().findViewById(R.id.asset_link_artist_chip);
checkAndSetRatingContainerVisibility(); checkAndSetRatingContainerVisibility();
} }
@ -219,6 +230,8 @@ public class PlayerControllerFragment extends Fragment {
|| mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null || mediaMetadata.extras != null && Objects.equals(mediaMetadata.extras.getString("type"), Constants.MEDIA_TYPE_RADIO) && mediaMetadata.extras.getString("uri") != null
? View.VISIBLE ? View.VISIBLE
: View.GONE); : View.GONE);
updateAssetLinkChips(mediaMetadata);
} }
private void setMediaInfo(MediaMetadata mediaMetadata) { private void setMediaInfo(MediaMetadata mediaMetadata) {
@ -259,6 +272,110 @@ public class PlayerControllerFragment extends Fragment {
}); });
} }
private void updateAssetLinkChips(MediaMetadata mediaMetadata) {
if (assetLinkChipGroup == null) return;
String mediaType = mediaMetadata.extras != null ? mediaMetadata.extras.getString("type", Constants.MEDIA_TYPE_MUSIC) : Constants.MEDIA_TYPE_MUSIC;
if (!Constants.MEDIA_TYPE_MUSIC.equals(mediaType)) {
clearAssetLinkChip(playerSongLinkChip);
clearAssetLinkChip(playerAlbumLinkChip);
clearAssetLinkChip(playerArtistLinkChip);
syncAssetLinkGroupVisibility();
return;
}
String songId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("id") : null;
String albumId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("albumId") : null;
String artistId = mediaMetadata.extras != null ? mediaMetadata.extras.getString("artistId") : null;
AssetLinkUtil.AssetLink songLink = bindAssetLinkChip(playerSongLinkChip, AssetLinkUtil.TYPE_SONG, songId);
AssetLinkUtil.AssetLink albumLink = bindAssetLinkChip(playerAlbumLinkChip, AssetLinkUtil.TYPE_ALBUM, albumId);
AssetLinkUtil.AssetLink artistLink = bindAssetLinkChip(playerArtistLinkChip, AssetLinkUtil.TYPE_ARTIST, artistId);
bindAssetLinkView(playerMediaTitleLabel, songLink);
bindAssetLinkView(playerArtistNameLabel, artistLink != null ? artistLink : songLink);
bindAssetLinkView(playerMediaCoverViewPager, songLink);
syncAssetLinkGroupVisibility();
}
private AssetLinkUtil.AssetLink bindAssetLinkChip(Chip chip, String type, String id) {
if (chip == null) return null;
if (TextUtils.isEmpty(id)) {
clearAssetLinkChip(chip);
return null;
}
String label = getString(AssetLinkUtil.getLabelRes(type));
AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.buildAssetLink(type, id);
if (assetLink == null) {
clearAssetLinkChip(chip);
return null;
}
chip.setText(getString(R.string.asset_link_chip_text, label, assetLink.id));
chip.setVisibility(View.VISIBLE);
chip.setOnClickListener(v -> {
if (assetLink != null) {
activity.openAssetLink(assetLink);
}
});
chip.setOnLongClickListener(v -> {
if (assetLink != null) {
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
}
return true;
});
return assetLink;
}
private void clearAssetLinkChip(Chip chip) {
if (chip == null) return;
chip.setVisibility(View.GONE);
chip.setText("");
chip.setOnClickListener(null);
chip.setOnLongClickListener(null);
}
private void bindAssetLinkView(View view, AssetLinkUtil.AssetLink assetLink) {
if (view == null) return;
if (assetLink == null) {
AssetLinkUtil.clearLinkAppearance(view);
view.setOnClickListener(null);
view.setOnLongClickListener(null);
view.setClickable(false);
view.setLongClickable(false);
return;
}
view.setClickable(true);
view.setLongClickable(true);
AssetLinkUtil.applyLinkAppearance(view);
view.setOnClickListener(v -> {
boolean collapse = !AssetLinkUtil.TYPE_SONG.equals(assetLink.type);
activity.openAssetLink(assetLink, collapse);
});
view.setOnLongClickListener(v -> {
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show();
return true;
});
}
private void syncAssetLinkGroupVisibility() {
if (assetLinkChipGroup == null) return;
boolean hasVisible = false;
for (int i = 0; i < assetLinkChipGroup.getChildCount(); i++) {
View child = assetLinkChipGroup.getChildAt(i);
if (child.getVisibility() == View.VISIBLE) {
hasVisible = true;
break;
}
}
assetLinkChipGroup.setVisibility(hasVisible ? View.VISIBLE : View.GONE);
}
private void setMediaControllerUI(MediaBrowser mediaBrowser) { private void setMediaControllerUI(MediaBrowser mediaBrowser) {
initPlaybackSpeedButton(mediaBrowser); initPlaybackSpeedButton(mediaBrowser);
@ -548,4 +665,4 @@ public class PlayerControllerFragment extends Fragment {
isServiceBound = false; isServiceBound = false;
} }
} }
} }

View file

@ -30,6 +30,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog; import com.cappielloantonio.tempo.ui.dialog.PlaylistChooserDialog;
import com.cappielloantonio.tempo.ui.dialog.RatingDialog; import com.cappielloantonio.tempo.ui.dialog.RatingDialog;
import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.Constants; import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.DownloadUtil; import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.ExternalAudioReader; import com.cappielloantonio.tempo.util.ExternalAudioReader;
@ -39,6 +40,8 @@ import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.HomeViewModel; import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel; import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
import com.google.android.material.chip.Chip;
import com.google.android.material.chip.ChipGroup;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import android.content.Intent; import android.content.Intent;
@ -56,6 +59,13 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
private TextView downloadButton; private TextView downloadButton;
private TextView removeButton; private TextView removeButton;
private ChipGroup assetLinkChipGroup;
private Chip songLinkChip;
private Chip albumLinkChip;
private Chip artistLinkChip;
private AssetLinkUtil.AssetLink currentSongLink;
private AssetLinkUtil.AssetLink currentAlbumLink;
private AssetLinkUtil.AssetLink currentArtistLink;
private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture; private ListenableFuture<MediaBrowser> mediaBrowserListenableFuture;
@ -109,6 +119,11 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
TextView artistSong = view.findViewById(R.id.song_artist_text_view); TextView artistSong = view.findViewById(R.id.song_artist_text_view);
artistSong.setText(songBottomSheetViewModel.getSong().getArtist()); artistSong.setText(songBottomSheetViewModel.getSong().getArtist());
initAssetLinkChips(view);
bindAssetLinkView(coverSong, currentSongLink);
bindAssetLinkView(titleSong, currentSongLink);
bindAssetLinkView(artistSong, currentArtistLink != null ? currentArtistLink : currentSongLink);
ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite); ToggleButton favoriteToggle = view.findViewById(R.id.button_favorite);
favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null); favoriteToggle.setChecked(songBottomSheetViewModel.getSong().getStarred() != null);
favoriteToggle.setOnClickListener(v -> { favoriteToggle.setOnClickListener(v -> {
@ -282,6 +297,95 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
} }
} }
private void initAssetLinkChips(View root) {
assetLinkChipGroup = root.findViewById(R.id.asset_link_chip_group);
songLinkChip = root.findViewById(R.id.asset_link_song_chip);
albumLinkChip = root.findViewById(R.id.asset_link_album_chip);
artistLinkChip = root.findViewById(R.id.asset_link_artist_chip);
currentSongLink = bindAssetLinkChip(songLinkChip, AssetLinkUtil.TYPE_SONG, song.getId());
currentAlbumLink = bindAssetLinkChip(albumLinkChip, AssetLinkUtil.TYPE_ALBUM, song.getAlbumId());
currentArtistLink = bindAssetLinkChip(artistLinkChip, AssetLinkUtil.TYPE_ARTIST, song.getArtistId());
syncAssetLinkGroupVisibility();
}
private AssetLinkUtil.AssetLink bindAssetLinkChip(@Nullable Chip chip, String type, @Nullable String id) {
if (chip == null) return null;
if (id == null || id.isEmpty()) {
clearAssetLinkChip(chip);
return null;
}
String label = getString(AssetLinkUtil.getLabelRes(type));
AssetLinkUtil.AssetLink assetLink = AssetLinkUtil.buildAssetLink(type, id);
if (assetLink == null) {
clearAssetLinkChip(chip);
return null;
}
chip.setText(getString(R.string.asset_link_chip_text, label, assetLink.id));
chip.setVisibility(View.VISIBLE);
chip.setOnClickListener(v -> {
if (assetLink != null) {
((MainActivity) requireActivity()).openAssetLink(assetLink);
}
});
chip.setOnLongClickListener(v -> {
if (assetLink != null) {
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, id), Toast.LENGTH_SHORT).show();
}
return true;
});
return assetLink;
}
private void clearAssetLinkChip(@Nullable Chip chip) {
if (chip == null) return;
chip.setVisibility(View.GONE);
chip.setText("");
chip.setOnClickListener(null);
chip.setOnLongClickListener(null);
}
private void syncAssetLinkGroupVisibility() {
if (assetLinkChipGroup == null) return;
boolean hasVisible = false;
for (int i = 0; i < assetLinkChipGroup.getChildCount(); i++) {
View child = assetLinkChipGroup.getChildAt(i);
if (child.getVisibility() == View.VISIBLE) {
hasVisible = true;
break;
}
}
assetLinkChipGroup.setVisibility(hasVisible ? View.VISIBLE : View.GONE);
}
private void bindAssetLinkView(@Nullable View view, @Nullable AssetLinkUtil.AssetLink assetLink) {
if (view == null) return;
if (assetLink == null) {
AssetLinkUtil.clearLinkAppearance(view);
view.setOnClickListener(null);
view.setOnLongClickListener(null);
view.setClickable(false);
view.setLongClickable(false);
return;
}
view.setClickable(true);
view.setLongClickable(true);
AssetLinkUtil.applyLinkAppearance(view);
view.setOnClickListener(v -> ((MainActivity) requireActivity()).openAssetLink(assetLink, !AssetLinkUtil.TYPE_SONG.equals(assetLink.type)));
view.setOnLongClickListener(v -> {
AssetLinkUtil.copyToClipboard(requireContext(), assetLink);
Toast.makeText(requireContext(), getString(R.string.asset_link_copied_toast, assetLink.id), Toast.LENGTH_SHORT).show();
return true;
});
}
private void initializeMediaBrowser() { private void initializeMediaBrowser() {
mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync(); mediaBrowserListenableFuture = new MediaBrowser.Builder(requireContext(), new SessionToken(requireContext(), new ComponentName(requireContext(), MediaService.class))).buildAsync();
} }
@ -293,4 +397,4 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
private void refreshShares() { private void refreshShares() {
homeViewModel.refreshShares(requireActivity()); homeViewModel.refreshShares(requireActivity());
} }
} }

View file

@ -0,0 +1,188 @@
package com.cappielloantonio.tempo.util;
import android.os.Bundle;
import android.widget.Toast;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Observer;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.NavController;
import androidx.navigation.NavOptions;
import com.cappielloantonio.tempo.BuildConfig;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.repository.AlbumRepository;
import com.cappielloantonio.tempo.repository.ArtistRepository;
import com.cappielloantonio.tempo.repository.PlaylistRepository;
import com.cappielloantonio.tempo.repository.SongRepository;
import com.cappielloantonio.tempo.subsonic.models.AlbumID3;
import com.cappielloantonio.tempo.subsonic.models.ArtistID3;
import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.subsonic.models.Playlist;
import com.cappielloantonio.tempo.subsonic.models.Genre;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.fragment.bottomsheetdialog.SongBottomSheetDialog;
import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
public final class AssetLinkNavigator {
private final MainActivity activity;
private final SongRepository songRepository = new SongRepository();
private final AlbumRepository albumRepository = new AlbumRepository();
private final ArtistRepository artistRepository = new ArtistRepository();
private final PlaylistRepository playlistRepository = new PlaylistRepository();
public AssetLinkNavigator(@NonNull MainActivity activity) {
this.activity = activity;
}
public void open(@Nullable AssetLinkUtil.AssetLink assetLink) {
if (assetLink == null) {
return;
}
switch (assetLink.type) {
case AssetLinkUtil.TYPE_SONG:
openSong(assetLink.id);
break;
case AssetLinkUtil.TYPE_ALBUM:
openAlbum(assetLink.id);
break;
case AssetLinkUtil.TYPE_ARTIST:
openArtist(assetLink.id);
break;
case AssetLinkUtil.TYPE_PLAYLIST:
openPlaylist(assetLink.id);
break;
case AssetLinkUtil.TYPE_GENRE:
openGenre(assetLink.id);
break;
case AssetLinkUtil.TYPE_YEAR:
openYear(assetLink.id);
break;
default:
Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
break;
}
}
private void openSong(@NonNull String id) {
MutableLiveData<Child> liveData = songRepository.getSong(id);
Observer<Child> observer = new Observer<Child>() {
@Override
public void onChanged(Child child) {
liveData.removeObserver(this);
if (child == null) {
Toast.makeText(activity, R.string.asset_link_error_song, Toast.LENGTH_SHORT).show();
return;
}
SongBottomSheetViewModel viewModel = new ViewModelProvider(activity).get(SongBottomSheetViewModel.class);
viewModel.setSong(child);
SongBottomSheetDialog dialog = new SongBottomSheetDialog();
Bundle args = new Bundle();
args.putParcelable(Constants.TRACK_OBJECT, child);
dialog.setArguments(args);
dialog.show(activity.getSupportFragmentManager(), null);
}
};
liveData.observe(activity, observer);
}
private void openAlbum(@NonNull String id) {
MutableLiveData<AlbumID3> liveData = albumRepository.getAlbum(id);
Observer<AlbumID3> observer = new Observer<AlbumID3>() {
@Override
public void onChanged(AlbumID3 album) {
liveData.removeObserver(this);
if (album == null) {
Toast.makeText(activity, R.string.asset_link_error_album, Toast.LENGTH_SHORT).show();
return;
}
Bundle args = new Bundle();
args.putParcelable(Constants.ALBUM_OBJECT, album);
navigateSafely(R.id.albumPageFragment, args);
}
};
liveData.observe(activity, observer);
}
private void openArtist(@NonNull String id) {
MutableLiveData<ArtistID3> liveData = artistRepository.getArtist(id);
Observer<ArtistID3> observer = new Observer<ArtistID3>() {
@Override
public void onChanged(ArtistID3 artist) {
liveData.removeObserver(this);
if (artist == null) {
Toast.makeText(activity, R.string.asset_link_error_artist, Toast.LENGTH_SHORT).show();
return;
}
Bundle args = new Bundle();
args.putParcelable(Constants.ARTIST_OBJECT, artist);
navigateSafely(R.id.artistPageFragment, args);
}
};
liveData.observe(activity, observer);
}
private void openPlaylist(@NonNull String id) {
MutableLiveData<Playlist> liveData = playlistRepository.getPlaylist(id);
Observer<Playlist> observer = new Observer<Playlist>() {
@Override
public void onChanged(Playlist playlist) {
liveData.removeObserver(this);
if (playlist == null) {
Toast.makeText(activity, R.string.asset_link_error_playlist, Toast.LENGTH_SHORT).show();
return;
}
Bundle args = new Bundle();
args.putParcelable(Constants.PLAYLIST_OBJECT, playlist);
navigateSafely(R.id.playlistPageFragment, args);
}
};
liveData.observe(activity, observer);
}
private void openGenre(@NonNull String genreName) {
String trimmed = genreName.trim();
if (trimmed.isEmpty()) {
Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
return;
}
Genre genre = new Genre();
genre.setGenre(trimmed);
genre.setSongCount(0);
genre.setAlbumCount(0);
Bundle args = new Bundle();
args.putParcelable(Constants.GENRE_OBJECT, genre);
args.putString(Constants.MEDIA_BY_GENRE, Constants.MEDIA_BY_GENRE);
navigateSafely(R.id.songListPageFragment, args);
}
private void openYear(@NonNull String yearValue) {
try {
int year = Integer.parseInt(yearValue.trim());
Bundle args = new Bundle();
args.putInt("year_object", year);
args.putString(Constants.MEDIA_BY_YEAR, Constants.MEDIA_BY_YEAR);
navigateSafely(R.id.songListPageFragment, args);
} catch (NumberFormatException ex) {
Toast.makeText(activity, R.string.asset_link_error_unsupported, Toast.LENGTH_SHORT).show();
}
}
private void navigateSafely(int destinationId, @Nullable Bundle args) {
activity.runOnUiThread(() -> {
NavController navController = activity.navController;
if (navController == null) {
return;
}
if (navController.getCurrentDestination() != null
&& navController.getCurrentDestination().getId() == destinationId) {
navController.navigate(destinationId, args, new NavOptions.Builder().setLaunchSingleTop(true).build());
} else {
navController.navigate(destinationId, args);
}
});
}
}

View file

@ -0,0 +1,188 @@
package com.cappielloantonio.tempo.util;
import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.view.View;
import android.widget.TextView;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringRes;
import androidx.core.content.ContextCompat;
import com.cappielloantonio.tempo.R;
import java.util.Objects;
import com.google.android.material.color.MaterialColors;
public final class AssetLinkUtil {
public static final String SCHEME = "tempo";
public static final String HOST_ASSET = "asset";
public static final String TYPE_SONG = "song";
public static final String TYPE_ALBUM = "album";
public static final String TYPE_ARTIST = "artist";
public static final String TYPE_PLAYLIST = "playlist";
public static final String TYPE_GENRE = "genre";
public static final String TYPE_YEAR = "year";
private AssetLinkUtil() {
}
@Nullable
public static AssetLink parse(@Nullable Intent intent) {
if (intent == null) return null;
return parse(intent.getData());
}
@Nullable
public static AssetLink parse(@Nullable Uri uri) {
if (uri == null) {
return null;
}
if (!SCHEME.equalsIgnoreCase(uri.getScheme())) {
return null;
}
String host = uri.getHost();
if (!HOST_ASSET.equalsIgnoreCase(host)) {
return null;
}
if (uri.getPathSegments().size() < 2) {
return null;
}
String type = uri.getPathSegments().get(0);
String id = uri.getPathSegments().get(1);
if (TextUtils.isEmpty(type) || TextUtils.isEmpty(id)) {
return null;
}
if (!isSupportedType(type)) {
return null;
}
return new AssetLink(type, id, uri);
}
public static boolean isSupportedType(@Nullable String type) {
if (type == null) return false;
switch (type) {
case TYPE_SONG:
case TYPE_ALBUM:
case TYPE_ARTIST:
case TYPE_PLAYLIST:
case TYPE_GENRE:
case TYPE_YEAR:
return true;
default:
return false;
}
}
@NonNull
public static Uri buildUri(@NonNull String type, @NonNull String id) {
return new Uri.Builder()
.scheme(SCHEME)
.authority(HOST_ASSET)
.appendPath(type)
.appendPath(id)
.build();
}
@Nullable
public static String buildLink(@Nullable String type, @Nullable String id) {
if (TextUtils.isEmpty(type) || TextUtils.isEmpty(id) || !isSupportedType(type)) {
return null;
}
return buildUri(Objects.requireNonNull(type), Objects.requireNonNull(id)).toString();
}
@Nullable
public static AssetLink buildAssetLink(@Nullable String type, @Nullable String id) {
String link = buildLink(type, id);
return parseLinkString(link);
}
@Nullable
public static AssetLink parseLinkString(@Nullable String link) {
if (TextUtils.isEmpty(link)) {
return null;
}
return parse(Uri.parse(link));
}
public static void copyToClipboard(@NonNull Context context, @NonNull AssetLink assetLink) {
ClipboardManager clipboardManager = (ClipboardManager) context.getSystemService(Context.CLIPBOARD_SERVICE);
if (clipboardManager == null) {
return;
}
ClipData clipData = ClipData.newPlainText(context.getString(R.string.asset_link_clipboard_label), assetLink.uri.toString());
clipboardManager.setPrimaryClip(clipData);
}
@StringRes
public static int getLabelRes(@NonNull String type) {
switch (type) {
case TYPE_SONG:
return R.string.asset_link_label_song;
case TYPE_ALBUM:
return R.string.asset_link_label_album;
case TYPE_ARTIST:
return R.string.asset_link_label_artist;
case TYPE_PLAYLIST:
return R.string.asset_link_label_playlist;
case TYPE_GENRE:
return R.string.asset_link_label_genre;
case TYPE_YEAR:
return R.string.asset_link_label_year;
default:
return R.string.asset_link_label_unknown;
}
}
public static void applyLinkAppearance(@NonNull View view) {
if (view instanceof TextView) {
TextView textView = (TextView) view;
if (textView.getTag(R.id.tag_link_original_color) == null) {
textView.setTag(R.id.tag_link_original_color, textView.getCurrentTextColor());
}
int accent = MaterialColors.getColor(view, com.google.android.material.R.attr.colorPrimary,
ContextCompat.getColor(view.getContext(), android.R.color.holo_blue_light));
textView.setTextColor(accent);
}
}
public static void clearLinkAppearance(@NonNull View view) {
if (view instanceof TextView) {
TextView textView = (TextView) view;
Object original = textView.getTag(R.id.tag_link_original_color);
if (original instanceof Integer) {
textView.setTextColor((Integer) original);
} else {
int defaultColor = MaterialColors.getColor(view, com.google.android.material.R.attr.colorOnSurface,
ContextCompat.getColor(view.getContext(), android.R.color.primary_text_light));
textView.setTextColor(defaultColor);
}
}
}
public static final class AssetLink {
public final String type;
public final String id;
public final Uri uri;
AssetLink(@NonNull String type, @NonNull String id, @NonNull Uri uri) {
this.type = type;
this.id = id;
this.uri = uri;
}
}
}

View file

@ -17,6 +17,9 @@ import com.cappielloantonio.tempo.repository.DownloadRepository;
import com.cappielloantonio.tempo.subsonic.models.Child; import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.activity.MainActivity;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream; import java.io.OutputStream;
import java.net.HttpURLConnection; import java.net.HttpURLConnection;
@ -102,35 +105,76 @@ public class ExternalAudioWriter {
ExternalDownloadMetadataStore.remove(metadataKey); ExternalDownloadMetadataStore.remove(metadataKey);
return; return;
} }
String scheme = mediaUri.getScheme();
if (scheme == null || (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) { String scheme = mediaUri.getScheme() != null ? mediaUri.getScheme().toLowerCase(Locale.ROOT) : "";
notifyFailure(context, "Unsupported media URI.");
ExternalDownloadMetadataStore.remove(metadataKey);
return;
}
HttpURLConnection connection = null; HttpURLConnection connection = null;
DocumentFile sourceDocument = null;
File sourceFile = null;
long remoteLength = -1;
String mimeType = null;
DocumentFile targetFile = null; DocumentFile targetFile = null;
try {
connection = (HttpURLConnection) new URL(mediaUri.toString()).openConnection();
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
connection.setReadTimeout(READ_TIMEOUT_MS);
connection.setRequestProperty("Accept-Encoding", "identity");
connection.connect();
int responseCode = connection.getResponseCode(); try {
if (responseCode >= HttpURLConnection.HTTP_BAD_REQUEST) { if (scheme.equals("http") || scheme.equals("https")) {
notifyFailure(context, "Server returned " + responseCode); connection = (HttpURLConnection) new URL(mediaUri.toString()).openConnection();
connection.setConnectTimeout(CONNECT_TIMEOUT_MS);
connection.setReadTimeout(READ_TIMEOUT_MS);
connection.setRequestProperty("Accept-Encoding", "identity");
connection.connect();
int responseCode = connection.getResponseCode();
if (responseCode >= HttpURLConnection.HTTP_BAD_REQUEST) {
notifyFailure(context, "Server returned " + responseCode);
ExternalDownloadMetadataStore.remove(metadataKey);
return;
}
mimeType = connection.getContentType();
remoteLength = connection.getContentLengthLong();
} else if (scheme.equals("content")) {
sourceDocument = DocumentFile.fromSingleUri(context, mediaUri);
mimeType = context.getContentResolver().getType(mediaUri);
if (sourceDocument != null) {
remoteLength = sourceDocument.length();
}
} else if (scheme.equals("file")) {
String path = mediaUri.getPath();
if (path != null) {
sourceFile = new File(path);
if (sourceFile.exists()) {
remoteLength = sourceFile.length();
}
}
String ext = MimeTypeMap.getFileExtensionFromUrl(mediaUri.toString());
if (ext != null && !ext.isEmpty()) {
mimeType = MimeTypeMap.getSingleton().getMimeTypeFromExtension(ext);
}
} else {
notifyFailure(context, "Unsupported media URI.");
ExternalDownloadMetadataStore.remove(metadataKey); ExternalDownloadMetadataStore.remove(metadataKey);
return; return;
} }
String mimeType = connection.getContentType();
if (mimeType == null || mimeType.isEmpty()) { if (mimeType == null || mimeType.isEmpty()) {
mimeType = "application/octet-stream"; mimeType = "application/octet-stream";
} }
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType); String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
if ((extension == null || extension.isEmpty()) && sourceDocument != null && sourceDocument.getName() != null) {
String name = sourceDocument.getName();
int dot = name.lastIndexOf('.');
if (dot >= 0 && dot < name.length() - 1) {
extension = name.substring(dot + 1);
}
}
if ((extension == null || extension.isEmpty()) && sourceFile != null) {
String name = sourceFile.getName();
int dot = name.lastIndexOf('.');
if (dot >= 0 && dot < name.length() - 1) {
extension = name.substring(dot + 1);
}
}
if (extension == null || extension.isEmpty()) { if (extension == null || extension.isEmpty()) {
String suffix = child.getSuffix(); String suffix = child.getSuffix();
if (suffix != null && !suffix.isEmpty()) { if (suffix != null && !suffix.isEmpty()) {
@ -146,7 +190,6 @@ public class ExternalAudioWriter {
String fileName = sanitized + "." + extension; String fileName = sanitized + "." + extension;
DocumentFile existingFile = findFile(directory, fileName); DocumentFile existingFile = findFile(directory, fileName);
long remoteLength = connection.getContentLengthLong();
Long recordedSize = ExternalDownloadMetadataStore.getSize(metadataKey); Long recordedSize = ExternalDownloadMetadataStore.getSize(metadataKey);
if (existingFile != null && existingFile.exists()) { if (existingFile != null && existingFile.exists()) {
long localLength = existingFile.length(); long localLength = existingFile.length();
@ -175,7 +218,7 @@ public class ExternalAudioWriter {
} }
Uri targetUri = targetFile.getUri(); Uri targetUri = targetFile.getUri();
try (InputStream in = connection.getInputStream(); try (InputStream in = openInputStream(context, mediaUri, scheme, connection, sourceFile);
OutputStream out = context.getContentResolver().openOutputStream(targetUri)) { OutputStream out = context.getContentResolver().openOutputStream(targetUri)) {
if (out == null) { if (out == null) {
notifyFailure(context, "Cannot open output stream."); notifyFailure(context, "Cannot open output stream.");
@ -319,4 +362,32 @@ public class ExternalAudioWriter {
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
); );
} }
private static InputStream openInputStream(Context context,
Uri mediaUri,
String scheme,
HttpURLConnection connection,
File sourceFile) throws IOException {
switch (scheme) {
case "http":
case "https":
if (connection == null) {
throw new IOException("Connection not initialized");
}
return connection.getInputStream();
case "content":
InputStream contentStream = context.getContentResolver().openInputStream(mediaUri);
if (contentStream == null) {
throw new IOException("Cannot open content stream");
}
return contentStream;
case "file":
if (sourceFile == null || !sourceFile.exists()) {
throw new IOException("Missing source file");
}
return new FileInputStream(sourceFile);
default:
throw new IOException("Unsupported scheme " + scheme);
}
}
} }

View file

@ -74,6 +74,12 @@ public class MappingUtil {
bundle.putInt("originalWidth", media.getOriginalWidth() != null ? media.getOriginalWidth() : 0); bundle.putInt("originalWidth", media.getOriginalWidth() != null ? media.getOriginalWidth() : 0);
bundle.putInt("originalHeight", media.getOriginalHeight() != null ? media.getOriginalHeight() : 0); bundle.putInt("originalHeight", media.getOriginalHeight() != null ? media.getOriginalHeight() : 0);
bundle.putString("uri", uri.toString()); bundle.putString("uri", uri.toString());
bundle.putString("assetLinkSong", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, media.getId()));
bundle.putString("assetLinkAlbum", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, media.getAlbumId()));
bundle.putString("assetLinkArtist", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, media.getArtistId()));
bundle.putString("assetLinkGenre", AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_GENRE, media.getGenre()));
Integer year = media.getYear();
bundle.putString("assetLinkYear", year != null && year != 0 ? AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_YEAR, String.valueOf(year)) : null);
return new MediaItem.Builder() return new MediaItem.Builder()
.setMediaId(media.getId()) .setMediaId(media.getId())

View file

@ -5,17 +5,20 @@ import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider; import android.appwidget.AppWidgetProvider;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.text.TextUtils;
import android.widget.RemoteViews; import android.widget.RemoteViews;
import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.R;
import android.app.TaskStackBuilder; import android.app.TaskStackBuilder;
import android.app.PendingIntent;
import com.cappielloantonio.tempo.ui.activity.MainActivity; import com.cappielloantonio.tempo.ui.activity.MainActivity;
import android.util.Log; import android.util.Log;
import androidx.annotation.Nullable;
public class WidgetProvider extends AppWidgetProvider { public class WidgetProvider extends AppWidgetProvider {
private static final String TAG = "TempoWidget"; private static final String TAG = "TempoWidget";
public static final String ACT_PLAY_PAUSE = "tempo.widget.PLAY_PAUSE"; public static final String ACT_PLAY_PAUSE = "tempo.widget.PLAY_PAUSE";
@ -28,7 +31,7 @@ public class WidgetProvider extends AppWidgetProvider {
public void onUpdate(Context ctx, AppWidgetManager mgr, int[] ids) { public void onUpdate(Context ctx, AppWidgetManager mgr, int[] ids) {
for (int id : ids) { for (int id : ids) {
RemoteViews rv = WidgetUpdateManager.chooseBuild(ctx, id); RemoteViews rv = WidgetUpdateManager.chooseBuild(ctx, id);
attachIntents(ctx, rv, id); attachIntents(ctx, rv, id, null, null, null);
mgr.updateAppWidget(id, rv); mgr.updateAppWidget(id, rv);
} }
} }
@ -50,16 +53,23 @@ public class WidgetProvider extends AppWidgetProvider {
public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, android.os.Bundle newOptions) { public void onAppWidgetOptionsChanged(Context context, AppWidgetManager appWidgetManager, int appWidgetId, android.os.Bundle newOptions) {
super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions); super.onAppWidgetOptionsChanged(context, appWidgetManager, appWidgetId, newOptions);
RemoteViews rv = WidgetUpdateManager.chooseBuild(context, appWidgetId); RemoteViews rv = WidgetUpdateManager.chooseBuild(context, appWidgetId);
attachIntents(context, rv, appWidgetId); attachIntents(context, rv, appWidgetId, null, null, null);
appWidgetManager.updateAppWidget(appWidgetId, rv); appWidgetManager.updateAppWidget(appWidgetId, rv);
WidgetUpdateManager.refreshFromController(context); WidgetUpdateManager.refreshFromController(context);
} }
public static void attachIntents(Context ctx, RemoteViews rv) { public static void attachIntents(Context ctx, RemoteViews rv) {
attachIntents(ctx, rv, 0); attachIntents(ctx, rv, 0, null, null, null);
} }
public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase) { public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase) {
attachIntents(ctx, rv, requestCodeBase, null, null, null);
}
public static void attachIntents(Context ctx, RemoteViews rv, int requestCodeBase,
String songLink,
String albumLink,
String artistLink) {
PendingIntent playPause = PendingIntent.getBroadcast( PendingIntent playPause = PendingIntent.getBroadcast(
ctx, ctx,
requestCodeBase + 0, requestCodeBase + 0,
@ -97,9 +107,31 @@ public class WidgetProvider extends AppWidgetProvider {
rv.setOnClickPendingIntent(R.id.btn_shuffle, shuffle); rv.setOnClickPendingIntent(R.id.btn_shuffle, shuffle);
rv.setOnClickPendingIntent(R.id.btn_repeat, repeat); rv.setOnClickPendingIntent(R.id.btn_repeat, repeat);
PendingIntent launch = TaskStackBuilder.create(ctx) PendingIntent launch = buildMainActivityPendingIntent(ctx, requestCodeBase + 10, null);
.addNextIntentWithParentStack(new Intent(ctx, MainActivity.class))
.getPendingIntent(requestCodeBase + 10, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
rv.setOnClickPendingIntent(R.id.root, launch); rv.setOnClickPendingIntent(R.id.root, launch);
PendingIntent songPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 20, songLink);
PendingIntent artistPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 21, artistLink);
PendingIntent albumPending = buildMainActivityPendingIntent(ctx, requestCodeBase + 22, albumLink);
PendingIntent fallback = launch;
rv.setOnClickPendingIntent(R.id.album_art, songPending != null ? songPending : fallback);
rv.setOnClickPendingIntent(R.id.title, songPending != null ? songPending : fallback);
rv.setOnClickPendingIntent(R.id.subtitle,
artistPending != null ? artistPending : (songPending != null ? songPending : fallback));
rv.setOnClickPendingIntent(R.id.album, albumPending != null ? albumPending : fallback);
}
private static PendingIntent buildMainActivityPendingIntent(Context ctx, int requestCode, @Nullable String link) {
Intent intent;
if (!TextUtils.isEmpty(link)) {
intent = new Intent(Intent.ACTION_VIEW, Uri.parse(link), ctx, MainActivity.class);
} else {
intent = new Intent(ctx, MainActivity.class);
}
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
TaskStackBuilder stackBuilder = TaskStackBuilder.create(ctx);
stackBuilder.addNextIntentWithParentStack(intent);
return stackBuilder.getPendingIntent(requestCode, PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT);
} }
} }

View file

@ -4,8 +4,9 @@ import android.appwidget.AppWidgetManager;
import android.content.ComponentName; import android.content.ComponentName;
import android.content.Context; import android.content.Context;
import android.graphics.Bitmap; import android.graphics.Bitmap;
import android.text.TextUtils;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.os.Bundle;
import android.text.TextUtils;
import com.bumptech.glide.request.target.CustomTarget; import com.bumptech.glide.request.target.CustomTarget;
import com.bumptech.glide.request.transition.Transition; import com.bumptech.glide.request.transition.Transition;
@ -17,6 +18,7 @@ import androidx.media3.session.MediaController;
import androidx.media3.session.SessionToken; import androidx.media3.session.SessionToken;
import com.cappielloantonio.tempo.service.MediaService; import com.cappielloantonio.tempo.service.MediaService;
import com.cappielloantonio.tempo.util.AssetLinkUtil;
import com.cappielloantonio.tempo.util.MusicUtil; import com.cappielloantonio.tempo.util.MusicUtil;
import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFuture;
import com.google.common.util.concurrent.MoreExecutors; import com.google.common.util.concurrent.MoreExecutors;
@ -34,7 +36,10 @@ public final class WidgetUpdateManager {
boolean shuffleEnabled, boolean shuffleEnabled,
int repeatMode, int repeatMode,
long positionMs, long positionMs,
long durationMs) { long durationMs,
String songLink,
String albumLink,
String artistLink) {
if (TextUtils.isEmpty(title)) title = ctx.getString(R.string.widget_not_playing); if (TextUtils.isEmpty(title)) title = ctx.getString(R.string.widget_not_playing);
if (TextUtils.isEmpty(artist)) artist = ctx.getString(R.string.widget_placeholder_subtitle); if (TextUtils.isEmpty(artist)) artist = ctx.getString(R.string.widget_placeholder_subtitle);
if (TextUtils.isEmpty(album)) album = ""; if (TextUtils.isEmpty(album)) album = "";
@ -46,7 +51,7 @@ public final class WidgetUpdateManager {
for (int id : ids) { for (int id : ids) {
android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, album, art, playing, android.widget.RemoteViews rv = choosePopulate(ctx, title, artist, album, art, playing,
timing.elapsedText, timing.totalText, timing.progress, shuffleEnabled, repeatMode, id); timing.elapsedText, timing.totalText, timing.progress, shuffleEnabled, repeatMode, id);
WidgetProvider.attachIntents(ctx, rv, id); WidgetProvider.attachIntents(ctx, rv, id, songLink, albumLink, artistLink);
mgr.updateAppWidget(id, rv); mgr.updateAppWidget(id, rv);
} }
} }
@ -56,7 +61,7 @@ public final class WidgetUpdateManager {
int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class)); int[] ids = mgr.getAppWidgetIds(new ComponentName(ctx, WidgetProvider4x1.class));
for (int id : ids) { for (int id : ids) {
android.widget.RemoteViews rv = chooseBuild(ctx, id); android.widget.RemoteViews rv = chooseBuild(ctx, id);
WidgetProvider.attachIntents(ctx, rv, id); WidgetProvider.attachIntents(ctx, rv, id, null, null, null);
mgr.updateAppWidget(id, rv); mgr.updateAppWidget(id, rv);
} }
} }
@ -70,7 +75,10 @@ public final class WidgetUpdateManager {
boolean shuffleEnabled, boolean shuffleEnabled,
int repeatMode, int repeatMode,
long positionMs, long positionMs,
long durationMs) { long durationMs,
String songLink,
String albumLink,
String artistLink) {
final Context appCtx = ctx.getApplicationContext(); final Context appCtx = ctx.getApplicationContext();
final String t = TextUtils.isEmpty(title) ? appCtx.getString(R.string.widget_not_playing) : title; final String t = TextUtils.isEmpty(title) ? appCtx.getString(R.string.widget_not_playing) : title;
final String a = TextUtils.isEmpty(artist) ? appCtx.getString(R.string.widget_placeholder_subtitle) : artist; final String a = TextUtils.isEmpty(artist) ? appCtx.getString(R.string.widget_placeholder_subtitle) : artist;
@ -79,6 +87,9 @@ public final class WidgetUpdateManager {
final boolean sh = shuffleEnabled; final boolean sh = shuffleEnabled;
final int rep = repeatMode; final int rep = repeatMode;
final TimingInfo timing = createTimingInfo(positionMs, durationMs); final TimingInfo timing = createTimingInfo(positionMs, durationMs);
final String songLinkFinal = songLink;
final String albumLinkFinal = albumLink;
final String artistLinkFinal = artistLink;
if (!TextUtils.isEmpty(coverArtId)) { if (!TextUtils.isEmpty(coverArtId)) {
CustomGlideRequest.loadAlbumArtBitmap( CustomGlideRequest.loadAlbumArtBitmap(
@ -93,7 +104,7 @@ public final class WidgetUpdateManager {
for (int id : ids) { for (int id : ids) {
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, resource, p, android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, resource, p,
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
WidgetProvider.attachIntents(appCtx, rv, id); WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
mgr.updateAppWidget(id, rv); mgr.updateAppWidget(id, rv);
} }
} }
@ -105,7 +116,7 @@ public final class WidgetUpdateManager {
for (int id : ids) { for (int id : ids) {
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p, android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
WidgetProvider.attachIntents(appCtx, rv, id); WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
mgr.updateAppWidget(id, rv); mgr.updateAppWidget(id, rv);
} }
} }
@ -117,7 +128,7 @@ public final class WidgetUpdateManager {
for (int id : ids) { for (int id : ids) {
android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p, android.widget.RemoteViews rv = choosePopulate(appCtx, t, a, alb, null, p,
timing.elapsedText, timing.totalText, timing.progress, sh, rep, id); timing.elapsedText, timing.totalText, timing.progress, sh, rep, id);
WidgetProvider.attachIntents(appCtx, rv, id); WidgetProvider.attachIntents(appCtx, rv, id, songLinkFinal, albumLinkFinal, artistLinkFinal);
mgr.updateAppWidget(id, rv); mgr.updateAppWidget(id, rv);
} }
} }
@ -133,6 +144,7 @@ public final class WidgetUpdateManager {
MediaController c = future.get(); MediaController c = future.get();
androidx.media3.common.MediaItem mi = c.getCurrentMediaItem(); androidx.media3.common.MediaItem mi = c.getCurrentMediaItem();
String title = null, artist = null, album = null, coverId = null; String title = null, artist = null, album = null, coverId = null;
String songLink = null, albumLink = null, artistLink = null;
if (mi != null && mi.mediaMetadata != null) { if (mi != null && mi.mediaMetadata != null) {
if (mi.mediaMetadata.title != null) title = mi.mediaMetadata.title.toString(); if (mi.mediaMetadata.title != null) title = mi.mediaMetadata.title.toString();
if (mi.mediaMetadata.artist != null) if (mi.mediaMetadata.artist != null)
@ -140,10 +152,26 @@ public final class WidgetUpdateManager {
if (mi.mediaMetadata.albumTitle != null) if (mi.mediaMetadata.albumTitle != null)
album = mi.mediaMetadata.albumTitle.toString(); album = mi.mediaMetadata.albumTitle.toString();
if (mi.mediaMetadata.extras != null) { if (mi.mediaMetadata.extras != null) {
Bundle extras = mi.mediaMetadata.extras;
if (title == null) title = mi.mediaMetadata.extras.getString("title"); if (title == null) title = mi.mediaMetadata.extras.getString("title");
if (artist == null) artist = mi.mediaMetadata.extras.getString("artist"); if (artist == null) artist = mi.mediaMetadata.extras.getString("artist");
if (album == null) album = mi.mediaMetadata.extras.getString("album"); if (album == null) album = mi.mediaMetadata.extras.getString("album");
coverId = mi.mediaMetadata.extras.getString("coverArtId"); coverId = extras.getString("coverArtId");
songLink = extras.getString("assetLinkSong");
if (songLink == null) {
songLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras.getString("id"));
}
albumLink = extras.getString("assetLinkAlbum");
if (albumLink == null) {
albumLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras.getString("albumId"));
}
artistLink = extras.getString("assetLinkArtist");
if (artistLink == null) {
artistLink = AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras.getString("artistId"));
}
} }
} }
long position = c.getCurrentPosition(); long position = c.getCurrentPosition();
@ -159,7 +187,10 @@ public final class WidgetUpdateManager {
c.getShuffleModeEnabled(), c.getShuffleModeEnabled(),
c.getRepeatMode(), c.getRepeatMode(),
position, position,
duration); duration,
songLink,
albumLink,
artistLink);
c.release(); c.release();
} catch (ExecutionException | InterruptedException ignored) { } catch (ExecutionException | InterruptedException ignored) {
} }

View file

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="?attr/colorOnSurfaceVariant"
android:pathData="M3.9,12c0,1.71 1.39,3.1 3.1,3.1h3v1.8h-3c-2.7,0 -4.9,-2.2 -4.9,-4.9s2.2,-4.9 4.9,-4.9h3v1.8h-3c-1.71,0 -3.1,1.39 -3.1,3.1zM7,13h10v-2H7v2zM17,6.9h-3v-1.8h3c2.7,0 4.9,2.2 4.9,4.9s-2.2,4.9 -4.9,4.9h-3v-1.8h3c1.71,0 3.1,-1.39 3.1,-3.1s-1.39,-3.1 -3.1,-3.1z" />
</vector>

View file

@ -68,6 +68,14 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/song_asset_link_row"
layout="@layout/view_asset_link_row"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingStart="20dp"
android:paddingEnd="12dp" />
<LinearLayout <LinearLayout
android:id="@+id/option_linear_layout" android:id="@+id/option_linear_layout"
android:layout_width="match_parent" android:layout_width="match_parent"
@ -209,4 +217,4 @@
android:text="@string/song_bottom_sheet_share" android:text="@string/song_bottom_sheet_share"
android:visibility="gone"/> android:visibility="gone"/>
</LinearLayout> </LinearLayout>
</LinearLayout> </LinearLayout>

View file

@ -57,6 +57,17 @@
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
<include
android:id="@+id/player_asset_link_row"
layout="@layout/view_asset_link_row"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/player_media_quality_sector" />
<androidx.viewpager2.widget.ViewPager2 <androidx.viewpager2.widget.ViewPager2
android:id="@+id/player_media_cover_view_pager" android:id="@+id/player_media_cover_view_pager"
android:layout_width="0dp" android:layout_width="0dp"
@ -66,7 +77,7 @@
app:layout_constraintBottom_toTopOf="@id/guideline" app:layout_constraintBottom_toTopOf="@id/guideline"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/player_media_quality_sector" /> app:layout_constraintTop_toBottomOf="@+id/player_asset_link_row" />
<androidx.constraintlayout.widget.Guideline <androidx.constraintlayout.widget.Guideline
android:id="@+id/guideline" android:id="@+id/guideline"
@ -400,4 +411,4 @@
app:srcCompat="@drawable/ic_eq" /> app:srcCompat="@drawable/ic_eq" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.chip.ChipGroup xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:id="@+id/asset_link_chip_group"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:visibility="gone"
android:paddingTop="4dp"
android:paddingBottom="4dp"
app:singleLine="true"
app:selectionRequired="false"
app:singleSelection="false">
<com.google.android.material.chip.Chip
android:id="@+id/asset_link_song_chip"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="false"
android:clickable="true"
android:ellipsize="end"
android:maxLines="1"
android:text=""
app:chipIcon="@drawable/ic_link"
app:chipIconTint="?attr/colorOnSurfaceVariant"
app:rippleColor="@color/ripple_material_light" />
<com.google.android.material.chip.Chip
android:id="@+id/asset_link_album_chip"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="false"
android:clickable="true"
android:ellipsize="end"
android:maxLines="1"
android:text=""
app:chipIcon="@drawable/ic_link"
app:chipIconTint="?attr/colorOnSurfaceVariant"
app:rippleColor="@color/ripple_material_light" />
<com.google.android.material.chip.Chip
android:id="@+id/asset_link_artist_chip"
style="@style/Widget.Material3.Chip.Assist"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:checkable="false"
android:clickable="true"
android:ellipsize="end"
android:maxLines="1"
android:text=""
app:chipIcon="@drawable/ic_link"
app:chipIconTint="?attr/colorOnSurfaceVariant"
app:rippleColor="@color/ripple_material_light" />
</com.google.android.material.chip.ChipGroup>

View file

@ -0,0 +1,3 @@
<resources>
<item name="tag_link_original_color" type="id" />
</resources>

View file

@ -410,6 +410,22 @@
<string name="share_bottom_sheet_update">Update share</string> <string name="share_bottom_sheet_update">Update share</string>
<string name="share_subtitle_item">Expiration date: %1$s</string> <string name="share_subtitle_item">Expiration date: %1$s</string>
<string name="share_unsupported_error">Sharing is not supported or not enabled</string> <string name="share_unsupported_error">Sharing is not supported or not enabled</string>
<string name="asset_link_clipboard_label">Tempo asset link</string>
<string name="asset_link_label_song">Song UID</string>
<string name="asset_link_label_album">Album UID</string>
<string name="asset_link_label_artist">Artist UID</string>
<string name="asset_link_label_playlist">Playlist UID</string>
<string name="asset_link_label_genre">Genre UID</string>
<string name="asset_link_label_year">Year UID</string>
<string name="asset_link_label_unknown">Asset UID</string>
<string name="asset_link_error_unsupported">Unsupported asset link</string>
<string name="asset_link_error_song">Song could not be opened</string>
<string name="asset_link_error_album">Album could not be opened</string>
<string name="asset_link_error_artist">Artist could not be opened</string>
<string name="asset_link_error_playlist">Playlist could not be opened</string>
<string name="asset_link_chip_text">%1$s • %2$s</string>
<string name="asset_link_copied_toast">Copied %1$s to clipboard</string>
<string name="asset_link_debug_toast">Asset link: %1$s</string>
<string name="share_update_dialog_hint_description">Description</string> <string name="share_update_dialog_hint_description">Description</string>
<string name="share_update_dialog_hint_expiration_date">Expiration date</string> <string name="share_update_dialog_hint_expiration_date">Expiration date</string>
<string name="share_update_dialog_negative_button">Cancel</string> <string name="share_update_dialog_negative_button">Cancel</string>

View file

@ -20,6 +20,7 @@ import androidx.media3.session.*
import androidx.media3.session.MediaSession.ControllerInfo import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.tempo.R import com.cappielloantonio.tempo.R
import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.ui.activity.MainActivity
import com.cappielloantonio.tempo.util.AssetLinkUtil
import com.cappielloantonio.tempo.util.Constants import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.DownloadUtil import com.cappielloantonio.tempo.util.DownloadUtil
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
@ -421,7 +422,14 @@ class MediaService : MediaLibraryService() {
?: mi?.mediaMetadata?.extras?.getString("artist") ?: mi?.mediaMetadata?.extras?.getString("artist")
val album = mi?.mediaMetadata?.albumTitle?.toString() val album = mi?.mediaMetadata?.albumTitle?.toString()
?: mi?.mediaMetadata?.extras?.getString("album") ?: mi?.mediaMetadata?.extras?.getString("album")
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId") val extras = mi?.mediaMetadata?.extras
val coverId = extras?.getString("coverArtId")
val songLink = extras?.getString("assetLinkSong")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
val albumLink = extras?.getString("assetLinkAlbum")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
val artistLink = extras?.getString("assetLinkArtist")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
WidgetUpdateManager.updateFromState( WidgetUpdateManager.updateFromState(
@ -434,7 +442,10 @@ class MediaService : MediaLibraryService() {
player.shuffleModeEnabled, player.shuffleModeEnabled,
player.repeatMode, player.repeatMode,
position, position,
duration duration,
songLink,
albumLink,
artistLink
) )
} }

View file

@ -22,6 +22,7 @@ import androidx.media3.session.MediaLibraryService
import androidx.media3.session.MediaSession.ControllerInfo import androidx.media3.session.MediaSession.ControllerInfo
import com.cappielloantonio.tempo.repository.AutomotiveRepository import com.cappielloantonio.tempo.repository.AutomotiveRepository
import com.cappielloantonio.tempo.ui.activity.MainActivity import com.cappielloantonio.tempo.ui.activity.MainActivity
import com.cappielloantonio.tempo.util.AssetLinkUtil
import com.cappielloantonio.tempo.util.Constants import com.cappielloantonio.tempo.util.Constants
import com.cappielloantonio.tempo.util.DownloadUtil import com.cappielloantonio.tempo.util.DownloadUtil
import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory import com.cappielloantonio.tempo.util.DynamicMediaSourceFactory
@ -262,7 +263,14 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
?: mi?.mediaMetadata?.extras?.getString("artist") ?: mi?.mediaMetadata?.extras?.getString("artist")
val album = mi?.mediaMetadata?.albumTitle?.toString() val album = mi?.mediaMetadata?.albumTitle?.toString()
?: mi?.mediaMetadata?.extras?.getString("album") ?: mi?.mediaMetadata?.extras?.getString("album")
val coverId = mi?.mediaMetadata?.extras?.getString("coverArtId") val extras = mi?.mediaMetadata?.extras
val coverId = extras?.getString("coverArtId")
val songLink = extras?.getString("assetLinkSong")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_SONG, extras?.getString("id"))
val albumLink = extras?.getString("assetLinkAlbum")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ALBUM, extras?.getString("albumId"))
val artistLink = extras?.getString("assetLinkArtist")
?: AssetLinkUtil.buildLink(AssetLinkUtil.TYPE_ARTIST, extras?.getString("artistId"))
val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L val position = player.currentPosition.takeIf { it != C.TIME_UNSET } ?: 0L
val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L val duration = player.duration.takeIf { it != C.TIME_UNSET } ?: 0L
WidgetUpdateManager.updateFromState( WidgetUpdateManager.updateFromState(
@ -275,7 +283,10 @@ class MediaService : MediaLibraryService(), SessionAvailabilityListener {
player.shuffleModeEnabled, player.shuffleModeEnabled,
player.repeatMode, player.repeatMode,
position, position,
duration duration,
songLink,
albumLink,
artistLink
) )
} }