From 458d8732c65af7ab9a9d12e54e15ff077307977d Mon Sep 17 00:00:00 2001 From: Hans-Peter Lehmann Date: Thu, 13 Nov 2025 20:06:50 +0100 Subject: [PATCH] Convert sleep timer to bottom sheet (#8090) --- .../ui/screen/playback/SleepTimerDialog.java | 237 ++++++++---------- app/src/main/res/layout/time_dialog.xml | 28 ++- 2 files changed, 122 insertions(+), 143 deletions(-) diff --git a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/SleepTimerDialog.java b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/SleepTimerDialog.java index 38c40cecb..b8a5da0a4 100644 --- a/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/SleepTimerDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/ui/screen/playback/SleepTimerDialog.java @@ -8,22 +8,22 @@ import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.text.format.DateFormat; +import android.view.LayoutInflater; import android.view.View; +import android.view.ViewGroup; import android.view.inputmethod.InputMethodManager; import android.widget.AdapterView; import android.widget.ArrayAdapter; -import android.widget.Button; -import android.widget.CheckBox; -import android.widget.EditText; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.Spinner; +import android.widget.FrameLayout; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.appcompat.app.AlertDialog; -import androidx.fragment.app.DialogFragment; +import com.google.android.material.bottomsheet.BottomSheetBehavior; +import com.google.android.material.bottomsheet.BottomSheetDialog; +import com.google.android.material.bottomsheet.BottomSheetDialogFragment; import com.google.android.material.dialog.MaterialAlertDialogBuilder; import com.google.android.material.snackbar.Snackbar; @@ -37,6 +37,7 @@ import java.util.Locale; import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.R; +import de.danoeh.antennapod.databinding.TimeDialogBinding; import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent; import de.danoeh.antennapod.playback.base.PlayerStatus; import de.danoeh.antennapod.playback.service.PlaybackController; @@ -54,28 +55,7 @@ import io.reactivex.rxjava3.core.Single; import io.reactivex.rxjava3.disposables.Disposable; import io.reactivex.rxjava3.schedulers.Schedulers; -public class SleepTimerDialog extends DialogFragment { - private PlaybackController controller; - private EditText etxtTime; - private TextView sleepTimerHintText; - private LinearLayout timeSetup; - private LinearLayout timeDisplay; - private TextView time; - private Spinner sleepTimerType; - private CheckBox chAutoEnable; - private ImageView changeTimesButton; - private CheckBox cbVibrate; - private CheckBox cbShakeToReset; - private Button setTimerButton; - private Button playbackPreferencesButton; - - private Disposable disposable; - private volatile Integer currentQueueSize = null; - - Button extendSleepFiveMinutesButton; - Button extendSleepTenMinutesButton; - Button extendSleepTwentyMinutesButton; - +public class SleepTimerDialog extends BottomSheetDialogFragment { private static final int EXTEND_FEW_MINUTES_DISPLAY_VALUE = 5; private static final int EXTEND_FEW_MINUTES = 5 * 1000 * 60; private static final int EXTEND_MID_MINUTES_DISPLAY_VALUE = 10; @@ -85,9 +65,13 @@ public class SleepTimerDialog extends DialogFragment { private static final int EXTEND_FEW_EPISODES = 1; private static final int EXTEND_MID_EPISODES = 2; private static final int EXTEND_LOTS_EPISODES = 3; - private static final int SLEEP_DURATION_DAILY_HOURS_CUTOFF = 12; + private PlaybackController controller; + private TimeDialogBinding viewBinding; + private Disposable disposable; + private volatile Integer currentQueueSize = null; + public SleepTimerDialog() { } @@ -124,13 +108,30 @@ public class SleepTimerDialog extends DialogFragment { @NonNull @Override - public Dialog onCreateDialog(Bundle savedInstanceState) { - View content = View.inflate(getContext(), R.layout.time_dialog, null); - MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext()); - builder.setTitle(R.string.sleep_timer_label); - builder.setView(content); - builder.setPositiveButton(R.string.close_label, null); + public Dialog onCreateDialog(@Nullable Bundle savedInstanceState) { + Dialog dialog = super.onCreateDialog(savedInstanceState); + dialog.setOnShowListener(dialogInterface -> { + BottomSheetDialog bottomSheetDialog = (BottomSheetDialog) dialogInterface; + setupFullHeight(bottomSheetDialog); + }); + return dialog; + } + private void setupFullHeight(BottomSheetDialog bottomSheetDialog) { + FrameLayout bottomSheet = bottomSheetDialog.findViewById(R.id.design_bottom_sheet); + if (bottomSheet != null) { + BottomSheetBehavior behavior = BottomSheetBehavior.from(bottomSheet); + ViewGroup.LayoutParams layoutParams = bottomSheet.getLayoutParams(); + bottomSheet.setLayoutParams(layoutParams); + behavior.setState(BottomSheetBehavior.STATE_EXPANDED); + } + } + + @Nullable + @Override + public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container, + @Nullable Bundle savedInstanceState) { + viewBinding = TimeDialogBinding.inflate(inflater); List spinnerContent = new ArrayList<>(); // add "title" for all options spinnerContent.add(getString(R.string.time_minutes)); @@ -138,10 +139,9 @@ public class SleepTimerDialog extends DialogFragment { ArrayAdapter spinnerAdapter = new ArrayAdapter<>( getContext(), android.R.layout.simple_spinner_item, spinnerContent); spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); - sleepTimerType = content.findViewById(R.id.sleepTimerType); - sleepTimerType.setAdapter(spinnerAdapter); - sleepTimerType.setSelection(SleepTimerPreferences.getSleepTimerType().index); - sleepTimerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + viewBinding.sleepTimerType.setAdapter(spinnerAdapter); + viewBinding.sleepTimerType.setSelection(SleepTimerPreferences.getSleepTimerType().index); + viewBinding.sleepTimerType.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { private boolean sleepTimerTypeInitialized = false; @Override @@ -153,13 +153,13 @@ public class SleepTimerDialog extends DialogFragment { if (sleepTimerTypeInitialized) { // disable auto sleep timer if they've configured it for most of the day if (isSleepTimerConfiguredForMostOfTheDay()) { - chAutoEnable.setChecked(false); + viewBinding.autoEnableCheckbox.setChecked(false); } // change suggested value back to default value for sleep type if (sleepType == SleepTimerType.EPISODES) { - etxtTime.setText(SleepTimerPreferences.DEFAULT_SLEEP_TIMER_EPISODES); + viewBinding.timeEditText.setText(SleepTimerPreferences.DEFAULT_SLEEP_TIMER_EPISODES); } else { - etxtTime.setText(SleepTimerPreferences.DEFAULT_SLEEP_TIMER_MINUTES); + viewBinding.timeEditText.setText(SleepTimerPreferences.DEFAULT_SLEEP_TIMER_MINUTES); } } sleepTimerTypeInitialized = true; @@ -170,27 +170,9 @@ public class SleepTimerDialog extends DialogFragment { public void onNothingSelected(AdapterView parent) { } }); - - etxtTime = content.findViewById(R.id.etxtTime); - sleepTimerHintText = content.findViewById(R.id.sleepTimerHintText); - timeSetup = content.findViewById(R.id.timeSetup); - timeDisplay = content.findViewById(R.id.timeDisplay); - timeDisplay.setVisibility(View.GONE); - time = content.findViewById(R.id.time); - - cbShakeToReset = content.findViewById(R.id.cbShakeToReset); - cbVibrate = content.findViewById(R.id.cbVibrate); - chAutoEnable = content.findViewById(R.id.chAutoEnable); - changeTimesButton = content.findViewById(R.id.changeTimesButton); - setTimerButton = content.findViewById(R.id.setSleeptimerButton); - playbackPreferencesButton = content.findViewById(R.id.playbackPreferencesButton); - - extendSleepFiveMinutesButton = content.findViewById(R.id.extendSleepFiveMinutesButton); - extendSleepTenMinutesButton = content.findViewById(R.id.extendSleepTenMinutesButton); - extendSleepTwentyMinutesButton = content.findViewById(R.id.extendSleepTwentyMinutesButton); - - etxtTime.setText(SleepTimerPreferences.lastTimerValue()); - etxtTime.addTextChangedListener(new TextWatcher() { + viewBinding.timeDisplayContainer.setVisibility(View.GONE); + viewBinding.timeEditText.setText(SleepTimerPreferences.lastTimerValue()); + viewBinding.timeEditText.addTextChangedListener(new TextWatcher() { @Override public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) { } @@ -204,30 +186,27 @@ public class SleepTimerDialog extends DialogFragment { refreshUiState(); } }); - - playbackPreferencesButton.setOnClickListener(view -> { + viewBinding.playbackPreferencesButton.setOnClickListener(view -> { final Intent playbackIntent = new Intent(getActivity(), PreferenceActivity.class); playbackIntent.putExtra(PreferenceActivity.OPEN_PLAYBACK_SETTINGS, true); startActivity(playbackIntent); dismiss(); }); - - etxtTime.postDelayed(() -> { + viewBinding.timeEditText.postDelayed(() -> { InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT); + imm.showSoftInput(viewBinding.timeEditText, InputMethodManager.SHOW_IMPLICIT); }, 100); - refreshUiState(); - chAutoEnable.setChecked(SleepTimerPreferences.autoEnable()); - cbShakeToReset.setChecked(SleepTimerPreferences.shakeToReset()); - cbVibrate.setChecked(SleepTimerPreferences.vibrate()); + viewBinding.autoEnableCheckbox.setChecked(SleepTimerPreferences.autoEnable()); + viewBinding.shakeToResetCheckbox.setChecked(SleepTimerPreferences.shakeToReset()); + viewBinding.vibrateCheckbox.setChecked(SleepTimerPreferences.vibrate()); refreshAutoEnableControls(SleepTimerPreferences.autoEnable()); - cbShakeToReset.setOnCheckedChangeListener((buttonView, isChecked) + viewBinding.shakeToResetCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> SleepTimerPreferences.setShakeToReset(isChecked)); - cbVibrate.setOnCheckedChangeListener((buttonView, isChecked) + viewBinding.vibrateCheckbox.setOnCheckedChangeListener((buttonView, isChecked) -> SleepTimerPreferences.setVibrate(isChecked)); - chAutoEnable.setOnCheckedChangeListener((compoundButton, isChecked) + viewBinding.autoEnableCheckbox.setOnCheckedChangeListener((compoundButton, isChecked) -> { boolean mostOfDay = isSleepTimerConfiguredForMostOfTheDay(); if (isChecked && mostOfDay && SleepTimerPreferences.getSleepTimerType() == SleepTimerType.EPISODES) { @@ -237,23 +216,20 @@ public class SleepTimerDialog extends DialogFragment { }); updateAutoEnableText(); - changeTimesButton.setOnClickListener(changeTimesBtn -> { + viewBinding.changeTimesButton.setOnClickListener(changeTimesBtn -> { int from = SleepTimerPreferences.autoEnableFrom(); int to = SleepTimerPreferences.autoEnableTo(); showTimeRangeDialog(getContext(), from, to); }); - - Button disableButton = content.findViewById(R.id.disableSleeptimerButton); - disableButton.setOnClickListener(v -> { + viewBinding.disableSleeptimerButton.setOnClickListener(v -> { if (controller != null) { controller.disableSleepTimer(); } }); - - setTimerButton.setOnClickListener(v -> { + viewBinding.setSleeptimerButton.setOnClickListener(v -> { if (!PlaybackService.isRunning || (controller != null && controller.getStatus() != PlayerStatus.PLAYING)) { - Snackbar.make(content, R.string.no_media_playing_label, Snackbar.LENGTH_LONG).show(); + Snackbar.make(viewBinding.getRoot(), R.string.no_media_playing_label, Snackbar.LENGTH_LONG).show(); return; } try { @@ -261,13 +237,13 @@ public class SleepTimerDialog extends DialogFragment { if (controller != null) { controller.setSleepTimer(SleepTimerPreferences.timerMillisOrEpisodes()); } - closeKeyboard(content); + closeKeyboard(viewBinding.getRoot()); } catch (NumberFormatException e) { e.printStackTrace(); - Snackbar.make(content, R.string.time_dialog_invalid_input, Snackbar.LENGTH_LONG).show(); + Snackbar.make(viewBinding.getRoot(), R.string.time_dialog_invalid_input, Snackbar.LENGTH_LONG).show(); } }); - return builder.create(); + return viewBinding.getRoot(); } private boolean isSleepTimerConfiguredForMostOfTheDay() { @@ -282,7 +258,7 @@ public class SleepTimerDialog extends DialogFragment { (dialogInterface, i) -> { // disable continuous playback and also disable the auto sleep timer UserPreferences.setFollowQueue(false); - chAutoEnable.setChecked(false); + viewBinding.autoEnableCheckbox.setChecked(false); refreshUiState(); }) .setPositiveButton(R.string.sleep_timer_without_continuous_playback_change_hours, @@ -292,15 +268,14 @@ public class SleepTimerDialog extends DialogFragment { showTimeRangeDialog(getContext(), from, to); }) .create(); - dialog.setOnCancelListener(dialogInterface -> chAutoEnable.setChecked(false)); + dialog.setOnCancelListener(dialogInterface -> viewBinding.autoEnableCheckbox.setChecked(false)); dialog.show(); - // mark the disable continuous playback option in red dialog.getButton(AlertDialog.BUTTON_NEGATIVE) .setTextColor(ThemeUtils.getColorFromAttr(requireContext(), R.attr.colorError)); } private long getSelectedSleepTime() throws NumberFormatException { - long time = Long.parseLong(etxtTime.getText().toString()); + long time = Long.parseLong(viewBinding.timeEditText.getText().toString()); if (time == 0) { throw new NumberFormatException("Timer must not be zero"); } @@ -309,7 +284,7 @@ public class SleepTimerDialog extends DialogFragment { private void refreshAutoEnableControls(boolean enabled) { SleepTimerPreferences.setAutoEnable(enabled); - changeTimesButton.setAlpha(enabled ? 1.0f : 0.5f); + viewBinding.changeTimesButton.setAlpha(enabled ? 1.0f : 0.5f); } private void refreshUiState() { @@ -319,23 +294,23 @@ public class SleepTimerDialog extends DialogFragment { boolean noEpisodeSelection = isEpisodeType && !UserPreferences.isFollowQueue(); if (noEpisodeSelection) { - etxtTime.setEnabled(false); - playbackPreferencesButton.setVisibility(View.VISIBLE); - sleepTimerHintText.setText(R.string.multiple_sleep_episodes_while_continuous_playback_disabled); - sleepTimerHintText.setVisibility(View.VISIBLE); - chAutoEnable.setVisibility(View.GONE); - changeTimesButton.setVisibility(View.GONE); - cbShakeToReset.setVisibility(View.GONE); - cbVibrate.setVisibility(View.GONE); - setTimerButton.setEnabled(false); + viewBinding.timeEditText.setEnabled(false); + viewBinding.playbackPreferencesButton.setVisibility(View.VISIBLE); + viewBinding.sleepTimerHintText.setText(R.string.multiple_sleep_episodes_while_continuous_playback_disabled); + viewBinding.sleepTimerHintText.setVisibility(View.VISIBLE); + viewBinding.autoEnableCheckbox.setVisibility(View.GONE); + viewBinding.changeTimesButton.setVisibility(View.GONE); + viewBinding.shakeToResetCheckbox.setVisibility(View.GONE); + viewBinding.vibrateCheckbox.setVisibility(View.GONE); + viewBinding.setSleeptimerButton.setEnabled(false); } else { - playbackPreferencesButton.setVisibility(View.GONE); - chAutoEnable.setVisibility(View.VISIBLE); - changeTimesButton.setVisibility(View.VISIBLE); - cbShakeToReset.setVisibility(isEpisodeType ? View.GONE : View.VISIBLE); - cbVibrate.setVisibility(View.VISIBLE); - setTimerButton.setEnabled(true); - etxtTime.setEnabled(true); + viewBinding.playbackPreferencesButton.setVisibility(View.GONE); + viewBinding.autoEnableCheckbox.setVisibility(View.VISIBLE); + viewBinding.changeTimesButton.setVisibility(View.VISIBLE); + viewBinding.shakeToResetCheckbox.setVisibility(isEpisodeType ? View.GONE : View.VISIBLE); + viewBinding.vibrateCheckbox.setVisibility(View.VISIBLE); + viewBinding.setSleeptimerButton.setEnabled(true); + viewBinding.timeEditText.setEnabled(true); long selectedSleepTime; try { selectedSleepTime = getSelectedSleepTime(); @@ -346,13 +321,13 @@ public class SleepTimerDialog extends DialogFragment { if (isEpisodeType) { // for episode timers check if the queue length exceeds the number of sleep episodes we have if (currentQueueSize != null && selectedSleepTime > currentQueueSize) { - sleepTimerHintText.setText(getResources().getQuantityString( + viewBinding.sleepTimerHintText.setText(getResources().getQuantityString( R.plurals.episodes_sleep_timer_exceeds_queue, currentQueueSize, currentQueueSize)); - sleepTimerHintText.setVisibility(View.VISIBLE); + viewBinding.sleepTimerHintText.setVisibility(View.VISIBLE); } else { - sleepTimerHintText.setVisibility(View.GONE); // could maybe show duration in minutes + viewBinding.sleepTimerHintText.setVisibility(View.GONE); } } else { // for time sleep timers check if the selected value exceeds the remaining play time in the episode @@ -361,43 +336,43 @@ public class SleepTimerDialog extends DialogFragment { final long timer = TimeUnit.MINUTES.toMillis(selectedSleepTime); if ((timer > remaining) && !UserPreferences.isFollowQueue()) { final int remainingMinutes = Math.toIntExact(TimeUnit.MILLISECONDS.toMinutes(remaining)); - sleepTimerHintText + viewBinding.sleepTimerHintText .setText(getResources().getQuantityString( R.plurals.timer_exceeds_remaining_time_while_continuous_playback_disabled, remainingMinutes, remainingMinutes )); - sleepTimerHintText.setVisibility(View.VISIBLE); + viewBinding.sleepTimerHintText.setVisibility(View.VISIBLE); } else { // don't show it at all - sleepTimerHintText.setVisibility(View.GONE); + viewBinding.sleepTimerHintText.setVisibility(View.GONE); // could maybe show duration in minutes } } } // disable extension for episodes if we're not moving to next one - extendSleepFiveMinutesButton.setEnabled(!noEpisodeSelection); - extendSleepTenMinutesButton.setEnabled(!noEpisodeSelection); - extendSleepTwentyMinutesButton.setEnabled(!noEpisodeSelection); + viewBinding.extendSleepFiveMinutesButton.setEnabled(!noEpisodeSelection); + viewBinding.extendSleepTenMinutesButton.setEnabled(!noEpisodeSelection); + viewBinding.extendSleepTwentyMinutesButton.setEnabled(!noEpisodeSelection); if (SleepTimerPreferences.getSleepTimerType() == SleepTimerType.CLOCK) { - setupExtendButton(extendSleepFiveMinutesButton, + setupExtendButton(viewBinding.extendSleepFiveMinutesButton, getString(R.string.extend_sleep_timer_label, EXTEND_FEW_MINUTES_DISPLAY_VALUE), EXTEND_FEW_MINUTES); - setupExtendButton(extendSleepTenMinutesButton, + setupExtendButton(viewBinding.extendSleepTenMinutesButton, getString(R.string.extend_sleep_timer_label, EXTEND_MID_MINUTES_DISPLAY_VALUE), EXTEND_MID_MINUTES); - setupExtendButton(extendSleepTwentyMinutesButton, + setupExtendButton(viewBinding.extendSleepTwentyMinutesButton, getString(R.string.extend_sleep_timer_label, EXTEND_LOTS_MINUTES_DISPLAY_VALUE), EXTEND_LOTS_MINUTES); } else { - setupExtendButton(extendSleepFiveMinutesButton, + setupExtendButton(viewBinding.extendSleepFiveMinutesButton, "+" + getResources().getQuantityString(R.plurals.num_episodes, EXTEND_FEW_EPISODES, EXTEND_FEW_EPISODES), EXTEND_FEW_EPISODES); - setupExtendButton(extendSleepTenMinutesButton, + setupExtendButton(viewBinding.extendSleepTenMinutesButton, "+" + getResources().getQuantityString(R.plurals.num_episodes, EXTEND_MID_EPISODES, EXTEND_MID_EPISODES), EXTEND_MID_EPISODES); - setupExtendButton(extendSleepTwentyMinutesButton, + setupExtendButton(viewBinding.extendSleepTwentyMinutesButton, "+" + getResources().getQuantityString(R.plurals.num_episodes, EXTEND_LOTS_EPISODES, EXTEND_LOTS_EPISODES), EXTEND_LOTS_EPISODES); } @@ -422,8 +397,9 @@ public class SleepTimerDialog extends DialogFragment { // only change the state if true, don't change it regardless of flag (although we could) if (mostOfDay && SleepTimerPreferences.getSleepTimerType() == SleepTimerType.EPISODES) { confirmAlwaysSleepTimerDialog(); - } else if (!chAutoEnable.isChecked()) { // if it's not checked, then make sure it's checked in UI too - chAutoEnable.setChecked(true); + } else if (!viewBinding.autoEnableCheckbox.isChecked()) { + // if it's not checked, then make sure it's checked in UI too + viewBinding.autoEnableCheckbox.setChecked(true); } updateAutoEnableText(); }); @@ -449,21 +425,22 @@ public class SleepTimerDialog extends DialogFragment { text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo); } - chAutoEnable.setText(text); + viewBinding.autoEnableCheckbox.setText(text); } @Subscribe(threadMode = ThreadMode.MAIN, sticky = true) @SuppressWarnings("unused") public void timerUpdated(SleepTimerUpdatedEvent event) { - timeDisplay.setVisibility(event.isOver() || event.isCancelled() ? View.GONE : View.VISIBLE); - timeSetup.setVisibility(event.isOver() || event.isCancelled() ? View.VISIBLE : View.GONE); - sleepTimerType.setEnabled(event.isOver() || event.isCancelled()); + viewBinding.timeDisplayContainer.setVisibility( + event.isOver() || event.isCancelled() ? View.GONE : View.VISIBLE); + viewBinding.timeSetupContainer.setVisibility(event.isOver() || event.isCancelled() ? View.VISIBLE : View.GONE); + viewBinding.sleepTimerType.setEnabled(event.isOver() || event.isCancelled()); if (SleepTimerPreferences.getSleepTimerType() == SleepTimerType.EPISODES) { - time.setText(getResources().getQuantityString(R.plurals.num_episodes, + viewBinding.time.setText(getResources().getQuantityString(R.plurals.num_episodes, (int) event.getDisplayTimeLeft(), (int) event.getDisplayTimeLeft())); } else { - time.setText(Converter.getDurationStringLong((int) event.getDisplayTimeLeft())); + viewBinding.time.setText(Converter.getDurationStringLong((int) event.getDisplayTimeLeft())); } } diff --git a/app/src/main/res/layout/time_dialog.xml b/app/src/main/res/layout/time_dialog.xml index 7a773f7c0..a2e45cf33 100644 --- a/app/src/main/res/layout/time_dialog.xml +++ b/app/src/main/res/layout/time_dialog.xml @@ -14,7 +14,7 @@ android:padding="16dp"> @@ -25,7 +25,7 @@ android:orientation="horizontal"> + android:text="@string/set_sleeptimer_label" + style="@style/Widget.Material3.Button" /> -