Add option to enable sleep timer based on current time (#6384)

This commit is contained in:
mueller-ma 2023-04-15 21:08:03 +02:00 committed by GitHub
parent 8396a34670
commit 0bdf9d9e28
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 346 additions and 32 deletions

View File

@ -0,0 +1,22 @@
package de.test.antennapod.service.playback;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
public class SleepTimerPreferencesTest {
@Test
public void testIsInTimeRange() {
assertTrue(SleepTimerPreferences.isInTimeRange(0, 10, 8));
assertTrue(SleepTimerPreferences.isInTimeRange(1, 10, 8));
assertTrue(SleepTimerPreferences.isInTimeRange(1, 10, 1));
assertTrue(SleepTimerPreferences.isInTimeRange(20, 10, 8));
assertTrue(SleepTimerPreferences.isInTimeRange(20, 20, 8));
assertFalse(SleepTimerPreferences.isInTimeRange(1, 6, 8));
assertFalse(SleepTimerPreferences.isInTimeRange(1, 6, 6));
assertFalse(SleepTimerPreferences.isInTimeRange(20, 6, 8));
}
}

View File

@ -4,6 +4,7 @@ import android.app.Activity;
import android.app.Dialog;
import android.content.Context;
import android.os.Bundle;
import android.text.format.DateFormat;
import android.view.View;
import android.view.inputmethod.InputMethodManager;
import android.widget.Button;
@ -11,19 +12,25 @@ import android.widget.CheckBox;
import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.TextView;
import androidx.annotation.NonNull;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import androidx.fragment.app.DialogFragment;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import com.google.android.material.snackbar.Snackbar;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.Locale;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.service.playback.PlaybackService;
import de.danoeh.antennapod.core.util.Converter;
import de.danoeh.antennapod.core.util.playback.PlaybackController;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
public class SleepTimerDialog extends DialogFragment {
private PlaybackController controller;
@ -31,6 +38,7 @@ public class SleepTimerDialog extends DialogFragment {
private LinearLayout timeSetup;
private LinearLayout timeDisplay;
private TextView time;
private CheckBox chAutoEnable;
public SleepTimerDialog() {
@ -99,20 +107,38 @@ public class SleepTimerDialog extends DialogFragment {
imm.showSoftInput(etxtTime, InputMethodManager.SHOW_IMPLICIT);
}, 100);
CheckBox cbShakeToReset = content.findViewById(R.id.cbShakeToReset);
CheckBox cbVibrate = content.findViewById(R.id.cbVibrate);
CheckBox chAutoEnable = content.findViewById(R.id.chAutoEnable);
final CheckBox cbShakeToReset = content.findViewById(R.id.cbShakeToReset);
final CheckBox cbVibrate = content.findViewById(R.id.cbVibrate);
chAutoEnable = content.findViewById(R.id.chAutoEnable);
final TextView changeTimesButton = content.findViewById(R.id.changeTimes);
cbShakeToReset.setChecked(SleepTimerPreferences.shakeToReset());
cbVibrate.setChecked(SleepTimerPreferences.vibrate());
chAutoEnable.setChecked(SleepTimerPreferences.autoEnable());
changeTimesButton.setEnabled(chAutoEnable.isChecked());
cbShakeToReset.setOnCheckedChangeListener((buttonView, isChecked)
-> SleepTimerPreferences.setShakeToReset(isChecked));
cbVibrate.setOnCheckedChangeListener((buttonView, isChecked)
-> SleepTimerPreferences.setVibrate(isChecked));
chAutoEnable.setOnCheckedChangeListener((compoundButton, isChecked)
-> SleepTimerPreferences.setAutoEnable(isChecked));
-> {
SleepTimerPreferences.setAutoEnable(isChecked);
changeTimesButton.setEnabled(isChecked);
});
updateAutoEnableText();
changeTimesButton.setOnClickListener(changeTimesBtn -> {
int from = SleepTimerPreferences.autoEnableFrom();
int to = SleepTimerPreferences.autoEnableTo();
TimeRangeDialog dialog = new TimeRangeDialog(getContext(), from, to);
dialog.setOnDismissListener(v -> {
SleepTimerPreferences.setAutoEnableFrom(dialog.getFrom());
SleepTimerPreferences.setAutoEnableTo(dialog.getTo());
updateAutoEnableText();
});
dialog.show();
});
Button disableButton = content.findViewById(R.id.disableSleeptimerButton);
disableButton.setOnClickListener(v -> {
@ -144,6 +170,28 @@ public class SleepTimerDialog extends DialogFragment {
return builder.create();
}
private void updateAutoEnableText() {
String text;
int from = SleepTimerPreferences.autoEnableFrom();
int to = SleepTimerPreferences.autoEnableTo();
if (from == to) {
text = getString(R.string.auto_enable_label);
} else if (DateFormat.is24HourFormat(getContext())) {
String formattedFrom = String.format(Locale.getDefault(), "%02d:00", from);
String formattedTo = String.format(Locale.getDefault(), "%02d:00", to);
text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo);
} else {
String formattedFrom = String.format(Locale.getDefault(), "%02d:00 %s",
from % 12, from >= 12 ? "PM" : "AM");
String formattedTo = String.format(Locale.getDefault(), "%02d:00 %s",
to % 12, to >= 12 ? "PM" : "AM");
text = getString(R.string.auto_enable_label_with_times, formattedFrom, formattedTo);
}
chAutoEnable.setText(text);
}
@Subscribe(threadMode = ThreadMode.MAIN)
@SuppressWarnings("unused")
public void timerUpdated(SleepTimerUpdatedEvent event) {

View File

@ -0,0 +1,187 @@
package de.danoeh.antennapod.dialog;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Paint;
import android.graphics.Point;
import android.graphics.RectF;
import android.text.format.DateFormat;
import android.view.MotionEvent;
import android.view.View;
import androidx.annotation.NonNull;
import com.google.android.material.dialog.MaterialAlertDialogBuilder;
import de.danoeh.antennapod.R;
import de.danoeh.antennapod.ui.common.ThemeUtils;
import java.util.Locale;
public class TimeRangeDialog extends MaterialAlertDialogBuilder {
private final TimeRangeView view;
public TimeRangeDialog(@NonNull Context context, int from, int to) {
super(context);
view = new TimeRangeView(context, from, to);
setView(view);
setPositiveButton(android.R.string.ok, null);
}
public int getFrom() {
return view.from;
}
public int getTo() {
return view.to;
}
static class TimeRangeView extends View {
private static final int DIAL_ALPHA = 120;
private final Paint paintDial = new Paint();
private final Paint paintSelected = new Paint();
private final Paint paintText = new Paint();
private int from;
private int to;
private final RectF bounds = new RectF();
int touching = 0;
public TimeRangeView(Context context) { // Used by Android tools
this(context, 0, 0);
}
public TimeRangeView(Context context, int from, int to) {
super(context);
this.from = from;
this.to = to;
setup();
}
private void setup() {
paintDial.setAntiAlias(true);
paintDial.setStyle(Paint.Style.STROKE);
paintDial.setStrokeCap(Paint.Cap.ROUND);
paintDial.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorPrimary));
paintDial.setAlpha(DIAL_ALPHA);
paintSelected.setAntiAlias(true);
paintSelected.setStyle(Paint.Style.STROKE);
paintSelected.setStrokeCap(Paint.Cap.ROUND);
paintSelected.setColor(ThemeUtils.getColorFromAttr(getContext(), R.attr.colorAccent));
paintText.setAntiAlias(true);
paintText.setStyle(Paint.Style.FILL);
paintText.setColor(ThemeUtils.getColorFromAttr(getContext(), android.R.attr.textColorPrimary));
paintText.setTextAlign(Paint.Align.CENTER);
}
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY
&& MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
} else if (MeasureSpec.getMode(widthMeasureSpec) == MeasureSpec.EXACTLY) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
} else if (MeasureSpec.getMode(heightMeasureSpec) == MeasureSpec.EXACTLY) {
super.onMeasure(heightMeasureSpec, heightMeasureSpec);
} else if (MeasureSpec.getSize(widthMeasureSpec) < MeasureSpec.getSize(heightMeasureSpec)) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec);
} else {
super.onMeasure(heightMeasureSpec, heightMeasureSpec);
}
}
@Override
protected void onDraw(Canvas canvas) {
super.onDraw(canvas);
float size = getHeight(); // square
float padding = size * 0.1f;
paintDial.setStrokeWidth(size * 0.005f);
bounds.set(padding, padding, size - padding, size - padding);
paintText.setAlpha(DIAL_ALPHA);
canvas.drawArc(bounds, 0, 360, false, paintDial);
for (int i = 0; i < 24; i++) {
paintDial.setStrokeWidth(size * 0.005f);
if (i % 6 == 0) {
paintDial.setStrokeWidth(size * 0.01f);
Point textPos = radToPoint(i / 24.0f * 360.f, size / 2 - 2.5f * padding);
paintText.setTextSize(0.4f * padding);
canvas.drawText(String.valueOf(i), textPos.x,
textPos.y + (-paintText.descent() - paintText.ascent()) / 2, paintText);
}
Point outer = radToPoint(i / 24.0f * 360.f, size / 2 - 1.7f * padding);
Point inner = radToPoint(i / 24.0f * 360.f, size / 2 - 1.9f * padding);
canvas.drawLine(outer.x, outer.y, inner.x, inner.y, paintDial);
}
paintText.setAlpha(255);
float angleFrom = (float) from / 24 * 360 - 90;
float angleDistance = (float) ((to - from + 24) % 24) / 24 * 360;
paintSelected.setStrokeWidth(padding / 6);
paintSelected.setStyle(Paint.Style.STROKE);
canvas.drawArc(bounds, angleFrom, angleDistance, false, paintSelected);
paintSelected.setStyle(Paint.Style.FILL);
Point p1 = radToPoint(angleFrom + 90, size / 2 - padding);
canvas.drawCircle(p1.x, p1.y, padding / 2, paintSelected);
Point p2 = radToPoint(angleFrom + angleDistance + 90, size / 2 - padding);
canvas.drawCircle(p2.x, p2.y, padding / 2, paintSelected);
paintText.setTextSize(0.6f * padding);
String timeRange;
if (from == to) {
timeRange = getContext().getString(R.string.sleep_timer_always);
} else if (DateFormat.is24HourFormat(getContext())) {
timeRange = String.format(Locale.getDefault(), "%02d:00 - %02d:00", from, to);
} else {
timeRange = String.format(Locale.getDefault(), "%02d:00 %s - %02d:00 %s", from % 12,
from >= 12 ? "PM" : "AM", to % 12, to >= 12 ? "PM" : "AM");
}
canvas.drawText(timeRange, size / 2, (size - paintText.descent() - paintText.ascent()) / 2, paintText);
}
protected Point radToPoint(float angle, float radius) {
return new Point((int) (getWidth() / 2 + radius * Math.sin(-angle * Math.PI / 180 + Math.PI)),
(int) (getHeight() / 2 + radius * Math.cos(-angle * Math.PI / 180 + Math.PI)));
}
@Override
public boolean onTouchEvent(MotionEvent event) {
getParent().requestDisallowInterceptTouchEvent(true);
Point center = new Point(getWidth() / 2, getHeight() / 2);
double angleRad = Math.atan2(center.y - event.getY(), center.x - event.getX());
float angle = (float) (angleRad * (180 / Math.PI));
angle += 360 + 360 - 90;
angle %= 360;
if (event.getAction() == MotionEvent.ACTION_DOWN) {
float fromDistance = Math.abs(angle - (float) from / 24 * 360);
float toDistance = Math.abs(angle - (float) to / 24 * 360);
if (fromDistance < 15 || fromDistance > (360 - 15)) {
touching = 1;
return true;
} else if (toDistance < 15 || toDistance > (360 - 15)) {
touching = 2;
return true;
}
} else if (event.getAction() == MotionEvent.ACTION_MOVE) {
int newTime = (int) (24 * (angle / 360.0));
if (from == to && touching != 0) {
// Switch which handle is focussed such that selection is the smaller arc
touching = (((newTime - to + 24) % 24) < 12) ? 2 : 1;
}
if (touching == 1) {
from = newTime;
invalidate();
return true;
} else if (touching == 2) {
to = newTime;
invalidate();
return true;
}
} else if (touching != 0) {
touching = 0;
return true;
}
return super.onTouchEvent(event);
}
}
}

View File

@ -141,6 +141,14 @@
android:layout_height="wrap_content"
android:text="@string/auto_enable_label" />
<Button
android:id="@+id/changeTimes"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/auto_enable_change_times"
android:layout_gravity="center"
style="@style/Widget.MaterialComponents.Button.TextButton" />
</LinearLayout>
</LinearLayout>

View File

@ -2,9 +2,10 @@ package de.danoeh.antennapod.core.preferences;
import android.content.Context;
import android.content.SharedPreferences;
import androidx.annotation.NonNull;
import android.util.Log;
import androidx.annotation.NonNull;
import java.util.concurrent.TimeUnit;
public class SleepTimerPreferences {
@ -17,8 +18,12 @@ public class SleepTimerPreferences {
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_VALUE = "15";
private static final String DEFAULT_LAST_TIMER = "15";
private static final int DEFAULT_AUTO_ENABLE_FROM = 22;
private static final int DEFAULT_AUTO_ENABLE_TO = 6;
private static SharedPreferences prefs;
@ -37,7 +42,7 @@ public class SleepTimerPreferences {
}
public static String lastTimerValue() {
return prefs.getString(PREF_VALUE, DEFAULT_VALUE);
return prefs.getString(PREF_VALUE, DEFAULT_LAST_TIMER);
}
public static long timerMillis() {
@ -69,4 +74,33 @@ public class SleepTimerPreferences {
return prefs.getBoolean(PREF_AUTO_ENABLE, false);
}
public static void setAutoEnableFrom(int hourOfDay) {
prefs.edit().putInt(PREF_AUTO_ENABLE_FROM, hourOfDay).apply();
}
public static int autoEnableFrom() {
return prefs.getInt(PREF_AUTO_ENABLE_FROM, DEFAULT_AUTO_ENABLE_FROM);
}
public static void setAutoEnableTo(int hourOfDay) {
prefs.edit().putInt(PREF_AUTO_ENABLE_TO, hourOfDay).apply();
}
public static int autoEnableTo() {
return prefs.getInt(PREF_AUTO_ENABLE_TO, DEFAULT_AUTO_ENABLE_TO);
}
public static boolean isInTimeRange(int from, int to, int current) {
// Range covers one day
if (from < to) {
return from <= current && current < to;
}
// Range covers two days
if (from <= current) {
return true;
}
return current < to;
}
}

View File

@ -44,37 +44,22 @@ import androidx.core.app.NotificationCompat;
import androidx.core.app.NotificationManagerCompat;
import androidx.media.MediaBrowserServiceCompat;
import de.danoeh.antennapod.core.service.QuickSettingsTileService;
import de.danoeh.antennapod.core.util.playback.PlayableUtils;
import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.event.PlayerErrorEvent;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.SortOrder;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.playback.cast.CastPsmp;
import de.danoeh.antennapod.playback.cast.CastStateListener;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collections;
import java.util.GregorianCalendar;
import java.util.List;
import java.util.concurrent.TimeUnit;
import de.danoeh.antennapod.core.R;
import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent;
import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent;
import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent;
import de.danoeh.antennapod.core.preferences.PlaybackPreferences;
import de.danoeh.antennapod.core.preferences.SleepTimerPreferences;
import de.danoeh.antennapod.core.receiver.MediaButtonReceiver;
import de.danoeh.antennapod.core.service.QuickSettingsTileService;
import de.danoeh.antennapod.core.storage.DBReader;
import de.danoeh.antennapod.core.storage.DBWriter;
import de.danoeh.antennapod.core.storage.FeedSearcher;
@ -83,14 +68,31 @@ import de.danoeh.antennapod.core.util.FeedItemUtil;
import de.danoeh.antennapod.core.util.IntentUtils;
import de.danoeh.antennapod.core.util.NetworkUtils;
import de.danoeh.antennapod.core.util.gui.NotificationUtils;
import de.danoeh.antennapod.core.util.playback.PlayableUtils;
import de.danoeh.antennapod.core.util.playback.PlaybackServiceStarter;
import de.danoeh.antennapod.core.widget.WidgetUpdater;
import de.danoeh.antennapod.event.MessageEvent;
import de.danoeh.antennapod.event.PlayerErrorEvent;
import de.danoeh.antennapod.event.playback.BufferUpdateEvent;
import de.danoeh.antennapod.event.playback.PlaybackPositionEvent;
import de.danoeh.antennapod.event.playback.PlaybackServiceEvent;
import de.danoeh.antennapod.event.playback.SleepTimerUpdatedEvent;
import de.danoeh.antennapod.event.settings.SkipIntroEndingChangedEvent;
import de.danoeh.antennapod.event.settings.SpeedPresetChangedEvent;
import de.danoeh.antennapod.event.settings.VolumeAdaptionChangedEvent;
import de.danoeh.antennapod.model.feed.Feed;
import de.danoeh.antennapod.model.feed.FeedItem;
import de.danoeh.antennapod.model.feed.FeedItemFilter;
import de.danoeh.antennapod.model.feed.FeedMedia;
import de.danoeh.antennapod.model.feed.FeedPreferences;
import de.danoeh.antennapod.model.feed.SortOrder;
import de.danoeh.antennapod.model.playback.MediaType;
import de.danoeh.antennapod.model.playback.Playable;
import de.danoeh.antennapod.playback.base.PlaybackServiceMediaPlayer;
import de.danoeh.antennapod.playback.base.PlayerStatus;
import de.danoeh.antennapod.playback.cast.CastPsmp;
import de.danoeh.antennapod.playback.cast.CastStateListener;
import de.danoeh.antennapod.storage.preferences.UserPreferences;
import de.danoeh.antennapod.ui.appstartintent.MainActivityStarter;
import de.danoeh.antennapod.ui.appstartintent.VideoPlayerActivityStarter;
import io.reactivex.Completable;
@ -797,8 +799,18 @@ public class PlaybackService extends MediaBrowserServiceCompat {
stateManager.validStartCommandWasReceived();
stateManager.startForeground(R.id.notification_playing, notificationBuilder.build());
// set sleep timer if auto-enabled
boolean autoEnableByTime = true;
int fromSetting = SleepTimerPreferences.autoEnableFrom();
int toSetting = SleepTimerPreferences.autoEnableTo();
if (fromSetting != toSetting) {
Calendar now = new GregorianCalendar();
now.setTimeInMillis(System.currentTimeMillis());
int currentHour = now.get(Calendar.HOUR_OF_DAY);
autoEnableByTime = SleepTimerPreferences.isInTimeRange(fromSetting, toSetting, currentHour);
}
if (newInfo.oldPlayerStatus != null && newInfo.oldPlayerStatus != PlayerStatus.SEEKING
&& SleepTimerPreferences.autoEnable() && !sleepTimerActive()) {
&& SleepTimerPreferences.autoEnable() && autoEnableByTime && !sleepTimerActive()) {
setSleepTimer(SleepTimerPreferences.timerMillis());
EventBus.getDefault().post(new MessageEvent(getString(R.string.sleep_timer_enabled_label),
PlaybackService.this::disableSleepTimer));

View File

@ -591,6 +591,7 @@
<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_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>
<string name="shake_to_reset_label">Shake to reset</string>
@ -610,7 +611,9 @@
<item quantity="one">1 hour</item>
<item quantity="other">%d hours</item>
</plurals>
<string name="auto_enable_label">Auto-enable</string>
<string name="auto_enable_label">Automatically activate the sleep timer when pressing play</string>
<string name="auto_enable_label_with_times">Automatically activate the sleep timer when pressing play between %s and %s</string>
<string name="auto_enable_change_times">Change times</string>
<string name="sleep_timer_enabled_label">Sleep timer enabled</string>
<!-- Synchronisation -->