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)
|
.setTitle(R.string.download_storage_dialog_title)
|
||||||
.setPositiveButton(R.string.download_storage_external_dialog_positive_button, null)
|
.setPositiveButton(R.string.download_storage_external_dialog_positive_button, null)
|
||||||
.setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null)
|
.setNegativeButton(R.string.download_storage_internal_dialog_negative_button, null)
|
||||||
|
.setNeutralButton(R.string.download_storage_directory_dialog_neutral_button, null)
|
||||||
.create();
|
.create();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -74,6 +75,20 @@ public class DownloadStorageDialog extends DialogFragment {
|
||||||
|
|
||||||
dialog.dismiss();
|
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);
|
Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE);
|
||||||
positiveButton.setOnClickListener(v -> {
|
positiveButton.setOnClickListener(v -> {
|
||||||
starredSyncViewModel.getStarredTracks(requireActivity()).observe(requireActivity(), songs -> {
|
starredSyncViewModel.getStarredTracks(requireActivity()).observe(requireActivity(), songs -> {
|
||||||
if (songs != null) {
|
if (songs != null && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(context).download(
|
DownloadUtil.getDownloadTracker(context).download(
|
||||||
MappingUtil.mapDownloads(songs),
|
MappingUtil.mapDownloads(songs),
|
||||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
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.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
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.AlbumPageViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaybackViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
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) {
|
if (item.getItemId() == R.id.action_download_album) {
|
||||||
albumPageViewModel.getAlbumSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,9 @@ import com.cappielloantonio.tempo.ui.adapter.MusicDirectoryAdapter;
|
||||||
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
import com.cappielloantonio.tempo.ui.dialog.DownloadDirectoryDialog;
|
||||||
import com.cappielloantonio.tempo.util.Constants;
|
import com.cappielloantonio.tempo.util.Constants;
|
||||||
import com.cappielloantonio.tempo.util.DownloadUtil;
|
import com.cappielloantonio.tempo.util.DownloadUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
|
import com.cappielloantonio.tempo.viewmodel.DirectoryViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
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 -> {
|
directoryViewModel.loadMusicDirectory(getArguments().getString(Constants.MUSIC_DIRECTORY_ID)).observe(getViewLifecycleOwner(), directory -> {
|
||||||
if (isVisible() && getActivity() != null) {
|
if (isVisible() && getActivity() != null) {
|
||||||
List<Child> songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList());
|
List<Child> songs = directory.getChildren().stream().filter(child -> !child.isDir()).collect(Collectors.toList());
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
MappingUtil.mapDownloads(songs),
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
songs.stream().map(Download::new).collect(Collectors.toList())
|
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.android.material.appbar.MaterialToolbar;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
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.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
@ -40,6 +45,7 @@ import java.util.Objects;
|
||||||
@UnstableApi
|
@UnstableApi
|
||||||
public class DownloadFragment extends Fragment implements ClickCallback {
|
public class DownloadFragment extends Fragment implements ClickCallback {
|
||||||
private static final String TAG = "DownloadFragment";
|
private static final String TAG = "DownloadFragment";
|
||||||
|
private static final int REQUEST_CODE_PICK_DIRECTORY = 1002;
|
||||||
|
|
||||||
private FragmentDownloadBinding bind;
|
private FragmentDownloadBinding bind;
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
|
|
@ -216,6 +222,10 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
||||||
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null));
|
downloadViewModel.initViewStack(new DownloadStack(Constants.DOWNLOAD_TYPE_YEAR, null));
|
||||||
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR);
|
Preferences.setDefaultDownloadViewType(Constants.DOWNLOAD_TYPE_YEAR);
|
||||||
return true;
|
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;
|
return false;
|
||||||
|
|
@ -267,4 +277,20 @@ public class DownloadFragment extends Fragment implements ClickCallback {
|
||||||
public void onDownloadGroupLongClick(Bundle bundle) {
|
public void onDownloadGroupLongClick(Bundle bundle) {
|
||||||
Navigation.findNavController(requireView()).navigate(R.id.downloadBottomSheetDialog, 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.android.material.snackbar.Snackbar;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
import com.google.common.util.concurrent.ListenableFuture;
|
||||||
|
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.stream.Collectors;
|
import java.util.stream.Collectors;
|
||||||
|
|
@ -277,7 +279,7 @@ public class HomeTabMusicFragment extends Fragment implements ClickCallback {
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initSyncStarredView() {
|
private void initSyncStarredView() {
|
||||||
if (Preferences.isStarredSyncEnabled()) {
|
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
|
homeViewModel.getAllStarredTracks().observeForever(new Observer<List<Child>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(List<Child> songs) {
|
public void onChanged(List<Child> songs) {
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import java.util.ArrayList;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.lifecycle.ViewModelProvider;
|
import androidx.lifecycle.ViewModelProvider;
|
||||||
|
import androidx.media3.common.MediaItem;
|
||||||
import androidx.media3.common.MediaMetadata;
|
import androidx.media3.common.MediaMetadata;
|
||||||
import androidx.media3.common.Player;
|
import androidx.media3.common.Player;
|
||||||
import androidx.media3.common.util.UnstableApi;
|
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.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.Preferences;
|
import com.cappielloantonio.tempo.util.Preferences;
|
||||||
|
import com.cappielloantonio.tempo.util.ExternalAudioWriter;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlayerBottomSheetViewModel;
|
||||||
import com.cappielloantonio.tempo.subsonic.models.Child;
|
import com.cappielloantonio.tempo.subsonic.models.Child;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
@ -115,10 +117,16 @@ public class PlayerCoverFragment extends Fragment {
|
||||||
playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), song -> {
|
playerBottomSheetViewModel.getLiveMedia().observe(getViewLifecycleOwner(), song -> {
|
||||||
if (song != null && bind != null) {
|
if (song != null && bind != null) {
|
||||||
bind.innerButtonTopLeft.setOnClickListener(view -> {
|
bind.innerButtonTopLeft.setOnClickListener(view -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
MappingUtil.mapDownload(song),
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
new Download(song)
|
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 -> {
|
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.DownloadUtil;
|
||||||
import com.cappielloantonio.tempo.util.MappingUtil;
|
import com.cappielloantonio.tempo.util.MappingUtil;
|
||||||
import com.cappielloantonio.tempo.util.MusicUtil;
|
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.PlaybackViewModel;
|
||||||
import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel;
|
import com.cappielloantonio.tempo.viewmodel.PlaylistPageViewModel;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
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) {
|
if (item.getItemId() == R.id.action_download_playlist) {
|
||||||
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
playlistPageViewModel.getPlaylistSongLiveList().observe(getViewLifecycleOwner(), songs -> {
|
||||||
if (isVisible() && getActivity() != null) {
|
if (isVisible() && getActivity() != null) {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
MappingUtil.mapDownloads(songs),
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
songs.stream().map(child -> {
|
MappingUtil.mapDownloads(songs),
|
||||||
Download toDownload = new Download(child);
|
songs.stream().map(child -> {
|
||||||
toDownload.setPlaylistId(playlistPageViewModel.getPlaylist().getId());
|
Download toDownload = new Download(child);
|
||||||
toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName());
|
toDownload.setPlaylistId(playlistPageViewModel.getPlaylist().getId());
|
||||||
return toDownload;
|
toDownload.setPlaylistName(playlistPageViewModel.getPlaylist().getName());
|
||||||
}).collect(Collectors.toList())
|
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;
|
return true;
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,19 @@
|
||||||
package com.cappielloantonio.tempo.ui.fragment;
|
package com.cappielloantonio.tempo.ui.fragment;
|
||||||
|
|
||||||
import android.content.ComponentName;
|
import android.app.Activity;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.ComponentName;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.ServiceConnection;
|
import android.content.ServiceConnection;
|
||||||
import android.media.audiofx.AudioEffect;
|
import android.media.audiofx.AudioEffect;
|
||||||
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.os.IBinder;
|
import android.os.IBinder;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.view.WindowManager;
|
import android.view.WindowManager;
|
||||||
|
import android.widget.Toast;
|
||||||
|
|
||||||
import androidx.activity.result.ActivityResultLauncher;
|
import androidx.activity.result.ActivityResultLauncher;
|
||||||
import androidx.activity.result.contract.ActivityResultContracts;
|
import androidx.activity.result.contract.ActivityResultContracts;
|
||||||
|
|
@ -59,7 +61,8 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
private MainActivity activity;
|
private MainActivity activity;
|
||||||
private SettingViewModel settingViewModel;
|
private SettingViewModel settingViewModel;
|
||||||
|
|
||||||
private ActivityResultLauncher<Intent> someActivityResultLauncher;
|
private ActivityResultLauncher<Intent> equalizerResultLauncher;
|
||||||
|
private ActivityResultLauncher<Intent> directoryPickerLauncher;
|
||||||
|
|
||||||
private MediaService.LocalBinder mediaServiceBinder;
|
private MediaService.LocalBinder mediaServiceBinder;
|
||||||
private boolean isServiceBound = false;
|
private boolean isServiceBound = false;
|
||||||
|
|
@ -68,9 +71,30 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
public void onCreate(@Nullable Bundle savedInstanceState) {
|
public void onCreate(@Nullable Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
someActivityResultLauncher = registerForActivityResult(
|
equalizerResultLauncher = registerForActivityResult(
|
||||||
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
|
result -> {}
|
||||||
|
);
|
||||||
|
|
||||||
|
directoryPickerLauncher = registerForActivityResult(
|
||||||
new ActivityResultContracts.StartActivityForResult(),
|
new ActivityResultContracts.StartActivityForResult(),
|
||||||
result -> {
|
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();
|
checkSystemEqualizer();
|
||||||
checkCacheStorage();
|
checkCacheStorage();
|
||||||
checkStorage();
|
checkStorage();
|
||||||
|
checkDownloadDirectory();
|
||||||
|
|
||||||
setStreamingCacheSize();
|
setStreamingCacheSize();
|
||||||
setAppLanguage();
|
setAppLanguage();
|
||||||
|
|
@ -114,6 +139,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
actionSyncStarredArtists();
|
actionSyncStarredArtists();
|
||||||
actionChangeStreamingCacheStorage();
|
actionChangeStreamingCacheStorage();
|
||||||
actionChangeDownloadStorage();
|
actionChangeDownloadStorage();
|
||||||
|
actionSetDownloadDirectory();
|
||||||
actionDeleteDownloadStorage();
|
actionDeleteDownloadStorage();
|
||||||
actionKeepScreenOn();
|
actionKeepScreenOn();
|
||||||
actionAutoDownloadLyrics();
|
actionAutoDownloadLyrics();
|
||||||
|
|
@ -151,7 +177,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
|
|
||||||
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
if ((intent.resolveActivity(requireActivity().getPackageManager()) != null)) {
|
||||||
equalizer.setOnPreferenceClickListener(preference -> {
|
equalizer.setOnPreferenceClickListener(preference -> {
|
||||||
someActivityResultLauncher.launch(intent);
|
equalizerResultLauncher.launch(intent);
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -168,7 +194,7 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||||
storage.setVisible(false);
|
storage.setVisible(false);
|
||||||
} else {
|
} 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) {
|
} catch (Exception exception) {
|
||||||
storage.setVisible(false);
|
storage.setVisible(false);
|
||||||
|
|
@ -184,13 +210,46 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
if (requireContext().getExternalFilesDirs(null)[1] == null) {
|
||||||
storage.setVisible(false);
|
storage.setVisible(false);
|
||||||
} else {
|
} 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) {
|
} catch (Exception exception) {
|
||||||
storage.setVisible(false);
|
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() {
|
private void setStreamingCacheSize() {
|
||||||
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
ListPreference streamingCachePreference = findPreference("streaming_cache_size");
|
||||||
|
|
||||||
|
|
@ -338,11 +397,19 @@ public class SettingsFragment extends PreferenceFragmentCompat {
|
||||||
@Override
|
@Override
|
||||||
public void onPositiveClick() {
|
public void onPositiveClick() {
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button);
|
||||||
|
checkDownloadDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onNegativeClick() {
|
public void onNegativeClick() {
|
||||||
findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button);
|
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);
|
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() {
|
private void actionDeleteDownloadStorage() {
|
||||||
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
findPreference("delete_download_storage").setOnPreferenceClickListener(preference -> {
|
||||||
DeleteDownloadStorageDialog dialog = new DeleteDownloadStorageDialog();
|
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.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.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;
|
||||||
|
|
@ -163,7 +164,14 @@ 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());
|
||||||
|
|
||||||
downloadAll.setOnClickListener(v -> {
|
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();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
@ -238,7 +246,7 @@ 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 (DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
if (Preferences.getDownloadDirectoryUri() == null && DownloadUtil.getDownloadTracker(requireContext()).areDownloaded(mediaItems)) {
|
||||||
removeAll.setVisibility(View.VISIBLE);
|
removeAll.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -39,6 +39,10 @@ import com.cappielloantonio.tempo.viewmodel.SongBottomSheetViewModel;
|
||||||
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
import com.google.android.material.bottomsheet.BottomSheetDialogFragment;
|
||||||
import com.google.common.util.concurrent.ListenableFuture;
|
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.ArrayList;
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
|
|
@ -159,10 +163,16 @@ 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 -> {
|
||||||
DownloadUtil.getDownloadTracker(requireContext()).download(
|
MediaItem item = MappingUtil.mapMediaItem(song);
|
||||||
MappingUtil.mapDownload(song),
|
String title = item.mediaMetadata.title != null ? item.mediaMetadata.title.toString() : item.mediaId;
|
||||||
new Download(song)
|
if (Preferences.getDownloadDirectoryUri() == null) {
|
||||||
);
|
DownloadUtil.getDownloadTracker(requireContext()).download(
|
||||||
|
MappingUtil.mapDownload(song),
|
||||||
|
new Download(song)
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
ExternalAudioWriter.downloadToUserDirectory(requireContext(), item, title);
|
||||||
|
}
|
||||||
dismissBottomSheet();
|
dismissBottomSheet();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,19 +187,21 @@ public final class DownloadUtil {
|
||||||
|
|
||||||
private static synchronized File getDownloadDirectory(Context context) {
|
private static synchronized File getDownloadDirectory(Context context) {
|
||||||
if (downloadDirectory == null) {
|
if (downloadDirectory == null) {
|
||||||
if (Preferences.getDownloadStoragePreference() == 0) {
|
int pref = Preferences.getDownloadStoragePreference();
|
||||||
|
if (pref == 0) {
|
||||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||||
if (downloadDirectory == null) {
|
if (downloadDirectory == null) {
|
||||||
downloadDirectory = context.getFilesDir();
|
downloadDirectory = context.getFilesDir();
|
||||||
}
|
}
|
||||||
} else {
|
} else if (pref == 1) {
|
||||||
try {
|
try {
|
||||||
downloadDirectory = context.getExternalFilesDirs(null)[1];
|
downloadDirectory = context.getExternalFilesDirs(null)[1];
|
||||||
} catch (Exception exception) {
|
} catch (Exception exception) {
|
||||||
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
downloadDirectory = context.getExternalFilesDirs(null)[0];
|
||||||
Preferences.setDownloadStoragePreference(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 AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority"
|
||||||
private const val STREAMING_CACHE_STORAGE = "streaming_cache_storage"
|
private const val STREAMING_CACHE_STORAGE = "streaming_cache_storage"
|
||||||
private const val DOWNLOAD_STORAGE = "download_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 DEFAULT_DOWNLOAD_VIEW_TYPE = "default_download_view_type"
|
||||||
private const val AUDIO_TRANSCODE_DOWNLOAD = "audio_transcode_download"
|
private const val AUDIO_TRANSCODE_DOWNLOAD = "audio_transcode_download"
|
||||||
private const val AUDIO_TRANSCODE_DOWNLOAD_PRIORITY = "audio_transcode_download_priority"
|
private const val AUDIO_TRANSCODE_DOWNLOAD_PRIORITY = "audio_transcode_download_priority"
|
||||||
|
|
@ -452,6 +453,16 @@ object Preferences {
|
||||||
).apply()
|
).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
|
@JvmStatic
|
||||||
fun getDefaultDownloadViewType(): String {
|
fun getDefaultDownloadViewType(): String {
|
||||||
return App.getInstance().preferences.getString(
|
return App.getInstance().preferences.getString(
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ public class PlayerBottomSheetViewModel extends AndroidViewModel {
|
||||||
|
|
||||||
media.setStarred(new Date());
|
media.setStarred(new Date());
|
||||||
|
|
||||||
if (Preferences.isStarredSyncEnabled()) {
|
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(context).download(
|
DownloadUtil.getDownloadTracker(context).download(
|
||||||
MappingUtil.mapDownload(media),
|
MappingUtil.mapDownload(media),
|
||||||
new Download(media)
|
new Download(media)
|
||||||
|
|
|
||||||
|
|
@ -109,7 +109,7 @@ public class SongBottomSheetViewModel extends AndroidViewModel {
|
||||||
|
|
||||||
media.setStarred(new Date());
|
media.setStarred(new Date());
|
||||||
|
|
||||||
if (Preferences.isStarredSyncEnabled()) {
|
if (Preferences.isStarredSyncEnabled() && Preferences.getDownloadDirectoryUri() == null) {
|
||||||
DownloadUtil.getDownloadTracker(context).download(
|
DownloadUtil.getDownloadTracker(context).download(
|
||||||
MappingUtil.mapDownload(media),
|
MappingUtil.mapDownload(media),
|
||||||
new Download(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
|
<item
|
||||||
android:id="@+id/menu_download_group_by_year"
|
android:id="@+id/menu_download_group_by_year"
|
||||||
android:title="@string/menu_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>
|
</menu>
|
||||||
|
|
@ -68,6 +68,7 @@
|
||||||
<string name="download_directory_dialog_positive_button">Download</string>
|
<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_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_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_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_info_empty_title">No downloads yet!</string>
|
||||||
<string name="download_item_multiple_subtitle_formatter">%1$s • %2$s items</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_dialog_title">Select storage option</string>
|
||||||
<string name="download_storage_external_dialog_positive_button">External</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_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="download_title_section">Downloads</string>
|
||||||
<string name="downloaded_bottom_sheet_add_to_queue">Add to queue</string>
|
<string name="downloaded_bottom_sheet_add_to_queue">Add to queue</string>
|
||||||
<string name="downloaded_bottom_sheet_play_next">Play next</string>
|
<string name="downloaded_bottom_sheet_play_next">Play next</string>
|
||||||
|
|
|
||||||
|
|
@ -180,6 +180,14 @@
|
||||||
android:key="download_storage"
|
android:key="download_storage"
|
||||||
app:title="@string/settings_download_storage_title" />
|
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
|
<Preference
|
||||||
android:key="delete_download_storage"
|
android:key="delete_download_storage"
|
||||||
app:title="@string/settings_delete_download_storage_title"
|
app:title="@string/settings_delete_download_storage_title"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue