mirror of
https://github.com/AntennaPod/AntennaPod.git
synced 2025-12-01 12:31:45 +00:00
Add episode count sleep timers (#7841)
This commit is contained in:
@ -99,4 +99,20 @@ public class CancelablePSMPCallback implements PlaybackServiceMediaPlayer.PSMPCa
|
||||
}
|
||||
originalCallback.ensureMediaInfoLoaded(media);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void episodeFinishedPlayback() {
|
||||
if (isCancelled) {
|
||||
return;
|
||||
}
|
||||
originalCallback.episodeFinishedPlayback();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldContinueToNextEpisode() {
|
||||
if (isCancelled) {
|
||||
return false;
|
||||
}
|
||||
return originalCallback.shouldContinueToNextEpisode();
|
||||
}
|
||||
}
|
||||
@ -56,4 +56,13 @@ public class DefaultPSMPCallback implements PlaybackServiceMediaPlayer.PSMPCallb
|
||||
@Override
|
||||
public void ensureMediaInfoLoaded(@NonNull Playable media) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void episodeFinishedPlayback() {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldContinueToNextEpisode() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -3,47 +3,92 @@ package de.danoeh.antennapod.ui.screen.playback;
|
||||
import android.app.Activity;
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.text.Editable;
|
||||
import android.text.TextWatcher;
|
||||
import android.text.format.DateFormat;
|
||||
import android.view.View;
|
||||
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.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
import androidx.fragment.app.DialogFragment;
|
||||
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import de.danoeh.antennapod.playback.service.PlaybackController;
|
||||
import de.danoeh.antennapod.playback.service.PlaybackService;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import de.danoeh.antennapod.R;
|
||||
import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences;
|
||||
import de.danoeh.antennapod.ui.common.Converter;
|
||||
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
|
||||
import de.danoeh.antennapod.playback.base.PlayerStatus;
|
||||
import de.danoeh.antennapod.playback.service.PlaybackController;
|
||||
import de.danoeh.antennapod.playback.service.PlaybackService;
|
||||
import de.danoeh.antennapod.storage.database.DBReader;
|
||||
import de.danoeh.antennapod.storage.preferences.PlaybackPreferences;
|
||||
import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences;
|
||||
import de.danoeh.antennapod.storage.preferences.SleepTimerType;
|
||||
import de.danoeh.antennapod.storage.preferences.UserPreferences;
|
||||
import de.danoeh.antennapod.ui.common.Converter;
|
||||
import de.danoeh.antennapod.ui.common.ThemeUtils;
|
||||
import de.danoeh.antennapod.ui.screen.preferences.PreferenceActivity;
|
||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||
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;
|
||||
|
||||
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;
|
||||
private static final int EXTEND_MID_MINUTES = 10 * 1000 * 60;
|
||||
private static final int EXTEND_LOTS_MINUTES_DISPLAY_VALUE = 30;
|
||||
private static final int EXTEND_LOTS_MINUTES = 30 * 1000 * 60;
|
||||
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;
|
||||
|
||||
public SleepTimerDialog() {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -56,6 +101,12 @@ public class SleepTimerDialog extends DialogFragment {
|
||||
};
|
||||
controller.init();
|
||||
EventBus.getDefault().register(this);
|
||||
|
||||
disposable = Single.fromCallable(() ->
|
||||
DBReader.getRemainingQueueSize(PlaybackPreferences.getCurrentlyPlayingFeedMediaId()))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(result -> currentQueueSize = result);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -64,6 +115,10 @@ public class SleepTimerDialog extends DialogFragment {
|
||||
if (controller != null) {
|
||||
controller.release();
|
||||
}
|
||||
if (disposable != null) {
|
||||
disposable.dispose();
|
||||
disposable = null;
|
||||
}
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@ -71,54 +126,102 @@ public class SleepTimerDialog extends DialogFragment {
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
View content = View.inflate(getContext(), R.layout.time_dialog, null);
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(getContext());
|
||||
MaterialAlertDialogBuilder builder = new MaterialAlertDialogBuilder(requireContext());
|
||||
builder.setTitle(R.string.sleep_timer_label);
|
||||
builder.setView(content);
|
||||
builder.setPositiveButton(R.string.close_label, null);
|
||||
|
||||
List<String> spinnerContent = new ArrayList<>();
|
||||
// add "title" for all options
|
||||
spinnerContent.add(getString(R.string.time_minutes));
|
||||
spinnerContent.add(getString(R.string.sleep_timer_episodes_label));
|
||||
ArrayAdapter<String> 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() {
|
||||
private boolean sleepTimerTypeInitialized = false;
|
||||
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
final SleepTimerType sleepType = SleepTimerType.fromIndex(position);
|
||||
SleepTimerPreferences.setSleepTimerType(sleepType);
|
||||
// this callback is called even when the spinner is first initialized
|
||||
// we need to differentiate these calls
|
||||
if (sleepTimerTypeInitialized) {
|
||||
// disable auto sleep timer if they've configured it for most of the day
|
||||
if (isSleepTimerConfiguredForMostOfTheDay()) {
|
||||
chAutoEnable.setChecked(false);
|
||||
}
|
||||
// change suggested value back to default value for sleep type
|
||||
if (sleepType == SleepTimerType.EPISODES) {
|
||||
etxtTime.setText(SleepTimerPreferences.DEFAULT_SLEEP_TIMER_EPISODES);
|
||||
} else {
|
||||
etxtTime.setText(SleepTimerPreferences.DEFAULT_SLEEP_TIMER_MINUTES);
|
||||
}
|
||||
}
|
||||
sleepTimerTypeInitialized = true;
|
||||
refreshUiState();
|
||||
}
|
||||
|
||||
@Override
|
||||
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);
|
||||
Button extendSleepFiveMinutesButton = content.findViewById(R.id.extendSleepFiveMinutesButton);
|
||||
extendSleepFiveMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 5));
|
||||
Button extendSleepTenMinutesButton = content.findViewById(R.id.extendSleepTenMinutesButton);
|
||||
extendSleepTenMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 10));
|
||||
Button extendSleepTwentyMinutesButton = content.findViewById(R.id.extendSleepTwentyMinutesButton);
|
||||
extendSleepTwentyMinutesButton.setText(getString(R.string.extend_sleep_timer_label, 20));
|
||||
extendSleepFiveMinutesButton.setOnClickListener(v -> {
|
||||
if (controller != null) {
|
||||
controller.extendSleepTimer(5 * 1000 * 60);
|
||||
|
||||
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() {
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
}
|
||||
});
|
||||
extendSleepTenMinutesButton.setOnClickListener(v -> {
|
||||
if (controller != null) {
|
||||
controller.extendSleepTimer(10 * 1000 * 60);
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence charSequence, int i, int i1, int i2) {
|
||||
}
|
||||
});
|
||||
extendSleepTwentyMinutesButton.setOnClickListener(v -> {
|
||||
if (controller != null) {
|
||||
controller.extendSleepTimer(20 * 1000 * 60);
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable editable) {
|
||||
refreshUiState();
|
||||
}
|
||||
});
|
||||
|
||||
etxtTime.setText(SleepTimerPreferences.lastTimerValue());
|
||||
playbackPreferencesButton.setOnClickListener(view -> {
|
||||
final Intent playbackIntent = new Intent(getActivity(), PreferenceActivity.class);
|
||||
playbackIntent.putExtra(PreferenceActivity.OPEN_PLAYBACK_SETTINGS, true);
|
||||
startActivity(playbackIntent);
|
||||
dismiss();
|
||||
});
|
||||
|
||||
etxtTime.postDelayed(() -> {
|
||||
InputMethodManager imm = (InputMethodManager) getContext().getSystemService(Context.INPUT_METHOD_SERVICE);
|
||||
imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT);
|
||||
}, 100);
|
||||
|
||||
final CheckBox cbShakeToReset = content.findViewById(R.id.cbShakeToReset);
|
||||
final CheckBox cbVibrate = content.findViewById(R.id.cbVibrate);
|
||||
chAutoEnable = content.findViewById(R.id.chAutoEnable);
|
||||
final ImageView changeTimesButton = content.findViewById(R.id.changeTimesButton);
|
||||
|
||||
refreshUiState();
|
||||
chAutoEnable.setChecked(SleepTimerPreferences.autoEnable());
|
||||
cbShakeToReset.setChecked(SleepTimerPreferences.shakeToReset());
|
||||
cbVibrate.setChecked(SleepTimerPreferences.vibrate());
|
||||
chAutoEnable.setChecked(SleepTimerPreferences.autoEnable());
|
||||
changeTimesButton.setEnabled(chAutoEnable.isChecked());
|
||||
changeTimesButton.setAlpha(chAutoEnable.isChecked() ? 1.0f : 0.5f);
|
||||
refreshAutoEnableControls(SleepTimerPreferences.autoEnable());
|
||||
|
||||
cbShakeToReset.setOnCheckedChangeListener((buttonView, isChecked)
|
||||
-> SleepTimerPreferences.setShakeToReset(isChecked));
|
||||
@ -126,9 +229,11 @@ public class SleepTimerDialog extends DialogFragment {
|
||||
-> SleepTimerPreferences.setVibrate(isChecked));
|
||||
chAutoEnable.setOnCheckedChangeListener((compoundButton, isChecked)
|
||||
-> {
|
||||
SleepTimerPreferences.setAutoEnable(isChecked);
|
||||
changeTimesButton.setEnabled(isChecked);
|
||||
changeTimesButton.setAlpha(isChecked ? 1.0f : 0.5f);
|
||||
boolean mostOfDay = isSleepTimerConfiguredForMostOfTheDay();
|
||||
if (isChecked && mostOfDay && SleepTimerPreferences.getSleepTimerType() == SleepTimerType.EPISODES) {
|
||||
confirmAlwaysSleepTimerDialog();
|
||||
}
|
||||
refreshAutoEnableControls(isChecked);
|
||||
});
|
||||
updateAutoEnableText();
|
||||
|
||||
@ -144,21 +249,17 @@ public class SleepTimerDialog extends DialogFragment {
|
||||
controller.disableSleepTimer();
|
||||
}
|
||||
});
|
||||
Button setButton = content.findViewById(R.id.setSleeptimerButton);
|
||||
setButton.setOnClickListener(v -> {
|
||||
|
||||
setTimerButton.setOnClickListener(v -> {
|
||||
if (!PlaybackService.isRunning
|
||||
|| (controller != null && controller.getStatus() != PlayerStatus.PLAYING)) {
|
||||
Snackbar.make(content, R.string.no_media_playing_label, Snackbar.LENGTH_LONG).show();
|
||||
return;
|
||||
}
|
||||
try {
|
||||
long time = Long.parseLong(etxtTime.getText().toString());
|
||||
if (time == 0) {
|
||||
throw new NumberFormatException("Timer must not be zero");
|
||||
}
|
||||
SleepTimerPreferences.setLastTimer(etxtTime.getText().toString());
|
||||
SleepTimerPreferences.setLastTimer("" + getSelectedSleepTime());
|
||||
if (controller != null) {
|
||||
controller.setSleepTimer(SleepTimerPreferences.timerMillis());
|
||||
controller.setSleepTimer(SleepTimerPreferences.timerMillisOrEpisodes());
|
||||
}
|
||||
closeKeyboard(content);
|
||||
} catch (NumberFormatException e) {
|
||||
@ -169,11 +270,161 @@ public class SleepTimerDialog extends DialogFragment {
|
||||
return builder.create();
|
||||
}
|
||||
|
||||
private boolean isSleepTimerConfiguredForMostOfTheDay() {
|
||||
return SleepTimerPreferences.autoEnableDuration() > SLEEP_DURATION_DAILY_HOURS_CUTOFF;
|
||||
}
|
||||
|
||||
private void confirmAlwaysSleepTimerDialog() {
|
||||
AlertDialog dialog = new MaterialAlertDialogBuilder(requireContext())
|
||||
.setTitle(R.string.sleep_timer_without_continuous_playback)
|
||||
.setMessage(R.string.sleep_timer_without_continuous_playback_message)
|
||||
.setNegativeButton(R.string.sleep_timer_without_continuous_playback,
|
||||
(dialogInterface, i) -> {
|
||||
// disable continuous playback and also disable the auto sleep timer
|
||||
UserPreferences.setFollowQueue(false);
|
||||
chAutoEnable.setChecked(false);
|
||||
refreshUiState();
|
||||
})
|
||||
.setPositiveButton(R.string.sleep_timer_without_continuous_playback_change_hours,
|
||||
(dialogInterface, i) -> {
|
||||
int from = SleepTimerPreferences.autoEnableFrom();
|
||||
int to = SleepTimerPreferences.autoEnableTo();
|
||||
showTimeRangeDialog(getContext(), from, to);
|
||||
})
|
||||
.create();
|
||||
dialog.setOnCancelListener(dialogInterface -> chAutoEnable.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());
|
||||
if (time == 0) {
|
||||
throw new NumberFormatException("Timer must not be zero");
|
||||
}
|
||||
return time;
|
||||
}
|
||||
|
||||
private void refreshAutoEnableControls(boolean enabled) {
|
||||
SleepTimerPreferences.setAutoEnable(enabled);
|
||||
changeTimesButton.setAlpha(enabled ? 1.0f : 0.5f);
|
||||
}
|
||||
|
||||
private void refreshUiState() {
|
||||
// if we're using episode timer and continuous playback is disabled, don't
|
||||
// let the user use anything other than 1 episode
|
||||
boolean isEpisodeType = SleepTimerPreferences.getSleepTimerType() == SleepTimerType.EPISODES;
|
||||
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);
|
||||
} 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);
|
||||
long selectedSleepTime;
|
||||
try {
|
||||
selectedSleepTime = getSelectedSleepTime();
|
||||
} catch (NumberFormatException nex) {
|
||||
selectedSleepTime = 0;
|
||||
}
|
||||
|
||||
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(
|
||||
R.plurals.episodes_sleep_timer_exceeds_queue,
|
||||
currentQueueSize,
|
||||
currentQueueSize));
|
||||
sleepTimerHintText.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
sleepTimerHintText.setVisibility(View.GONE); // could maybe show duration in minutes
|
||||
}
|
||||
} else {
|
||||
// for time sleep timers check if the selected value exceeds the remaining play time in the episode
|
||||
final int remaining = controller != null ? controller.getDuration() - controller.getPosition() :
|
||||
Integer.MAX_VALUE;
|
||||
final long timer = TimeUnit.MINUTES.toMillis(selectedSleepTime);
|
||||
if ((timer > remaining) && !UserPreferences.isFollowQueue()) {
|
||||
final int remainingMinutes = Math.toIntExact(TimeUnit.MILLISECONDS.toMinutes(remaining));
|
||||
sleepTimerHintText
|
||||
.setText(getResources().getQuantityString(
|
||||
R.plurals.timer_exceeds_remaining_time_while_continuous_playback_disabled,
|
||||
remainingMinutes,
|
||||
remainingMinutes
|
||||
));
|
||||
sleepTimerHintText.setVisibility(View.VISIBLE);
|
||||
} else {
|
||||
// don't show it at all
|
||||
sleepTimerHintText.setVisibility(View.GONE);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// disable extension for episodes if we're not moving to next one
|
||||
extendSleepFiveMinutesButton.setEnabled(!noEpisodeSelection);
|
||||
extendSleepTenMinutesButton.setEnabled(!noEpisodeSelection);
|
||||
extendSleepTwentyMinutesButton.setEnabled(!noEpisodeSelection);
|
||||
|
||||
if (SleepTimerPreferences.getSleepTimerType() == SleepTimerType.CLOCK) {
|
||||
setupExtendButton(extendSleepFiveMinutesButton,
|
||||
getString(R.string.extend_sleep_timer_label, EXTEND_FEW_MINUTES_DISPLAY_VALUE),
|
||||
EXTEND_FEW_MINUTES);
|
||||
setupExtendButton(extendSleepTenMinutesButton,
|
||||
getString(R.string.extend_sleep_timer_label, EXTEND_MID_MINUTES_DISPLAY_VALUE),
|
||||
EXTEND_MID_MINUTES);
|
||||
setupExtendButton(extendSleepTwentyMinutesButton,
|
||||
getString(R.string.extend_sleep_timer_label, EXTEND_LOTS_MINUTES_DISPLAY_VALUE),
|
||||
EXTEND_LOTS_MINUTES);
|
||||
} else {
|
||||
setupExtendButton(extendSleepFiveMinutesButton,
|
||||
"+" + getResources().getQuantityString(R.plurals.num_episodes,
|
||||
EXTEND_FEW_EPISODES, EXTEND_FEW_EPISODES), EXTEND_FEW_EPISODES);
|
||||
setupExtendButton(extendSleepTenMinutesButton,
|
||||
"+" + getResources().getQuantityString(R.plurals.num_episodes,
|
||||
EXTEND_MID_EPISODES, EXTEND_MID_EPISODES), EXTEND_MID_EPISODES);
|
||||
setupExtendButton(extendSleepTwentyMinutesButton,
|
||||
"+" + getResources().getQuantityString(R.plurals.num_episodes,
|
||||
EXTEND_LOTS_EPISODES, EXTEND_LOTS_EPISODES), EXTEND_LOTS_EPISODES);
|
||||
}
|
||||
}
|
||||
|
||||
void setupExtendButton(TextView button, String text, int extendValue) {
|
||||
button.setText(text);
|
||||
button.setOnClickListener(v -> {
|
||||
if (controller != null) {
|
||||
controller.extendSleepTimer(extendValue);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void showTimeRangeDialog(Context context, int from, int to) {
|
||||
TimeRangeDialog dialog = new TimeRangeDialog(context, from, to);
|
||||
dialog.setOnDismissListener(v -> {
|
||||
SleepTimerPreferences.setAutoEnableFrom(dialog.getFrom());
|
||||
SleepTimerPreferences.setAutoEnableTo(dialog.getTo());
|
||||
boolean mostOfDay = isSleepTimerConfiguredForMostOfTheDay();
|
||||
// disable the checkbox if they've selected always
|
||||
// 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);
|
||||
}
|
||||
updateAutoEnableText();
|
||||
});
|
||||
dialog.show();
|
||||
@ -206,7 +457,14 @@ public class SleepTimerDialog extends DialogFragment {
|
||||
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);
|
||||
time.setText(Converter.getDurationStringLong((int) event.getTimeLeft()));
|
||||
sleepTimerType.setEnabled(event.isOver() || event.isCancelled());
|
||||
|
||||
if (SleepTimerPreferences.getSleepTimerType() == SleepTimerType.EPISODES) {
|
||||
time.setText(getResources().getQuantityString(R.plurals.num_episodes,
|
||||
(int) event.getDisplayTimeLeft(), (int) event.getDisplayTimeLeft()));
|
||||
} else {
|
||||
time.setText(Converter.getDurationStringLong((int) event.getDisplayTimeLeft()));
|
||||
}
|
||||
}
|
||||
|
||||
private void closeKeyboard(View content) {
|
||||
|
||||
@ -306,6 +306,10 @@ public class AudioPlayerFragment extends Fragment implements
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@SuppressWarnings("unused")
|
||||
public void sleepTimerUpdate(SleepTimerUpdatedEvent event) {
|
||||
if (event.isOver()) {
|
||||
toolbar.getMenu().findItem(R.id.set_sleeptimer_item).setVisible(true);
|
||||
toolbar.getMenu().findItem(R.id.disable_sleeptimer_item).setVisible(false);
|
||||
}
|
||||
if (event.isCancelled() || event.wasJustEnabled() || event.isOver()) {
|
||||
AudioPlayerFragment.this.loadMediaInfo(false);
|
||||
}
|
||||
|
||||
@ -32,6 +32,7 @@ import org.greenrobot.eventbus.ThreadMode;
|
||||
public class PreferenceActivity extends ToolbarActivity implements SearchPreferenceResultListener {
|
||||
private static final String FRAGMENT_TAG = "tag_preferences";
|
||||
public static final String OPEN_AUTO_DOWNLOAD_SETTINGS = "OpenAutoDownloadSettings";
|
||||
public static final String OPEN_PLAYBACK_SETTINGS = "OpenPlaybackSettings";
|
||||
private SettingsActivityBinding binding;
|
||||
|
||||
@Override
|
||||
@ -55,6 +56,9 @@ public class PreferenceActivity extends ToolbarActivity implements SearchPrefere
|
||||
if (intent.getBooleanExtra(OPEN_AUTO_DOWNLOAD_SETTINGS, false)) {
|
||||
openScreen(R.xml.preferences_autodownload);
|
||||
}
|
||||
if (intent.getBooleanExtra(OPEN_PLAYBACK_SETTINGS, false)) {
|
||||
openScreen(R.xml.preferences_playback);
|
||||
}
|
||||
}
|
||||
|
||||
private PreferenceFragmentCompat getPreferenceScreen(int screen) {
|
||||
|
||||
@ -35,7 +35,8 @@
|
||||
android:inputType="number"
|
||||
android:maxLength="3" />
|
||||
|
||||
<TextView
|
||||
<Spinner
|
||||
android:id="@+id/sleepTimerType"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:text="@string/time_minutes"
|
||||
@ -44,6 +45,17 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<TextView
|
||||
android:id="@+id/sleepTimerHintText"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="0dp"
|
||||
android:visibility="visible"
|
||||
android:layout_weight="1"
|
||||
android:selectAllOnFocus="true"
|
||||
android:layout_margin="8dp"
|
||||
android:ems="2"
|
||||
android:text="@string/multiple_sleep_episodes_while_continuous_playback_disabled" />
|
||||
|
||||
<Button
|
||||
android:id="@+id/setSleeptimerButton"
|
||||
android:layout_width="match_parent"
|
||||
@ -163,6 +175,13 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<Button
|
||||
android:id="@+id/playbackPreferencesButton"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:visibility="visible"
|
||||
android:text="@string/settings_label" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
@ -1,38 +1,45 @@
|
||||
package de.danoeh.antennapod.event.playback;
|
||||
|
||||
import de.danoeh.antennapod.model.playback.TimerValue;
|
||||
|
||||
public class SleepTimerUpdatedEvent {
|
||||
private static final long CANCELLED = Long.MAX_VALUE;
|
||||
private final long timeLeft;
|
||||
private final TimerValue timerValue;
|
||||
|
||||
private SleepTimerUpdatedEvent(long timeLeft) {
|
||||
this.timeLeft = timeLeft;
|
||||
private SleepTimerUpdatedEvent(final TimerValue timerValue) {
|
||||
this.timerValue = timerValue;
|
||||
}
|
||||
|
||||
public static SleepTimerUpdatedEvent justEnabled(long timeLeft) {
|
||||
return new SleepTimerUpdatedEvent(-timeLeft);
|
||||
public static SleepTimerUpdatedEvent justEnabled(final TimerValue timer) {
|
||||
return new SleepTimerUpdatedEvent(new TimerValue(timer.getDisplayValue(), -timer.getMillisValue()));
|
||||
}
|
||||
|
||||
public static SleepTimerUpdatedEvent updated(long timeLeft) {
|
||||
return new SleepTimerUpdatedEvent(Math.max(0, timeLeft));
|
||||
public static SleepTimerUpdatedEvent updated(final TimerValue timer) {
|
||||
return new SleepTimerUpdatedEvent(
|
||||
new TimerValue(Math.max(timer.getDisplayValue(), 0), Math.max(0, timer.getMillisValue())));
|
||||
}
|
||||
|
||||
public static SleepTimerUpdatedEvent cancelled() {
|
||||
return new SleepTimerUpdatedEvent(CANCELLED);
|
||||
return new SleepTimerUpdatedEvent(new TimerValue(CANCELLED, CANCELLED));
|
||||
}
|
||||
|
||||
public long getTimeLeft() {
|
||||
return Math.abs(timeLeft);
|
||||
public long getMillisTimeLeft() {
|
||||
return Math.abs(timerValue.getMillisValue());
|
||||
}
|
||||
|
||||
public long getDisplayTimeLeft() {
|
||||
return Math.abs(timerValue.getDisplayValue());
|
||||
}
|
||||
|
||||
public boolean isOver() {
|
||||
return timeLeft == 0;
|
||||
return timerValue.getMillisValue() == 0;
|
||||
}
|
||||
|
||||
public boolean wasJustEnabled() {
|
||||
return timeLeft < 0;
|
||||
return timerValue.getMillisValue() < 0;
|
||||
}
|
||||
|
||||
public boolean isCancelled() {
|
||||
return timeLeft == CANCELLED;
|
||||
return timerValue.getMillisValue() == CANCELLED;
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
package de.danoeh.antennapod.model.playback;
|
||||
|
||||
public class TimerValue {
|
||||
private final long displayValue; // Value shown to user (milliseconds or number of episodes)
|
||||
private final long millisValue;
|
||||
|
||||
public TimerValue(long displayValue, long millisValue) {
|
||||
this.displayValue = displayValue;
|
||||
this.millisValue = millisValue;
|
||||
}
|
||||
|
||||
public long getDisplayValue() {
|
||||
return displayValue;
|
||||
}
|
||||
|
||||
public long getMillisValue() {
|
||||
return millisValue;
|
||||
}
|
||||
}
|
||||
@ -338,6 +338,10 @@ public abstract class PlaybackServiceMediaPlayer {
|
||||
|
||||
void shouldStop();
|
||||
|
||||
void episodeFinishedPlayback();
|
||||
|
||||
boolean shouldContinueToNextEpisode();
|
||||
|
||||
void onMediaChanged(boolean reloadUI);
|
||||
|
||||
void onPostPlayback(@NonNull Playable media, boolean ended, boolean skipped, boolean playingNext);
|
||||
|
||||
@ -14,6 +14,7 @@ import android.view.SurfaceHolder;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import de.danoeh.antennapod.playback.service.internal.PlayableUtils;
|
||||
import de.danoeh.antennapod.model.playback.TimerValue;
|
||||
import de.danoeh.antennapod.storage.database.DBReader;
|
||||
import de.danoeh.antennapod.storage.database.DBWriter;
|
||||
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
|
||||
@ -355,18 +356,18 @@ public abstract class PlaybackController {
|
||||
}
|
||||
}
|
||||
|
||||
public long getSleepTimerTimeLeft() {
|
||||
public TimerValue getSleepTimerTimeLeft() {
|
||||
if (playbackService != null) {
|
||||
return playbackService.getSleepTimerTimeLeft();
|
||||
} else {
|
||||
return Playable.INVALID_TIME;
|
||||
return new TimerValue(Playable.INVALID_TIME, Playable.INVALID_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
public void extendSleepTimer(long extendTime) {
|
||||
long timeLeft = getSleepTimerTimeLeft();
|
||||
if (playbackService != null && timeLeft != Playable.INVALID_TIME) {
|
||||
setSleepTimer(timeLeft + extendTime);
|
||||
TimerValue timeLeft = getSleepTimerTimeLeft();
|
||||
if (playbackService != null && timeLeft.getMillisValue() != Playable.INVALID_TIME) {
|
||||
setSleepTimer(timeLeft.getDisplayValue() + extendTime);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -57,13 +57,16 @@ import androidx.media.utils.MediaConstants;
|
||||
import de.danoeh.antennapod.event.PlayerStatusEvent;
|
||||
import de.danoeh.antennapod.net.sync.serviceinterface.SynchronizationQueue;
|
||||
import de.danoeh.antennapod.playback.service.internal.ClockSleepTimer;
|
||||
import de.danoeh.antennapod.playback.service.internal.EpisodeSleepTimer;
|
||||
import de.danoeh.antennapod.playback.service.internal.LocalPSMP;
|
||||
import de.danoeh.antennapod.playback.service.internal.PlayableUtils;
|
||||
import de.danoeh.antennapod.playback.service.internal.PlaybackServiceNotificationBuilder;
|
||||
import de.danoeh.antennapod.playback.service.internal.PlaybackServiceStateManager;
|
||||
import de.danoeh.antennapod.playback.service.internal.PlaybackServiceTaskManager;
|
||||
import de.danoeh.antennapod.playback.service.internal.PlaybackVolumeUpdater;
|
||||
import de.danoeh.antennapod.model.playback.TimerValue;
|
||||
import de.danoeh.antennapod.playback.service.internal.WearMediaSession;
|
||||
import de.danoeh.antennapod.storage.preferences.SleepTimerType;
|
||||
import de.danoeh.antennapod.ui.notifications.NotificationUtils;
|
||||
import de.danoeh.antennapod.ui.widget.WidgetUpdater;
|
||||
import io.reactivex.rxjava3.disposables.CompositeDisposable;
|
||||
@ -341,6 +344,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
unregisterReceiver(audioBecomingNoisy);
|
||||
mediaPlayer.shutdown();
|
||||
taskManager.shutdown();
|
||||
disableSleepTimer();
|
||||
EventBus.getDefault().unregister(this);
|
||||
}
|
||||
|
||||
@ -900,7 +904,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
|
||||
if (newInfo.getOldPlayerStatus() != null && newInfo.getOldPlayerStatus() != PlayerStatus.SEEKING
|
||||
&& SleepTimerPreferences.autoEnable() && autoEnableByTime && !sleepTimerActive()) {
|
||||
setSleepTimer(SleepTimerPreferences.timerMillis());
|
||||
setSleepTimer(SleepTimerPreferences.timerMillisOrEpisodes());
|
||||
EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_enabled_label),
|
||||
(ctx) -> disableSleepTimer(), getString(R.string.undo)));
|
||||
}
|
||||
@ -984,6 +988,16 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
return PlaybackService.this.getNextInQueue(currentMedia);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldContinueToNextEpisode() {
|
||||
return PlaybackService.this.shouldContinueToNextEpisode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void episodeFinishedPlayback() {
|
||||
PlaybackService.this.episodeFinishedPlayback();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public Playable findMedia(@NonNull String url) {
|
||||
@ -1038,9 +1052,9 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
int newPosition = mediaPlayer.getPosition() - (int) SleepTimer.NOTIFICATION_THRESHOLD / 2;
|
||||
newPosition = Math.max(newPosition, 0);
|
||||
seekTo(newPosition);
|
||||
} else if (event.getTimeLeft() < SleepTimer.NOTIFICATION_THRESHOLD) {
|
||||
} else if (event.getMillisTimeLeft() < SleepTimer.NOTIFICATION_THRESHOLD) {
|
||||
final float[] multiplicators = {0.1f, 0.2f, 0.3f, 0.3f, 0.3f, 0.4f, 0.4f, 0.4f, 0.6f, 0.8f};
|
||||
float multiplicator = multiplicators[Math.max(0, (int) event.getTimeLeft() / 1000)];
|
||||
float multiplicator = multiplicators[Math.max(0, (int) event.getMillisTimeLeft() / 1000)];
|
||||
Log.d(TAG, "onSleepTimerAlmostExpired: " + multiplicator);
|
||||
mediaPlayer.setVolume(multiplicator, multiplicator);
|
||||
} else if (event.isCancelled()) {
|
||||
@ -1076,7 +1090,11 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!UserPreferences.isFollowQueue()) {
|
||||
// continue playback if user has enabled continuous playback
|
||||
// OR they enabled an episode sleep timer and there are still episodes left to play
|
||||
final boolean continuousPlayback = UserPreferences.isFollowQueue() && shouldContinueToNextEpisode();
|
||||
|
||||
if (!continuousPlayback) {
|
||||
Log.d(TAG, "getNextInQueue(), but follow queue is not enabled.");
|
||||
PlaybackPreferences.writeMediaPlaying(nextItem.getMedia());
|
||||
updateNotificationAndMediaSession(nextItem.getMedia());
|
||||
@ -1095,6 +1113,23 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
return nextItem.getMedia();
|
||||
}
|
||||
|
||||
private void episodeFinishedPlayback() {
|
||||
if (sleepTimer != null) {
|
||||
sleepTimer.episodeFinishedPlayback();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tells if we should continue to next episode
|
||||
* @return True if we can proceed to the next episode (might be blocked by other things), or not
|
||||
*/
|
||||
private boolean shouldContinueToNextEpisode() {
|
||||
if (sleepTimer != null) {
|
||||
return sleepTimer.shouldContinueToNextEpisode();
|
||||
}
|
||||
return true; // always allow when no sleep timer is active
|
||||
}
|
||||
|
||||
/**
|
||||
* Set of instructions to be performed when playback ends.
|
||||
*/
|
||||
@ -1104,6 +1139,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
if (stopPlaying) {
|
||||
taskManager.cancelPositionSaver();
|
||||
cancelPositionObserver();
|
||||
disableSleepTimer();
|
||||
if (!isCasting) {
|
||||
stateManager.stopForeground(true);
|
||||
stateManager.stopService();
|
||||
@ -1218,7 +1254,8 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
if (sleepTimerActive()) {
|
||||
sleepTimer.updateRemainingTime(waitingTime);
|
||||
} else {
|
||||
sleepTimer = new ClockSleepTimer(getApplicationContext());
|
||||
sleepTimer = SleepTimerPreferences.getSleepTimerType() == SleepTimerType.CLOCK ?
|
||||
new ClockSleepTimer(getApplicationContext()) : new EpisodeSleepTimer(getApplicationContext());
|
||||
sleepTimer.start(waitingTime);
|
||||
}
|
||||
}
|
||||
@ -1512,11 +1549,11 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
return sleepTimer != null && sleepTimer.isActive();
|
||||
}
|
||||
|
||||
public long getSleepTimerTimeLeft() {
|
||||
public TimerValue getSleepTimerTimeLeft() {
|
||||
if (sleepTimerActive()) {
|
||||
return sleepTimer.getTimeLeft();
|
||||
} else {
|
||||
return 0;
|
||||
return new TimerValue(0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@ -2037,7 +2074,7 @@ public class PlaybackService extends MediaBrowserServiceCompat {
|
||||
if (sleepTimerActive()) {
|
||||
disableSleepTimer();
|
||||
} else {
|
||||
setSleepTimer(SleepTimerPreferences.timerMillis());
|
||||
setSleepTimer(SleepTimerPreferences.timerMillisOrEpisodes());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
|
||||
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
|
||||
import de.danoeh.antennapod.model.playback.TimerValue;
|
||||
import de.danoeh.antennapod.storage.preferences.SleepTimerPreferences;
|
||||
|
||||
public class ClockSleepTimer implements SleepTimer {
|
||||
@ -42,12 +43,13 @@ public class ClockSleepTimer implements SleepTimer {
|
||||
}
|
||||
timeLeft -= timeSinceLastTick;
|
||||
|
||||
EventBus.getDefault().postSticky(SleepTimerUpdatedEvent.updated(timeLeft));
|
||||
final TimerValue left = getTimeLeft();
|
||||
EventBus.getDefault().postSticky(SleepTimerUpdatedEvent.updated(left));
|
||||
if (timeLeft < NOTIFICATION_THRESHOLD) {
|
||||
notifyAboutExpiry();
|
||||
}
|
||||
if (timeLeft <= 0) {
|
||||
Log.d(TAG, "Sleep timer expired");
|
||||
Log.d(TAG, "Clock Sleep timer expired");
|
||||
stop();
|
||||
}
|
||||
}
|
||||
@ -95,11 +97,11 @@ public class ClockSleepTimer implements SleepTimer {
|
||||
|
||||
// make sure we've registered for events first
|
||||
EventBus.getDefault().register(this);
|
||||
final long left = getTimeLeft();
|
||||
final TimerValue left = getTimeLeft();
|
||||
EventBus.getDefault().post(SleepTimerUpdatedEvent.justEnabled(left));
|
||||
|
||||
lastTick = System.currentTimeMillis();
|
||||
EventBus.getDefault().postSticky(SleepTimerUpdatedEvent.updated(timeLeft));
|
||||
EventBus.getDefault().postSticky(SleepTimerUpdatedEvent.updated(left));
|
||||
|
||||
isRunning = true;
|
||||
}
|
||||
@ -121,8 +123,8 @@ public class ClockSleepTimer implements SleepTimer {
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getTimeLeft() {
|
||||
return timeLeft;
|
||||
public TimerValue getTimeLeft() {
|
||||
return new TimerValue(timeLeft, timeLeft);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -134,4 +136,19 @@ public class ClockSleepTimer implements SleepTimer {
|
||||
public void reset() {
|
||||
updateRemainingTime(initialWaitingTime);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEndingThisEpisode(long episodeRemainingMillis) {
|
||||
return episodeRemainingMillis >= getTimeLeft().getMillisValue();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldContinueToNextEpisode() {
|
||||
return getTimeLeft().getMillisValue() > 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void episodeFinishedPlayback() {
|
||||
//no-op
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,67 @@
|
||||
package de.danoeh.antennapod.playback.service.internal;
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
|
||||
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
|
||||
import de.danoeh.antennapod.model.playback.TimerValue;
|
||||
|
||||
public class EpisodeSleepTimer extends ClockSleepTimer {
|
||||
|
||||
public EpisodeSleepTimer(final Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEndingThisEpisode(long episodeRemainingMillis) {
|
||||
return getTimeLeft().getDisplayValue() == 1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public TimerValue getTimeLeft() {
|
||||
TimerValue x = super.getTimeLeft();
|
||||
return new TimerValue(x.getDisplayValue(), TimeUnit.DAYS.toMillis(x.getDisplayValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playbackPositionUpdate(PlaybackPositionEvent playbackPositionEvent) {
|
||||
long currentEpisodeTimeLeft = playbackPositionEvent.getDuration() - playbackPositionEvent.getPosition();
|
||||
|
||||
final TimerValue current = getTimeLeft();
|
||||
|
||||
if (isEndingThisEpisode(playbackPositionEvent.getPosition())) {
|
||||
// if we're ending this episode send the "correct" remaining time
|
||||
// this ensures that the last 10 seconds the playback volume will be reduced
|
||||
EventBus.getDefault().post(SleepTimerUpdatedEvent.updated(new TimerValue(
|
||||
current.getDisplayValue(), currentEpisodeTimeLeft)));
|
||||
|
||||
if (currentEpisodeTimeLeft < NOTIFICATION_THRESHOLD) {
|
||||
notifyAboutExpiry();
|
||||
}
|
||||
} else {
|
||||
// if we have more than 1 episode left then just report the current values
|
||||
EventBus.getDefault().post(SleepTimerUpdatedEvent.updated(current));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void episodeFinishedPlayback() {
|
||||
// episode has finished, decrease the number of episodes left
|
||||
updateRemainingTime(getTimeLeft().getDisplayValue() - 1);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean shouldContinueToNextEpisode() {
|
||||
boolean cont = getTimeLeft().getDisplayValue() > 0; // number of episodes left
|
||||
// stop ourselves too if we're blocking playback
|
||||
if (!cont) {
|
||||
stop();
|
||||
}
|
||||
|
||||
return cont;
|
||||
}
|
||||
}
|
||||
@ -678,9 +678,11 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
|
||||
|
||||
@Override
|
||||
protected void endPlayback(final boolean hasEnded, final boolean wasSkipped,
|
||||
final boolean shouldContinue, final boolean toStoppedState) {
|
||||
boolean shouldContinue, final boolean toStoppedState) {
|
||||
releaseWifiLockIfNecessary();
|
||||
|
||||
callback.episodeFinishedPlayback(); // notify that the current episode just finished
|
||||
|
||||
boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
|
||||
|
||||
// we're relying on the position stored in the Playable object for post-playback processing
|
||||
@ -700,6 +702,9 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer {
|
||||
final Playable currentMedia = media;
|
||||
Playable nextMedia = null;
|
||||
|
||||
// we should continue to next episode if we were told to continue and we're allowed to (by sleep timer)
|
||||
shouldContinue &= callback.shouldContinueToNextEpisode();
|
||||
|
||||
if (shouldContinue) {
|
||||
// Load next episode if previous episode was in the queue and if there
|
||||
// is an episode in the queue left.
|
||||
|
||||
@ -1,13 +1,15 @@
|
||||
package de.danoeh.antennapod.playback.service.internal;
|
||||
|
||||
import de.danoeh.antennapod.model.playback.TimerValue;
|
||||
|
||||
public interface SleepTimer {
|
||||
|
||||
long NOTIFICATION_THRESHOLD = 10000;
|
||||
|
||||
/**
|
||||
* @return Returns time left for this timer, in millis
|
||||
* @return Returns time left for this sleep timer, both display value and in milis
|
||||
*/
|
||||
long getTimeLeft();
|
||||
TimerValue getTimeLeft();
|
||||
|
||||
/**
|
||||
* Starts the sleep timer.
|
||||
@ -35,4 +37,19 @@ public interface SleepTimer {
|
||||
* @return True if sleep timer is active, false otherwise
|
||||
*/
|
||||
boolean isActive();
|
||||
|
||||
/**
|
||||
* @param episodeRemainingMillis Remaining milliseconds of current episode
|
||||
* @return Returns true if the sleep timer will terminate sometime during this episode, false otherwise
|
||||
*/
|
||||
boolean isEndingThisEpisode(long episodeRemainingMillis);
|
||||
|
||||
/**
|
||||
* Called when sleep timer is asked if playback is allowed to proceed to next episode.
|
||||
* Should take into account the time left, episodes left, etc.
|
||||
* @return True if playback is allowed to continue to next episode, false otherwise
|
||||
*/
|
||||
boolean shouldContinueToNextEpisode();
|
||||
|
||||
void episodeFinishedPlayback();
|
||||
}
|
||||
|
||||
@ -198,6 +198,23 @@ public final class DBReader {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the remaining queue size, given a current item, including the current item.
|
||||
* If the current item is not found it will return 0.
|
||||
*/
|
||||
public static int getRemainingQueueSize(long existingId) {
|
||||
final LongList wholeQueue = getQueueIDList();
|
||||
|
||||
// now try to find the id
|
||||
for (int i = 0; i < wholeQueue.size(); ++i) {
|
||||
if (wholeQueue.get(i) == existingId) {
|
||||
return wholeQueue.size() - i; // return however many are left, including us
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads a list of the FeedItems in the queue. If the FeedItems of the queue are not used directly, consider using
|
||||
* {@link #getQueueIDList()} instead.
|
||||
|
||||
@ -15,13 +15,16 @@ public class SleepTimerPreferences {
|
||||
public static final String PREF_NAME = "SleepTimerDialog";
|
||||
private static final String PREF_VALUE = "LastValue";
|
||||
|
||||
private static final String PREF_TIMER_TYPE = "sleepTimerType";
|
||||
private static final String PREF_VIBRATE = "Vibrate";
|
||||
private static final String PREF_SHAKE_TO_RESET = "ShakeToReset";
|
||||
private static final String PREF_AUTO_ENABLE = "AutoEnable";
|
||||
private static final String PREF_AUTO_ENABLE_FROM = "AutoEnableFrom";
|
||||
private static final String PREF_AUTO_ENABLE_TO = "AutoEnableTo";
|
||||
|
||||
private static final String DEFAULT_LAST_TIMER = "15";
|
||||
public static final String DEFAULT_SLEEP_TIMER_MINUTES = "15";
|
||||
public static final String DEFAULT_SLEEP_TIMER_EPISODES = "1";
|
||||
private static final int DEFAULT_TIMER_TYPE = 0;
|
||||
private static final int DEFAULT_AUTO_ENABLE_FROM = 22;
|
||||
private static final int DEFAULT_AUTO_ENABLE_TO = 6;
|
||||
|
||||
@ -42,12 +45,22 @@ public class SleepTimerPreferences {
|
||||
}
|
||||
|
||||
public static String lastTimerValue() {
|
||||
return prefs.getString(PREF_VALUE, DEFAULT_LAST_TIMER);
|
||||
return prefs.getString(PREF_VALUE, DEFAULT_SLEEP_TIMER_MINUTES);
|
||||
}
|
||||
|
||||
public static long timerMillis() {
|
||||
long value = Long.parseLong(lastTimerValue());
|
||||
return TimeUnit.MINUTES.toMillis(value);
|
||||
public static long timerMillisOrEpisodes() {
|
||||
return switch (getSleepTimerType()) {
|
||||
case CLOCK -> TimeUnit.MINUTES.toMillis(Long.parseLong(lastTimerValue()));
|
||||
case EPISODES -> Long.parseLong(SleepTimerPreferences.lastTimerValue());
|
||||
};
|
||||
}
|
||||
|
||||
public static SleepTimerType getSleepTimerType() {
|
||||
return SleepTimerType.fromIndex(prefs.getInt(PREF_TIMER_TYPE, DEFAULT_TIMER_TYPE));
|
||||
}
|
||||
|
||||
public static void setSleepTimerType(SleepTimerType newType) {
|
||||
prefs.edit().putInt(PREF_TIMER_TYPE, newType.index).apply();
|
||||
}
|
||||
|
||||
public static void setVibrate(boolean vibrate) {
|
||||
@ -90,6 +103,16 @@ public class SleepTimerPreferences {
|
||||
return prefs.getInt(PREF_AUTO_ENABLE_TO, DEFAULT_AUTO_ENABLE_TO);
|
||||
}
|
||||
|
||||
public static int autoEnableDuration() {
|
||||
final int from = SleepTimerPreferences.autoEnableFrom();
|
||||
final int to = SleepTimerPreferences.autoEnableTo();
|
||||
if (from >= to) {
|
||||
return 24 - (from - to);
|
||||
} else {
|
||||
return to - from;
|
||||
}
|
||||
}
|
||||
|
||||
public static boolean isInTimeRange(int from, int to, int current) {
|
||||
// Range covers one day
|
||||
if (from < to) {
|
||||
|
||||
@ -0,0 +1,21 @@
|
||||
package de.danoeh.antennapod.storage.preferences;
|
||||
|
||||
public enum SleepTimerType {
|
||||
CLOCK(0),
|
||||
EPISODES(1);
|
||||
|
||||
public final int index;
|
||||
|
||||
SleepTimerType(int index) {
|
||||
this.index = index;
|
||||
}
|
||||
|
||||
public static SleepTimerType fromIndex(int index) {
|
||||
for (SleepTimerType stt : values()) {
|
||||
if (stt.index == index) {
|
||||
return stt;
|
||||
}
|
||||
}
|
||||
return SleepTimerType.EPISODES;
|
||||
}
|
||||
}
|
||||
@ -9,7 +9,6 @@ import android.view.KeyEvent;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
||||
@ -400,7 +399,6 @@ public abstract class UserPreferences {
|
||||
/**
|
||||
* Set to true to enable Continuous Playback
|
||||
*/
|
||||
@VisibleForTesting
|
||||
public static void setFollowQueue(boolean value) {
|
||||
prefs.edit().putBoolean(UserPreferences.PREF_FOLLOW_QUEUE, value).apply();
|
||||
}
|
||||
|
||||
@ -640,9 +640,23 @@
|
||||
<string name="set_sleeptimer_label">Set sleep timer</string>
|
||||
<string name="disable_sleeptimer_label">Disable sleep timer</string>
|
||||
<string name="extend_sleep_timer_label">+%d min</string>
|
||||
<string name="sleep_timer_episodes_label">episodes</string>
|
||||
<string name="sleep_timer_without_continuous_playback">Disable continuous playback</string>
|
||||
<string name="sleep_timer_without_continuous_playback_change_hours">Change hours</string>
|
||||
<string name="sleep_timer_without_continuous_playback_message">Instead of always automatically enabling the sleep timer with one episode, please instead disable \"continuous playback\" in the settings.</string>
|
||||
<string name="sleep_timer_always">Always</string>
|
||||
<string name="sleep_timer_label">Sleep timer</string>
|
||||
<string name="time_dialog_invalid_input">Invalid input, time has to be an integer</string>
|
||||
<plurals name="timer_exceeds_remaining_time_while_continuous_playback_disabled">
|
||||
<item quantity="one">Continuous playback is disabled and the current episode has only %1$d minute left</item>
|
||||
<item quantity="other">Continuous playback is disabled and the current episode has only %1$d minutes left</item>
|
||||
</plurals>
|
||||
<string name="multiple_sleep_episodes_while_continuous_playback_disabled">Continuous playback is disabled in the settings. Always stopping at the end of the episode.</string>
|
||||
<plurals name="episodes_sleep_timer_exceeds_queue">
|
||||
<item quantity="zero">There are %1$d episodes left in your queue</item>
|
||||
<item quantity="one">There is only %1$d episode left in your queue</item>
|
||||
<item quantity="other">There are only %1$d episodes left in your queue</item>
|
||||
</plurals>
|
||||
<string name="shake_to_reset_label">Shake to reset</string>
|
||||
<string name="timer_vibration_label">Vibrate shortly before end</string>
|
||||
<string name="time_seconds">seconds</string>
|
||||
|
||||
Reference in New Issue
Block a user