From 0bdf9d9e28572bba38fd36ad518e817dbdd04fe5 Mon Sep 17 00:00:00 2001 From: mueller-ma Date: Sat, 15 Apr 2023 21:08:03 +0200 Subject: Add option to enable sleep timer based on current time (#6384) --- .../playback/SleepTimerPreferencesTest.java | 22 +++ .../danoeh/antennapod/dialog/SleepTimerDialog.java | 66 +++++++- .../danoeh/antennapod/dialog/TimeRangeDialog.java | 187 +++++++++++++++++++++ app/src/main/res/layout/time_dialog.xml | 8 + .../core/preferences/SleepTimerPreferences.java | 40 ++++- .../core/service/playback/PlaybackService.java | 50 +++--- ui/i18n/src/main/res/values/strings.xml | 5 +- 7 files changed, 346 insertions(+), 32 deletions(-) create mode 100644 app/src/androidTest/java/de/test/antennapod/service/playback/SleepTimerPreferencesTest.java create mode 100644 app/src/main/java/de/danoeh/antennapod/dialog/TimeRangeDialog.java diff --git a/app/src/androidTest/java/de/test/antennapod/service/playback/SleepTimerPreferencesTest.java b/app/src/androidTest/java/de/test/antennapod/service/playback/SleepTimerPreferencesTest.java new file mode 100644 index 000000000..4339d6cd7 --- /dev/null +++ b/app/src/androidTest/java/de/test/antennapod/service/playback/SleepTimerPreferencesTest.java @@ -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)); + } +} diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java index 52e6f7807..ecbc1d873 100644 --- a/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java +++ b/app/src/main/java/de/danoeh/antennapod/dialog/SleepTimerDialog.java @@ -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) { diff --git a/app/src/main/java/de/danoeh/antennapod/dialog/TimeRangeDialog.java b/app/src/main/java/de/danoeh/antennapod/dialog/TimeRangeDialog.java new file mode 100644 index 000000000..85913043e --- /dev/null +++ b/app/src/main/java/de/danoeh/antennapod/dialog/TimeRangeDialog.java @@ -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); + } + } +} diff --git a/app/src/main/res/layout/time_dialog.xml b/app/src/main/res/layout/time_dialog.xml index 50001bf9c..1a8f02c7c 100644 --- a/app/src/main/res/layout/time_dialog.xml +++ b/app/src/main/res/layout/time_dialog.xml @@ -141,6 +141,14 @@ android:layout_height="wrap_content" android:text="@string/auto_enable_label" /> +