mirror of
https://github.com/antebudimir/tempus.git
synced 2025-12-31 17:43:32 +00:00
feat: Support user-defined download directory for media
This commit is contained in:
parent
fda586c4d8
commit
cce6456951
21 changed files with 500 additions and 38 deletions
|
|
@ -0,0 +1,61 @@
|
|||
package com.cappielloantonio.tempo.ui.dialog;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.cappielloantonio.tempo.R;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
public class DownloadDirectoryPickerDialog extends DialogFragment {
|
||||
|
||||
private ActivityResultLauncher<Intent> folderPickerLauncher;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public android.app.Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
// Register launcher *before* button triggers
|
||||
folderPickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == android.app.Activity.RESULT_OK) {
|
||||
Intent data = result.getData();
|
||||
if (data != null) {
|
||||
Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
requireContext().getContentResolver().takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
);
|
||||
|
||||
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||
|
||||
Toast.makeText(requireContext(), "Download directory set:\n" + uri.toString(), Toast.LENGTH_LONG).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
return new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle("Set Download Directory")
|
||||
.setMessage("Choose a folder where downloaded songs will be stored.")
|
||||
.setPositiveButton("Choose Folder", (dialog, which) -> {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
folderPickerLauncher.launch(intent);
|
||||
})
|
||||
.setNegativeButton(android.R.string.cancel, null)
|
||||
.create();
|
||||
}
|
||||
}
|
||||
|
|
@ -34,6 +34,7 @@ public class DownloadStorageDialog extends DialogFragment {
|
|||
.setTitle(R.string.download_storage_dialog_title)
|
||||
.setPositiveButton(R.string.download_storage_external_dialog_positive_button, null)
|
||||
.setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null)
|
||||
.setNeutralButton(R.string.download_storage_directory_dialog_neutral_button, null)
|
||||
.create();
|
||||
}
|
||||
|
||||
|
|
@ -74,6 +75,20 @@ public class DownloadStorageDialog extends DialogFragment {
|
|||
|
||||
dialog.dismiss();
|
||||
});
|
||||
|
||||
Button neutralButton = dialog.getButton(Dialog.BUTTON_NEUTRAL);
|
||||
neutralButton.setOnClickListener(v -> {
|
||||
int currentPreference = Preferences.getDownloadStoragePreference();
|
||||
int newPreference = 2;
|
||||
|
||||
if (currentPreference != newPreference) {
|
||||
Preferences.setDownloadStoragePreference(newPreference);
|
||||
DownloadUtil.getDownloadTracker(requireContext()).removeAll();
|
||||
dialogClickCallback.onNeutralClick();
|
||||
}
|
||||
|
||||
dialog.dismiss();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ public class StarredSyncDialog extends DialogFragment {
|
|||
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||
positiveButton.setOnClickListener(v -> {
|
||||
starredSyncViewModel.getStarredTracks(requireActivity()).observe(requireActivity(), songs -> {
|
||||
if (songs != null) {
|
||||
if (songs != null && Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(context).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
|
|
|
|||
|
|
@ -39,6 +39,8 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.AlbumPageViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
|
@ -130,7 +132,17 @@ public class AlbumPageFragment extends Fragment implements ClickCallback {
|
|||
|
||||
if (item.getItemId() == R.id.action_download_album) {
|
||||
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(MappingUtil.mapDownloads(songs), songs.stream().map(Download::new).collect(Collectors.toList()));
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
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);
|
||||
});
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,7 +33,9 @@ import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
|
|||
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
||||
import com.cappielloantonio.tempo.util.Constants;
|
||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
|
|
@ -109,10 +111,17 @@ public class DirectoryFragment extends Fragment implements ClickCallback {
|
|||
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
|
||||
if (isVisible() && getActivity() != null) {
|
||||
List<Child> songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList());
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
||||
);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownloads(songs),
|
||||
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);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ import com.cappielloantonio.tempo.viewmodel.DownloadViewModel;
|
|||
import com.google.android.material.appbar.MaterialToolbar;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.app.Activity;
|
||||
import android.net.Uri;
|
||||
import android.widget.Toast;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
|
|
@ -40,6 +45,7 @@ import java.util.Objects;
|
|||
@UnstableApi
|
||||
public class DownloadFragment extends Fragment implements ClickCallback {
|
||||
private static final String TAG = "DownloadFragment";
|
||||
private static final int REQUEST_CODE_PICK_DIRECTORY = 1002;
|
||||
|
||||
private FragmentDownloadBinding bind;
|
||||
private MainActivity activity;
|
||||
|
|
@ -216,6 +222,10 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
|||
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null));
|
||||
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR);
|
||||
return true;
|
||||
} else if (menuItem.getItemId() == R.id.menu_download_set_directory) {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
startActivityForResult(intent, REQUEST_CODE_PICK_DIRECTORY);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
|
|
@ -267,4 +277,20 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
|||
public void onDownloadGroupLongClick(Bundle bundle) {
|
||||
Navigation.findNavController(requireView()).navigate(R.id.downloadBottomSheetDialog, bundle);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
super.onActivityResult(requestCode, resultCode, data);
|
||||
if (requestCode == REQUEST_CODE_PICK_DIRECTORY && resultCode == Activity.RESULT_OK) {
|
||||
Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
requireContext().getContentResolver().takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
);
|
||||
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||
Toast.makeText(requireContext(), "Download directory set", Toast.LENGTH_SHORT).show();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,6 +66,8 @@ import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
|||
import com.google.android.material.snackbar.Snackbar;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import androidx.media3.common.MediaItem;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
|
@ -277,7 +279,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
|||
}
|
||||
|
||||
private void initSyncStarredView() {
|
||||
if (Preferences.isStarredSyncEnabled()) {
|
||||
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
|
||||
@Override
|
||||
public void onChanged(List<Child> songs) {
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ import java.util.ArrayList;
|
|||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.lifecycle.ViewModelProvider;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import androidx.media3.common.MediaMetadata;
|
||||
import androidx.media3.common.Player;
|
||||
import androidx.media3.common.util.UnstableApi;
|
||||
|
|
@ -31,6 +32,7 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
|
@ -115,10 +117,16 @@ public class PlayerCoverFragment extends Fragment {
|
|||
playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), song -> {
|
||||
if (song != null && bind != null) {
|
||||
bind.innerButtonTopLeft.setOnClickListener(view -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownload(song),
|
||||
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);
|
||||
}
|
||||
});
|
||||
|
||||
bind.innerButtonTopRight.setOnClickListener(view -> {
|
||||
|
|
|
|||
|
|
@ -37,6 +37,8 @@ import com.cappielloantonio.tempo.util.Constants;
|
|||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
|
@ -140,15 +142,22 @@ public class PlaylistPageFragment extends Fragment implements ClickCallback {
|
|||
if (item.getItemId() == R.id.action_download_playlist) {
|
||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||
if (isVisible() && getActivity() != 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())
|
||||
);
|
||||
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())
|
||||
);
|
||||
} else {
|
||||
MappingUtil.mapMediaItems(songs).forEach(media -> {
|
||||
String title = media.mediaMetadata.title != null ? media.mediaMetadata.title.toString() : media.mediaId;
|
||||
ExternalAudioWriter.downloadToUserDirectory(requireContext(), media, title);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return true;
|
||||
|
|
|
|||
|
|
@ -1,17 +1,19 @@
|
|||
package com.cappielloantonio.tempo.ui.fragment;
|
||||
|
||||
import android.content.ComponentName;
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Intent;
|
||||
import android.content.ServiceConnection;
|
||||
import android.media.audiofx.AudioEffect;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.IBinder;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.activity.result.ActivityResultLauncher;
|
||||
import androidx.activity.result.contract.ActivityResultContracts;
|
||||
|
|
@ -59,7 +61,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
private MainActivity activity;
|
||||
private SettingViewModel settingViewModel;
|
||||
|
||||
private ActivityResultLauncher<Intent> someActivityResultLauncher;
|
||||
private ActivityResultLauncher<Intent> equalizerResultLauncher;
|
||||
private ActivityResultLauncher<Intent> directoryPickerLauncher;
|
||||
|
||||
private MediaService.LocalBinder mediaServiceBinder;
|
||||
private boolean isServiceBound = false;
|
||||
|
|
@ -68,9 +71,30 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
someActivityResultLauncher = registerForActivityResult(
|
||||
equalizerResultLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {}
|
||||
);
|
||||
|
||||
directoryPickerLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == Activity.RESULT_OK) {
|
||||
Intent data = result.getData();
|
||||
if (data != null) {
|
||||
Uri uri = data.getData();
|
||||
if (uri != null) {
|
||||
requireContext().getContentResolver().takePersistableUriPermission(
|
||||
uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION | Intent.FLAG_GRANT_WRITE_URI_PERMISSION
|
||||
);
|
||||
|
||||
Preferences.setDownloadDirectoryUri(uri.toString());
|
||||
Toast.makeText(requireContext(), "Download folder set.", Toast.LENGTH_SHORT).show();
|
||||
checkDownloadDirectory();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -102,6 +126,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
checkSystemEqualizer();
|
||||
checkCacheStorage();
|
||||
checkStorage();
|
||||
checkDownloadDirectory();
|
||||
|
||||
setStreamingCacheSize();
|
||||
setAppLanguage();
|
||||
|
|
@ -114,6 +139,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
actionSyncStarredArtists();
|
||||
actionChangeStreamingCacheStorage();
|
||||
actionChangeDownloadStorage();
|
||||
actionSetDownloadDirectory();
|
||||
actionDeleteDownloadStorage();
|
||||
actionKeepScreenOn();
|
||||
actionAutoDownloadLyrics();
|
||||
|
|
@ -151,7 +177,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
|
||||
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
||||
equalizer.setOnPreferenceClickListener(preference -> {
|
||||
someActivityResultLauncher.launch(intent);
|
||||
equalizerResultLauncher.launch(intent);
|
||||
return true;
|
||||
});
|
||||
} else {
|
||||
|
|
@ -168,7 +194,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||
storage.setVisible(false);
|
||||
} else {
|
||||
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||
storage.setSummary(Preferences.getStreamingCacheStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
storage.setVisible(false);
|
||||
|
|
@ -184,13 +210,46 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||
storage.setVisible(false);
|
||||
} else {
|
||||
storage.setSummary(Preferences.getDownloadStoragePreference() == 0 ? R.string.download_storage_internal_dialog_negative_button : R.string.download_storage_external_dialog_positive_button);
|
||||
int pref = Preferences.getDownloadStoragePreference();
|
||||
if (pref == 0) {
|
||||
storage.setSummary(R.string.download_storage_internal_dialog_negative_button);
|
||||
} else if (pref == 1) {
|
||||
storage.setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||
} else {
|
||||
storage.setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
||||
}
|
||||
}
|
||||
} catch (Exception exception) {
|
||||
storage.setVisible(false);
|
||||
}
|
||||
}
|
||||
|
||||
private void checkDownloadDirectory() {
|
||||
Preference storage = findPreference("download_storage");
|
||||
Preference directory = findPreference("set_download_directory");
|
||||
|
||||
if (directory == null) return;
|
||||
|
||||
String current = Preferences.getDownloadDirectoryUri();
|
||||
if (current != null) {
|
||||
if (storage != null) storage.setVisible(false);
|
||||
directory.setVisible(true);
|
||||
directory.setIcon(R.drawable.ic_close);
|
||||
directory.setTitle("Clear download folder");
|
||||
directory.setSummary(current);
|
||||
} else {
|
||||
if (storage != null) storage.setVisible(true);
|
||||
if (Preferences.getDownloadStoragePreference() == 2) {
|
||||
directory.setVisible(true);
|
||||
directory.setIcon(R.drawable.ic_folder);
|
||||
directory.setTitle("Set download folder");
|
||||
directory.setSummary("Choose a folder for downloaded music files");
|
||||
} else {
|
||||
directory.setVisible(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void setStreamingCacheSize() {
|
||||
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
||||
|
||||
|
|
@ -338,11 +397,19 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
@Override
|
||||
public void onPositiveClick() {
|
||||
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||
checkDownloadDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNegativeClick() {
|
||||
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
|
||||
checkDownloadDirectory();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onNeutralClick() {
|
||||
findPreference("download_storage").setSummary(R.string.download_storage_directory_dialog_neutral_button);
|
||||
checkDownloadDirectory();
|
||||
}
|
||||
});
|
||||
dialog.show(activity.getSupportFragmentManager(), null);
|
||||
|
|
@ -350,6 +417,30 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
|||
});
|
||||
}
|
||||
|
||||
private void actionSetDownloadDirectory() {
|
||||
Preference pref = findPreference("set_download_directory");
|
||||
if (pref != null) {
|
||||
pref.setOnPreferenceClickListener(preference -> {
|
||||
String current = Preferences.getDownloadDirectoryUri();
|
||||
|
||||
if (current != null) {
|
||||
Preferences.setDownloadDirectoryUri(null);
|
||||
Preferences.setDownloadStoragePreference(0);
|
||||
Toast.makeText(requireContext(), "Download folder cleared.", Toast.LENGTH_SHORT).show();
|
||||
checkStorage();
|
||||
checkDownloadDirectory();
|
||||
} else {
|
||||
Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT_TREE);
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_READ_URI_PERMISSION
|
||||
| Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
directoryPickerLauncher.launch(intent);
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private void actionDeleteDownloadStorage() {
|
||||
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
||||
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
|
||||
|
|
|
|||
|
|
@ -37,6 +37,7 @@ import com.cappielloantonio.tempo.util.DownloadUtil;
|
|||
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.viewmodel.AlbumBottomSheetViewModel;
|
||||
import com.cappielloantonio.tempo.viewmodel.HomeViewModel;
|
||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
|
|
@ -163,7 +164,14 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
List<Download> downloads = songs.stream().map(Download::new).collect(Collectors.toList());
|
||||
|
||||
downloadAll.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(mediaItems, downloads);
|
||||
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);
|
||||
});
|
||||
}
|
||||
dismissBottomSheet();
|
||||
});
|
||||
});
|
||||
|
|
@ -238,7 +246,7 @@ public class AlbumBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
albumBottomSheetViewModel.getAlbumTracks().observe(getViewLifecycleOwner(), songs -> {
|
||||
List<MediaItem> mediaItems = MappingUtil.mapDownloads(songs);
|
||||
|
||||
if (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
||||
if (Preferences.getDownloadDirectoryUri() == null && DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
||||
removeAll.setVisibility(View.VISIBLE);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -39,6 +39,10 @@ import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
|
|||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||
import com.google.common.util.concurrent.ListenableFuture;
|
||||
|
||||
import android.content.Intent;
|
||||
import androidx.media3.common.MediaItem;
|
||||
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
|
||||
|
|
@ -159,10 +163,16 @@ public class SongBottomSheetDialog extends BottomSheetDialogFragment implements
|
|||
|
||||
TextView download = view.findViewById(R.id.download_text_view);
|
||||
download.setOnClickListener(v -> {
|
||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||
MappingUtil.mapDownload(song),
|
||||
new Download(song)
|
||||
);
|
||||
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);
|
||||
}
|
||||
dismissBottomSheet();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -187,19 +187,21 @@ public final class DownloadUtil {
|
|||
|
||||
private static synchronized File getDownloadDirectory(Context context) {
|
||||
if (downloadDirectory == null) {
|
||||
if (Preferences.getDownloadStoragePreference() == 0) {
|
||||
int pref = Preferences.getDownloadStoragePreference();
|
||||
if (pref == 0) {
|
||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||
if (downloadDirectory == null) {
|
||||
downloadDirectory = context.getFilesDir();
|
||||
}
|
||||
} else {
|
||||
} else if (pref == 1) {
|
||||
try {
|
||||
downloadDirectory = context.getExternalFilesDirs(null)[1];
|
||||
} catch (Exception exception) {
|
||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||
Preferences.setDownloadStoragePreference(0);
|
||||
}
|
||||
|
||||
} else {
|
||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,176 @@
|
|||
package com.cappielloantonio.tempo.util;
|
||||
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.provider.Settings;
|
||||
import android.webkit.MimeTypeMap;
|
||||
import android.widget.Toast;
|
||||
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.documentfile.provider.DocumentFile;
|
||||
import androidx.media3.common.MediaItem;
|
||||
|
||||
import com.cappielloantonio.tempo.util.Preferences;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
import java.text.Normalizer;
|
||||
import java.util.Locale;
|
||||
|
||||
public class ExternalAudioWriter {
|
||||
|
||||
private static String sanitizeFileName(String name) {
|
||||
String sanitized = name.replaceAll("[\\/:*?\\\"<>|]", "_");
|
||||
sanitized = sanitized.replaceAll("\\s+", " ").trim();
|
||||
return sanitized;
|
||||
}
|
||||
|
||||
private static String normalizeForComparison(String name) {
|
||||
String s = sanitizeFileName(name);
|
||||
s = Normalizer.normalize(s, Normalizer.Form.NFKD);
|
||||
s = s.replaceAll("\\p{InCombiningDiacriticalMarks}+", "");
|
||||
return s.toLowerCase(Locale.ROOT);
|
||||
}
|
||||
|
||||
private static DocumentFile findFile(DocumentFile dir, String fileName) {
|
||||
String normalized = normalizeForComparison(fileName);
|
||||
for (DocumentFile file : dir.listFiles()) {
|
||||
String existing = file.getName();
|
||||
if (existing != null && normalizeForComparison(existing).equals(normalized)) {
|
||||
return file;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public static void downloadToUserDirectory(Context context, MediaItem mediaItem, String fallbackName) {
|
||||
new Thread(() -> {
|
||||
String uriString = Preferences.getDownloadDirectoryUri();
|
||||
|
||||
if (uriString == null) {
|
||||
notifyUnavailable(context);
|
||||
return;
|
||||
}
|
||||
|
||||
Uri treeUri = Uri.parse(uriString);
|
||||
DocumentFile directory = DocumentFile.fromTreeUri(context, treeUri);
|
||||
if (directory == null || !directory.canWrite()) {
|
||||
notifyFailure(context, "Cannot write to folder.");
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
Uri mediaUri = mediaItem.requestMetadata.mediaUri;
|
||||
if (mediaUri == null) {
|
||||
notifyFailure(context, "Invalid media URI.");
|
||||
return;
|
||||
}
|
||||
|
||||
HttpURLConnection connection = (HttpURLConnection) new URL(mediaUri.toString()).openConnection();
|
||||
connection.connect();
|
||||
|
||||
String mimeType = connection.getContentType();
|
||||
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;
|
||||
}
|
||||
|
||||
try (InputStream in = connection.getInputStream();
|
||||
OutputStream out = context.getContentResolver().openOutputStream(targetFile.getUri())) {
|
||||
if (out == null) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
} catch (Exception e) {
|
||||
notifyFailure(context, e.getMessage());
|
||||
}
|
||||
}).start();
|
||||
}
|
||||
|
||||
private static void notifyUnavailable(Context context) {
|
||||
NotificationManager manager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
Intent settingsIntent = new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
|
||||
Uri.fromParts("package", context.getPackageName(), null));
|
||||
PendingIntent openSettings = PendingIntent.getActivity(context, 0, settingsIntent,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT | PendingIntent.FLAG_IMMUTABLE);
|
||||
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(context, DownloadUtil.DOWNLOAD_NOTIFICATION_CHANNEL_ID)
|
||||
.setContentTitle("No download folder set")
|
||||
.setContentText("Tap to set one in settings")
|
||||
.setSmallIcon(android.R.drawable.stat_notify_error)
|
||||
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||
.setSilent(true)
|
||||
.setContentIntent(openSettings)
|
||||
.setAutoCancel(true);
|
||||
|
||||
manager.notify(1011, builder.build());
|
||||
}
|
||||
|
||||
private static void notifyFailure(Context context, String message) {
|
||||
new Handler(Looper.getMainLooper()).post(() ->
|
||||
Toast.makeText(context, "External download failed: " + message, Toast.LENGTH_LONG).show()
|
||||
);
|
||||
}
|
||||
|
||||
private static void notifySuccess(Context context, String name) {
|
||||
new Handler(Looper.getMainLooper()).post(() ->
|
||||
Toast.makeText(context, "Download success: " + name, Toast.LENGTH_SHORT).show()
|
||||
);
|
||||
}
|
||||
|
||||
private static void notifyExists(Context context, String name) {
|
||||
new Handler(Looper.getMainLooper()).post(() ->
|
||||
Toast.makeText(context, "Already exists: " + name, Toast.LENGTH_SHORT).show()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -52,6 +52,7 @@ object Preferences {
|
|||
private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
|
||||
private const val STREAMING_CACHE_STORAGE = "streaming_cache_storage"
|
||||
private const val DOWNLOAD_STORAGE = "download_storage"
|
||||
private const val DOWNLOAD_DIRECTORY_URI = "download_directory_uri"
|
||||
private const val DEFAULT_DOWNLOAD_VIEW_TYPE = "default_download_view_type"
|
||||
private const val AUDIO_TRANSCODE_DOWNLOAD = "audio_transcode_download"
|
||||
private const val AUDIO_TRANSCODE_DOWNLOAD_PRIORITY = "audio_transcode_download_priority"
|
||||
|
|
@ -452,6 +453,16 @@ object Preferences {
|
|||
).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDownloadDirectoryUri(): String? {
|
||||
return App.getInstance().preferences.getString(DOWNLOAD_DIRECTORY_URI, null)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun setDownloadDirectoryUri(uri: String?) {
|
||||
App.getInstance().preferences.edit().putString(DOWNLOAD_DIRECTORY_URI, uri).apply()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getDefaultDownloadViewType(): String {
|
||||
return App.getInstance().preferences.getString(
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
|||
|
||||
media.setStarred(new Date());
|
||||
|
||||
if (Preferences.isStarredSyncEnabled()) {
|
||||
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(context).download(
|
||||
MappingUtil.mapDownload(media),
|
||||
new Download(media)
|
||||
|
|
|
|||
|
|
@ -109,7 +109,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
|||
|
||||
media.setStarred(new Date());
|
||||
|
||||
if (Preferences.isStarredSyncEnabled()) {
|
||||
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||
DownloadUtil.getDownloadTracker(context).download(
|
||||
MappingUtil.mapDownload(media),
|
||||
new Download(media)
|
||||
|
|
|
|||
9
app/src/main/res/drawable/ic_folder.xml
Normal file
9
app/src/main/res/drawable/ic_folder.xml
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<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="@color/titleTextColor"
|
||||
android:pathData="M10,4L12,6H20c1.1,0 2,0.9 2,2v10c0,1.1 -0.9,2 -2,2H4c-1.1,0 -2,-0.9 -2,-2V6c0,-1.1 0.9,-2 2,-2h6z"/>
|
||||
</vector>
|
||||
|
|
@ -16,4 +16,7 @@
|
|||
<item
|
||||
android:id="@+id/menu_download_group_by_year"
|
||||
android:title="@string/menu_group_by_year" />
|
||||
<item
|
||||
android:id="@+id/menu_download_set_directory"
|
||||
android:title="@string/download_directory_set" />
|
||||
</menu>
|
||||
|
|
@ -68,6 +68,7 @@
|
|||
<string name="download_directory_dialog_positive_button">Download</string>
|
||||
<string name="download_directory_dialog_summary">All tracks in this folder will be downloaded. Tracks present in subfolders will not be downloaded.</string>
|
||||
<string name="download_directory_dialog_title">Download the tracks</string>
|
||||
<string name="download_directory_set">Set where music is downloaded</string>
|
||||
<string name="download_info_empty_subtitle">Once you download a song, you\'ll find it here</string>
|
||||
<string name="download_info_empty_title">No downloads yet!</string>
|
||||
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s items</string>
|
||||
|
|
@ -78,6 +79,7 @@
|
|||
<string name="download_storage_dialog_title">Select storage option</string>
|
||||
<string name="download_storage_external_dialog_positive_button">External</string>
|
||||
<string name="download_storage_internal_dialog_negative_button">Internal</string>
|
||||
<string name="download_storage_directory_dialog_neutral_button">Directory</string>
|
||||
<string name="download_title_section">Downloads</string>
|
||||
<string name="downloaded_bottom_sheet_add_to_queue">Add to queue</string>
|
||||
<string name="downloaded_bottom_sheet_play_next">Play next</string>
|
||||
|
|
|
|||
|
|
@ -180,6 +180,14 @@
|
|||
android:key="download_storage"
|
||||
app:title="@string/settings_download_storage_title" />
|
||||
|
||||
<Preference
|
||||
android:key="set_download_directory"
|
||||
android:title="Set download folder"
|
||||
android:summary="Choose a folder for downloaded music files"
|
||||
android:icon="@drawable/ic_folder"
|
||||
android:order="104"
|
||||
app:isPreferenceVisible="false" />
|
||||
|
||||
<Preference
|
||||
android:key="delete_download_storage"
|
||||
app:title="@string/settings_delete_download_storage_title"
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue