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" />
+
+