diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/PodcastRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/PodcastRepository.java index edfbbcc6..4237d763 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/PodcastRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/PodcastRepository.java @@ -66,88 +66,33 @@ public class PodcastRepository { return liveNewestPodcastEpisodes; } - public void refreshPodcasts() { - App.getSubsonicClientInstance(false) + public Call refreshPodcasts() { + return App.getSubsonicClientInstance(false) .getPodcastClient() - .refreshPodcasts() - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + .refreshPodcasts(); } - public void createPodcastChannel(String url) { - App.getSubsonicClientInstance(false) + public Call createPodcastChannel(String url) { + return App.getSubsonicClientInstance(false) .getPodcastClient() - .createPodcastChannel(url) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + .createPodcastChannel(url); } - public void deletePodcastChannel(String channelId) { - App.getSubsonicClientInstance(false) + public Call deletePodcastChannel(String channelId) { + return App.getSubsonicClientInstance(false) .getPodcastClient() - .deletePodcastChannel(channelId) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + .deletePodcastChannel(channelId); } - public void deletePodcastEpisode(String episodeId) { - App.getSubsonicClientInstance(false) + public Call deletePodcastEpisode(String episodeId) { + return App.getSubsonicClientInstance(false) .getPodcastClient() - .deletePodcastEpisode(episodeId) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + .deletePodcastEpisode(episodeId); } - public void downloadPodcastEpisode(String episodeId) { - App.getSubsonicClientInstance(false) + public Call downloadPodcastEpisode(String episodeId) { + return App.getSubsonicClientInstance(false) .getPodcastClient() - .downloadPodcastEpisode(episodeId) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + .downloadPodcastEpisode(episodeId); } } diff --git a/app/src/main/java/com/cappielloantonio/tempo/repository/RadioRepository.java b/app/src/main/java/com/cappielloantonio/tempo/repository/RadioRepository.java index 9ad8a111..dea17558 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/repository/RadioRepository.java +++ b/app/src/main/java/com/cappielloantonio/tempo/repository/RadioRepository.java @@ -38,54 +38,22 @@ public class RadioRepository { return radioStation; } - public void createInternetRadioStation(String name, String streamURL, String homepageURL) { - App.getSubsonicClientInstance(false) + public Call createInternetRadioStation(String name, String streamURL, String homepageURL) { + return App.getSubsonicClientInstance(false) .getInternetRadioClient() - .createInternetRadioStation(streamURL, name, homepageURL) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + .createInternetRadioStation(streamURL, name, homepageURL); } - public void updateInternetRadioStation(String id, String name, String streamURL, String homepageURL) { - App.getSubsonicClientInstance(false) + public Call updateInternetRadioStation(String id, String name, String streamURL, String homepageURL) { + return App.getSubsonicClientInstance(false) .getInternetRadioClient() - .updateInternetRadioStation(id, streamURL, name, homepageURL) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + .updateInternetRadioStation(id, streamURL, name, homepageURL); } - public void deleteInternetRadioStation(String id) { - App.getSubsonicClientInstance(false) + public Call deleteInternetRadioStation(String id) { + return App.getSubsonicClientInstance(false) .getInternetRadioClient() - .deleteInternetRadioStation(id) - .enqueue(new Callback() { - @Override - public void onResponse(@NonNull Call call, @NonNull Response response) { - - } - - @Override - public void onFailure(@NonNull Call call, @NonNull Throwable t) { - - } - }); + .deleteInternetRadioStation(id); } + } diff --git a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java index b4aba968..487bb916 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java +++ b/app/src/main/java/com/cappielloantonio/tempo/ui/dialog/RadioEditorDialog.java @@ -3,11 +3,13 @@ package com.cappielloantonio.tempo.ui.dialog; import android.app.Dialog; import android.os.Bundle; import android.text.TextUtils; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.fragment.app.DialogFragment; import androidx.lifecycle.ViewModelProvider; +import com.cappielloantonio.tempo.App; import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.databinding.DialogRadioEditorBinding; import com.cappielloantonio.tempo.interfaces.RadioCallback; @@ -21,7 +23,6 @@ import java.util.Objects; public class RadioEditorDialog extends DialogFragment { private DialogRadioEditorBinding bind; private RadioEditorViewModel radioEditorViewModel; - private final RadioCallback radioCallback; private String radioName; @@ -36,25 +37,26 @@ public class RadioEditorDialog extends DialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { bind = DialogRadioEditorBinding.inflate(getLayoutInflater()); - radioEditorViewModel = new ViewModelProvider(requireActivity()).get(RadioEditorViewModel.class); + setupObservers(); + return new MaterialAlertDialogBuilder(requireContext()) .setView(bind.getRoot()) .setTitle(R.string.radio_editor_dialog_title) .setPositiveButton(R.string.radio_editor_dialog_positive_button, (dialog, id) -> { if (validateInput()) { if (radioEditorViewModel.getRadioToEdit() == null) { - radioEditorViewModel.createRadio(radioName, radioStreamURL, radioHomepageURL.isEmpty() ? null : radioHomepageURL); + radioEditorViewModel.createRadio(radioName, radioStreamURL, + radioHomepageURL.isEmpty() ? null : radioHomepageURL); } else { - radioEditorViewModel.updateRadio(radioName, radioStreamURL, radioHomepageURL.isEmpty() ? null : radioHomepageURL); + radioEditorViewModel.updateRadio(radioName, radioStreamURL, + radioHomepageURL.isEmpty() ? null : radioHomepageURL); } - dismissDialog(); } }) .setNeutralButton(R.string.radio_editor_dialog_neutral_button, (dialog, id) -> { radioEditorViewModel.deleteRadio(); - dismissDialog(); }) .setNegativeButton(R.string.radio_editor_dialog_negative_button, (dialog, id) -> { dialog.cancel(); @@ -62,6 +64,24 @@ public class RadioEditorDialog extends DialogFragment { .create(); } + private void setupObservers() { + radioEditorViewModel.getIsSuccess().observe(this, isSuccess -> { + if (isSuccess != null && isSuccess) { + Toast.makeText(requireContext(), + radioEditorViewModel.getRadioToEdit() == null ? + App.getContext().getString(R.string.radio_editor_dialog_added) : App.getContext().getString(R.string.radio_editor_dialog_updated), + Toast.LENGTH_SHORT).show(); + dismissDialog(); + } + }); + radioEditorViewModel.getErrorMessage().observe(this, error -> { + if (error != null && !error.isEmpty()) { + Toast.makeText(requireContext(), error, Toast.LENGTH_LONG).show(); + radioEditorViewModel.clearError(); + } + }); + } + @Override public void onStart() { super.onStart(); @@ -77,7 +97,6 @@ public class RadioEditorDialog extends DialogFragment { private void setParameterInfo() { if (getArguments() != null && getArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT) != null) { InternetRadioStation toEdit = requireArguments().getParcelable(Constants.INTERNET_RADIO_STATION_OBJECT); - radioEditorViewModel.setRadioToEdit(toEdit); bind.internetRadioStationNameTextView.setText(toEdit.getName()); @@ -90,22 +109,21 @@ public class RadioEditorDialog extends DialogFragment { radioName = Objects.requireNonNull(bind.internetRadioStationNameTextView.getText()).toString().trim(); radioStreamURL = Objects.requireNonNull(bind.internetRadioStationStreamUrlTextView.getText()).toString().trim(); radioHomepageURL = Objects.requireNonNull(bind.internetRadioStationHomepageUrlTextView.getText()).toString().trim(); - if (TextUtils.isEmpty(radioName)) { bind.internetRadioStationNameTextView.setError(getString(R.string.error_required)); return false; } - if (TextUtils.isEmpty(radioStreamURL)) { bind.internetRadioStationStreamUrlTextView.setError(getString(R.string.error_required)); return false; } - return true; } private void dismissDialog() { - radioCallback.onDismiss(); + if (radioCallback != null) { + radioCallback.onDismiss(); + } Objects.requireNonNull(getDialog()).dismiss(); } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelBottomSheetViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelBottomSheetViewModel.java index fe2b6ac9..423e3dd3 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelBottomSheetViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelBottomSheetViewModel.java @@ -1,14 +1,24 @@ package com.cappielloantonio.tempo.viewmodel; import android.app.Application; +import android.util.Log; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; import com.cappielloantonio.tempo.repository.PodcastRepository; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.PodcastChannel; +import java.io.IOException; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + public class PodcastChannelBottomSheetViewModel extends AndroidViewModel { + private static final String TAG = "PodcastChannelBottomSheetViewModel"; private final PodcastRepository podcastRepository; private PodcastChannel podcastChannel; @@ -28,6 +38,59 @@ public class PodcastChannelBottomSheetViewModel extends AndroidViewModel { } public void deletePodcastChannel() { - if (podcastChannel != null) podcastRepository.deletePodcastChannel(podcastChannel.getId()); + if (podcastChannel != null && podcastChannel.getId() != null) { + podcastRepository.deletePodcastChannel(podcastChannel.getId()) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.code() == 501) { + Toast.makeText(getApplication(), + "Podcasts are not supported by this server", + Toast.LENGTH_LONG).show(); + return; + } + + if (response.isSuccessful() && response.body() != null) { + ApiResponse apiResponse = response.body(); + + String status = apiResponse.subsonicResponse.getStatus(); + + if ("ok".equals(status)) { + Toast.makeText(getApplication(), + "Podcast channel deleted", + Toast.LENGTH_SHORT).show(); + //TODO refresh the UI after deleting + //podcastRepository.refreshPodcasts(); + } + } else { + handleHttpError(response); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + Toast.makeText(getApplication(), + "Network error: " + t.getMessage(), + Toast.LENGTH_LONG).show(); + } + }); + } } + + private void handleHttpError(Response response) { + String errorMsg = "HTTP error: " + response.code(); + if (response.errorBody() != null) { + try { + String serverMsg = response.errorBody().string(); + if (!serverMsg.isEmpty()) { + errorMsg += " - " + serverMsg; + } + } catch (IOException e) { + Log.e(TAG, "Error reading error body", e); + } + } + + Toast.makeText(getApplication(), errorMsg, Toast.LENGTH_LONG).show(); + } + } diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelEditorViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelEditorViewModel.java index 759da750..573affda 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelEditorViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/PodcastChannelEditorViewModel.java @@ -1,27 +1,99 @@ package com.cappielloantonio.tempo.viewmodel; import android.app.Application; +import android.util.Log; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.repository.PodcastRepository; -import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; + +import java.io.IOException; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; public class PodcastChannelEditorViewModel extends AndroidViewModel { - private static final String TAG = "RadioEditorViewModel"; + private static final String TAG = "PodcastChannelEditorViewModel"; private final PodcastRepository podcastRepository; - private InternetRadioStation toEdit; + private final MutableLiveData isSuccess = new MutableLiveData<>(false); + private final MutableLiveData errorMessage = new MutableLiveData<>(); public PodcastChannelEditorViewModel(@NonNull Application application) { super(application); - podcastRepository = new PodcastRepository(); } - public void createChannel(String url) { - podcastRepository.createPodcastChannel(url); + public LiveData getIsSuccess() { + return isSuccess; } -} + + public LiveData getErrorMessage() { + return errorMessage; + } + + public void clearError() { + errorMessage.setValue(null); + } + + public void createChannel(String url) { + errorMessage.setValue(null); + isSuccess.setValue(false); + + podcastRepository.createPodcastChannel(url) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.code() == 501) { + showError(getApplication().getString(R.string.podcast_channel_not_supported_snackbar)); + return; + } + + if (response.isSuccessful() && response.body() != null) { + ApiResponse apiResponse = response.body(); + + String status = apiResponse.subsonicResponse.getStatus(); + if ("ok".equals(status)) { + isSuccess.setValue(true); + } + } else { + handleHttpError(response); + } + } + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + showError("Network error: " + t.getMessage()); + Log.e(TAG, "Network error", t); + } + }); + } + + private void handleHttpError(Response response) { + String errorMsg = "HTTP error: " + response.code(); + if (response.errorBody() != null) { + try { + String serverMsg = response.errorBody().string(); + if (!serverMsg.isEmpty()) { + errorMsg += " - " + serverMsg; + } + } catch (IOException e) { + Log.e(TAG, "Error reading error body", e); + } + } + showError(errorMsg); + } + + private void showError(String message) { + Toast.makeText(getApplication(), message, Toast.LENGTH_LONG).show(); + errorMessage.setValue(message); + Log.e(TAG, "Error shown: " + message); + } +} \ No newline at end of file diff --git a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioEditorViewModel.java b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioEditorViewModel.java index c15ea93f..871b0906 100644 --- a/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioEditorViewModel.java +++ b/app/src/main/java/com/cappielloantonio/tempo/viewmodel/RadioEditorViewModel.java @@ -1,26 +1,47 @@ package com.cappielloantonio.tempo.viewmodel; import android.app.Application; +import android.util.Log; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.lifecycle.AndroidViewModel; +import androidx.lifecycle.LiveData; +import androidx.lifecycle.MutableLiveData; +import com.cappielloantonio.tempo.R; import com.cappielloantonio.tempo.repository.RadioRepository; +import com.cappielloantonio.tempo.subsonic.base.ApiResponse; import com.cappielloantonio.tempo.subsonic.models.InternetRadioStation; +import java.io.IOException; + +import retrofit2.Call; +import retrofit2.Callback; +import retrofit2.Response; + public class RadioEditorViewModel extends AndroidViewModel { private static final String TAG = "RadioEditorViewModel"; private final RadioRepository radioRepository; - private InternetRadioStation toEdit; + + private final MutableLiveData isSuccess = new MutableLiveData<>(false); + private final MutableLiveData errorMessage = new MutableLiveData<>(); public RadioEditorViewModel(@NonNull Application application) { super(application); - radioRepository = new RadioRepository(); } + + public LiveData getIsSuccess() { return isSuccess; } + public LiveData getErrorMessage() { return errorMessage; } + + public void clearError() { + errorMessage.setValue(null); + } + public InternetRadioStation getRadioToEdit() { return toEdit; } @@ -30,14 +51,120 @@ public class RadioEditorViewModel extends AndroidViewModel { } public void createRadio(String name, String streamURL, String homepageURL) { - radioRepository.createInternetRadioStation(name, streamURL, homepageURL); + errorMessage.setValue(null); + isSuccess.setValue(false); + + radioRepository.createInternetRadioStation(name, streamURL, homepageURL) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + // Handle HTTP 501 (Not Implemented) from Navidrome + if (response.code() == 501) { + showError(getApplication().getString(R.string.radio_dialog_not_supported_snackbar)); + return; + } + if (response.isSuccessful() && response.body() != null) { + ApiResponse apiResponse = response.body(); + String status = apiResponse.subsonicResponse.getStatus(); + if ("ok".equals(status)) { + isSuccess.setValue(true); + } else if ("failed".equals(status)) { + handleFailedResponse(apiResponse); + } + } else { + errorMessage.setValue("HTTP error: " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + errorMessage.setValue("Network error: " + t.getMessage()); + } + }); } public void updateRadio(String name, String streamURL, String homepageURL) { - if (toEdit != null) radioRepository.updateInternetRadioStation(toEdit.getId(), name, streamURL, homepageURL); + if (toEdit != null && toEdit.getId() != null) { + errorMessage.setValue(null); + isSuccess.setValue(false); + + radioRepository.updateInternetRadioStation(toEdit.getId(), name, streamURL, homepageURL) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse apiResponse = response.body(); + if (apiResponse.subsonicResponse != null) { + String status = apiResponse.subsonicResponse.getStatus(); + if ("ok".equals(status)) { + isSuccess.setValue(true); + } else if ("failed".equals(status)) { + handleFailedResponse(apiResponse); + } + } + } else { + errorMessage.setValue("HTTP error: " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + errorMessage.setValue("Network error: " + t.getMessage()); + } + }); + } } public void deleteRadio() { - if (toEdit != null) radioRepository.deleteInternetRadioStation(toEdit.getId()); + if (toEdit != null && toEdit.getId() != null) { + errorMessage.setValue(null); + isSuccess.setValue(false); + + radioRepository.deleteInternetRadioStation(toEdit.getId()) + .enqueue(new Callback() { + @Override + public void onResponse(@NonNull Call call, @NonNull Response response) { + if (response.isSuccessful() && response.body() != null) { + ApiResponse apiResponse = response.body(); + + String status = apiResponse.subsonicResponse.getStatus(); + + if ("ok".equals(status)) { + isSuccess.setValue(true); + } else if ("failed".equals(status)) { + handleFailedResponse(apiResponse); + } + } else { + errorMessage.setValue("HTTP error: " + response.code()); + } + } + + @Override + public void onFailure(@NonNull Call call, @NonNull Throwable t) { + errorMessage.setValue("Network error: " + t.getMessage()); + } + }); + } } -} + + private void showError(String message) { + Toast.makeText(getApplication(), message, Toast.LENGTH_LONG).show(); + errorMessage.setValue(message); + } + + private void handleFailedResponse(ApiResponse apiResponse) { + String errorMsg = "Unknown server error"; + + if (apiResponse.subsonicResponse.getError() != null) { + errorMsg = apiResponse.subsonicResponse.getError().getMessage(); + + if ("Not implemented".equals(errorMsg)) { + errorMsg = getApplication().getString((R.string.radio_dialog_not_supported_snackbar)); + } + } + + Toast.makeText(getApplication(), errorMsg, Toast.LENGTH_LONG).show(); + + errorMessage.setValue(errorMsg); + } +} \ 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 ca37fc86..ee8eebe5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -256,6 +256,7 @@ Browse Channels RSS Url Podcast Channel + Podcasts are not supported by this server. Description Episodes No episodes available @@ -270,10 +271,13 @@ Cancel Delete Save + Radio station added + Radio station updated Internet Radio Station Click to hide the section\nThe effects will be visible on restart Once you add a radio station, you\'ll find it here No stations found! + Internet radio management are not supported by this server. Cancel Save Rate