mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 09:33:33 +00:00
feat: Add metadata caching and proper integration for external media files
This commit is contained in:
parent
24864637f9
commit
682f63ef38
17 changed files with 515 additions and 136 deletions
|
|
@ -223,6 +223,25 @@ public class MediaManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void playDownloadedMediaItem(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, MediaItem mediaItem) {
|
||||||
|
if (mediaBrowserListenableFuture != null && mediaItem != null) {
|
||||||
|
mediaBrowserListenableFuture.addListener(() -> {
|
||||||
|
try {
|
||||||
|
if (mediaBrowserListenableFuture.isDone()) {
|
||||||
|
MediaBrowser mediaBrowser = mediaBrowserListenableFuture.get();
|
||||||
|
mediaBrowser.clearMediaItems();
|
||||||
|
mediaBrowser.setMediaItem(mediaItem);
|
||||||
|
mediaBrowser.prepare();
|
||||||
|
mediaBrowser.play();
|
||||||
|
clearDatabase();
|
||||||
|
}
|
||||||
|
} catch (ExecutionException | InterruptedException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}, MoreExecutors.directExecutor());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void startRadio(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, InternetRadioStation internetRadioStation) {
|
public static void startRadio(ListenableFuture<MediaBrowser> mediaBrowserListenableFuture, InternetRadioStation internetRadioStation) {
|
||||||
if (mediaBrowserListenableFuture != null) {
|
if (mediaBrowserListenableFuture != null) {
|
||||||
mediaBrowserListenableFuture.addListener(() -> {
|
mediaBrowserListenableFuture.addListener(() -> {
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,14 @@
|
||||||
package com.cappielloantonio.tempo.ui.activity;
|
package com.cappielloantonio.tempo.ui.activity;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
import android.content.IntentFilter;
|
||||||
import android.net.ConnectivityManager;
|
import android.net.ConnectivityManager;
|
||||||
import android.net.NetworkInfo;
|
import android.net.NetworkInfo;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
|
import android.text.TextUtils;
|
||||||
import android.util.Log;
|
import android.util.Log;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
|
||||||
|
|
@ -13,7 +16,10 @@ import androidx.annotation.NonNull;
|
||||||
import androidx.core.splashscreen.SplashScreen;
|
import androidx.core.splashscreen.SplashScreen;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
import androidx.media3.common.MediaMetadata;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
|
import androidx.media3.common.MimeTypes;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
import androidx.media3.common.util.UnstableApi;
|
||||||
import androidx.navigation.NavController;
|
import androidx.navigation.NavController;
|
||||||
import androidx.navigation.fragment.NavHostFragment;
|
import androidx.navigation.fragment.NavHostFragment;
|
||||||
|
|
@ -56,6 +62,7 @@ public class MainActivity extends BaseActivity {
|
||||||
private BottomSheetBehavior bottomSheetBehavior;
|
private BottomSheetBehavior bottomSheetBehavior;
|
||||||
|
|
||||||
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
ConnectivityStatusBroadcastReceiver connectivityStatusBroadcastReceiver;
|
||||||
|
private Intent pendingDownloadPlaybackIntent;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedInstanceState) {
|
protected void onCreate(Bundle savedInstanceState) {
|
||||||
|
|
@ -77,6 +84,8 @@ public class MainActivity extends BaseActivity {
|
||||||
checkConnectionType();
|
checkConnectionType();
|
||||||
getOpenSubsonicExtensions();
|
getOpenSubsonicExtensions();
|
||||||
checkTempoUpdate();
|
checkTempoUpdate();
|
||||||
|
|
||||||
|
maybeSchedulePlaybackIntent(getIntent());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -84,6 +93,7 @@ public class MainActivity extends BaseActivity {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
pingServer();
|
pingServer();
|
||||||
initService();
|
initService();
|
||||||
|
consumePendingPlaybackIntent();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
@ -99,6 +109,14 @@ public class MainActivity extends BaseActivity {
|
||||||
bind = null;
|
bind = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onNewIntent(Intent intent) {
|
||||||
|
super.onNewIntent(intent);
|
||||||
|
setIntent(intent);
|
||||||
|
maybeSchedulePlaybackIntent(intent);
|
||||||
|
consumePendingPlaybackIntent();
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBackPressed() {
|
public void onBackPressed() {
|
||||||
if (bottomSheetBehavior.getState() == BottomSheetBehavior.STATE_EXPANDED)
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -16,6 +16,7 @@ import com.cappielloantonio.tempo.R;
|
||||||
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
|
import com.cappielloantonio.tempo.databinding.DialogDeleteDownloadStorageBinding;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalDownloadMetadataStore;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
|
|
||||||
|
|
@ -47,7 +48,10 @@ public class DeleteDownloadStorageDialog extends DialogFragment {
|
||||||
if (dialog != null) {
|
if (dialog != null) {
|
||||||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||||
positiveButton.setOnClickListener(v -> {
|
positiveButton.setOnClickListener(v -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
|
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
||||||
|
}
|
||||||
|
|
||||||
String uriString = Preferences.getDownloadDirectoryUri();
|
String uriString = Preferences.getDownloadDirectoryUri();
|
||||||
if (uriString != null) {
|
if (uriString != null) {
|
||||||
DocumentFile directory = DocumentFile.fromTreeUri(requireContext(), Uri.parse(uriString));
|
DocumentFile directory = DocumentFile.fromTreeUri(requireContext(), Uri.parse(uriString));
|
||||||
|
|
@ -57,6 +61,7 @@ public class DeleteDownloadStorageDialog extends DialogFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ExternalAudioReader.refreshCache();
|
ExternalAudioReader.refreshCache();
|
||||||
|
ExternalDownloadMetadataStore.clear();
|
||||||
}
|
}
|
||||||
dialog.dismiss();
|
dialog.dismiss();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import androidx.fragment.app.DialogFragment;
|
||||||
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||||
import com.cappielloantonio.tempo.R;
|
import com.cappielloantonio.tempo.R;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
|
||||||
public class DownloadDirectoryPickerDialog extends DialogFragment {
|
public class DownloadDirectoryPickerDialog extends DialogFragment {
|
||||||
|
|
@ -37,6 +38,7 @@ public class DownloadDirectoryPickerDialog extends DialogFragment {
|
||||||
);
|
);
|
||||||
|
|
||||||
Preferences.setDownloadDirectoryUri(uri.toString());
|
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
|
||||||
Toast.makeText(requireContext(), "Download directory set:\n" + uri.toString(), Toast.LENGTH_LONG).show();
|
Toast.makeText(requireContext(), "Download directory set:\n" + uri.toString(), Toast.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -138,10 +138,7 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
||||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
MappingUtil.mapMediaItems(songs).forEach(media -> {
|
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||||
String title = media.mediaMetadata.title != null ? media.mediaMetadata.title.toString() : media.mediaId;
|
|
||||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), media, title);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
return true;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -117,10 +117,7 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
||||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
MappingUtil.mapMediaItems(songs).forEach(media -> {
|
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||||
String title = media.mediaMetadata.title != null ? media.mediaMetadata.title.toString() : media.mediaId;
|
|
||||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), media, title);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,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.adapter.DownloadHorizontalAdapter;
|
import com.cappielloantonio.tempo.ui.adapter.DownloadHorizontalAdapter;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
|
import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
|
||||||
import com.google.android.material.appbar.MaterialToolbar;
|
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
|
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||||
);
|
);
|
||||||
Preferences.setDownloadDirectoryUri(uri.toString());
|
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
Toast.makeText(requireContext(), "Download directory set", Toast.LENGTH_SHORT).show();
|
Toast.makeText(requireContext(), "Download directory set", Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -123,9 +123,7 @@ public class PlayerCoverFragment extends Fragment {
|
||||||
new Download(song)
|
new Download(song)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
MediaItem item = MappingUtil.mapMediaItem(song);
|
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||||
String title = item.mediaMetadata.title != null ? item.mediaMetadata.title.toString() : item.mediaId;
|
|
||||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), item, title);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -144,19 +144,16 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
||||||
if (isVisible() && getActivity() != null) {
|
if (isVisible() && getActivity() != null) {
|
||||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
MappingUtil.mapDownloads(songs),
|
MappingUtil.mapDownloads(songs),
|
||||||
songs.stream().map(child -> {
|
songs.stream().map(child -> {
|
||||||
Download toDownload = new Download(child);
|
Download toDownload = new Download(child);
|
||||||
toDownload.setPlaylistId(playlistPageViewModel.getPlaylist().getId());
|
toDownload.setPlaylistId(playlistPageViewModel.getPlaylist().getId());
|
||||||
toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName());
|
toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName());
|
||||||
return toDownload;
|
return toDownload;
|
||||||
}).collect(Collectors.toList())
|
}).collect(Collectors.toList())
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
MappingUtil.mapMediaItems(songs).forEach(media -> {
|
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||||
String title = media.mediaMetadata.title != null ? media.mediaMetadata.title.toString() : media.mediaId;
|
|
||||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), media, title);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -428,6 +428,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
if (current != null) {
|
if (current != null) {
|
||||||
Preferences.setDownloadDirectoryUri(null);
|
Preferences.setDownloadDirectoryUri(null);
|
||||||
Preferences.setDownloadStoragePreference(0);
|
Preferences.setDownloadStoragePreference(0);
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
Toast.makeText(requireContext(), "Download folder cleared.", Toast.LENGTH_SHORT).show();
|
Toast.makeText(requireContext(), "Download folder cleared.", Toast.LENGTH_SHORT).show();
|
||||||
checkStorage();
|
checkStorage();
|
||||||
checkDownloadDirectory();
|
checkDownloadDirectory();
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
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.util.ExternalAudioWriter;
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
||||||
import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.AlbumBottomSheetViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
|
|
@ -167,10 +168,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
|
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
|
||||||
} else {
|
} else {
|
||||||
MappingUtil.mapMediaItems(songs).forEach(media -> {
|
songs.forEach(child -> ExternalAudioWriter.downloadToUserDirectory(requireContext(), child));
|
||||||
String title = media.mediaMetadata.title != null ? media.mediaMetadata.title.toString() : media.mediaId;
|
|
||||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), media, title);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
@ -196,7 +194,11 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||||
|
|
||||||
removeAll.setOnClickListener(v -> {
|
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();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -246,8 +248,15 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||||
|
|
||||||
if (Preferences.getDownloadDirectoryUri() == null && DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
removeAll.setVisibility(View.VISIBLE);
|
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);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,15 +164,13 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
||||||
|
|
||||||
TextView download = view.findViewById(R.id.download_text_view);
|
TextView download = view.findViewById(R.id.download_text_view);
|
||||||
download.setOnClickListener(v -> {
|
download.setOnClickListener(v -> {
|
||||||
MediaItem item = MappingUtil.mapMediaItem(song);
|
|
||||||
String title = item.mediaMetadata.title != null ? item.mediaMetadata.title.toString() : item.mediaId;
|
|
||||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
MappingUtil.mapDownload(song),
|
MappingUtil.mapDownload(song),
|
||||||
new Download(song)
|
new Download(song)
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), item, title);
|
ExternalAudioWriter.downloadToUserDirectory(requireContext(), song);
|
||||||
}
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -85,6 +85,13 @@ object Constants {
|
||||||
const val MEDIA_LEAST_RECENTLY_STARRED = "MEDIA_LEAST_RECENTLY_STARRED"
|
const val MEDIA_LEAST_RECENTLY_STARRED = "MEDIA_LEAST_RECENTLY_STARRED"
|
||||||
|
|
||||||
const val DOWNLOAD_URI = "rest/download"
|
const val DOWNLOAD_URI = "rest/download"
|
||||||
|
const val ACTION_PLAY_EXTERNAL_DOWNLOAD = "com.cappielloantonio.tempo.action.PLAY_EXTERNAL_DOWNLOAD"
|
||||||
|
const val EXTRA_DOWNLOAD_URI = "EXTRA_DOWNLOAD_URI"
|
||||||
|
const val EXTRA_DOWNLOAD_MEDIA_ID = "EXTRA_DOWNLOAD_MEDIA_ID"
|
||||||
|
const val EXTRA_DOWNLOAD_TITLE = "EXTRA_DOWNLOAD_TITLE"
|
||||||
|
const val EXTRA_DOWNLOAD_ARTIST = "EXTRA_DOWNLOAD_ARTIST"
|
||||||
|
const val EXTRA_DOWNLOAD_ALBUM = "EXTRA_DOWNLOAD_ALBUM"
|
||||||
|
const val EXTRA_DOWNLOAD_DURATION = "EXTRA_DOWNLOAD_DURATION"
|
||||||
|
|
||||||
const val DOWNLOAD_TYPE_TRACK = "download_type_track"
|
const val DOWNLOAD_TYPE_TRACK = "download_type_track"
|
||||||
const val DOWNLOAD_TYPE_ALBUM = "download_type_album"
|
const val DOWNLOAD_TYPE_ALBUM = "download_type_album"
|
||||||
|
|
|
||||||
|
|
@ -10,8 +10,10 @@ import com.cappielloantonio.tempo.subsonic.models.PodcastEpisode;
|
||||||
|
|
||||||
import java.text.Normalizer;
|
import java.text.Normalizer;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
public class ExternalAudioReader {
|
public class ExternalAudioReader {
|
||||||
|
|
||||||
|
|
@ -36,6 +38,7 @@ public class ExternalAudioReader {
|
||||||
if (uriString == null) {
|
if (uriString == null) {
|
||||||
cache.clear();
|
cache.clear();
|
||||||
cachedDirUri = null;
|
cachedDirUri = null;
|
||||||
|
ExternalDownloadMetadataStore.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -43,12 +46,36 @@ public class ExternalAudioReader {
|
||||||
|
|
||||||
cache.clear();
|
cache.clear();
|
||||||
DocumentFile directory = DocumentFile.fromTreeUri(App.getContext(), Uri.parse(uriString));
|
DocumentFile directory = DocumentFile.fromTreeUri(App.getContext(), Uri.parse(uriString));
|
||||||
|
Map<String, Long> expectedSizes = ExternalDownloadMetadataStore.snapshot();
|
||||||
|
Set<String> verifiedKeys = new HashSet<>();
|
||||||
if (directory != null && directory.canRead()) {
|
if (directory != null && directory.canRead()) {
|
||||||
for (DocumentFile file : directory.listFiles()) {
|
for (DocumentFile file : directory.listFiles()) {
|
||||||
|
if (file == null || file.isDirectory()) continue;
|
||||||
String existing = file.getName();
|
String existing = file.getName();
|
||||||
if (existing != null) {
|
if (existing == null) continue;
|
||||||
String base = existing.replaceFirst("\\.[^\\.]+$", "");
|
|
||||||
cache.put(normalizeForComparison(base), file);
|
String base = existing.replaceFirst("\\.[^\\.]+$", "");
|
||||||
|
String key = normalizeForComparison(base);
|
||||||
|
Long expected = expectedSizes.get(key);
|
||||||
|
long actualLength = file.length();
|
||||||
|
|
||||||
|
if (expected != null && expected > 0 && actualLength == expected) {
|
||||||
|
cache.put(key, file);
|
||||||
|
verifiedKeys.add(key);
|
||||||
|
} else {
|
||||||
|
ExternalDownloadMetadataStore.remove(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!expectedSizes.isEmpty()) {
|
||||||
|
if (verifiedKeys.isEmpty()) {
|
||||||
|
ExternalDownloadMetadataStore.clear();
|
||||||
|
} else {
|
||||||
|
for (String key : expectedSizes.keySet()) {
|
||||||
|
if (!verifiedKeys.contains(key)) {
|
||||||
|
ExternalDownloadMetadataStore.remove(key);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -56,7 +83,6 @@ public class ExternalAudioReader {
|
||||||
cachedDirUri = uriString;
|
cachedDirUri = uriString;
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Rebuilds the cache on next access. */
|
|
||||||
public static synchronized void refreshCache() {
|
public static synchronized void refreshCache() {
|
||||||
cachedDirUri = null;
|
cachedDirUri = null;
|
||||||
cache.clear();
|
cache.clear();
|
||||||
|
|
@ -96,6 +122,7 @@ public class ExternalAudioReader {
|
||||||
}
|
}
|
||||||
if (deleted) {
|
if (deleted) {
|
||||||
cache.remove(key);
|
cache.remove(key);
|
||||||
|
ExternalDownloadMetadataStore.remove(key);
|
||||||
}
|
}
|
||||||
return deleted;
|
return deleted;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,8 @@ import androidx.core.app.NotificationCompat;
|
||||||
import androidx.documentfile.provider.DocumentFile;
|
import androidx.documentfile.provider.DocumentFile;
|
||||||
import androidx.media3.common.MediaItem;
|
import androidx.media3.common.MediaItem;
|
||||||
|
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.cappielloantonio.tempo.util.ExternalAudioReader;
|
import com.cappielloantonio.tempo.ui.activity.MainActivity;
|
||||||
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
|
|
@ -21,9 +21,19 @@ import java.net.HttpURLConnection;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
import java.text.Normalizer;
|
import java.text.Normalizer;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
public class ExternalAudioWriter {
|
public class ExternalAudioWriter {
|
||||||
|
|
||||||
|
private static final ExecutorService EXECUTOR = Executors.newSingleThreadExecutor();
|
||||||
|
private static final int BUFFER_SIZE = 8192;
|
||||||
|
private static final int CONNECT_TIMEOUT_MS = 15_000;
|
||||||
|
private static final int READ_TIMEOUT_MS = 60_000;
|
||||||
|
|
||||||
|
private ExternalAudioWriter() {
|
||||||
|
}
|
||||||
|
|
||||||
private static String sanitizeFileName(String name) {
|
private static String sanitizeFileName(String name) {
|
||||||
String sanitized = name.replaceAll("[\\/:*?\\\"<>|]", "_");
|
String sanitized = name.replaceAll("[\\/:*?\\\"<>|]", "_");
|
||||||
sanitized = sanitized.replaceAll("\\s+", " ").trim();
|
sanitized = sanitized.replaceAll("\\s+", " ").trim();
|
||||||
|
|
@ -40,6 +50,7 @@ public class ExternalAudioWriter {
|
||||||
private static DocumentFile findFile(DocumentFile dir, String fileName) {
|
private static DocumentFile findFile(DocumentFile dir, String fileName) {
|
||||||
String normalized = normalizeForComparison(fileName);
|
String normalized = normalizeForComparison(fileName);
|
||||||
for (DocumentFile file : dir.listFiles()) {
|
for (DocumentFile file : dir.listFiles()) {
|
||||||
|
if (file.isDirectory()) continue;
|
||||||
String existing = file.getName();
|
String existing = file.getName();
|
||||||
if (existing != null && normalizeForComparison(existing).equals(normalized)) {
|
if (existing != null && normalizeForComparison(existing).equals(normalized)) {
|
||||||
return file;
|
return file;
|
||||||
|
|
@ -48,115 +59,182 @@ public class ExternalAudioWriter {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void downloadToUserDirectory(Context context, MediaItem mediaItem, String fallbackName) {
|
public static void downloadToUserDirectory(Context context, Child child) {
|
||||||
new Thread(() -> {
|
if (context == null || child == null) {
|
||||||
String uriString = Preferences.getDownloadDirectoryUri();
|
return;
|
||||||
|
}
|
||||||
|
Context appContext = context.getApplicationContext();
|
||||||
|
MediaItem mediaItem = MappingUtil.mapDownload(child);
|
||||||
|
String fallbackName = child.getTitle() != null ? child.getTitle() : child.getId();
|
||||||
|
EXECUTOR.execute(() -> performDownload(appContext, mediaItem, fallbackName, child));
|
||||||
|
}
|
||||||
|
|
||||||
if (uriString == null) {
|
private static void performDownload(Context context, MediaItem mediaItem, String fallbackName, Child child) {
|
||||||
notifyUnavailable(context);
|
String uriString = Preferences.getDownloadDirectoryUri();
|
||||||
|
if (uriString == null) {
|
||||||
|
notifyUnavailable(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DocumentFile directory = DocumentFile.fromTreeUri(context, Uri.parse(uriString));
|
||||||
|
if (directory == null || !directory.canWrite()) {
|
||||||
|
notifyFailure(context, "Cannot write to folder.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String artist = child.getArtist() != null ? child.getArtist() : "";
|
||||||
|
String title = child.getTitle() != null ? child.getTitle() : fallbackName;
|
||||||
|
String album = child.getAlbum() != null ? child.getAlbum() : "";
|
||||||
|
String baseName = artist.isEmpty() ? title : artist + " - " + title;
|
||||||
|
if (!album.isEmpty()) baseName += " (" + album + ")";
|
||||||
|
if (baseName.isEmpty()) {
|
||||||
|
baseName = fallbackName != null ? fallbackName : "download";
|
||||||
|
}
|
||||||
|
String metadataKey = normalizeForComparison(baseName);
|
||||||
|
|
||||||
|
Uri mediaUri = mediaItem != null && mediaItem.requestMetadata != null
|
||||||
|
? mediaItem.requestMetadata.mediaUri
|
||||||
|
: null;
|
||||||
|
if (mediaUri == null) {
|
||||||
|
notifyFailure(context, "Invalid media URI.");
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
String scheme = mediaUri.getScheme();
|
||||||
|
if (scheme == null || (!scheme.equalsIgnoreCase("http") && !scheme.equalsIgnoreCase("https"))) {
|
||||||
|
notifyFailure(context, "Unsupported media URI.");
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpURLConnection connection = 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();
|
||||||
|
if (responseCode >= HttpURLConnection.HTTP_BAD_REQUEST) {
|
||||||
|
notifyFailure(context, "Server returned " + responseCode);
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Uri treeUri = Uri.parse(uriString);
|
String mimeType = connection.getContentType();
|
||||||
DocumentFile directory = DocumentFile.fromTreeUri(context, treeUri);
|
if (mimeType == null || mimeType.isEmpty()) {
|
||||||
if (directory == null || !directory.canWrite()) {
|
mimeType = "application/octet-stream";
|
||||||
notifyFailure(context, "Cannot write to folder.");
|
}
|
||||||
|
|
||||||
|
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
||||||
|
if (extension == null || extension.isEmpty()) {
|
||||||
|
String suffix = child.getSuffix();
|
||||||
|
if (suffix != null && !suffix.isEmpty()) {
|
||||||
|
extension = suffix;
|
||||||
|
} else {
|
||||||
|
extension = "bin";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String sanitized = sanitizeFileName(baseName);
|
||||||
|
if (sanitized.isEmpty()) sanitized = sanitizeFileName(fallbackName);
|
||||||
|
if (sanitized.isEmpty()) sanitized = "download";
|
||||||
|
String fileName = sanitized + "." + extension;
|
||||||
|
|
||||||
|
DocumentFile existingFile = findFile(directory, fileName);
|
||||||
|
long remoteLength = connection.getContentLengthLong();
|
||||||
|
Long recordedSize = ExternalDownloadMetadataStore.getSize(metadataKey);
|
||||||
|
if (existingFile != null && existingFile.exists()) {
|
||||||
|
long localLength = existingFile.length();
|
||||||
|
boolean matches = false;
|
||||||
|
if (remoteLength > 0 && localLength == remoteLength) {
|
||||||
|
matches = true;
|
||||||
|
} else if (remoteLength <= 0 && recordedSize != null && localLength == recordedSize) {
|
||||||
|
matches = true;
|
||||||
|
}
|
||||||
|
if (matches) {
|
||||||
|
ExternalDownloadMetadataStore.recordSize(metadataKey, localLength);
|
||||||
|
ExternalAudioReader.refreshCache();
|
||||||
|
notifyExists(context, fileName);
|
||||||
|
return;
|
||||||
|
} else {
|
||||||
|
existingFile.delete();
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
targetFile = directory.createFile(mimeType, fileName);
|
||||||
|
if (targetFile == null) {
|
||||||
|
notifyFailure(context, "Failed to create file.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
Uri targetUri = targetFile.getUri();
|
||||||
Uri mediaUri = mediaItem.requestMetadata.mediaUri;
|
try (InputStream in = connection.getInputStream();
|
||||||
if (mediaUri == null) {
|
OutputStream out = context.getContentResolver().openOutputStream(targetUri)) {
|
||||||
notifyFailure(context, "Invalid media URI.");
|
if (out == null) {
|
||||||
|
notifyFailure(context, "Cannot open output stream.");
|
||||||
|
targetFile.delete();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
String scheme = mediaUri.getScheme();
|
byte[] buffer = new byte[BUFFER_SIZE];
|
||||||
if (scheme == null || (!scheme.equals("http") && !scheme.equals("https"))) {
|
int len;
|
||||||
notifyExists(context, fallbackName);
|
long total = 0;
|
||||||
|
while ((len = in.read(buffer)) != -1) {
|
||||||
|
out.write(buffer, 0, len);
|
||||||
|
total += len;
|
||||||
|
}
|
||||||
|
out.flush();
|
||||||
|
|
||||||
|
if (total <= 0) {
|
||||||
|
targetFile.delete();
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
notifyFailure(context, "Empty download.");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
HttpURLConnection connection = (HttpURLConnection) new URL(mediaUri.toString()).openConnection();
|
if (remoteLength > 0 && total != remoteLength) {
|
||||||
connection.connect();
|
targetFile.delete();
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
String mimeType = connection.getContentType();
|
notifyFailure(context, "Incomplete download.");
|
||||||
if (mimeType == null) mimeType = "application/octet-stream";
|
|
||||||
|
|
||||||
String extension = MimeTypeMap.getSingleton().getExtensionFromMimeType(mimeType);
|
|
||||||
if (extension == null) extension = "bin";
|
|
||||||
|
|
||||||
String artist = mediaItem.mediaMetadata.artist != null ? mediaItem.mediaMetadata.artist.toString() : "";
|
|
||||||
String title = mediaItem.mediaMetadata.title != null ? mediaItem.mediaMetadata.title.toString() : fallbackName;
|
|
||||||
String album = mediaItem.mediaMetadata.albumTitle != null ? mediaItem.mediaMetadata.albumTitle.toString() : "";
|
|
||||||
String name = artist.isEmpty() ? title : artist + " - " + title;
|
|
||||||
if (!album.isEmpty()) name += " (" + album + ")";
|
|
||||||
|
|
||||||
String sanitized = sanitizeFileName(name);
|
|
||||||
String fullName = sanitized + "." + extension;
|
|
||||||
|
|
||||||
DocumentFile existingFile = findFile(directory, fullName);
|
|
||||||
long remoteLength = connection.getContentLengthLong();
|
|
||||||
if (existingFile != null && existingFile.exists()) {
|
|
||||||
long localLength = existingFile.length();
|
|
||||||
if (remoteLength > 0 && localLength == remoteLength) {
|
|
||||||
notifyExists(context, fullName);
|
|
||||||
return;
|
|
||||||
} else {
|
|
||||||
existingFile.delete();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DocumentFile targetFile = directory.createFile(mimeType, fullName);
|
|
||||||
if (targetFile == null) {
|
|
||||||
notifyFailure(context, "Failed to create file.");
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try (InputStream in = connection.getInputStream();
|
ExternalDownloadMetadataStore.recordSize(metadataKey, total);
|
||||||
OutputStream out = context.getContentResolver().openOutputStream(targetFile.getUri())) {
|
notifySuccess(context, fileName, child, targetUri);
|
||||||
if (out == null) {
|
ExternalAudioReader.refreshCache();
|
||||||
notifyFailure(context, "Cannot open output stream.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
byte[] buffer = new byte[8192];
|
|
||||||
int len;
|
|
||||||
long total = 0;
|
|
||||||
while ((len = in.read(buffer)) != -1) {
|
|
||||||
out.write(buffer, 0, len);
|
|
||||||
total += len;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (remoteLength > 0 && total != remoteLength) {
|
|
||||||
targetFile.delete();
|
|
||||||
notifyFailure(context, "Incomplete download.");
|
|
||||||
} else {
|
|
||||||
notifySuccess(context, fullName);
|
|
||||||
ExternalAudioReader.refreshCache();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (Exception e) {
|
|
||||||
notifyFailure(context, e.getMessage());
|
|
||||||
}
|
}
|
||||||
}).start();
|
} catch (Exception e) {
|
||||||
|
if (targetFile != null) {
|
||||||
|
targetFile.delete();
|
||||||
|
}
|
||||||
|
ExternalDownloadMetadataStore.remove(metadataKey);
|
||||||
|
notifyFailure(context, e.getMessage() != null ? e.getMessage() : "Download failed");
|
||||||
|
} finally {
|
||||||
|
if (connection != null) {
|
||||||
|
connection.disconnect();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void notifyUnavailable(Context context) {
|
private static void notifyUnavailable(Context context) {
|
||||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
Intent settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
Intent settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||||
Uri.fromParts("package", context.getPackageName(), null));
|
Uri.fromParts("package", context.getPackageName(), null));
|
||||||
PendingIntent openSettings = PendingIntent.getActivity(context, 0, settingsIntent,
|
PendingIntent openSettings = PendingIntent.getActivity(context, 0, settingsIntent,
|
||||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||||
|
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||||
.setContentTitle("No download folder set")
|
.setContentTitle("No download folder set")
|
||||||
.setContentText("Tap to set one in settings")
|
.setContentText("Tap to set one in settings")
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
.setSilent(true)
|
.setSilent(true)
|
||||||
.setContentIntent(openSettings)
|
.setContentIntent(openSettings)
|
||||||
.setAutoCancel(true);
|
.setAutoCancel(true);
|
||||||
|
|
||||||
manager.notify(1011, builder.build());
|
manager.notify(1011, builder.build());
|
||||||
}
|
}
|
||||||
|
|
@ -164,30 +242,63 @@ public class ExternalAudioWriter {
|
||||||
private static void notifyFailure(Context context, String message) {
|
private static void notifyFailure(Context context, String message) {
|
||||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||||
.setContentTitle("Download failed")
|
.setContentTitle("Download failed")
|
||||||
.setContentText(message)
|
.setContentText(message)
|
||||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||||
.setAutoCancel(true);
|
.setAutoCancel(true);
|
||||||
manager.notify((int) System.currentTimeMillis(), builder.build());
|
manager.notify((int) System.currentTimeMillis(), builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void notifySuccess(Context context, String name) {
|
private static void notifySuccess(Context context, String name, Child child, Uri fileUri) {
|
||||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||||
.setContentTitle("Download complete")
|
.setContentTitle("Download complete")
|
||||||
.setContentText(name)
|
.setContentText(name)
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
||||||
.setAutoCancel(true);
|
.setAutoCancel(true);
|
||||||
|
|
||||||
|
PendingIntent playIntent = buildPlayIntent(context, child, fileUri);
|
||||||
|
if (playIntent != null) {
|
||||||
|
builder.setContentIntent(playIntent);
|
||||||
|
}
|
||||||
|
|
||||||
manager.notify((int) System.currentTimeMillis(), builder.build());
|
manager.notify((int) System.currentTimeMillis(), builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void notifyExists(Context context, String name) {
|
private static void notifyExists(Context context, String name) {
|
||||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||||
.setContentTitle("Already downloaded")
|
.setContentTitle("Already downloaded")
|
||||||
.setContentText(name)
|
.setContentText(name)
|
||||||
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
.setSmallIcon(android.R.drawable.stat_sys_warning)
|
||||||
.setAutoCancel(true);
|
.setAutoCancel(true);
|
||||||
manager.notify((int) System.currentTimeMillis(), builder.build());
|
manager.notify((int) System.currentTimeMillis(), builder.build());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static PendingIntent buildPlayIntent(Context context, Child child, Uri fileUri) {
|
||||||
|
if (fileUri == null) return null;
|
||||||
|
Intent intent = new Intent(context, MainActivity.class)
|
||||||
|
.setAction(Constants.ACTION_PLAY_EXTERNAL_DOWNLOAD)
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_URI, fileUri.toString())
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_MEDIA_ID, child.getId())
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_TITLE, child.getTitle())
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_ARTIST, child.getArtist())
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_ALBUM, child.getAlbum())
|
||||||
|
.putExtra(Constants.EXTRA_DOWNLOAD_DURATION, child.getDuration() != null ? child.getDuration() : 0)
|
||||||
|
.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP | Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||||
|
|
||||||
|
int requestCode;
|
||||||
|
if (child.getId() != null) {
|
||||||
|
requestCode = Math.abs(child.getId().hashCode());
|
||||||
|
} else {
|
||||||
|
requestCode = Math.abs(fileUri.toString().hashCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
return PendingIntent.getActivity(
|
||||||
|
context,
|
||||||
|
requestCode,
|
||||||
|
intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,123 @@
|
||||||
|
package com.cappielloantonio.tempo.util;
|
||||||
|
|
||||||
|
import android.content.SharedPreferences;
|
||||||
|
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
import com.cappielloantonio.tempo.App;
|
||||||
|
|
||||||
|
import org.json.JSONException;
|
||||||
|
import org.json.JSONObject;
|
||||||
|
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.HashSet;
|
||||||
|
import java.util.Iterator;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
|
public final class ExternalDownloadMetadataStore {
|
||||||
|
|
||||||
|
private static final String PREF_KEY = "external_download_metadata";
|
||||||
|
|
||||||
|
private ExternalDownloadMetadataStore() {
|
||||||
|
}
|
||||||
|
|
||||||
|
private static SharedPreferences preferences() {
|
||||||
|
return App.getInstance().getPreferences();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static JSONObject readAll() {
|
||||||
|
String raw = preferences().getString(PREF_KEY, "{}");
|
||||||
|
try {
|
||||||
|
return new JSONObject(raw);
|
||||||
|
} catch (JSONException e) {
|
||||||
|
return new JSONObject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void writeAll(JSONObject object) {
|
||||||
|
preferences().edit().putString(PREF_KEY, object.toString()).apply();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void clear() {
|
||||||
|
writeAll(new JSONObject());
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void recordSize(String key, long size) {
|
||||||
|
if (key == null || size <= 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONObject object = readAll();
|
||||||
|
try {
|
||||||
|
object.put(key, size);
|
||||||
|
} catch (JSONException ignored) {
|
||||||
|
}
|
||||||
|
writeAll(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void remove(String key) {
|
||||||
|
if (key == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONObject object = readAll();
|
||||||
|
object.remove(key);
|
||||||
|
writeAll(object);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Nullable
|
||||||
|
public static synchronized Long getSize(String key) {
|
||||||
|
if (key == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
JSONObject object = readAll();
|
||||||
|
if (!object.has(key)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
long size = object.optLong(key, -1L);
|
||||||
|
return size > 0 ? size : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized Map<String, Long> snapshot() {
|
||||||
|
JSONObject object = readAll();
|
||||||
|
if (object.length() == 0) {
|
||||||
|
return Collections.emptyMap();
|
||||||
|
}
|
||||||
|
Map<String, Long> sizes = new HashMap<>();
|
||||||
|
Iterator<String> keys = object.keys();
|
||||||
|
while (keys.hasNext()) {
|
||||||
|
String key = keys.next();
|
||||||
|
long size = object.optLong(key, -1L);
|
||||||
|
if (size > 0) {
|
||||||
|
sizes.put(key, size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sizes;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static synchronized void retainOnly(Set<String> keysToKeep) {
|
||||||
|
if (keysToKeep == null || keysToKeep.isEmpty()) {
|
||||||
|
clear();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
JSONObject object = readAll();
|
||||||
|
if (object.length() == 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Set<String> keys = new HashSet<>();
|
||||||
|
Iterator<String> iterator = object.keys();
|
||||||
|
while (iterator.hasNext()) {
|
||||||
|
keys.add(iterator.next());
|
||||||
|
}
|
||||||
|
boolean changed = false;
|
||||||
|
for (String key : keys) {
|
||||||
|
if (!keysToKeep.contains(key)) {
|
||||||
|
object.remove(key);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (changed) {
|
||||||
|
writeAll(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -460,6 +460,10 @@ object Preferences {
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun setDownloadDirectoryUri(uri: String?) {
|
fun setDownloadDirectoryUri(uri: String?) {
|
||||||
|
val current = App.getInstance().preferences.getString(DOWNLOAD_DIRECTORY_URI, null)
|
||||||
|
if (current != uri) {
|
||||||
|
ExternalDownloadMetadataStore.clear()
|
||||||
|
}
|
||||||
App.getInstance().preferences.edit().putString(DOWNLOAD_DIRECTORY_URI, uri).apply()
|
App.getInstance().preferences.edit().putString(DOWNLOAD_DIRECTORY_URI, uri).apply()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue