From c12a43889128569eac4b34d6da7c8bd8e4a2d509 Mon Sep 17 00:00:00 2001 From: David Allison <62114487+david-allison-1@users.noreply.github.com> Date: Tue, 26 Jan 2021 07:56:29 +0000 Subject: [PATCH] Sched: Enable New Timezone Handling Code Copied from Anki at commit 131d37dca52c29033432e0149052093ab1c79461 Except one line in Collection which accepts data if server == true. https://github.com/ankitects/anki/blob/131d37dca52c29033432e0149052093ab1c79461/pylib/anki/collection.py https://github.com/ankitects/anki/blob/131d37dca52c29033432e0149052093ab1c79461/pylib/anki/schedv2.py * Bumps SYNC_VERSION to 10 if using Rust * Add local_minutes_west and sched_timing_today to Backend * Add "New timezone handling" preference * Set "localOffset" preference if using SchedV2 under Rust * Fix SchedV2:_updateCutoff to better handle timezones We will calculate this information in the Rust in V2 of the Rust Conversion We can't do this yet as we're still on V11 of the database schema Fixes 5805 --- .../main/java/com/ichi2/anki/Preferences.java | 29 +++++++ .../java/com/ichi2/libanki/Collection.java | 9 ++- .../main/java/com/ichi2/libanki/Consts.java | 6 +- .../ichi2/libanki/backend/DroidBackend.java | 25 ++++++ .../libanki/backend/DroidBackendFactory.java | 9 ++- .../libanki/backend/JavaDroidBackend.java | 14 ++++ .../libanki/backend/RustDroidBackend.java | 16 +++- .../BackendNotSupportedException.java | 26 ++++++ .../backend/model/SchedTimingToday.java | 28 +++++++ .../backend/model/SchedTimingTodayProto.java | 43 ++++++++++ .../ichi2/libanki/sched/AbstractSched.java | 14 +++- .../java/com/ichi2/libanki/sched/SchedV2.java | 79 +++++++++++++++++-- .../src/main/res/values/10-preferences.xml | 1 + .../main/res/xml/preferences_reviewing.xml | 5 ++ .../com/ichi2/libanki/sched/SchedV2Test.java | 31 ++++++++ 15 files changed, 323 insertions(+), 12 deletions(-) create mode 100644 AnkiDroid/src/main/java/com/ichi2/libanki/backend/exception/BackendNotSupportedException.java create mode 100644 AnkiDroid/src/main/java/com/ichi2/libanki/backend/model/SchedTimingToday.java create mode 100644 AnkiDroid/src/main/java/com/ichi2/libanki/backend/model/SchedTimingTodayProto.java diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/Preferences.java b/AnkiDroid/src/main/java/com/ichi2/anki/Preferences.java index 4d55723a2266..4a8d69464273 100644 --- a/AnkiDroid/src/main/java/com/ichi2/anki/Preferences.java +++ b/AnkiDroid/src/main/java/com/ichi2/anki/Preferences.java @@ -54,6 +54,8 @@ import com.ichi2.compat.CompatHelper; import com.ichi2.libanki.Collection; import com.ichi2.libanki.Utils; +import com.ichi2.libanki.backend.exception.BackendNotSupportedException; +import com.ichi2.libanki.sched.AbstractSched; import com.ichi2.preferences.NumberRangePreference; import com.ichi2.themes.Themes; import com.ichi2.ui.AppCompatPreferenceActivity; @@ -586,6 +588,14 @@ private void initPreference(android.preference.Preference pref) { Calendar calendar = col.crtGregorianCalendar(); ((SeekBarPreference)pref).setValue(calendar.get(Calendar.HOUR_OF_DAY)); break; + case "newTimezoneHandling": + android.preference.CheckBoxPreference checkBox = (android.preference.CheckBoxPreference) pref; + checkBox.setChecked(col.getSched()._new_timezone_enabled()); + if (col.schedVer() <= 1 || !col.isUsingRustBackend()) { + Timber.d("Disabled 'newTimezoneHandling' box"); + checkBox.setEnabled(false); + } + break; case "schedVer": ((android.preference.CheckBoxPreference)pref).setChecked(conf.optInt("schedVer", 1) == 2); } @@ -722,6 +732,25 @@ private void updatePreference(SharedPreferences prefs, String key, PreferenceCon pm.setComponentEnabledSetting(providerName, state, PackageManager.DONT_KILL_APP); break; } + case "newTimezoneHandling" : { + if (getCol().schedVer() != 1 && getCol().isUsingRustBackend()) { + AbstractSched sched = getCol().getSched(); + boolean was_enabled = sched._new_timezone_enabled(); + boolean is_enabled = ((android.preference.CheckBoxPreference) pref).isChecked(); + if (was_enabled != is_enabled) { + if (is_enabled) { + try { + sched.set_creation_offset(); + } catch (BackendNotSupportedException e) { + throw e.alreadyUsingRustBackend(); + } + } else { + sched.clear_creation_offset(); + } + } + } + break; + } case "schedVer": { boolean wantNew = ((android.preference.CheckBoxPreference) pref).isChecked(); boolean haveNew = getCol().schedVer() == 2; diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.java b/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.java index f7cf92631a9f..97732be5a6df 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.java +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/Collection.java @@ -23,7 +23,6 @@ import android.content.res.Resources; import android.database.Cursor; import android.database.sqlite.SQLiteDatabaseLockedException; -import android.os.Build; import android.text.TextUtils; import android.util.Pair; @@ -37,6 +36,7 @@ import com.ichi2.libanki.backend.DroidBackend; import com.ichi2.async.ProgressSender; import com.ichi2.async.TaskManager; +import com.ichi2.libanki.backend.exception.BackendNotSupportedException; import com.ichi2.libanki.exception.NoSuchDeckException; import com.ichi2.libanki.exception.UnknownDatabaseVersionException; import com.ichi2.libanki.hooks.ChessFilter; @@ -246,6 +246,13 @@ private void _loadScheduler() { mSched = new Sched(this); } else if (ver == 2) { mSched = new SchedV2(this); + if (!getServer() && isUsingRustBackend()) { + try { + getConf().put("localOffset", getSched()._current_timezone_offset()); + } catch (BackendNotSupportedException e) { + throw e.alreadyUsingRustBackend(); + } + } } } diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/Consts.java b/AnkiDroid/src/main/java/com/ichi2/libanki/Consts.java index 2936d4579672..c71f54be93c7 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/Consts.java +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/Consts.java @@ -15,6 +15,8 @@ ****************************************************************************************/ package com.ichi2.libanki; +import net.ankiweb.rsdroid.RustCleanup; + import java.lang.annotation.Retention; import androidx.annotation.IntDef; @@ -124,7 +126,9 @@ public class Consts { public static final int SYNC_ZIP_COUNT = 25; public static final String SYNC_BASE = "https://sync%s.ankiweb.net/"; public static final Integer DEFAULT_HOST_NUM = null; - public static final int SYNC_VER = 9; + /* Note: 10 if using Rust backend, 9 if using Java. Set in BackendFactory.getInstance */ + @RustCleanup("Use 10") + public static int SYNC_VER = 9; public static final String HELP_SITE = "http://ankisrs.net/docs/manual.html"; diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackend.java b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackend.java index 15f766879050..c731ad6bf833 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackend.java +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackend.java @@ -17,6 +17,8 @@ package com.ichi2.libanki.backend; import com.ichi2.libanki.DB; +import com.ichi2.libanki.backend.exception.BackendNotSupportedException; +import com.ichi2.libanki.backend.model.SchedTimingToday; import androidx.annotation.VisibleForTesting; @@ -34,4 +36,27 @@ public interface DroidBackend { @VisibleForTesting(otherwise = VisibleForTesting.NONE) void debugEnsureNoOpenPointers(); + + /** + * Obtains Timing information for the current day. + * + * @param createdSecs A UNIX timestamp of the collection creation time + * @param createdMinsWest The offset west of UTC at the time of creation (eg UTC+10 hours is -600) + * @param nowSecs timestamp of the current time + * @param nowMinsWest The current offset west of UTC + * @param rolloverHour The hour of the day the rollover happens (eg 4 for 4am) + * @return Timing information for the current day. See {@link SchedTimingToday}. + */ + SchedTimingToday sched_timing_today(long createdSecs, int createdMinsWest, long nowSecs, int nowMinsWest, int rolloverHour) throws BackendNotSupportedException; + + /** + * For the given timestamp, return minutes west of UTC in the local timezone.

