From 5733dca68a5c0ad20c1970809bb7a43efb77679e Mon Sep 17 00:00:00 2001 From: antonio Date: Fri, 4 Aug 2023 23:46:33 +0200 Subject: [PATCH] feat: implemented the ability to choose external storage (if available) as storage for offline file downloads --- .../tempo/interfaces/DialogClickCallback.java | 13 +++ .../tempo/service/DownloaderManager.java | 10 +++ .../ui/dialog/DownloadStorageDialog.java | 89 +++++++++++++++++++ .../tempo/ui/fragment/SettingsFragment.java | 36 ++++++++ .../tempo/util/DownloadUtil.java | 16 +++- .../tempo/util/Preferences.kt | 14 +++ .../res/layout/dialog_download_storage.xml | 23 +++++ app/src/main/res/values/strings.xml | 6 ++ app/src/main/res/xml/global_preferences.xml | 16 ++-- 9 files changed, 214 insertions(+), 9 deletions(-) create mode 100644 app/src/main/java/com/cappielloantonio/tempo/interfaces/DialogClickCallback.java create mode 100644 app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadStorageDialog.java create mode 100644 app/src/main/res/layout/dialog_download_storage.xml diff --git a/app/src/main/java/com/cappielloantonio/tempo/interfaces/DialogClickCallback.java b/app/src/main/java/com/cappielloantonio/tempo/interfaces/DialogClickCallback.java new file mode 100644 index 00000000..adc39373 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/interfaces/DialogClickCallback.java @@ -0,0 +1,13 @@ +package com.cappielloantonio.tempo.interfaces; + + +import androidx.annotation.Keep; + +@Keep +public interface DialogClickCallback { + default void onPositiveClick() {} + + default void onNegativeClick() {} + + default void onNeutralClick() {} +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderManager.java b/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderManager.java index a55640b8..ef27164b 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderManager.java +++ b/app/src/main/java/com/cappielloantonio/tempo/service/DownloaderManager.java @@ -89,6 +89,7 @@ public class DownloaderManager { public void remove(MediaItem mediaItem, com.cappielloantonio.tempo.model.Download download) { DownloadService.sendRemoveDownload(context, DownloaderService.class, buildDownloadRequest(mediaItem).id, false); + deleteDatabase(download.getId()); } public void remove(List mediaItems, List downloads) { @@ -97,6 +98,11 @@ public class DownloaderManager { } } + public void removeAll() { + DownloadService.sendRemoveAllDownloads(context, DownloaderService.class, false); + deleteAllDatabase(); + } + private void loadDownloads() { try (DownloadCursor loadedDownloads = downloadIndex.getDownloads()) { while (loadedDownloads.moveToNext()) { @@ -125,6 +131,10 @@ public class DownloaderManager { getDownloadRepository().delete(id); } + public static void deleteAllDatabase() { + getDownloadRepository().deleteAll(); + } + public static void updateDatabase(String id) { getDownloadRepository().update(id); } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadStorageDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadStorageDialog.java new file mode 100644 index 00000000..476dabe1 --- /dev/null +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/DownloadStorageDialog.java @@ -0,0 +1,89 @@ +package com.cappielloantonio.tempo.ui.dialog; + +import android.app.AlertDialog; +import android.app.Dialog; +import android.os.Bundle; +import android.widget.Button; + +import androidx.annotation.NonNull; +import androidx.annotation.OptIn; +import androidx.fragment.app.DialogFragment; +import androidx.media3.common.util.UnstableApi; + +import com.cappielloantonio.tempo.R; +import com.cappielloantonio.tempo.databinding.DialogDownloadStorageBinding; +import com.cappielloantonio.tempo.interfaces.DialogClickCallback; +import com.cappielloantonio.tempo.util.DownloadUtil; +import com.cappielloantonio.tempo.util.Preferences; + +@OptIn(markerClass = UnstableApi.class) +public class DownloadStorageDialog extends DialogFragment { + private DialogDownloadStorageBinding bind; + + private final DialogClickCallback dialogClickCallback; + + public DownloadStorageDialog(DialogClickCallback dialogClickCallback) { + this.dialogClickCallback = dialogClickCallback; + } + + @NonNull + @Override + public Dialog onCreateDialog(Bundle savedInstanceState) { + bind = DialogDownloadStorageBinding.inflate(getLayoutInflater()); + + AlertDialog.Builder builder = new AlertDialog.Builder(getActivity()); + + builder.setView(bind.getRoot()) + .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); + + return builder.create(); + } + + @Override + public void onResume() { + super.onResume(); + setButtonAction(); + } + + @Override + public void onDestroyView() { + super.onDestroyView(); + bind = null; + } + + private void setButtonAction() { + AlertDialog dialog = ((AlertDialog) getDialog()); + + if (dialog != null) { + Button positiveButton = dialog.getButton(Dialog.BUTTON_POSITIVE); + positiveButton.setOnClickListener(v -> { + int currentPreference = Preferences.getDownloadStoragePreference(); + int newPreference = 1; + + if (currentPreference != newPreference) { + Preferences.setDownloadStoragePreference(newPreference); + DownloadUtil.getDownloadTracker(requireContext()).removeAll(); + dialogClickCallback.onPositiveClick(); + } + + dialog.dismiss(); + }); + + Button negativeButton = dialog.getButton(Dialog.BUTTON_NEGATIVE); + negativeButton.setOnClickListener(v -> { + int currentPreference = Preferences.getDownloadStoragePreference(); + int newPreference = 0; + + if (currentPreference != newPreference) { + Preferences.setDownloadStoragePreference(newPreference); + DownloadUtil.getDownloadTracker(requireContext()).removeAll(); + dialogClickCallback.onNegativeClick(); + } + + dialog.dismiss(); + }); + } + } +} diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java index 7e07aa58..7548491c 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/fragment/SettingsFragment.java @@ -21,8 +21,10 @@ import androidx.preference.PreferenceFragmentCompat; import com.cappielloantonio.tempo.BuildConfig; import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.helper.ThemeHelper; +import com.cappielloantonio.tempo.interfaces.DialogClickCallback; import com.cappielloantonio.tempo.interfaces.ScanCallback; import com.cappielloantonio.tempo.ui.activity.MainActivity; +import com.cappielloantonio.tempo.ui.dialog.DownloadStorageDialog; import com.cappielloantonio.tempo.ui.dialog.StarredSyncDialog; import com.cappielloantonio.tempo.util.Preferences; import com.cappielloantonio.tempo.viewmodel.SettingViewModel; @@ -73,6 +75,8 @@ public class SettingsFragment extends PreferenceFragmentCompat { checkEqualizer(); + checkStorage(); + findPreference("version").setSummary(BuildConfig.VERSION_NAME); findPreference("logout").setOnPreferenceClickListener(preference -> { @@ -105,6 +109,22 @@ public class SettingsFragment extends PreferenceFragmentCompat { } return true; }); + + findPreference("download_storage").setOnPreferenceClickListener(preference -> { + DownloadStorageDialog dialog = new DownloadStorageDialog(new DialogClickCallback() { + @Override + public void onPositiveClick() { + findPreference("download_storage").setSummary(R.string.download_storage_external_dialog_positive_button); + } + + @Override + public void onNegativeClick() { + findPreference("download_storage").setSummary(R.string.download_storage_internal_dialog_negative_button); + } + }); + dialog.show(activity.getSupportFragmentManager(), null); + return true; + }); } @Override @@ -144,6 +164,22 @@ public class SettingsFragment extends PreferenceFragmentCompat { } } + private void checkStorage() { + Preference storage = findPreference("download_storage"); + + if (storage == null) return; + + try { + 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); + } + } catch (Exception exception) { + storage.setVisible(false); + } + } + private void getScanStatus() { settingViewModel.getScanStatus(new ScanCallback() { @Override diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java b/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java index bde650f3..6290d54c 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java +++ b/app/src/main/java/com/cappielloantonio/tempo/util/DownloadUtil.java @@ -127,9 +127,19 @@ public final class DownloadUtil { private static synchronized File getDownloadDirectory(Context context) { if (downloadDirectory == null) { - downloadDirectory = context.getExternalFilesDir(null); - if (downloadDirectory == null) { - downloadDirectory = context.getFilesDir(); + if (Preferences.getDownloadStoragePreference() == 0) { + downloadDirectory = context.getExternalFilesDirs(null)[0]; + if (downloadDirectory == null) { + downloadDirectory = context.getFilesDir(); + } + } else { + try { + downloadDirectory = context.getExternalFilesDirs(null)[1]; + } catch (Exception exception) { + downloadDirectory = context.getExternalFilesDirs(null)[0]; + Preferences.setDownloadStoragePreference(0); + } + } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt index 5acdf535..aec83d93 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt +++ b/app/src/main/java/com/cappielloantonio/tempo/util/Preferences.kt @@ -33,6 +33,7 @@ object Preferences { private const val MUSIC_DIRECTORY_SECTION_VISIBILITY = "music_directory_section_visibility" private const val REPLAY_GAIN_MODE = "replay_gain_mode" private const val AUDIO_TRANSCODE_PRIORITY = "audio_transcode_priority" + private const val DOWNLOAD_STORAGE = "download_storage" @JvmStatic fun getServer(): String? { @@ -258,4 +259,17 @@ object Preferences { fun isServerPrioritized(): Boolean { return App.getInstance().preferences.getBoolean(AUDIO_TRANSCODE_PRIORITY, false) } + + @JvmStatic + fun getDownloadStoragePreference(): Int { + return App.getInstance().preferences.getString(DOWNLOAD_STORAGE, "0")!!.toInt() + } + + @JvmStatic + fun setDownloadStoragePreference(storagePreference: Int) { + return App.getInstance().preferences.edit().putString( + DOWNLOAD_STORAGE, + storagePreference.toString() + ).apply() + } } \ No newline at end of file diff --git a/app/src/main/res/layout/dialog_download_storage.xml b/app/src/main/res/layout/dialog_download_storage.xml new file mode 100644 index 00000000..7538eefb --- /dev/null +++ b/app/src/main/res/layout/dialog_download_storage.xml @@ -0,0 +1,23 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f1f18ba0..6141c16e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -52,6 +52,11 @@ Once you download a song, you\'ll find it here No downloads yet! Downloads + Internal + External + Changing the destination of downloaded files from one storage to another will result in the immediate deletion of any previously downloaded files in the other storage. + For the changes to take effect, restart the app. + Select storage option Required http or https prefix required @@ -180,6 +185,7 @@ Size of artwork cache In order to reduce data consumption, avoid downloading covers. Limit mobile data usage + Download storage Adjust audio settings Equalizer https://github.com/CappielloAntonio/tempo diff --git a/app/src/main/res/xml/global_preferences.xml b/app/src/main/res/xml/global_preferences.xml index c7904a33..738f101b 100644 --- a/app/src/main/res/xml/global_preferences.xml +++ b/app/src/main/res/xml/global_preferences.xml @@ -78,12 +78,6 @@ app:title="@string/settings_image_size" app:useSimpleSummaryProvider="true" /> - - + + + +