feat: Add metadata caching and proper integration for external media files

This commit is contained in:
le-firehawk 2025-09-16 23:22:18 +09:30
parent 24864637f9
commit 682f63ef38
17 changed files with 515 additions and 136 deletions

View file

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

View file

@ -16,6 +16,7 @@ import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
import com.cappielloantonio.tempo.util.DownloadUtil;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.util.ExternalDownloadMetadataStore;
import com.cappielloantonio.tempo.util.Preferences;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
@ -47,7 +48,10 @@ public class DeleteDownloadStorageDialog extends DialogFragment {
if (dialog != null) {
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
positiveButton.setOnClickListener(v -> {
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
}
String uriString = Preferences.getDownloadDirectoryUri();
if (uriString != null) {
DocumentFile directory = DocumentFile.fromTreeUri(requireContext(), Uri.parse(uriString));
@ -57,6 +61,7 @@ public class DeleteDownloadStorageDialog extends DialogFragment {
}
}
ExternalAudioReader.refreshCache();
ExternalDownloadMetadataStore.clear();
}
dialog.dismiss();
});

View file

@ -13,6 +13,7 @@ import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.cappielloantonio.tempo.R;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.util.Preferences;
public class DownloadDirectoryPickerDialog extends DialogFragment {
@ -37,6 +38,7 @@ public class DownloadDirectoryPickerDialog extends DialogFragment {
);
Preferences.setDownloadDirectoryUri(uri.toString());
ExternalAudioReader.refreshCache();
Toast.makeText(requireContext(), "Download directory set:\n" + uri.toString(), Toast.LENGTH_LONG).show();
}

View file

@ -138,10 +138,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
songs.stream().map(Download::new).collect(Collectors.toList())
);
} else {
MappingUtil.mapMediaItems(songs).forEach(media -> {
String title = media.mediaMetadata.title != null ? media.mediaMetadata.title.toString() : media.mediaId;
ExternalAudioWriter.downloadToUserDirectory(requireContext(), media, title);
});
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
}
});
return true;

View file

@ -117,10 +117,7 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
songs.stream().map(Download::new).collect(Collectors.toList())
);
} else {
MappingUtil.mapMediaItems(songs).forEach(media -> {
String title = media.mediaMetadata.title != null ? media.mediaMetadata.title.toString() : media.mediaId;
ExternalAudioWriter.downloadToUserDirectory(requireContext(), media, title);
});
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
}
}
});

View file

@ -28,6 +28,7 @@ import com.cappielloantonio.tempo.subsonic.models.Child;
import com.cappielloantonio.tempo.ui.activity.MainActivity;
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
import com.cappielloantonio.tempo.util.Constants;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
import com.google.android.material.appbar.MaterialToolbar;
@ -289,6 +290,7 @@ public class DownloadFragment extends Fragment implements ClickCallback {
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
);
Preferences.setDownloadDirectoryUri(uri.toString());
ExternalAudioReader.refreshCache();
Toast.makeText(requireContext(), "Download directory set", Toast.LENGTH_SHORT).show();
}
}

View file

@ -123,9 +123,7 @@ public class PlayerCoverFragment extends Fragment {
new Download(song)
);
} else {
MediaItem item = MappingUtil.mapMediaItem(song);
String title = item.mediaMetadata.title != null ? item.mediaMetadata.title.toString() : item.mediaId;
ExternalAudioWriter.downloadToUserDirectory(requireContext(), item, title);
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
}
});

View file

@ -144,19 +144,16 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
if (isVisible() && getActivity() != null) {
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownloads(songs),
songs.stream().map(child -> {
Download toDownload = new Download(child);
toDownload.setPlaylistId(playlistPageViewModel.getPlaylist().getId());
toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName());
return toDownload;
}).collect(Collectors.toList())
MappingUtil.mapDownloads(songs),
songs.stream().map(child -> {
Download toDownload = new Download(child);
toDownload.setPlaylistId(playlistPageViewModel.getPlaylist().getId());
toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName());
return toDownload;
}).collect(Collectors.toList())
);
} else {
MappingUtil.mapMediaItems(songs).forEach(media -> {
String title = media.mediaMetadata.title != null ? media.mediaMetadata.title.toString() : media.mediaId;
ExternalAudioWriter.downloadToUserDirectory(requireContext(), media, title);
});
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
}
}
});

View file

@ -428,6 +428,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
if (current != null) {
Preferences.setDownloadDirectoryUri(null);
Preferences.setDownloadStoragePreference(0);
ExternalAudioReader.refreshCache();
Toast.makeText(requireContext(), "Download folder cleared.", Toast.LENGTH_SHORT).show();
checkStorage();
checkDownloadDirectory();

View file

@ -38,6 +38,7 @@ import com.cappielloantonio.tempo.util.MappingUtil;
import com.cappielloantonio.tempo.util.MusicUtil;
import com.cappielloantonio.tempo.util.Preferences;
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
import com.cappielloantonio.tempo.util.ExternalAudioReader;
import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel;
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
@ -167,10 +168,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
} else {
MappingUtil.mapMediaItems(songs).forEach(media -> {
String title = media.mediaMetadata.title != null ? media.mediaMetadata.title.toString() : media.mediaId;
ExternalAudioWriter.downloadToUserDirectory(requireContext(), media, title);
});
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
}
dismissBottomSheet();
});
@ -196,7 +194,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
removeAll.setOnClickListener(v -> {
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).remove(mediaItems, downloads);
} else {
songs.forEach(ExternalAudioReader::delete);
}
dismissBottomSheet();
});
});
@ -246,8 +248,15 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
if (Preferences.getDownloadDirectoryUri() == null && DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
removeAll.setVisibility(View.VISIBLE);
if (Preferences.getDownloadDirectoryUri() == null) {
if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
removeAll.setVisibility(View.VISIBLE);
} else {
removeAll.setVisibility(View.GONE);
}
} else {
boolean hasLocal = songs.stream().anyMatch(song -> ExternalAudioReader.getUri(song) != null);
removeAll.setVisibility(hasLocal ? View.VISIBLE : View.GONE);
}
});
}

View file

@ -164,15 +164,13 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
TextView download = view.findViewById(R.id.download_text_view);
download.setOnClickListener(v -> {
MediaItem item = MappingUtil.mapMediaItem(song);
String title = item.mediaMetadata.title != null ? item.mediaMetadata.title.toString() : item.mediaId;
if (Preferences.getDownloadDirectoryUri() == null) {
DownloadUtil.getDownloadTracker(requireContext()).download(
MappingUtil.mapDownload(song),
new Download(song)
);
} else {
ExternalAudioWriter.downloadToUserDirectory(requireContext(), item, title);
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
}
dismissBottomSheet();
});