+ * + * eg, Australia at +10 hours is -600.
+ * Includes the daylight savings offset if applicable. + * + * @param timestampSeconds The timestamp in seconds + * @return minutes west of UTC in the local timezone + */ + int local_minutes_west(long timestampSeconds) throws BackendNotSupportedException; } diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackendFactory.java b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackendFactory.java index adfc4b58bb03..1b8720406bc5 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackendFactory.java +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/DroidBackendFactory.java @@ -17,9 +17,11 @@ package com.ichi2.libanki.backend; import com.ichi2.anki.AnkiDroidApp; +import com.ichi2.libanki.Consts; import net.ankiweb.rsdroid.BackendFactory; import net.ankiweb.rsdroid.RustBackendFailedException; +import net.ankiweb.rsdroid.RustCleanup; import androidx.annotation.Nullable; import timber.log.Timber; @@ -36,6 +38,7 @@ private DroidBackendFactory() { * Obtains an instance of a {@link DroidBackend}. * Each call to this method will generate a separate instance which can handle a new Anki collection */ + @RustCleanup("Change back to a constant SYNC_VER") public static DroidBackend getInstance(boolean useBackend) { BackendFactory backendFactory = null; @@ -47,7 +50,11 @@ public static DroidBackend getInstance(boolean useBackend) { AnkiDroidApp.sendExceptionReport(e, "DroidBackendFactory::getInstance"); } } - return getInstance(backendFactory); + + DroidBackend instance = getInstance(backendFactory); + // Update the Sync version if we can load the Rust + Consts.SYNC_VER = backendFactory == null ? 9 : 10; + return instance; } private static DroidBackend getInstance(@Nullable BackendFactory backendFactory) { diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/JavaDroidBackend.java b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/JavaDroidBackend.java index 4fcede2d4acd..83379a90d43e 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/JavaDroidBackend.java +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/JavaDroidBackend.java @@ -17,6 +17,8 @@ package com.ichi2.libanki.backend; import com.ichi2.libanki.DB; +import com.ichi2.libanki.backend.exception.BackendNotSupportedException; +import com.ichi2.libanki.backend.model.SchedTimingToday; import net.ankiweb.rsdroid.RustCleanup; @@ -56,4 +58,16 @@ public boolean isUsingRustBackend() { public void debugEnsureNoOpenPointers() { // no-op } + + + @Override + public SchedTimingToday sched_timing_today(long createdSecs, int createdMinsWest, long nowSecs, int nowMinsWest, int rolloverHour) throws BackendNotSupportedException { + throw new BackendNotSupportedException(); + } + + + @Override + public int local_minutes_west(long timestampSeconds) throws BackendNotSupportedException { + throw new BackendNotSupportedException(); + } } diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/RustDroidBackend.java b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/RustDroidBackend.java index 131e103e7bb2..34cc312fd598 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/RustDroidBackend.java +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/RustDroidBackend.java @@ -17,12 +17,12 @@ package com.ichi2.libanki.backend; import com.ichi2.libanki.DB; +import com.ichi2.libanki.backend.model.SchedTimingToday; +import com.ichi2.libanki.backend.model.SchedTimingTodayProto; import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.BackendUtils; import BackendProto.AdBackend; -import timber.log.Timber; /** The Backend in Rust */ public class RustDroidBackend implements DroidBackend { @@ -69,4 +69,16 @@ public void debugEnsureNoOpenPointers() { throw new IllegalStateException("Contained unclosed sequence numbers: " + numbers); } } + + @Override + public SchedTimingToday sched_timing_today(long createdSecs, int createdMinsWest, long nowSecs, int nowMinsWest, int rolloverHour) { + AdBackend.SchedTimingTodayOut2 res = mBackend.getBackend().schedTimingTodayLegacy(createdSecs, createdMinsWest, nowSecs, nowMinsWest, rolloverHour); + return new SchedTimingTodayProto(res); + } + + + @Override + public int local_minutes_west(long timestampSeconds) { + return mBackend.getBackend().localMinutesWest(timestampSeconds).getVal(); + } } diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/exception/BackendNotSupportedException.java b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/exception/BackendNotSupportedException.java new file mode 100644 index 000000000000..e89da97ab26d --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/exception/BackendNotSupportedException.java @@ -0,0 +1,26 @@ +/* + * Copyright (c) 2021 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package com.ichi2.libanki.backend.exception; + +import net.ankiweb.rsdroid.RustCleanup; + +@RustCleanup("All of these should be removed when JavaBackend is removed") +public class BackendNotSupportedException extends Exception { + public RuntimeException alreadyUsingRustBackend() { + return new RuntimeException("A BackendNotSupportedException should not occur when using Rust backend", this); + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/model/SchedTimingToday.java b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/model/SchedTimingToday.java new file mode 100644 index 000000000000..4f6a9a920fde --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/model/SchedTimingToday.java @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2021 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package com.ichi2.libanki.backend.model; + +public interface SchedTimingToday { + /** The number of days that have passed since the collection was created.
+ * uint32 upstream */ + int days_elapsed(); + /** + * Timestamp of the next day rollover.
+ * int64 upstream */ + long next_day_at(); +} + diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/backend/model/SchedTimingTodayProto.java b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/model/SchedTimingTodayProto.java new file mode 100644 index 000000000000..d4bef522758a --- /dev/null +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/backend/model/SchedTimingTodayProto.java @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2021 David Allison + * + * This program is free software; you can redistribute it and/or modify it under + * the terms of the GNU General Public License as published by the Free Software + * Foundation; either version 3 of the License, or (at your option) any later + * version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT ANY + * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A + * PARTICULAR PURPOSE. See the GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ + +package com.ichi2.libanki.backend.model; + +import BackendProto.AdBackend; + +/** + * Adapter for SchedTimingTodayOut2 result from Rust + */ +public class SchedTimingTodayProto implements SchedTimingToday { + + private final AdBackend.SchedTimingTodayOut2 mData; + + public SchedTimingTodayProto(AdBackend.SchedTimingTodayOut2 data) { + mData = data; + } + + + @Override + public int days_elapsed() { + return mData.getDaysElapsed(); + } + + + @Override + public long next_day_at() { + return mData.getNextDayAt(); + } +} diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/sched/AbstractSched.java b/AnkiDroid/src/main/java/com/ichi2/libanki/sched/AbstractSched.java index 57f9fbee0e97..760447b6a6d2 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/sched/AbstractSched.java +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/sched/AbstractSched.java @@ -14,6 +14,7 @@ import com.ichi2.libanki.Deck; import com.ichi2.libanki.DeckConfig; import com.ichi2.libanki.Collection; +import com.ichi2.libanki.backend.exception.BackendNotSupportedException; import java.lang.ref.WeakReference; import java.util.List; @@ -23,8 +24,6 @@ import androidx.annotation.VisibleForTesting; import timber.log.Timber; -import androidx.annotation.NonNull; - /** * In this documentation, I will call "normal use" the fact that between two successive calls to `getCard`, either the * result of the first `getCard` is sent to `answerCard` or the scheduler is reset (through `reset` or `defer @@ -518,4 +517,15 @@ protected static void leech(@NonNull Card card, @Nullable Activity activity) { * @return The number of revlog in the collection */ public abstract int logCount(); + + public abstract int _current_timezone_offset() throws BackendNotSupportedException; + + public abstract boolean _new_timezone_enabled(); + /** + * Save the UTC west offset at the time of creation into the DB. + * Once stored, this activates the new timezone handling code. + */ + public abstract void set_creation_offset() throws BackendNotSupportedException; + + public abstract void clear_creation_offset(); } diff --git a/AnkiDroid/src/main/java/com/ichi2/libanki/sched/SchedV2.java b/AnkiDroid/src/main/java/com/ichi2/libanki/sched/SchedV2.java index 2e7c55ca26ea..ea0bd0ccc508 100644 --- a/AnkiDroid/src/main/java/com/ichi2/libanki/sched/SchedV2.java +++ b/AnkiDroid/src/main/java/com/ichi2/libanki/sched/SchedV2.java @@ -44,6 +44,8 @@ import com.ichi2.libanki.Deck; import com.ichi2.libanki.DeckConfig; +import com.ichi2.libanki.backend.exception.BackendNotSupportedException; +import com.ichi2.libanki.backend.model.SchedTimingToday; import com.ichi2.libanki.utils.Time; import com.ichi2.utils.Assert; import com.ichi2.utils.JSONArray; @@ -51,6 +53,8 @@ import com.ichi2.utils.JSONObject; import com.ichi2.utils.SyncStatus; +import net.ankiweb.rsdroid.RustCleanup; + import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.Calendar; @@ -2173,12 +2177,23 @@ private int _previewDelay(@NonNull Card card) { */ /* Overriden: other way to count time*/ + @RustCleanup("remove timing == null check once JavaBackend is removed") public void _updateCutoff() { int oldToday = mToday == null ? 0 : mToday; - // days since col created - mToday = _daysSinceCreation(); - // end of day cutoff - mDayCutoff = _dayCutoff(); + + SchedTimingToday timing = _timingToday(); + + if (timing == null) { + mToday = _daysSinceCreation(); + mDayCutoff = _dayCutoff(); + } else if (_new_timezone_enabled()) { + mToday = timing.days_elapsed(); + mDayCutoff = timing.next_day_at(); + } else { + mToday = _daysSinceCreation(); + mDayCutoff = _dayCutoff(); + } + if (oldToday != mToday) { mCol.log(mToday, mDayCutoff); } @@ -2217,7 +2232,7 @@ private long _dayCutoff() { private int _daysSinceCreation() { Calendar c = mCol.crtCalendar(); - c.set(Calendar.HOUR, mCol.getConf().optInt("rollover", 4)); + c.set(Calendar.HOUR, _rolloverHour()); c.set(Calendar.MINUTE, 0); c.set(Calendar.SECOND, 0); c.set(Calendar.MILLISECOND, 0); @@ -2225,6 +2240,60 @@ private int _daysSinceCreation() { return (int) (((getTime().intTimeMS() - c.getTimeInMillis()) / 1000) / SECONDS_PER_DAY); } + private int _rolloverHour() { + return getCol().getConf().optInt("rollover", 4); + } + + // New timezone handling + ////////////////////////////////////////////////////////////////////////// + + @Override + public boolean _new_timezone_enabled() { + JSONObject conf = getCol().getConf(); + return conf.has("creationOffset") && !conf.isNull("creationOffset"); + } + + @Nullable + private SchedTimingToday _timingToday() { + try { + return getCol().getBackend().sched_timing_today( + getCol().getCrt(), + _creation_timezone_offset(), + getTime().intTime(), + _current_timezone_offset(), + _rolloverHour()); + } catch (BackendNotSupportedException e) { + return null; + } + } + + @Override + public int _current_timezone_offset() throws BackendNotSupportedException { + if (getCol().getServer()) { + return getCol().getConf().optInt("localOffset", 0); + } else { + return getCol().getBackend().local_minutes_west(getTime().intTime()); + } + } + + private int _creation_timezone_offset() { + return getCol().getConf().optInt("creationOffset", 0); + } + + @Override + public void set_creation_offset() throws BackendNotSupportedException { + int mins_west = getCol().getBackend().local_minutes_west(getCol().getCrt()); + getCol().getConf().put("creationOffset", mins_west); + getCol().setMod(); + } + + @Override + public void clear_creation_offset() { + if (getCol().getConf().has("creationOffset")) { + getCol().getConf().remove("creationOffset"); + getCol().setMod(); + } + } protected void update(@NonNull Deck g) { for (String t : new String[] { "new", "rev", "lrn", "time" }) { diff --git a/AnkiDroid/src/main/res/values/10-preferences.xml b/AnkiDroid/src/main/res/values/10-preferences.xml index 4c2f5c8f5b14..54c27a9656a6 100644 --- a/AnkiDroid/src/main/res/values/10-preferences.xml +++ b/AnkiDroid/src/main/res/values/10-preferences.xml @@ -154,6 +154,7 @@ Learn ahead limit Timebox time limit XXX min + New timezone handling Start of next day XXX hours past midnight Experimental V2 scheduler diff --git a/AnkiDroid/src/main/res/xml/preferences_reviewing.xml b/AnkiDroid/src/main/res/xml/preferences_reviewing.xml index a1273b28f8b5..45e9ba4bd455 100644 --- a/AnkiDroid/src/main/res/xml/preferences_reviewing.xml +++ b/AnkiDroid/src/main/res/xml/preferences_reviewing.xml @@ -50,6 +50,11 @@ android:title="@string/time_limit" app:max="9999" app:min="0" /> + +