From a8649ede460c242cf7ca0d6626535246834abf0f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 16:05:50 +1000 Subject: [PATCH 01/61] Migrate most rsdroid code into rslib patch - Reuses existing protobuf infrastructure to greatly cut down on the JNI boilerplate - Ensures all calls have errors handled, and happen behind a mutex - Removes separate downgrade + open11 calls in favour of a flag to the standard collection opening routine - Various code cleanups --- build.gradle | 6 +- docs/source/sequence_open_collection.plantuml | 2 +- .../ankiweb/rsdroid/BackendForTesting.java | 19 +- .../rsdroid/BackendIntegrationTests.java | 2 +- rsdroid/build.gradle | 1 + rsdroid/proto.gradle | 3 +- .../ankiweb/rsdroid/AnkiDroidBackendImpl.java | 50 - .../net/ankiweb/rsdroid/BackendException.java | 91 +- .../net/ankiweb/rsdroid/BackendFactory.java | 52 - .../net/ankiweb/rsdroid/BackendFactory.kt | 58 + .../net/ankiweb/rsdroid/BackendMutex.java | 2084 ++++++++--------- .../net/ankiweb/rsdroid/BackendUtils.java | 5 +- .../java/net/ankiweb/rsdroid/BackendV1.java | 21 - .../java/net/ankiweb/rsdroid/BackendV1.kt | 9 + .../net/ankiweb/rsdroid/BackendV1Impl.java | 395 ---- .../java/net/ankiweb/rsdroid/BackendV1Impl.kt | 283 +++ .../net/ankiweb/rsdroid/NativeMethods.java | 119 - .../java/net/ankiweb/rsdroid/NativeMethods.kt | 44 + .../java/net/ankiweb/rsdroid/Pointer.java | 15 - .../RustVNextSupportSQLiteOpenHelper.java | 2 +- .../ankiweb/rsdroid/database/SQLHandler.java | 6 +- .../net/ankiweb/rsdroid/database/Session.java | 6 +- .../StreamingProtobufSQLiteCursor.java | 24 +- .../BackendDeckIsFilteredException.java | 4 +- .../exceptions/BackendExistingException.java | 4 +- .../BackendInterruptedException.java | 4 +- .../BackendInvalidInputException.java | 12 +- .../exceptions/BackendIoException.java | 4 +- .../exceptions/BackendJsonException.java | 4 +- .../exceptions/BackendNetworkException.java | 44 +- .../exceptions/BackendNotFoundException.java | 4 +- .../exceptions/BackendProtoException.java | 4 +- .../exceptions/BackendSyncException.java | 88 +- .../exceptions/BackendTemplateException.java | 8 +- rslib-bridge/Cargo.lock | 1839 ++++++++++----- rslib-bridge/Cargo.toml | 6 +- rslib-bridge/anki | 2 +- rslib-bridge/build.rs | 109 - rslib-bridge/proto/AdBackend.proto | 70 - rslib-bridge/src/ankidroid.rs | 11 - rslib-bridge/src/dbcommand.rs | 350 --- rslib-bridge/src/lib.rs | 606 +---- rslib-bridge/src/sqlite.rs | 215 -- tools/protoc-gen/protoc-gen.py | 429 ++-- tools/protoc-gen/test/.gitignore | 1 - tools/protoc-gen/test/backend.proto | 1021 -------- tools/protoc-gen/test/fluent.proto | 277 --- tools/protoc-gen/test/test_anki.bat | 2 - 48 files changed, 3111 insertions(+), 5304 deletions(-) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/AnkiDroidBackendImpl.java delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/Pointer.java delete mode 100644 rslib-bridge/build.rs delete mode 100644 rslib-bridge/proto/AdBackend.proto delete mode 100644 rslib-bridge/src/ankidroid.rs delete mode 100644 rslib-bridge/src/dbcommand.rs delete mode 100644 rslib-bridge/src/sqlite.rs delete mode 100644 tools/protoc-gen/test/.gitignore delete mode 100644 tools/protoc-gen/test/backend.proto delete mode 100644 tools/protoc-gen/test/fluent.proto delete mode 100644 tools/protoc-gen/test/test_anki.bat diff --git a/build.gradle b/build.gradle index 2e758f923..b625f655e 100644 --- a/build.gradle +++ b/build.gradle @@ -10,6 +10,8 @@ buildscript { appcompatVersion = "1.3.1" androidxTestJunitVersion = "1.1.3" sqliteVersion = "2.2.0" + kotlin_version = '1.6.21' + version_dokka = "1.6.20" } repositories { @@ -21,7 +23,9 @@ buildscript { } dependencies { classpath "com.android.tools.build:gradle:4.2.2" - + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + classpath "org.jetbrains.kotlin:kotlin-android-extensions:$kotlin_version" + classpath "org.jetbrains.dokka:dokka-gradle-plugin:${version_dokka}" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files classpath 'com.google.protobuf:protobuf-gradle-plugin:0.8.18' diff --git a/docs/source/sequence_open_collection.plantuml b/docs/source/sequence_open_collection.plantuml index 68961940e..a35f8ed11 100644 --- a/docs/source/sequence_open_collection.plantuml +++ b/docs/source/sequence_open_collection.plantuml @@ -37,7 +37,7 @@ BackendUtils -> RustBackend : openAnkiDroidCollection(OpenCollectionIn) rslibbridge -> NativeMethods: byte[] NativeMethods -> RustBackend: byte[] RustBackend -> RustBackend: Check for error - RustBackend -> RustBackend: Response is Backend.Empty.\nReturn void + RustBackend -> RustBackend: Response is Generic.Empty.\nReturn void end RustBackend -> user diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java index 88c52184a..2bdd29c7f 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java @@ -20,7 +20,7 @@ import com.google.protobuf.InvalidProtocolBufferException; -import BackendProto.Backend; +import anki.backend; public class BackendForTesting extends BackendV1Impl { @@ -46,21 +46,8 @@ public static BackendForTesting create() { */ @VisibleForTesting public void debugProduceError(ErrorType error) { - byte[] result = null; - try { - Pointer backendPointer = ensureBackend(); - result = NativeMethods.debugProduceError(backendPointer.toJni(), error.toString()); - - // This should fail validate - Backend.Empty message = Backend.Empty.parseFrom(result); - validateMessage(result, message); - - throw new IllegalStateException("An exception should have been thrown"); - - } catch (InvalidProtocolBufferException ex) { - validateResult(result); - throw BackendException.fromException(ex); - } + super.debugProduceError(error.toString()); + throw new IllegalStateException("An exception should have been thrown"); } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java index 41c534e78..a4eba91d5 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java @@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit; -import BackendProto.Backend.SchedTimingTodayOut; +import anki.backend.SchedTimingTodayOut; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; diff --git a/rsdroid/build.gradle b/rsdroid/build.gradle index bc3a401ce..305cd8e94 100644 --- a/rsdroid/build.gradle +++ b/rsdroid/build.gradle @@ -1,5 +1,6 @@ apply plugin: 'com.android.library' // required for aar generation to link to from AnkiDroid apply plugin: 'com.google.protobuf' +apply plugin: "kotlin-android" apply plugin: 'signing' apply plugin: 'com.vanniktech.maven.publish' diff --git a/rsdroid/proto.gradle b/rsdroid/proto.gradle index 8ba0f056e..ef1b4ae94 100644 --- a/rsdroid/proto.gradle +++ b/rsdroid/proto.gradle @@ -1,7 +1,6 @@ import org.gradle.internal.os.OperatingSystem def protobufFolder = new File(rootDir, "rslib-bridge/anki/proto").getAbsolutePath() -def droidProtobufFolder = new File(rootDir, "rslib-bridge/proto").getAbsolutePath() android { if (!new File(protobufFolder).exists()) { @@ -13,7 +12,6 @@ android { main { proto { srcDir protobufFolder - srcDir droidProtobufFolder } } } @@ -35,6 +33,7 @@ protobuf { } generateProtoTasks { all().each { task -> + task.outputs.upToDateWhen { false } task.builtins { java { // Options ref: diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/AnkiDroidBackendImpl.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/AnkiDroidBackendImpl.java deleted file mode 100644 index 8dc54daac..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/AnkiDroidBackendImpl.java +++ /dev/null @@ -1,50 +0,0 @@ -/* - * 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 net.ankiweb.rsdroid; - -public class AnkiDroidBackendImpl extends net.ankiweb.rsdroid.AdbackendImpl { - - private final PointerGen mPointerGen; - - public AnkiDroidBackendImpl(PointerGen pointerGen) { - this.mPointerGen = pointerGen; - } - - @Override - public Pointer ensureBackend() { - return mPointerGen.generatePointer(); - } - - @Override - protected byte[] executeCommand(long backendPointer, int command, byte[] args) { - return NativeMethods.executeAnkiDroidCommand(backendPointer, command, args); - } - - public void downgradeBackend(String collectionPath) { - String ret = NativeMethods.downgradeDatabase(collectionPath); - - if (ret != null && ret.length() != 0) { - throw new BackendException(ret); - } - - // otherwise, return - } - - public interface PointerGen { - Pointer generatePointer(); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.java index 9b92b4d1f..8d4a06e96 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.java @@ -23,6 +23,7 @@ import androidx.annotation.Nullable; +import net.ankiweb.rsdroid.database.NotImplementedException; import net.ankiweb.rsdroid.exceptions.BackendDeckIsFilteredException; import net.ankiweb.rsdroid.exceptions.BackendExistingException; import net.ankiweb.rsdroid.exceptions.BackendInterruptedException; @@ -39,14 +40,14 @@ import java.util.regex.Matcher; import java.util.regex.Pattern; -import BackendProto.Backend; +import anki.backend.BackendError.Kind; public class BackendException extends RuntimeException { @SuppressWarnings({"unused", "RedundantSuppression"}) @Nullable - private final Backend.BackendError error; + private final anki.backend.BackendError error; - public BackendException(Backend.BackendError error) { + public BackendException(anki.backend.BackendError error) { super(error.getLocalized()); this.error = error; } @@ -56,40 +57,44 @@ public BackendException(String message) { error = null; } - public static BackendException fromError(Backend.BackendError error) { - switch (error.getValueCase()) { - case DB_ERROR: - return BackendDbException.fromDbError(error); - case JSON_ERROR: - return new BackendJsonException(error.getJsonError()); - case SYNC_ERROR: - return BackendSyncException.fromSyncError(error); - case FATAL_ERROR: - // This should have produced a hasFatalError property - throw new BackendFatalError(error.getFatalError()); - case EXISTS: - return new BackendExistingException(error); - case DECK_IS_FILTERED: - return new BackendDeckIsFilteredException(error); - case INTERRUPTED: - return new BackendInterruptedException(error); - case PROTO_ERROR: - return new BackendProtoException(error); - case NOT_FOUND_ERROR: - return new BackendNotFoundException(error); - case INVALID_INPUT: - return BackendInvalidInputException.fromInvalidInputError(error); - case NETWORK_ERROR: - return BackendNetworkException.fromNetworkError(error); - case TEMPLATE_PARSE: - return BackendTemplateException.fromTemplateError(error); - case IO_ERROR: - return new BackendIoException(error); - case VALUE_NOT_SET: - } - - - return new BackendException(error); + public static BackendException fromError(anki.backend.BackendError error) { + throw new NotImplementedException(); +// switch (error.getKindValue()) { +// case Kind.DB_ERROR.: +// return BackendDbException.fromDbError(error); +//// case Kind.JSON_ERROR: +//// return new BackendJsonException(error.getJsonError()); +// case Kind.SYNC_AUTH_ERROR: +// return new BackendSyncException.BackendSyncAuthFailedException(error); +// case Kind.SYNC_OTHER_ERROR: +// return new BackendSyncException.BackendSyncException(error); +// return BackendSyncException.fromSyncError(error); +// case Kind.FATAL_ERROR: +// // This should have produced a hasFatalError property +// throw new BackendFatalError(error.getFatalError()); +// case Kind.EXISTS: +// return new BackendExistingException(error); +//// case Kind.DECK_IS_FILTERED: +//// return new BackendDeckIsFilteredException(error); +// case Kind.INTERRUPTED: +// return new BackendInterruptedException(error); +// case Kind.PROTO_ERROR: +// return new BackendProtoException(error); +// case Kind.NOT_FOUND_ERROR: +// return new BackendNotFoundException(error); +// case Kind.INVALID_INPUT: +// return BackendInvalidInputException.fromInvalidInputError(error); +// case Kind.NETWORK_ERROR: +// return BackendNetworkException.fromNetworkError(error); +// case Kind.TEMPLATE_PARSE: +// return BackendTemplateException.fromTemplateError(error); +// case Kind.IO_ERROR: +// return new BackendIoException(error); +// case Kind.VALUE_NOT_SET: +// } + + +// return new BackendException(error); } public static RuntimeException fromException(Exception ex) { @@ -104,14 +109,14 @@ public RuntimeException toSQLiteException(String query) { public static class BackendDbException extends BackendException { - public BackendDbException(Backend.BackendError error) { + public BackendDbException(anki.backend.BackendError error) { // This is very simple for now and matches Anki Desktop (error is currently text) // Later on, we may want to use structured error messages // DBError { info: "SqliteFailure(Error { code: Unknown, extended_code: 1 }, Some(\"no such table: aa\"))", kind: Other } super(error); } - public static BackendException fromDbError(Backend.BackendError error) { + public static BackendException fromDbError(anki.backend.BackendError error) { String localised = error.getLocalized(); @@ -170,25 +175,25 @@ public RuntimeException toSQLiteException(String query) { } public static class BackendDbFileTooNewException extends BackendException { - public BackendDbFileTooNewException(Backend.BackendError error) { + public BackendDbFileTooNewException(anki.backend.BackendError error) { super(error); } } public static class BackendDbFileTooOldException extends BackendException { - public BackendDbFileTooOldException(Backend.BackendError error) { + public BackendDbFileTooOldException(anki.backend.BackendError error) { super(error); } } public static class BackendDbLockedException extends BackendException { - public BackendDbLockedException(Backend.BackendError error) { + public BackendDbLockedException(anki.backend.BackendError error) { super(error); } } public static class BackendDbMissingEntityException extends BackendException { - public BackendDbMissingEntityException(Backend.BackendError error) { + public BackendDbMissingEntityException(anki.backend.BackendError error) { super(error); } } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.java deleted file mode 100644 index 109807759..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid; - -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -public abstract class BackendFactory { - - private BackendV1 backend; - - // Force users to go through getInstance - for now we need to handle the backend failure - protected BackendFactory() { - - } - - @RustCleanup("Use BackendV[11/Next]Factory") - public static BackendFactory createInstance() throws RustBackendFailedException { - return BackendV11Factory.createInstance(); - } - - public synchronized BackendV1 getBackend() { - if (backend == null) { - backend = new BackendMutex(new BackendV1Impl()); - } - return backend; - } - - public synchronized void closeCollection() { - if (backend == null) { - return; - } - - // we could swallow the exception here, most of the time it will be "collection is already closed" - backend.closeCollection(false); - } - - public abstract SupportSQLiteOpenHelper.Factory getSQLiteOpener(); -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt new file mode 100644 index 000000000..414b398fd --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt @@ -0,0 +1,58 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid + +import net.ankiweb.rsdroid.BackendV1 +import net.ankiweb.rsdroid.BackendV1Impl +import androidx.sqlite.db.SupportSQLiteOpenHelper +import net.ankiweb.rsdroid.RustCleanup +import kotlin.Throws +import net.ankiweb.rsdroid.RustBackendFailedException +import net.ankiweb.rsdroid.BackendFactory +import net.ankiweb.rsdroid.BackendV11Factory + +abstract class BackendFactory // Force users to go through getInstance - for now we need to handle the backend failure +protected constructor() { + private var backend: BackendV1? = null + @Synchronized + fun getBackend(): BackendV1 { + if (backend == null) { + backend = BackendV1Impl() // new BackendMutex(new BackendV1Impl()); + } + return backend!! + } + + @Synchronized + fun closeCollection() { + if (backend == null) { + return + } + + // we could swallow the exception here, most of the time it will be "collection is already closed" +// backend.closeCollection(false); + } + + abstract val sQLiteOpener: SupportSQLiteOpenHelper.Factory? + + companion object { + @JvmStatic + @RustCleanup("Use BackendV[11/Next]Factory") + @Throws(RustBackendFailedException::class) + open fun createInstance(): BackendFactory { + return BackendV11Factory.createInstance() + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendMutex.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendMutex.java index 879c072dd..a0adf5484 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendMutex.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendMutex.java @@ -1,1042 +1,1042 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid; - -import androidx.annotation.Nullable; - -import com.google.protobuf.ByteString; - -import org.json.JSONArray; - -import java.io.IOException; -import java.util.List; -import java.util.concurrent.locks.ReentrantLock; - -import BackendProto.AdBackend; -import BackendProto.Backend; -import BackendProto.Sqlite; - -/** - * Ensures that a single thread accesses RustBackend at the same time. - * This is because rslib-bridge currently has no distinction between threads, and handles the state of - * transactions. - */ -public class BackendMutex implements BackendV1 { - // This class exists as the Rust backend uses a single connection for SQLite, rather than a connection pool - // This means that SQL can occur cross-threads. - // There are a few problems with this: - // * When inside a transaction, another thread can add commands, or close the transaction - // * Commands can either be sent from the Java, or from the Rust. - // * We have no knowledge about whether a Rust command will start a transaction - - // We handle this using a mutex and some invariants: - // * If a transaction is held by a thread, have the thread keep the mutex until the transaction is closed - // * Only one Rust command can run at a time - already true as with_col in rust uses a mutex, but we'll lock on the Java side - - private final ReentrantLock lock = new ReentrantLock(); - private final BackendV1 backend; - - public BackendMutex(BackendV1 backend) { - this.backend = backend; - } - - @Override - public void beginTransaction() { - lock.lock(); - backend.beginTransaction(); - } - - @Override - public void commitTransaction() { - try { - backend.commitTransaction(); - } finally { - lock.unlock(); - } - } - - @Override - public void rollbackTransaction() { - try { - backend.rollbackTransaction(); - } finally { - lock.unlock(); - } - } - - @Override - public JSONArray fullQuery(String query, Object... bindArgs) { - try { - lock.lock(); - return backend.fullQuery(query, bindArgs); - } finally { - lock.unlock(); - } - } - - @Override - public int executeGetRowsAffected(String sql, Object... bindArgs) { - try { - lock.lock(); - return backend.executeGetRowsAffected(sql, bindArgs); - } finally { - lock.unlock(); - } - } - - @Override - public long insertForId(String sql, Object... bindArgs) { - try { - lock.lock(); - return backend.insertForId(sql, bindArgs); - } finally { - lock.unlock(); - } - } - - @Override - public String[] getColumnNames(String sql) { - try { - lock.lock(); - return backend.getColumnNames(sql); - } finally { - lock.unlock(); - } - } - - @Override - public void closeDatabase() { - try { - lock.lock(); - backend.closeDatabase(); - } finally { - lock.unlock(); - } - } - - @Override - public String getPath() { - try { - lock.lock(); - return backend.getPath(); - } finally { - lock.unlock(); - } - } - - @Override - public Sqlite.DBResponse getNextSlice(long startIndex, int sequenceNumber) { - try { - lock.lock(); - return backend.getNextSlice(startIndex, sequenceNumber); - } finally { - lock.unlock(); - } - } - - @Override - public Sqlite.DBResponse fullQueryProto(String query, Object... bindArgs) { - try { - lock.lock(); - return backend.fullQueryProto(query, bindArgs); - } finally { - lock.unlock(); - } - } - - @Override - public void cancelCurrentProtoQuery(int sequenceNumber) { - try { - lock.lock(); - backend.cancelCurrentProtoQuery(sequenceNumber); - } finally { - lock.unlock(); - } - } - - @Override - public void cancelAllProtoQueries() { - try { - lock.lock(); - backend.cancelAllProtoQueries(); - } finally { - lock.unlock(); - } - } - - @Override - public void setPageSize(long pageSizeBytes) { - try { - lock.lock(); - backend.setPageSize(pageSizeBytes); - } finally { - lock.unlock(); - } - } - - // RustBackend Implementation - - @Override - public Backend.Progress latestProgress() { - try { - lock.lock(); - return backend.latestProgress(); - } finally { - lock.unlock(); - } - } - - @Override - public void setWantsAbort() { - try { - lock.lock(); - backend.setWantsAbort(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.ExtractAVTagsOut extractAVTags(@Nullable String text, boolean questionSide) { - try { - lock.lock(); - return backend.extractAVTags(text, questionSide); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.ExtractLatexOut extractLatex(@Nullable String text, boolean svg, boolean expandClozes) { - try { - lock.lock(); - return backend.extractLatex(text, svg, expandClozes); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.EmptyCardsReport getEmptyCards() { - try { - lock.lock(); - return backend.getEmptyCards(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.RenderCardOut renderExistingCard(long cardId, boolean browser) { - try { - lock.lock(); - return backend.renderExistingCard(cardId, browser); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.RenderCardOut renderUncommittedCard(@Nullable Backend.Note note, int cardOrd, @Nullable ByteString template, boolean fillEmpty) { - try { - lock.lock(); - return backend.renderUncommittedCard(note, cardOrd, template, fillEmpty); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String stripAVTags(String args) { - try { - lock.lock(); - return backend.stripAVTags(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.SearchCardsOut searchCards(@Nullable String search, @Nullable Backend.SortOrder order) { - try { - lock.lock(); - return backend.searchCards(search, order); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.SearchNotesOut searchNotes(@Nullable String search) { - try { - lock.lock(); - return backend.searchNotes(search); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.UInt32 findAndReplace(List nids, @Nullable String search, @Nullable String replacement, boolean regex, boolean matchCase, @Nullable String fieldName) { - try { - lock.lock(); - return backend.findAndReplace(nids, search, replacement, regex, matchCase, fieldName); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Int32 localMinutesWest(long args) { - try { - lock.lock(); - return backend.localMinutesWest(args); - } finally { - lock.unlock(); - } - } - - @Override - public void setLocalMinutesWest(int args) { - try { - lock.lock(); - backend.setLocalMinutesWest(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.SchedTimingTodayOut schedTimingToday() { - try { - lock.lock(); - return backend.schedTimingToday(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String studiedToday(int cards, double seconds) { - try { - lock.lock(); - return backend.studiedToday(cards, seconds); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String congratsLearnMessage(float nextDue, int remaining) { - try { - lock.lock(); - return backend.congratsLearnMessage(nextDue, remaining); - } finally { - lock.unlock(); - } - } - - @Override - public void updateStats(long deckId, int newDelta, int reviewDelta, int millisecondDelta) { - try { - lock.lock(); - backend.updateStats(deckId, newDelta, reviewDelta, millisecondDelta); - } finally { - lock.unlock(); - } - } - - @Override - public void extendLimits(long deckId, int newDelta, int reviewDelta) { - try { - lock.lock(); - backend.extendLimits(deckId, newDelta, reviewDelta); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.CountsForDeckTodayOut countsForDeckToday(long did) { - try { - lock.lock(); - return backend.countsForDeckToday(did); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String cardStats(long cid) { - try { - lock.lock(); - return backend.cardStats(cid); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.GraphsOut graphs(@Nullable String search, int days) { - try { - lock.lock(); - return backend.graphs(search, days); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.CheckMediaOut checkMedia() { - try { - lock.lock(); - return backend.checkMedia(); - } finally { - lock.unlock(); - } - } - - @Override - public void trashMediaFiles(List fnames) { - try { - lock.lock(); - backend.trashMediaFiles(fnames); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String addMediaFile(@Nullable String desiredName, @Nullable ByteString data) { - try { - lock.lock(); - return backend.addMediaFile(desiredName, data); - } finally { - lock.unlock(); - } - } - - @Override - public void emptyTrash() { - try { - lock.lock(); - backend.emptyTrash(); - } finally { - lock.unlock(); - } - } - - @Override - public void restoreTrash() { - try { - lock.lock(); - backend.restoreTrash(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.DeckID addOrUpdateDeckLegacy(@Nullable ByteString deck, boolean preserveUsnAndMtime) { - try { - lock.lock(); - return backend.addOrUpdateDeckLegacy(deck, preserveUsnAndMtime); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.DeckTreeNode deckTree(long now, long topDeckId) { - try { - lock.lock(); - return backend.deckTree(now, topDeckId); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json deckTreeLegacy() { - try { - lock.lock(); - return backend.deckTreeLegacy(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getAllDecksLegacy() { - try { - lock.lock(); - return backend.getAllDecksLegacy(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.DeckID getDeckIDByName(String name) { - try { - lock.lock(); - return backend.getDeckIDByName(name); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getDeckLegacy(long did) { - try { - lock.lock(); - return backend.getDeckLegacy(did); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.DeckNames getDeckNames(boolean skipEmptyDefault, boolean includeFiltered) { - try { - lock.lock(); - return backend.getDeckNames(skipEmptyDefault, includeFiltered); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json newDeckLegacy(boolean args) { - try { - lock.lock(); - return backend.newDeckLegacy(args); - } finally { - lock.unlock(); - } - } - - @Override - public void removeDeck(long args) { - try { - lock.lock(); - backend.removeDeck(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.DeckConfigID addOrUpdateDeckConfigLegacy(@Nullable ByteString config, boolean preserveUsnAndMtime) { - try { - lock.lock(); - return backend.addOrUpdateDeckConfigLegacy(config, preserveUsnAndMtime); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json allDeckConfigLegacy() { - try { - lock.lock(); - return backend.allDeckConfigLegacy(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getDeckConfigLegacy(long dConfId) { - try { - lock.lock(); - return backend.getDeckConfigLegacy(dConfId); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json newDeckConfigLegacy() { - try { - lock.lock(); - return backend.newDeckConfigLegacy(); - } finally { - lock.unlock(); - } - } - - @Override - public void removeDeckConfig(long dConfId) { - try { - lock.lock(); - backend.removeDeckConfig(dConfId); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Card getCard(long cid) { - try { - lock.lock(); - return backend.getCard(cid); - } finally { - lock.unlock(); - } - } - - @Override - public void updateCard(Backend.Card args) { - try { - lock.lock(); - backend.updateCard(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.CardID addCard(Backend.Card args) { - try { - lock.lock(); - return backend.addCard(args); - } finally { - lock.unlock(); - } - } - - @Override - public void removeCards(List cardIds) { - try { - lock.lock(); - backend.removeCards(cardIds); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Note newNote(long noteTypidId) { - try { - lock.lock(); - return backend.newNote(noteTypidId); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteID addNote(@Nullable Backend.Note note, long deckId) { - try { - lock.lock(); - return backend.addNote(note, deckId); - } finally { - lock.unlock(); - } - } - - @Override - public void updateNote(Backend.Note args) { - try { - lock.lock(); - backend.updateNote(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Note getNote(long nid) { - try { - lock.lock(); - return backend.getNote(nid); - } finally { - lock.unlock(); - } - } - - @Override - public void removeNotes(List noteIds, List cardIds) { - try { - lock.lock(); - backend.removeNotes(noteIds, cardIds); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.UInt32 addNoteTags(List nids, @Nullable String tags) { - try { - lock.lock(); - return backend.addNoteTags(nids, tags); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.UInt32 updateNoteTags(List nids, @Nullable String tags, @Nullable String replacement, boolean regex) { - try { - lock.lock(); - return backend.updateNoteTags(nids, tags, replacement, regex); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.ClozeNumbersInNoteOut clozeNumbersInNote(Backend.Note args) { - try { - lock.lock(); - return backend.clozeNumbersInNote(args); - } finally { - lock.unlock(); - } - } - - @Override - public void afterNoteUpdates(List nids, boolean markNotesModified, boolean generateCards) { - try { - lock.lock(); - backend.afterNoteUpdates(nids, markNotesModified, generateCards); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.FieldNamesForNotesOut fieldNamesForNotes(List nids) { - try { - lock.lock(); - return backend.fieldNamesForNotes(nids); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteIsDuplicateOrEmptyOut noteIsDuplicateOrEmpty(Backend.Note args) { - try { - lock.lock(); - return backend.noteIsDuplicateOrEmpty(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteTypeID addOrUpdateNotetype(@Nullable ByteString json, boolean preserveUsnAndMtime) { - try { - lock.lock(); - return backend.addOrUpdateNotetype(json, preserveUsnAndMtime); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getStockNotetypeLegacy(@Nullable Backend.StockNoteType kind) { - try { - lock.lock(); - return backend.getStockNotetypeLegacy(kind); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getNotetypeLegacy(long noteTypeId) { - try { - lock.lock(); - return backend.getNotetypeLegacy(noteTypeId); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteTypeNames getNotetypeNames() { - try { - lock.lock(); - return backend.getNotetypeNames(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteTypeUseCounts getNotetypeNamesAndCounts() { - try { - lock.lock(); - return backend.getNotetypeNamesAndCounts(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.NoteTypeID getNotetypeIDByName(String name) { - try { - lock.lock(); - return backend.getNotetypeIDByName(name); - } finally { - lock.unlock(); - } - } - - @Override - public void removeNotetype(long noteTypeId) { - try { - lock.lock(); - backend.removeNotetype(noteTypeId); - } finally { - lock.unlock(); - } - } - - @Override - public void openCollection(@Nullable String collectionPath, @Nullable String mediaFolderPath, @Nullable String mediaDbPath, @Nullable String logPath) { - try { - lock.lock(); - backend.openCollection(collectionPath, mediaFolderPath, mediaDbPath, logPath); - } finally { - lock.unlock(); - } - } - - @Override - public void closeCollection(boolean downgradeToSchema11) { - try { - lock.lock(); - backend.closeCollection(downgradeToSchema11); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.CheckDatabaseOut checkDatabase() { - try { - lock.lock(); - return backend.checkDatabase(); - } finally { - lock.unlock(); - } - } - - @Override - public void beforeUpload() { - try { - lock.lock(); - backend.beforeUpload(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String translateString(Backend.TranslateStringIn args) { - try { - lock.lock(); - return backend.translateString(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.String formatTimespan(float seconds, @Nullable Backend.FormatTimespanIn.Context context) { - try { - lock.lock(); - return backend.formatTimespan(seconds, context); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json i18nResources() { - try { - lock.lock(); - return backend.i18nResources(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Bool registerTags(@Nullable String tags, boolean preserveUsn, int usn, boolean clearFirst) { - try { - lock.lock(); - return backend.registerTags(tags, preserveUsn, usn, clearFirst); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.AllTagsOut allTags() { - try { - lock.lock(); - return backend.allTags(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getConfigJson(String args) { - try { - lock.lock(); - return backend.getConfigJson(args); - } finally { - lock.unlock(); - } - } - - @Override - public void setConfigJson(@Nullable String key, @Nullable ByteString valueJson) { - try { - lock.lock(); - backend.setConfigJson(key, valueJson); - } finally { - lock.unlock(); - } - } - - @Override - public void removeConfig(String args) { - try { - lock.lock(); - backend.removeConfig(args); - } finally { - lock.unlock(); - } - } - - @Override - public void setAllConfig(Backend.Json args) { - try { - lock.lock(); - backend.setAllConfig(args); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Json getAllConfig() { - try { - lock.lock(); - return backend.getAllConfig(); - } finally { - lock.unlock(); - } - } - - @Override - public Backend.Preferences getPreferences() { - try { - lock.lock(); - return backend.getPreferences(); - } finally { - lock.unlock(); - } - } - - @Override - public void setPreferences(Backend.Preferences args) { - try { - lock.lock(); - backend.setPreferences(args); - } finally { - lock.unlock(); - } - } - - @Override - public void openAnkiDroidCollection(Backend.OpenCollectionIn args) { - try { - lock.lock(); - backend.openAnkiDroidCollection(args); - } finally { - lock.unlock(); - } - } - - @Override - public boolean isOpen() { - try { - lock.lock(); - return backend.isOpen(); - } finally { - lock.unlock(); - } - } - - @Override - public void downgradeBackend(String collectionPath) throws BackendException { - try { - lock.lock(); - backend.downgradeBackend(collectionPath); - } finally { - lock.unlock(); - } - } - - @Override - public void close() throws IOException { - try { - lock.lock(); - backend.close(); - } finally { - lock.unlock(); - } - } - - @Override - public AdBackend.SchedTimingTodayOut2 schedTimingTodayLegacy(long createdSecs, int createdMinsWest, long nowSecs, int nowMinsWest, int rolloverHour) { - try { - lock.lock(); - return backend.schedTimingTodayLegacy(createdSecs, createdMinsWest, nowSecs, nowMinsWest, rolloverHour); - } finally { - lock.unlock(); - } - } - - @Override - public AdBackend.LocalMinutesWestOut localMinutesWestLegacy(long collectionCreationTime) { - try { - lock.lock(); - return backend.localMinutesWestLegacy(collectionCreationTime); - } finally { - lock.unlock(); - } - } - - @Override - public AdBackend.DebugActiveDatabaseSequenceNumbersOut debugActiveDatabaseSequenceNumbers(long backendPtr) { - try { - lock.lock(); - return backend.debugActiveDatabaseSequenceNumbers(backendPtr); - } finally { - lock.unlock(); - } - } -} +///* +// * Copyright (c) 2020 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 net.ankiweb.rsdroid; +// +//import androidx.annotation.Nullable; +// +//import com.google.protobuf.ByteString; +// +//import org.json.JSONArray; +// +//import java.io.IOException; +//import java.util.List; +//import java.util.concurrent.locks.ReentrantLock; +// +//import anki.AdBackend; +//import anki.backend; +//import anki.ankidroid; +// +///** +// * Ensures that a single thread accesses RustBackend at the same time. +// * This is because rslib-bridge currently has no distinction between threads, and handles the state of +// * transactions. +// */ +//public class BackendMutex implements BackendV1 { +// // This class exists as the Rust backend uses a single connection for SQLite, rather than a connection pool +// // This means that SQL can occur cross-threads. +// // There are a few problems with this: +// // * When inside a transaction, another thread can add commands, or close the transaction +// // * Commands can either be sent from the Java, or from the Rust. +// // * We have no knowledge about whether a Rust command will start a transaction +// +// // We handle this using a mutex and some invariants: +// // * If a transaction is held by a thread, have the thread keep the mutex until the transaction is closed +// // * Only one Rust command can run at a time - already true as with_col in rust uses a mutex, but we'll lock on the Java side +// +// private final ReentrantLock lock = new ReentrantLock(); +// private final BackendV1 backend; +// +// public BackendMutex(BackendV1 backend) { +// this.backend = backend; +// } +// +// @Override +// public void beginTransaction() { +// lock.lock(); +// backend.beginTransaction(); +// } +// +// @Override +// public void commitTransaction() { +// try { +// backend.commitTransaction(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void rollbackTransaction() { +// try { +// backend.rollbackTransaction(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public JSONArray fullQuery(String query, Object... bindArgs) { +// try { +// lock.lock(); +// return backend.fullQuery(query, bindArgs); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public int executeGetRowsAffected(String sql, Object... bindArgs) { +// try { +// lock.lock(); +// return backend.executeGetRowsAffected(sql, bindArgs); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public long insertForId(String sql, Object... bindArgs) { +// try { +// lock.lock(); +// return backend.insertForId(sql, bindArgs); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public String[] getColumnNames(String sql) { +// try { +// lock.lock(); +// return backend.getColumnNames(sql); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void closeDatabase() { +// try { +// lock.lock(); +// backend.closeDatabase(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public String getPath() { +// try { +// lock.lock(); +// return backend.getPath(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Sqlite.DBResponse getNextSlice(long startIndex, int sequenceNumber) { +// try { +// lock.lock(); +// return backend.getNextSlice(startIndex, sequenceNumber); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Sqlite.DBResponse fullQueryProto(String query, Object... bindArgs) { +// try { +// lock.lock(); +// return backend.fullQueryProto(query, bindArgs); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void cancelCurrentProtoQuery(int sequenceNumber) { +// try { +// lock.lock(); +// backend.cancelCurrentProtoQuery(sequenceNumber); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void cancelAllProtoQueries() { +// try { +// lock.lock(); +// backend.cancelAllProtoQueries(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void setPageSize(long pageSizeBytes) { +// try { +// lock.lock(); +// backend.setPageSize(pageSizeBytes); +// } finally { +// lock.unlock(); +// } +// } +// +// // RustBackend Implementation +// +// @Override +// public Backend.Progress latestProgress() { +// try { +// lock.lock(); +// return backend.latestProgress(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void setWantsAbort() { +// try { +// lock.lock(); +// backend.setWantsAbort(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.ExtractAVTagsOut extractAVTags(@Nullable String text, boolean questionSide) { +// try { +// lock.lock(); +// return backend.extractAVTags(text, questionSide); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.ExtractLatexOut extractLatex(@Nullable String text, boolean svg, boolean expandClozes) { +// try { +// lock.lock(); +// return backend.extractLatex(text, svg, expandClozes); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Generic.EmptyCardsReport getEmptyCards() { +// try { +// lock.lock(); +// return backend.getEmptyCards(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.RenderCardOut renderExistingCard(long cardId, boolean browser) { +// try { +// lock.lock(); +// return backend.renderExistingCard(cardId, browser); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.RenderCardOut renderUncommittedCard(@Nullable Backend.Note note, int cardOrd, @Nullable ByteString template, boolean fillEmpty) { +// try { +// lock.lock(); +// return backend.renderUncommittedCard(note, cardOrd, template, fillEmpty); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.String stripAVTags(String args) { +// try { +// lock.lock(); +// return backend.stripAVTags(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.SearchCardsOut searchCards(@Nullable String search, @Nullable Backend.SortOrder order) { +// try { +// lock.lock(); +// return backend.searchCards(search, order); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.SearchNotesOut searchNotes(@Nullable String search) { +// try { +// lock.lock(); +// return backend.searchNotes(search); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.UInt32 findAndReplace(List nids, @Nullable String search, @Nullable String replacement, boolean regex, boolean matchCase, @Nullable String fieldName) { +// try { +// lock.lock(); +// return backend.findAndReplace(nids, search, replacement, regex, matchCase, fieldName); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Int32 localMinutesWest(long args) { +// try { +// lock.lock(); +// return backend.localMinutesWest(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void setLocalMinutesWest(int args) { +// try { +// lock.lock(); +// backend.setLocalMinutesWest(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.SchedTimingTodayOut schedTimingToday() { +// try { +// lock.lock(); +// return backend.schedTimingToday(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.String studiedToday(int cards, double seconds) { +// try { +// lock.lock(); +// return backend.studiedToday(cards, seconds); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.String congratsLearnMessage(float nextDue, int remaining) { +// try { +// lock.lock(); +// return backend.congratsLearnMessage(nextDue, remaining); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void updateStats(long deckId, int newDelta, int reviewDelta, int millisecondDelta) { +// try { +// lock.lock(); +// backend.updateStats(deckId, newDelta, reviewDelta, millisecondDelta); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void extendLimits(long deckId, int newDelta, int reviewDelta) { +// try { +// lock.lock(); +// backend.extendLimits(deckId, newDelta, reviewDelta); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.CountsForDeckTodayOut countsForDeckToday(long did) { +// try { +// lock.lock(); +// return backend.countsForDeckToday(did); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.String cardStats(long cid) { +// try { +// lock.lock(); +// return backend.cardStats(cid); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.GraphsOut graphs(@Nullable String search, int days) { +// try { +// lock.lock(); +// return backend.graphs(search, days); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.CheckMediaOut checkMedia() { +// try { +// lock.lock(); +// return backend.checkMedia(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void trashMediaFiles(List fnames) { +// try { +// lock.lock(); +// backend.trashMediaFiles(fnames); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.String addMediaFile(@Nullable String desiredName, @Nullable ByteString data) { +// try { +// lock.lock(); +// return backend.addMediaFile(desiredName, data); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void emptyTrash() { +// try { +// lock.lock(); +// Generic.EmptyTrash(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void restoreTrash() { +// try { +// lock.lock(); +// backend.restoreTrash(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.DeckID addOrUpdateDeckLegacy(@Nullable ByteString deck, boolean preserveUsnAndMtime) { +// try { +// lock.lock(); +// return backend.addOrUpdateDeckLegacy(deck, preserveUsnAndMtime); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.DeckTreeNode deckTree(long now, long topDeckId) { +// try { +// lock.lock(); +// return backend.deckTree(now, topDeckId); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json deckTreeLegacy() { +// try { +// lock.lock(); +// return backend.deckTreeLegacy(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json getAllDecksLegacy() { +// try { +// lock.lock(); +// return backend.getAllDecksLegacy(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.DeckID getDeckIDByName(String name) { +// try { +// lock.lock(); +// return backend.getDeckIDByName(name); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json getDeckLegacy(long did) { +// try { +// lock.lock(); +// return backend.getDeckLegacy(did); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.DeckNames getDeckNames(boolean skipEmptyDefault, boolean includeFiltered) { +// try { +// lock.lock(); +// return backend.getDeckNames(skipEmptyDefault, includeFiltered); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json newDeckLegacy(boolean args) { +// try { +// lock.lock(); +// return backend.newDeckLegacy(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void removeDeck(long args) { +// try { +// lock.lock(); +// backend.removeDeck(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.DeckConfigID addOrUpdateDeckConfigLegacy(@Nullable ByteString config, boolean preserveUsnAndMtime) { +// try { +// lock.lock(); +// return backend.addOrUpdateDeckConfigLegacy(config, preserveUsnAndMtime); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json allDeckConfigLegacy() { +// try { +// lock.lock(); +// return backend.allDeckConfigLegacy(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json getDeckConfigLegacy(long dConfId) { +// try { +// lock.lock(); +// return backend.getDeckConfigLegacy(dConfId); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json newDeckConfigLegacy() { +// try { +// lock.lock(); +// return backend.newDeckConfigLegacy(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void removeDeckConfig(long dConfId) { +// try { +// lock.lock(); +// backend.removeDeckConfig(dConfId); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Card getCard(long cid) { +// try { +// lock.lock(); +// return backend.getCard(cid); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void updateCard(Backend.Card args) { +// try { +// lock.lock(); +// backend.updateCard(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.CardID addCard(Backend.Card args) { +// try { +// lock.lock(); +// return backend.addCard(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void removeCards(List cardIds) { +// try { +// lock.lock(); +// backend.removeCards(cardIds); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Note newNote(long noteTypidId) { +// try { +// lock.lock(); +// return backend.newNote(noteTypidId); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.NoteID addNote(@Nullable Backend.Note note, long deckId) { +// try { +// lock.lock(); +// return backend.addNote(note, deckId); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void updateNote(Backend.Note args) { +// try { +// lock.lock(); +// backend.updateNote(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Note getNote(long nid) { +// try { +// lock.lock(); +// return backend.getNote(nid); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void removeNotes(List noteIds, List cardIds) { +// try { +// lock.lock(); +// backend.removeNotes(noteIds, cardIds); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.UInt32 addNoteTags(List nids, @Nullable String tags) { +// try { +// lock.lock(); +// return backend.addNoteTags(nids, tags); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.UInt32 updateNoteTags(List nids, @Nullable String tags, @Nullable String replacement, boolean regex) { +// try { +// lock.lock(); +// return backend.updateNoteTags(nids, tags, replacement, regex); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.ClozeNumbersInNoteOut clozeNumbersInNote(Backend.Note args) { +// try { +// lock.lock(); +// return backend.clozeNumbersInNote(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void afterNoteUpdates(List nids, boolean markNotesModified, boolean generateCards) { +// try { +// lock.lock(); +// backend.afterNoteUpdates(nids, markNotesModified, generateCards); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.FieldNamesForNotesOut fieldNamesForNotes(List nids) { +// try { +// lock.lock(); +// return backend.fieldNamesForNotes(nids); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.NoteIsDuplicateOrEmptyOut noteIsDuplicateOrEmpty(Backend.Note args) { +// try { +// lock.lock(); +// return backend.noteIsDuplicateOrEmpty(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.NoteTypeID addOrUpdateNotetype(@Nullable ByteString json, boolean preserveUsnAndMtime) { +// try { +// lock.lock(); +// return backend.addOrUpdateNotetype(json, preserveUsnAndMtime); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json getStockNotetypeLegacy(@Nullable Backend.StockNoteType kind) { +// try { +// lock.lock(); +// return backend.getStockNotetypeLegacy(kind); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json getNotetypeLegacy(long noteTypeId) { +// try { +// lock.lock(); +// return backend.getNotetypeLegacy(noteTypeId); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.NoteTypeNames getNotetypeNames() { +// try { +// lock.lock(); +// return backend.getNotetypeNames(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.NoteTypeUseCounts getNotetypeNamesAndCounts() { +// try { +// lock.lock(); +// return backend.getNotetypeNamesAndCounts(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.NoteTypeID getNotetypeIDByName(String name) { +// try { +// lock.lock(); +// return backend.getNotetypeIDByName(name); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void removeNotetype(long noteTypeId) { +// try { +// lock.lock(); +// backend.removeNotetype(noteTypeId); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void openCollection(@Nullable String collectionPath, @Nullable String mediaFolderPath, @Nullable String mediaDbPath, @Nullable String logPath) { +// try { +// lock.lock(); +// backend.openCollection(collectionPath, mediaFolderPath, mediaDbPath, logPath); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void closeCollection(boolean downgradeToSchema11) { +// try { +// lock.lock(); +// backend.closeCollection(downgradeToSchema11); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.CheckDatabaseOut checkDatabase() { +// try { +// lock.lock(); +// return backend.checkDatabase(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void beforeUpload() { +// try { +// lock.lock(); +// backend.beforeUpload(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.String translateString(Backend.TranslateStringIn args) { +// try { +// lock.lock(); +// return backend.translateString(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.String formatTimespan(float seconds, @Nullable Backend.FormatTimespanIn.Context context) { +// try { +// lock.lock(); +// return backend.formatTimespan(seconds, context); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json i18nResources() { +// try { +// lock.lock(); +// return backend.i18nResources(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Bool registerTags(@Nullable String tags, boolean preserveUsn, int usn, boolean clearFirst) { +// try { +// lock.lock(); +// return backend.registerTags(tags, preserveUsn, usn, clearFirst); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.AllTagsOut allTags() { +// try { +// lock.lock(); +// return backend.allTags(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json getConfigJson(String args) { +// try { +// lock.lock(); +// return backend.getConfigJson(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void setConfigJson(@Nullable String key, @Nullable ByteString valueJson) { +// try { +// lock.lock(); +// backend.setConfigJson(key, valueJson); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void removeConfig(String args) { +// try { +// lock.lock(); +// backend.removeConfig(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void setAllConfig(Backend.Json args) { +// try { +// lock.lock(); +// backend.setAllConfig(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Json getAllConfig() { +// try { +// lock.lock(); +// return backend.getAllConfig(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public Backend.Preferences getPreferences() { +// try { +// lock.lock(); +// return backend.getPreferences(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void setPreferences(Backend.Preferences args) { +// try { +// lock.lock(); +// backend.setPreferences(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void openAnkiDroidCollection(Backend.OpenCollectionIn args) { +// try { +// lock.lock(); +// backend.openAnkiDroidCollection(args); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public boolean isOpen() { +// try { +// lock.lock(); +// return backend.isOpen(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void downgradeBackend(String collectionPath) throws BackendException { +// try { +// lock.lock(); +// backend.downgradeBackend(collectionPath); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public void close() throws IOException { +// try { +// lock.lock(); +// backend.close(); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public AdBackend.SchedTimingTodayOut2 schedTimingTodayLegacy(long createdSecs, int createdMinsWest, long nowSecs, int nowMinsWest, int rolloverHour) { +// try { +// lock.lock(); +// return backend.schedTimingTodayLegacy(createdSecs, createdMinsWest, nowSecs, nowMinsWest, rolloverHour); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public AdBackend.LocalMinutesWestOut localMinutesWestLegacy(long collectionCreationTime) { +// try { +// lock.lock(); +// return backend.localMinutesWestLegacy(collectionCreationTime); +// } finally { +// lock.unlock(); +// } +// } +// +// @Override +// public AdBackend.DebugActiveDatabaseSequenceNumbersOut debugActiveDatabaseSequenceNumbers(long backendPtr) { +// try { +// lock.lock(); +// return backend.debugActiveDatabaseSequenceNumbers(backendPtr); +// } finally { +// lock.unlock(); +// } +// } +//} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.java index 5ae8415bf..4f7098d38 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.java @@ -18,7 +18,7 @@ import androidx.annotation.NonNull; -import BackendProto.Backend; +import net.ankiweb.rsdroid.database.NotImplementedException; public class BackendUtils { /** @@ -26,7 +26,8 @@ public class BackendUtils { * @throws android.database.sqlite.SQLiteDatabaseCorruptException If database is corrupt */ public static void openAnkiDroidCollection(BackendV1 backendV1, String path) { - backendV1.openAnkiDroidCollection(Backend.OpenCollectionIn.newBuilder().setCollectionPath(path).build()); + throw new NotImplementedException(); +// backendV1.openAnkiDroidCollection(Backend.OpenCol.newBuilder().setCollectionPath(path).build()); } @NonNull diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.java deleted file mode 100644 index 37fa681bc..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.java +++ /dev/null @@ -1,21 +0,0 @@ -package net.ankiweb.rsdroid; - -import net.ankiweb.rsdroid.database.SQLHandler; - -import java.io.Closeable; - -import BackendProto.Backend; - -public interface BackendV1 extends SQLHandler, net.ankiweb.rsdroid.RustBackend, net.ankiweb.rsdroid.Adbackend, Closeable { - void openAnkiDroidCollection(Backend.OpenCollectionIn args) throws BackendException; - - boolean isOpen(); - - /** - * Downgrades the collection from Schema 16 to Schema 11 - * @param collectionPath The fully qualified path to collection.anki2 - * @throws BackendException The collection is not schema 16 - * @throws BackendException Collection is already open - */ - void downgradeBackend(String collectionPath) throws BackendException; -} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt new file mode 100644 index 000000000..b2a689c64 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt @@ -0,0 +1,9 @@ +package net.ankiweb.rsdroid + +import net.ankiweb.rsdroid.database.SQLHandler +import java.io.Closeable + +interface BackendV1 : SQLHandler, Closeable { + fun openCollection(collectionPath: String, mediaFolderPath: String, mediaDbPath: String, logPath: String, forceSchema11: Boolean); + fun isOpen(): Boolean +} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.java deleted file mode 100644 index 82b024b86..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.java +++ /dev/null @@ -1,395 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid; - -import androidx.annotation.CheckResult; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import com.google.protobuf.InvalidProtocolBufferException; - -import org.json.JSONArray; -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.Closeable; -import java.io.File; -import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; - -import BackendProto.AdBackend; -import BackendProto.Backend; -import BackendProto.Sqlite; -import timber.log.Timber; - -/** - * Do not use an instance of this class directly - it should be handled via BackendMutex - * All public methods should be accessed via interface - * */ -public class BackendV1Impl extends net.ankiweb.rsdroid.RustBackendImpl implements BackendV1, Closeable { - - /* - Note: java.lang.RuntimeException: com.google.protobuf.InvalidProtocolBufferException: Protocol message end-group tag did not match expected tag. - Implies that ? - */ - - private Pointer backEndPointer = null; - @Nullable - private String collectionPath; - private boolean isDisposed; - private final AnkiDroidBackendImpl ankiDroidBackend; - - // intentionally package private - use BackendFactory - BackendV1Impl() { - ankiDroidBackend = new AnkiDroidBackendImpl(this::ensureBackend); - } - - /** - * Obtains a pointer to the Rust backend. - * This pointer is to a structure containing environment-level data such as the - * optional collection reference, - * - * Java is responsible for disposing of this pointer, or a memory leak will occur. - * - * TODO: This defines the locales, logging and translation files. We do not use these yet. - * */ - @Override - @CheckResult - public Pointer ensureBackend() { - if (isDisposed) { - throw new BackendException("Backend has been closed"); - } - - if (backEndPointer == null) { - boolean isServer = false; - String langs = "en"; - String localeFolderPath = ""; - - Timber.i("Opening rust backend. Server: %b. Langs: '%s', path: %s", isServer, langs, localeFolderPath); - - Backend.BackendInit.Builder builder = Backend.BackendInit.newBuilder() - .setServer(isServer) - .addPreferredLangs(langs) - .setLocaleFolderPath(localeFolderPath); - - long backendPointer = NativeMethods.openBackend(builder.build().toByteArray()); - - backEndPointer = new Pointer(backendPointer); - } - return backEndPointer; - } - - @Override - protected byte[] executeCommand(long backendPointer, int command, byte[] args) { - return NativeMethods.executeCommand(backendPointer, command, args); - } - - @Override - public boolean isOpen() { - return backEndPointer != null; - } - - @Override - public void close() { - Timber.i("Closing rust backend"); - if (backEndPointer != null) { - try { - closeDatabase(); - } catch (BackendException ex) { - // Typically: CollectionNotOpen - Timber.w(ex, "Error while closing rust database"); - } - Timber.d("Executing close backend command"); - NativeMethods.closeBackend(backEndPointer.toJni()); - } - this.isDisposed = true; - backEndPointer = null; - } - - public void openAnkiDroidCollection(Backend.OpenCollectionIn args) { - collectionPath = args.getCollectionPath(); - try { - Pointer backendPointer = ensureBackend(); - Timber.i("Opening Collection: '%s' '%s' '%s' '%s'", args.getCollectionPath(), args.getLogPath(), args.getMediaDbPath(), args.getMediaFolderPath()); - byte[] result = NativeMethods.openCollection(backendPointer.toJni(), args.toByteArray()); - Backend.Empty message = Backend.Empty.parseFrom(result); - validateMessage(result, message); - } catch (BackendException.BackendDbException ex) { - collectionPath = null; - throw ex.toSQLiteException("openAnkiDroidCollection"); - } catch (InvalidProtocolBufferException ex) { - collectionPath = null; - throw BackendException.fromException(ex); - } - } - - @CheckResult - public JSONArray fullQuery(String sql, @Nullable Object... args) { - try { - Timber.i("Rust: SQL query: '%s'", sql); - return fullQueryInternal(sql, args); - } catch (JSONException e) { - throw new RuntimeException(e); - } - } - - private JSONArray fullQueryInternal(String sql, @Nullable Object[] args) throws JSONException { - List asList = args == null ? new ArrayList<>() : Arrays.asList(args); - JSONObject o = new JSONObject(); - - o.put("kind", "query"); - o.put("sql", sql); - o.put("args", new JSONArray(asList)); - o.put("first_row_only", false); - - byte[] data = jsonToBytes(o); - - Pointer backend = ensureBackend(); - byte[] result = NativeMethods.fullDatabaseCommand(backend.toJni(), data); - - String json = new String(result); - - try { - return new JSONArray(json); - } catch (Exception e) { - Backend.BackendError pbError; - try { - pbError = Backend.BackendError.parseFrom(result); - } catch (InvalidProtocolBufferException invalidProtocolBufferException) { - throw BackendException.fromException(invalidProtocolBufferException); - } - throw BackendException.fromError(pbError); - } - } - - public long insertForId(String sql, Object[] args) { - try { - Timber.i("Rust: sql insert %s", sql); - - List asList = args == null ? new ArrayList<>() : Arrays.asList(args); - JSONObject o = new JSONObject(); - o.put("sql", sql); - o.put("args", new JSONArray(asList)); - - byte[] data = jsonToBytes(o); - - Pointer backend = ensureBackend(); - byte[] result = NativeMethods.sqlInsertForId(backend.toJni(), data); - - Backend.Int64 message = Backend.Int64.parseFrom(result); - validateMessage(result, message); - return message.getVal(); - - } catch (JSONException e) { - throw new RuntimeException(e); - } catch (InvalidProtocolBufferException e) { - throw BackendException.fromException(e); - } - } - - public int executeGetRowsAffected(String sql, Object[] bindArgs) { - try { - Timber.i("Rust: executeGetRowsAffected %s", sql); - - List asList = bindArgs == null ? new ArrayList<>() : Arrays.asList(bindArgs); - JSONObject o = new JSONObject(); - o.put("sql", sql); - o.put("args", new JSONArray(asList)); - - byte[] data = jsonToBytes(o); - - Pointer backend = ensureBackend(); - byte[] result = NativeMethods.sqlQueryForAffected(backend.toJni(), data); - - Backend.Int32 message = Backend.Int32.parseFrom(result); - validateMessage(result, message); - return message.getVal(); - } catch (JSONException e) { - throw new RuntimeException(e); - } catch (InvalidProtocolBufferException e) { - throw BackendException.fromException(e); - } - } - - /* Begin Protobuf-based database streaming methods (#6) */ - - @Override - public Sqlite.DBResponse fullQueryProto(String query, Object... args) { - byte[] result = null; - try { - Timber.d("Rust: fullQueryProto %s", query); - - List asList = args == null ? new ArrayList<>() : Arrays.asList(args); - JSONObject o = new JSONObject(); - - o.put("kind", "query"); - o.put("sql", query); - o.put("args", new JSONArray(asList)); - o.put("first_row_only", false); - - byte[] data = jsonToBytes(o); - - Pointer backend = ensureBackend(); - result = NativeMethods.databaseCommand(backend.toJni(), data); - - Sqlite.DBResponse message = Sqlite.DBResponse.parseFrom(result); - validateMessage(result, message); - return message; - } catch (JSONException e) { - throw new RuntimeException(e); - } catch (InvalidProtocolBufferException e) { - validateResult(result); - throw BackendException.fromException(e); - } - } - - @Override - public Sqlite.DBResponse getNextSlice(long startIndex, int sequenceNumber) { - byte[] result = null; - try { - Timber.d("Rust: getNextSlice %d", startIndex); - - Pointer backend = ensureBackend(); - result = NativeMethods.databaseGetNextResultPage(backend.toJni(), sequenceNumber, startIndex); - - Sqlite.DBResponse message = Sqlite.DBResponse.parseFrom(result); - validateMessage(result, message); - return message; - } catch (InvalidProtocolBufferException e) { - validateResult(result); - throw BackendException.fromException(e); - } - } - - @Override - public void cancelCurrentProtoQuery(int sequenceNumber) { - Timber.d("cancelCurrentProtoQuery"); - NativeMethods.cancelCurrentProtoQuery(ensureBackend().toJni(), sequenceNumber); - } - - @Override - public void cancelAllProtoQueries() { - Timber.d("cancelAllProtoQueries"); - NativeMethods.cancelAllProtoQueries(ensureBackend().toJni()); - - } - - /* End protobuf-based database streaming methods */ - - public void beginTransaction() { - // Note: Casing is important here. - performTransaction("begin"); - } - - - public void commitTransaction() { - performTransaction("commit"); - } - - public void rollbackTransaction() { - performTransaction("rollback"); - } - - private void performTransaction(String kind) { - try { - Timber.i("Rust: transaction %s", kind); - - JSONObject o = new JSONObject(); - - o.put("kind", kind); - - byte[] data = jsonToBytes(o); - - Pointer backend = ensureBackend(); - byte[] result = NativeMethods.fullDatabaseCommand(backend.toJni(), data); - - String asString = new String(result); - - if (!"null".equals(asString)) { - Backend.BackendError ex = Backend.BackendError.parseFrom(result); - throw BackendException.fromError(ex); - } - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - public static void setPageSizeForTesting(long pageSizeInBytes) { - // TODO: Make this nonstatic - NativeMethods.setDbPageSize(pageSizeInBytes); - } - - @Override - public void setPageSize(long pageSizeInBytes) { - NativeMethods.setDbPageSize(pageSizeInBytes); - } - - @Override - public String[] getColumnNames(String sql) { - Timber.i("Rust: getColumnNames %s", sql); - return NativeMethods.getColumnNames(ensureBackend().toJni(), sql); - } - - @Override - public void closeCollection(boolean downgradeToSchema11) { - cancelAllProtoQueries(); - super.closeCollection(downgradeToSchema11); - } - - @Override - public void closeDatabase() { - closeCollection(false); - } - - @Override - public String getPath() { - return collectionPath; - } - - @SuppressWarnings("CharsetObjectCanBeUsed") - private byte[] jsonToBytes(JSONObject o) { - return o.toString().getBytes(Charset.forName("UTF-8")); - } - - @Override - public AdBackend.SchedTimingTodayOut2 schedTimingTodayLegacy(long createdSecs, int createdMinsWest, long nowSecs, int nowMinsWest, int rolloverHour) { - return ankiDroidBackend.schedTimingTodayLegacy(createdSecs, createdMinsWest, nowSecs, nowMinsWest, rolloverHour); - } - - @Override - public AdBackend.LocalMinutesWestOut localMinutesWestLegacy(long collectionCreationTime) { - return ankiDroidBackend.localMinutesWestLegacy(collectionCreationTime); - } - - @Override - @RustCleanup("Architecture - backendPtr param is not required") - public AdBackend.DebugActiveDatabaseSequenceNumbersOut debugActiveDatabaseSequenceNumbers(long backendPtr) { - return ankiDroidBackend.debugActiveDatabaseSequenceNumbers(ensureBackend().toJni()); - } - - @Override - public void downgradeBackend(String collectionPath) { - if (!new File(collectionPath).exists()) { - throw new BackendException(collectionPath + " not found"); - } - - ankiDroidBackend.downgradeBackend(collectionPath); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.kt new file mode 100644 index 000000000..7818f8741 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.kt @@ -0,0 +1,283 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid + +import androidx.annotation.CheckResult +import androidx.annotation.VisibleForTesting +import anki.ankidroid.DBResponse +import anki.backend.BackendError +import anki.backend.BackendInit +import anki.backend.GeneratedBackend +import anki.generic.Int64 +import com.google.protobuf.ByteString +import com.google.protobuf.GeneratedMessageV3 +import com.google.protobuf.InvalidProtocolBufferException +import org.json.JSONArray +import org.json.JSONException +import org.json.JSONObject +import timber.log.Timber +import java.io.Closeable +import java.util.concurrent.locks.ReentrantLock +import kotlin.concurrent.withLock + +open class BackendV1Impl(langs: Iterable = listOf("en")) : GeneratedBackend(), BackendV1, Closeable { + // Set on init; unset on .close(). Access via withBackend() + private var backendPointer: Long? = null + private val lock = ReentrantLock() + // Only stored to satisfy .getPath() interface in SQL connection + private var collectionPath: String? = null + + override fun isOpen(): Boolean { + return backendPointer != null + } + + + /** + * Open a backend instance, loading the shared library if not already loaded. + */ + init { + NativeMethods.ensureSetup() + Timber.i("Opening rust backend with lang=$langs") + val input = BackendInit.newBuilder() + .addAllPreferredLangs(langs) + .build() + .toByteArray() + val outBytes = unpackResult(NativeMethods.openBackend(input)) + backendPointer = Int64.parseFrom(outBytes).`val` + } + + /** + * Close the backend, and any open collection. This object can not be used after this. + */ + override fun close() { + Timber.i("Closing rust backend") + lock.withLock { + // Must be checked inside lock to avoid race + if (backendPointer != null) { + withBackend { + backendPointer = null + NativeMethods.closeBackend(it) + } + } + } + } + + /** + * Open a collection. There must not already be an open collection. + */ + override fun openCollection(collectionPath: String, mediaFolderPath: String, mediaDbPath: String, logPath: String, forceSchema11: Boolean) { + super.openCollection(collectionPath, mediaFolderPath, mediaDbPath, logPath, forceSchema11) + this.collectionPath = collectionPath + } + + /** + * Closes an open collection. There must be an open collection. + */ + override fun closeCollection(downgradeToSchema11: Boolean) { + cancelAllProtoQueries() + collectionPath = null + super.closeCollection(downgradeToSchema11) + } + + /** + * All backend methods (except for backend init/close) flow through this. + */ + override fun runMethodRaw(service: Int, method: Int, input: ByteArray): ByteArray { + return withBackend { + unpackResult(NativeMethods.runMethodRaw(it, service, method, input)) + } + } + + /** + * Run the provided closure with locked access to the backend. + * The backend maintains its own lock for backend commands, so this extra + * level of locks is only useful for executing begin+sql+commit/rollback commands + * without other commands being interleaved. When AnkiDroid has migrated to more + * of the backend, it can probably remove this and leave the transaction handling + * up to the backend. + */ + private fun withBackend(fn: (ptr: Long) -> T): T { + lock.withLock { + if (backendPointer == null) { + throw BackendException("Backend has been closed") + } + return fn(backendPointer!!) + } + } + + // transactions hold the lock until commit/rollback + + override fun beginTransaction() { + lock.lock() + performTransaction(DbRequestKind.Begin) + } + + override fun commitTransaction() { + try { + performTransaction(DbRequestKind.Commit) + } finally { + lock.unlock() + } + } + + override fun rollbackTransaction() { + try { + performTransaction(DbRequestKind.Rollback) + } finally { + lock.unlock() + } + } + + // other DB methods + + override fun closeDatabase() { + closeCollection(false) + } + + override fun getPath(): String { + return collectionPath!! + } + + @CheckResult + override fun fullQuery(sql: String, vararg args: Any?): JSONArray { + return try { + Timber.i("Rust: SQL query: '%s'", sql) + fullQueryInternal(sql, args as Array) + } catch (e: JSONException) { + throw RuntimeException(e) + } + } + + @Throws(JSONException::class) + private fun fullQueryInternal(sql: String, args: Array): JSONArray { + return JSONArray(runDbCommand(dbRequestJson(sql, args)).json.toString()) + } + + override fun insertForId(sql: String, args: Array): Long { + Timber.i("Rust: sql insert %s", sql) + return super.insertForId(dbRequestJson(sql, args)).`val` + } + + override fun executeGetRowsAffected(sql: String, args: Array): Int { + Timber.i("Rust: executeGetRowsAffected %s", sql) + return runDbCommandForRowCount(dbRequestJson(sql, args)).`val`.toInt() + } + + /* Begin Protobuf-based database streaming methods (#6) */ + override fun fullQueryProto(query: String, vararg args: Any?): DBResponse { + Timber.d("Rust: fullQueryProto %s", query) + return runDbCommandProto(dbRequestJson(query, args as Array)) + } + + override fun getNextSlice(startIndex: Long, sequenceNumber: Int): DBResponse { + Timber.d("Rust: getNextSlice %d", startIndex) + return getNextResultPage(sequenceNumber, startIndex) + } + + override fun cancelCurrentProtoQuery(sequenceNumber: Int) { + Timber.d("cancelCurrentProtoQuery") + flushQuery(sequenceNumber) + } + + override fun cancelAllProtoQueries() { + Timber.d("cancelAllProtoQueries") + flushAllQueries() + } + + private fun performTransaction(kind: DbRequestKind) { + Timber.i("Rust: transaction %s", kind) + runDbCommand(dbRequestJson(kind=kind)) + } + + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + override fun setPageSize(pageSizeInBytes: Long) { + super.setPageSize(pageSizeInBytes) + } + + override fun getColumnNames(sql: String): Array { + Timber.i("Rust: getColumnNames %s", sql) + return getColumnNamesFromQuery(sql).valsList.toTypedArray() + } + + // @Override + // public AdBackend.SchedTimingTodayOut2 schedTimingTodayLegacy(long createdSecs, int createdMinsWest, long nowSecs, int nowMinsWest, int rolloverHour) { + // return ankiDroidBackend.schedTimingTodayLegacy(createdSecs, createdMinsWest, nowSecs, nowMinsWest, rolloverHour); + // } + // + // @Override + // public AdBackend.LocalMinutesWestOut localMinutesWestLegacy(long collectionCreationTime) { + // return ankiDroidBackend.localMinutesWestLegacy(collectionCreationTime); + // } + // + // @Override + // @RustCleanup("Architecture - backendPtr param is not required") + // public AdBackend.DebugActiveDatabaseSequenceNumbersOut debugActiveDatabaseSequenceNumbers(long backendPtr) { + // return ankiDroidBackend.debugActiveDatabaseSequenceNumbers(ensureBackend().toJni()); + // } + // + // @Override + // public void downgradeBackend(String collectionPath) { + // if (!new File(collectionPath).exists()) { + // throw new BackendException(collectionPath + " not found"); + // } + // + // ankiDroidBackend.downgradeBackend(collectionPath); + // } +} + +/** + * Build a JSON DB request + */ +private fun dbRequestJson(sql: String = "", args: Array = arrayOf(), kind: DbRequestKind = DbRequestKind.Query, firstRowOnly: Boolean = false): ByteString { + val o = JSONObject() + o.put("kind", kind.name.lowercase()) + o.put("sql", sql) + o.put("args", JSONArray(args)) + o.put("first_row_only", firstRowOnly) + return ByteString.copyFromUtf8(o.toString()) +} + +enum class DbRequestKind { + Query, + Begin, + Commit, + Rollback, + ExecuteMany +} + +/** + * Unpack success/error tuple from backend, and throw if error. + */ +private fun unpackResult(result: Array?): ByteArray { + if (result == null) { + throw BackendException("null return from backend method") + } + val (successBytes, errorBytes) = result + if (errorBytes != null) { + // convert the error to an exception + val pbError: BackendError = try { + BackendError.parseFrom(errorBytes) + } catch (invalidProtocolBufferException: InvalidProtocolBufferException) { + throw BackendException.fromException(invalidProtocolBufferException) + } + throw BackendException.fromError(pbError) + } else if (successBytes != null) { + return successBytes + } else { + // should not happen + throw BackendException("both ok & err cases null") + } +} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.java deleted file mode 100644 index 50b6babe3..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.java +++ /dev/null @@ -1,119 +0,0 @@ -package net.ankiweb.rsdroid; - -import android.annotation.SuppressLint; -import android.os.Build; - -import androidx.annotation.CheckResult; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import timber.log.Timber; - -public class NativeMethods { - - private static boolean hasSetUp = false; - private static RustBackendFailedException setupException; - - public static boolean isRoboUnitTest() { - return "robolectric".equals(Build.FINGERPRINT); - } - - - public static synchronized void ensureSetup() throws RustBackendFailedException { - if (hasSetUp) { - if (setupException != null) { - throw setupException; - } - return; - } - try { - System.loadLibrary("rsdroid"); - } catch (UnsatisfiedLinkError e) { - if (!isRoboUnitTest()) { - setupException = new RustBackendFailedException(e); - throw setupException; - } - // In Robolectric, assume setup works (setupException == null) if the library throws. - // As the library is loaded at a later time (or a failure will be quickly found). - } finally { - hasSetUp = true; - } - } - - public static byte[] executeCommand(long backendPointer, final int command, byte[] args) { - Timber.i("ExecuteCommand: %s", net.ankiweb.rsdroid.RustBackendMethods.commandName(command)); - return command(backendPointer, command, args); - } - - @CheckResult - private static native byte[] command(long backendPointer, final int command, byte[] args); - - @CheckResult - static native long openBackend(byte[] data); - - @SuppressLint("CheckResult") - static void execCommand(long backendPointer, final int command, byte[] args) { - command(backendPointer, command, args); - } - - @CheckResult - static native byte[] openCollection(long backendPointer, byte[] data); - - - /** Temporary: perform a database command and obtain the result as a JSON string without streaming. */ - @CheckResult - static native byte[] fullDatabaseCommand(long backendPointer, byte[] data); - - /** Input: JSON serialized request - * @return DbResult object */ - @CheckResult - static native byte[] databaseCommand(long backendPointer, byte[] data); - - /** Returns the next page of results after a databaseCommand. - * @return DbResult object */ - @CheckResult - static native byte[] databaseGetNextResultPage(long backendPointer, int sequenceNumber, long startIndex); - - /** Clears the memory from the current protobuf query. */ - static native int cancelCurrentProtoQuery(long backendPointer, int sequenceNumber); - - /** Clears the memory from the all protobuf queries. */ - static native void cancelAllProtoQueries(long backendPointer); - - /** - * Performs an insert and returns the last inserted row id. - * data: json encoded data - */ - @CheckResult - static native byte[] sqlInsertForId(long backendPointer, byte[] data); - - @CheckResult - static native byte[] sqlQueryForAffected(long backendPointer, byte[] data); - - @Nullable - @CheckResult - static native String[] getColumnNames(long backendPointer, String sql); - - static native long closeBackend(long backendPointer); - - static native byte[] executeAnkiDroidCommand(long backendPointer, int command, byte[] args); - - /** - * Sets the maximum number of bytes that a page of database results should return - * {@link net.ankiweb.rsdroid.database.StreamingProtobufSQLiteCursor} - */ - static native void setDbPageSize(long numberOfBytes); - - /** - * Produces all possible Rust-based errors. - */ - @VisibleForTesting(otherwise = VisibleForTesting.NONE) - static native byte[] debugProduceError(long backendPointer, String command); - - /** - * - * @param path The path of the collection to downgrade - * @return Error message, or empty string for success - */ - static native String downgradeDatabase(String path); -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt new file mode 100644 index 000000000..af97c902a --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt @@ -0,0 +1,44 @@ +package net.ankiweb.rsdroid + +import net.ankiweb.rsdroid.RustBackendFailedException +import android.os.Build +import androidx.annotation.CheckResult +import kotlin.Throws +import net.ankiweb.rsdroid.NativeMethods + +object NativeMethods { + private var hasSetUp = false + private var setupException: RustBackendFailedException? = null + val isRoboUnitTest: Boolean + get() = "robolectric" == Build.FINGERPRINT + + @JvmStatic + @Synchronized + @Throws(RustBackendFailedException::class) + fun ensureSetup() { + if (hasSetUp) { + if (setupException != null) { + throw setupException!! + } + return + } + try { + System.loadLibrary("rsdroid") + } catch (e: UnsatisfiedLinkError) { + if (!isRoboUnitTest) { + setupException = RustBackendFailedException(e) + throw setupException!! + } + // In Robolectric, assume setup works (setupException == null) if the library throws. + // As the library is loaded at a later time (or a failure will be quickly found). + } finally { + hasSetUp = true + } + } + + @CheckResult + external fun runMethodRaw(backendPointer: Long, service: Int, method: Int, args: ByteArray?): Array? + @CheckResult + external fun openBackend(data: ByteArray?): Array? + external fun closeBackend(backendPointer: Long) +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Pointer.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/Pointer.java deleted file mode 100644 index d2505209a..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Pointer.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.ankiweb.rsdroid; - -public class Pointer { - private final long pointer; - - - public Pointer(long backendPointer) { - pointer = backendPointer; - } - - - public long toJni() { - return pointer; - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.java index b854dd3a8..08316667f 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.java @@ -40,7 +40,7 @@ protected SupportSQLiteDatabase createRustSupportSQLiteDatabase(@SuppressWarning if (configuration != null) { BackendV1 backend = backendFactory.getBackend(); // openCollection opens and upgrades the collection - backend.openCollection(configuration.name, null, null, null); +// backend.openCollection(configuration.name, null, null, null); return new RustSupportSQLiteDatabase(backend, readOnly); } else { return new RustSupportSQLiteDatabase(backend, readOnly); diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.java index f0e8c8496..d1e1e3b93 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.java @@ -20,8 +20,6 @@ import org.json.JSONArray; -import BackendProto.Sqlite; - public interface SQLHandler { @CheckResult JSONArray fullQuery(String query, Object... bindArgs); @@ -41,8 +39,8 @@ public interface SQLHandler { String getPath(); /* Protobuf-related (#6) */ - Sqlite.DBResponse getNextSlice(long startIndex, int sequenceNumber); - Sqlite.DBResponse fullQueryProto(String query, Object... bindArgs); + anki.ankidroid.DBResponse getNextSlice(long startIndex, int sequenceNumber); + anki.ankidroid.DBResponse fullQueryProto(String query, Object... bindArgs); void cancelCurrentProtoQuery(int sequenceNumber); void cancelAllProtoQueries(); diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.java index 985ba65a6..e5aae9f9f 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.java @@ -38,8 +38,6 @@ import java.util.Stack; -import BackendProto.Sqlite; - /** Handles transaction state management */ public class Session implements SQLHandler { private final SQLHandler backend; @@ -86,12 +84,12 @@ public String getPath() { } @Override - public Sqlite.DBResponse getNextSlice(long startIndex, int sequenceNumber) { + public anki.ankidroid.DBResponse getNextSlice(long startIndex, int sequenceNumber) { return backend.getNextSlice(startIndex, sequenceNumber); } @Override - public Sqlite.DBResponse fullQueryProto(String query, Object... bindArgs) { + public anki.ankidroid.DBResponse fullQueryProto(String query, Object... bindArgs) { return backend.fullQueryProto(query, bindArgs); } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.java index 8de785bd1..065dd4614 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.java @@ -26,8 +26,6 @@ import java.util.Locale; -import BackendProto.Sqlite; - public class StreamingProtobufSQLiteCursor extends AnkiDatabaseCursor { /** * Rust Implementation: @@ -41,7 +39,7 @@ public class StreamingProtobufSQLiteCursor extends AnkiDatabaseCursor { private final SQLHandler backend; private final String query; - private Sqlite.DBResponse results; + private anki.ankidroid.DBResponse results; /** The local position in the current slice */ private int positionInSlice = -1; private String[] columnMapping; @@ -167,7 +165,7 @@ public int getColumnCount() { @Override public String getString(int columnIndex) { - Sqlite.SqlValue field = getFieldAtIndex(columnIndex); + anki.ankidroid.SqlValue field = getFieldAtIndex(columnIndex); switch (field.getDataCase()) { case BLOBVALUE: throw new SQLiteException("unknown error (code 0): Unable to convert BLOB to string"); case LONGVALUE: return Long.toString(field.getLongValue()); @@ -180,7 +178,7 @@ public String getString(int columnIndex) { @Override public long getLong(int columnIndex) { - Sqlite.SqlValue field = getFieldAtIndex(columnIndex); + anki.ankidroid.SqlValue field = getFieldAtIndex(columnIndex); switch (field.getDataCase()) { case BLOBVALUE: throw new SQLiteException("unknown error (code 0): Unable to convert BLOB to long"); case LONGVALUE: return field.getLongValue(); @@ -193,7 +191,7 @@ public long getLong(int columnIndex) { @Override public double getDouble(int columnIndex) { - Sqlite.SqlValue field = getFieldAtIndex(columnIndex); + anki.ankidroid.SqlValue field = getFieldAtIndex(columnIndex); switch (field.getDataCase()) { case BLOBVALUE: throw new SQLiteException("unknown error (code 0): Unable to convert BLOB to double"); case LONGVALUE: return field.getLongValue(); @@ -219,13 +217,13 @@ public float getFloat(int columnIndex) { return (float) getDouble(columnIndex); } - private boolean isNull(Sqlite.SqlValue field) { - return field.getDataCase() == Sqlite.SqlValue.DataCase.DATA_NOT_SET; + private boolean isNull(anki.ankidroid.SqlValue field) { + return field.getDataCase() == anki.ankidroid.SqlValue.DataCase.DATA_NOT_SET; } @Override public boolean isNull(int columnIndex) { - Sqlite.SqlValue field = getFieldAtIndex(columnIndex); + anki.ankidroid.SqlValue field = getFieldAtIndex(columnIndex); return isNull(field); } @@ -242,7 +240,7 @@ public boolean isClosed() { @Override public int getType(int columnIndex) { - Sqlite.SqlValue field = getFieldAtIndex(columnIndex); + anki.ankidroid.SqlValue field = getFieldAtIndex(columnIndex); switch (field.getDataCase()) { case BLOBVALUE: return FIELD_TYPE_BLOB; case LONGVALUE: return FIELD_TYPE_INTEGER; @@ -253,8 +251,8 @@ public int getType(int columnIndex) { } } - protected Sqlite.Row getRowAtCurrentPosition() { - Sqlite.DBResult result = results.getResult(); + protected anki.ankidroid.Row getRowAtCurrentPosition() { + anki.ankidroid.DbResult result = results.getResult(); int rowCount = getCurrentSliceRowCount(); if (positionInSlice < 0 || positionInSlice >= rowCount) { throw new CursorIndexOutOfBoundsException(String.format(Locale.ROOT, "Index %d requested, with a size of %d", positionInSlice, rowCount)); @@ -262,7 +260,7 @@ protected Sqlite.Row getRowAtCurrentPosition() { return result.getRows(positionInSlice); } - private Sqlite.SqlValue getFieldAtIndex(int columnIndex) { + private anki.ankidroid.SqlValue getFieldAtIndex(int columnIndex) { return getRowAtCurrentPosition().getFields(columnIndex); } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.java index 24c1bbe6e..e7fb6959f 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.java @@ -18,10 +18,8 @@ import net.ankiweb.rsdroid.BackendException; -import BackendProto.Backend; - public class BackendDeckIsFilteredException extends BackendException { - public BackendDeckIsFilteredException(Backend.BackendError error) { + public BackendDeckIsFilteredException(anki.backend.BackendError error) { super(error); } } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.java index ff6eedda3..59ac7ddc0 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.java @@ -18,13 +18,11 @@ import net.ankiweb.rsdroid.BackendException; -import BackendProto.Backend; - /** * TODO: Document this */ public class BackendExistingException extends BackendException { - public BackendExistingException(Backend.BackendError error) { + public BackendExistingException(anki.backend.BackendError error) { super(error); } } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.java index 6eb47a3fb..a2c060ceb 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.java @@ -18,10 +18,8 @@ import net.ankiweb.rsdroid.BackendException; -import BackendProto.Backend; - public class BackendInterruptedException extends BackendException { - public BackendInterruptedException(Backend.BackendError error) { + public BackendInterruptedException(anki.backend.BackendError error) { super(error); } } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.java index 3d420ee34..61844affb 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.java @@ -18,8 +18,6 @@ import net.ankiweb.rsdroid.BackendException; -import BackendProto.Backend; - /** * A lot of exceptions get converted to Invalid Input when returned: * @@ -28,11 +26,11 @@ * SearchError */ public class BackendInvalidInputException extends BackendException { - public BackendInvalidInputException(Backend.BackendError error) { + public BackendInvalidInputException(anki.backend.BackendError error) { super(error); } - public static BackendInvalidInputException fromInvalidInputError(Backend.BackendError error) { + public static BackendInvalidInputException fromInvalidInputError(anki.backend.BackendError error) { switch (error.getLocalized()) { case "CollectionAlreadyOpen": return new BackendCollectionAlreadyOpenException(error); case "CollectionNotOpen": return new BackendCollectionNotOpenException(error); @@ -43,19 +41,19 @@ public static BackendInvalidInputException fromInvalidInputError(Backend.Backend } public static class BackendCollectionAlreadyOpenException extends BackendInvalidInputException { - public BackendCollectionAlreadyOpenException(Backend.BackendError error) { + public BackendCollectionAlreadyOpenException(anki.backend.BackendError error) { super(error); } } public static class BackendCollectionNotOpenException extends BackendInvalidInputException { - public BackendCollectionNotOpenException(Backend.BackendError error) { + public BackendCollectionNotOpenException(anki.backend.BackendError error) { super(error); } } public static class BackendSearchException extends BackendInvalidInputException { - public BackendSearchException(Backend.BackendError error) { + public BackendSearchException(anki.backend.BackendError error) { super(error); } } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.java index 5295a7e08..b79925781 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.java @@ -18,10 +18,8 @@ import net.ankiweb.rsdroid.BackendException; -import BackendProto.Backend; - public class BackendIoException extends BackendException { - public BackendIoException(Backend.BackendError error) { + public BackendIoException(anki.backend.BackendError error) { super(error); } } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.java index 55845ff30..8c8fafa24 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.java @@ -18,10 +18,8 @@ import net.ankiweb.rsdroid.BackendException; -import BackendProto.Backend; - public class BackendJsonException extends BackendException { - public BackendJsonException(Backend.BackendError error) { + public BackendJsonException(anki.backend.BackendError error) { super(error); } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.java index 26127abff..056da35cd 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.java @@ -17,47 +17,47 @@ package net.ankiweb.rsdroid.exceptions; import net.ankiweb.rsdroid.BackendException; - -import BackendProto.Backend; +import net.ankiweb.rsdroid.database.NotImplementedException; public class BackendNetworkException extends BackendException { - public BackendNetworkException(Backend.BackendError error) { + public BackendNetworkException(anki.backend.BackendError error) { super(error); } - public static BackendNetworkException fromNetworkError(Backend.BackendError error) { - - if (!error.hasNetworkError()) { - return new BackendNetworkException(error); - } - - Backend.NetworkError networkError = error.getNetworkError(); - - switch (networkError.getKind()) { - case OFFLINE: return new BackendNetworkOfflineException(error); - case TIMEOUT: return new BackendNetworkTimeoutException(error); - case PROXY_AUTH: return new BackendNetworkProxyAuthException(error); - case UNRECOGNIZED: - case OTHER: - } + public static BackendNetworkException fromNetworkError(anki.backend.BackendError error) { + throw new NotImplementedException(); - return new BackendNetworkException(error); +// if (!error.hasNetworkError()) { +// return new BackendNetworkException(error); +// } +// +// anki.backend.NetworkError networkError = error.getNetworkError(); +// +// switch (networkError.getKind()) { +// case OFFLINE: return new BackendNetworkOfflineException(error); +// case TIMEOUT: return new BackendNetworkTimeoutException(error); +// case PROXY_AUTH: return new BackendNetworkProxyAuthException(error); +// case UNRECOGNIZED: +// case OTHER: +// } +// +// return new BackendNetworkException(error); } public static class BackendNetworkOfflineException extends BackendNetworkException { - public BackendNetworkOfflineException(Backend.BackendError error) { + public BackendNetworkOfflineException(anki.backend.BackendError error) { super(error); } } public static class BackendNetworkTimeoutException extends BackendNetworkException { - public BackendNetworkTimeoutException(Backend.BackendError error) { + public BackendNetworkTimeoutException(anki.backend.BackendError error) { super(error); } } public static class BackendNetworkProxyAuthException extends BackendNetworkException { - public BackendNetworkProxyAuthException(Backend.BackendError error) { + public BackendNetworkProxyAuthException(anki.backend.BackendError error) { super(error); } } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.java index f26ab729f..5c746708c 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.java @@ -18,11 +18,9 @@ import net.ankiweb.rsdroid.BackendException; -import BackendProto.Backend; - /** An item was not found (example: a deck from backend.get_deck_legacy) */ public class BackendNotFoundException extends BackendException { - public BackendNotFoundException(Backend.BackendError error) { + public BackendNotFoundException(anki.backend.BackendError error) { super(error); } } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.java index 5ebe4d8e5..88f9b3c90 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.java @@ -18,10 +18,8 @@ import net.ankiweb.rsdroid.BackendException; -import BackendProto.Backend; - public class BackendProtoException extends BackendException { - public BackendProtoException(Backend.BackendError error) { + public BackendProtoException(anki.backend.BackendError error) { super(error); } } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.java index c59d49209..4ca1b2d9d 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.java @@ -18,101 +18,17 @@ import net.ankiweb.rsdroid.BackendException; -import BackendProto.Backend; - public class BackendSyncException extends BackendException { - public BackendSyncException(Backend.BackendError error) { + public BackendSyncException(anki.backend.BackendError error) { super(error); } - - public static BackendSyncException fromSyncError(Backend.BackendError error) { - - switch (error.getSyncError().getKind()) { - case CONFLICT: - throw new BackendSyncConflictException(error); - case AUTH_FAILED: - throw new BackendSyncAuthFailedException(error); - case SERVER_ERROR: - throw new BackendSyncServerErrorException(error); - case UNRECOGNIZED: - throw new BackendSyncUnrecognizedException(error); - case CLIENT_TOO_OLD: - throw new BackendSyncClientTooOldException(error); - case SERVER_MESSAGE: - throw new BackendSyncServerMessageException(error); - case CLOCK_INCORRECT: - throw new BackendSyncClockIncorrectException(error); - case RESYNC_REQUIRED: - throw new BackendSyncResyncRequiredException(error); - case MEDIA_CHECK_REQUIRED: - throw new BackendSyncMediaCheckRequiredException(error); - case DATABASE_CHECK_REQUIRED: - throw new BackendSyncDatabaseCheckRequiredException(error); - case OTHER: - default: - throw new BackendSyncException(error); - } - } - - public static class BackendSyncConflictException extends BackendSyncException { - public BackendSyncConflictException(Backend.BackendError error) { - super(error); - } - } - public static class BackendSyncAuthFailedException extends BackendSyncException { - public BackendSyncAuthFailedException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncServerErrorException extends BackendSyncException { - public BackendSyncServerErrorException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncUnrecognizedException extends BackendSyncException { - public BackendSyncUnrecognizedException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncClientTooOldException extends BackendSyncException { - public BackendSyncClientTooOldException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncClockIncorrectException extends BackendSyncException { - public BackendSyncClockIncorrectException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncServerMessageException extends BackendSyncException { - public BackendSyncServerMessageException(Backend.BackendError error) { + public BackendSyncAuthFailedException(anki.backend.BackendError error) { super(error); } } - public static class BackendSyncResyncRequiredException extends BackendSyncException { - public BackendSyncResyncRequiredException(Backend.BackendError error) { - super(error); - } - } - - public static class BackendSyncMediaCheckRequiredException extends BackendSyncException { - public BackendSyncMediaCheckRequiredException(Backend.BackendError error) { - super(error); - } - } - public static class BackendSyncDatabaseCheckRequiredException extends BackendSyncException { - public BackendSyncDatabaseCheckRequiredException(Backend.BackendError error) { - super(error); - } - } } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.java index 8f16012da..7f6563423 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.java @@ -18,14 +18,12 @@ import net.ankiweb.rsdroid.BackendException; -import BackendProto.Backend; - public class BackendTemplateException extends BackendException { - public BackendTemplateException(Backend.BackendError error) { + public BackendTemplateException(anki.backend.BackendError error) { super(error); } - public static BackendTemplateException fromTemplateError(Backend.BackendError error) { + public static BackendTemplateException fromTemplateError(anki.backend.BackendError error) { if (error.getLocalized() == null) { return new BackendTemplateException(error); @@ -39,7 +37,7 @@ public static BackendTemplateException fromTemplateError(Backend.BackendError er } public static class BackendTemplateSaveException extends BackendTemplateException { - public BackendTemplateSaveException(Backend.BackendError error) { + public BackendTemplateSaveException(anki.backend.BackendError error) { super(error); } } diff --git a/rslib-bridge/Cargo.lock b/rslib-bridge/Cargo.lock index 184f651a9..b37833a69 100644 --- a/rslib-bridge/Cargo.lock +++ b/rslib-bridge/Cargo.lock @@ -3,60 +3,80 @@ version = 3 [[package]] -name = "addr2line" -version = "0.13.0" +name = "adler" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b6a2d3371669ab3ca9797670853d61402b03d0b4b9ebf33d677dfa720203072" -dependencies = [ - "gimli", -] +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" [[package]] -name = "adler" -version = "0.2.3" +name = "ahash" +version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2a4ec343196209d6594e19543ae87a39f96d5534d7174822a3ad825dd6ed7e" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] [[package]] name = "aho-corasick" -version = "0.7.13" +version = "0.7.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "043164d8ba5c4c3035fec9bbee8647c0261d788f3474306f93bb65901cae0e86" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" dependencies = [ "memchr", ] +[[package]] +name = "ammonia" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5ed2509ee88cc023cccee37a6fab35826830fe8b748b3869790e7720c2c4a74" +dependencies = [ + "html5ever", + "maplit", + "once_cell", + "tendril", + "url", +] + [[package]] name = "anki" -version = "2.1.34" +version = "0.0.0" dependencies = [ - "askama", - "async-compression", + "ammonia", + "anki_i18n", + "async-trait", "blake3", - "bytes", + "bytes 1.1.0", "chrono", "coarsetime", - "failure", "flate2", "fluent", - "fluent-syntax", + "fluent-bundle", + "fnv", "futures", "hex", "htmlescape", - "hyper", + "id_tree", "intl-memoizer", - "itertools 0.9.0", + "itertools", "lazy_static", "nom", - "num-format", "num-integer", + "num_cpus", "num_enum", "once_cell", + "pct-str", "pin-project", + "proc-macro-nested", "prost", "prost-build", + "pulldown-cmark", "rand", "regex", + "reqwest", "rusqlite", "scopeguard", "serde", @@ -70,13 +90,33 @@ dependencies = [ "slog-async", "slog-envlogger", "slog-term", + "strum", "tempfile", "tokio", + "tokio-util 0.6.10", "unic-langid", + "unic-ucd-category", "unicase", "unicode-normalization", "utime", "zip", + "zstd", +] + +[[package]] +name = "anki_i18n" +version = "0.0.0" +dependencies = [ + "fluent", + "fluent-bundle", + "fluent-syntax", + "inflections", + "intl-memoizer", + "num-format", + "phf", + "serde", + "serde_json", + "unic-langid", ] [[package]] @@ -87,9 +127,9 @@ checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" [[package]] name = "arc-swap" -version = "0.4.8" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabe5a181f83789739c194cbe5a897dde195078fac08568d09221fd6137a7ba8" +checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" [[package]] name = "arrayref" @@ -113,63 +153,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" [[package]] -name = "askama" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70a6e7ebd44d0047fd48206c83c5cd3214acc7b9d87f001da170145c47ef7d12" -dependencies = [ - "askama_derive", - "askama_escape", - "askama_shared", -] - -[[package]] -name = "askama_derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1d7169690c4f56343dcd821ab834972a22570a2662a19a84fd7775d5e1c3881" -dependencies = [ - "askama_shared", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "askama_escape" -version = "0.10.1" +name = "arrayvec" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90c108c1a94380c89d2215d0ac54ce09796823cca0fd91b299cfff3b33e346fb" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" [[package]] -name = "askama_shared" -version = "0.10.4" +name = "async-trait" +version = "0.1.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62fc272363345c8cdc030e4c259d9d028237f8b057dc9bb327772a257bde6bb5" +checksum = "96cf8829f67d2eab0b2dfa42c5d0ef737e0724e4a82b01b3e292456202b19716" dependencies = [ - "askama_escape", - "humansize", - "nom", - "num-traits", - "percent-encoding", "proc-macro2", "quote", - "serde", "syn", - "toml", -] - -[[package]] -name = "async-compression" -version = "0.3.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9021768bcce77296b64648cc7a7460e3df99979b97ed5c925c38d1cc83778d98" -dependencies = [ - "bytes", - "flate2", - "futures-core", - "memchr", - "pin-project-lite", ] [[package]] @@ -180,7 +177,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" dependencies = [ "hermit-abi", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -189,25 +186,11 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" -[[package]] -name = "backtrace" -version = "0.3.50" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46254cf2fdcdf1badb5934448c1bcbe046a56537b3987d96c51a7afc5d03f293" -dependencies = [ - "addr2line", - "cfg-if 0.1.10", - "libc", - "miniz_oxide", - "object", - "rustc-demangle", -] - [[package]] name = "base64" -version = "0.11.0" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" @@ -216,31 +199,34 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" [[package]] -name = "blake2b_simd" -version = "0.5.10" +name = "blake3" +version = "1.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8fb2d74254a3a0b5cac33ac9f8ed0e44aa50378d9dbb2e5d83bd21ed1dc2c8a" +checksum = "a08e53fc5a564bb15bfe6fae56bd71522205f1f91893f9c0116edad6496c183f" dependencies = [ "arrayref", - "arrayvec 0.5.1", + "arrayvec 0.7.2", + "cc", + "cfg-if 1.0.0", "constant_time_eq", + "digest", ] [[package]] -name = "blake3" -version = "0.3.6" +name = "block-buffer" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce4f9586c9a3151c4b49b19e82ba163dd073614dd057e53c969e1a4db5b52720" +checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" dependencies = [ - "arrayref", - "arrayvec 0.5.1", - "cc", - "cfg-if 0.1.10", - "constant_time_eq", - "crypto-mac", - "digest", + "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" + [[package]] name = "byteorder" version = "1.3.4" @@ -253,11 +239,20 @@ version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +[[package]] +name = "bytes" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" + [[package]] name = "cc" -version = "1.0.58" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9a06fb2e53271d7c279ec1efea6ab691c35a2ae67ec0d91d7acec0caf13b518" +checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +dependencies = [ + "jobserver", +] [[package]] name = "cesu8" @@ -279,23 +274,27 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.15" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ + "libc", "num-integer", "num-traits", - "time", + "time 0.1.43", + "winapi", ] [[package]] name = "coarsetime" -version = "0.1.14" -source = "git+https://github.com/ankitects/rust-coarsetime.git?branch=old-mac-compat#f9e2c86216f0f4803bc75404828318fc206dab29" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "454038500439e141804c655b4cd1bc6a70bcb95cd2bc9463af5661b6956f0e46" dependencies = [ - "lazy_static", "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "once_cell", + "wasi 0.11.0+wasi-snapshot-preview1", + "wasm-bindgen", ] [[package]] @@ -304,7 +303,7 @@ version = "4.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8e5ef862b2df927249f4e2bdc29c1bd13a33105f900884b0c32acdf32aff584" dependencies = [ - "bytes", + "bytes 0.5.6", "memchr", ] @@ -314,6 +313,22 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" +[[package]] +name = "core-foundation" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "194a7a9e6de53fa55116934067c844d9d749312f75c6f6d0980e8c252f8c2146" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" + [[package]] name = "crc32fast" version = "1.2.0" @@ -330,11 +345,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" dependencies = [ "cfg-if 0.1.10", - "crossbeam-channel", + "crossbeam-channel 0.4.4", "crossbeam-deque", "crossbeam-epoch", "crossbeam-queue", - "crossbeam-utils", + "crossbeam-utils 0.7.2", ] [[package]] @@ -343,10 +358,20 @@ version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" dependencies = [ - "crossbeam-utils", + "crossbeam-utils 0.7.2", "maybe-uninit", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" +dependencies = [ + "cfg-if 1.0.0", + "crossbeam-utils 0.8.8", +] + [[package]] name = "crossbeam-deque" version = "0.7.4" @@ -354,7 +379,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" dependencies = [ "crossbeam-epoch", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "maybe-uninit", ] @@ -366,7 +391,7 @@ checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" dependencies = [ "autocfg", "cfg-if 0.1.10", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "lazy_static", "maybe-uninit", "memoffset", @@ -380,7 +405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" dependencies = [ "cfg-if 0.1.10", - "crossbeam-utils", + "crossbeam-utils 0.7.2", "maybe-uninit", ] @@ -396,91 +421,79 @@ dependencies = [ ] [[package]] -name = "crypto-mac" -version = "0.8.0" +name = "crossbeam-utils" +version = "0.8.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" dependencies = [ - "generic-array", - "subtle", + "cfg-if 1.0.0", + "lazy_static", ] [[package]] -name = "derivative" -version = "2.1.1" +name = "crypto-common" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb582b60359da160a9477ee80f15c8d784c477e69c217ef2cdd4169c24ea380f" +checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8" dependencies = [ - "proc-macro2", - "quote", - "syn", + "generic-array", + "typenum", ] [[package]] name = "digest" -version = "0.9.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506" dependencies = [ - "generic-array", + "block-buffer", + "crypto-common", + "subtle", ] [[package]] -name = "dirs" -version = "2.0.2" +name = "dirs-next" +version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13aea89a5c93364a98e9b37b2fa237effbb694d5cfe01c5b70941f7eb087d5e3" +checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if 0.1.10", - "dirs-sys", + "cfg-if 1.0.0", + "dirs-sys-next", ] [[package]] -name = "dirs-sys" -version = "0.3.5" +name = "dirs-sys-next" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e93d7f5705de3e49895a2b5e0b8855a1c27f080192ae9c32a6432d50741a57a" +checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d" dependencies = [ "libc", "redox_users", - "winapi 0.3.9", + "winapi", ] [[package]] name = "either" -version = "1.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd56b59865bce947ac5958779cfa508f6c3b9497cc762b7e24a12d11ccde2c4f" - -[[package]] -name = "error-chain" -version = "0.12.4" +version = "1.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" -dependencies = [ - "version_check", -] +checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] -name = "failure" -version = "0.1.8" +name = "encoding_rs" +version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d32e9bd16cc02eae7db7ef620b392808b89f6a5e16bb3497d159c6b92a0f4f86" +checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" dependencies = [ - "backtrace", - "failure_derive", + "cfg-if 1.0.0", ] [[package]] -name = "failure_derive" -version = "0.1.8" +name = "error-chain" +version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa4da3c766cd7a0db8242e326e9e4e081edd567072893ed320008189715366a4" +checksum = "2d2f06b9cac1506ece98fe3231e3cc9c4410ec3d5b1f24ae1c8946f0742cdefc" dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", + "version_check", ] [[package]] @@ -495,28 +508,36 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastrand" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +dependencies = [ + "instant", +] + [[package]] name = "fixedbitset" -version = "0.2.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37ab347416e802de484e4d03c7316c48f1ecb56574dfd4a46a80f173ce1de04d" +checksum = "279fb028e20b3c4c320317955b77c5e0c9701f05a1d309905d6fc702cdc5053e" [[package]] name = "flate2" -version = "1.0.16" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68c90b0fc46cf89d227cc78b40e494ff81287a92dd07631e5af0d06fe3cf885e" +checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" dependencies = [ - "cfg-if 0.1.10", "crc32fast", - "libc", "miniz_oxide", ] [[package]] name = "fluent" -version = "0.10.2" -source = "git+https://github.com/ankitects/fluent-rs.git?branch=32bit-panic#f61c5e10a53161ef5261f3c87b62047f12e4aa74" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f69378194459db76abd2ce3952b790db103ceb003008d3d50d97c41ff847a7" dependencies = [ "fluent-bundle", "unic-langid", @@ -524,32 +545,37 @@ dependencies = [ [[package]] name = "fluent-bundle" -version = "0.10.2" -source = "git+https://github.com/ankitects/fluent-rs.git?branch=32bit-panic#f61c5e10a53161ef5261f3c87b62047f12e4aa74" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e242c601dec9711505f6d5bbff5bedd4b61b2469f2e8bb8e57ee7c9747a87ffd" dependencies = [ "fluent-langneg", "fluent-syntax", "intl-memoizer", "intl_pluralrules", - "rental", + "rustc-hash", + "self_cell", "smallvec", "unic-langid", ] [[package]] name = "fluent-langneg" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe5815efd5542e40841cd34ef9003822352b04c67a70c595c6758597c72e1f56" +checksum = "2c4ad0989667548f06ccd0e306ed56b61bd4d35458d54df5ec7587c0e8ed5e94" dependencies = [ "unic-langid", ] [[package]] name = "fluent-syntax" -version = "0.9.3" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac0f7e83d14cccbf26e165d8881dcac5891af0d85a88543c09dd72ebd31d91ba" +checksum = "c0abed97648395c902868fee9026de96483933faa54ea3b40d652f7dfe61ca78" +dependencies = [ + "thiserror", +] [[package]] name = "fnv" @@ -558,26 +584,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" [[package]] -name = "fuchsia-zircon" -version = "0.3.3" +name = "form_urlencoded" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" +checksum = "5fc25a87fa4fd2094bffb06925852034d90a17f0d1e05197d4956d3555752191" dependencies = [ - "bitflags", - "fuchsia-zircon-sys", + "matches", + "percent-encoding", ] [[package]] -name = "fuchsia-zircon-sys" -version = "0.3.3" +name = "futf" +version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" +checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" +dependencies = [ + "mac", + "new_debug_unreachable", +] [[package]] name = "futures" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e05b85ec287aac0dc34db7d4a569323df697f9c55b99b15d6b4ef8cde49f613" +checksum = "f73fe65f54d1e12b726f517d3e2135ca3125a437b6d998caf1962961f7172d9e" dependencies = [ "futures-channel", "futures-core", @@ -590,9 +620,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f366ad74c28cca6ba456d95e6422883cfb4b252a83bed929c83abfdbbf2967d5" +checksum = "c3083ce4b914124575708913bca19bfe887522d6e2e6d0952943f5eac4a74010" dependencies = [ "futures-core", "futures-sink", @@ -600,15 +630,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59f5fff90fd5d971f936ad674802482ba441b6f09ba5e15fd8b39145582ca399" +checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3" [[package]] name = "futures-executor" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10d6bb888be1153d3abeb9006b11b02cf5e9b209fda28693c31ae1e4e012e314" +checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6" dependencies = [ "futures-core", "futures-task", @@ -617,17 +647,16 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de27142b013a8e869c14957e6d2edeef89e97c289e69d042ee3a49acd8b51789" +checksum = "fc4045962a5a5e935ee2fdedaa4e08284547402885ab326734432bed5d12966b" [[package]] name = "futures-macro" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0b5a30a4328ab5473878237c447333c093297bded83a4983d10f4deea240d39" +checksum = "33c1e13800337f4d4d7a316bf45a567dbcb6ffe087f16424852d97e97a91f512" dependencies = [ - "proc-macro-hack", "proc-macro2", "quote", "syn", @@ -635,9 +664,9 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f2032893cb734c7a05d85ce0cc8b8c4075278e93b24b66f9de99d6eb0fa8acc" +checksum = "21163e139fa306126e6eedaf49ecdb4588f939600f0b1e770f4205ee4b7fa868" [[package]] name = "futures-task" @@ -647,9 +676,9 @@ checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a" [[package]] name = "futures-util" -version = "0.3.5" +version = "0.3.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8764574ff08b701a084482c3c7031349104b07ac897393010494beaa18ce32c6" +checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a" dependencies = [ "futures-channel", "futures-core", @@ -658,22 +687,11 @@ dependencies = [ "futures-sink", "futures-task", "memchr", - "pin-project", + "pin-project-lite", "pin-utils", - "proc-macro-hack", - "proc-macro-nested", "slab", ] -[[package]] -name = "fxhash" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" -dependencies = [ - "byteorder", -] - [[package]] name = "generic-array" version = "0.14.4" @@ -685,29 +703,32 @@ dependencies = [ ] [[package]] -name = "getrandom" -version = "0.1.14" +name = "getopts" +version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7abc8dd8451921606d809ba32e95b6111925cd2906060d2dcc29c070220503eb" +checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" dependencies = [ - "cfg-if 0.1.10", - "libc", - "wasi 0.9.0+wasi-snapshot-preview1", + "unicode-width", ] [[package]] -name = "gimli" -version = "0.22.0" +name = "getrandom" +version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aaf91faf136cb47367fa430cd46e37a788775e7fa104f8b4bcb3861dc389b724" +checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "wasi 0.10.0+wasi-snapshot-preview1", +] [[package]] name = "h2" -version = "0.2.6" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993f9e0baeed60001cf565546b0d3dbe6a6ad23f2bd31644a133c641eccf6d53" +checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ - "bytes", + "bytes 1.1.0", "fnv", "futures-core", "futures-sink", @@ -716,17 +737,26 @@ dependencies = [ "indexmap", "slab", "tokio", - "tokio-util", + "tokio-util 0.7.3", "tracing", ] [[package]] name = "hashbrown" -version = "0.8.2" +version = "0.11.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e91b62f79061a0bc2e046024cb7ba44b08419ed238ecbd9adbd787434b9e8c25" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" dependencies = [ - "autocfg", + "ahash", +] + +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown", ] [[package]] @@ -749,9 +779,23 @@ dependencies = [ [[package]] name = "hex" -version = "0.4.2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "html5ever" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" +dependencies = [ + "log", + "mac", + "markup5ever", + "proc-macro2", + "quote", + "syn", +] [[package]] name = "htmlescape" @@ -765,40 +809,41 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" dependencies = [ - "bytes", + "bytes 0.5.6", "fnv", - "itoa", + "itoa 0.4.6", ] [[package]] name = "http-body" -version = "0.3.1" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" +checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes", + "bytes 1.1.0", "http", + "pin-project-lite", ] [[package]] name = "httparse" -version = "1.3.4" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd179ae861f0c2e53da70d892f5f3029f9594be0c41dc5269cd371691b1dc2f9" +checksum = "496ce29bb5a52785b44e0f7ca2847ae0bb839c9bd28f69acac9b99d461c0c04c" [[package]] -name = "humansize" -version = "1.1.0" +name = "httpdate" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e" +checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.13.7" +version = "0.14.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e68a8dd9716185d9e64ea473ea6ef63529252e3e27623295a0378a19665d5eb" +checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" dependencies = [ - "bytes", + "bytes 1.1.0", "futures-channel", "futures-core", "futures-util", @@ -806,10 +851,10 @@ dependencies = [ "http", "http-body", "httparse", - "itoa", - "pin-project", + "httpdate", + "itoa 1.0.2", + "pin-project-lite", "socket2", - "time", "tokio", "tower-service", "tracing", @@ -817,66 +862,107 @@ dependencies = [ ] [[package]] -name = "indexmap" -version = "1.5.1" +name = "hyper-rustls" +version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86b45e59b16c76b11bf9738fd5d38879d3bd28ad292d7b313608becb17ae2df9" +checksum = "5f9f7a97316d44c0af9b0301e65010573a853a9fc97046d7331d7f6bc0fd5a64" dependencies = [ - "autocfg", - "hashbrown", + "futures-util", + "hyper", + "log", + "rustls", + "tokio", + "tokio-rustls", + "webpki", ] [[package]] -name = "intl-memoizer" -version = "0.3.0" -source = "git+https://github.com/ankitects/fluent-rs.git?branch=32bit-panic#f61c5e10a53161ef5261f3c87b62047f12e4aa74" +name = "hyper-timeout" +version = "0.4.1" +source = "git+https://github.com/ankitects/hyper-timeout.git?rev=0cb6f7d14c62819e37cd221736f8b0555e823712#0cb6f7d14c62819e37cd221736f8b0555e823712" dependencies = [ - "type-map", - "unic-langid", + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", ] [[package]] -name = "intl_pluralrules" -version = "6.0.0" +name = "id_tree" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d82c14d8eece42c03353e0ce86a4d3f97b1f1cef401e4d962dca6c6214a85002" +checksum = "bcd9db8dd5be8bde5a2624ed4b2dfb74368fe7999eb9c4940fd3ca344b61071a" dependencies = [ - "tinystr", - "unic-langid", + "snowflake", ] [[package]] -name = "iovec" -version = "0.1.4" +name = "idna" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" +checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" dependencies = [ - "libc", + "matches", + "unicode-bidi", + "unicode-normalization", ] [[package]] -name = "itertools" -version = "0.8.2" +name = "indexmap" +version = "1.8.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f56a2d0bc861f9165be4eb3442afd3c236d8a98afd426f65d92324ae1091a484" +checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" dependencies = [ - "either", + "autocfg", + "hashbrown", ] [[package]] -name = "itertools" -version = "0.9.0" +name = "inflections" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a257582fdcde896fd96463bf2d40eefea0580021c0712a0e2b028b60b47a837a" + +[[package]] +name = "instant" +version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "284f18f85651fe11e8a991b2adb42cb078325c996ed026d994719efcfca1d54b" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "either", + "cfg-if 1.0.0", +] + +[[package]] +name = "intl-memoizer" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c310433e4a310918d6ed9243542a6b83ec1183df95dff8f23f87bb88a264a66f" +dependencies = [ + "type-map", + "unic-langid", +] + +[[package]] +name = "intl_pluralrules" +version = "7.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b18f988384267d7066cc2be425e6faf352900652c046b6971d2e228d3b1c5ecf" +dependencies = [ + "tinystr", + "unic-langid", ] +[[package]] +name = "ipnet" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" + [[package]] name = "itertools" -version = "0.10.0" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d572918e350e82412fe766d24b15e6682fb2ed2bbe018280caa810397cb319" +checksum = "a9a9d19fa1e79b6215ff29b9d6880b706147f16e9b1dbb1e4e5947b5b02bc5e3" dependencies = [ "either", ] @@ -887,6 +973,12 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +[[package]] +name = "itoa" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "112c678d4050afce233f4f2852bb2eb519230b3cf12f33585275537d7e41578d" + [[package]] name = "jni" version = "0.17.0" @@ -908,13 +1000,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] -name = "kernel32-sys" -version = "0.2.2" +name = "jobserver" +version = "0.1.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af25a77299a7f711a01975c35a6a424eb6862092cc2d6c72c4ed6cbc56dfc1fa" +dependencies = [ + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" +checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "wasm-bindgen", ] [[package]] @@ -938,15 +1038,15 @@ dependencies = [ [[package]] name = "libc" -version = "0.2.76" +version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "755456fae044e6fa1ebbbd1b3e902ae19e73097ed4ed87bb79934a867c007bc3" +checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" [[package]] name = "libsqlite3-sys" -version = "0.18.0" +version = "0.23.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e704a02bcaecd4a08b93a23f6be59d0bd79cd161e0963e9499165a0a35df7bd" +checksum = "d2cafc7c74096c336d9d27145f7ebd4f4b6f95ba16aa5a282387267e6925cb58" dependencies = [ "cc", "pkg-config", @@ -954,10 +1054,13 @@ dependencies = [ ] [[package]] -name = "linked-hash-map" -version = "0.5.3" +name = "lock_api" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8dd5a6d5999d9907cda8ed67bbd137d3af8085216c2ac62de5be860bd41f304a" +checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +dependencies = [ + "scopeguard", +] [[package]] name = "log" @@ -969,14 +1072,37 @@ dependencies = [ ] [[package]] -name = "lru-cache" -version = "0.1.2" +name = "mac" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" + +[[package]] +name = "maplit" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d" + +[[package]] +name = "markup5ever" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31e24f1ad8321ca0e8a1e0ac13f23cb668e6f5466c2c57319f6a5cf1cc8e3b1c" +checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" dependencies = [ - "linked-hash-map", + "log", + "phf", + "phf_codegen", + "string_cache", + "string_cache_codegen", + "tendril", ] +[[package]] +name = "matches" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" + [[package]] name = "maybe-uninit" version = "2.0.0" @@ -985,9 +1111,9 @@ checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" [[package]] name = "memchr" -version = "2.3.3" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3728d817d99e5ac407411fa471ff9800a778d88a24685968b36824eaf4bee400" +checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" [[package]] name = "memoffset" @@ -999,43 +1125,46 @@ dependencies = [ ] [[package]] -name = "miniz_oxide" -version = "0.4.0" +name = "mime" +version = "0.3.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" + +[[package]] +name = "mime_guess" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be0f75932c1f6cfae3c04000e40114adf955636e19040f9c0a2c380702aa1c7f" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" dependencies = [ - "adler", + "mime", + "unicase", ] [[package]] -name = "mio" -version = "0.6.22" +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fce347092656428bc8eaf6201042cb551b8d67855af7374542a92a0fbfcac430" +checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" dependencies = [ - "cfg-if 0.1.10", - "fuchsia-zircon", - "fuchsia-zircon-sys", - "iovec", - "kernel32-sys", - "libc", - "log", - "miow", - "net2", - "slab", - "winapi 0.2.8", + "adler", ] [[package]] -name = "miow" -version = "0.2.2" +name = "mio" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" +checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" dependencies = [ - "kernel32-sys", - "net2", - "winapi 0.2.8", - "ws2_32-sys", + "libc", + "log", + "wasi 0.11.0+wasi-snapshot-preview1", + "windows-sys", ] [[package]] @@ -1045,15 +1174,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8883adfde9756c1d30b0f519c9b8c502a94b41ac62f696453c37c7fc0a958ce" [[package]] -name = "net2" -version = "0.2.37" +name = "new_debug_unreachable" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae" -dependencies = [ - "cfg-if 0.1.10", - "libc", - "winapi 0.3.9", -] +checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" [[package]] name = "nodrop" @@ -1063,13 +1187,12 @@ checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" [[package]] name = "nom" -version = "5.1.2" +version = "7.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb4262d26ed83a1c0a33a38fe2bb15797329c85770da05e6b828ddb782627af" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" dependencies = [ - "lexical-core", "memchr", - "version_check", + "minimal-lexical", ] [[package]] @@ -1079,14 +1202,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" dependencies = [ "arrayvec 0.4.12", - "itoa", + "itoa 0.4.6", ] [[package]] name = "num-integer" -version = "0.1.43" +version = "0.1.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d59457e662d541ba17869cf51cf177c0b5f0cbf476c66bdc90bf1edac4f875b" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" dependencies = [ "autocfg", "num-traits", @@ -1103,9 +1226,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", @@ -1113,19 +1236,18 @@ dependencies = [ [[package]] name = "num_enum" -version = "0.5.1" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "226b45a5c2ac4dd696ed30fa6b94b057ad909c7b7fc2e0d0808192bced894066" +checksum = "cf5395665662ef45796a4ff5486c5d41d29e0c09640af4c5f17fd94ee2c119c9" dependencies = [ - "derivative", "num_enum_derive", ] [[package]] name = "num_enum_derive" -version = "0.5.1" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c0fd9eba1d5db0994a239e09c1be402d35622277e35468ba891aa5e3188ce7e" +checksum = "3b0498641e53dd6ac1a4f22547548caa6864cc4933784319cd1775271c5a46ce" dependencies = [ "proc-macro-crate", "proc-macro2", @@ -1134,16 +1256,56 @@ dependencies = [ ] [[package]] -name = "object" -version = "0.20.0" +name = "num_threads" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" +checksum = "2819ce041d2ee131036f4fc9d6ae7ae125a3a40e97ba64d04fe799ad9dabbb44" +dependencies = [ + "libc", +] [[package]] name = "once_cell" -version = "1.5.2" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" + +[[package]] +name = "openssl-probe" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13bd41f508810a131401606d54ac32a467c97172d74ba7662562ebba5ad07fa0" +checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" +dependencies = [ + "cfg-if 1.0.0", + "libc", + "redox_syscall", + "smallvec", + "windows-sys", +] + +[[package]] +name = "pct-str" +version = "1.1.0" +source = "git+https://github.com/timothee-haudebourg/pct-str.git?rev=4adccd8d4a222ab2672350a102f06ae832a0572d#4adccd8d4a222ab2672350a102f06ae832a0572d" +dependencies = [ + "utf8-decode", +] [[package]] name = "percent-encoding" @@ -1153,28 +1315,82 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" [[package]] name = "petgraph" -version = "0.5.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "467d164a6de56270bd7c4d070df81d07beace25012d5103ced4e9ff08d6afdb7" +checksum = "e6d5014253a1331579ce62aa67443b4a658c5e7dd03d4bc6d302b94474888143" dependencies = [ "fixedbitset", "indexmap", ] +[[package]] +name = "phf" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" +dependencies = [ + "phf_macros", + "phf_shared", + "proc-macro-hack", +] + +[[package]] +name = "phf_codegen" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" +dependencies = [ + "phf_generator", + "phf_shared", +] + +[[package]] +name = "phf_generator" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" +dependencies = [ + "phf_shared", + "rand", +] + +[[package]] +name = "phf_macros" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" +dependencies = [ + "phf_generator", + "phf_shared", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "phf_shared" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" +dependencies = [ + "siphasher", +] + [[package]] name = "pin-project" -version = "0.4.23" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca4433fff2ae79342e497d9f8ee990d174071408f28f726d6d83af93e58e48aa" +checksum = "58ad3879ad3baf4e44784bc6a718a8698867bb991f8ce24d1bcbe2cfb4c3a75e" dependencies = [ "pin-project-internal", ] [[package]] name = "pin-project-internal" -version = "0.4.23" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c0e815c3ee9a031fdf5af21c10aa17c573c9c6a566328d99e3936c34e36461f" +checksum = "744b6f092ba29c3650faf274db506afd39944f48420f6c86b17cfe0ee1cb36bb" dependencies = [ "proc-macro2", "quote", @@ -1183,9 +1399,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.1.7" +version = "0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282adbf10f2698a7a77f8e983a74b2d18176c19a7fd32a45446139ae7b02b715" +checksum = "e0a7ae3ac2f1173085d398531c705756c94a4c56843785df85a60c1a0afac116" [[package]] name = "pin-utils" @@ -1195,15 +1411,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d36492546b6af1463394d46f0c834346f31548646f6ba10849802c9c9a27ac33" - -[[package]] -name = "podio" -version = "0.1.7" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b18befed8bc2b61abc79a457295e7e838417326da1586050b919414073977f19" +checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "ppv-lite86" @@ -1211,12 +1421,19 @@ version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" +[[package]] +name = "precomputed-hash" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" + [[package]] name = "proc-macro-crate" -version = "0.1.5" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d6ea3c4595b96363c13943497db34af4460fb474a95c43f4446ad341b8c9785" +checksum = "e17d47ce914bf4de440332250b0edd23ce48c005f59fab39d3335866b114f11a" dependencies = [ + "thiserror", "toml", ] @@ -1228,55 +1445,57 @@ checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" [[package]] name = "proc-macro-nested" -version = "0.1.6" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eba180dafb9038b050a4c280019bbedf9f2467b61e5d892dcad585bb57aadc5a" +checksum = "bc881b2c22681370c6a780e47af9840ef841837bc98118431d4e1868bd0c1086" [[package]] name = "proc-macro2" -version = "1.0.19" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "04f5f085b5d71e2188cb8271e5da0161ad52c3f227a661a3c135fdf28e258b12" +checksum = "c54b25569025b7fc9651de43004ae593a75ad88543b17178aa5e1b9c4f15f56f" dependencies = [ - "unicode-xid", + "unicode-ident", ] [[package]] name = "prost" -version = "0.6.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce49aefe0a6144a45de32927c77bd2859a5f7677b55f220ae5b744e87389c212" +checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ - "bytes", + "bytes 1.1.0", "prost-derive", ] [[package]] name = "prost-build" -version = "0.6.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02b10678c913ecbd69350e8535c3aef91a8676c0773fc1d7b95cdd196d7f2f26" +checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ - "bytes", + "bytes 1.1.0", "heck", - "itertools 0.8.2", + "itertools", + "lazy_static", "log", "multimap", "petgraph", "prost", "prost-types", + "regex", "tempfile", "which", ] [[package]] name = "prost-derive" -version = "0.6.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "537aa19b95acde10a12fec4301466386f757403de4cd4e5b4fa78fb5ecb18f72" +checksum = "f9cc1a3263e07e0bf68e96268f37665207b49560d98739662cdfaae215c720fe" dependencies = [ "anyhow", - "itertools 0.8.2", + "itertools", "proc-macro2", "quote", "syn", @@ -1284,14 +1503,26 @@ dependencies = [ [[package]] name = "prost-types" -version = "0.6.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1834f67c0697c001304b75be76f67add9c89742eda3a085ad8ee0bb38c3417aa" +checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ - "bytes", + "bytes 1.1.0", "prost", ] +[[package]] +name = "pulldown-cmark" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" +dependencies = [ + "bitflags", + "getopts", + "memchr", + "unicase", +] + [[package]] name = "quote" version = "1.0.7" @@ -1303,22 +1534,20 @@ dependencies = [ [[package]] name = "rand" -version = "0.7.3" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" dependencies = [ - "getrandom", "libc", "rand_chacha", "rand_core", - "rand_hc", ] [[package]] name = "rand_chacha" -version = "0.2.2" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" dependencies = [ "ppv-lite86", "rand_core", @@ -1326,56 +1555,49 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.5.1" +version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" dependencies = [ "getrandom", ] [[package]] -name = "rand_hc" -version = "0.2.0" +name = "redox_syscall" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" dependencies = [ - "rand_core", + "bitflags", ] -[[package]] -name = "redox_syscall" -version = "0.1.57" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41cc0f7e4d5d4544e8861606a285bb08d3e70712ccc7d2b84d7c0ccfaf4b05ce" - [[package]] name = "redox_users" -version = "0.3.4" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b23093265f8d200fa7b4c2c76297f47e681c655f6f1285a8780d6a022f7431" +checksum = "b033d837a7cf162d7993aded9304e30a83213c648b6e389db233191f891e5c2b" dependencies = [ "getrandom", "redox_syscall", - "rust-argon2", + "thiserror", ] [[package]] name = "regex" -version = "1.3.9" +version = "1.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c3780fcf44b193bc4d09f36d2a3c87b251da4a046c87795a0d35f4f927ad8e6" +checksum = "d83f127d94bdbcda4c8cc2e50f6f84f4b611f69c902699ca385a39c3a75f9ff1" dependencies = [ "aho-corasick", "memchr", "regex-syntax", - "thread_local", ] [[package]] name = "regex-syntax" -version = "0.6.18" +version = "0.6.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "26412eb97c6b088a6997e05f69403a802a92d520de2f8e63c2b65f9e0f47c4e8" +checksum = "49b3de9ec5dc0a3417da371aab17d729997c15010e7fd24ff707773a33bddb64" [[package]] name = "remove_dir_all" @@ -1383,28 +1605,61 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] -name = "rental" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8545debe98b2b139fb04cad8618b530e9b07c152d99a5de83c860b877d67847f" +name = "reqwest" +version = "0.11.3" +source = "git+https://github.com/ankitects/reqwest.git?rev=7591444614de02b658ddab125efba7b2bb4e2335#7591444614de02b658ddab125efba7b2bb4e2335" dependencies = [ - "rental-impl", - "stable_deref_trait", + "base64", + "bytes 1.1.0", + "encoding_rs", + "futures-core", + "futures-util", + "http", + "http-body", + "hyper", + "hyper-rustls", + "hyper-timeout", + "ipnet", + "js-sys", + "lazy_static", + "log", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "rustls", + "rustls-native-certs", + "serde", + "serde_json", + "serde_urlencoded", + "tokio", + "tokio-rustls", + "tokio-socks", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "webpki-roots", + "winreg", ] [[package]] -name = "rental-impl" -version = "0.5.5" +name = "ring" +version = "0.16.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "475e68978dc5b743f2f40d8e0a8fdc83f1c5e78cbf4b8fa5e74e73beebc340de" +checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" dependencies = [ - "proc-macro2", - "quote", - "syn", + "cc", + "libc", + "once_cell", + "spin", + "untrusted", + "web-sys", + "winapi", ] [[package]] @@ -1412,7 +1667,7 @@ name = "rsdroid" version = "0.1.0" dependencies = [ "anki", - "itertools 0.10.0", + "itertools", "jni", "lazy_static", "lexical-core", @@ -1427,37 +1682,55 @@ dependencies = [ [[package]] name = "rusqlite" -version = "0.23.1" +version = "0.26.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "45d0fd62e1df63d254714e6cb40d0a0e82e7a1623e7a27f679d851af092ae58b" +checksum = "4ba4d3462c8b2e4d7f4fcfcf2b296dc6b65404fbbc7b63daa37fd485c149daf7" dependencies = [ "bitflags", "fallible-iterator", "fallible-streaming-iterator", + "hashlink", "libsqlite3-sys", - "lru-cache", "memchr", "smallvec", - "time", ] [[package]] -name = "rust-argon2" -version = "0.7.0" +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustls" +version = "0.19.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2bc8af4bda8e1ff4932523b94d3dd20ee30a87232323eda55903ffd71d2fb017" +checksum = "35edb675feee39aec9c99fa5ff985081995a06d594114ae14cbe797ad7b7a6d7" dependencies = [ "base64", - "blake2b_simd", - "constant_time_eq", - "crossbeam-utils", + "log", + "ring", + "sct", + "webpki", ] [[package]] -name = "rustc-demangle" -version = "0.1.16" +name = "rustls-native-certs" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c691c0e608126e00913e33f0ccf3727d5fc84573623b8d65b2df340b5201783" +checksum = "5a07b7c1885bd8ed3831c289b7870b13ef46fe0e856d288c30d9cc17d75a2092" +dependencies = [ + "openssl-probe", + "rustls", + "schannel", + "security-framework", +] + +[[package]] +name = "rustversion" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" @@ -1474,38 +1747,86 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "schannel" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88d6731146462ea25d9244b2ed5fd1d716d25c52e4d54aa4fb0f3c4e9854dbe2" +dependencies = [ + "lazy_static", + "windows-sys", +] + [[package]] name = "scopeguard" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" +[[package]] +name = "sct" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b362b83898e0e69f38515b82ee15aa80636befe47c3b6d3d89a911e78fc228ce" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "security-framework" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0160a13a177a45bfb43ce71c01580998474f556ad854dcbca936dd2841a5c556" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "self_cell" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ef965a420fe14fdac7dd018862966a4c14094f900e1650bbc71ddd7d580c8af" + [[package]] name = "serde" -version = "1.0.115" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54c9a88f2da7238af84b5101443f0c0d0a3bbdc455e34a5c9497b1903ed55d5" +checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" dependencies = [ "serde_derive", ] [[package]] name = "serde-aux" -version = "0.6.1" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae50f53d4b01e854319c1f5b854cd59471f054ea7e554988850d3f36ca1dc852" +checksum = "93abf9799c576f004252b2a05168d58527fb7c54de12e94b4d12fe3475ffad24" dependencies = [ "chrono", "serde", - "serde_derive", "serde_json", ] [[package]] name = "serde_derive" -version = "1.0.115" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "609feed1d0a73cc36a0182a840a9b37b4a82f0b1150369f0536a9e3f2a31dc48" +checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" dependencies = [ "proc-macro2", "quote", @@ -1514,20 +1835,20 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.57" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164eacbdb13512ec2745fb09d51fd5b22b0d65ed294a1dcf7285a360c80a675c" +checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c" dependencies = [ - "itoa", + "itoa 1.0.2", "ryu", "serde", ] [[package]] name = "serde_repr" -version = "0.1.6" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dc6b7951b17b051f3210b063f12cc17320e2fe30ae05b0fe2a3abb068551c76" +checksum = "a2ad84e47328a31223de7fed7a4f5087f2d6ddfe586cf3ca25b7a165bc0a5aed" dependencies = [ "proc-macro2", "quote", @@ -1555,12 +1876,30 @@ dependencies = [ "syn", ] +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa 1.0.2", + "ryu", + "serde", +] + [[package]] name = "sha1" version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +[[package]] +name = "siphasher" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" + [[package]] name = "slab" version = "0.4.2" @@ -1569,17 +1908,17 @@ checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" [[package]] name = "slog" -version = "2.5.2" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1cc9c640a4adbfbcc11ffb95efe5aa7af7309e002adab54b185507dbf2377b99" +checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" [[package]] name = "slog-async" -version = "2.5.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b3336ce47ce2f96673499fc07eb85e3472727b9a7a2959964b002c2ce8fbbb" +checksum = "766c59b252e62a34651412870ff55d8c4e6d04df19b43eecb2703e417b097ffe" dependencies = [ - "crossbeam-channel", + "crossbeam-channel 0.5.4", "slog", "take_mut", "thread_local", @@ -1625,15 +1964,15 @@ dependencies = [ [[package]] name = "slog-term" -version = "2.6.0" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab1d807cf71129b05ce36914e1dbb6fbfbdecaf686301cb457f4fa967f9f5b6" +checksum = "87d29185c55b7b258b4f120eab00f48557d4d9bc814f41713f449d35b0f8977c" dependencies = [ "atty", - "chrono", "slog", "term", "thread_local", + "time 0.3.9", ] [[package]] @@ -1642,22 +1981,27 @@ version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +[[package]] +name = "snowflake" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27207bb65232eda1f588cf46db2fee75c0808d557f6b3cf19a75f5d6d7c94df1" + [[package]] name = "socket2" -version = "0.3.19" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" +checksum = "66d72b759436ae32898a2af0a14218dbf55efde3feeb170eb623637db85ee1e0" dependencies = [ - "cfg-if 1.0.0", "libc", - "winapi 0.3.9", + "winapi", ] [[package]] -name = "stable_deref_trait" -version = "1.2.0" +name = "spin" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" [[package]] name = "static_assertions" @@ -1666,32 +2010,68 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" [[package]] -name = "subtle" -version = "2.2.3" +name = "string_cache" +version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502d53007c02d7605a05df1c1a73ee436952781653da5d0bf57ad608f66932c1" +checksum = "213494b7a2b503146286049378ce02b482200519accc31872ee8be91fa820a08" +dependencies = [ + "new_debug_unreachable", + "once_cell", + "parking_lot", + "phf_shared", + "precomputed-hash", + "serde", +] [[package]] -name = "syn" -version = "1.0.38" +name = "string_cache_codegen" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e69abc24912995b3038597a7a593be5053eb0fb44f3cc5beec0deb421790c1f4" +checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" dependencies = [ + "phf_generator", + "phf_shared", "proc-macro2", "quote", - "unicode-xid", ] [[package]] -name = "synstructure" -version = "0.12.4" +name = "strum" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cae14b91c7d11c9a851d3fbc80a963198998c2a64eec840477fa92d8ce9b70bb" +dependencies = [ + "strum_macros", +] + +[[package]] +name = "strum_macros" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b834f2d66f734cb897113e34aaff2f1ab4719ca946f9a7358dba8f8064148701" +checksum = "5bb0dc7ee9c15cea6199cde9a127fa16a4c5819af85395457ad72d68edc85a38" dependencies = [ + "heck", "proc-macro2", "quote", + "rustversion", "syn", - "unicode-xid", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.96" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0748dd251e24453cb8717f0354206b91557e4ec8703673a4b30208f2abaf1ebf" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", ] [[package]] @@ -1702,35 +2082,67 @@ checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" [[package]] name = "tempfile" -version = "3.1.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a6e24d9338a0a5be79593e2fa15a648add6138caa803e2d5bc782c371732ca9" +checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ - "cfg-if 0.1.10", + "cfg-if 1.0.0", + "fastrand", "libc", - "rand", "redox_syscall", "remove_dir_all", - "winapi 0.3.9", + "winapi", +] + +[[package]] +name = "tendril" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" +dependencies = [ + "futf", + "mac", + "utf-8", ] [[package]] name = "term" -version = "0.6.1" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c59df8ac95d96ff9bede18eb7300b0fda5e5d8d90960e76f8e14ae765eedbf1f" +dependencies = [ + "dirs-next", + "rustversion", + "winapi", +] + +[[package]] +name = "thiserror" +version = "1.0.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0863a3345e70f61d613eab32ee046ccd1bcc5f9105fe402c61fcd0c13eeb8b5" +checksum = "bd829fe32373d27f76265620b5309d0340cb8550f523c1dda251d6298069069a" dependencies = [ - "dirs", - "winapi 0.3.9", + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0396bc89e626244658bef819e22d0cc459e795a5ebe878e6ec336d1674a8d79a" +dependencies = [ + "proc-macro2", + "quote", + "syn", ] [[package]] name = "thread_local" -version = "1.1.4" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" dependencies = [ - "once_cell", + "lazy_static", ] [[package]] @@ -1740,9 +2152,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", +] + +[[package]] +name = "time" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2702e08a7a860f005826c6815dcac101b19b5eb330c27fe4a5928fec1d20ddd" +dependencies = [ + "itoa 1.0.2", + "libc", + "num_threads", + "time-macros", ] +[[package]] +name = "time-macros" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" + [[package]] name = "tinystr" version = "0.3.3" @@ -1751,35 +2181,75 @@ checksum = "707151f004e8db265b83b1c7509d6c3b4c2c2bc8696113cbe0a8e595c2fdbd3b" [[package]] name = "tinyvec" -version = "0.3.3" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53953d2d3a5ad81d9f844a32f14ebb121f50b650cd59d0ee2a07cf13c617efed" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "0.2.22" +version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d34ca54d84bf2b5b4d7d31e901a8464f7b60ac145a284fba25ceb801f2ddccd" +checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" dependencies = [ - "bytes", - "fnv", - "futures-core", - "iovec", - "lazy_static", + "bytes 1.1.0", + "libc", "memchr", "mio", "num_cpus", + "once_cell", "pin-project-lite", - "slab", + "socket2", + "winapi", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.1.1" +source = "git+https://github.com/ankitects/tokio-io-timeout.git?rev=1ee0892217e9a76bba4bb369ec5fab8854935a3c#1ee0892217e9a76bba4bb369ec5fab8854935a3c" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6" +dependencies = [ + "rustls", + "tokio", + "webpki", +] + +[[package]] +name = "tokio-socks" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51165dfa029d2a65969413a6cc96f354b86b464498702f174a4efa13608fd8c0" +dependencies = [ + "either", + "futures-util", + "thiserror", + "tokio", ] [[package]] name = "tokio-util" -version = "0.3.1" +version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" +checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ - "bytes", + "bytes 1.1.0", "futures-core", "futures-sink", "log", @@ -1787,6 +2257,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +dependencies = [ + "bytes 1.1.0", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", + "tracing", +] + [[package]] name = "toml" version = "0.5.6" @@ -1804,22 +2288,22 @@ checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" [[package]] name = "tracing" -version = "0.1.19" +version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d79ca061b032d6ce30c660fded31189ca0b9922bf483cd70759f13a2d86786c" +checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" dependencies = [ - "cfg-if 0.1.10", - "log", + "cfg-if 1.0.0", + "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.14" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db63662723c316b43ca36d833707cc93dff82a02ba3d7e354f342682cc8b3545" +checksum = "7709595b8878a4965ce5e87ebf880a7d39c9afc6837721b21a5a816a8117d921" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] @@ -1830,24 +2314,45 @@ checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" [[package]] name = "type-map" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d2741b1474c327d95c1f1e3b0a2c3977c8e128409c572a33af2914e7d636717" +checksum = "b6d3364c5e96cb2ad1603037ab253ddd34d7fb72a58bdddf4b7350760fc69a46" dependencies = [ - "fxhash", + "rustc-hash", ] [[package]] name = "typenum" -version = "1.12.0" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" + +[[package]] +name = "unic-char-property" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221" +dependencies = [ + "unic-char-range", +] + +[[package]] +name = "unic-char-range" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33" +checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc" + +[[package]] +name = "unic-common" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc" [[package]] name = "unic-langid" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d81136159f779c35b10655f45210c71cd5ca5a45aadfe9840a61c7071735ed" +checksum = "73328fcd730a030bdb19ddf23e192187a6b01cd98be6d3140622a89129459ce5" dependencies = [ "unic-langid-impl", "unic-langid-macros", @@ -1855,18 +2360,18 @@ dependencies = [ [[package]] name = "unic-langid-impl" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43c61e94492eb67f20facc7b025778a904de83d953d8fcb60dd9adfd6e2d0ea" +checksum = "1a4a8eeaf0494862c1404c95ec2f4c33a2acff5076f64314b465e3ddae1b934d" dependencies = [ "tinystr", ] [[package]] name = "unic-langid-macros" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "49bd90791278634d57e3ed4a4073108e3f79bfb87ab6a7b8664ba097425703df" +checksum = "18f980d6d87e8805f2836d64b4138cc95aa7986fa63b1f51f67d5fbff64dd6e5" dependencies = [ "proc-macro-hack", "tinystr", @@ -1876,9 +2381,9 @@ dependencies = [ [[package]] name = "unic-langid-macros-impl" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0098f77bd754f8fb7850cdf4ab143aa821898c4ac6dc16bcb2aa3e62ce858d1" +checksum = "29396ffd97e27574c3e01368b1a64267d3064969e4848e2e130ff668be9daa9f" dependencies = [ "proc-macro-hack", "quote", @@ -1886,6 +2391,27 @@ dependencies = [ "unic-langid-impl", ] +[[package]] +name = "unic-ucd-category" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8d4591f5fcfe1bd4453baaf803c40e1b1e69ff8455c47620440b46efef91c0" +dependencies = [ + "matches", + "unic-char-property", + "unic-char-range", + "unic-ucd-version", +] + +[[package]] +name = "unic-ucd-version" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4" +dependencies = [ + "unic-common", +] + [[package]] name = "unicase" version = "2.6.0" @@ -1895,11 +2421,23 @@ dependencies = [ "version_check", ] +[[package]] +name = "unicode-bidi" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" + +[[package]] +name = "unicode-ident" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" + [[package]] name = "unicode-normalization" -version = "0.1.13" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6fb19cf769fa8c6a80a162df694621ebeb4dafb606470b2b2fce0be40a98a977" +checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" dependencies = [ "tinyvec", ] @@ -1911,10 +2449,40 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" [[package]] -name = "unicode-xid" -version = "0.2.1" +name = "unicode-width" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" + +[[package]] +name = "untrusted" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" + +[[package]] +name = "url" +version = "2.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564" +checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +dependencies = [ + "form_urlencoded", + "idna", + "matches", + "percent-encoding", +] + +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + +[[package]] +name = "utf8-decode" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca61eb27fa339aa08826a29f03e87b99b4d8f0fc2255306fd266bb1b6a9de498" [[package]] name = "utime" @@ -1923,7 +2491,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91baa0c65eabd12fcbdac8cc35ff16159cab95cae96d0222d6d0271db6193cef" dependencies = [ "libc", - "winapi 0.3.9", + "winapi", ] [[package]] @@ -1945,7 +2513,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" dependencies = [ "same-file", - "winapi 0.3.9", + "winapi", "winapi-util", ] @@ -1961,30 +2529,123 @@ dependencies = [ [[package]] name = "wasi" -version = "0.9.0+wasi-snapshot-preview1" +version = "0.10.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" +checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" [[package]] name = "wasi" -version = "0.10.0+wasi-snapshot-preview1" +version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] -name = "which" -version = "3.1.1" +name = "wasm-bindgen" +version = "0.2.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d011071ae14a2f6671d0b74080ae0cd8ebf3a6f8c9589a2cd45f23126fe29724" +checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" dependencies = [ - "libc", + "cfg-if 1.0.0", + "serde", + "serde_json", + "wasm-bindgen-macro", ] [[package]] -name = "winapi" -version = "0.2.8" +name = "wasm-bindgen-backend" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +dependencies = [ + "bumpalo", + "lazy_static", + "log", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +dependencies = [ + "cfg-if 1.0.0", + "js-sys", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.80" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" + +[[package]] +name = "web-sys" +version = "0.3.57" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "webpki" +version = "0.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8e38c0608262c46d4a56202ebabdeb094cef7e560ca7a226c6bf055188aa4ea" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "webpki-roots" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aabe153544e473b775453675851ecc86863d2a81d786d741f6b76778f2a48940" +dependencies = [ + "webpki", +] + +[[package]] +name = "which" +version = "4.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" +checksum = "5c4fb54e6113b6a8772ee41c3404fb0301ac79604489467e0a9ce1f3e97c24ae" +dependencies = [ + "either", + "lazy_static", + "libc", +] [[package]] name = "winapi" @@ -1996,12 +2657,6 @@ dependencies = [ "winapi-x86_64-pc-windows-gnu", ] -[[package]] -name = "winapi-build" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" - [[package]] name = "winapi-i686-pc-windows-gnu" version = "0.4.0" @@ -2014,7 +2669,7 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" dependencies = [ - "winapi 0.3.9", + "winapi", ] [[package]] @@ -2024,23 +2679,95 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "ws2_32-sys" -version = "0.2.1" +name = "windows-sys" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +dependencies = [ + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" + +[[package]] +name = "windows_i686_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" + +[[package]] +name = "windows_i686_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.36.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" + +[[package]] +name = "winreg" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" +checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" dependencies = [ - "winapi 0.2.8", - "winapi-build", + "winapi", ] [[package]] name = "zip" -version = "0.5.6" +version = "0.5.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58287c28d78507f5f91f2a4cf1e8310e2c76fd4c6932f93ac60fd1ceb402db7d" +checksum = "93ab48844d61251bb3835145c521d88aa4031d7139e8485990f60ca911fa0815" dependencies = [ + "byteorder", "crc32fast", "flate2", - "podio", - "time", + "thiserror", + "time 0.1.43", +] + +[[package]] +name = "zstd" +version = "0.10.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f4a6bd64f22b5e3e94b4e238669ff9f10815c27a5180108b849d24174a83847" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "4.1.6+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94b61c51bb270702d6167b8ce67340d2754b088d0c091b06e593aa772c3ee9bb" +dependencies = [ + "libc", + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "1.6.3+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc49afa5c8d634e75761feda8c592051e7eeb4683ba827211eb0d731d3402ea8" +dependencies = [ + "cc", + "libc", ] diff --git a/rslib-bridge/Cargo.toml b/rslib-bridge/Cargo.toml index adaadbf41..db28ba8bd 100644 --- a/rslib-bridge/Cargo.toml +++ b/rslib-bridge/Cargo.toml @@ -12,7 +12,7 @@ crate_type = ["dylib"] [dependencies] jni = { version = "0.17.0", default-features = false } anki = { path = "anki/rslib" } -prost = "0.6.1" +prost = "0.9" serde = "1.0.114" serde_json = "1.0.56" serde_derive = "1.0.114" @@ -22,10 +22,10 @@ itertools = "0.10.0" lexical-core = "0.7.5" # picked bundled - TODO: Is this correct? -rusqlite = { version = "0.23.1", features = ["trace", "functions", "collation", "bundled"] } +rusqlite = { version = "0.26.0", features = ["trace", "functions", "collation", "bundled"] } [features] no-android = [] [build-dependencies] -prost-build = "0.6.1" \ No newline at end of file +prost-build = "0.9" \ No newline at end of file diff --git a/rslib-bridge/anki b/rslib-bridge/anki index cae833d61..50d0af6ac 160000 --- a/rslib-bridge/anki +++ b/rslib-bridge/anki @@ -1 +1 @@ -Subproject commit cae833d61c4555bc60db345ad6da72f5a5864256 +Subproject commit 50d0af6ac11c770332390928ef088a8fe33db02c diff --git a/rslib-bridge/build.rs b/rslib-bridge/build.rs deleted file mode 100644 index ae46e0682..000000000 --- a/rslib-bridge/build.rs +++ /dev/null @@ -1,109 +0,0 @@ -use std::fmt::Write; -use std::path::Path; -use std::process::Command; - -// TODO: See if we can reference anki/build.rs so there's no code duplication - -struct CustomGenerator {} - -fn write_method_enum(buf: &mut String, service: &prost_build::Service) { - buf.push_str( - r#" -use num_enum::TryFromPrimitive; -#[derive(PartialEq,TryFromPrimitive)] -#[repr(u32)] -pub enum BackendMethod { -"#, - ); - for (idx, method) in service.methods.iter().enumerate() { - writeln!(buf, " {} = {},", method.proto_name, idx + 1).unwrap(); - } - buf.push_str("}\n\n"); -} - -fn write_method_trait(buf: &mut String, service: &prost_build::Service) { - buf.push_str( - r#" -use prost::Message; -pub type BackendResult = std::result::Result; -pub trait DroidBackendService { - fn run_command_bytes2_inner_ad(&self, method: u32, input: &[u8]) -> std::result::Result, anki::err::AnkiError> { - match method { -"#, - ); - - for (idx, method) in service.methods.iter().enumerate() { - write!( - buf, - concat!(" ", - "{idx} => {{ let input = {input_type}::decode(input)?;\n", - "let output = self.{rust_method}(input)?;\n", - "let mut out_bytes = Vec::new(); output.encode(&mut out_bytes)?; Ok(out_bytes) }}, "), - idx = idx + 1, - input_type = method.input_type, - rust_method = method.name - ) - .unwrap(); - } - buf.push_str( - r#" - _ => Err(anki::err::AnkiError::invalid_input("invalid command")), - } - } -"#, - ); - - for method in &service.methods { - write!( - buf, - concat!( - " fn {method_name}(&self, input: {input_type}) -> ", - "BackendResult<{output_type}>;\n" - ), - method_name = method.name, - input_type = method.input_type, - output_type = method.output_type - ) - .unwrap(); - } - buf.push_str("}\n"); -} - -impl prost_build::ServiceGenerator for CustomGenerator { - fn generate(&mut self, service: prost_build::Service, buf: &mut String) { - write_method_enum(buf, &service); - write_method_trait(buf, &service); - } -} - -fn service_generator() -> Box { - Box::new(CustomGenerator {}) -} - - -fn main() -> std::io::Result<()> { - // output protobuf generated code - println!("cargo:rerun-if-changed=proto/AdBackend.proto"); - - let mut config = prost_build::Config::new(); - config - // we avoid default OUT_DIR for now, as it breaks code completion - .out_dir("src") - .service_generator(service_generator()) - .compile_protos(&["proto/AdBackend.proto"], &["proto"]) - .unwrap(); - - if let Err(e) = std::env::var("DONT_RUSTFMT") { - assert_eq!(e, std::env::VarError::NotPresent); - println!("Using rustfmt to format src/backend_proto.rs"); - // rustfmt the protobuf code - let rustfmt = Command::new("rustfmt") - .arg(Path::new("src/backend_proto.rs")) - .status() - .unwrap(); - - assert!(rustfmt.success(), "rustfmt backend_proto.rs failed"); - } - - Ok(()) -} \ No newline at end of file diff --git a/rslib-bridge/proto/AdBackend.proto b/rslib-bridge/proto/AdBackend.proto deleted file mode 100644 index 86ea1493f..000000000 --- a/rslib-bridge/proto/AdBackend.proto +++ /dev/null @@ -1,70 +0,0 @@ -syntax = "proto3"; - -package BackendProto; - -option java_generic_services = true; - -// Generic containers -/////////////////////////////////////////////////////////// - -// IDs used in RPC calls -/////////////////////////////////////////////////////////// - -// New style RPC definitions -/////////////////////////////////////////////////////////// - -service AnkiDroidBackendService { - rpc SchedTimingTodayLegacy (SchedTimingTodayIn) returns (SchedTimingTodayOut2); - rpc LocalMinutesWestLegacy (LocalMinutesWestIn) returns (LocalMinutesWestOut); - rpc DebugActiveDatabaseSequenceNumbers (DebugActiveDatabaseSequenceNumbersIn) returns (DebugActiveDatabaseSequenceNumbersOut); -} - -// Protobuf stored in .anki2 files -// These should be moved to a separate file in the future -/////////////////////////////////////////////////////////// - -// Containers for passing around database objects -/////////////////////////////////////////////////////////// - -// Backend -/////////////////////////////////////////////////////////// - - -// Errors -/////////////////////////////////////////////////////////// - -// Progress -/////////////////////////////////////////////////////////// - -// Messages -/////////////////////////////////////////////////////////// - -message DebugActiveDatabaseSequenceNumbersIn { - int64 backend_ptr = 1; -} - -message DebugActiveDatabaseSequenceNumbersOut { - repeated int32 sequence_numbers = 1; -} - -message LocalMinutesWestIn { - int64 collection_creation_time = 1; -} - -message LocalMinutesWestOut { - sint32 mins_west = 1; -} - -message SchedTimingTodayIn { - int64 created_secs = 1; - sint32 created_mins_west = 2; - int64 now_secs = 3; - sint32 now_mins_west = 4; - sint32 rollover_hour = 5; -} - -message SchedTimingTodayOut2 { - uint32 days_elapsed = 1; - int64 next_day_at = 2; -} - diff --git a/rslib-bridge/src/ankidroid.rs b/rslib-bridge/src/ankidroid.rs deleted file mode 100644 index 871306926..000000000 --- a/rslib-bridge/src/ankidroid.rs +++ /dev/null @@ -1,11 +0,0 @@ -use anki::backend::Backend as RustBackend; - -pub(crate) struct AnkiDroidBackend { - pub backend: RustBackend, -} - -impl AnkiDroidBackend { - pub fn new(backend: RustBackend) -> AnkiDroidBackend { - AnkiDroidBackend { backend } - } -} \ No newline at end of file diff --git a/rslib-bridge/src/dbcommand.rs b/rslib-bridge/src/dbcommand.rs deleted file mode 100644 index 943da044c..000000000 --- a/rslib-bridge/src/dbcommand.rs +++ /dev/null @@ -1,350 +0,0 @@ -use std::collections::HashMap; -use std::sync::Mutex; - -use anki::backend_proto::{DbResponse, DbResult}; - -// Handles global variables for DbResponse streaming -// COULD_BE_BETTER: Consider converting this into an object returned from the function -// (accessible via JNI) - more idiomatic, but probably less clean than this. - -// COULD_BE_BETTER: make DBResponse.DbResult non-optional - -use i64 as backend_pointer; -use i64 as dbresponse_pointer; - -use anki::backend_proto::{Row, SqlValue}; -use std::mem::size_of; -use anki::backend_proto::sql_value::Data; -use itertools::{Itertools, FoldWhile}; -use itertools::FoldWhile::{Done, Continue}; -use std::ops::Deref; - - -pub trait Sizable { - /** Estimates the heap size of the value, in bytes */ - fn estimate_size(&self) -> usize; -} - -impl Sizable for Data { - fn estimate_size(&self) -> usize { - match self { - Data::StringValue(s) => { s.len() } - Data::LongValue(_) => { size_of::() } - Data::DoubleValue(_) => { size_of::() } - Data::BlobValue(b) => { b.len() } - } - } -} - -impl Sizable for SqlValue { - fn estimate_size(&self) -> usize { - // Add a byte for the optional - self.data.as_ref().map(|f| f.estimate_size() + 1).unwrap_or(1) - } -} - -impl Sizable for Row { - fn estimate_size(&self) -> usize { - self.fields.iter().map(|x| x.estimate_size()).sum() - } -} - -impl Sizable for DbResult { - fn estimate_size(&self) -> usize { - // Performance: It might be best to take the first x rows and determine the data types - // If we have floats or longs, they'll be a fixed size (excluding nulls) and should speed - // up the calculation as we'll only calculate a subset of the columns. - self.rows.iter().map(|x| x.estimate_size()).sum() - } -} - -pub(crate) fn select_next_slice<'a>(mut rows : impl Iterator) -> Vec { - select_slice_of_size(rows, get_max_page_size()).into_inner().1 -} - -fn select_slice_of_size<'a>(mut rows : impl Iterator, max_size: usize) -> FoldWhile<(usize, Vec)> { - let init: Vec = Vec::new(); - let folded = rows.fold_while((0, init), |mut acc, x| { - let new_size = acc.0 + x.estimate_size(); - // If the accumulator is 0, but we're over the size: return a single result so we don't loop forever. - // Theoretically, this shouldn't happen as data should be reasonably sized - if new_size > max_size && acc.0 > 0 { - Done(acc) - } else { - // PERF: should be faster to return (size, numElements) then bulk copy/slice - acc.1.push(x.to_owned()); - Continue((new_size, acc.1)) - } - }); - folded -} - -lazy_static! { - // backend_pointer => Map - static ref HASHMAP: Mutex>> = { - Mutex::new(HashMap::new()) - }; -} - -pub(crate) unsafe fn flush_cache(ptr : &backend_pointer, sequence_number : i32) { - let mut map = HASHMAP.lock().unwrap(); - let entries = map.get_mut(ptr); - match entries { - Some(seq_to_ptr) => { - let entry = seq_to_ptr.remove_entry(&sequence_number); - match entry { - Some(ptr) => { - let raw = ptr.1 as *mut DbResponse; - Box::from_raw(raw); - } - None => { } - } - } - None => { } - } -} - - -pub(crate) unsafe fn flush_all(ptr: &backend_pointer) { - let mut map = HASHMAP.lock().unwrap(); - - // clear the map - let entries = map.remove_entry(ptr); - - match entries { - Some(seq_to_ptr_map) => { - // then clear each value - for val in seq_to_ptr_map.1.values() { - let raw = (*val) as *mut DbResponse; - Box::from_raw(raw); - } - } - None => { } - } -} - -pub(crate) fn active_sequences(ptr : backend_pointer) -> Vec { - let mut map = HASHMAP.lock().unwrap(); - - match map.get_mut(&ptr) { - Some(x) => { - let keys = x.keys(); - keys.into_iter().map(|i| *i).collect_vec() - }, - None => { - Vec::new() - } - } -} - -/** -Store the data in the cache if larger than than the page size.
-Returns: The data capped to the page size -*/ -pub(crate) fn trim_and_cache_remaining(backend_ptr: i64, values: DbResult, sequence_number: i32) -> DbResponse { - let start_index = 0; - - // PERF: Could speed this up by not creating the vector and just calculating the count - let first_result = select_next_slice(values.rows.iter()); - - let row_count = values.rows.len() as i32; - if first_result.len() < values.rows.len() { - let to_store = DbResponse { result: Some(values), sequence_number, row_count, start_index }; - insert_cache(backend_ptr, to_store); - - DbResponse { result: Some(DbResult { rows: first_result }), sequence_number, row_count, start_index } - } else { - DbResponse { result: Some(values), sequence_number, row_count, start_index } - } -} - -fn insert_cache(ptr : backend_pointer, result : DbResponse) { - let mut map = HASHMAP.lock().unwrap(); - - match map.get_mut(&ptr) { - Some(_) => { }, - None => { - let map2 : HashMap = HashMap::new(); - map.insert(ptr, map2); - } - }; - - let out_hash_map = map.get_mut(&ptr).unwrap(); - - out_hash_map.insert(result.sequence_number, Box::into_raw(Box::new(result)) as dbresponse_pointer); -} - -pub(crate) unsafe fn get_next(ptr : backend_pointer, sequence_number : i32, start_index : i64) -> Option { - let result = get_next_result(ptr, &sequence_number, start_index); - - match result.as_ref() { - Some(x) => { - if x.result.is_none() || x.result.as_ref().unwrap().rows.is_empty() { - flush_cache(&ptr, sequence_number) - } - }, - None => {} - } - - result -} - -unsafe fn get_next_result(ptr: backend_pointer, sequence_number: &i32, start_index: i64) -> Option { - let map = HASHMAP.lock().unwrap(); - - let result_map = map.get(&ptr)?; - - let backend_ptr = *result_map.get(&sequence_number)?; - - let current_result = &mut *(backend_ptr as *mut DbResponse); - - // TODO: This shouldn't need to exist - let tmp: Vec = Vec::new(); - let next_rows = current_result.result.as_ref().map(|x| x.rows.iter()).unwrap_or(tmp.iter()); - - let skipped_rows = next_rows.clone().skip(start_index as usize).collect_vec(); - println!("{}", skipped_rows.len()); - - let filtered_rows = select_next_slice(next_rows.skip(start_index as usize)); - - let result = DbResult { rows: filtered_rows }; - - let trimmed_result = DbResponse { result: Some(result), sequence_number: current_result.sequence_number, row_count: current_result.row_count, start_index }; - - Some(trimmed_result) -} - -static mut SEQUENCE_NUMBER: i32 = 0; - -pub(crate) unsafe fn next_sequence_number() -> i32 { - SEQUENCE_NUMBER = SEQUENCE_NUMBER + 1; - SEQUENCE_NUMBER -} - -lazy_static!{ - // same as we get from io.requery.android.database.CursorWindow.sCursorWindowSize - static ref DB_COMMAND_PAGE_SIZE: Mutex = Mutex::new(1024 * 1024 * 2); -} - -pub(crate) fn set_max_page_size(size: usize) { - let mut state = DB_COMMAND_PAGE_SIZE.lock().expect("Could not lock mutex"); - *state = size; -} - -fn get_max_page_size() -> usize { - *DB_COMMAND_PAGE_SIZE.lock().unwrap() -} - - -#[cfg(test)] -mod tests { - use super::*; - - use anki::backend_proto::{sql_value, Row, SqlValue}; - use crate::dbcommand::{Sizable, select_slice_of_size}; - use std::borrow::Borrow; - - fn gen_data() -> Vec { - vec![ - SqlValue{ - data: Some(sql_value::Data::DoubleValue(12.0)) - }, - SqlValue{ - data: Some(sql_value::Data::LongValue(12)) - }, - SqlValue{ - data: Some(sql_value::Data::StringValue("Hellooooooo World".to_string())) - }, - SqlValue{ - data: Some(sql_value::Data::BlobValue(vec![])) - } - ] - } - - #[test] - fn test_size_estimate() { - let row = Row { fields: gen_data() }; - let result = DbResult { rows: vec![row.clone(), row.clone()] }; - - let actual_size = result.estimate_size(); - - let expected_size = (17 + 8 + 8) * 2; // 1 variable string, 1 long, 1 float - let expected_overhead = (4 * 1) * 2; // 4 optional columns - - assert_eq!(actual_size, expected_overhead + expected_size); - } - - #[test] - fn test_stream_size() { - let row = Row { fields: gen_data() }; - let result = DbResult { rows: vec![row.clone(), row.clone(), row.clone()] }; - let limit = 74 + 1; // two rows are 74 - - let result = select_slice_of_size(result.rows.iter(), limit).into_inner(); - - assert_eq!(2, result.1.len(), "The final element should not be included"); - assert_eq!(74, result.0, "The size should be the size of the first two objects"); - } - - #[test] - fn test_stream_size_too_small() { - let row = Row { fields: gen_data() }; - let result = DbResult { rows: vec![row.clone()] }; - let limit = 1; - - let result = select_slice_of_size(result.rows.iter(), limit).into_inner(); - - assert_eq!(1, result.1.len(), "If the limit is too small, a result is still returned"); - assert_eq!(37, result.0, "The size should be the size of the first objects"); - } - - const BACKEND_PTR: i64 = 12; - const SEQUENCE_NUMBER: i32 = 1; - - fn get(index : i64) -> Option { - unsafe { return get_next(BACKEND_PTR, SEQUENCE_NUMBER, index) }; - } - - fn get_first(result : DbResult) -> DbResponse { - trim_and_cache_remaining(BACKEND_PTR, result, SEQUENCE_NUMBER) - } - - fn seq_number_used() -> bool { - HASHMAP.lock().unwrap().get(&BACKEND_PTR).unwrap().contains_key(&SEQUENCE_NUMBER) - } - - #[test] - fn integration_test() { - let row = Row { fields: gen_data() }; - - // return one row at a time - set_max_page_size(row.estimate_size() - 1); - - let db_query_result = DbResult { rows: vec![row.clone(), row.clone()] }; - - let first_jni_response = get_first(db_query_result); - - assert_eq!(row_count(&first_jni_response), 1, "The first call should only return one row"); - - let next_index = first_jni_response.start_index + row_count(&first_jni_response); - - let second_response = get(next_index); - - assert!(second_response.is_some(), "The second response should return a value"); - let valid_second_response = second_response.unwrap(); - assert_eq!(row_count(&valid_second_response), 1); - - let final_index = valid_second_response.start_index + row_count(&valid_second_response); - - assert!(seq_number_used(), "The sequence number is assigned"); - - let final_response = get(final_index); - assert!(final_response.is_some(), "The third call should return something with no rows"); - assert_eq!(row_count(&final_response.unwrap()), 0, "The third call should return something with no rows"); - assert!(!seq_number_used(), "Sequence number data has been cleared"); - } - - fn row_count(resp: &DbResponse) -> i64 { - resp.result.as_ref().map(|x| x.rows.len()).unwrap_or(0) as i64 - } -} \ No newline at end of file diff --git a/rslib-bridge/src/lib.rs b/rslib-bridge/src/lib.rs index 86194b872..c681c51cb 100644 --- a/rslib-bridge/src/lib.rs +++ b/rslib-bridge/src/lib.rs @@ -1,565 +1,125 @@ -#[macro_use] -extern crate lazy_static; +#![allow(clippy::missing_safety_doc)] +use anki::backend_proto::{backend_error, BackendError, Int64}; +use jni::objects::{JClass, JObject}; +use jni::sys::{jarray, jbyteArray, jint, jlong}; use jni::JNIEnv; -use jni::objects::{JClass, JString, JObject}; -use jni::sys::{jbyteArray, jint, jlong, jobjectArray, jarray, jstring}; -use anki::{backend_proto as pb, log}; -use pb::OpenCollectionIn; -use crate::sqlite::{open_collection_ankidroid, insert_for_id, query_for_affected, get_open_collection_for_downgrade}; - -use anki::backend::{init_backend, anki_error_to_proto_error, Backend}; -use crate::ankidroid::AnkiDroidBackend; - -// allows encode/decode +use anki::backend::{init_backend, Backend}; +use anki::error::{AnkiError, Result}; +use anki::i18n::I18n; use prost::Message; -use std::panic::{catch_unwind, AssertUnwindSafe}; use std::any::Any; -use anki::err::AnkiError; -use anki::i18n::I18n; - -use anki::backend_proto::{DbResult, DbResponse}; -use core::result; -use crate::backend_proto::{DroidBackendService, LocalMinutesWestIn, SchedTimingTodayIn, SchedTimingTodayOut2, LocalMinutesWestOut, DebugActiveDatabaseSequenceNumbersIn, DebugActiveDatabaseSequenceNumbersOut}; -use anki::sched::cutoff; -use anki::timestamp::TimestampSecs; -use anki::sched::cutoff::SchedTimingToday; - -mod dbcommand; -mod sqlite; -mod ankidroid; -mod backend_proto; - -// TODO: Use a macro to handle panics to reduce code duplication - -impl From for SchedTimingTodayOut2 { - fn from(data: SchedTimingToday) -> Self { - SchedTimingTodayOut2 { - days_elapsed: data.days_elapsed, - next_day_at: data.next_day_at - } - } -} - -impl backend_proto::DroidBackendService for Backend { - fn sched_timing_today_legacy(&self, input: SchedTimingTodayIn) -> Result { - let result = cutoff::sched_timing_today( - TimestampSecs::from(input.created_secs), - TimestampSecs::from(input.now_secs), - Some(input.created_mins_west), - Some(input.now_mins_west), - Some(input.rollover_hour as u8) - ); - Ok(SchedTimingTodayOut2::from(result)) - } - - fn local_minutes_west_legacy(&self, input : LocalMinutesWestIn) -> Result { - let out = LocalMinutesWestOut { - mins_west: cutoff::local_minutes_west_for_stamp(input.collection_creation_time) - }; - Ok(out) - } - - fn debug_active_database_sequence_numbers(&self, input: DebugActiveDatabaseSequenceNumbersIn) -> Result { - let backend_ptr = input.backend_ptr.clone(); - let result = DebugActiveDatabaseSequenceNumbersOut { - sequence_numbers: dbcommand::active_sequences(backend_ptr) - }; - Ok(result) - } -} +use std::panic::{catch_unwind, AssertUnwindSafe}; #[no_mangle] pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_openBackend( env: JNIEnv, _: JClass, - args: jbyteArray) -> jlong { - // TODO: This does not handle panics - we currently return a pointer - convert to protobuf - - let rust_backend = init_backend(env.convert_byte_array(args).unwrap().as_slice()).unwrap(); - let backend = AnkiDroidBackend::new(rust_backend); - - - Box::into_raw(Box::new(backend)) as jlong -} - -/// Produces an error -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_debugProduceError( - _env: JNIEnv, - _: JClass, - backend_ptr : jlong, - error : JString) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - // We need to go from string -> AnkiError -> BackendError. BackendError is less expressive than - // AnkiError, and we want to test all possibilities. - let error_type_string: String = _env.get_string(error).expect("Couldn't get java string!").into(); - - use anki::err::NetworkErrorKind as Net; - use anki::err::SyncErrorKind as Sync; - use anki::err::DBErrorKind as DB; - - let error_value = "error_value".to_string(); - let err = match error_type_string.as_ref() { - "InvalidInput" => AnkiError::InvalidInput { info: error_value }, - "TemplateError" => AnkiError::TemplateError { info: error_value }, - "TemplateSaveError" => AnkiError::TemplateSaveError { ordinal: 1 }, - "IOError" => AnkiError::IOError { info: error_value }, - "DbErrorFileTooNew" => AnkiError::DBError { info: error_value, kind: DB::FileTooNew }, - "DbErrorFileTooOld" => AnkiError::DBError { info: error_value, kind: DB::FileTooOld }, - "DbErrorMissingEntity" => AnkiError::DBError { info: error_value, kind: DB::MissingEntity }, - "DbErrorCorrupt" => AnkiError::DBError { info: error_value, kind: DB::Corrupt }, - "DbErrorLocked" => AnkiError::DBError { info: error_value, kind: DB::Locked }, - "DbErrorOther" => AnkiError::DBError { info: error_value, kind: DB::Other }, - "NetworkErrorOffline" => AnkiError::NetworkError { info: error_value, kind: Net::Offline}, - "NetworkErrorTimeout" => AnkiError::NetworkError { info: error_value, kind: Net::Timeout}, - "NetworkErrorProxyAuth" => AnkiError::NetworkError { info: error_value, kind: Net::ProxyAuth}, - "NetworkErrorOther" => AnkiError::NetworkError { info: error_value, kind: Net::Other}, - "SyncErrorConflict" => AnkiError::SyncError { info: error_value, kind: Sync::Conflict }, - "SyncErrorServerError" => AnkiError::SyncError { info: error_value, kind: Sync::ServerError }, - "SyncErrorClientTooOld" => AnkiError::SyncError { info: error_value, kind: Sync::ClientTooOld }, - "SyncErrorAuthFailed" => AnkiError::SyncError { info: error_value, kind: Sync::AuthFailed }, - "SyncErrorServerMessage" => AnkiError::SyncError { info: error_value, kind: Sync::ServerMessage }, - "SyncErrorClockIncorrect" => AnkiError::SyncError { info: error_value, kind: Sync::ClockIncorrect}, - "SyncErrorOther" => AnkiError::SyncError { info: error_value, kind: Sync::Other }, - "SyncErrorResyncRequired" => AnkiError::SyncError { info: error_value, kind: Sync::ResyncRequired }, - "SyncErrorDatabaseCheckRequired"=> AnkiError::SyncError { info: error_value, kind: Sync::DatabaseCheckRequired }, - "JSONError" => AnkiError::JSONError { info: error_value}, - "ProtoError" => AnkiError::ProtoError { info: error_value}, - "Interrupted" => AnkiError::Interrupted, - "CollectionNotOpen" => AnkiError::CollectionNotOpen, - "CollectionAlreadyOpen" => AnkiError::CollectionAlreadyOpen, - "NotFound" => AnkiError::NotFound, - "Existing" => AnkiError::Existing, - "DeckIsFiltered" => AnkiError::DeckIsFiltered, - "SearchError" => AnkiError::SearchError(Some(error_value)), - "FatalError" => AnkiError::FatalError { info: error_value}, - unknown => AnkiError::FatalError { info: format!("Unknown Error code: {}", unknown) }, - }; - - let error_as_proto = anki_error_to_proto_error(err, &backend.backend.i18n); - - let mut bytes = Vec::new(); - error_as_proto.encode(&mut bytes).unwrap(); - _env.byte_array_from_slice(bytes.as_slice()).unwrap() + args: jbyteArray, +) -> jarray { + let input = env.convert_byte_array(args).unwrap(); + let result = init_backend(&input, None) + .map(|backend| { + let backend_ptr = Box::into_raw(Box::new(backend)) as i64; + Int64 { val: backend_ptr }.encode_to_vec() + }) + .map_err(|err| { + BackendError { + localized: err, + kind: backend_error::Kind::FatalError as i32, + ..Default::default() + } + .encode_to_vec() + }); + pack_result(result, &env) } - #[no_mangle] pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_closeBackend( _env: JNIEnv, _: JClass, - args: jlong) -> jlong { - // TODO: This does not handle panics - we currently return a pointer - convert to protobuf - - let raw = args as *mut AnkiDroidBackend; + args: jlong, +) { + let raw = args as *mut Backend; Box::from_raw(raw); - - 1 } - #[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_openCollection( +pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_runMethodRaw( env: JNIEnv, _: JClass, backend_ptr: jlong, - args: jbyteArray) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let in_bytes = env.convert_byte_array(args).unwrap(); - - let command = OpenCollectionIn::decode(in_bytes.as_slice()).unwrap(); - - let ret = open_collection_ankidroid(&backend.backend,command).and_then(|empty| { - let mut out_bytes = Vec::new(); - empty.encode(&mut out_bytes)?; - Ok(out_bytes) - }).map_err(|err| { - let backend_err = anki_error_to_proto_error(err, &backend.backend.i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - bytes - }); - - match ret { - Ok(_s) => env.byte_array_from_slice(_s.as_slice()).unwrap(), - Err(_err) => env.byte_array_from_slice(_err.as_slice()).unwrap(), - } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_command( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - command: jint, + service: jint, + method: jint, args: jbyteArray, ) -> jbyteArray { - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let command: u32 = command as u32; - let in_bytes = env.convert_byte_array(args).unwrap(); - - // We might want to later change this to append a bit to the head of the stream to specify - // the return type. - - match backend.backend.run_command_bytes(command, &in_bytes) { - Ok(_s) => env.byte_array_from_slice(&_s).unwrap(), - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), - } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -/// Opens a database of V16 or earlier and transforms it into a database of V11 -/// This exists to allow -/// We do not require or keep a backend open for this operation: -/// We wouldn't be able to open a backend - openAnkiDroidCollection fails due to the schema mismatch -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_downgradeDatabase( - env: JNIEnv, - _: JClass, - path: JString) -> jstring { - - let i18n = I18n::new(&[""], "", log::default_logger(None).expect("logger failed")); - let result = catch_unwind(|| { - let collection_path: String = env.get_string(path).expect("Couldn't get java string!").into(); - - // Obtains a collection only if it's version 16. Otherwise throws. - // Cause: The .close() method does not check for schema versions, so can't downgrade - // if tables are missing - let collection = match get_open_collection_for_downgrade(collection_path) { - Ok(r) => r, - Err(err) => panic!(err.localized_description(&i18n)) - }; - - const DOWNGRADE_TO_11: bool = true; - if let Err(msg) = collection.close(DOWNGRADE_TO_11) { - panic!(msg.localized_description(&i18n)) - } - }); - - let result_string = match result { - Ok(_) => "".to_owned(), - Err(s) => panic_to_anki_error(s.as_ref()).localized_description(&i18n), - }; - - env.new_string(result_string) - .expect("Failed to produce string") - .into_inner() -} - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_executeAnkiDroidCommand( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - command: jint, - args: jbyteArray, -) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let command: u32 = command as u32; - let in_bytes = env.convert_byte_array(args).unwrap(); - - match run_ad_command_bytes(backend, command, &in_bytes) { - Ok(_s) => env.byte_array_from_slice(&_s).unwrap(), - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), - } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -pub(crate) fn run_ad_command_bytes(backend: &mut AnkiDroidBackend, method: u32, input: &[u8]) -> result::Result, Vec> { - backend.backend.run_command_bytes2_inner_ad(method, input).map_err(|err| { - let backend_err = anki_error_to_proto_error(err, &backend.backend.i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - bytes + let service: u32 = service as u32; + let method: u32 = method as u32; + let input = env.convert_byte_array(args).unwrap(); + with_packed_result(&env, backend.i18n(), || { + backend.run_method(service, method, &input) }) } -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_fullDatabaseCommand( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - input : jbyteArray -) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let in_bytes = env.convert_byte_array(input).unwrap(); - - // Don't map the error for now - let out_res = backend.backend.run_db_command_bytes(&in_bytes); - - match out_res { - Ok(_s) => env.byte_array_from_slice(&_s).unwrap(), - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), - } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_databaseGetNextResultPage( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - sequence_number: jint, - requested_index: jlong -) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - - let next_page = dbcommand::get_next( - backend_ptr, - sequence_number, - requested_index - ).unwrap(); - - - let mut out_bytes = Vec::new(); - next_page.encode(&mut out_bytes).unwrap(); - env.byte_array_from_slice(&out_bytes).unwrap() - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_cancelCurrentProtoQuery( - _: JNIEnv, - _: JClass, - backend_ptr : jlong, - sequence_number: jint -) { - dbcommand::flush_cache(&backend_ptr, sequence_number); -} - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_cancelAllProtoQueries( - _: JNIEnv, - _: JClass, - backend_ptr : jlong -) { - dbcommand::flush_all(&backend_ptr); +unsafe fn to_backend(ptr: jlong) -> &'static mut Backend { + &mut *(ptr as *mut Backend) } -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_databaseCommand( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - input : jbyteArray -) -> jbyteArray { - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let in_bytes = env.convert_byte_array(input).unwrap(); - - // Normally we'd want this as a Vec, but - let out_res = backend.backend.run_db_command_proto(&in_bytes); - - match out_res { - Ok(db_result) => { - let trimmed = dbcommand::trim_and_cache_remaining(backend_ptr, db_result, dbcommand::next_sequence_number()); - - let mut out_bytes = Vec::new(); - trimmed.encode(&mut out_bytes).unwrap(); - env.byte_array_from_slice(&out_bytes).unwrap() - } - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), +macro_rules! null_on_error { + ($arg:expr) => { + match ($arg) { + Ok(ok) => ok, + Err(_) => return JObject::null().into_inner(), } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -// We define these here to avoid the need for a union of positive return values. -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_sqlInsertForId( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - input : jbyteArray -) -> jbyteArray { - - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let in_bytes = env.convert_byte_array(input).unwrap(); - - let out_res = insert_for_id(&in_bytes, backend); - - match out_res { - Ok(_s) => env.byte_array_from_slice(&_s).unwrap(), - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), - } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } - - -} - -// We define these here to avoid the need for a union of positive return values. -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_sqlQueryForAffected( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - input : jbyteArray -) -> jbyteArray { - - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - let in_bytes = env.convert_byte_array(input).unwrap(); - - let out_res = query_for_affected(&in_bytes, backend); - - match out_res { - Ok(_s) => env.byte_array_from_slice(&_s).unwrap(), - Err(_err) => env.byte_array_from_slice(&_err).unwrap(), - } - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - - -// We define these here to avoid the need for a union of positive return values. -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_getColumnNames( - env: JNIEnv, - _: JClass, - backend_ptr : jlong, - input : JString -) -> jarray { - let backend = to_backend(backend_ptr); - - let result = catch_unwind(AssertUnwindSafe(|| { - - let ret = backend.backend.with_col(|col| { - - let str : String = env.get_string(input).expect("Couldn't get java string!").into(); - let stmt = col.storage.db.prepare(&str)?; - let names = stmt.column_names(); - - let array: jobjectArray = env - .new_object_array( - names.len() as i32, - env.find_class("java/lang/String").unwrap(), - *env.new_string("").unwrap(), - ) - .unwrap(); - - - for (i, name) in names.iter().enumerate() { - env.set_object_array_element( - array, - i as i32, - *env.new_string(&name) - .unwrap() - .to_owned(), - ) - .expect("Could not perform set_object_array_element on array element."); - } - Ok(array) - }); - - match ret { - Ok(_s) => _s, - // This may be incorrect - Err(_) => *JObject::null() - } - - })); - - match result { - Ok(_s) => _s, - Err(err) => panic_to_bytes(env,err.as_ref(), &backend.backend.i18n) - } -} - -#[no_mangle] -pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_setDbPageSize( - _: JNIEnv, - _: JClass, - page_size: jlong) { - dbcommand::set_max_page_size(page_size as usize); + }; } -unsafe fn to_backend(ptr: jlong) -> &'static mut AnkiDroidBackend { - // TODO: This is not unwindable, but we can't hard-crash as Android won't send it to ACRA - // As long as the FatalError is sent below, we're OK - &mut *(ptr as *mut AnkiDroidBackend) -} +/// Run provided func and pack result into jarray. Catches panics. +fn with_packed_result(env: &JNIEnv, tr: &I18n, func: F) -> jarray +where + F: FnOnce() -> Result, Vec>, +{ + let result = match catch_unwind(AssertUnwindSafe(func)) { + Ok(result) => result, + Err(panic) => Err(panic_to_anki_error(&panic) + .into_protobuf(tr) + .encode_to_vec()), + }; + pack_result(result, env) +} + +/// Pack Result into jArray[okBytes, null] | jarray[null, errBytes] | null +/// Null returned in case conversion to a jbyteArray fails (eg low mem), +fn pack_result(result: Result, Vec>, env: &JNIEnv) -> jarray { + // create the outer 2-element array + let byte_array_class = null_on_error!(env.find_class("[B")); + let outer_array = null_on_error!(env.new_object_array(2, byte_array_class, JObject::null())); + // pack return/error into bytearrays + let elems = match result { + Ok(msg) => ( + null_on_error!(env.byte_array_from_slice(&msg)), + JObject::null().into_inner(), + ), + Err(err) => ( + JObject::null().into_inner(), + null_on_error!(env.byte_array_from_slice(&err)), + ), + }; + // pack into outer + null_on_error!(env.set_object_array_element(outer_array, 0, elems.0)); + null_on_error!(env.set_object_array_element(outer_array, 1, elems.1)); -fn panic_to_bytes(env: JNIEnv , s: &(dyn Any + Send), i18n: &I18n) -> jbyteArray { - let ret = panic_to_anki_error(s); - let backend_err = anki_error_to_proto_error(ret, i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - env.byte_array_from_slice(bytes.as_slice()).unwrap() + outer_array } fn panic_to_anki_error(s: &(dyn Any + Send)) -> AnkiError { - if let Some(msg) = s.downcast_ref::(){ - AnkiError::FatalError { - info: msg.to_string() - } + if let Some(msg) = s.downcast_ref::() { + AnkiError::FatalError(msg.to_string()) } else { // The TypeId (the only thing you can reasonably get from Any) doesn't carry the type name // Confirm an as_ref() rather than a borrow was passed in here. - AnkiError::FatalError { - info: "panic with unknown info".to_string() - } + AnkiError::FatalError("panic with unknown info".to_string()) } -} \ No newline at end of file +} diff --git a/rslib-bridge/src/sqlite.rs b/rslib-bridge/src/sqlite.rs deleted file mode 100644 index 8a1ec23c0..000000000 --- a/rslib-bridge/src/sqlite.rs +++ /dev/null @@ -1,215 +0,0 @@ -/* -While porting: open_collection upgrades the collection in a non-backwards compatible manner. - -We should be able to perform a database migration to V13 as we're past SQLite 3.9 - - - - */ - -use std::path::{PathBuf, Path}; -use anki::i18n::I18n; -use anki::log::Logger; -use anki::collection::{Collection, CollectionState}; -use anki::err::{DBErrorKind, AnkiError}; -use anki::storage::SqliteStorage; -use anki::backend::{Backend, anki_error_to_proto_error}; -use anki::{backend_proto as pb, log}; -use anki::config; - -use rusqlite::{params, NO_PARAMS}; - - -// allows encode/decode -use prost::Message; - -use crate::ankidroid::AnkiDroidBackend; - -#[derive(Deserialize)] -struct DBArgs { - sql: String, - args: Vec -} -use serde_derive::Deserialize; -use anki::sched::cutoff::v1_creation_date; - -pub type AnkiResult = std::result::Result; - -pub fn open_collection_no_update>( - path: P, - media_folder: P, - media_db: P, - server: bool, - i18n: I18n, - log: Logger, - min_schama: u8, - max_schema: u8 -) -> AnkiResult { - let col_path = path.into(); - let storage = open_or_create_no_update(&col_path, &i18n, server, min_schama, max_schema)?; - - let col = Collection { - storage, - col_path, - media_folder: media_folder.into(), - media_db: media_db.into(), - i18n, - log, - server, - state: CollectionState::default(), - }; - - Ok(col) -} - -pub fn open_collection_ankidroid(backend : &Backend, input: pb::OpenCollectionIn) -> AnkiResult { - let mut col = backend.col.lock().unwrap(); - if col.is_some() { - return Err(AnkiError::CollectionAlreadyOpen); - } - - let mut path = input.collection_path.clone(); - path.push_str(".log"); - - let log_path = match input.log_path.as_str() { - "" => None, - path => Some(path), - }; - let logger = log::default_logger(log_path)?; - - const SCHEMA_ANKIDROID_VERSION: u8 = 11; - - let new_col = open_collection_no_update( - input.collection_path, - input.media_folder_path, - input.media_db_path, - false, - backend.i18n.clone(), - logger, - SCHEMA_ANKIDROID_VERSION, - SCHEMA_ANKIDROID_VERSION - )?; - - *col = Some(new_col); - - Ok(().into()) -} - -pub fn get_open_collection_for_downgrade(collection_path: String) -> AnkiResult { - let logger = log::default_logger(None)?; - - const SCHEMA_ANKIDROID_MAX_VERSION: u8 = 16; - - open_collection_no_update( - collection_path, - "".to_owned(), - "".to_owned(), - false, - I18n::new(&[""], "", logger.clone()), - logger, - SCHEMA_ANKIDROID_MAX_VERSION, - SCHEMA_ANKIDROID_MAX_VERSION - ) -} - - -pub(crate) fn open_or_create_no_update(path: &Path, _i18n: &I18n, _server: bool, current_schema: u8, max_schema : u8) -> AnkiResult { - let db = anki::storage::sqlite::open_or_create_collection_db(path)?; - - let (create, ver) = anki::storage::sqlite::schema_version(&db)?; - // Requery uses "TRUNCATE" by default if WAL is not enabled. - // We copy this behaviour here. See https://github.com/ankidroid/Anki-Android/pull/7977 for - // analysis. We may be able to enable WAL at a later time. - db.pragma_update(None, "journal_mode", &"TRUNCATE")?; - - let err = match ver { - v if v < current_schema => Some(DBErrorKind::FileTooOld), - v if v > max_schema => Some(DBErrorKind::FileTooNew), - _ => None, - }; - if let Some(kind) = err { - return Err(AnkiError::DBError { - info: "Got Schema".to_owned() + &*ver.to_string(), - kind, - }); - } - - if !create { - return Ok(SqliteStorage { db }) - } - - - db.execute("begin exclusive", NO_PARAMS)?; - db.execute_batch(include_str!("../anki/rslib/src/storage/schema11.sql"))?; - // start at schema 11, then upgrade below - let crt = v1_creation_date(); - db.execute( - "update col set crt=?, scm=?, ver=?, conf=?", - params![ - crt, - crt * 1000, - current_schema, - &config::schema11_config_as_string() - ], - )?; - - let storage = SqliteStorage { db }; - - // storage.add_default_deck_config(i18n)?; - // storage.add_default_deck(i18n)?; - // storage.add_stock_notetypes(i18n)?; - - storage.commit_trx()?; - - - Ok(storage) -} - - -fn get_args(in_bytes: &Vec) -> AnkiResult { - let ret : DBArgs = serde_json::from_slice(&in_bytes)?; - Ok(ret) -} - - -pub(crate) fn insert_for_id(in_bytes: &Vec, backend: &mut AnkiDroidBackend) -> Result, Vec> { - - let req = get_args(&in_bytes) - .and_then(|req| { - backend.backend.with_col(|col| { - col.storage.db.execute(&req.sql, req.args)?; - Ok(col.storage.db.last_insert_rowid()) - }) - }) - .map_err(|err| { - let backend_err = anki_error_to_proto_error(err, &backend.backend.i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - bytes - })?; - - let mut out_bytes : Vec = Vec::new(); - pb::Int64 { val: req }.encode(&mut out_bytes); - Ok(out_bytes) -} - -pub(crate) fn query_for_affected(in_bytes: &Vec, backend: &mut AnkiDroidBackend) -> Result, Vec> { - - let req = get_args(&in_bytes) - .and_then(|req| { - backend.backend.with_col(|col| { - Ok(col.storage.db.execute(&req.sql, req.args)?) - }) - }) - .map_err(|err| { - let backend_err = anki_error_to_proto_error(err, &backend.backend.i18n); - let mut bytes = Vec::new(); - backend_err.encode(&mut bytes).unwrap(); - bytes - })?; - - let mut out_bytes : Vec = Vec::new(); - let as_i32 : i32 = req as i32; - pb::Int32 { val: as_i32 }.encode(&mut out_bytes); - Ok(out_bytes) -} diff --git a/tools/protoc-gen/protoc-gen.py b/tools/protoc-gen/protoc-gen.py index dca687527..8d347f878 100755 --- a/tools/protoc-gen/protoc-gen.py +++ b/tools/protoc-gen/protoc-gen.py @@ -4,69 +4,96 @@ # This is the serialization mechanism/calling conventions for protobufs between rslib and rsdroid import sys +import stringcase from google.protobuf.compiler import plugin_pb2 as plugin # Needs map<> and Fluent import rather than Backend ignore_methods_accepting = ["TranslateStringIn"] -backend_name = "Backend" +def fix_namespace(f): + return f.replace(".anki.", "anki.") -class Method: - def __init__(self, method): - self.method = method - self.fields = method.field + +def basename(f): + return f.split(".")[-1] + + +def proto_name_to_symbol(f): + base = f.replace(".proto", "") + return base.replace("anki/", "") + +def proto_name_to_package(f): + base = f.replace(".proto", "") + head = base.replace("anki/", "anki.") + return head + +def get_annotation(type, optional=False): + primitive = type not in [9, 12, 11, 14] + if primitive: + return "" + elif optional: + return "@Nullable" + else: + return "@NonNull" + +class Message: + def __init__(self, message, proto_file): + self.method = message + self.fields = message.field + self.proto_file = proto_file # https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.descriptor def to_java_type(self, type, field): ### Converts a given protobuf type to the Java Equivalent: (int, or List for example) ### primitive_list = { - 1: "double", - 2 : "float", - 3 : "long", - # 4 : "uint64", - 5 : "int", - 8 : "boolean", - 9 : "java.lang.String", - 12 : "com.google.protobuf.ByteString", - 13: "int", #"uint32" - 17: "int", - 18: "long" - } - - def fix_namespace(f): - return f.replace(".BackendProto.", "Backend.") - + 1: "Double", + 2: "Float", + 3: "Long", + # 4 : "uint64", + 5: "Int", + 8: "Boolean", + 9: "String", + 12: "com.google.protobuf.ByteString", + 13: "Int", # "uint32" + 17: "Int", + 18: "Long", + } if self.is_repeating(field): - primitive_map = {1 : "Double", - 2 : "Float", - 3 : "Long", - 5 : "Integer", - 8 : "Boolean", - 13: "Integer"} + primitive_map = { + 1: "Double", + 2: "Float", + 3: "Long", + 5: "Int", + 8: "Boolean", + 13: "Int", + } if type != 14 and type != 11: - new_type = primitive_map[type] if type in primitive_map else primitive_list[type] + new_type = ( + primitive_map[type] + if type in primitive_map + else primitive_list[type] + ) else: new_type = fix_namespace(field.type_name) - return "java.util.List<{}>".format(new_type) + return "Iterable<{}>".format(new_type) - if type == 14 or type == 11: # enum/message + if type == 14 or type == 11: # enum/message return fix_namespace(field.type_name) return primitive_list[type] - def label_to_annotation(self, label): - return { - 1: "@Nullable ", # LABEL_OPTIONAL - 3: "" # LABEL_REPEATED - }[label] def as_param(self, field): - annotation = self.label_to_annotation(field.label) - # avoid annotations for primitives - if field.type not in [9, 12, 11, 14]: - annotation = "" - return "{}{} {}".format(annotation, self.to_java_type(field.type, field), field.json_name) + optional = getattr(field, "proto3_optional") + annotation = "?" if optional else "" + name = field.json_name + if name == "val": + name = "`val`" + + return "{}: {}{}".format( + name, self.to_java_type(field.type, field), annotation, + ) def is_repeating(self, field): return field.label == 3 @@ -76,8 +103,15 @@ def as_setter_name(self, field): return field[0].upper() + field[1:] def as_setter(self, field): + optional = getattr(field, "proto3_optional", False) prefix = "set" if not self.is_repeating(field) else "addAll" - return ".{}{}({})".format(prefix, self.as_setter_name(field.json_name), field.json_name) + name = field.json_name + if name == "val": + name = "`val`" + + return ".{}{}({})".format( + prefix, self.as_setter_name(field.json_name), "it" if optional else name + ) def getFieldSetters(self): return [(self.as_setter(f), f) for f in self.fields] @@ -88,26 +122,33 @@ def is_primitive(self, field): def as_builder(self): # we can't set fields to null, so we can't use the builder fluent syntax. - ret = "{namespace}.{methodName}.Builder builder = {namespace}.{methodName}.newBuilder();\n".format(methodName=self.method.name, namespace=backend_name) + ret = "val builder = {namespace}.{methodName}.newBuilder();\n".format( + methodName=self.method.name, namespace=self.proto_file.package + ) for setter, field in self.getFieldSetters(): - if self.is_primitive(field): - ret += " builder{};\n".format(setter) + if not getattr(field, "proto3_optional", False): # self.is_primitive(field): + ret += " builder{};\n".format(setter) else: - ret += " if ({} != null) {{ builder{}; }}\n".format(field.json_name, setter) - - - ret += " {}.{} protobuf = builder.build();\n".format(backend_name, self.method.name) - return ret + ret += " {}?.let {{ builder{}; }}\n".format( + field.json_name, setter + ) + ret += " val protobuf = builder.build();\n".format( + self.proto_file.package, self.method.name + ) + return ret.replace("I18n.", "I18N.") def as_params(self): return ", ".join([self.as_param(f) for f in self.fields]) + class RPC: - def __init__(self, service, command_num, methods): + def __init__(self, service, service_index, command_num, messages, proto_file): self.method = service + self.service_index = service_index self.command_num = command_num - self.method_lookup = methods + self.messages = messages + self.proto_file = proto_file def is_valid(self): return self.get_input() and self.get_output() @@ -122,241 +163,143 @@ def parse(self, str, input): def parse_input(str): if str == ".BackendProto.Empty": return "()" - return "(" + str.replace(".BackendProto.", backend_name + ".") + " args)" + return "(" + fix_namespace(str) + " args)" @staticmethod def parse_output(str): - if str == ".BackendProto.Empty": + str = fix_namespace(str) + if str == "anki.generic.Empty": return "void" - return str.replace(".BackendProto.", backend_name + ".") + return str def get_input(self): return self.parse(self.method.input_type, True) def get_output(self): return self.parse(self.method.output_type, False) - + def as_command_name(self): - return "case {}: return \"{}\";".format(self.command_num, self.method_name()) - - def as_interface(self): - if self.get_output() == "void": - k = self.method.input_type.replace(".BackendProto.", "") - if k in self.method_lookup: - return " {out} {name}{inv};".format(out=self.get_output(), name=self.method_name(), inv="({})".format(self.method_lookup[k].as_params())) - else: - return " {out} {name}{inv};".format(out=self.get_output(), name=self.method_name(), inv=self.get_input()) - else: - k = self.method.input_type.replace(".BackendProto.", "") - if k in self.method_lookup: - return " {out} {name}{inv};".format(out=self.get_output(), name=self.method_name(), inv="({})".format(self.method_lookup[k].as_params())) - else: - return " {out} {name}{inv};".format(out=self.get_output(), name=self.method_name(), inv=self.get_input()) + return 'case {}: return "{}";'.format(self.command_num, self.method_name()) def __repr__(self): - args = "args.toByteArray()" if self.get_input() != "()" else "Backend.Empty.getDefaultInstance().toByteArray()" - # These previously were very different - validation changed this. - # Might want to merge these branches now they're so similar. - - if self.get_output() == "void": - k = self.method.input_type.replace(".BackendProto.", "") - if k in self.method_lookup: - return " public {out} {name}{inv} {{ \n" \ - " byte[] result = null;\n" \ - " try {{\n" \ - " {deser}\n" \ - " Pointer backendPointer = ensureBackend();\n" \ - " result = executeCommand(backendPointer.toJni(), {num}, protobuf.toByteArray());\n" \ - " Backend.Empty message = Backend.Empty.parseFrom(result);\n" \ - " validateMessage(result, message);\n" \ - " }} catch (InvalidProtocolBufferException ex) {{\n" \ - " validateResult(result);\n" \ - " throw BackendException.fromException(ex);\n" \ - " }}\n" \ - " }}".format(out=self.get_output(), name=self.method_name(), inv="({})".format(self.method_lookup[k].as_params()), - num=self.command_num, - deser=self.method_lookup[k].as_builder()) + k = fix_namespace(self.method.input_type) + + out=self.get_output() + name=self.method_name() + inv="({})".format(self.messages[k].as_params()) + service=self.service_index + method=self.command_num + deser=self.messages[k].as_builder() + + buf = f""" +@Throws(BackendException::class) +fun {name}Raw(input: ByteArray): ByteArray {{ + return runMethodRaw({service}, {method}, input); +}} +""" + + if k != "anki.i18n.TranslateStringRequest": + # maps not currently supported + if out == "void": + return_segment = f""" + {name}Raw(protobuf.toByteArray()); + """ + out_with_colon = "" else: - return " public {out} {name}{inv} {{ \n" \ - " byte[] result = null;\n" \ - " try {{\n" \ - " Pointer backendPointer = ensureBackend();\n" \ - " result = executeCommand(backendPointer.toJni(), {num}, {args});\n" \ - " Backend.Empty message = Backend.Empty.parseFrom(result);\n" \ - " validateMessage(result, message);\n" \ - " }} catch (InvalidProtocolBufferException ex) {{\n" \ - " validateResult(result);\n" \ - " throw BackendException.fromException(ex);\n" \ - " }}\n" \ - " }}".format(out=self.get_output(), name=self.method_name(), inv=self.get_input(), - num=self.command_num, - args=args) - else: - k = self.method.input_type.replace(".BackendProto.", "") - if k in self.method_lookup: - return " public {out} {name}{inv} {{ \n" \ - " byte[] result = null;\n" \ - " try {{\n" \ - " {deser}\n" \ - " Pointer backendPointer = ensureBackend();\n" \ - " result = executeCommand(backendPointer.toJni(), {num}, protobuf.toByteArray());\n" \ - " {out} message = {out}.parseFrom(result);\n" \ - " validateMessage(result, message);\n" \ - " return message;\n" \ - " }} catch (InvalidProtocolBufferException ex) {{\n" \ - " validateResult(result);\n" \ - " throw BackendException.fromException(ex);\n" \ - " }}\n" \ - " }}".format(out=self.get_output(), name=self.method_name(), inv="({})".format(self.method_lookup[k].as_params()), - num=self.command_num, - deser=self.method_lookup[k].as_builder()) - else: - # Definitely empty - and TranslateStringIn (manually ignored) - return " public {out} {name}{inv} {{ \n" \ - " byte[] result = null;\n" \ - " try {{\n" \ - " Pointer backendPointer = ensureBackend();\n" \ - " result = executeCommand(backendPointer.toJni(), {num}, {args});\n" \ - " {out} message = {out}.parseFrom(result);\n" \ - " validateMessage(result, message);\n" \ - " return message;\n" \ - " }} catch (InvalidProtocolBufferException ex) {{\n" \ - " validateResult(result);\n" \ - " throw BackendException.fromException(ex);\n" \ - " }}\n" \ - " }}".format(out=self.get_output(), name=self.method_name(), inv=self.get_input(), - num=self.command_num, - args=args) + out_with_colon = f": {out}" + return_segment = f"""\ + try {{ + return {out}.parseFrom({name}Raw(protobuf.toByteArray())); + }} catch (exc: com.google.protobuf.InvalidProtocolBufferException) {{ + throw BackendException("protobuf parsing failed"); + }}""" + + buf += f""" +@Throws(BackendException::class) +open fun {name}{inv}{out_with_colon} {{ + {deser} + {return_segment} +}} + """ + return buf def method_name(self): return self.method.name[0].lower() + self.method.name[1:] -def traverse(proto_file): +def gather_classes(proto_file, all_messages, service_index): classes = [] - allowed_params = {"NoteTypeID", "NoteID", "CardID", "DeckID", "DeckConfigID", "String", "Bool", "Int32", "Int64"} - methods = [Method(m) for m in proto_file.message_type if m.name.endswith("In") or m.name in allowed_params] - - method_lookup = {item.method.name: item for item in set(methods) if item.method.name not in ignore_methods_accepting} for f in proto_file.service: for i, m in enumerate(f.method): - cls = RPC(m, i + 1, method_lookup) + cls = RPC(m, service_index, i, all_messages, proto_file) if not cls.is_valid(): raise ValueError(str(m)) classes.append(cls) return classes + def logRepr(s): sys.stderr.write("\n".join(dir(s))) -def log(s): - sys.stderr.write(str(s) + "\n") - -def gen_backend_methods(file, proto_file, methods, class_name): - contents = ["/*\n " - " This class was autogenerated from {} by {}\n" - " Please Rebuild project to regenerate." - " */\n\n" - "package net.ankiweb.rsdroid;\n\n" - "import androidx.annotation.Nullable;\n\n" - "import BackendProto.Backend;\n\n" - "public class {} {{" - " public static String commandName(int command) {{\n" - " switch (command) {{".format(proto_file.name, __file__, class_name)] - - for method in methods: - contents.append(" " + str(method.as_command_name())) - - - contents.append(" default: return \"unknown: \" + command;\n }\n}") - contents.append("\n}") - file = response.file.add() - file.name = class_name + ".java" - file.content = "\n".join(contents) + +def log(*args): + print(*args, file=open("/tmp/log.txt", "a")) + def generate_code(request, response): - global backend_name + # gather all messages and service indexes + all_messages = {} + service_index = {} for proto_file in request.proto_file: + for message in proto_file.message_type: + all_messages[f"{proto_file.package}.{message.name}"] = Message(message, proto_file) + for enum in proto_file.enum_type: + if enum.name == "ServiceIndex": + for value in enum.value: + pkg = value.name.replace("SERVICE_INDEX_", "").lower() + if pkg == "deck_config": + pkg = "deckconfig" + service_index[f"anki.{pkg}"] = value.number - service_methods = traverse(proto_file) - if not service_methods: + file_contents = [ + """ +/* Auto-generated from the .proto files in AnkiDroidBackend. */ + +package anki.backend; + +import com.google.protobuf.InvalidProtocolBufferException; +import com.google.protobuf.GeneratedMessageV3; + +import net.ankiweb.rsdroid.BackendException; + +public abstract class GeneratedBackend { + +@Throws(BackendException::class) +protected abstract fun runMethodRaw(service: Int, method: Int, input: ByteArray): ByteArray; + +""" + ] + + for proto_file in request.proto_file: + if not len(proto_file.service): continue - backend_name = proto_file.name.replace(".proto", "") - if backend_name == "backend": - backend_name = "Backend" - - class_name = proto_file.name.capitalize().replace(".proto", "").replace("Backend", "RustBackend") - current_import = "" if backend_name == "Backend" else "import BackendProto.{};".format(backend_name) - file_contents = ["/*\n " - " This class was autogenerated from {} by {}\n" - " Please Rebuild project to regenerate." - " */\n\n" - "package net.ankiweb.rsdroid;\n\n" - "import androidx.annotation.Nullable;\n\n" - "import com.google.protobuf.InvalidProtocolBufferException;\n" - "import com.google.protobuf.GeneratedMessageV3;\n\n" - "import BackendProto.Backend;\n" - "{currentImport}\n\n" - "public abstract class {cls}Impl implements net.ankiweb.rsdroid.{cls} {{\n\n" - " public abstract Pointer ensureBackend();\n\n\n" - " protected void validateMessage(byte[] result, GeneratedMessageV3 message) throws InvalidProtocolBufferException {{\n" - " if (message.getUnknownFields().asMap().isEmpty()) {{\n" - " return;\n" - " }}\n" - " Backend.BackendError ex = Backend.BackendError.parseFrom(result);\n" - " throw BackendException.fromError(ex);\n" - " }}" - "\n" - " protected abstract byte[] executeCommand(long backendPointer, final int command, byte[] args);\n" - "\n" - " protected void validateResult(@Nullable byte[] result) {{\n" - " if (result == null) {{\n" - " return;\n" - " }}\n" - " try {{\n" - " Backend.BackendError ex = Backend.BackendError.parseFrom(result);\n" - " throw BackendException.fromError(ex);\n" - " }} catch (InvalidProtocolBufferException e) {{\n" - " // ignore - throw the original exception\n" - " }}\n" - " }}".format(proto_file.name, __file__, cls=class_name, currentImport = current_import)] + service_methods = gather_classes(proto_file, all_messages, service_index[proto_file.package]) + if not service_methods: + continue for method in service_methods: file_contents.append("\n\n" + str(method)) - - file_contents.append("\n}") - - # Fill response - f = response.file.add() - f.name = class_name + "Impl.java" - f.content = "\n".join(file_contents) - - # generate interface (if methods) - - iface_contents = ["/*\n " - " This class was autogenerated from {} by {}\n" - " Please Rebuild project to regenerate." - " */\n\n" - "package net.ankiweb.rsdroid;\n\n" - "import androidx.annotation.Nullable;\n\n" - "import BackendProto.{backend};\n\n" - "public interface {} {{".format(proto_file.name, __file__, class_name, backend=backend_name)] - for method in service_methods: - iface_contents.append("\n" + str(method.as_interface())) - iface_contents.append("\n}") - iface = response.file.add() - iface.name = class_name + ".java" - iface.content = "\n".join(iface_contents) - # generate BackendMethods - - gen_backend_methods(response.file.add(), proto_file, service_methods, class_name + "Methods") + file_contents.append("\n}") + f = response.file.add() + f.name = "GeneratedBackend.kt" + f.content = "\n".join(file_contents) -if __name__ == '__main__': +if __name__ == "__main__": # Read request message from stdin data = sys.stdin.buffer.read() @@ -366,6 +309,8 @@ def generate_code(request, response): # Create response response = plugin.CodeGeneratorResponse() + # fixme: check these are actually being handled + response.supported_features |= plugin.CodeGeneratorResponse.FEATURE_PROTO3_OPTIONAL # Generate code generate_code(request, response) diff --git a/tools/protoc-gen/test/.gitignore b/tools/protoc-gen/test/.gitignore deleted file mode 100644 index 466e24805..000000000 --- a/tools/protoc-gen/test/.gitignore +++ /dev/null @@ -1 +0,0 @@ -out/ \ No newline at end of file diff --git a/tools/protoc-gen/test/backend.proto b/tools/protoc-gen/test/backend.proto deleted file mode 100644 index 6746726ce..000000000 --- a/tools/protoc-gen/test/backend.proto +++ /dev/null @@ -1,1021 +0,0 @@ -syntax = "proto3"; - -import "fluent.proto"; - -package BackendProto; - -option java_generic_services = true; - - -// Generic containers -/////////////////////////////////////////////////////////// - -message Empty {} - -message OptionalInt32 { - sint32 val = 1; -} - -message OptionalUInt32 { - uint32 val = 1; -} - -message Int32 { - sint32 val = 1; -} - -message UInt32 { - uint32 val = 1; -} - -message Int64 { - int64 val = 1; -} - -message String { - string val = 1; -} - -message Json { - bytes json = 1; -} - -message Bool { - bool val = 1; -} - -// IDs used in RPC calls -/////////////////////////////////////////////////////////// - -message NoteTypeID { - int64 ntid = 1; -} - -message NoteID { - int64 nid = 1; -} - -message CardID { - int64 cid = 1; -} - -message DeckID { - int64 did = 1; -} - -message DeckConfigID { - int64 dcid = 1; -} - -// New style RPC definitions -/////////////////////////////////////////////////////////// - -service BackendService { - rpc LatestProgress (Empty) returns (Progress); - rpc SetWantsAbort (Empty) returns (Empty); - - // card rendering - - rpc ExtractAVTags (ExtractAVTagsIn) returns (ExtractAVTagsOut); - rpc ExtractLatex (ExtractLatexIn) returns (ExtractLatexOut); - rpc GetEmptyCards (Empty) returns (EmptyCardsReport); - rpc RenderExistingCard (RenderExistingCardIn) returns (RenderCardOut); - rpc RenderUncommittedCard (RenderUncommittedCardIn) returns (RenderCardOut); - rpc StripAVTags (String) returns (String); - - // searching - - rpc SearchCards (SearchCardsIn) returns (SearchCardsOut); - rpc SearchNotes (SearchNotesIn) returns (SearchNotesOut); - rpc FindAndReplace (FindAndReplaceIn) returns (UInt32); - - // scheduling - - rpc LocalMinutesWest (Int64) returns (Int32); - rpc SetLocalMinutesWest (Int32) returns (Empty); - rpc SchedTimingToday (Empty) returns (SchedTimingTodayOut); - rpc StudiedToday (StudiedTodayIn) returns (String); - rpc CongratsLearnMessage (CongratsLearnMessageIn) returns (String); - rpc UpdateStats (UpdateStatsIn) returns (Empty); - rpc ExtendLimits (ExtendLimitsIn) returns (Empty); - rpc CountsForDeckToday (DeckID) returns (CountsForDeckTodayOut); - - // stats - - rpc CardStats (CardID) returns (String); - rpc Graphs(GraphsIn) returns (GraphsOut); - - // media - - rpc CheckMedia (Empty) returns (CheckMediaOut); - rpc TrashMediaFiles (TrashMediaFilesIn) returns (Empty); - rpc AddMediaFile (AddMediaFileIn) returns (String); - rpc EmptyTrash (Empty) returns (Empty); - rpc RestoreTrash (Empty) returns (Empty); - - // decks - - rpc AddOrUpdateDeckLegacy (AddOrUpdateDeckLegacyIn) returns (DeckID); - rpc DeckTree (DeckTreeIn) returns (DeckTreeNode); - rpc DeckTreeLegacy (Empty) returns (Json); - rpc GetAllDecksLegacy (Empty) returns (Json); - rpc GetDeckIDByName (String) returns (DeckID); - rpc GetDeckLegacy (DeckID) returns (Json); - rpc GetDeckNames (GetDeckNamesIn) returns (DeckNames); - rpc NewDeckLegacy (Bool) returns (Json); - rpc RemoveDeck (DeckID) returns (Empty); - - // deck config - - rpc AddOrUpdateDeckConfigLegacy (AddOrUpdateDeckConfigLegacyIn) returns (DeckConfigID); - rpc AllDeckConfigLegacy (Empty) returns (Json); - rpc GetDeckConfigLegacy (DeckConfigID) returns (Json); - rpc NewDeckConfigLegacy (Empty) returns (Json); - rpc RemoveDeckConfig (DeckConfigID) returns (Empty); - - // cards - - rpc GetCard (CardID) returns (Card); - rpc UpdateCard (Card) returns (Empty); - rpc AddCard (Card) returns (CardID); - rpc RemoveCards (RemoveCardsIn) returns (Empty); - - // notes - - rpc NewNote (NoteTypeID) returns (Note); - rpc AddNote (AddNoteIn) returns (NoteID); - rpc UpdateNote (Note) returns (Empty); - rpc GetNote (NoteID) returns (Note); - rpc RemoveNotes (RemoveNotesIn) returns (Empty); - rpc AddNoteTags (AddNoteTagsIn) returns (UInt32); - rpc UpdateNoteTags (UpdateNoteTagsIn) returns (UInt32); - rpc ClozeNumbersInNote (Note) returns (ClozeNumbersInNoteOut); - rpc AfterNoteUpdates (AfterNoteUpdatesIn) returns (Empty); - rpc FieldNamesForNotes (FieldNamesForNotesIn) returns (FieldNamesForNotesOut); - rpc NoteIsDuplicateOrEmpty (Note) returns (NoteIsDuplicateOrEmptyOut); - - // note types - - rpc AddOrUpdateNotetype (AddOrUpdateNotetypeIn) returns (NoteTypeID); - rpc GetStockNotetypeLegacy (GetStockNotetypeIn) returns (Json); - rpc GetNotetypeLegacy (NoteTypeID) returns (Json); - rpc GetNotetypeNames (Empty) returns (NoteTypeNames); - rpc GetNotetypeNamesAndCounts (Empty) returns (NoteTypeUseCounts); - rpc GetNotetypeIDByName (String) returns (NoteTypeID); - rpc RemoveNotetype (NoteTypeID) returns (Empty); - - // collection - - rpc OpenCollection (OpenCollectionIn) returns (Empty); - rpc CloseCollection (CloseCollectionIn) returns (Empty); - rpc CheckDatabase (Empty) returns (CheckDatabaseOut); - - // sync - - // rpc SyncMedia (SyncAuth) returns (Empty); - // rpc AbortSync (Empty) returns (Empty); - // rpc AbortMediaSync (Empty) returns (Empty); - rpc BeforeUpload (Empty) returns (Empty); - // rpc SyncLogin (SyncLoginIn) returns (SyncAuth); - // rpc SyncStatus (SyncAuth) returns (SyncStatusOut); - // rpc SyncCollection (SyncAuth) returns (SyncCollectionOut); - // rpc FullUpload (SyncAuth) returns (Empty); - // rpc FullDownload (SyncAuth) returns (Empty); - - // translation/messages - - rpc TranslateString (TranslateStringIn) returns (String); - rpc FormatTimespan (FormatTimespanIn) returns (String); - rpc I18nResources (Empty) returns (Json); - - // tags - - rpc RegisterTags (RegisterTagsIn) returns (Bool); - rpc AllTags (Empty) returns (AllTagsOut); - - // config/preferences - - rpc GetConfigJson (String) returns (Json); - rpc SetConfigJson (SetConfigJsonIn) returns (Empty); - rpc RemoveConfig (String) returns (Empty); - rpc SetAllConfig (Json) returns (Empty); - rpc GetAllConfig (Empty) returns (Json); - rpc GetPreferences (Empty) returns (Preferences); - rpc SetPreferences (Preferences) returns (Empty); -} - -// Protobuf stored in .anki2 files -// These should be moved to a separate file in the future -/////////////////////////////////////////////////////////// - -message DeckConfigInner { - enum NewCardOrder { - NEW_CARD_ORDER_DUE = 0; - NEW_CARD_ORDER_RANDOM = 1; - } - - enum LeechAction { - LEECH_ACTION_SUSPEND = 0; - LEECH_ACTION_TAG_ONLY = 1; - } - - repeated float learn_steps = 1; - repeated float relearn_steps = 2; - - reserved 3 to 8; - - uint32 new_per_day = 9; - uint32 reviews_per_day = 10; - - float initial_ease = 11; - float easy_multiplier = 12; - float hard_multiplier = 13; - float lapse_multiplier = 14; - float interval_multiplier = 15; - - uint32 maximum_review_interval = 16; - uint32 minimum_review_interval = 17; - - uint32 graduating_interval_good = 18; - uint32 graduating_interval_easy = 19; - - NewCardOrder new_card_order = 20; - - LeechAction leech_action = 21; - uint32 leech_threshold = 22; - - bool disable_autoplay = 23; - uint32 cap_answer_time_to_secs = 24; - uint32 visible_timer_secs = 25; - bool skip_question_when_replaying_answer = 26; - - bool bury_new = 27; - bool bury_reviews = 28; - - bytes other = 255; -} - -message DeckCommon { - bool study_collapsed = 1; - bool browser_collapsed = 2; - - uint32 last_day_studied = 3; - int32 new_studied = 4; - int32 review_studied = 5; - int32 milliseconds_studied = 7; - - // previously set in the v1 scheduler, - // but not currently used for anything - int32 learning_studied = 6; - - bytes other = 255; -} - -message DeckKind { - oneof kind { - NormalDeck normal = 1; - FilteredDeck filtered = 2; - } -} - -message NormalDeck { - int64 config_id = 1; - uint32 extend_new = 2; - uint32 extend_review = 3; - string description = 4; -} - -message FilteredDeck { - bool reschedule = 1; - repeated FilteredSearchTerm search_terms = 2; - // v1 scheduler only - repeated float delays = 3; - // v2 scheduler only - uint32 preview_delay = 4; -} - -message FilteredSearchTerm { - enum FilteredSearchOrder { - FILTERED_SEARCH_ORDER_OLDEST_FIRST = 0; - FILTERED_SEARCH_ORDER_RANDOM = 1; - FILTERED_SEARCH_ORDER_INTERVALS_ASCENDING = 2; - FILTERED_SEARCH_ORDER_INTERVALS_DESCENDING = 3; - FILTERED_SEARCH_ORDER_LAPSES = 4; - FILTERED_SEARCH_ORDER_ADDED = 5; - FILTERED_SEARCH_ORDER_DUE = 6; - FILTERED_SEARCH_ORDER_REVERSE_ADDED = 7; - FILTERED_SEARCH_ORDER_DUE_PRIORITY = 8; - } - - string search = 1; - uint32 limit = 2; - FilteredSearchOrder order = 3; -} - -message NoteFieldConfig { - bool sticky = 1; - bool rtl = 2; - string font_name = 3; - uint32 font_size = 4; - - bytes other = 255; -} - -message CardTemplateConfig { - string q_format = 1; - string a_format = 2; - string q_format_browser = 3; - string a_format_browser= 4; - int64 target_deck_id = 5; - string browser_font_name = 6; - uint32 browser_font_size = 7; - - bytes other = 255; -} - -message NoteTypeConfig { - enum Kind { - KIND_NORMAL = 0; - KIND_CLOZE = 1; - } - Kind kind = 1; - uint32 sort_field_idx = 2; - string css = 3; - int64 target_deck_id = 4; - string latex_pre = 5; - string latex_post = 6; - bool latex_svg = 7; - repeated CardRequirement reqs = 8; - - bytes other = 255; -} - -message CardRequirement { - enum Kind { - KIND_NONE = 0; - KIND_ANY = 1; - KIND_ALL = 2; - } - uint32 card_ord = 1; - Kind kind = 2; - repeated uint32 field_ords = 3; -} - -// Containers for passing around database objects -/////////////////////////////////////////////////////////// - -message Deck { - int64 id = 1; - string name = 2; - uint32 mtime_secs = 3; - int32 usn = 4; - DeckCommon common = 5; - oneof kind { - NormalDeck normal = 6; - FilteredDeck filtered = 7; - } -} - -message NoteType { - int64 id = 1; - string name = 2; - uint32 mtime_secs = 3; - sint32 usn = 4; - NoteTypeConfig config = 7; - repeated NoteField fields = 8; - repeated CardTemplate templates = 9; -} - -message NoteField { - OptionalUInt32 ord = 1; - string name = 2; - NoteFieldConfig config = 5; -} - -message CardTemplate { - OptionalUInt32 ord = 1; - string name = 2; - uint32 mtime_secs = 3; - sint32 usn = 4; - CardTemplateConfig config = 5; -} - -message Note { - int64 id = 1; - string guid = 2; - int64 ntid = 3; - uint32 mtime_secs = 4; - int32 usn = 5; - repeated string tags = 6; - repeated string fields = 7; -} - -message Card { - int64 id = 1; - int64 nid = 2; - int64 did = 3; - uint32 ord = 4; - int64 mtime = 5; - sint32 usn = 6; - uint32 ctype = 7; - sint32 queue = 8; - sint32 due = 9; - uint32 ivl = 10; - uint32 factor = 11; - uint32 reps = 12; - uint32 lapses = 13; - uint32 left = 14; - sint32 odue = 15; - int64 odid = 16; - uint32 flags = 17; - string data = 18; -} - -// Backend -/////////////////////////////////////////////////////////// - -message BackendInit { - repeated string preferred_langs = 1; - string locale_folder_path = 2; - bool server = 3; -} - -message I18nBackendInit { - repeated string preferred_langs = 4; - string locale_folder_path = 5; -} - -// Errors -/////////////////////////////////////////////////////////// - -message BackendError { - // localized error description suitable for displaying to the user - string localized = 1; - // error specifics - oneof value { - Empty invalid_input = 2; - Empty template_parse = 3; - Empty io_error = 4; - Empty db_error = 5; - NetworkError network_error = 6; - SyncError sync_error = 7; - // user interrupted operation - Empty interrupted = 8; - string json_error = 9; - string proto_error = 10; - Empty not_found_error = 11; - Empty exists = 12; - Empty deck_is_filtered = 13; - } -} - -message NetworkError { - enum NetworkErrorKind { - OTHER = 0; - OFFLINE = 1; - TIMEOUT = 2; - PROXY_AUTH = 3; - } - NetworkErrorKind kind = 1; -} - -message SyncError { - enum SyncErrorKind { - OTHER = 0; - CONFLICT = 1; - SERVER_ERROR = 2; - CLIENT_TOO_OLD = 3; - AUTH_FAILED = 4; - SERVER_MESSAGE = 5; - MEDIA_CHECK_REQUIRED = 6; - RESYNC_REQUIRED = 7; - CLOCK_INCORRECT = 8; - DATABASE_CHECK_REQUIRED = 9; - } - SyncErrorKind kind = 1; -} - -// Progress -/////////////////////////////////////////////////////////// - -message Progress { - oneof value { - Empty none = 1; - MediaSyncProgress media_sync = 2; - string media_check = 3; - FullSyncProgress full_sync = 4; - NormalSyncProgress normal_sync = 5; - DatabaseCheckProgress database_check = 6; - } -} - -message MediaSyncProgress { - string checked = 1; - string added = 2; - string removed = 3; -} - -message FullSyncProgress { - uint32 transferred = 1; - uint32 total = 2; -} - -message MediaSyncUploadProgress { - uint32 files = 1; - uint32 deletions = 2; -} - -message NormalSyncProgress { - string stage = 1; - string added = 2; - string removed = 3; -} - -message DatabaseCheckProgress { - string stage = 1; - uint32 stage_total = 2; - uint32 stage_current = 3; -} - - -// Messages -/////////////////////////////////////////////////////////// - - -message SchedTimingTodayOut { - uint32 days_elapsed = 1; - int64 next_day_at = 2; -} - -message DeckTreeIn { - // if non-zero, counts for the provided timestamp will be included - int64 now = 1; - int64 top_deck_id = 2; -} - -message DeckTreeNode { - int64 deck_id = 1; - string name = 2; - repeated DeckTreeNode children = 3; - uint32 level = 4; - bool collapsed = 5; - - uint32 review_count = 6; - uint32 learn_count = 7; - uint32 new_count = 8; - - bool filtered = 16; -} - -message RenderExistingCardIn { - int64 card_id = 1; - bool browser = 2; -} - -message RenderUncommittedCardIn { - Note note = 1; - uint32 card_ord = 2; - bytes template = 3; - bool fill_empty = 4; -} - -message RenderCardOut { - repeated RenderedTemplateNode question_nodes = 1; - repeated RenderedTemplateNode answer_nodes = 2; -} - -message RenderedTemplateNode { - oneof value { - string text = 1; - RenderedTemplateReplacement replacement = 2; - } -} - -message RenderedTemplateReplacement { - string field_name = 1; - string current_text = 2; - repeated string filters = 3; -} - -message ExtractAVTagsIn { - string text = 1; - bool question_side = 2; -} - -message ExtractAVTagsOut { - string text = 1; - repeated AVTag av_tags = 2; -} - -message AVTag { - oneof value { - string sound_or_video = 1; - TTSTag tts = 2; - } -} - -message TTSTag { - string field_text = 1; - string lang = 2; - repeated string voices = 3; - float speed = 4; - repeated string other_args = 5; -} - -message ExtractLatexIn { - string text = 1; - bool svg = 2; - bool expand_clozes = 3; -} - -message ExtractLatexOut { - string text = 1; - repeated ExtractedLatex latex = 2; -} - -message ExtractedLatex { - string filename = 1; - string latex_body = 2; -} - -message AddMediaFileIn { - string desired_name = 1; - bytes data = 2; -} - -message CheckMediaOut { - repeated string unused = 1; - repeated string missing = 2; - string report = 3; - bool have_trash = 4; -} - -message TrashMediaFilesIn { - repeated string fnames = 1; -} - -message TranslateStringIn { - FluentString key = 2; - map args = 3; -} - -message TranslateArgValue { - oneof value { - string str = 1; - double number = 2; - } -} - -message FormatTimespanIn { - enum Context { - PRECISE = 0; - ANSWER_BUTTONS = 1; - INTERVALS = 2; - } - - float seconds = 1; - Context context = 2; -} - -message StudiedTodayIn { - uint32 cards = 1; - double seconds = 2; -} - -message CongratsLearnMessageIn { - float next_due = 1; - uint32 remaining = 2; -} - -message OpenCollectionIn { - string collection_path = 1; - string media_folder_path = 2; - string media_db_path = 3; - string log_path = 4; -} - -message SearchCardsIn { - string search = 1; - SortOrder order = 2; -} - -message SearchCardsOut { - repeated int64 card_ids = 1; - -} - -message SortOrder { - oneof value { - Empty from_config = 1; - Empty none = 2; - string custom = 3; - BuiltinSearchOrder builtin = 4; - } -} - -message SearchNotesIn { - string search = 1; -} - -message SearchNotesOut { - repeated int64 note_ids = 2; -} - -message BuiltinSearchOrder { - enum BuiltinSortKind { - NOTE_CREATION = 0; - NOTE_MOD = 1; - NOTE_FIELD = 2; - NOTE_TAGS = 3; - NOTE_TYPE = 4; - CARD_MOD = 5; - CARD_REPS = 6; - CARD_DUE = 7; - CARD_EASE = 8; - CARD_LAPSES = 9; - CARD_INTERVAL = 10; - CARD_DECK = 11; - CARD_TEMPLATE = 12; - } - BuiltinSortKind kind = 1; - bool reverse = 2; -} - -message CloseCollectionIn { - bool downgrade_to_schema11 = 1; -} - -message AddOrUpdateDeckConfigLegacyIn { - bytes config = 1; - bool preserve_usn_and_mtime = 2; -} - -message RegisterTagsIn { - string tags = 1; - bool preserve_usn = 2; - int32 usn = 3; - bool clear_first = 4; -} - -message AllTagsOut { - repeated TagUsnTuple tags = 1; -} - -message TagUsnTuple { - string tag = 1; - sint32 usn = 2; -} - -message GetChangedTagsOut { - repeated string tags = 1; -} - -message SetConfigJsonIn { - string key = 1; - bytes value_json = 2; -} - -enum StockNoteType { - STOCK_NOTE_TYPE_BASIC = 0; - STOCK_NOTE_TYPE_BASIC_AND_REVERSED = 1; - STOCK_NOTE_TYPE_BASIC_OPTIONAL_REVERSED = 2; - STOCK_NOTE_TYPE_BASIC_TYPING = 3; - STOCK_NOTE_TYPE_CLOZE = 4; -} - -message GetStockNotetypeIn { - StockNoteType kind = 1; -} - -message NoteTypeNames { - repeated NoteTypeNameID entries = 1; -} - -message NoteTypeUseCounts { - repeated NoteTypeNameIDUseCount entries = 1; -} - -message NoteTypeNameID { - int64 id = 1; - string name = 2; - -} - -message NoteTypeNameIDUseCount { - int64 id = 1; - string name = 2; - uint32 use_count = 3; -} - -message AddOrUpdateNotetypeIn { - bytes json = 1; - bool preserve_usn_and_mtime = 2; -} - -message AddNoteIn { - Note note = 1; - int64 deck_id = 2; -} - -message EmptyCardsReport { - string report = 1; - repeated NoteWithEmptyCards notes = 2; -} - -message NoteWithEmptyCards { - int64 note_id = 1; - repeated int64 card_ids = 2; - bool will_delete_note = 3; -} - -message DeckNames { - repeated DeckNameID entries = 1; -} - -message DeckNameID { - int64 id = 1; - string name = 2; -} - -message AddOrUpdateDeckLegacyIn { - bytes deck = 1; - bool preserve_usn_and_mtime = 2; -} - -message FieldNamesForNotesIn { - repeated int64 nids = 1; -} - -message FieldNamesForNotesOut { - repeated string fields = 1; -} - -message FindAndReplaceIn { - repeated int64 nids = 1; - string search = 2; - string replacement = 3; - bool regex = 4; - bool match_case = 5; - string field_name = 6; -} - -message AfterNoteUpdatesIn { - repeated int64 nids = 1; - bool mark_notes_modified = 2; - bool generate_cards = 3; -} - -message AddNoteTagsIn { - repeated int64 nids = 1; - string tags = 2; -} - -message UpdateNoteTagsIn { - repeated int64 nids = 1; - string tags = 2; - string replacement = 3; - bool regex = 4; -} - -message CheckDatabaseOut { - repeated string problems = 1; -} - -message CollectionSchedulingSettings { - enum NewReviewMix { - DISTRIBUTE = 0; - REVIEWS_FIRST = 1; - NEW_FIRST = 2; - } - - uint32 scheduler_version = 1; - uint32 rollover = 2; - uint32 learn_ahead_secs = 3; - NewReviewMix new_review_mix = 4; - bool show_remaining_due_counts = 5; - bool show_intervals_on_buttons = 6; - uint32 time_limit_secs = 7; - - // v2 only - bool new_timezone = 8; - bool day_learn_first = 9; -} - -message Preferences { - CollectionSchedulingSettings sched = 1; -} - -message ClozeNumbersInNoteOut { - repeated uint32 numbers = 1; -} - -message GetDeckNamesIn { - bool skip_empty_default = 1; - // if unset, implies skip_empty_default - bool include_filtered = 2; -} - -message NoteIsDuplicateOrEmptyOut { - enum State { - NORMAL = 0; - EMPTY = 1; - DUPLICATE = 2; - } - State state = 1; -} - -message SyncLoginIn { - string username = 1; - string password = 2; -} - -message SyncStatusOut { - enum Required { - NO_CHANGES = 0; - NORMAL_SYNC = 1; - FULL_SYNC = 2; - } - Required required = 1; -} - -message SyncCollectionOut { - enum ChangesRequired { - NO_CHANGES = 0; - NORMAL_SYNC = 1; - FULL_SYNC = 2; - // local collection has no cards; upload not an option - FULL_DOWNLOAD = 3; - // remote collection has no cards; download not an option - FULL_UPLOAD = 4; - } - - uint32 host_number = 1; - string server_message = 2; - ChangesRequired required = 3; -} - -message SyncAuth { - string hkey = 1; - uint32 host_number = 2; -} - -message RemoveNotesIn { - repeated int64 note_ids = 1; - repeated int64 card_ids = 2; -} - -message RemoveCardsIn { - repeated int64 card_ids = 1; -} - -message UpdateStatsIn { - int64 deck_id = 1; - int32 new_delta = 2; - int32 review_delta = 4; - int32 millisecond_delta = 5; -} - -message ExtendLimitsIn { - int64 deck_id = 1; - int32 new_delta = 2; - int32 review_delta = 3; -} - -message CountsForDeckTodayOut { - int32 new = 1; - int32 review = 2; -} - -message GraphsIn { - string search = 1; - uint32 days = 2; -} - -message GraphsOut { - repeated Card cards = 1; - repeated RevlogEntry revlog = 2; - uint32 days_elapsed = 3; - // Based on rollover hour - uint32 next_day_at_secs = 4; - uint32 scheduler_version = 5; - /// Seconds to add to UTC timestamps to get local time. - int32 local_offset_secs = 7; -} - -message RevlogEntry { - enum ReviewKind { - LEARNING = 0; - REVIEW = 1; - RELEARNING = 2; - EARLY_REVIEW = 3; - } - int64 id = 1; - int64 cid = 2; - int32 usn = 3; - uint32 button_chosen = 4; - int32 interval = 5; - int32 last_interval = 6; - uint32 ease_factor = 7; - uint32 taken_millis = 8; - ReviewKind review_kind = 9; -} diff --git a/tools/protoc-gen/test/fluent.proto b/tools/protoc-gen/test/fluent.proto deleted file mode 100644 index ab4eaace6..000000000 --- a/tools/protoc-gen/test/fluent.proto +++ /dev/null @@ -1,277 +0,0 @@ -// This file is automatically generated as part of the build process. - -syntax = "proto3"; -package BackendProto; -enum FluentString { - CARD_STATS_ADDED = 0; - CARD_STATS_AVERAGE_TIME = 1; - CARD_STATS_CARD_ID = 2; - CARD_STATS_CARD_TEMPLATE = 3; - CARD_STATS_DECK_NAME = 4; - CARD_STATS_EASE = 5; - CARD_STATS_FIRST_REVIEW = 6; - CARD_STATS_INTERVAL = 7; - CARD_STATS_LAPSE_COUNT = 8; - CARD_STATS_LATEST_REVIEW = 9; - CARD_STATS_NEW_CARD_POSITION = 10; - CARD_STATS_NOTE_ID = 11; - CARD_STATS_NOTE_TYPE = 12; - CARD_STATS_REVIEW_COUNT = 13; - CARD_STATS_REVIEW_LOG_DATE = 14; - CARD_STATS_REVIEW_LOG_RATING = 15; - CARD_STATS_REVIEW_LOG_TIME_TAKEN = 16; - CARD_STATS_REVIEW_LOG_TYPE = 17; - CARD_STATS_REVIEW_LOG_TYPE_FILTERED = 18; - CARD_STATS_REVIEW_LOG_TYPE_LEARN = 19; - CARD_STATS_REVIEW_LOG_TYPE_RELEARN = 20; - CARD_STATS_REVIEW_LOG_TYPE_REVIEW = 21; - CARD_STATS_TOTAL_TIME = 22; - CARD_TEMPLATE_RENDERING_BACK_SIDE_PROBLEM = 23; - CARD_TEMPLATE_RENDERING_CONDITIONAL_NOT_CLOSED = 24; - CARD_TEMPLATE_RENDERING_CONDITIONAL_NOT_OPEN = 25; - CARD_TEMPLATE_RENDERING_EMPTY_FRONT = 26; - CARD_TEMPLATE_RENDERING_FRONT_SIDE_PROBLEM = 27; - CARD_TEMPLATE_RENDERING_MISSING_CLOZE = 28; - CARD_TEMPLATE_RENDERING_MORE_INFO = 29; - CARD_TEMPLATE_RENDERING_NO_CLOSING_BRACKETS = 30; - CARD_TEMPLATE_RENDERING_NO_SUCH_FIELD = 31; - CARD_TEMPLATE_RENDERING_WRONG_CONDITIONAL_CLOSED = 32; - CARD_TEMPLATES_ADD_MOBILE_CLASS = 33; - CARD_TEMPLATES_BACK_PREVIEW = 34; - CARD_TEMPLATES_BACK_TEMPLATE = 35; - CARD_TEMPLATES_CARD_TYPE = 36; - CARD_TEMPLATES_CHANGES_SAVED = 37; - CARD_TEMPLATES_CHANGES_WILL_AFFECT_NOTES = 38; - CARD_TEMPLATES_DISCARD_CHANGES = 39; - CARD_TEMPLATES_FILL_EMPTY = 40; - CARD_TEMPLATES_FRONT_PREVIEW = 41; - CARD_TEMPLATES_FRONT_TEMPLATE = 42; - CARD_TEMPLATES_INVALID_TEMPLATE_NUMBER = 43; - CARD_TEMPLATES_NIGHT_MODE = 44; - CARD_TEMPLATES_PREVIEW_BOX = 45; - CARD_TEMPLATES_PREVIEW_SETTINGS = 46; - CARD_TEMPLATES_SAMPLE_CLOZE = 47; - CARD_TEMPLATES_TEMPLATE_BOX = 48; - CARD_TEMPLATES_TEMPLATE_STYLING = 49; - DATABASE_CHECK_CARD_MISSING_NOTE = 50; - DATABASE_CHECK_CARD_PROPERTIES = 51; - DATABASE_CHECK_CHECKING_CARDS = 52; - DATABASE_CHECK_CHECKING_HISTORY = 53; - DATABASE_CHECK_CHECKING_INTEGRITY = 54; - DATABASE_CHECK_CHECKING_NOTES = 55; - DATABASE_CHECK_CORRUPT = 56; - DATABASE_CHECK_DUPLICATE_CARD_ORDS = 57; - DATABASE_CHECK_FIELD_COUNT = 58; - DATABASE_CHECK_MISSING_DECKS = 59; - DATABASE_CHECK_MISSING_TEMPLATES = 60; - DATABASE_CHECK_NEW_CARD_HIGH_DUE = 61; - DATABASE_CHECK_REBUILDING = 62; - DATABASE_CHECK_REBUILT = 63; - DATABASE_CHECK_REVLOG_PROPERTIES = 64; - DATABASE_CHECK_TITLE = 65; - DECK_CONFIG_DEFAULT_NAME = 66; - DECK_CONFIG_USED_BY_DECKS = 67; - EMPTY_CARDS_COUNT_LINE = 68; - EMPTY_CARDS_DELETE_BUTTON = 69; - EMPTY_CARDS_DELETE_EMPTY_CARDS = 70; - EMPTY_CARDS_DELETE_EMPTY_NOTES = 71; - EMPTY_CARDS_DELETED_COUNT = 72; - EMPTY_CARDS_DELETING = 73; - EMPTY_CARDS_FOR_NOTE_TYPE = 74; - EMPTY_CARDS_NOT_FOUND = 75; - EMPTY_CARDS_PRESERVE_NOTES_CHECKBOX = 76; - EMPTY_CARDS_WINDOW_TITLE = 77; - FILTERING_IS_DUE = 78; - FINDREPLACE_NOTES_UPDATED = 79; - IMPORTING_FAILED_DEBUG_INFO = 80; - MEDIA_CHECK_ALL_LATEX_RENDERED = 81; - MEDIA_CHECK_CHECK_MEDIA_ACTION = 82; - MEDIA_CHECK_CHECKED = 83; - MEDIA_CHECK_DELETE_UNUSED = 84; - MEDIA_CHECK_DELETE_UNUSED_COMPLETE = 85; - MEDIA_CHECK_DELETE_UNUSED_CONFIRM = 86; - MEDIA_CHECK_EMPTY_TRASH = 87; - MEDIA_CHECK_FILES_REMAINING = 88; - MEDIA_CHECK_MISSING_COUNT = 89; - MEDIA_CHECK_MISSING_FILE = 90; - MEDIA_CHECK_MISSING_HEADER = 91; - MEDIA_CHECK_OVERSIZE_COUNT = 92; - MEDIA_CHECK_OVERSIZE_FILE = 93; - MEDIA_CHECK_OVERSIZE_HEADER = 94; - MEDIA_CHECK_RENAMED_COUNT = 95; - MEDIA_CHECK_RENAMED_FILE = 96; - MEDIA_CHECK_RENAMED_HEADER = 97; - MEDIA_CHECK_RENDER_LATEX = 98; - MEDIA_CHECK_RESTORE_TRASH = 99; - MEDIA_CHECK_SUBFOLDER_COUNT = 100; - MEDIA_CHECK_SUBFOLDER_FILE = 101; - MEDIA_CHECK_SUBFOLDER_HEADER = 102; - MEDIA_CHECK_TRASH_COUNT = 103; - MEDIA_CHECK_TRASH_EMPTIED = 104; - MEDIA_CHECK_TRASH_RESTORED = 105; - MEDIA_CHECK_UNUSED_COUNT = 106; - MEDIA_CHECK_UNUSED_FILE = 107; - MEDIA_CHECK_UNUSED_HEADER = 108; - MEDIA_CHECK_WINDOW_TITLE = 109; - NETWORK_DETAILS = 110; - NETWORK_OFFLINE = 111; - NETWORK_OTHER = 112; - NETWORK_PROXY_AUTH = 113; - NETWORK_TIMEOUT = 114; - NOTETYPES_ADD_REVERSE_FIELD = 115; - NOTETYPES_BACK_EXTRA_FIELD = 116; - NOTETYPES_BACK_FIELD = 117; - NOTETYPES_BASIC_NAME = 118; - NOTETYPES_BASIC_OPTIONAL_REVERSED_NAME = 119; - NOTETYPES_BASIC_REVERSED_NAME = 120; - NOTETYPES_BASIC_TYPE_ANSWER_NAME = 121; - NOTETYPES_CARD_1_NAME = 122; - NOTETYPES_CARD_2_NAME = 123; - NOTETYPES_CLOZE_NAME = 124; - NOTETYPES_FRONT_FIELD = 125; - NOTETYPES_TEXT_FIELD = 126; - SCHEDULING_ANSWER_BUTTON_TIME_DAYS = 127; - SCHEDULING_ANSWER_BUTTON_TIME_HOURS = 128; - SCHEDULING_ANSWER_BUTTON_TIME_MINUTES = 129; - SCHEDULING_ANSWER_BUTTON_TIME_MONTHS = 130; - SCHEDULING_ANSWER_BUTTON_TIME_SECONDS = 131; - SCHEDULING_ANSWER_BUTTON_TIME_YEARS = 132; - SCHEDULING_BURIED_CARDS_WERE_DELAYED = 133; - SCHEDULING_CONGRATULATIONS_FINISHED = 134; - SCHEDULING_LEARN_REMAINING = 135; - SCHEDULING_NEXT_LEARN_DUE = 136; - SCHEDULING_TIME_SPAN_DAYS = 137; - SCHEDULING_TIME_SPAN_HOURS = 138; - SCHEDULING_TIME_SPAN_MINUTES = 139; - SCHEDULING_TIME_SPAN_MONTHS = 140; - SCHEDULING_TIME_SPAN_SECONDS = 141; - SCHEDULING_TIME_SPAN_YEARS = 142; - SCHEDULING_TODAY_NEW_LIMIT_REACHED = 143; - SCHEDULING_TODAY_REVIEW_LIMIT_REACHED = 144; - SEARCH_CARD_MODIFIED = 145; - SEARCH_INVALID = 146; - SEARCH_NOTE_MODIFIED = 147; - STATISTICS_ADDED_SUBTITLE = 148; - STATISTICS_ADDED_TITLE = 149; - STATISTICS_AMOUNT_OF_TOTAL_WITH_PERCENTAGE = 150; - STATISTICS_ANSWER_BUTTONS_BUTTON_NUMBER = 151; - STATISTICS_ANSWER_BUTTONS_BUTTON_PRESSED = 152; - STATISTICS_ANSWER_BUTTONS_SUBTITLE = 153; - STATISTICS_ANSWER_BUTTONS_TITLE = 154; - STATISTICS_AVERAGE = 155; - STATISTICS_AVERAGE_ANSWER_TIME = 156; - STATISTICS_AVERAGE_ANSWER_TIME_LABEL = 157; - STATISTICS_AVERAGE_FOR_DAYS_STUDIED = 158; - STATISTICS_AVERAGE_INTERVAL = 159; - STATISTICS_AVERAGE_OVER_PERIOD = 160; - STATISTICS_BACKLOG_CHECKBOX = 161; - STATISTICS_CALENDAR_TITLE = 162; - STATISTICS_CARD_EASE_SUBTITLE = 163; - STATISTICS_CARD_EASE_TITLE = 164; - STATISTICS_CARD_EASE_TOOLTIP = 165; - STATISTICS_CARDS = 166; - STATISTICS_CARDS_DUE = 167; - STATISTICS_CARDS_PER_DAY = 168; - STATISTICS_CARDS_PER_MIN = 169; - STATISTICS_COUNTS_BURIED_CARDS = 170; - STATISTICS_COUNTS_EARLY_CARDS = 171; - STATISTICS_COUNTS_LEARNING_CARDS = 172; - STATISTICS_COUNTS_MATURE_CARDS = 173; - STATISTICS_COUNTS_NEW_CARDS = 174; - STATISTICS_COUNTS_RELEARNING_CARDS = 175; - STATISTICS_COUNTS_SUSPENDED_CARDS = 176; - STATISTICS_COUNTS_TITLE = 177; - STATISTICS_COUNTS_TOTAL_CARDS = 178; - STATISTICS_COUNTS_YOUNG_CARDS = 179; - STATISTICS_DAYS_AGO_RANGE = 180; - STATISTICS_DAYS_AGO_SINGLE = 181; - STATISTICS_DAYS_STUDIED = 182; - STATISTICS_DUE_COUNT = 183; - STATISTICS_DUE_DATE = 184; - STATISTICS_DUE_FOR_NEW_CARD = 185; - STATISTICS_DUE_TOMORROW = 186; - STATISTICS_ELAPSED_TIME_DAYS = 187; - STATISTICS_ELAPSED_TIME_HOURS = 188; - STATISTICS_ELAPSED_TIME_MINUTES = 189; - STATISTICS_ELAPSED_TIME_MONTHS = 190; - STATISTICS_ELAPSED_TIME_SECONDS = 191; - STATISTICS_ELAPSED_TIME_YEARS = 192; - STATISTICS_ERROR_FETCHING = 193; - STATISTICS_FUTURE_DUE_SUBTITLE = 194; - STATISTICS_FUTURE_DUE_TITLE = 195; - STATISTICS_HOURS_CORRECT = 196; - STATISTICS_HOURS_RANGE = 197; - STATISTICS_HOURS_SUBTITLE = 198; - STATISTICS_HOURS_TITLE = 199; - STATISTICS_IN_DAYS_RANGE = 200; - STATISTICS_IN_DAYS_SINGLE = 201; - STATISTICS_IN_TIME_SPAN_DAYS = 202; - STATISTICS_IN_TIME_SPAN_HOURS = 203; - STATISTICS_IN_TIME_SPAN_MINUTES = 204; - STATISTICS_IN_TIME_SPAN_MONTHS = 205; - STATISTICS_IN_TIME_SPAN_SECONDS = 206; - STATISTICS_IN_TIME_SPAN_YEARS = 207; - STATISTICS_INTERVALS_DAY_RANGE = 208; - STATISTICS_INTERVALS_DAY_SINGLE = 209; - STATISTICS_INTERVALS_SUBTITLE = 210; - STATISTICS_INTERVALS_TITLE = 211; - STATISTICS_LONGEST_INTERVAL = 212; - STATISTICS_MINUTES_PER_DAY = 213; - STATISTICS_NO_DATA = 214; - STATISTICS_RANGE_1_YEAR_HISTORY = 215; - STATISTICS_RANGE_ALL_HISTORY = 216; - STATISTICS_RANGE_ALL_TIME = 217; - STATISTICS_RANGE_COLLECTION = 218; - STATISTICS_RANGE_DECK = 219; - STATISTICS_RANGE_SEARCH = 220; - STATISTICS_REVIEWS = 221; - STATISTICS_REVIEWS_COUNT_SUBTITLE = 222; - STATISTICS_REVIEWS_PER_DAY = 223; - STATISTICS_REVIEWS_TIME_CHECKBOX = 224; - STATISTICS_REVIEWS_TIME_SUBTITLE = 225; - STATISTICS_REVIEWS_TITLE = 226; - STATISTICS_RUNNING_TOTAL = 227; - STATISTICS_SECONDS_TAKEN = 228; - STATISTICS_STUDIED_TODAY = 229; - STATISTICS_TODAY_AGAIN_COUNT = 230; - STATISTICS_TODAY_CORRECT_MATURE = 231; - STATISTICS_TODAY_NO_CARDS = 232; - STATISTICS_TODAY_NO_MATURE_CARDS = 233; - STATISTICS_TODAY_TITLE = 234; - STATISTICS_TODAY_TYPE_COUNTS = 235; - STATISTICS_TOTAL = 236; - SYNC_ABORT_BUTTON = 237; - SYNC_ACCOUNT_REQUIRED = 238; - SYNC_ADDED_UPDATED_COUNT = 239; - SYNC_ANKIWEB_ID_LABEL = 240; - SYNC_CANCEL_BUTTON = 241; - SYNC_CHECKING = 242; - SYNC_CLIENT_TOO_OLD = 243; - SYNC_CLOCK_OFF = 244; - SYNC_CONFIRM_EMPTY_DOWNLOAD = 245; - SYNC_CONFLICT = 246; - SYNC_CONFLICT_EXPLANATION = 247; - SYNC_CONNECTING = 248; - SYNC_DOWNLOAD_FROM_ANKIWEB = 249; - SYNC_DOWNLOADING_FROM_ANKIWEB = 250; - SYNC_MEDIA_ABORTED = 251; - SYNC_MEDIA_ABORTING = 252; - SYNC_MEDIA_ADDED_COUNT = 253; - SYNC_MEDIA_CHECKED_COUNT = 254; - SYNC_MEDIA_COMPLETE = 255; - SYNC_MEDIA_DISABLED = 256; - SYNC_MEDIA_FAILED = 257; - SYNC_MEDIA_LOG_BUTTON = 258; - SYNC_MEDIA_LOG_TITLE = 259; - SYNC_MEDIA_REMOVED_COUNT = 260; - SYNC_MEDIA_STARTING = 261; - SYNC_MUST_WAIT_FOR_END = 262; - SYNC_PASSWORD_LABEL = 263; - SYNC_RESYNC_REQUIRED = 264; - SYNC_SANITY_CHECK_FAILED = 265; - SYNC_SERVER_ERROR = 266; - SYNC_SYNCING = 267; - SYNC_UPLOAD_TO_ANKIWEB = 268; - SYNC_UPLOADING_TO_ANKIWEB = 269; - SYNC_WRONG_PASS = 270; -} diff --git a/tools/protoc-gen/test/test_anki.bat b/tools/protoc-gen/test/test_anki.bat deleted file mode 100644 index bcc70c355..000000000 --- a/tools/protoc-gen/test/test_anki.bat +++ /dev/null @@ -1,2 +0,0 @@ -if not exist "%~dp0\out" mkdir "%~dp0\out" -protoc --include_source_info --plugin=protoc-gen-anki="..\protoc-gen.bat" --anki_out="out" backend.proto \ No newline at end of file From 6fb558a5dcf044d1fffd84e5a3cba5a54926be42 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 12:14:54 +1000 Subject: [PATCH 02/61] Update majority of remaining source files to Kotlin; address test failures --- .../ankiweb/rsdroid/BackendForTesting.java | 93 +- .../rsdroid/BackendIntegrationTests.java | 8 +- .../net/ankiweb/rsdroid/BackendMutexTest.java | 2 +- .../net/ankiweb/rsdroid/ExceptionTest.java | 25 +- .../rsdroid/ankiutil/InstrumentedTest.java | 17 +- .../DatabaseRegularCorruptionTest.java | 5 + .../rsdroid/database/DowngradeTest.java | 61 +- rsdroid/src/main/AndroidManifest.xml | 3 +- .../net/ankiweb/rsdroid/BackendException.java | 201 ---- .../net/ankiweb/rsdroid/BackendException.kt | 123 ++ .../net/ankiweb/rsdroid/BackendFactory.kt | 18 +- ...ndFatalError.java => BackendFatalError.kt} | 9 +- .../net/ankiweb/rsdroid/BackendMutex.java | 1042 ----------------- .../{BackendUtils.java => BackendUtils.kt} | 24 +- .../java/net/ankiweb/rsdroid/BackendV1.kt | 19 + .../ankiweb/rsdroid/BackendV11Factory.java | 38 - .../net/ankiweb/rsdroid/BackendV11Factory.kt | 39 + .../java/net/ankiweb/rsdroid/BackendV1Impl.kt | 38 +- .../ankiweb/rsdroid/BackendVNextFactory.java | 39 - .../ankiweb/rsdroid/BackendVNextFactory.kt | 40 + .../java/net/ankiweb/rsdroid/NativeMethods.kt | 4 +- ...ion.java => RustBackendFailedException.kt} | 17 +- .../{RustCleanup.java => RustCleanup.kt} | 17 +- ...llection.java => RustCleanupCollection.kt} | 13 +- .../{RustV1Cleanup.java => RustV1Cleanup.kt} | 10 +- .../rsdroid/database/AnkiDatabaseCursor.java | 215 ---- .../rsdroid/database/AnkiDatabaseCursor.kt | 153 +++ .../database/AnkiJsonDatabaseCursor.java | 198 ---- .../database/AnkiJsonDatabaseCursor.kt | 166 +++ ...eption.java => NotImplementedException.kt} | 25 +- .../rsdroid/database/RustSQLiteStatement.java | 132 --- .../rsdroid/database/RustSQLiteStatement.kt | 101 ++ .../database/RustSupportSQLiteDatabase.java | 368 ------ .../database/RustSupportSQLiteDatabase.kt | 312 +++++ .../database/RustSupportSQLiteOpenHelper.java | 85 -- .../database/RustSupportSQLiteOpenHelper.kt | 67 ++ .../RustV11SQLiteOpenHelperFactory.java | 39 - .../RustV11SQLiteOpenHelperFactory.kt | 28 + .../RustV11SupportSQLiteOpenHelper.java | 51 - .../RustV11SupportSQLiteOpenHelper.kt | 39 + .../RustVNextSQLiteOpenHelperFactory.java | 39 - .../RustVNextSQLiteOpenHelperFactory.kt | 28 + .../RustVNextSupportSQLiteOpenHelper.java | 49 - .../RustVNextSupportSQLiteOpenHelper.kt | 39 + .../ankiweb/rsdroid/database/SQLHandler.java | 57 - .../ankiweb/rsdroid/database/SQLHandler.kt | 56 + .../net/ankiweb/rsdroid/database/Session.java | 198 ---- .../net/ankiweb/rsdroid/database/Session.kt | 155 +++ ...ThreadLocal.java => SessionThreadLocal.kt} | 21 +- .../StreamingProtobufSQLiteCursor.java | 287 ----- .../database/StreamingProtobufSQLiteCursor.kt | 262 +++++ ...java => BackendDeckIsFilteredException.kt} | 12 +- ...ption.java => BackendExistingException.kt} | 12 +- ...on.java => BackendInterruptedException.kt} | 13 +- .../BackendInvalidInputException.java | 60 - .../BackendInvalidInputException.kt | 41 + ...edException.java => BackendIoException.kt} | 12 +- .../exceptions/BackendJsonException.java | 29 - .../exceptions/BackendJsonException.kt | 24 + .../exceptions/BackendNetworkException.java | 64 - ...eption.java => BackendNetworkException.kt} | 13 +- ...ption.java => BackendNotFoundException.kt} | 14 +- .../exceptions/BackendProtoException.kt | 21 + .../exceptions/BackendSyncException.kt | 23 + .../exceptions/BackendTemplateException.java | 44 - ...ption.java => BackendTemplateException.kt} | 29 +- 66 files changed, 1881 insertions(+), 3605 deletions(-) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.kt rename rsdroid/src/main/java/net/ankiweb/rsdroid/{BackendFatalError.java => BackendFatalError.kt} (88%) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendMutex.java rename rsdroid/src/main/java/net/ankiweb/rsdroid/{BackendUtils.java => BackendUtils.kt} (62%) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.kt rename rsdroid/src/main/java/net/ankiweb/rsdroid/{RustBackendFailedException.java => RustBackendFailedException.kt} (79%) rename rsdroid/src/main/java/net/ankiweb/rsdroid/{RustCleanup.java => RustCleanup.kt} (75%) rename rsdroid/src/main/java/net/ankiweb/rsdroid/{RustCleanupCollection.java => RustCleanupCollection.kt} (79%) rename rsdroid/src/main/java/net/ankiweb/rsdroid/{RustV1Cleanup.java => RustV1Cleanup.kt} (81%) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.kt rename rsdroid/src/main/java/net/ankiweb/rsdroid/database/{NotImplementedException.java => NotImplementedException.kt} (65%) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.kt rename rsdroid/src/main/java/net/ankiweb/rsdroid/database/{SessionThreadLocal.java => SessionThreadLocal.kt} (79%) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.kt rename rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/{BackendIoException.java => BackendDeckIsFilteredException.kt} (75%) rename rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/{BackendExistingException.java => BackendExistingException.kt} (74%) rename rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/{BackendProtoException.java => BackendInterruptedException.kt} (74%) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.kt rename rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/{BackendInterruptedException.java => BackendIoException.kt} (73%) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.java rename rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/{BackendDeckIsFilteredException.java => BackendNetworkException.kt} (73%) rename rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/{BackendNotFoundException.java => BackendNotFoundException.kt} (74%) create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.kt create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.java rename rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/{BackendSyncException.java => BackendTemplateException.kt} (51%) diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java index 2bdd29c7f..3d6fabd3b 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java @@ -18,10 +18,6 @@ import androidx.annotation.VisibleForTesting; -import com.google.protobuf.InvalidProtocolBufferException; - -import anki.backend; - public class BackendForTesting extends BackendV1Impl { BackendForTesting() { @@ -54,27 +50,16 @@ public void debugProduceError(ErrorType error) { public enum ErrorType { InvalidInput, TemplateError, - TemplateSaveError, - IOError, + IoError, DbErrorFileTooNew, DbErrorFileTooOld, DbErrorMissingEntity, DbErrorCorrupt, DbErrorLocked, DbErrorOther, - NetworkErrorOffline, - NetworkErrorTimeout, - NetworkErrorProxyAuth, - NetworkErrorOther, - SyncErrorConflict, - SyncErrorServerError, - SyncErrorClientTooOld, + NetworkError, SyncErrorAuthFailed, - SyncErrorServerMessage, - SyncErrorClockIncorrect, SyncErrorOther, - SyncErrorResyncRequired, - SyncErrorDatabaseCheckRequired, JSONError, ProtoError, Interrupted, @@ -82,80 +67,8 @@ public enum ErrorType { CollectionAlreadyOpen, NotFound, Existing, - DeckIsFiltered, + FilteredDeckError, SearchError, FatalError; - - public String toString() { - switch (this) { - case InvalidInput: - return "InvalidInput"; - case TemplateError: - return "TemplateError"; - case TemplateSaveError: - return "TemplateSaveError"; - case IOError: - return "IOError"; - case DbErrorFileTooNew: - return "DbErrorFileTooNew"; - case DbErrorFileTooOld: - return "DbErrorFileTooOld"; - case DbErrorMissingEntity: - return "DbErrorMissingEntity"; - case DbErrorCorrupt: - return "DbErrorCorrupt"; - case DbErrorLocked: - return "DbErrorLocked"; - case DbErrorOther: - return "DbErrorOther"; - case NetworkErrorOffline: - return "NetworkErrorOffline"; - case NetworkErrorTimeout: - return "NetworkErrorTimeout"; - case NetworkErrorProxyAuth: - return "NetworkErrorProxyAuth"; - case NetworkErrorOther: - return "NetworkErrorOther"; - case SyncErrorConflict: - return "SyncErrorConflict"; - case SyncErrorServerError: - return "SyncErrorServerError"; - case SyncErrorClientTooOld: - return "SyncErrorClientTooOld"; - case SyncErrorAuthFailed: - return "SyncErrorAuthFailed"; - case SyncErrorServerMessage: - return "SyncErrorServerMessage"; - case SyncErrorClockIncorrect: - return "SyncErrorClockIncorrect"; - case SyncErrorOther: - return "SyncErrorOther"; - case SyncErrorResyncRequired: - return "SyncErrorResyncRequired"; - case SyncErrorDatabaseCheckRequired: - return "SyncErrorDatabaseCheckRequired"; - case JSONError: - return "JSONError"; - case ProtoError: - return "ProtoError"; - case Interrupted: - return "Interrupted"; - case CollectionNotOpen: - return "CollectionNotOpen"; - case CollectionAlreadyOpen: - return "CollectionAlreadyOpen"; - case NotFound: - return "NotFound"; - case Existing: - return "Existing"; - case DeckIsFiltered: - return "DeckIsFiltered"; - case SearchError: - return "SearchError"; - case FatalError: - return "FatalError"; - default: throw new IllegalStateException("Unknown: " + this); - } - } } } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java index a4eba91d5..0df3c542e 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java @@ -31,7 +31,7 @@ import java.util.concurrent.TimeUnit; -import anki.backend.SchedTimingTodayOut; +import anki.scheduler.SchedTimingTodayResponse; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.core.Is.is; @@ -49,12 +49,12 @@ public void test() { throw new IllegalArgumentException("do not run on real device yet"); } } - + @Test public void testBackendException() { BackendV1 backendV1 = getClosedBackend(); try { - SchedTimingTodayOut ret = backendV1.schedTimingToday(); + backendV1.closeCollection(true); Assert.fail("call should have failed - needs an open collection"); } catch (BackendException ex) { // OK @@ -64,7 +64,7 @@ public void testBackendException() { @Test public void schedTimingTodayCall() { BackendV1 backendV1 = getBackend("initial_version_2_12_1.anki2"); - SchedTimingTodayOut ret = backendV1.schedTimingToday(); + SchedTimingTodayResponse ret = backendV1.schedTimingTodayLegacy(1655258084, 0, 1655258084, 0, 0); int elapsed = ret.getDaysElapsed(); long nextDayAt = ret.getNextDayAt(); } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java index b0230e3d5..d68649ac3 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java @@ -36,7 +36,7 @@ public class BackendMutexTest extends InstrumentedTest { @Test public void ensureDatabaseInTransactionIsLocked() throws JSONException, InterruptedException { - BackendMutex b = (BackendMutex) super.getBackend("initial_version_2_12_1.anki2"); + BackendV1 b = (BackendV1) super.getBackend("initial_version_2_12_1.anki2"); b.fullQuery("create table test (id int)"); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java index 234eab831..2bbd35c2f 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java @@ -16,8 +16,6 @@ package net.ankiweb.rsdroid; -import android.database.sqlite.SQLiteDatabaseCorruptException; - import net.ankiweb.rsdroid.database.NotImplementedException; import net.ankiweb.rsdroid.exceptions.BackendDeckIsFilteredException; import net.ankiweb.rsdroid.exceptions.BackendExistingException; @@ -67,14 +65,6 @@ public static java.util.Collection initParameters() { { BackendForTesting.ErrorType.SearchError, NOT_POSSIBLE }, { BackendForTesting.ErrorType.SyncErrorAuthFailed, BackendSyncException.BackendSyncAuthFailedException.class }, - { BackendForTesting.ErrorType.SyncErrorClientTooOld, BackendSyncException.BackendSyncClientTooOldException.class }, - { BackendForTesting.ErrorType.SyncErrorClockIncorrect, BackendSyncException.BackendSyncClockIncorrectException.class }, - { BackendForTesting.ErrorType.SyncErrorConflict, BackendSyncException.BackendSyncConflictException.class }, - { BackendForTesting.ErrorType.SyncErrorDatabaseCheckRequired, BackendSyncException.BackendSyncDatabaseCheckRequiredException.class }, - { BackendForTesting.ErrorType.SyncErrorResyncRequired, BackendSyncException.BackendSyncResyncRequiredException.class }, - { BackendForTesting.ErrorType.SyncErrorServerMessage, BackendSyncException.BackendSyncServerMessageException.class }, - { BackendForTesting.ErrorType.SyncErrorServerError, BackendSyncException.BackendSyncServerErrorException.class }, - { BackendForTesting.ErrorType.SyncErrorOther, BackendSyncException.class }, { BackendForTesting.ErrorType.DbErrorCorrupt, NOT_POSSIBLE }, @@ -83,26 +73,17 @@ public static java.util.Collection initParameters() { { BackendForTesting.ErrorType.DbErrorFileTooOld, BackendException.BackendDbException.BackendDbFileTooOldException.class}, { BackendForTesting.ErrorType.DbErrorLocked, BackendException.BackendDbException.BackendDbLockedException.class}, { BackendForTesting.ErrorType.DbErrorMissingEntity, BackendException.BackendDbException.BackendDbMissingEntityException.class}, - { BackendForTesting.ErrorType.DbErrorOther, BackendException.BackendDbException.class}, - - - { BackendForTesting.ErrorType.NetworkErrorOffline, BackendNetworkException.BackendNetworkOfflineException.class}, - { BackendForTesting.ErrorType.NetworkErrorProxyAuth, BackendNetworkException.BackendNetworkProxyAuthException.class}, - { BackendForTesting.ErrorType.NetworkErrorTimeout, BackendNetworkException.BackendNetworkTimeoutException.class}, - - { BackendForTesting.ErrorType.NetworkErrorOther, BackendNetworkException.class}, - - { BackendForTesting.ErrorType.DeckIsFiltered, BackendDeckIsFilteredException.class }, + { BackendForTesting.ErrorType.NetworkError, BackendNetworkException.class}, + { BackendForTesting.ErrorType.FilteredDeckError, BackendDeckIsFilteredException.class }, { BackendForTesting.ErrorType.Existing, BackendExistingException.class }, { BackendForTesting.ErrorType.FatalError, BackendFatalError.class }, { BackendForTesting.ErrorType.Interrupted, BackendInterruptedException.class}, { BackendForTesting.ErrorType.InvalidInput, BackendInvalidInputException.class}, - { BackendForTesting.ErrorType.IOError, BackendIoException.class}, + { BackendForTesting.ErrorType.IoError, BackendIoException.class}, { BackendForTesting.ErrorType.JSONError, BackendJsonException.class }, { BackendForTesting.ErrorType.ProtoError, BackendProtoException.class }, { BackendForTesting.ErrorType.TemplateError, BackendTemplateException.class}, - { BackendForTesting.ErrorType.TemplateSaveError, BackendTemplateException.BackendTemplateSaveException.class}, { BackendForTesting.ErrorType.NotFound, BackendNotFoundException.class}, }); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java index b473d782c..2e07bf6fe 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java @@ -22,12 +22,14 @@ import androidx.test.platform.app.InstrumentationRegistry; +import net.ankiweb.rsdroid.BackendException; import net.ankiweb.rsdroid.BackendFactory; import net.ankiweb.rsdroid.BackendUtils; import net.ankiweb.rsdroid.BackendV1; import net.ankiweb.rsdroid.BackendV1Impl; import net.ankiweb.rsdroid.NativeMethods; import net.ankiweb.rsdroid.RustBackendFailedException; +import net.ankiweb.rsdroid.exceptions.BackendInvalidInputException; import org.jetbrains.annotations.NotNull; import org.junit.After; @@ -36,6 +38,7 @@ import java.util.ArrayList; import java.util.List; +import static org.hamcrest.Matchers.containsString; import static org.hamcrest.Matchers.empty; import static org.junit.Assert.assertThat; @@ -62,14 +65,21 @@ public void before() { } catch (RustBackendFailedException e) { throw new RuntimeException(e); } - BackendV1Impl.setPageSizeForTesting(TEST_PAGE_SIZE); } @After public void after() { for (BackendV1 b : backendList) { if (b != null && b.isOpen()) { - assertThat("All database cursors should be closed", b.debugActiveDatabaseSequenceNumbers(0).getSequenceNumbersList(), empty()); + + List numbers; + try { + numbers = b.getActiveSequenceNumbers().getNumbersList(); + } catch (BackendInvalidInputException exc) { + assertThat(exc.getLocalizedMessage(), containsString("CollectionNotOpen")); + continue; + } + assertThat("All database cursors should be closed", numbers, empty()); } } backendList.clear(); @@ -120,7 +130,8 @@ protected BackendV1 getBackend(String fileName) { @NotNull protected BackendV1 getBackendFromPath(String path) { BackendV1 backendV1 = getClosedBackend(); - BackendUtils.openAnkiDroidCollection(backendV1, path); + backendV1.setPageSize(TEST_PAGE_SIZE); + BackendUtils.openAnkiDroidCollection(backendV1, path, true); this.backendList.add(backendV1); return backendV1; } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DatabaseRegularCorruptionTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DatabaseRegularCorruptionTest.java index f0cc6e43c..3455d0bcf 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DatabaseRegularCorruptionTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DatabaseRegularCorruptionTest.java @@ -18,6 +18,7 @@ import android.database.sqlite.SQLiteDatabaseCorruptException; +import net.ankiweb.rsdroid.BackendException; import net.ankiweb.rsdroid.database.testutils.DatabaseCorruption; import org.junit.runner.RunWith; @@ -35,8 +36,12 @@ public class DatabaseRegularCorruptionTest extends DatabaseCorruption { protected void assertCorruption(Exception setupException) { // Rust: net.ankiweb.rsdroid.BackendException$BackendDbException: DBError { info: "SqliteFailure(Error { code: DatabaseCorrupt, extended_code: 11 }, Some(\"database disk image is malformed\"))", kind: Other } // Java: database disk image is malformed (code 11): , while compiling: PRAGMA journal_mode + +// assertThat(setupException.getClass(), typeCompatibleWith(BackendException.BackendDbException.class)); assertThat(setupException.getClass(), typeCompatibleWith(SQLiteDatabaseCorruptException.class)); + // this mapping to an unrelated exception should be done at a higher level + assertThat(setupException.getLocalizedMessage(), containsString("database disk image is malformed")); assertThat(setupException.getLocalizedMessage(), containsString("11")); } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java index 051077eb3..e6cfc397d 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java @@ -36,62 +36,16 @@ public class DowngradeTest extends InstrumentedTest { @Test - public void downgradeWithV16() throws IOException, JSONException { + public void downgradeWithLaterSchema() throws IOException, JSONException { String fileName = "schema_16.anki2"; - String path = getAssetFilePath(fileName); - - // opening the backend fails - assertOpeningFails(path); - - downgrade(path); - - // now it works try (BackendV1 backendV1 = super.getBackendFromPath(path) ){ assertSchemaVer(backendV1, 11); } - } - - @Test - public void downgradeWithV17FailsAsTooNew() { - String fileName = "schema_17.anki2"; - - String path = getAssetFilePath(fileName); - - // opening the backend fails - assertOpeningFails(path); - - try { - downgrade(path); - fail(); - } catch (BackendException ex) { - assertThat(ex.getMessage(), containsString("FileTooNew")); - } - } - - - @Test - public void downgradeWithV11Fails() throws JSONException, IOException { - // A downgrade will throw an exception if the file is less than schema 16 - - String fileName = "initial_version_2_12_1.anki2"; - - String path = getAssetFilePath(fileName); - - try (BackendV1 backend = getBackendFromPath(path)) { - assertSchemaVer(backend, 11); - } - - try { - downgrade(path); - fail(); - } catch (Exception e) { - assertThat(e.getMessage(), containsString("FileTooOld")); - assertThat(e.getMessage(), containsString("Schema11")); - } - - try (BackendV1 backend = getBackendFromPath(path)) { - assertSchemaVer(backend, 11); + fileName = "schema_17.anki2"; + path = getAssetFilePath(fileName); + try (BackendV1 backendV1 = super.getBackendFromPath(path) ){ + assertSchemaVer(backendV1, 11); } } @@ -104,11 +58,6 @@ private void assertSchemaVer(BackendV1 backendV1, @SuppressWarnings("SameParamet assertThat(subArray.optInt(0, 0), is(expectedVersion)); } - private void downgrade(String collectionPath) { - BackendV1 backendV1 = getClosedBackend(); - backendV1.downgradeBackend(collectionPath); - } - @SuppressWarnings({"unused", "RedundantSuppression"}) private void assertOpeningFails(String path) { try (BackendV1 unused = super.getBackendFromPath(path)) { diff --git a/rsdroid/src/main/AndroidManifest.xml b/rsdroid/src/main/AndroidManifest.xml index a768e28aa..e1b89d9ec 100644 --- a/rsdroid/src/main/AndroidManifest.xml +++ b/rsdroid/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.java deleted file mode 100644 index 8d4a06e96..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.java +++ /dev/null @@ -1,201 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid; - -import android.database.sqlite.SQLiteConstraintException; -import android.database.sqlite.SQLiteDatabaseCorruptException; -import android.database.sqlite.SQLiteException; -import android.database.sqlite.SQLiteFullException; - -import androidx.annotation.Nullable; - -import net.ankiweb.rsdroid.database.NotImplementedException; -import net.ankiweb.rsdroid.exceptions.BackendDeckIsFilteredException; -import net.ankiweb.rsdroid.exceptions.BackendExistingException; -import net.ankiweb.rsdroid.exceptions.BackendInterruptedException; -import net.ankiweb.rsdroid.exceptions.BackendInvalidInputException; -import net.ankiweb.rsdroid.exceptions.BackendIoException; -import net.ankiweb.rsdroid.exceptions.BackendJsonException; -import net.ankiweb.rsdroid.exceptions.BackendNetworkException; -import net.ankiweb.rsdroid.exceptions.BackendNotFoundException; -import net.ankiweb.rsdroid.exceptions.BackendProtoException; -import net.ankiweb.rsdroid.exceptions.BackendSyncException; -import net.ankiweb.rsdroid.exceptions.BackendTemplateException; - -import java.util.Locale; -import java.util.regex.Matcher; -import java.util.regex.Pattern; - -import anki.backend.BackendError.Kind; - -public class BackendException extends RuntimeException { - @SuppressWarnings({"unused", "RedundantSuppression"}) - @Nullable - private final anki.backend.BackendError error; - - public BackendException(anki.backend.BackendError error) { - super(error.getLocalized()); - this.error = error; - } - - public BackendException(String message) { - super(message); - error = null; - } - - public static BackendException fromError(anki.backend.BackendError error) { - throw new NotImplementedException(); -// switch (error.getKindValue()) { -// case Kind.DB_ERROR.: -// return BackendDbException.fromDbError(error); -//// case Kind.JSON_ERROR: -//// return new BackendJsonException(error.getJsonError()); -// case Kind.SYNC_AUTH_ERROR: -// return new BackendSyncException.BackendSyncAuthFailedException(error); -// case Kind.SYNC_OTHER_ERROR: -// return new BackendSyncException.BackendSyncException(error); -// return BackendSyncException.fromSyncError(error); -// case Kind.FATAL_ERROR: -// // This should have produced a hasFatalError property -// throw new BackendFatalError(error.getFatalError()); -// case Kind.EXISTS: -// return new BackendExistingException(error); -//// case Kind.DECK_IS_FILTERED: -//// return new BackendDeckIsFilteredException(error); -// case Kind.INTERRUPTED: -// return new BackendInterruptedException(error); -// case Kind.PROTO_ERROR: -// return new BackendProtoException(error); -// case Kind.NOT_FOUND_ERROR: -// return new BackendNotFoundException(error); -// case Kind.INVALID_INPUT: -// return BackendInvalidInputException.fromInvalidInputError(error); -// case Kind.NETWORK_ERROR: -// return BackendNetworkException.fromNetworkError(error); -// case Kind.TEMPLATE_PARSE: -// return BackendTemplateException.fromTemplateError(error); -// case Kind.IO_ERROR: -// return new BackendIoException(error); -// case Kind.VALUE_NOT_SET: -// } - - -// return new BackendException(error); - } - - public static RuntimeException fromException(Exception ex) { - return new RuntimeException(ex); - } - - - public RuntimeException toSQLiteException(String query) { - String message = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, this.getLocalizedMessage()); - return new SQLiteException(message, this); - } - - public static class BackendDbException extends BackendException { - - public BackendDbException(anki.backend.BackendError error) { - // This is very simple for now and matches Anki Desktop (error is currently text) - // Later on, we may want to use structured error messages - // DBError { info: "SqliteFailure(Error { code: Unknown, extended_code: 1 }, Some(\"no such table: aa\"))", kind: Other } - super(error); - } - - public static BackendException fromDbError(anki.backend.BackendError error) { - - String localised = error.getLocalized(); - - if (localised == null) { - return new BackendDbException(error); - } - - if (localised.contains("kind: FileTooNew")) { - return new BackendDbFileTooNewException(error); - } - if (localised.contains("kind: FileTooOld")) { - return new BackendDbFileTooOldException(error); - } - if (localised.contains("kind: MissingEntity")) { - return new BackendDbMissingEntityException(error); - } - if (localised.contains("kind: Other")) { - return new BackendDbException(error); - } - // Anki already open, or media currently syncing. - if (localised.startsWith("Anki already open")) { - return new BackendDbLockedException(error); - } - - return new BackendDbException(error); - } - - @Override - public RuntimeException toSQLiteException(String query) { - String message = this.getLocalizedMessage(); - - if (message == null) { - String outMessage = String.format(Locale.ROOT, "Unknown error while compiling: \"%s\"", query); - throw new SQLiteException(outMessage, this); - } - - if (message.contains("InvalidParameterCount")) { - Matcher p = Pattern.compile("InvalidParameterCount\\((\\d*), (\\d*)\\)").matcher(this.getMessage()); - if (p.find()) { - int paramCount = Integer.parseInt(p.group(1)); - int index = Integer.parseInt(p.group(2)); - String errorMessage = String.format(Locale.ROOT, "Cannot bind argument at index %d because the index is out of range. The statement has %d parameters.", index, paramCount); - throw new IllegalArgumentException(errorMessage, this); - } - } else if (message.contains("ConstraintViolation")) { - throw new SQLiteConstraintException(message); - } else if (message.contains("DiskFull")) { - throw new SQLiteFullException(message); - } else if (message.contains("DatabaseCorrupt")) { - String outMessage = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, message); - throw new SQLiteDatabaseCorruptException(outMessage); - } - - String outMessage = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, message); - throw new SQLiteException(outMessage, this); - } - - public static class BackendDbFileTooNewException extends BackendException { - public BackendDbFileTooNewException(anki.backend.BackendError error) { - super(error); - } - } - - public static class BackendDbFileTooOldException extends BackendException { - public BackendDbFileTooOldException(anki.backend.BackendError error) { - super(error); - } - } - - public static class BackendDbLockedException extends BackendException { - public BackendDbLockedException(anki.backend.BackendError error) { - super(error); - } - } - - public static class BackendDbMissingEntityException extends BackendException { - public BackendDbMissingEntityException(anki.backend.BackendError error) { - super(error); - } - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.kt new file mode 100644 index 000000000..61e1582b2 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.kt @@ -0,0 +1,123 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid + +import android.database.sqlite.SQLiteConstraintException +import android.database.sqlite.SQLiteDatabaseCorruptException +import android.database.sqlite.SQLiteException +import android.database.sqlite.SQLiteFullException +import anki.backend.BackendError +import net.ankiweb.rsdroid.exceptions.* +import net.ankiweb.rsdroid.exceptions.BackendSyncException.BackendSyncAuthFailedException +import java.util.* +import java.util.regex.Pattern + +open class BackendException : RuntimeException { + private val error: BackendError? + + constructor(error: BackendError) : super(error.localized) { + this.error = error + } + + constructor(message: String?) : super(message) { + error = null + } + + open fun toSQLiteException(query: String?): RuntimeException { + val message = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, this.localizedMessage) + return SQLiteException(message, this) + } + + class BackendDbException(error: BackendError) : BackendException(error) { + override fun toSQLiteException(query: String?): RuntimeException { + val message = this.localizedMessage + if (message == null) { + val outMessage = String.format(Locale.ROOT, "Unknown error while compiling: \"%s\"", query) + return SQLiteException(outMessage, this) + } + if (message.contains("InvalidParameterCount")) { + val p = Pattern.compile("InvalidParameterCount\\((\\d*), (\\d*)\\)").matcher(this.message) + if (p.find()) { + val givenParams = p.group(1).toInt() + val expectedParams = p.group(2).toInt() + val errorMessage = String.format(Locale.ROOT, "Cannot bind argument at index %d because the index is out of range. The statement has %d parameters.", givenParams, expectedParams) + return IllegalArgumentException(errorMessage, this) + } + } else if (message.contains("ConstraintViolation")) { + return SQLiteConstraintException(message) + } else if (message.contains("DiskFull")) { + return SQLiteFullException(message) + } else if (message.contains("DatabaseCorrupt")) { + val outMessage = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, message) + return SQLiteDatabaseCorruptException(outMessage) + } + val outMessage = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, message) + return SQLiteException(outMessage, this) + } + + class BackendDbFileTooNewException(error: BackendError) : BackendException(error) + class BackendDbFileTooOldException(error: BackendError) : BackendException(error) + class BackendDbLockedException(error: BackendError) : BackendException(error) + class BackendDbMissingEntityException(error: BackendError) : BackendException(error) + companion object { + fun fromDbError(error: BackendError): BackendException { + val localised = error.localized ?: return BackendDbException(error) + if (localised.contains("kind: FileTooNew")) { + return BackendDbFileTooNewException(error) + } + if (localised.contains("kind: FileTooOld")) { + return BackendDbFileTooOldException(error) + } + if (localised.contains("kind: MissingEntity")) { + return BackendDbMissingEntityException(error) + } + if (localised.contains("kind: Other")) { + return BackendDbException(error) + } + // Anki already open, or media currently syncing. + return if (localised.startsWith("Anki already open")) { + BackendDbLockedException(error) + } else BackendDbException(error) + } + } + } + + companion object { + fun fromError(error: BackendError): BackendException { + when (error.kind) { + BackendError.Kind.DB_ERROR -> return BackendDbException.fromDbError(error) + BackendError.Kind.JSON_ERROR -> return BackendJsonException(error) + BackendError.Kind.SYNC_AUTH_ERROR -> return BackendSyncAuthFailedException(error) + BackendError.Kind.SYNC_OTHER_ERROR -> return BackendSyncException(error) + BackendError.Kind.FATAL_ERROR -> throw BackendFatalError(error.localized) + BackendError.Kind.EXISTS -> return BackendExistingException(error) + BackendError.Kind.FILTERED_DECK_ERROR -> return BackendDeckIsFilteredException(error) + BackendError.Kind.INTERRUPTED -> return BackendInterruptedException(error) + BackendError.Kind.PROTO_ERROR -> return BackendProtoException(error) + BackendError.Kind.NOT_FOUND_ERROR -> return BackendNotFoundException(error) + BackendError.Kind.INVALID_INPUT -> return BackendInvalidInputException.fromInvalidInputError(error) + BackendError.Kind.NETWORK_ERROR -> return BackendNetworkException(error) + BackendError.Kind.TEMPLATE_PARSE -> return BackendTemplateException.fromTemplateError(error) + BackendError.Kind.IO_ERROR -> return BackendIoException(error) + } + return BackendException(error) + } + + fun fromException(ex: Exception?): RuntimeException { + return RuntimeException(ex) + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt index 414b398fd..9e46ec364 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt @@ -15,14 +15,8 @@ */ package net.ankiweb.rsdroid -import net.ankiweb.rsdroid.BackendV1 -import net.ankiweb.rsdroid.BackendV1Impl import androidx.sqlite.db.SupportSQLiteOpenHelper -import net.ankiweb.rsdroid.RustCleanup -import kotlin.Throws import net.ankiweb.rsdroid.RustBackendFailedException -import net.ankiweb.rsdroid.BackendFactory -import net.ankiweb.rsdroid.BackendV11Factory abstract class BackendFactory // Force users to go through getInstance - for now we need to handle the backend failure protected constructor() { @@ -30,21 +24,11 @@ protected constructor() { @Synchronized fun getBackend(): BackendV1 { if (backend == null) { - backend = BackendV1Impl() // new BackendMutex(new BackendV1Impl()); + backend = BackendV1Impl() } return backend!! } - @Synchronized - fun closeCollection() { - if (backend == null) { - return - } - - // we could swallow the exception here, most of the time it will be "collection is already closed" -// backend.closeCollection(false); - } - abstract val sQLiteOpener: SupportSQLiteOpenHelper.Factory? companion object { diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.kt similarity index 88% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.kt index 03f159411..fa23fe535 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.kt @@ -13,8 +13,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -package net.ankiweb.rsdroid; +package net.ankiweb.rsdroid /** A Java implementation of a rust "panic". * @@ -25,8 +24,4 @@ * * This error delays the panic in a form that ACRA can catch, log, then crash more gracefully with. */ -public class BackendFatalError extends Error { - public BackendFatalError(String message) { - super(message); - } -} +class BackendFatalError(message: String?) : Error(message) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendMutex.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendMutex.java deleted file mode 100644 index a0adf5484..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendMutex.java +++ /dev/null @@ -1,1042 +0,0 @@ -///* -// * Copyright (c) 2020 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 net.ankiweb.rsdroid; -// -//import androidx.annotation.Nullable; -// -//import com.google.protobuf.ByteString; -// -//import org.json.JSONArray; -// -//import java.io.IOException; -//import java.util.List; -//import java.util.concurrent.locks.ReentrantLock; -// -//import anki.AdBackend; -//import anki.backend; -//import anki.ankidroid; -// -///** -// * Ensures that a single thread accesses RustBackend at the same time. -// * This is because rslib-bridge currently has no distinction between threads, and handles the state of -// * transactions. -// */ -//public class BackendMutex implements BackendV1 { -// // This class exists as the Rust backend uses a single connection for SQLite, rather than a connection pool -// // This means that SQL can occur cross-threads. -// // There are a few problems with this: -// // * When inside a transaction, another thread can add commands, or close the transaction -// // * Commands can either be sent from the Java, or from the Rust. -// // * We have no knowledge about whether a Rust command will start a transaction -// -// // We handle this using a mutex and some invariants: -// // * If a transaction is held by a thread, have the thread keep the mutex until the transaction is closed -// // * Only one Rust command can run at a time - already true as with_col in rust uses a mutex, but we'll lock on the Java side -// -// private final ReentrantLock lock = new ReentrantLock(); -// private final BackendV1 backend; -// -// public BackendMutex(BackendV1 backend) { -// this.backend = backend; -// } -// -// @Override -// public void beginTransaction() { -// lock.lock(); -// backend.beginTransaction(); -// } -// -// @Override -// public void commitTransaction() { -// try { -// backend.commitTransaction(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void rollbackTransaction() { -// try { -// backend.rollbackTransaction(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public JSONArray fullQuery(String query, Object... bindArgs) { -// try { -// lock.lock(); -// return backend.fullQuery(query, bindArgs); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public int executeGetRowsAffected(String sql, Object... bindArgs) { -// try { -// lock.lock(); -// return backend.executeGetRowsAffected(sql, bindArgs); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public long insertForId(String sql, Object... bindArgs) { -// try { -// lock.lock(); -// return backend.insertForId(sql, bindArgs); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public String[] getColumnNames(String sql) { -// try { -// lock.lock(); -// return backend.getColumnNames(sql); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void closeDatabase() { -// try { -// lock.lock(); -// backend.closeDatabase(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public String getPath() { -// try { -// lock.lock(); -// return backend.getPath(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Sqlite.DBResponse getNextSlice(long startIndex, int sequenceNumber) { -// try { -// lock.lock(); -// return backend.getNextSlice(startIndex, sequenceNumber); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Sqlite.DBResponse fullQueryProto(String query, Object... bindArgs) { -// try { -// lock.lock(); -// return backend.fullQueryProto(query, bindArgs); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void cancelCurrentProtoQuery(int sequenceNumber) { -// try { -// lock.lock(); -// backend.cancelCurrentProtoQuery(sequenceNumber); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void cancelAllProtoQueries() { -// try { -// lock.lock(); -// backend.cancelAllProtoQueries(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void setPageSize(long pageSizeBytes) { -// try { -// lock.lock(); -// backend.setPageSize(pageSizeBytes); -// } finally { -// lock.unlock(); -// } -// } -// -// // RustBackend Implementation -// -// @Override -// public Backend.Progress latestProgress() { -// try { -// lock.lock(); -// return backend.latestProgress(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void setWantsAbort() { -// try { -// lock.lock(); -// backend.setWantsAbort(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.ExtractAVTagsOut extractAVTags(@Nullable String text, boolean questionSide) { -// try { -// lock.lock(); -// return backend.extractAVTags(text, questionSide); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.ExtractLatexOut extractLatex(@Nullable String text, boolean svg, boolean expandClozes) { -// try { -// lock.lock(); -// return backend.extractLatex(text, svg, expandClozes); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Generic.EmptyCardsReport getEmptyCards() { -// try { -// lock.lock(); -// return backend.getEmptyCards(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.RenderCardOut renderExistingCard(long cardId, boolean browser) { -// try { -// lock.lock(); -// return backend.renderExistingCard(cardId, browser); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.RenderCardOut renderUncommittedCard(@Nullable Backend.Note note, int cardOrd, @Nullable ByteString template, boolean fillEmpty) { -// try { -// lock.lock(); -// return backend.renderUncommittedCard(note, cardOrd, template, fillEmpty); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.String stripAVTags(String args) { -// try { -// lock.lock(); -// return backend.stripAVTags(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.SearchCardsOut searchCards(@Nullable String search, @Nullable Backend.SortOrder order) { -// try { -// lock.lock(); -// return backend.searchCards(search, order); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.SearchNotesOut searchNotes(@Nullable String search) { -// try { -// lock.lock(); -// return backend.searchNotes(search); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.UInt32 findAndReplace(List nids, @Nullable String search, @Nullable String replacement, boolean regex, boolean matchCase, @Nullable String fieldName) { -// try { -// lock.lock(); -// return backend.findAndReplace(nids, search, replacement, regex, matchCase, fieldName); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Int32 localMinutesWest(long args) { -// try { -// lock.lock(); -// return backend.localMinutesWest(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void setLocalMinutesWest(int args) { -// try { -// lock.lock(); -// backend.setLocalMinutesWest(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.SchedTimingTodayOut schedTimingToday() { -// try { -// lock.lock(); -// return backend.schedTimingToday(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.String studiedToday(int cards, double seconds) { -// try { -// lock.lock(); -// return backend.studiedToday(cards, seconds); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.String congratsLearnMessage(float nextDue, int remaining) { -// try { -// lock.lock(); -// return backend.congratsLearnMessage(nextDue, remaining); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void updateStats(long deckId, int newDelta, int reviewDelta, int millisecondDelta) { -// try { -// lock.lock(); -// backend.updateStats(deckId, newDelta, reviewDelta, millisecondDelta); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void extendLimits(long deckId, int newDelta, int reviewDelta) { -// try { -// lock.lock(); -// backend.extendLimits(deckId, newDelta, reviewDelta); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.CountsForDeckTodayOut countsForDeckToday(long did) { -// try { -// lock.lock(); -// return backend.countsForDeckToday(did); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.String cardStats(long cid) { -// try { -// lock.lock(); -// return backend.cardStats(cid); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.GraphsOut graphs(@Nullable String search, int days) { -// try { -// lock.lock(); -// return backend.graphs(search, days); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.CheckMediaOut checkMedia() { -// try { -// lock.lock(); -// return backend.checkMedia(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void trashMediaFiles(List fnames) { -// try { -// lock.lock(); -// backend.trashMediaFiles(fnames); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.String addMediaFile(@Nullable String desiredName, @Nullable ByteString data) { -// try { -// lock.lock(); -// return backend.addMediaFile(desiredName, data); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void emptyTrash() { -// try { -// lock.lock(); -// Generic.EmptyTrash(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void restoreTrash() { -// try { -// lock.lock(); -// backend.restoreTrash(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.DeckID addOrUpdateDeckLegacy(@Nullable ByteString deck, boolean preserveUsnAndMtime) { -// try { -// lock.lock(); -// return backend.addOrUpdateDeckLegacy(deck, preserveUsnAndMtime); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.DeckTreeNode deckTree(long now, long topDeckId) { -// try { -// lock.lock(); -// return backend.deckTree(now, topDeckId); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json deckTreeLegacy() { -// try { -// lock.lock(); -// return backend.deckTreeLegacy(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json getAllDecksLegacy() { -// try { -// lock.lock(); -// return backend.getAllDecksLegacy(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.DeckID getDeckIDByName(String name) { -// try { -// lock.lock(); -// return backend.getDeckIDByName(name); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json getDeckLegacy(long did) { -// try { -// lock.lock(); -// return backend.getDeckLegacy(did); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.DeckNames getDeckNames(boolean skipEmptyDefault, boolean includeFiltered) { -// try { -// lock.lock(); -// return backend.getDeckNames(skipEmptyDefault, includeFiltered); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json newDeckLegacy(boolean args) { -// try { -// lock.lock(); -// return backend.newDeckLegacy(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void removeDeck(long args) { -// try { -// lock.lock(); -// backend.removeDeck(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.DeckConfigID addOrUpdateDeckConfigLegacy(@Nullable ByteString config, boolean preserveUsnAndMtime) { -// try { -// lock.lock(); -// return backend.addOrUpdateDeckConfigLegacy(config, preserveUsnAndMtime); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json allDeckConfigLegacy() { -// try { -// lock.lock(); -// return backend.allDeckConfigLegacy(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json getDeckConfigLegacy(long dConfId) { -// try { -// lock.lock(); -// return backend.getDeckConfigLegacy(dConfId); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json newDeckConfigLegacy() { -// try { -// lock.lock(); -// return backend.newDeckConfigLegacy(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void removeDeckConfig(long dConfId) { -// try { -// lock.lock(); -// backend.removeDeckConfig(dConfId); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Card getCard(long cid) { -// try { -// lock.lock(); -// return backend.getCard(cid); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void updateCard(Backend.Card args) { -// try { -// lock.lock(); -// backend.updateCard(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.CardID addCard(Backend.Card args) { -// try { -// lock.lock(); -// return backend.addCard(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void removeCards(List cardIds) { -// try { -// lock.lock(); -// backend.removeCards(cardIds); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Note newNote(long noteTypidId) { -// try { -// lock.lock(); -// return backend.newNote(noteTypidId); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.NoteID addNote(@Nullable Backend.Note note, long deckId) { -// try { -// lock.lock(); -// return backend.addNote(note, deckId); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void updateNote(Backend.Note args) { -// try { -// lock.lock(); -// backend.updateNote(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Note getNote(long nid) { -// try { -// lock.lock(); -// return backend.getNote(nid); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void removeNotes(List noteIds, List cardIds) { -// try { -// lock.lock(); -// backend.removeNotes(noteIds, cardIds); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.UInt32 addNoteTags(List nids, @Nullable String tags) { -// try { -// lock.lock(); -// return backend.addNoteTags(nids, tags); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.UInt32 updateNoteTags(List nids, @Nullable String tags, @Nullable String replacement, boolean regex) { -// try { -// lock.lock(); -// return backend.updateNoteTags(nids, tags, replacement, regex); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.ClozeNumbersInNoteOut clozeNumbersInNote(Backend.Note args) { -// try { -// lock.lock(); -// return backend.clozeNumbersInNote(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void afterNoteUpdates(List nids, boolean markNotesModified, boolean generateCards) { -// try { -// lock.lock(); -// backend.afterNoteUpdates(nids, markNotesModified, generateCards); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.FieldNamesForNotesOut fieldNamesForNotes(List nids) { -// try { -// lock.lock(); -// return backend.fieldNamesForNotes(nids); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.NoteIsDuplicateOrEmptyOut noteIsDuplicateOrEmpty(Backend.Note args) { -// try { -// lock.lock(); -// return backend.noteIsDuplicateOrEmpty(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.NoteTypeID addOrUpdateNotetype(@Nullable ByteString json, boolean preserveUsnAndMtime) { -// try { -// lock.lock(); -// return backend.addOrUpdateNotetype(json, preserveUsnAndMtime); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json getStockNotetypeLegacy(@Nullable Backend.StockNoteType kind) { -// try { -// lock.lock(); -// return backend.getStockNotetypeLegacy(kind); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json getNotetypeLegacy(long noteTypeId) { -// try { -// lock.lock(); -// return backend.getNotetypeLegacy(noteTypeId); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.NoteTypeNames getNotetypeNames() { -// try { -// lock.lock(); -// return backend.getNotetypeNames(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.NoteTypeUseCounts getNotetypeNamesAndCounts() { -// try { -// lock.lock(); -// return backend.getNotetypeNamesAndCounts(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.NoteTypeID getNotetypeIDByName(String name) { -// try { -// lock.lock(); -// return backend.getNotetypeIDByName(name); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void removeNotetype(long noteTypeId) { -// try { -// lock.lock(); -// backend.removeNotetype(noteTypeId); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void openCollection(@Nullable String collectionPath, @Nullable String mediaFolderPath, @Nullable String mediaDbPath, @Nullable String logPath) { -// try { -// lock.lock(); -// backend.openCollection(collectionPath, mediaFolderPath, mediaDbPath, logPath); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void closeCollection(boolean downgradeToSchema11) { -// try { -// lock.lock(); -// backend.closeCollection(downgradeToSchema11); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.CheckDatabaseOut checkDatabase() { -// try { -// lock.lock(); -// return backend.checkDatabase(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void beforeUpload() { -// try { -// lock.lock(); -// backend.beforeUpload(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.String translateString(Backend.TranslateStringIn args) { -// try { -// lock.lock(); -// return backend.translateString(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.String formatTimespan(float seconds, @Nullable Backend.FormatTimespanIn.Context context) { -// try { -// lock.lock(); -// return backend.formatTimespan(seconds, context); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json i18nResources() { -// try { -// lock.lock(); -// return backend.i18nResources(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Bool registerTags(@Nullable String tags, boolean preserveUsn, int usn, boolean clearFirst) { -// try { -// lock.lock(); -// return backend.registerTags(tags, preserveUsn, usn, clearFirst); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.AllTagsOut allTags() { -// try { -// lock.lock(); -// return backend.allTags(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json getConfigJson(String args) { -// try { -// lock.lock(); -// return backend.getConfigJson(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void setConfigJson(@Nullable String key, @Nullable ByteString valueJson) { -// try { -// lock.lock(); -// backend.setConfigJson(key, valueJson); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void removeConfig(String args) { -// try { -// lock.lock(); -// backend.removeConfig(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void setAllConfig(Backend.Json args) { -// try { -// lock.lock(); -// backend.setAllConfig(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Json getAllConfig() { -// try { -// lock.lock(); -// return backend.getAllConfig(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public Backend.Preferences getPreferences() { -// try { -// lock.lock(); -// return backend.getPreferences(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void setPreferences(Backend.Preferences args) { -// try { -// lock.lock(); -// backend.setPreferences(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void openAnkiDroidCollection(Backend.OpenCollectionIn args) { -// try { -// lock.lock(); -// backend.openAnkiDroidCollection(args); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public boolean isOpen() { -// try { -// lock.lock(); -// return backend.isOpen(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void downgradeBackend(String collectionPath) throws BackendException { -// try { -// lock.lock(); -// backend.downgradeBackend(collectionPath); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public void close() throws IOException { -// try { -// lock.lock(); -// backend.close(); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public AdBackend.SchedTimingTodayOut2 schedTimingTodayLegacy(long createdSecs, int createdMinsWest, long nowSecs, int nowMinsWest, int rolloverHour) { -// try { -// lock.lock(); -// return backend.schedTimingTodayLegacy(createdSecs, createdMinsWest, nowSecs, nowMinsWest, rolloverHour); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public AdBackend.LocalMinutesWestOut localMinutesWestLegacy(long collectionCreationTime) { -// try { -// lock.lock(); -// return backend.localMinutesWestLegacy(collectionCreationTime); -// } finally { -// lock.unlock(); -// } -// } -// -// @Override -// public AdBackend.DebugActiveDatabaseSequenceNumbersOut debugActiveDatabaseSequenceNumbers(long backendPtr) { -// try { -// lock.lock(); -// return backend.debugActiveDatabaseSequenceNumbers(backendPtr); -// } finally { -// lock.unlock(); -// } -// } -//} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt similarity index 62% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt index 4f7098d38..14a3a1429 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt @@ -13,25 +13,19 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid -package net.ankiweb.rsdroid; - -import androidx.annotation.NonNull; - -import net.ankiweb.rsdroid.database.NotImplementedException; - -public class BackendUtils { +object BackendUtils { /** * * @throws android.database.sqlite.SQLiteDatabaseCorruptException If database is corrupt */ - public static void openAnkiDroidCollection(BackendV1 backendV1, String path) { - throw new NotImplementedException(); -// backendV1.openAnkiDroidCollection(Backend.OpenCol.newBuilder().setCollectionPath(path).build()); + // fixme: call backend directly + @JvmStatic + fun openAnkiDroidCollection(backendV1: BackendV1, path: String?) { + backendV1.openCollection(path!!) } - @NonNull - public static String getAnkiCommitHash() { - return BuildConfig.ANKI_COMMIT_HASH; - } -} + val ankiCommitHash: String + get() = BuildConfig.ANKI_COMMIT_HASH +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt index b2a689c64..5339bcb44 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt @@ -1,9 +1,28 @@ package net.ankiweb.rsdroid +import anki.ankidroid.GetActiveSequenceNumbersResponse +import anki.scheduler.SchedTimingTodayResponse import net.ankiweb.rsdroid.database.SQLHandler import java.io.Closeable +/** + * This interface describes valid methods when running the backend in the legacy schema 11 + * mode. + */ interface BackendV1 : SQLHandler, Closeable { fun openCollection(collectionPath: String, mediaFolderPath: String, mediaDbPath: String, logPath: String, forceSchema11: Boolean); + // schema11 + fun openCollection(collectionPath: String) { + openCollection(collectionPath, "", "", "", true) + } + fun closeCollection(downgrade: Boolean = false); + + fun schedTimingTodayLegacy(createdSecs: Long, createdMinsWest: Int, nowSecs: Long, nowMinsWest: Int, rolloverHour: Int): SchedTimingTodayResponse; + fun getActiveSequenceNumbers(): GetActiveSequenceNumbersResponse + + + /** + * Whether the backend (not collection) is open. Not really useful outside tests. + */ fun isOpen(): Boolean } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.java deleted file mode 100644 index 2f5837014..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.java +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid; - -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.database.RustV11SQLiteOpenHelperFactory; - -public class BackendV11Factory extends BackendFactory { - - /** - * Obtains an instance of BackendFactory which will connect to rsdroid. - * Each call will generate a separate instance which can handle a new Anki collection - */ - @RustV1Cleanup("RustBackendFailedException may be moved to a more appropriate location") - public static BackendV11Factory createInstance() throws RustBackendFailedException { - NativeMethods.ensureSetup(); - return new BackendV11Factory(); - } - - public SupportSQLiteOpenHelper.Factory getSQLiteOpener() { - return new RustV11SQLiteOpenHelperFactory(this); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt new file mode 100644 index 000000000..eef2f17db --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid + +import androidx.sqlite.db.SupportSQLiteOpenHelper +import net.ankiweb.rsdroid.NativeMethods.ensureSetup +import net.ankiweb.rsdroid.RustBackendFailedException +import net.ankiweb.rsdroid.database.RustV11SQLiteOpenHelperFactory + +class BackendV11Factory : BackendFactory() { + override val sQLiteOpener: SupportSQLiteOpenHelper.Factory + get() = RustV11SQLiteOpenHelperFactory(this) + + companion object { + /** + * Obtains an instance of BackendFactory which will connect to rsdroid. + * Each call will generate a separate instance which can handle a new Anki collection + */ + @RustV1Cleanup("RustBackendFailedException may be moved to a more appropriate location") + @Throws(RustBackendFailedException::class) + fun createInstance(): BackendV11Factory { + ensureSetup() + return BackendV11Factory() + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.kt index 7818f8741..76a5349f6 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.kt @@ -23,7 +23,6 @@ import anki.backend.BackendInit import anki.backend.GeneratedBackend import anki.generic.Int64 import com.google.protobuf.ByteString -import com.google.protobuf.GeneratedMessageV3 import com.google.protobuf.InvalidProtocolBufferException import org.json.JSONArray import org.json.JSONException @@ -44,7 +43,6 @@ open class BackendV1Impl(langs: Iterable = listOf("en")) : GeneratedBack return backendPointer != null } - /** * Open a backend instance, loading the shared library if not already loaded. */ @@ -79,7 +77,11 @@ open class BackendV1Impl(langs: Iterable = listOf("en")) : GeneratedBack * Open a collection. There must not already be an open collection. */ override fun openCollection(collectionPath: String, mediaFolderPath: String, mediaDbPath: String, logPath: String, forceSchema11: Boolean) { - super.openCollection(collectionPath, mediaFolderPath, mediaDbPath, logPath, forceSchema11) + try { + super.openCollection(collectionPath, mediaFolderPath, mediaDbPath, logPath, forceSchema11) + } catch (exc: BackendException.BackendDbException) { + throw exc.toSQLiteException("db open") + } this.collectionPath = collectionPath } @@ -146,40 +148,41 @@ open class BackendV1Impl(langs: Iterable = listOf("en")) : GeneratedBack override fun closeDatabase() { closeCollection(false) } - + override fun getPath(): String { return collectionPath!! } @CheckResult - override fun fullQuery(sql: String, vararg args: Any?): JSONArray { + override fun fullQuery(sql: String, bindArgs: Array?): JSONArray { return try { Timber.i("Rust: SQL query: '%s'", sql) - fullQueryInternal(sql, args as Array) + fullQueryInternal(sql, bindArgs) } catch (e: JSONException) { throw RuntimeException(e) } } @Throws(JSONException::class) - private fun fullQueryInternal(sql: String, args: Array): JSONArray { - return JSONArray(runDbCommand(dbRequestJson(sql, args)).json.toString()) + private fun fullQueryInternal(sql: String, bindArgs: Array?): JSONArray { + val output = runDbCommand(dbRequestJson(sql, bindArgs)).json.toStringUtf8() + return JSONArray(output) } - override fun insertForId(sql: String, args: Array): Long { + override fun insertForId(sql: String, bindArgs: Array?): Long { Timber.i("Rust: sql insert %s", sql) - return super.insertForId(dbRequestJson(sql, args)).`val` + return super.insertForId(dbRequestJson(sql, bindArgs)).`val` } - override fun executeGetRowsAffected(sql: String, args: Array): Int { + override fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int { Timber.i("Rust: executeGetRowsAffected %s", sql) - return runDbCommandForRowCount(dbRequestJson(sql, args)).`val`.toInt() + return runDbCommandForRowCount(dbRequestJson(sql, bindArgs)).`val`.toInt() } /* Begin Protobuf-based database streaming methods (#6) */ - override fun fullQueryProto(query: String, vararg args: Any?): DBResponse { - Timber.d("Rust: fullQueryProto %s", query) - return runDbCommandProto(dbRequestJson(query, args as Array)) + override fun fullQueryProto(query: String, bindArgs: Array?): DBResponse { + Timber.e("Rust: fullQueryProto %s", query) + return runDbCommandProto(dbRequestJson(query, bindArgs)) } override fun getNextSlice(startIndex: Long, sequenceNumber: Int): DBResponse { @@ -241,11 +244,11 @@ open class BackendV1Impl(langs: Iterable = listOf("en")) : GeneratedBack /** * Build a JSON DB request */ -private fun dbRequestJson(sql: String = "", args: Array = arrayOf(), kind: DbRequestKind = DbRequestKind.Query, firstRowOnly: Boolean = false): ByteString { +private fun dbRequestJson(sql: String = "", bindArgs: Array? = null, kind: DbRequestKind = DbRequestKind.Query, firstRowOnly: Boolean = false): ByteString { val o = JSONObject() o.put("kind", kind.name.lowercase()) o.put("sql", sql) - o.put("args", JSONArray(args)) + o.put("args", JSONArray((bindArgs ?: arrayOf()).toList())) o.put("first_row_only", firstRowOnly) return ByteString.copyFromUtf8(o.toString()) } @@ -273,6 +276,7 @@ private fun unpackResult(result: Array?): ByteArray { } catch (invalidProtocolBufferException: InvalidProtocolBufferException) { throw BackendException.fromException(invalidProtocolBufferException) } + print(pbError) throw BackendException.fromError(pbError) } else if (successBytes != null) { return successBytes diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.java deleted file mode 100644 index eee7b85b2..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid; - -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.database.RustVNextSQLiteOpenHelperFactory; - -/** A factory for the latest version of the backend (v16 currently) */ -public class BackendVNextFactory extends BackendFactory { - - /** - * Obtains an instance of BackendFactory which will connect to rsdroid. - * Each call will generate a separate instance which can handle a new Anki collection - */ - @RustV1Cleanup("RustBackendFailedException may be moved to a more appropriate location") - public static BackendVNextFactory createInstance() throws RustBackendFailedException { - NativeMethods.ensureSetup(); - return new BackendVNextFactory(); - } - - public SupportSQLiteOpenHelper.Factory getSQLiteOpener() { - return new RustVNextSQLiteOpenHelperFactory(this); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.kt new file mode 100644 index 000000000..a986cd11a --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.kt @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid + +import androidx.sqlite.db.SupportSQLiteOpenHelper +import net.ankiweb.rsdroid.NativeMethods.ensureSetup +import net.ankiweb.rsdroid.RustBackendFailedException +import net.ankiweb.rsdroid.database.RustVNextSQLiteOpenHelperFactory + +/** A factory for the latest version of the backend (v16 currently) */ +class BackendVNextFactory : BackendFactory() { + override val sQLiteOpener: SupportSQLiteOpenHelper.Factory + get() = RustVNextSQLiteOpenHelperFactory(this) + + companion object { + /** + * Obtains an instance of BackendFactory which will connect to rsdroid. + * Each call will generate a separate instance which can handle a new Anki collection + */ + @RustV1Cleanup("RustBackendFailedException may be moved to a more appropriate location") + @Throws(RustBackendFailedException::class) + fun createInstance(): BackendVNextFactory { + ensureSetup() + return BackendVNextFactory() + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt index af97c902a..acae2992c 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt @@ -1,10 +1,8 @@ package net.ankiweb.rsdroid -import net.ankiweb.rsdroid.RustBackendFailedException import android.os.Build import androidx.annotation.CheckResult -import kotlin.Throws -import net.ankiweb.rsdroid.NativeMethods +import net.ankiweb.rsdroid.RustBackendFailedException object NativeMethods { private var hasSetUp = false diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.kt similarity index 79% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.kt index 47935ec91..d5d75266e 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.kt @@ -13,8 +13,7 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -package net.ankiweb.rsdroid; +package net.ankiweb.rsdroid @RustV1Cleanup("This exists to force implementers to handle a `rsdroid failed to load` case" + "as I do not trust our ~16k target devices will all export the appropriate" + @@ -22,13 +21,7 @@ "This exists to ensure that there is a valid (working) fallback for V1 of the rust conversion" + "Once we prove this to be incorrect (or fix the bugs), we could remove this and assume that" + "rsdroid will always load without issue") -public class RustBackendFailedException extends Exception { - public RustBackendFailedException(Throwable error) { - super(error); - } - - @SuppressWarnings({"unused", "RedundantSuppression"}) - public RustBackendFailedException(String message) { - super(message); - } -} +class RustBackendFailedException : Exception { + constructor(error: Throwable?) : super(error) {} + constructor(message: String?) : super(message) {} +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.kt similarity index 75% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.kt index 501a45020..7673ac26e 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.kt @@ -13,21 +13,18 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid -package net.ankiweb.rsdroid; - -import java.lang.annotation.Repeatable; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy /** * Specifies that the provided class requires attention during the Rust conversion * These act as TODOs and should be audited before a production release is produced * After the Rust conversion is completed, this class should be deleted. */ -@Repeatable(RustCleanupCollection.class) +@JvmRepeatable(RustCleanupCollection::class) @Retention(RetentionPolicy.SOURCE) -public @interface RustCleanup { - /** Context and rationale for the cleanup, and the action which will be taken */ - String value(); -} +annotation class RustCleanup( + /** Context and rationale for the cleanup, and the action which will be taken */ + val value: String) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.kt similarity index 79% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.kt index 8b68b11d8..e84caf599 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.kt @@ -13,17 +13,14 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid -package net.ankiweb.rsdroid; - -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Retention +import java.lang.annotation.RetentionPolicy /** A collection of RustCleanup attributes. Not to be used directly * Allows multiple instances of @RustCleanup on a class - * See {@link java.lang.annotation.Repeatable}. + * See [java.lang.annotation.Repeatable]. */ @Retention(RetentionPolicy.SOURCE) -public @interface RustCleanupCollection { - RustCleanup[] value(); -} \ No newline at end of file +annotation class RustCleanupCollection(vararg val value: RustCleanup) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.kt similarity index 81% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.kt index 2b5ad24c1..88a2ff5ce 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustV1Cleanup.kt @@ -13,11 +13,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ - -package net.ankiweb.rsdroid; +package net.ankiweb.rsdroid @RustCleanup("To be removed (or converted to RustV2Cleanup)") -public @interface RustV1Cleanup { - /** Context and rationale for the cleanup, and the action which will be taken */ - String value(); -} +annotation class RustV1Cleanup( + /** Context and rationale for the cleanup, and the action which will be taken */ + val value: String) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.java deleted file mode 100644 index b529e6f5c..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database; - -import android.content.ContentResolver; -import android.database.CharArrayBuffer; -import android.database.ContentObserver; -import android.database.Cursor; -import android.database.DataSetObserver; -import android.net.Uri; -import android.os.Bundle; - -import timber.log.Timber; - -/** - * Base class for all database cursors, abstracting database methods to a common interface - * Throwing on non-database-related cursor-methods - * - * This is useful because cursors are an android-specific implementation and not a database-specific - * implementation, and many of the methods are not relevant. - */ -public abstract class AnkiDatabaseCursor implements Cursor { - - @Override - public boolean isFirst() { - return getPosition() == 0; - } - - @Override - public boolean isBeforeFirst() { - return getPosition() < 0; - } - - @Override - public abstract int getCount(); - @Override - public abstract int getPosition(); - - @Override - public abstract int getColumnIndex(String columnName); - - @Override - public abstract int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException; - - @Override - public abstract String getColumnName(int columnIndex); - - @Override - public abstract String[] getColumnNames(); - - @Override - public abstract int getColumnCount(); - - @Override - public abstract String getString(int columnIndex); - - @Override - public abstract short getShort(int columnIndex); - - @Override - public abstract int getInt(int columnIndex); - - @Override - public abstract long getLong(int columnIndex); - - @Override - public abstract float getFloat(int columnIndex); - - @Override - public abstract double getDouble(int columnIndex); - - @Override - public abstract boolean isNull(int columnIndex); - - @Override - public abstract void close(); - - @Override - public abstract boolean isClosed(); - - @Override - public abstract int getType(int columnIndex); - - @Override - public byte[] getBlob(int columnIndex) { - throw new NotImplementedException(); - } - - @Override - public void setNotificationUri(ContentResolver cr, Uri uri) { - throw new NotImplementedException(); - } - - @Override - public void deactivate() { - Timber.w("deactivate - not implemented - throwing"); - throw new NotImplementedException(); - } - - @Override - public void copyStringToBuffer(int columnIndex, CharArrayBuffer buffer) { - throw new NotImplementedException(); - } - - @Override - public boolean requery() { - Timber.w("requery - not implemented - throwing"); - throw new NotImplementedException(); - } - - @Override - public Uri getNotificationUri() { - throw new NotImplementedException(); - } - - @Override - public boolean getWantsAllOnMoveCalls() { - return false; - } - - @Override - public void setExtras(Bundle extras) { - throw new NotImplementedException(); - } - - @Override - public Bundle getExtras() { - throw new NotImplementedException(); - } - - @Override - public Bundle respond(Bundle extras) { - throw new NotImplementedException(); - } - - @Override - public abstract boolean moveToPosition(int position); - - @Override - public void registerContentObserver(ContentObserver observer) { - Timber.w("Not implemented: registerContentObserver - shouldn't matter unless requery() is called"); - } - - @Override - public void unregisterContentObserver(ContentObserver observer) { - Timber.w("Not implemented: unregisterContentObserver - shouldn't matter unless requery() is called"); - } - - @Override - public void registerDataSetObserver(DataSetObserver observer) { - Timber.w("Not implemented: registerDataSetObserver - shouldn't matter unless requery() is called"); - } - - @Override - public void unregisterDataSetObserver(DataSetObserver observer) { - Timber.w("Not implemented: unregisterDataSetObserver - shouldn't matter unless requery() is called"); - } - - - @Override - public boolean isLast() { - return getPosition() == getLastPosition(); - } - - @Override - public boolean isAfterLast() { - return getPosition() >= getCount(); - } - - @Override - public boolean move(int offset) { - return moveToPosition(getPosition() + offset); - } - - @Override - public boolean moveToLast() { - int toMoveTo = getLastPosition(); - return moveToPosition(toMoveTo); - } - - @Override - public boolean moveToFirst() { - return moveToPosition(0); - } - - @Override - public boolean moveToNext() { - int toMoveTo = getPosition() + 1; - return moveToPosition(toMoveTo); - } - - @Override - public boolean moveToPrevious() { - int toMoveTo = getPosition() - 1; - return moveToPosition(toMoveTo); - } - - protected int getLastPosition() { - return getCount() - 1; - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.kt new file mode 100644 index 000000000..f9e652f3d --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.kt @@ -0,0 +1,153 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid.database + +import android.content.ContentResolver +import android.database.CharArrayBuffer +import android.database.ContentObserver +import android.database.Cursor +import android.database.DataSetObserver +import android.net.Uri +import android.os.Bundle +import timber.log.Timber + +/** + * Base class for all database cursors, abstracting database methods to a common interface + * Throwing on non-database-related cursor-methods + * + * This is useful because cursors are an android-specific implementation and not a database-specific + * implementation, and many of the methods are not relevant. + */ +abstract class AnkiDatabaseCursor : Cursor { + override fun isFirst(): Boolean { + return position == 0 + } + + override fun isBeforeFirst(): Boolean { + return position < 0 + } + + abstract override fun getCount(): Int + abstract override fun getPosition(): Int + abstract override fun getColumnIndex(columnName: String): Int + @Throws(IllegalArgumentException::class) + abstract override fun getColumnIndexOrThrow(columnName: String): Int + abstract override fun getColumnName(columnIndex: Int): String + abstract override fun getColumnNames(): Array + abstract override fun getColumnCount(): Int + abstract override fun getString(columnIndex: Int): String? + abstract override fun getShort(columnIndex: Int): Short + abstract override fun getInt(columnIndex: Int): Int + abstract override fun getLong(columnIndex: Int): Long + abstract override fun getFloat(columnIndex: Int): Float + abstract override fun getDouble(columnIndex: Int): Double + abstract override fun isNull(columnIndex: Int): Boolean + abstract override fun close() + abstract override fun isClosed(): Boolean + abstract override fun getType(columnIndex: Int): Int + override fun getBlob(columnIndex: Int): ByteArray { + throw NotImplementedException() + } + + override fun setNotificationUri(cr: ContentResolver, uri: Uri) { + throw NotImplementedException() + } + + override fun deactivate() { + Timber.w("deactivate - not implemented - throwing") + throw NotImplementedException() + } + + override fun copyStringToBuffer(columnIndex: Int, buffer: CharArrayBuffer) { + throw NotImplementedException() + } + + override fun requery(): Boolean { + Timber.w("requery - not implemented - throwing") + throw NotImplementedException() + } + + override fun getNotificationUri(): Uri { + throw NotImplementedException() + } + + override fun getWantsAllOnMoveCalls(): Boolean { + return false + } + + override fun setExtras(extras: Bundle) { + throw NotImplementedException() + } + + override fun getExtras(): Bundle { + throw NotImplementedException() + } + + override fun respond(extras: Bundle): Bundle { + throw NotImplementedException() + } + + abstract override fun moveToPosition(position: Int): Boolean + override fun registerContentObserver(observer: ContentObserver) { + Timber.w("Not implemented: registerContentObserver - shouldn't matter unless requery() is called") + } + + override fun unregisterContentObserver(observer: ContentObserver) { + Timber.w("Not implemented: unregisterContentObserver - shouldn't matter unless requery() is called") + } + + override fun registerDataSetObserver(observer: DataSetObserver) { + Timber.w("Not implemented: registerDataSetObserver - shouldn't matter unless requery() is called") + } + + override fun unregisterDataSetObserver(observer: DataSetObserver) { + Timber.w("Not implemented: unregisterDataSetObserver - shouldn't matter unless requery() is called") + } + + override fun isLast(): Boolean { + return position == lastPosition + } + + override fun isAfterLast(): Boolean { + return position >= count + } + + override fun move(offset: Int): Boolean { + return moveToPosition(position + offset) + } + + override fun moveToLast(): Boolean { + val toMoveTo = lastPosition + return moveToPosition(toMoveTo) + } + + override fun moveToFirst(): Boolean { + return moveToPosition(0) + } + + override fun moveToNext(): Boolean { + val toMoveTo = position + 1 + return moveToPosition(toMoveTo) + } + + override fun moveToPrevious(): Boolean { + val toMoveTo = position - 1 + return moveToPosition(toMoveTo) + } + + protected val lastPosition: Int + protected get() = count - 1 +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.java deleted file mode 100644 index 3716670ff..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database; - -import org.json.JSONArray; -import org.json.JSONException; - -public abstract class AnkiJsonDatabaseCursor extends AnkiDatabaseCursor { - - private final Session backend; - protected final String query; - protected final Object[] bindArgs; - protected JSONArray results; - private String[] columnMapping; - - - public AnkiJsonDatabaseCursor(Session backend, String query, Object[] bindArgs) { - this.backend = backend; - this.query = query; - this.bindArgs = bindArgs; - } - - // There's no need to close this cursor. - @Override - public boolean isClosed() { - return true; - } - - - @Override - public int getColumnCount() { - if (results.length() == 0) { - return 0; - } else { - try { - return results.getJSONArray(0).length(); - } catch (JSONException e) { - return 0; - } - } - } - - @Override - public String getString(int columnIndex) { - try { - return getRowAtCurrentPosition().getString(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - - @Override - public short getShort(int columnIndex) { - try { - return (short) getRowAtCurrentPosition().getInt(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public int getInt(int columnIndex) { - try { - JSONArray rowAtCurrentPosition = getRowAtCurrentPosition(); - if (rowAtCurrentPosition.isNull(columnIndex)) { - return 0; - } - return rowAtCurrentPosition.getInt(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public long getLong(int columnIndex) { - try { - JSONArray rowAtCurrentPosition = getRowAtCurrentPosition(); - if (rowAtCurrentPosition.isNull(columnIndex)) { - return 0; - } - return rowAtCurrentPosition.getLong(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public float getFloat(int columnIndex) { - try { - JSONArray rowAtCurrentPosition = getRowAtCurrentPosition(); - if (rowAtCurrentPosition.isNull(columnIndex)) { - return 0.0f; - } - return (float) rowAtCurrentPosition.getDouble(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public double getDouble(int columnIndex) { - try { - JSONArray rowAtCurrentPosition = getRowAtCurrentPosition(); - if (rowAtCurrentPosition.isNull(columnIndex)) { - return 0.0f; - } - return rowAtCurrentPosition.getDouble(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public boolean isNull(int columnIndex) { - try { - return getRowAtCurrentPosition().isNull(columnIndex); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - @Override - public int getColumnIndex(String columnName) { - try { - String[] names = getColumnNames(); - for (int i = 0; i < names.length; i++) { - if (columnName.equals(names[i])) { - return i; - } - } - } catch (Exception e) { - return -1; - } - return -1; - } - - @Override - public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { - try { - String[] names = getColumnNames(); - for (int i = 0; i < names.length; i++) { - if (columnName.equals(names[i])) { - return i; - } - } - } catch (Exception e) { - throw new IllegalArgumentException(e); - } - throw new IllegalArgumentException(String.format("Could not find column '%s'", columnName)); - } - - @Override - public String getColumnName(int columnIndex) { - return getColumnNamesInternal()[columnIndex]; - } - - @Override - public String[] getColumnNames() { - return getColumnNamesInternal(); - } - - private String[] getColumnNamesInternal() { - if (columnMapping == null) { - columnMapping = backend.getColumnNames(query); - if (columnMapping == null) { - throw new IllegalStateException("unable to obtain column mapping"); - } - } - - return columnMapping; - } - - protected abstract JSONArray getRowAtCurrentPosition() throws JSONException; - - protected JSONArray fullQuery(String query, Object[] bindArgs) { - return backend.fullQuery(query, bindArgs); - } - - @Override - public int getType(int columnIndex) { - throw new NotImplementedException(); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.kt new file mode 100644 index 000000000..0542d9815 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.kt @@ -0,0 +1,166 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid.database + +import org.json.JSONArray +import org.json.JSONException + +abstract class AnkiJsonDatabaseCursor(private val backend: Session, protected val query: String, protected val bindArgs: Array) : AnkiDatabaseCursor() { + protected var results: JSONArray? = null + private var columnMapping: Array? = null + + // There's no need to close this cursor. + override fun isClosed(): Boolean { + return true + } + + override fun getColumnCount(): Int { + return if (results!!.length() == 0) { + 0 + } else { + try { + results!!.getJSONArray(0).length() + } catch (e: JSONException) { + 0 + } + } + } + + override fun getString(columnIndex: Int): String? { + return try { + rowAtCurrentPosition.getString(columnIndex) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + override fun getShort(columnIndex: Int): Short { + return try { + rowAtCurrentPosition.getInt(columnIndex).toShort() + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + override fun getInt(columnIndex: Int): Int { + return try { + val rowAtCurrentPosition = rowAtCurrentPosition + if (rowAtCurrentPosition.isNull(columnIndex)) { + 0 + } else rowAtCurrentPosition.getInt(columnIndex) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + override fun getLong(columnIndex: Int): Long { + return try { + val rowAtCurrentPosition = rowAtCurrentPosition + if (rowAtCurrentPosition.isNull(columnIndex)) { + 0 + } else rowAtCurrentPosition.getLong(columnIndex) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + override fun getFloat(columnIndex: Int): Float { + return try { + val rowAtCurrentPosition = rowAtCurrentPosition + if (rowAtCurrentPosition.isNull(columnIndex)) { + 0.0f + } else rowAtCurrentPosition.getDouble(columnIndex).toFloat() + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + override fun getDouble(columnIndex: Int): Double { + return try { + if (rowAtCurrentPosition.isNull(columnIndex)) { + 0.0 + } else { + rowAtCurrentPosition.getDouble(columnIndex) + } + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + override fun isNull(columnIndex: Int): Boolean { + return try { + rowAtCurrentPosition.isNull(columnIndex) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + override fun getColumnIndex(columnName: String): Int { + try { + val names = columnNames + for (i in names.indices) { + if (columnName == names[i]) { + return i + } + } + } catch (e: Exception) { + return -1 + } + return -1 + } + + @Throws(IllegalArgumentException::class) + override fun getColumnIndexOrThrow(columnName: String): Int { + try { + val names = columnNames + for (i in names.indices) { + if (columnName == names[i]) { + return i + } + } + } catch (e: Exception) { + throw IllegalArgumentException(e) + } + throw IllegalArgumentException(String.format("Could not find column '%s'", columnName)) + } + + override fun getColumnName(columnIndex: Int): String { + return columnNamesInternal[columnIndex] + } + + override fun getColumnNames(): Array { + return columnNamesInternal + } + + private val columnNamesInternal: Array + private get() { + if (columnMapping == null) { + columnMapping = backend.getColumnNames(query) + checkNotNull(columnMapping) { "unable to obtain column mapping" } + } + return columnMapping!! + } + + @get:Throws(JSONException::class) + protected abstract val rowAtCurrentPosition: JSONArray + protected fun fullQuery(query: String, bindArgs: Array?): JSONArray { + return backend.fullQuery(query, bindArgs) + } + + override fun getType(columnIndex: Int): Int { + throw NotImplementedException() + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.kt similarity index 65% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.kt index 23b00cd2f..7add3a8c4 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.kt @@ -13,21 +13,16 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.database -package net.ankiweb.rsdroid.database; +class NotImplementedException : RuntimeException { + constructor(message: String?) : super(message) {} + constructor() {} -public class NotImplementedException extends RuntimeException { - - public NotImplementedException(String message) { - super(message); - } - - public NotImplementedException() { - - } - - /** A method which we should implement */ - public static NotImplementedException todo() { - return new NotImplementedException(); + companion object { + /** A method which we should implement */ + fun todo(): NotImplementedException { + return NotImplementedException() + } } -} +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.java deleted file mode 100644 index cc892e02e..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database; - -import android.database.Cursor; -import android.database.sqlite.SQLiteDoneException; - -import androidx.sqlite.db.SupportSQLiteStatement; - -import java.util.HashMap; -import java.util.Set; - -public class RustSQLiteStatement implements SupportSQLiteStatement { - private final RustSupportSQLiteDatabase database; - private final String sql; - - private final HashMap mBindings = new HashMap<>(); - - public RustSQLiteStatement(RustSupportSQLiteDatabase database, String sql) { - this.database = database; - this.sql = sql; - } - - @Override - public void execute() { - database.query(sql, getBindings()).close(); - } - - @Override - public int executeUpdateDelete() { - return database.executeGetRowsAffected(sql, getBindings()); - } - - @Override - public long executeInsert() { - return database.insertForForId(sql, getBindings()); - } - - @Override - public long simpleQueryForLong() { - try (Cursor query = database.query(sql, getBindings())) { - if (!query.moveToFirst()) { - throw new SQLiteDoneException(); - } - return query.getLong(0); - } - } - - @Override - public String simpleQueryForString() { - try (Cursor query = database.query(sql, getBindings())) { - if (!query.moveToFirst()) { - throw new SQLiteDoneException(); - } - return query.getString(0); - } - } - - @Override - public void bindNull(int index) { - bind(index, null); - } - - @Override - public void bindLong(int index, long value) { - bind(index, value); - } - - @Override - public void bindDouble(int index, double value) { - bind(index, value); - } - - @Override - public void bindString(int index, String value) { - bind(index, value); - } - - @Override - public void bindBlob(int index, byte[] value) { - bind(index, value); - } - - @Override - public void clearBindings() { - mBindings.clear(); - } - - @Override - public void close() { - - } - - - private void bind(int index, Object value) { - mBindings.put(index, value); - } - - Object[] getBindings() { - int max = max(mBindings.keySet()); - - Object[] ret = new Object[max + 1]; - for (int i = 0; i <= max; i++) { - if (mBindings.containsKey(i)) { - ret[i] = mBindings.get(i); - } - } - return ret; - } - - private int max(Set integerSet) { - int max = -1; - for (int i : integerSet) { - max = Math.max(max, i); - } - return max; - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.kt new file mode 100644 index 000000000..75066321b --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.kt @@ -0,0 +1,101 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid.database + +import android.database.sqlite.SQLiteDoneException +import androidx.sqlite.db.SupportSQLiteStatement + +class RustSQLiteStatement(private val database: RustSupportSQLiteDatabase, private val sql: String) : SupportSQLiteStatement { + private val mBindings = HashMap() + override fun execute() { + database.query(sql, bindings).close() + } + + override fun executeUpdateDelete(): Int { + return database.executeGetRowsAffected(sql, bindings) + } + + override fun executeInsert(): Long { + return database.insertForForId(sql, bindings) + } + + override fun simpleQueryForLong(): Long { + database.query(sql, bindings).use { query -> + if (!query.moveToFirst()) { + throw SQLiteDoneException() + } + return query.getLong(0) + } + } + + override fun simpleQueryForString(): String { + database.query(sql, bindings).use { query -> + if (!query.moveToFirst()) { + throw SQLiteDoneException() + } + return query.getString(0) + } + } + + override fun bindNull(index: Int) { + bind(index, null) + } + + override fun bindLong(index: Int, value: Long) { + bind(index, value) + } + + override fun bindDouble(index: Int, value: Double) { + bind(index, value) + } + + override fun bindString(index: Int, value: String) { + bind(index, value) + } + + override fun bindBlob(index: Int, value: ByteArray) { + bind(index, value) + } + + override fun clearBindings() { + mBindings.clear() + } + + override fun close() {} + private fun bind(index: Int, value: Any?) { + mBindings[index] = value + } + + val bindings: Array + get() { + val max = max(mBindings.keys) + val ret = arrayOfNulls(max + 1) + for (i in 0..max) { + if (mBindings.containsKey(i)) { + ret[i] = mBindings[i] + } + } + return ret + } + + private fun max(integerSet: Set): Int { + var max = -1 + for (i in integerSet) { + max = Math.max(max, i) + } + return max + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.java deleted file mode 100644 index 57e15c38d..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.java +++ /dev/null @@ -1,368 +0,0 @@ -/* - * Copyright (c) 2020 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 . - * - * Contains code under the following license - * - * Copyright (C) 2006 The Android Open Source Project - * Copyright (C) 2016 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * from android.database.sqlite.SQLiteStatement - * from update/insert/delete - */ - -package net.ankiweb.rsdroid.database; - -import android.content.ContentValues; -import android.database.Cursor; -import android.database.SQLException; -import android.database.sqlite.SQLiteTransactionListener; -import android.os.CancellationSignal; -import android.util.Pair; - -import androidx.sqlite.db.SupportSQLiteDatabase; -import androidx.sqlite.db.SupportSQLiteQuery; -import androidx.sqlite.db.SupportSQLiteStatement; - -import net.ankiweb.rsdroid.BackendV1; - -import java.util.List; -import java.util.Locale; -import java.util.Map; - -import static android.text.TextUtils.isEmpty; - -public class RustSupportSQLiteDatabase implements SupportSQLiteDatabase { - private static final String[] CONFLICT_VALUES = new String[] - {"", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE "}; - - private final ThreadLocal sessionFactory; - private final boolean isReadOnly; - private boolean isOpen; - - public RustSupportSQLiteDatabase(BackendV1 backend, boolean readOnly) { - if (backend == null) { - throw new IllegalArgumentException("backend was null"); - } - this.sessionFactory = new SessionThreadLocal(backend); - this.isReadOnly = readOnly; - this.isOpen = true; - } - - @Override - public boolean isReadOnly() { - return isReadOnly; - } - - @Override - public boolean isOpen() { - return isOpen; - } - - @Override - public SupportSQLiteStatement compileStatement(String sql) { - return new RustSQLiteStatement(this, sql); - } - - @Override - public void beginTransaction() { - getSession().beginTransaction(); - } - - @Override - public void endTransaction() { - getSession().endTransaction(); - } - - @Override - public void setTransactionSuccessful() { - getSession().setTransactionSuccessful(); - } - - @Override - public boolean inTransaction() { - return getSession().inTransaction(); - } - - @Override - public int getVersion() { - throw NotImplementedException.todo(); - } - - @Override - public void setVersion(int version) { - throw NotImplementedException.todo(); - } - - @Override - public Cursor query(String query) { - return query(query, null); - } - - @Override - public Cursor query(String query, Object[] bindArgs) { - return new StreamingProtobufSQLiteCursor(getSession(), query, bindArgs); - } - - - @Override - public Cursor query(SupportSQLiteQuery query) { - throw NotImplementedException.todo(); - } - - @Override - public Cursor query(SupportSQLiteQuery query, CancellationSignal cancellationSignal) { - throw NotImplementedException.todo(); - } - - @Override - public long insert(String table, int conflictAlgorithm, ContentValues values) throws SQLException { - StringBuilder sql = new StringBuilder(); - sql.append("INSERT"); - sql.append(CONFLICT_VALUES[conflictAlgorithm]); - sql.append(" INTO "); - sql.append(table); - sql.append('('); - - Object[] bindArgs = null; - int size = (values != null && values.size() > 0) - ? values.size() : 0; - if (size > 0) { - bindArgs = new Object[size]; - int i = 0; - for (Map.Entry entry : values.valueSet()) { - sql.append((i > 0) ? "," : ""); - sql.append(entry.getKey()); - bindArgs[i++] = entry.getValue(); - } - sql.append(')'); - sql.append(" VALUES ("); - for (i = 0; i < size; i++) { - sql.append((i > 0) ? ",?" : "?"); - } - } else { - sql.append((String) null).append(") VALUES (NULL"); - } - sql.append(')'); - - query(sql.toString(), bindArgs).close(); - return 0; - } - - @Override - public int update(String table, int conflictAlgorithm, ContentValues values, String whereClause, Object[] whereArgs) { - // taken from SQLiteDatabase class. - if (values == null || values.size() == 0) { - throw new IllegalArgumentException("Empty values"); - } - StringBuilder sql = new StringBuilder(120); - sql.append("UPDATE "); - sql.append(CONFLICT_VALUES[conflictAlgorithm]); - sql.append(table); - sql.append(" SET "); - - // move all bind args to one array - int setValuesSize = values.size(); - int bindArgsSize = (whereArgs == null) ? setValuesSize : (setValuesSize + whereArgs.length); - Object[] bindArgs = new Object[bindArgsSize]; - int i = 0; - for (String colName : values.keySet()) { - sql.append((i > 0) ? "," : ""); - sql.append(colName); - bindArgs[i++] = values.get(colName); - sql.append("=?"); - } - if (whereArgs != null) { - for (i = setValuesSize; i < bindArgsSize; i++) { - bindArgs[i] = whereArgs[i - setValuesSize]; - } - } - if (!isEmpty(whereClause)) { - sql.append(" WHERE "); - sql.append(whereClause); - } - - return this.executeGetRowsAffected(sql.toString(), bindArgs); - } - - @Override - public void execSQL(String sql) throws SQLException { - execSQL(sql, null); - } - - @Override - public void execSQL(String sql, Object[] bindArgs) throws SQLException { - query(sql, bindArgs).close(); - } - - @Override - public boolean needUpgrade(int newVersion) { - // needed for metaDB, but not for Anki DB - throw NotImplementedException.todo(); - } - - @Override - public String getPath() { - return getSession().getPath(); - } - - - @Override - public boolean isWriteAheadLoggingEnabled() { - return false; - } - - @Override - public void disableWriteAheadLogging() { - // Nothing to do - openAnkiDroidCollection does this - } - - @Override - public boolean isDatabaseIntegrityOk() { - Cursor pragma_integrity_check = query("pragma integrity_check"); - if (!pragma_integrity_check.moveToFirst()) { - return false; - } - String value = pragma_integrity_check.getString(0); - return "ok".equals(value); - } - - @Override - public void close() { - isOpen = false; - getSession().closeDatabase(); - } - - /* Not part of interface */ - - public int executeGetRowsAffected(String sql, Object[] bindArgs) { - try { - return getSession().executeGetRowsAffected(sql, bindArgs); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - public long insertForForId(String sql, Object[] bindArgs) { - try { - return getSession().insertForId(sql, bindArgs); - } catch (Exception e) { - throw new RuntimeException(e); - } - } - - /** Helper methods */ - - private Session getSession() { - return sessionFactory.get(); - } - - /** Confirmed that the below are not used for our code */ - - - @Override - public int delete(String table, String whereClause, Object[] whereArgs) { - // Complex to implement and not required - throw new NotImplementedException(); - } - - @Override - public boolean isDbLockedByCurrentThread() { - throw new NotImplementedException(); - } - - @Override - public boolean yieldIfContendedSafely() { - throw new NotImplementedException(); - } - - @Override - public boolean yieldIfContendedSafely(long sleepAfterYieldDelay) { - throw new NotImplementedException(); - } - - @Override - public void setLocale(Locale locale) { - throw new NotImplementedException(); - } - - @Override - public void setMaxSqlCacheSize(int cacheSize) { - throw new NotImplementedException(); - } - - @Override - public long getMaximumSize() { - throw new NotImplementedException(); - } - - @Override - public long setMaximumSize(long numBytes) { - throw new NotImplementedException(); - } - - @Override - public long getPageSize() { - throw new NotImplementedException(); - } - - @Override - public void setPageSize(long numBytes) { - throw new NotImplementedException(); - } - - - @Override - public void setForeignKeyConstraintsEnabled(boolean enable) { - throw new NotImplementedException(); - } - - - @Override - public boolean enableWriteAheadLogging() { - throw new NotImplementedException(); - } - - @Override - public List> getAttachedDbs() { - throw new NotImplementedException(); - } - - @Override - public void beginTransactionNonExclusive() { - throw new NotImplementedException(); - } - - @Override - public void beginTransactionWithListener(SQLiteTransactionListener transactionListener) { - throw new NotImplementedException(); - } - - @Override - public void beginTransactionWithListenerNonExclusive(SQLiteTransactionListener transactionListener) { - throw new NotImplementedException(); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt new file mode 100644 index 000000000..ee5e5e533 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt @@ -0,0 +1,312 @@ +/* + * Copyright (c) 2020 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 . + * + * Contains code under the following license + * + * Copyright (C) 2006 The Android Open Source Project + * Copyright (C) 2016 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * from android.database.sqlite.SQLiteStatement + * from update/insert/delete + */ +package net.ankiweb.rsdroid.database + +import android.content.ContentValues +import android.database.Cursor +import android.database.SQLException +import android.database.sqlite.SQLiteTransactionListener +import android.os.CancellationSignal +import android.text.TextUtils +import android.util.Pair +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteQuery +import androidx.sqlite.db.SupportSQLiteStatement +import net.ankiweb.rsdroid.BackendV1 +import java.util.* + +class RustSupportSQLiteDatabase(backend: BackendV1?, readOnly: Boolean) : SupportSQLiteDatabase { + private val sessionFactory: ThreadLocal + private val isReadOnly: Boolean + private var isOpen: Boolean + override fun isReadOnly(): Boolean { + return isReadOnly + } + + override fun isOpen(): Boolean { + return isOpen + } + + override fun compileStatement(sql: String): SupportSQLiteStatement { + return RustSQLiteStatement(this, sql) + } + + override fun beginTransaction() { + session.beginTransaction() + } + + override fun endTransaction() { + session.endTransaction() + } + + override fun setTransactionSuccessful() { + session.setTransactionSuccessful() + } + + override fun inTransaction(): Boolean { + return session.inTransaction() + } + + override fun getVersion(): Int { + throw NotImplementedException.Companion.todo() + } + + override fun setVersion(version: Int) { + throw NotImplementedException.Companion.todo() + } + + override fun query(query: String): Cursor { + return query(query, null) + } + + override fun query(query: String, bindArgs: Array?): Cursor { + return StreamingProtobufSQLiteCursor(session, query, bindArgs) + } + + override fun query(query: SupportSQLiteQuery): Cursor { + throw NotImplementedException.Companion.todo() + } + + override fun query(query: SupportSQLiteQuery, cancellationSignal: CancellationSignal): Cursor { + throw NotImplementedException.Companion.todo() + } + + @Throws(SQLException::class) + override fun insert(table: String, conflictAlgorithm: Int, values: ContentValues): Long { + val sql = StringBuilder() + sql.append("INSERT") + sql.append(CONFLICT_VALUES[conflictAlgorithm]) + sql.append(" INTO ") + sql.append(table) + sql.append('(') + var bindArgs: Array? = null + val size = if (values != null && values.size() > 0) values.size() else 0 + if (size > 0) { + bindArgs = arrayOfNulls(size) + var i = 0 + for ((key, value) in values.valueSet()) { + sql.append(if (i > 0) "," else "") + sql.append(key) + bindArgs[i++] = value + } + sql.append(')') + sql.append(" VALUES (") + i = 0 + while (i < size) { + sql.append(if (i > 0) ",?" else "?") + i++ + } + } else { + sql.append(null as String?).append(") VALUES (NULL") + } + sql.append(')') + query(sql.toString(), bindArgs).close() + return 0 + } + + override fun update(table: String, conflictAlgorithm: Int, values: ContentValues, whereClause: String, whereArgs: Array?): Int { + // taken from SQLiteDatabase class. + require(!(values == null || values.size() == 0)) { "Empty values" } + val sql = StringBuilder(120) + sql.append("UPDATE ") + sql.append(CONFLICT_VALUES[conflictAlgorithm]) + sql.append(table) + sql.append(" SET ") + + // move all bind args to one array + val setValuesSize = values.size() + val bindArgsSize = if (whereArgs == null) setValuesSize else setValuesSize + whereArgs.size + val bindArgs = arrayOfNulls(bindArgsSize) + var i = 0 + for (colName in values.keySet()) { + sql.append(if (i > 0) "," else "") + sql.append(colName) + bindArgs[i++] = values[colName] + sql.append("=?") + } + if (whereArgs != null) { + i = setValuesSize + while (i < bindArgsSize) { + bindArgs[i] = whereArgs[i - setValuesSize] + i++ + } + } + if (!TextUtils.isEmpty(whereClause)) { + sql.append(" WHERE ") + sql.append(whereClause) + } + return executeGetRowsAffected(sql.toString(), bindArgs) + } + + @Throws(SQLException::class) + override fun execSQL(sql: String) { + execSQL(sql, null) + } + + @Throws(SQLException::class) + override fun execSQL(sql: String, bindArgs: Array?) { + query(sql, bindArgs).close() + } + + override fun needUpgrade(newVersion: Int): Boolean { + // needed for metaDB, but not for Anki DB + throw NotImplementedException.Companion.todo() + } + + override fun getPath(): String { + return session.getPath() + } + + override fun isWriteAheadLoggingEnabled(): Boolean { + return false + } + + override fun disableWriteAheadLogging() { + // Nothing to do - openAnkiDroidCollection does this + } + + override fun isDatabaseIntegrityOk(): Boolean { + val pragma_integrity_check = query("pragma integrity_check") + if (!pragma_integrity_check.moveToFirst()) { + return false + } + val value = pragma_integrity_check.getString(0) + return "ok" == value + } + + override fun close() { + isOpen = false + session.closeDatabase() + } + + /* Not part of interface */ + fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int { + return try { + session.executeGetRowsAffected(sql, bindArgs) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + fun insertForForId(sql: String, bindArgs: Array?): Long { + return try { + session.insertForId(sql, bindArgs) + } catch (e: Exception) { + throw RuntimeException(e) + } + } + + /** Helper methods */ + private val session: Session + private get() = sessionFactory.get() + + /** Confirmed that the below are not used for our code */ + override fun delete(table: String, whereClause: String, whereArgs: Array): Int { + // Complex to implement and not required + throw NotImplementedException() + } + + override fun isDbLockedByCurrentThread(): Boolean { + throw NotImplementedException() + } + + override fun yieldIfContendedSafely(): Boolean { + throw NotImplementedException() + } + + override fun yieldIfContendedSafely(sleepAfterYieldDelay: Long): Boolean { + throw NotImplementedException() + } + + override fun setLocale(locale: Locale) { + throw NotImplementedException() + } + + override fun setMaxSqlCacheSize(cacheSize: Int) { + throw NotImplementedException() + } + + override fun getMaximumSize(): Long { + throw NotImplementedException() + } + + override fun setMaximumSize(numBytes: Long): Long { + throw NotImplementedException() + } + + override fun getPageSize(): Long { + throw NotImplementedException() + } + + override fun setPageSize(numBytes: Long) { + throw NotImplementedException() + } + + override fun setForeignKeyConstraintsEnabled(enable: Boolean) { + throw NotImplementedException() + } + + override fun enableWriteAheadLogging(): Boolean { + throw NotImplementedException() + } + + override fun getAttachedDbs(): List> { + throw NotImplementedException() + } + + override fun beginTransactionNonExclusive() { + throw NotImplementedException() + } + + override fun beginTransactionWithListener(transactionListener: SQLiteTransactionListener) { + throw NotImplementedException() + } + + override fun beginTransactionWithListenerNonExclusive(transactionListener: SQLiteTransactionListener) { + throw NotImplementedException() + } + + companion object { + private val CONFLICT_VALUES = arrayOf("", " OR ROLLBACK ", " OR ABORT ", " OR FAIL ", " OR IGNORE ", " OR REPLACE ") + } + + init { + requireNotNull(backend) { "backend was null" } + sessionFactory = SessionThreadLocal(backend) + isReadOnly = readOnly + isOpen = true + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.java deleted file mode 100644 index eefc0d888..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.java +++ /dev/null @@ -1,85 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.sqlite.db.SupportSQLiteDatabase; -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.BackendV1; - -public abstract class RustSupportSQLiteOpenHelper implements SupportSQLiteOpenHelper { - @Nullable - protected final Configuration configuration; - @Nullable - protected final BackendV1 backend; - protected BackendFactory backendFactory; - protected SupportSQLiteDatabase database; - - public RustSupportSQLiteOpenHelper(@NonNull Configuration configuration, BackendFactory backendFactory) { - this.configuration = configuration; - this.backendFactory = backendFactory; - this.backend = null; - } - - public RustSupportSQLiteOpenHelper(@NonNull BackendV1 backend) { - if (!backend.isOpen()) { - throw new IllegalStateException("Backend should be open"); - } - this.backend = backend; - configuration = null; - } - - @Nullable - @Override - public String getDatabaseName() { - if (backend != null) { - return backend.getPath(); - } else if (configuration != null) { - return configuration.name; - } else { - throw new IllegalStateException("Class invalid: no config or backend"); - } - } - - @Override - public void setWriteAheadLoggingEnabled(boolean enabled) { - throw new NotImplementedException(); - } - - @Override - public SupportSQLiteDatabase getWritableDatabase() { - if (database == null) { - this.database = createRustSupportSQLiteDatabase(false); - } - return database; - } - - @Override - public SupportSQLiteDatabase getReadableDatabase() { - throw new NotImplementedException("Not supported by Rust - requires open collection"); - } - - @Override - public void close() { - - } - - protected abstract SupportSQLiteDatabase createRustSupportSQLiteDatabase(@SuppressWarnings("SameParameterValue") boolean readOnly); -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt new file mode 100644 index 000000000..e96a78fd2 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt @@ -0,0 +1,67 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid.database + +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteOpenHelper +import net.ankiweb.rsdroid.BackendFactory +import net.ankiweb.rsdroid.BackendV1 + +abstract class RustSupportSQLiteOpenHelper : SupportSQLiteOpenHelper { + protected val configuration: SupportSQLiteOpenHelper.Configuration? + protected val backend: BackendV1? + protected var backendFactory: BackendFactory? = null + protected var database: SupportSQLiteDatabase? = null + + constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) { + this.configuration = configuration + this.backendFactory = backendFactory + backend = null + } + + constructor(backend: BackendV1) { + check(backend.isOpen()) { "Backend should be open" } + this.backend = backend + configuration = null + } + + override fun getDatabaseName(): String? { + return backend?.getPath() + ?: if (configuration != null) { + configuration.name + } else { + throw IllegalStateException("Class invalid: no config or backend") + } + } + + override fun setWriteAheadLoggingEnabled(enabled: Boolean) { + throw NotImplementedException() + } + + override fun getWritableDatabase(): SupportSQLiteDatabase { + if (database == null) { + database = createRustSupportSQLiteDatabase(false) + } + return database!! + } + + override fun getReadableDatabase(): SupportSQLiteDatabase { + throw NotImplementedException("Not supported by Rust - requires open collection") + } + + override fun close() {} + protected abstract fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase? +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.java deleted file mode 100644 index 659c2cf95..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database; - -import androidx.annotation.NonNull; -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.BackendFactory; - -/** - * Implementation of {@link SupportSQLiteOpenHelper.Factory} using the Anki Desktop backend - */ -public class RustV11SQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { - private final BackendFactory backendFactory; - - public RustV11SQLiteOpenHelperFactory(BackendFactory backendFactory) { - this.backendFactory = backendFactory; - } - - @NonNull - @Override - public SupportSQLiteOpenHelper create(@NonNull SupportSQLiteOpenHelper.Configuration configuration) { - return new RustV11SupportSQLiteOpenHelper(configuration, backendFactory); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.kt new file mode 100644 index 000000000..b5fbb7616 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid.database + +import androidx.sqlite.db.SupportSQLiteOpenHelper +import net.ankiweb.rsdroid.BackendFactory + +/** + * Implementation of [SupportSQLiteOpenHelper.Factory] using the Anki Desktop backend + */ +class RustV11SQLiteOpenHelperFactory(private val backendFactory: BackendFactory) : SupportSQLiteOpenHelper.Factory { + override fun create(configuration: SupportSQLiteOpenHelper.Configuration): SupportSQLiteOpenHelper { + return RustV11SupportSQLiteOpenHelper(configuration, backendFactory) + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.java deleted file mode 100644 index 179fe2618..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.sqlite.db.SupportSQLiteDatabase; -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.BackendUtils; -import net.ankiweb.rsdroid.BackendV1; - -import timber.log.Timber; - -public class RustV11SupportSQLiteOpenHelper extends RustSupportSQLiteOpenHelper { - - public RustV11SupportSQLiteOpenHelper(@NonNull Configuration configuration, BackendFactory backendFactory) { - super(configuration, backendFactory); - } - - public RustV11SupportSQLiteOpenHelper(@NonNull BackendV1 backend) { - super(backend); - } - - @Override - protected SupportSQLiteDatabase createRustSupportSQLiteDatabase(@SuppressWarnings("SameParameterValue") boolean readOnly) { - Timber.d("createRustSupportSQLiteDatabase"); - if (configuration != null) { - BackendV1 backend = backendFactory.getBackend(); - BackendUtils.openAnkiDroidCollection(backend, configuration.name); - return new RustSupportSQLiteDatabase(backend, readOnly); - } else { - return new RustSupportSQLiteDatabase(backend, readOnly); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt new file mode 100644 index 000000000..e6ac1331d --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid.database + +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteOpenHelper +import net.ankiweb.rsdroid.BackendFactory +import net.ankiweb.rsdroid.BackendUtils.openAnkiDroidCollection +import net.ankiweb.rsdroid.BackendV1 +import timber.log.Timber + +class RustV11SupportSQLiteOpenHelper : RustSupportSQLiteOpenHelper { + constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) : super(configuration, backendFactory) {} + constructor(backend: BackendV1) : super(backend) {} + + override fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase? { + Timber.d("createRustSupportSQLiteDatabase") + return if (configuration != null) { + val backend = backendFactory!!.getBackend() + openAnkiDroidCollection(backend, configuration.name) + RustSupportSQLiteDatabase(backend, readOnly) + } else { + RustSupportSQLiteDatabase(backend, readOnly) + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.java deleted file mode 100644 index 8cb335684..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database; - -import androidx.annotation.NonNull; -import androidx.sqlite.db.SupportSQLiteOpenHelper; - -import net.ankiweb.rsdroid.BackendFactory; - -/** - * Implementation of {@link SupportSQLiteOpenHelper.Factory} using the Anki Desktop backend - */ -public class RustVNextSQLiteOpenHelperFactory implements SupportSQLiteOpenHelper.Factory { - private final BackendFactory backendFactory; - - public RustVNextSQLiteOpenHelperFactory(BackendFactory backendFactory) { - this.backendFactory = backendFactory; - } - - @NonNull - @Override - public SupportSQLiteOpenHelper create(@NonNull SupportSQLiteOpenHelper.Configuration configuration) { - return new RustVNextSupportSQLiteOpenHelper(configuration, backendFactory); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.kt new file mode 100644 index 000000000..ec0ce202c --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.kt @@ -0,0 +1,28 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid.database + +import androidx.sqlite.db.SupportSQLiteOpenHelper +import net.ankiweb.rsdroid.BackendFactory + +/** + * Implementation of [SupportSQLiteOpenHelper.Factory] using the Anki Desktop backend + */ +class RustVNextSQLiteOpenHelperFactory(private val backendFactory: BackendFactory) : SupportSQLiteOpenHelper.Factory { + override fun create(configuration: SupportSQLiteOpenHelper.Configuration): SupportSQLiteOpenHelper { + return RustVNextSupportSQLiteOpenHelper(configuration, backendFactory) + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.java deleted file mode 100644 index 08316667f..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.java +++ /dev/null @@ -1,49 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database; - -import androidx.annotation.NonNull; -import androidx.sqlite.db.SupportSQLiteDatabase; - -import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.BackendV1; - -import timber.log.Timber; - -public class RustVNextSupportSQLiteOpenHelper extends RustSupportSQLiteOpenHelper { - - public RustVNextSupportSQLiteOpenHelper(@NonNull Configuration configuration, BackendFactory backendFactory) { - super(configuration, backendFactory); - } - - public RustVNextSupportSQLiteOpenHelper(@NonNull BackendV1 backend) { - super(backend); - } - - @Override - protected SupportSQLiteDatabase createRustSupportSQLiteDatabase(@SuppressWarnings("SameParameterValue") boolean readOnly) { - Timber.d("createRustSupportSQLiteDatabase"); - if (configuration != null) { - BackendV1 backend = backendFactory.getBackend(); - // openCollection opens and upgrades the collection -// backend.openCollection(configuration.name, null, null, null); - return new RustSupportSQLiteDatabase(backend, readOnly); - } else { - return new RustSupportSQLiteDatabase(backend, readOnly); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt new file mode 100644 index 000000000..ac0433fa5 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt @@ -0,0 +1,39 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid.database + +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteOpenHelper +import net.ankiweb.rsdroid.BackendFactory +import net.ankiweb.rsdroid.BackendV1 +import timber.log.Timber + +class RustVNextSupportSQLiteOpenHelper : RustSupportSQLiteOpenHelper { + constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) : super(configuration, backendFactory) {} + constructor(backend: BackendV1) : super(backend) {} + + override fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase? { + Timber.d("createRustSupportSQLiteDatabase") + return if (configuration != null) { + val backend = backendFactory!!.getBackend() + // openCollection opens and upgrades the collection +// backend.openCollection(configuration.name, null, null, null); + RustSupportSQLiteDatabase(backend, readOnly) + } else { + RustSupportSQLiteDatabase(backend, readOnly) + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.java deleted file mode 100644 index d1e1e3b93..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database; - -import androidx.annotation.CheckResult; - -import org.json.JSONArray; - -public interface SQLHandler { - @CheckResult - JSONArray fullQuery(String query, Object... bindArgs); - int executeGetRowsAffected(String sql, Object... bindArgs); - long insertForId(String sql, Object... bindArgs); - - void beginTransaction(); - void commitTransaction(); - void rollbackTransaction(); - - @CheckResult - String[] getColumnNames(String sql); - - void closeDatabase(); - - @CheckResult - String getPath(); - - /* Protobuf-related (#6) */ - anki.ankidroid.DBResponse getNextSlice(long startIndex, int sequenceNumber); - anki.ankidroid.DBResponse fullQueryProto(String query, Object... bindArgs); - - void cancelCurrentProtoQuery(int sequenceNumber); - void cancelAllProtoQueries(); - - /** - * Sets the page size for all future calls to - * {@link SQLHandler#getNextSlice(long, int)} - * and - * {@link SQLHandler#fullQueryProto(String, Object...)} - * - * Default: 2MB - */ - void setPageSize(long pageSizeBytes); -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt new file mode 100644 index 000000000..a027ba8fc --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid.database + +import androidx.annotation.CheckResult +import anki.ankidroid.DBResponse +import org.json.JSONArray + +interface SQLHandler { + @CheckResult + fun fullQuery(query: String, bindArgs: Array?): JSONArray + fun fullQuery(query: String): JSONArray { + return fullQuery(query, null) + } + fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int + fun insertForId(sql: String, bindArgs: Array?): Long + fun beginTransaction() + fun commitTransaction() + fun rollbackTransaction() + + @CheckResult + fun getColumnNames(sql: String): Array + fun closeDatabase() + + @CheckResult + fun getPath(): String + + /* Protobuf-related (#6) */ + fun getNextSlice(startIndex: Long, sequenceNumber: Int): DBResponse + fun fullQueryProto(query: String, bindArgs: Array?): DBResponse + fun cancelCurrentProtoQuery(sequenceNumber: Int) + fun cancelAllProtoQueries() + + /** + * Sets the page size for all future calls to + * [SQLHandler.getNextSlice] + * and + * [SQLHandler.fullQueryProto] + * + * Default: 2MB + */ + fun setPageSize(pageSizeBytes: Long) +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.java deleted file mode 100644 index e5aae9f9f..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.java +++ /dev/null @@ -1,198 +0,0 @@ -/* - * Copyright (c) 2020 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 . - * - * Contains code under the following license - * - * Copyright (C) 2011 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - * From android.database.sqlite.SQLiteSession - */ - -package net.ankiweb.rsdroid.database; - -import org.json.JSONArray; - -import java.util.Stack; - -/** Handles transaction state management */ -public class Session implements SQLHandler { - private final SQLHandler backend; - private final Stack sessions = new Stack<>(); - - public Session(SQLHandler backend) { - this.backend = backend; - } - - private boolean mInTransaction() { - return !sessions.empty(); - } - - public void beginTransaction() { - if (!mInTransaction()) { - backend.beginTransaction(); - } - sessions.add(SessionState.initial()); - } - - @Override - public void commitTransaction() { - backend.commitTransaction(); - } - - @Override - public void rollbackTransaction() { - backend.rollbackTransaction(); - } - - @Override - public String[] getColumnNames(String sql) { - return backend.getColumnNames(sql); - } - - @Override - public void closeDatabase() { - backend.closeDatabase(); - } - - @Override - public String getPath() { - return backend.getPath(); - } - - @Override - public anki.ankidroid.DBResponse getNextSlice(long startIndex, int sequenceNumber) { - return backend.getNextSlice(startIndex, sequenceNumber); - } - - @Override - public anki.ankidroid.DBResponse fullQueryProto(String query, Object... bindArgs) { - return backend.fullQueryProto(query, bindArgs); - } - - @Override - public void cancelCurrentProtoQuery(int sequenceNumber) { - backend.cancelCurrentProtoQuery(sequenceNumber); - } - - @Override - public void cancelAllProtoQueries() { - backend.cancelAllProtoQueries(); - } - - @Override - public void setPageSize(long pageSizeBytes) { - backend.setPageSize(pageSizeBytes); - } - - - public void setTransactionSuccessful() { - if (!inTransaction()) { - throw new IllegalStateException("must be in a transaction"); - } - sessions.peek().markSuccessful(); - } - - public void endTransaction() { - if (!inTransaction()) { - throw new IllegalStateException("must be in a transaction"); - } - - SessionState currentState = pop(); - - if (sessions.size() != 0) { - if (!currentState.isSuccessful()) { - sessions.peek().markAsFailed(); - } - - return; - } - - // We have a single session - rollback or abort - - if (currentState.isSuccessful()) { - commitTransaction(); - } else { - rollbackTransaction(); - } - } - - private SessionState pop() { - return sessions.pop(); - } - - public boolean inTransaction() { - return mInTransaction(); - } - - public JSONArray fullQuery(String query, Object[] bindArgs) { - return backend.fullQuery(query, bindArgs); - } - - @Override - public int executeGetRowsAffected(String sql, Object[] bindArgs) { - return backend.executeGetRowsAffected(sql, bindArgs); - } - - @Override - public long insertForId(String sql, Object[] bindArgs) { - return backend.insertForId(sql, bindArgs); - } - - public static class SessionState { - private boolean mTransactionMarkedSuccessful; - private boolean mIsFailed; - - public SessionState(boolean success, boolean isFailed) { - mTransactionMarkedSuccessful = success; - mIsFailed = isFailed; - } - - public static SessionState initial() { - return new SessionState(false, false); - } - - public boolean isSuccessful() { - return isMarkedSuccessful() && !isFailed(); - } - - public boolean isMarkedSuccessful() { - return mTransactionMarkedSuccessful; - } - - public boolean isFailed() { - return mIsFailed; - } - - public void markAsFailed() { - mIsFailed = true; - } - - public void markSuccessful() { - mTransactionMarkedSuccessful = true; - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.kt new file mode 100644 index 000000000..7640db55d --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.kt @@ -0,0 +1,155 @@ +/* + * Copyright (c) 2020 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 . + * + * Contains code under the following license + * + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * From android.database.sqlite.SQLiteSession + */ +package net.ankiweb.rsdroid.database + +import anki.ankidroid.DBResponse +import org.json.JSONArray +import java.util.* + +/** Handles transaction state management */ +class Session(private val backend: SQLHandler) : SQLHandler { + private val sessions = Stack() + private fun mInTransaction(): Boolean { + return !sessions.empty() + } + + override fun beginTransaction() { + if (!mInTransaction()) { + backend.beginTransaction() + } + sessions.add(SessionState.initial()) + } + + override fun commitTransaction() { + backend.commitTransaction() + } + + override fun rollbackTransaction() { + backend.rollbackTransaction() + } + + override fun getColumnNames(sql: String): Array { + return backend.getColumnNames(sql) + } + + override fun closeDatabase() { + backend.closeDatabase() + } + + override fun getPath(): String { + return backend.getPath() + } + + override fun getNextSlice(startIndex: Long, sequenceNumber: Int): DBResponse { + return backend.getNextSlice(startIndex, sequenceNumber) + } + + override fun fullQueryProto(query: String, bindArgs: Array?): DBResponse { + return backend.fullQueryProto(query, bindArgs) + } + + override fun cancelCurrentProtoQuery(sequenceNumber: Int) { + backend.cancelCurrentProtoQuery(sequenceNumber) + } + + override fun cancelAllProtoQueries() { + backend.cancelAllProtoQueries() + } + + override fun setPageSize(pageSizeBytes: Long) { + backend.setPageSize(pageSizeBytes) + } + + fun setTransactionSuccessful() { + check(inTransaction()) { "must be in a transaction" } + sessions.peek().markSuccessful() + } + + fun endTransaction() { + check(inTransaction()) { "must be in a transaction" } + val currentState = pop() + if (sessions.size != 0) { + if (!currentState.isSuccessful) { + sessions.peek().markAsFailed() + } + return + } + + // We have a single session - rollback or abort + if (currentState.isSuccessful) { + commitTransaction() + } else { + rollbackTransaction() + } + } + + private fun pop(): SessionState { + return sessions.pop() + } + + fun inTransaction(): Boolean { + return mInTransaction() + } + + override fun fullQuery(query: String, bindArgs: Array?): JSONArray { + return backend.fullQuery(query, bindArgs) + } + + override fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int { + return backend.executeGetRowsAffected(sql, bindArgs) + } + + override fun insertForId(sql: String, bindArgs: Array?): Long { + return backend.insertForId(sql, bindArgs) + } + + class SessionState(var isMarkedSuccessful: Boolean, var isFailed: Boolean) { + val isSuccessful: Boolean + get() = isMarkedSuccessful && !isFailed + + fun markAsFailed() { + isFailed = true + } + + fun markSuccessful() { + isMarkedSuccessful = true + } + + companion object { + fun initial(): SessionState { + return SessionState(false, false) + } + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.kt similarity index 79% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.kt index 198a4ebc1..7c0bc81a6 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SessionThreadLocal.kt @@ -31,21 +31,10 @@ * * Source: android.database.sqlite.SQLiteDatabase */ +package net.ankiweb.rsdroid.database -package net.ankiweb.rsdroid.database; - -import androidx.annotation.Nullable; - -public class SessionThreadLocal extends ThreadLocal { - private final SQLHandler mBackend; - - public SessionThreadLocal(SQLHandler backend) { - this.mBackend = backend; - } - - @Nullable - @Override - protected Session initialValue() { - return new Session(mBackend); +class SessionThreadLocal(private val mBackend: SQLHandler) : ThreadLocal() { + override fun initialValue(): Session { + return Session(mBackend) } -} +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.java deleted file mode 100644 index 065dd4614..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.java +++ /dev/null @@ -1,287 +0,0 @@ -/* - * 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 net.ankiweb.rsdroid.database; - - -import android.database.CursorIndexOutOfBoundsException; -import android.database.sqlite.SQLiteException; - -import net.ankiweb.rsdroid.BackendException; -import net.ankiweb.rsdroid.utils.StringToDouble; -import net.ankiweb.rsdroid.utils.StringToLong; - -import java.util.Locale; - -public class StreamingProtobufSQLiteCursor extends AnkiDatabaseCursor { - /** - * Rust Implementation: - * - * When we request a query, rust calculates 2MB (default) of results and sends it to us - * - * We keep track of where we are with getSliceStartIndex: the index into the rust collection - * - * The next request should be for index: getSliceStartIndex() + getCurrentSliceRowCount() - */ - - private final SQLHandler backend; - private final String query; - private anki.ankidroid.DBResponse results; - /** The local position in the current slice */ - private int positionInSlice = -1; - private String[] columnMapping; - private boolean isClosed = false; - private final int sequenceNumber; - /** The total number of rows for the query */ - private final int rowCount; - - /**The current index into the collection or rows */ - private int getSliceStartIndex() { - return (int) results.getStartIndex(); - } - - public StreamingProtobufSQLiteCursor(SQLHandler backend, String query, Object[] bindArgs) { - this.backend = backend; - this.query = query; - - try { - results = this.backend.fullQueryProto(this.query, bindArgs); - sequenceNumber = results.getSequenceNumber(); - rowCount = results.getRowCount(); - } catch (BackendException e) { - throw e.toSQLiteException(this.query); - } - } - - private void loadPage(long startingAtIndex) { - try { - long requestedIndex = startingAtIndex == -1 ? 0 : startingAtIndex; - results = backend.getNextSlice(requestedIndex, sequenceNumber); - positionInSlice = startingAtIndex == -1 ? -1 : 0; - if (results.getSequenceNumber() != sequenceNumber) { - throw new IllegalStateException("rsdroid does not currently handle nested cursor-based queries. Please change the code to avoid holding a reference to the query, or implement the functionality in rsdroid"); - } - } catch (BackendException e) { - throw e.toSQLiteException(query); - } - } - - @Override - public int getCount() { - return rowCount; - } - - @Override - public int getPosition() { - return getSliceStartIndex() + positionInSlice; - } - - @Override - public boolean moveToPosition(int nextPositionGlobal) { - int nextPositionLocal = nextPositionGlobal - getSliceStartIndex(); - boolean isInCurrentSlice = nextPositionLocal >= 0 && nextPositionLocal < getCurrentSliceRowCount(); - if (!isInCurrentSlice && getCurrentSliceRowCount() > 0 && getCount() != getCurrentSliceRowCount()) { - // loadPage this resets the position to 0 - loadPage(nextPositionGlobal); - } else { - positionInSlice = nextPositionLocal; - } - // moving to -1 should return false and mutate the position - return positionInSlice >= 0 && getCurrentSliceRowCount() > 0 && positionInSlice < getCurrentSliceRowCount(); - } - - @Override - public int getColumnIndex(String columnName) { - try { - String[] names = getColumnNames(); - for (int i = 0; i < names.length; i++) { - if (columnName.equals(names[i])) { - return i; - } - } - } catch (Exception e) { - return -1; - } - return -1; - } - - @Override - public int getColumnIndexOrThrow(String columnName) throws IllegalArgumentException { - try { - String[] names = getColumnNames(); - for (int i = 0; i < names.length; i++) { - if (columnName.equals(names[i])) { - return i; - } - } - } catch (Exception e) { - throw new IllegalArgumentException(e); - } - throw new IllegalArgumentException(String.format("Could not find column '%s'", columnName)); - } - - @Override - public String getColumnName(int columnIndex) { - return getColumnNamesInternal()[columnIndex]; - } - - @Override - public String[] getColumnNames() { - return getColumnNamesInternal(); - } - - private String[] getColumnNamesInternal() { - if (columnMapping == null) { - columnMapping = backend.getColumnNames(query); - if (columnMapping == null) { - throw new IllegalStateException("unable to obtain column mapping"); - } - } - - return columnMapping; - } - - @Override - public int getColumnCount() { - if (getCurrentSliceRowCount() == 0) { - return 0; - } else { - return results.getResult().getRows(0).getFieldsCount(); - } - } - - @Override - public String getString(int columnIndex) { - anki.ankidroid.SqlValue field = getFieldAtIndex(columnIndex); - switch (field.getDataCase()) { - case BLOBVALUE: throw new SQLiteException("unknown error (code 0): Unable to convert BLOB to string"); - case LONGVALUE: return Long.toString(field.getLongValue()); - case DOUBLEVALUE: return Double.toString(field.getDoubleValue()); - case STRINGVALUE: return field.getStringValue(); - case DATA_NOT_SET: return null; - default: throw new IllegalStateException("Unknown data case: " + field.getDataCase()); - } - } - - @Override - public long getLong(int columnIndex) { - anki.ankidroid.SqlValue field = getFieldAtIndex(columnIndex); - switch (field.getDataCase()) { - case BLOBVALUE: throw new SQLiteException("unknown error (code 0): Unable to convert BLOB to long"); - case LONGVALUE: return field.getLongValue(); - case DOUBLEVALUE: return (long) field.getDoubleValue(); - case STRINGVALUE: return strtoll(field.getStringValue()); - case DATA_NOT_SET: return 0; - default: throw new IllegalStateException("Unknown data case: " + field.getDataCase()); - } - } - - @Override - public double getDouble(int columnIndex) { - anki.ankidroid.SqlValue field = getFieldAtIndex(columnIndex); - switch (field.getDataCase()) { - case BLOBVALUE: throw new SQLiteException("unknown error (code 0): Unable to convert BLOB to double"); - case LONGVALUE: return field.getLongValue(); - case DOUBLEVALUE: return field.getDoubleValue(); - case STRINGVALUE: return strtod(field.getStringValue()); - case DATA_NOT_SET: return 0d; - default: throw new IllegalStateException("Unknown data case: " + field.getDataCase()); - } - } - - @Override - public short getShort(int columnIndex) { - return (short) getLong(columnIndex); - } - - @Override - public int getInt(int columnIndex) { - return (int) getLong(columnIndex); - } - - @Override - public float getFloat(int columnIndex) { - return (float) getDouble(columnIndex); - } - - private boolean isNull(anki.ankidroid.SqlValue field) { - return field.getDataCase() == anki.ankidroid.SqlValue.DataCase.DATA_NOT_SET; - } - - @Override - public boolean isNull(int columnIndex) { - anki.ankidroid.SqlValue field = getFieldAtIndex(columnIndex); - return isNull(field); - } - - @Override - public void close() { - isClosed = true; - backend.cancelCurrentProtoQuery(sequenceNumber); - } - - @Override - public boolean isClosed() { - return isClosed; - } - - @Override - public int getType(int columnIndex) { - anki.ankidroid.SqlValue field = getFieldAtIndex(columnIndex); - switch (field.getDataCase()) { - case BLOBVALUE: return FIELD_TYPE_BLOB; - case LONGVALUE: return FIELD_TYPE_INTEGER; - case DOUBLEVALUE: return FIELD_TYPE_FLOAT; - case STRINGVALUE: return FIELD_TYPE_STRING; - case DATA_NOT_SET: return FIELD_TYPE_NULL; - default: throw new IllegalStateException("Unknown data case: " + field.getDataCase()); - } - } - - protected anki.ankidroid.Row getRowAtCurrentPosition() { - anki.ankidroid.DbResult result = results.getResult(); - int rowCount = getCurrentSliceRowCount(); - if (positionInSlice < 0 || positionInSlice >= rowCount) { - throw new CursorIndexOutOfBoundsException(String.format(Locale.ROOT, "Index %d requested, with a size of %d", positionInSlice, rowCount)); - } - return result.getRows(positionInSlice); - } - - private anki.ankidroid.SqlValue getFieldAtIndex(int columnIndex) { - return getRowAtCurrentPosition().getFields(columnIndex); - } - - protected int getCurrentSliceRowCount() { - return results.getResult().getRowsCount(); - } - - private long strtoll(String stringValue) { - try { - return StringToLong.strtol(stringValue); - } catch (NumberFormatException exception) { - return 0; - } - } - - private double strtod(String stringValue) { - try { - return StringToDouble.strtod(stringValue); - } catch (NumberFormatException exception) { - return 0.0; - } - } -} - diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.kt new file mode 100644 index 000000000..2a6592fef --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.kt @@ -0,0 +1,262 @@ +/* + * 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 net.ankiweb.rsdroid.database + +import android.database.Cursor +import android.database.CursorIndexOutOfBoundsException +import android.database.sqlite.SQLiteException +import anki.ankidroid.DBResponse +import anki.ankidroid.Row +import anki.ankidroid.SqlValue +import anki.ankidroid.SqlValue.DataCase +import net.ankiweb.rsdroid.BackendException +import net.ankiweb.rsdroid.utils.StringToDouble +import net.ankiweb.rsdroid.utils.StringToLong +import java.util.* + +open class StreamingProtobufSQLiteCursor( + /** + * Rust Implementation: + * + * When we request a query, rust calculates 2MB (default) of results and sends it to us + * + * We keep track of where we are with getSliceStartIndex: the index into the rust collection + * + * The next request should be for index: getSliceStartIndex() + getCurrentSliceRowCount() + */ + private val backend: SQLHandler, private val query: String, bindArgs: Array?) : AnkiDatabaseCursor() { + private var results: DBResponse? = null + + /** The local position in the current slice */ + private var positionInSlice = -1 + private var columnMapping: Array? = null + private var isClosed = false + private var sequenceNumber = 0 + + /** The total number of rows for the query */ + private var rowCount = 0 + + /**The current index into the collection or rows */ + private val sliceStartIndex: Int + private get() = results!!.startIndex.toInt() + + private fun loadPage(startingAtIndex: Long) { + try { + val requestedIndex = if (startingAtIndex == -1L) 0 else startingAtIndex + results = backend.getNextSlice(requestedIndex, sequenceNumber) + positionInSlice = if (startingAtIndex == -1L) -1 else 0 + check(results!!.sequenceNumber == sequenceNumber) { "rsdroid does not currently handle nested cursor-based queries. Please change the code to avoid holding a reference to the query, or implement the functionality in rsdroid" } + } catch (e: BackendException) { + throw e.toSQLiteException(query)!! + } + } + + override fun getCount(): Int { + return rowCount + } + + override fun getPosition(): Int { + return sliceStartIndex + positionInSlice + } + + override fun moveToPosition(nextPositionGlobal: Int): Boolean { + val nextPositionLocal = nextPositionGlobal - sliceStartIndex + val isInCurrentSlice = nextPositionLocal >= 0 && nextPositionLocal < currentSliceRowCount + if (!isInCurrentSlice && currentSliceRowCount > 0 && count != currentSliceRowCount) { + // loadPage this resets the position to 0 + loadPage(nextPositionGlobal.toLong()) + } else { + positionInSlice = nextPositionLocal + } + // moving to -1 should return false and mutate the position + return positionInSlice >= 0 && currentSliceRowCount > 0 && positionInSlice < currentSliceRowCount + } + + override fun getColumnIndex(columnName: String): Int { + try { + val names = columnNames + for (i in names.indices) { + if (columnName == names[i]) { + return i + } + } + } catch (e: Exception) { + return -1 + } + return -1 + } + + @Throws(IllegalArgumentException::class) + override fun getColumnIndexOrThrow(columnName: String): Int { + try { + val names = columnNames + for (i in names.indices) { + if (columnName == names[i]) { + return i + } + } + } catch (e: Exception) { + throw IllegalArgumentException(e) + } + throw IllegalArgumentException(String.format("Could not find column '%s'", columnName)) + } + + override fun getColumnName(columnIndex: Int): String { + return columnNamesInternal[columnIndex] + } + + override fun getColumnNames(): Array { + return columnNamesInternal + } + + private val columnNamesInternal: Array + private get() { + if (columnMapping == null) { + columnMapping = backend.getColumnNames(query) + checkNotNull(columnMapping) { "unable to obtain column mapping" } + } + return columnMapping!! + } + + override fun getColumnCount(): Int { + return if (currentSliceRowCount == 0) { + 0 + } else { + results!!.result.getRows(0).fieldsCount + } + } + + override fun getString(columnIndex: Int): String? { + val field = getFieldAtIndex(columnIndex) + return when (field.dataCase) { + DataCase.BLOBVALUE -> throw SQLiteException("unknown error (code 0): Unable to convert BLOB to string") + DataCase.LONGVALUE -> java.lang.Long.toString(field.longValue) + DataCase.DOUBLEVALUE -> java.lang.Double.toString(field.doubleValue) + DataCase.STRINGVALUE -> field.stringValue + DataCase.DATA_NOT_SET -> null + else -> throw IllegalStateException("Unknown data case: " + field.dataCase) + } + } + + override fun getLong(columnIndex: Int): Long { + val field = getFieldAtIndex(columnIndex) + return when (field.dataCase) { + DataCase.BLOBVALUE -> throw SQLiteException("unknown error (code 0): Unable to convert BLOB to long") + DataCase.LONGVALUE -> field.longValue + DataCase.DOUBLEVALUE -> field.doubleValue.toLong() + DataCase.STRINGVALUE -> strtoll(field.stringValue) + DataCase.DATA_NOT_SET -> 0 + else -> throw IllegalStateException("Unknown data case: " + field.dataCase) + } + } + + override fun getDouble(columnIndex: Int): Double { + val field = getFieldAtIndex(columnIndex) + return when (field.dataCase) { + DataCase.BLOBVALUE -> throw SQLiteException("unknown error (code 0): Unable to convert BLOB to double") + DataCase.LONGVALUE -> field.longValue.toDouble() + DataCase.DOUBLEVALUE -> field.doubleValue + DataCase.STRINGVALUE -> strtod(field.stringValue) + DataCase.DATA_NOT_SET -> 0.0 + else -> throw IllegalStateException("Unknown data case: " + field.dataCase) + } + } + + override fun getShort(columnIndex: Int): Short { + return getLong(columnIndex).toShort() + } + + override fun getInt(columnIndex: Int): Int { + return getLong(columnIndex).toInt() + } + + override fun getFloat(columnIndex: Int): Float { + return getDouble(columnIndex).toFloat() + } + + private fun isNull(field: SqlValue): Boolean { + return field.dataCase == DataCase.DATA_NOT_SET + } + + override fun isNull(columnIndex: Int): Boolean { + val field = getFieldAtIndex(columnIndex) + return isNull(field) + } + + override fun close() { + isClosed = true + backend.cancelCurrentProtoQuery(sequenceNumber) + } + + override fun isClosed(): Boolean { + return isClosed + } + + override fun getType(columnIndex: Int): Int { + val field = getFieldAtIndex(columnIndex) + return when (field.dataCase) { + DataCase.BLOBVALUE -> Cursor.FIELD_TYPE_BLOB + DataCase.LONGVALUE -> Cursor.FIELD_TYPE_INTEGER + DataCase.DOUBLEVALUE -> Cursor.FIELD_TYPE_FLOAT + DataCase.STRINGVALUE -> Cursor.FIELD_TYPE_STRING + DataCase.DATA_NOT_SET -> Cursor.FIELD_TYPE_NULL + else -> throw IllegalStateException("Unknown data case: " + field.dataCase) + } + } + + protected val rowAtCurrentPosition: Row + protected get() { + val result = results!!.result + val rowCount = currentSliceRowCount + if (positionInSlice < 0 || positionInSlice >= rowCount) { + throw CursorIndexOutOfBoundsException(String.format(Locale.ROOT, "Index %d requested, with a size of %d", positionInSlice, rowCount)) + } + return result.getRows(positionInSlice) + } + + private fun getFieldAtIndex(columnIndex: Int): SqlValue { + return rowAtCurrentPosition.getFields(columnIndex) + } + + protected val currentSliceRowCount: Int + protected get() = results!!.result.rowsCount + + private fun strtoll(stringValue: String): Long { + return try { + StringToLong.strtol(stringValue) + } catch (exception: NumberFormatException) { + 0 + } + } + + private fun strtod(stringValue: String): Double { + return try { + StringToDouble.strtod(stringValue) + } catch (exception: NumberFormatException) { + 0.0 + } + } + + init { + try { + results = backend.fullQueryProto(query, bindArgs) + sequenceNumber = results!!.sequenceNumber + rowCount = results!!.rowCount + } catch (e: BackendException) { + throw e.toSQLiteException(query)!! + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.kt similarity index 75% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.kt index b79925781..2c94620c9 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.kt @@ -13,13 +13,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException -import net.ankiweb.rsdroid.BackendException; - -public class BackendIoException extends BackendException { - public BackendIoException(anki.backend.BackendError error) { - super(error); - } -} +class BackendDeckIsFilteredException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.kt similarity index 74% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.kt index 59ac7ddc0..a3327430f 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendExistingException.kt @@ -13,16 +13,12 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException /** * TODO: Document this */ -public class BackendExistingException extends BackendException { - public BackendExistingException(anki.backend.BackendError error) { - super(error); - } -} +class BackendExistingException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.kt similarity index 74% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.kt index 88f9b3c90..02f882cbd 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.kt @@ -13,14 +13,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -public class BackendProtoException extends BackendException { - public BackendProtoException(anki.backend.BackendError error) { - super(error); - } -} +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException +class BackendInterruptedException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.java deleted file mode 100644 index 61844affb..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * 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 net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -/** - * A lot of exceptions get converted to Invalid Input when returned: - * - * CollectionNotOpen - * CollectionAlreadyOpen - * SearchError - */ -public class BackendInvalidInputException extends BackendException { - public BackendInvalidInputException(anki.backend.BackendError error) { - super(error); - } - - public static BackendInvalidInputException fromInvalidInputError(anki.backend.BackendError error) { - switch (error.getLocalized()) { - case "CollectionAlreadyOpen": return new BackendCollectionAlreadyOpenException(error); - case "CollectionNotOpen": return new BackendCollectionNotOpenException(error); - // TODO: We can't handle this case as there's no available properties. - // case "SearchError": return new BackendSearchException(error); - } - return new BackendInvalidInputException(error); - } - - public static class BackendCollectionAlreadyOpenException extends BackendInvalidInputException { - public BackendCollectionAlreadyOpenException(anki.backend.BackendError error) { - super(error); - } - } - - public static class BackendCollectionNotOpenException extends BackendInvalidInputException { - public BackendCollectionNotOpenException(anki.backend.BackendError error) { - super(error); - } - } - - public static class BackendSearchException extends BackendInvalidInputException { - public BackendSearchException(anki.backend.BackendError error) { - super(error); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.kt new file mode 100644 index 000000000..b0e17da76 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.kt @@ -0,0 +1,41 @@ +/* + * 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 net.ankiweb.rsdroid.exceptions + +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException + +/** + * A lot of exceptions get converted to Invalid Input when returned: + * + * CollectionNotOpen + * CollectionAlreadyOpen + * SearchError + */ +open class BackendInvalidInputException(error: BackendError?) : BackendException(error!!) { + class BackendCollectionAlreadyOpenException(error: BackendError?) : BackendInvalidInputException(error) + class BackendCollectionNotOpenException(error: BackendError?) : BackendInvalidInputException(error) + class BackendSearchException(error: BackendError?) : BackendInvalidInputException(error) + companion object { + fun fromInvalidInputError(error: BackendError): BackendInvalidInputException { + when (error.localized) { + "CollectionAlreadyOpen" -> return BackendCollectionAlreadyOpenException(error) + "CollectionNotOpen" -> return BackendCollectionNotOpenException(error) + } + return BackendInvalidInputException(error) + } + } +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.kt similarity index 73% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.kt index a2c060ceb..5f6016580 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInterruptedException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendIoException.kt @@ -13,13 +13,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException -import net.ankiweb.rsdroid.BackendException; - -public class BackendInterruptedException extends BackendException { - public BackendInterruptedException(anki.backend.BackendError error) { - super(error); - } -} +class BackendIoException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.java deleted file mode 100644 index 8c8fafa24..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.java +++ /dev/null @@ -1,29 +0,0 @@ -/* - * 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 net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -public class BackendJsonException extends BackendException { - public BackendJsonException(anki.backend.BackendError error) { - super(error); - } - - public BackendJsonException(String message) { - super(message); - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.kt new file mode 100644 index 000000000..4b67fbade --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.kt @@ -0,0 +1,24 @@ +/* + * 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 net.ankiweb.rsdroid.exceptions + +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException + +class BackendJsonException : BackendException { + constructor(error: BackendError?) : super(error!!) {} + constructor(message: String?) : super(message) {} +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.java deleted file mode 100644 index 056da35cd..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.java +++ /dev/null @@ -1,64 +0,0 @@ -/* - * 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 net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; -import net.ankiweb.rsdroid.database.NotImplementedException; - -public class BackendNetworkException extends BackendException { - public BackendNetworkException(anki.backend.BackendError error) { - super(error); - } - - public static BackendNetworkException fromNetworkError(anki.backend.BackendError error) { - throw new NotImplementedException(); - -// if (!error.hasNetworkError()) { -// return new BackendNetworkException(error); -// } -// -// anki.backend.NetworkError networkError = error.getNetworkError(); -// -// switch (networkError.getKind()) { -// case OFFLINE: return new BackendNetworkOfflineException(error); -// case TIMEOUT: return new BackendNetworkTimeoutException(error); -// case PROXY_AUTH: return new BackendNetworkProxyAuthException(error); -// case UNRECOGNIZED: -// case OTHER: -// } -// -// return new BackendNetworkException(error); - } - - public static class BackendNetworkOfflineException extends BackendNetworkException { - public BackendNetworkOfflineException(anki.backend.BackendError error) { - super(error); - } - } - - public static class BackendNetworkTimeoutException extends BackendNetworkException { - public BackendNetworkTimeoutException(anki.backend.BackendError error) { - super(error); - } - } - - public static class BackendNetworkProxyAuthException extends BackendNetworkException { - public BackendNetworkProxyAuthException(anki.backend.BackendError error) { - super(error); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.kt similarity index 73% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.kt index e7fb6959f..c28677992 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendDeckIsFilteredException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNetworkException.kt @@ -13,14 +13,9 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -public class BackendDeckIsFilteredException extends BackendException { - public BackendDeckIsFilteredException(anki.backend.BackendError error) { - super(error); - } -} +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException +class BackendNetworkException(error: BackendError) : BackendException(error) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.kt similarity index 74% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.kt index 5c746708c..3523fb8fa 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendNotFoundException.kt @@ -13,14 +13,10 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException -import net.ankiweb.rsdroid.BackendException; - -/** An item was not found (example: a deck from backend.get_deck_legacy) */ -public class BackendNotFoundException extends BackendException { - public BackendNotFoundException(anki.backend.BackendError error) { - super(error); - } -} +/** An item was not found (example: a deck from backend.get_deck_legacy) */ +class BackendNotFoundException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.kt new file mode 100644 index 000000000..90445a5ca --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendProtoException.kt @@ -0,0 +1,21 @@ +/* + * 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 net.ankiweb.rsdroid.exceptions + +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException + +class BackendProtoException(error: BackendError?) : BackendException(error!!) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.kt new file mode 100644 index 000000000..43df43cce --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.kt @@ -0,0 +1,23 @@ +/* + * 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 net.ankiweb.rsdroid.exceptions + +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException + +open class BackendSyncException(error: BackendError?) : BackendException(error!!) { + class BackendSyncAuthFailedException(error: BackendError?) : BackendSyncException(error) +} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.java deleted file mode 100644 index 7f6563423..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.java +++ /dev/null @@ -1,44 +0,0 @@ -/* - * 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 net.ankiweb.rsdroid.exceptions; - -import net.ankiweb.rsdroid.BackendException; - -public class BackendTemplateException extends BackendException { - public BackendTemplateException(anki.backend.BackendError error) { - super(error); - } - - public static BackendTemplateException fromTemplateError(anki.backend.BackendError error) { - - if (error.getLocalized() == null) { - return new BackendTemplateException(error); - } - - if (error.getLocalized().contains("has a problem")) { - return new BackendTemplateSaveException(error); - } - - return new BackendTemplateException(error); - } - - public static class BackendTemplateSaveException extends BackendTemplateException { - public BackendTemplateSaveException(anki.backend.BackendError error) { - super(error); - } - } -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.java b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.kt similarity index 51% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.java rename to rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.kt index 4ca1b2d9d..41240f733 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendSyncException.java +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendTemplateException.kt @@ -13,22 +13,21 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ +package net.ankiweb.rsdroid.exceptions -package net.ankiweb.rsdroid.exceptions; +import anki.backend.BackendError +import net.ankiweb.rsdroid.BackendException -import net.ankiweb.rsdroid.BackendException; - -public class BackendSyncException extends BackendException { - - public BackendSyncException(anki.backend.BackendError error) { - super(error); - } - - public static class BackendSyncAuthFailedException extends BackendSyncException { - public BackendSyncAuthFailedException(anki.backend.BackendError error) { - super(error); +open class BackendTemplateException(error: BackendError?) : BackendException(error!!) { + class BackendTemplateSaveException(error: BackendError?) : BackendTemplateException(error) + companion object { + fun fromTemplateError(error: BackendError): BackendTemplateException { + if (error.localized == null) { + return BackendTemplateException(error) + } + return if (error.localized.contains("has a problem")) { + BackendTemplateSaveException(error) + } else BackendTemplateException(error) } } - - -} +} \ No newline at end of file From be9cbf63c30dd439901d37c5aa71c782b8b9f1a7 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 15 Jun 2022 21:56:02 +1000 Subject: [PATCH 03/61] BackendV1 -> Backend --- .../ankiweb/rsdroid/BackendDisposalTests.java | 2 +- .../ankiweb/rsdroid/BackendForTesting.java | 2 +- .../rsdroid/BackendIntegrationTests.java | 14 +++---- .../net/ankiweb/rsdroid/BackendMutexTest.java | 2 +- .../net/ankiweb/rsdroid/BackendSlowTests.java | 2 +- .../rsdroid/RustDatabaseIntegrationTests.java | 2 +- .../rsdroid/ankiutil/InstrumentedTest.java | 16 ++++---- .../rsdroid/database/DowngradeTest.java | 10 ++--- .../StreamingProtobufSQLiteCursorTest.java | 16 ++++---- .../rsdroid/{BackendV1Impl.kt => Backend.kt} | 38 +++++-------------- .../net/ankiweb/rsdroid/BackendFactory.kt | 7 ++-- .../java/net/ankiweb/rsdroid/BackendUtils.kt | 4 +- .../java/net/ankiweb/rsdroid/BackendV1.kt | 28 -------------- .../net/ankiweb/rsdroid/BackendV11Factory.kt | 1 - .../database/RustSupportSQLiteDatabase.kt | 8 ++-- .../database/RustSupportSQLiteOpenHelper.kt | 6 +-- .../RustV11SupportSQLiteOpenHelper.kt | 7 ++-- .../RustVNextSupportSQLiteOpenHelper.kt | 8 ++-- .../ankiweb/rsdroid/database/SQLHandler.kt | 2 +- .../net/ankiweb/rsdroid/database/Session.kt | 2 +- 20 files changed, 64 insertions(+), 113 deletions(-) rename rsdroid/src/main/java/net/ankiweb/rsdroid/{BackendV1Impl.kt => Backend.kt} (85%) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java index 49f457a99..036f489fc 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java @@ -41,7 +41,7 @@ public void testDisposalDoesNotLeak() throws IOException { for (int i = 0; i < 10000; i++) { Timber.d("Iteration %d", i); - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = new RustV11SupportSQLiteOpenHelper(backend).getWritableDatabase(); int count = DatabaseUtil.queryScalar(db, "select count(*) from revlog"); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java index 3d6fabd3b..ad9439cf0 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java @@ -18,7 +18,7 @@ import androidx.annotation.VisibleForTesting; -public class BackendForTesting extends BackendV1Impl { +public class BackendForTesting extends Backend { BackendForTesting() { super(); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java index 0df3c542e..562570209 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendIntegrationTests.java @@ -52,9 +52,9 @@ public void test() { @Test public void testBackendException() { - BackendV1 backendV1 = getClosedBackend(); + Backend Backend = getClosedBackend(); try { - backendV1.closeCollection(true); + Backend.closeCollection(true); Assert.fail("call should have failed - needs an open collection"); } catch (BackendException ex) { // OK @@ -63,8 +63,8 @@ public void testBackendException() { @Test public void schedTimingTodayCall() { - BackendV1 backendV1 = getBackend("initial_version_2_12_1.anki2"); - SchedTimingTodayResponse ret = backendV1.schedTimingTodayLegacy(1655258084, 0, 1655258084, 0, 0); + Backend backend = getBackend("initial_version_2_12_1.anki2"); + SchedTimingTodayResponse ret = backend.schedTimingTodayLegacy(1655258084, 0, 1655258084, 0, 0); int elapsed = ret.getDaysElapsed(); long nextDayAt = ret.getNextDayAt(); } @@ -73,7 +73,7 @@ public void schedTimingTodayCall() { public void collectionIsVersion11AfterOpen() throws JSONException { // This test will be decomissioned, but before we get an upgrade strategy, we need to ensure we're not upgrading the database. - BackendV1 backendV1 = getBackend("initial_version_2_12_1.anki2"); + Backend backendV1 = getBackend("initial_version_2_12_1.anki2"); JSONArray array = backendV1.fullQuery("select ver from col"); @@ -86,13 +86,13 @@ public void collectionIsVersion11AfterOpen() throws JSONException { @Test public void fullQueryTest() { - BackendV1 backendV1 = getBackend("initial_version_2_12_1.anki2"); + Backend backendV1 = getBackend("initial_version_2_12_1.anki2"); JSONArray result = backendV1.fullQuery("select * from col"); } @Test public void columnNamesTest() { - BackendV1 backendV1 = getBackend("initial_version_2_12_1.anki2"); + Backend backendV1 = getBackend("initial_version_2_12_1.anki2"); String[] names = backendV1.getColumnNames("select * from col"); assertThat(names, is(new String[] { "id", "crt", "mod", "scm", "ver", "dty", "usn", "ls", "conf", "models", "decks", "dconf", "tags" })); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java index d68649ac3..d79479fbe 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendMutexTest.java @@ -36,7 +36,7 @@ public class BackendMutexTest extends InstrumentedTest { @Test public void ensureDatabaseInTransactionIsLocked() throws JSONException, InterruptedException { - BackendV1 b = (BackendV1) super.getBackend("initial_version_2_12_1.anki2"); + Backend b = (Backend) super.getBackend("initial_version_2_12_1.anki2"); b.fullQuery("create table test (id int)"); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java index 275d8498f..d29805a55 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java @@ -141,7 +141,7 @@ public void ensureSQLIsStreamed() throws IOException { at android.app.Instrumentation$InstrumentationThread.run(Instrumentation.java:1837) */ - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = new RustV11SupportSQLiteOpenHelper(backend).getWritableDatabase(); db.query("create table tmp (id varchar)"); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java index 27ba269eb..6a8f575f3 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java @@ -94,7 +94,7 @@ public void testUpdate() { @CheckResult private RustSupportSQLiteDatabase getDatabase() { try { - BackendV1 backendV1 = getBackend(fileName); + Backend backendV1 = getBackend(fileName); boolean readOnly = false; return new RustSupportSQLiteDatabase(backendV1, readOnly); } catch (Exception e) { diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java index 2e07bf6fe..ace5d53bd 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java @@ -22,11 +22,9 @@ import androidx.test.platform.app.InstrumentationRegistry; -import net.ankiweb.rsdroid.BackendException; import net.ankiweb.rsdroid.BackendFactory; import net.ankiweb.rsdroid.BackendUtils; -import net.ankiweb.rsdroid.BackendV1; -import net.ankiweb.rsdroid.BackendV1Impl; +import net.ankiweb.rsdroid.Backend; import net.ankiweb.rsdroid.NativeMethods; import net.ankiweb.rsdroid.RustBackendFailedException; import net.ankiweb.rsdroid.exceptions.BackendInvalidInputException; @@ -48,7 +46,7 @@ public class InstrumentedTest { Log.e("InstrumentedTest", "Timber has been disabled."); } - private final List backendList = new ArrayList<>(); + private final List backendList = new ArrayList<>(); protected final static int TEST_PAGE_SIZE = 1000; @@ -69,7 +67,7 @@ public void before() { @After public void after() { - for (BackendV1 b : backendList) { + for (Backend b : backendList) { if (b != null && b.isOpen()) { List numbers; @@ -122,21 +120,21 @@ protected Context getContext() { } @NotNull - protected BackendV1 getBackend(String fileName) { + protected Backend getBackend(String fileName) { String path = getAssetFilePath(fileName); return getBackendFromPath(path); } @NotNull - protected BackendV1 getBackendFromPath(String path) { - BackendV1 backendV1 = getClosedBackend(); + protected Backend getBackendFromPath(String path) { + Backend backendV1 = getClosedBackend(); backendV1.setPageSize(TEST_PAGE_SIZE); BackendUtils.openAnkiDroidCollection(backendV1, path, true); this.backendList.add(backendV1); return backendV1; } - protected BackendV1 getClosedBackend() { + protected Backend getClosedBackend() { try { return BackendFactory.createInstance().getBackend(); } catch (RustBackendFailedException e) { diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java index e6cfc397d..4275e96bf 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/DowngradeTest.java @@ -17,7 +17,7 @@ package net.ankiweb.rsdroid.database; import net.ankiweb.rsdroid.BackendException; -import net.ankiweb.rsdroid.BackendV1; +import net.ankiweb.rsdroid.Backend; import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; import org.json.JSONArray; @@ -39,17 +39,17 @@ public class DowngradeTest extends InstrumentedTest { public void downgradeWithLaterSchema() throws IOException, JSONException { String fileName = "schema_16.anki2"; String path = getAssetFilePath(fileName); - try (BackendV1 backendV1 = super.getBackendFromPath(path) ){ + try (Backend backendV1 = super.getBackendFromPath(path) ){ assertSchemaVer(backendV1, 11); } fileName = "schema_17.anki2"; path = getAssetFilePath(fileName); - try (BackendV1 backendV1 = super.getBackendFromPath(path) ){ + try (Backend backendV1 = super.getBackendFromPath(path) ){ assertSchemaVer(backendV1, 11); } } - private void assertSchemaVer(BackendV1 backendV1, @SuppressWarnings("SameParameterValue") int expectedVersion) throws JSONException { + private void assertSchemaVer(Backend backendV1, @SuppressWarnings("SameParameterValue") int expectedVersion) throws JSONException { JSONArray array = backendV1.fullQuery("select ver from col"); assertThat(array.length(), is(1)); @@ -60,7 +60,7 @@ private void assertSchemaVer(BackendV1 backendV1, @SuppressWarnings("SameParamet @SuppressWarnings({"unused", "RedundantSuppression"}) private void assertOpeningFails(String path) { - try (BackendV1 unused = super.getBackendFromPath(path)) { + try (Backend unused = super.getBackendFromPath(path)) { fail(); } catch (Exception e) { // ignore diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java index 19cf99927..00e81a6ae 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java @@ -20,7 +20,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase; -import net.ankiweb.rsdroid.BackendV1; +import net.ankiweb.rsdroid.Backend; import net.ankiweb.rsdroid.DatabaseIntegrationTests; import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; @@ -40,7 +40,7 @@ public class StreamingProtobufSQLiteCursorTest extends InstrumentedTest { @Test public void testPaging() throws IOException { - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id int)"); @@ -61,14 +61,14 @@ public void testPaging() throws IOException { } } - private SupportSQLiteDatabase getWritableDatabase(BackendV1 backend) { + private SupportSQLiteDatabase getWritableDatabase(Backend backend) { return new RustV11SupportSQLiteOpenHelper(backend).getWritableDatabase(); } @Test public void testBackwards() throws IOException { Timber.w("This is much slower than forwards"); - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id int)"); @@ -99,7 +99,7 @@ public void testBackwards() throws IOException { @Test public void moveToPositionStart() throws IOException { - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id int)"); @@ -155,7 +155,7 @@ private void checkValueAtRowEqualsRowNumBackwards(SupportSQLiteDatabase db) { public void testCorruptionIsHandled() throws IOException { int elements = DatabaseIntegrationTests.DB_PAGE_NUM_INT_ELEMENTS; - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id int)"); @@ -193,7 +193,7 @@ public void smallQueryHasOneCount() throws IOException { int elements = 30; // 465 - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id varchar)"); @@ -219,7 +219,7 @@ public void smallQueryHasOneCount() throws IOException { public void variableLengthStringsReturnDifferentRowCounts() throws IOException { int elements = 50; // 1275 > 1000 - try (BackendV1 backend = super.getBackend("initial_version_2_12_1.anki2")) { + try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { SupportSQLiteDatabase db = getWritableDatabase(backend); db.execSQL("create table tmp (id varchar)"); diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt similarity index 85% rename from rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.kt rename to rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt index 76a5349f6..c7fd1df36 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1Impl.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -24,6 +24,7 @@ import anki.backend.GeneratedBackend import anki.generic.Int64 import com.google.protobuf.ByteString import com.google.protobuf.InvalidProtocolBufferException +import net.ankiweb.rsdroid.database.SQLHandler import org.json.JSONArray import org.json.JSONException import org.json.JSONObject @@ -32,17 +33,21 @@ import java.io.Closeable import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock -open class BackendV1Impl(langs: Iterable = listOf("en")) : GeneratedBackend(), BackendV1, Closeable { +open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), SQLHandler, Closeable { // Set on init; unset on .close(). Access via withBackend() private var backendPointer: Long? = null private val lock = ReentrantLock() // Only stored to satisfy .getPath() interface in SQL connection private var collectionPath: String? = null - override fun isOpen(): Boolean { + fun isOpen(): Boolean { return backendPointer != null } + fun openCollection(collectionPath: String, forceSchema11: Boolean = true) { + openCollection(collectionPath, "", "", "", forceSchema11) + } + /** * Open a backend instance, loading the shared library if not already loaded. */ @@ -149,8 +154,8 @@ open class BackendV1Impl(langs: Iterable = listOf("en")) : GeneratedBack closeCollection(false) } - override fun getPath(): String { - return collectionPath!! + override fun getPath(): String? { + return collectionPath } @CheckResult @@ -214,31 +219,6 @@ open class BackendV1Impl(langs: Iterable = listOf("en")) : GeneratedBack Timber.i("Rust: getColumnNames %s", sql) return getColumnNamesFromQuery(sql).valsList.toTypedArray() } - - // @Override - // public AdBackend.SchedTimingTodayOut2 schedTimingTodayLegacy(long createdSecs, int createdMinsWest, long nowSecs, int nowMinsWest, int rolloverHour) { - // return ankiDroidBackend.schedTimingTodayLegacy(createdSecs, createdMinsWest, nowSecs, nowMinsWest, rolloverHour); - // } - // - // @Override - // public AdBackend.LocalMinutesWestOut localMinutesWestLegacy(long collectionCreationTime) { - // return ankiDroidBackend.localMinutesWestLegacy(collectionCreationTime); - // } - // - // @Override - // @RustCleanup("Architecture - backendPtr param is not required") - // public AdBackend.DebugActiveDatabaseSequenceNumbersOut debugActiveDatabaseSequenceNumbers(long backendPtr) { - // return ankiDroidBackend.debugActiveDatabaseSequenceNumbers(ensureBackend().toJni()); - // } - // - // @Override - // public void downgradeBackend(String collectionPath) { - // if (!new File(collectionPath).exists()) { - // throw new BackendException(collectionPath + " not found"); - // } - // - // ankiDroidBackend.downgradeBackend(collectionPath); - // } } /** diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt index 9e46ec364..23221e27d 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt @@ -16,15 +16,14 @@ package net.ankiweb.rsdroid import androidx.sqlite.db.SupportSQLiteOpenHelper -import net.ankiweb.rsdroid.RustBackendFailedException abstract class BackendFactory // Force users to go through getInstance - for now we need to handle the backend failure protected constructor() { - private var backend: BackendV1? = null + private var backend: Backend? = null @Synchronized - fun getBackend(): BackendV1 { + fun getBackend(): Backend { if (backend == null) { - backend = BackendV1Impl() + backend = Backend() } return backend!! } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt index 14a3a1429..dd78a4119 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt @@ -22,8 +22,8 @@ object BackendUtils { */ // fixme: call backend directly @JvmStatic - fun openAnkiDroidCollection(backendV1: BackendV1, path: String?) { - backendV1.openCollection(path!!) + fun openAnkiDroidCollection(backendV1: Backend, path: String?, forceSchema11: Boolean) { + backendV1.openCollection(path ?: ":memory:", forceSchema11 = forceSchema11) } val ankiCommitHash: String diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt deleted file mode 100644 index 5339bcb44..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV1.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.ankiweb.rsdroid - -import anki.ankidroid.GetActiveSequenceNumbersResponse -import anki.scheduler.SchedTimingTodayResponse -import net.ankiweb.rsdroid.database.SQLHandler -import java.io.Closeable - -/** - * This interface describes valid methods when running the backend in the legacy schema 11 - * mode. - */ -interface BackendV1 : SQLHandler, Closeable { - fun openCollection(collectionPath: String, mediaFolderPath: String, mediaDbPath: String, logPath: String, forceSchema11: Boolean); - // schema11 - fun openCollection(collectionPath: String) { - openCollection(collectionPath, "", "", "", true) - } - fun closeCollection(downgrade: Boolean = false); - - fun schedTimingTodayLegacy(createdSecs: Long, createdMinsWest: Int, nowSecs: Long, nowMinsWest: Int, rolloverHour: Int): SchedTimingTodayResponse; - fun getActiveSequenceNumbers(): GetActiveSequenceNumbersResponse - - - /** - * Whether the backend (not collection) is open. Not really useful outside tests. - */ - fun isOpen(): Boolean -} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt index eef2f17db..72e356421 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt @@ -17,7 +17,6 @@ package net.ankiweb.rsdroid import androidx.sqlite.db.SupportSQLiteOpenHelper import net.ankiweb.rsdroid.NativeMethods.ensureSetup -import net.ankiweb.rsdroid.RustBackendFailedException import net.ankiweb.rsdroid.database.RustV11SQLiteOpenHelperFactory class BackendV11Factory : BackendFactory() { diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt index ee5e5e533..ca0a1708d 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt @@ -45,10 +45,10 @@ import android.util.Pair import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteQuery import androidx.sqlite.db.SupportSQLiteStatement -import net.ankiweb.rsdroid.BackendV1 +import net.ankiweb.rsdroid.Backend import java.util.* -class RustSupportSQLiteDatabase(backend: BackendV1?, readOnly: Boolean) : SupportSQLiteDatabase { +class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportSQLiteDatabase { private val sessionFactory: ThreadLocal private val isReadOnly: Boolean private var isOpen: Boolean @@ -137,7 +137,7 @@ class RustSupportSQLiteDatabase(backend: BackendV1?, readOnly: Boolean) : Suppor return 0 } - override fun update(table: String, conflictAlgorithm: Int, values: ContentValues, whereClause: String, whereArgs: Array?): Int { + override fun update(table: String, conflictAlgorithm: Int, values: ContentValues, whereClause: String?, whereArgs: Array?): Int { // taken from SQLiteDatabase class. require(!(values == null || values.size() == 0)) { "Empty values" } val sql = StringBuilder(120) @@ -186,7 +186,7 @@ class RustSupportSQLiteDatabase(backend: BackendV1?, readOnly: Boolean) : Suppor throw NotImplementedException.Companion.todo() } - override fun getPath(): String { + override fun getPath(): String? { return session.getPath() } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt index e96a78fd2..25cd9603f 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt @@ -18,11 +18,11 @@ package net.ankiweb.rsdroid.database import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper import net.ankiweb.rsdroid.BackendFactory -import net.ankiweb.rsdroid.BackendV1 +import net.ankiweb.rsdroid.Backend abstract class RustSupportSQLiteOpenHelper : SupportSQLiteOpenHelper { protected val configuration: SupportSQLiteOpenHelper.Configuration? - protected val backend: BackendV1? + protected val backend: Backend? protected var backendFactory: BackendFactory? = null protected var database: SupportSQLiteDatabase? = null @@ -32,7 +32,7 @@ abstract class RustSupportSQLiteOpenHelper : SupportSQLiteOpenHelper { backend = null } - constructor(backend: BackendV1) { + constructor(backend: Backend) { check(backend.isOpen()) { "Backend should be open" } this.backend = backend configuration = null diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt index e6ac1331d..6b700ac53 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt @@ -19,18 +19,19 @@ import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper import net.ankiweb.rsdroid.BackendFactory import net.ankiweb.rsdroid.BackendUtils.openAnkiDroidCollection -import net.ankiweb.rsdroid.BackendV1 +import net.ankiweb.rsdroid.Backend +import net.ankiweb.rsdroid.BackendUtils import timber.log.Timber class RustV11SupportSQLiteOpenHelper : RustSupportSQLiteOpenHelper { constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) : super(configuration, backendFactory) {} - constructor(backend: BackendV1) : super(backend) {} + constructor(backend: Backend) : super(backend) {} override fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase? { Timber.d("createRustSupportSQLiteDatabase") return if (configuration != null) { val backend = backendFactory!!.getBackend() - openAnkiDroidCollection(backend, configuration.name) + openAnkiDroidCollection(backend, configuration.name, true) RustSupportSQLiteDatabase(backend, readOnly) } else { RustSupportSQLiteDatabase(backend, readOnly) diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt index ac0433fa5..6bfa60394 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt @@ -18,19 +18,21 @@ package net.ankiweb.rsdroid.database import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper import net.ankiweb.rsdroid.BackendFactory -import net.ankiweb.rsdroid.BackendV1 +import net.ankiweb.rsdroid.Backend +import net.ankiweb.rsdroid.BackendUtils +import net.ankiweb.rsdroid.BackendUtils.openAnkiDroidCollection import timber.log.Timber class RustVNextSupportSQLiteOpenHelper : RustSupportSQLiteOpenHelper { constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) : super(configuration, backendFactory) {} - constructor(backend: BackendV1) : super(backend) {} + constructor(backend: Backend) : super(backend) {} override fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase? { Timber.d("createRustSupportSQLiteDatabase") return if (configuration != null) { val backend = backendFactory!!.getBackend() // openCollection opens and upgrades the collection -// backend.openCollection(configuration.name, null, null, null); + openAnkiDroidCollection(backend, configuration.name, false) RustSupportSQLiteDatabase(backend, readOnly) } else { RustSupportSQLiteDatabase(backend, readOnly) diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt index a027ba8fc..5b25625fd 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt @@ -36,7 +36,7 @@ interface SQLHandler { fun closeDatabase() @CheckResult - fun getPath(): String + fun getPath(): String? /* Protobuf-related (#6) */ fun getNextSlice(startIndex: Long, sequenceNumber: Int): DBResponse diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.kt index 7640db55d..a49ffadbd 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/Session.kt @@ -67,7 +67,7 @@ class Session(private val backend: SQLHandler) : SQLHandler { backend.closeDatabase() } - override fun getPath(): String { + override fun getPath(): String? { return backend.getPath() } From 1e13e6fc69d32535f7fa8c3f145f8886388e758c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 12:14:22 +1000 Subject: [PATCH 04/61] Use messages for certain backend inputs Matches desktop behaviour --- tools/protoc-gen/protoc-gen.py | 58 ++++++++++++++++++++++------------ 1 file changed, 37 insertions(+), 21 deletions(-) diff --git a/tools/protoc-gen/protoc-gen.py b/tools/protoc-gen/protoc-gen.py index 8d347f878..7a01193cd 100755 --- a/tools/protoc-gen/protoc-gen.py +++ b/tools/protoc-gen/protoc-gen.py @@ -133,7 +133,7 @@ def as_builder(self): field.json_name, setter ) - ret += " val protobuf = builder.build();\n".format( + ret += " val input = builder.build();\n".format( self.proto_file.package, self.method.name ) return ret.replace("I18n.", "I18N.") @@ -182,14 +182,14 @@ def as_command_name(self): return 'case {}: return "{}";'.format(self.command_num, self.method_name()) def __repr__(self): - k = fix_namespace(self.method.input_type) - + input_type_name = fix_namespace(self.method.input_type) + input_msg = self.messages[input_type_name] out=self.get_output() name=self.method_name() - inv="({})".format(self.messages[k].as_params()) + inv="({})".format(self.messages[input_type_name].as_params()) service=self.service_index method=self.command_num - deser=self.messages[k].as_builder() + deser=self.messages[input_type_name].as_builder() buf = f""" @Throws(BackendException::class) @@ -198,29 +198,40 @@ def __repr__(self): }} """ - if k != "anki.i18n.TranslateStringRequest": + if input_type_name == "anki.i18n.TranslateStringRequest": # maps not currently supported - if out == "void": - return_segment = f""" - {name}Raw(protobuf.toByteArray()); - """ - out_with_colon = "" - else: - out_with_colon = f": {out}" - return_segment = f"""\ - try {{ - return {out}.parseFrom({name}Raw(protobuf.toByteArray())); - }} catch (exc: com.google.protobuf.InvalidProtocolBufferException) {{ - throw BackendException("protobuf parsing failed"); - }}""" - - buf += f""" + return buf + + if ((input_type_name.endswith("Request") or len(input_msg.fields) < 2) and not contains_oneof(input_msg)): + # unroll + pass + else: + # skip unroll + inv=f"(input: {input_type_name})" + deser = "" + + if out == "void": + return_segment = f""" +{name}Raw(input.toByteArray()); + """ + out_with_colon = "" + else: + out_with_colon = f": {out}" + return_segment = f"""\ +try {{ + return {out}.parseFrom({name}Raw(input.toByteArray())); +}} catch (exc: com.google.protobuf.InvalidProtocolBufferException) {{ + throw BackendException("protobuf parsing failed"); +}}""" + + buf += f""" @Throws(BackendException::class) open fun {name}{inv}{out_with_colon} {{ {deser} {return_segment} }} """ + return buf def method_name(self): @@ -298,6 +309,11 @@ def generate_code(request, response): f.name = "GeneratedBackend.kt" f.content = "\n".join(file_contents) +def contains_oneof(msg): + for field in msg.fields: + if field.oneof_index: + return True + return False if __name__ == "__main__": # Read request message from stdin From cb6cbaacab4ccb6582b7a320b7baaa1d5892cef2 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 13:17:26 +1000 Subject: [PATCH 05/61] Unpack single-field outputs like desktop --- .../rsdroid/ankiutil/InstrumentedTest.java | 2 +- .../main/java/net/ankiweb/rsdroid/Backend.kt | 8 +- tools/protoc-gen/protoc-gen.py | 115 +++++++++++------- 3 files changed, 76 insertions(+), 49 deletions(-) diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java index ace5d53bd..ac08ba5ce 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java @@ -72,7 +72,7 @@ public void after() { List numbers; try { - numbers = b.getActiveSequenceNumbers().getNumbersList(); + numbers = b.getActiveSequenceNumbers(); } catch (BackendInvalidInputException exc) { assertThat(exc.getLocalizedMessage(), containsString("CollectionNotOpen")); continue; diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt index c7fd1df36..224db8d2b 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -170,18 +170,18 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), @Throws(JSONException::class) private fun fullQueryInternal(sql: String, bindArgs: Array?): JSONArray { - val output = runDbCommand(dbRequestJson(sql, bindArgs)).json.toStringUtf8() + val output = runDbCommand(dbRequestJson(sql, bindArgs)).toStringUtf8() return JSONArray(output) } override fun insertForId(sql: String, bindArgs: Array?): Long { Timber.i("Rust: sql insert %s", sql) - return super.insertForId(dbRequestJson(sql, bindArgs)).`val` + return super.insertForId(dbRequestJson(sql, bindArgs)) } override fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int { Timber.i("Rust: executeGetRowsAffected %s", sql) - return runDbCommandForRowCount(dbRequestJson(sql, bindArgs)).`val`.toInt() + return runDbCommandForRowCount(dbRequestJson(sql, bindArgs)).toInt() } /* Begin Protobuf-based database streaming methods (#6) */ @@ -217,7 +217,7 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), override fun getColumnNames(sql: String): Array { Timber.i("Rust: getColumnNames %s", sql) - return getColumnNamesFromQuery(sql).valsList.toTypedArray() + return getColumnNamesFromQuery(sql).toTypedArray() } } diff --git a/tools/protoc-gen/protoc-gen.py b/tools/protoc-gen/protoc-gen.py index 7a01193cd..96e5b091c 100755 --- a/tools/protoc-gen/protoc-gen.py +++ b/tools/protoc-gen/protoc-gen.py @@ -8,6 +8,8 @@ from google.protobuf.compiler import plugin_pb2 as plugin +TYPE_ENUM = 14 + # Needs map<> and Fluent import rather than Backend ignore_methods_accepting = ["TranslateStringIn"] @@ -37,52 +39,67 @@ def get_annotation(type, optional=False): else: return "@NonNull" -class Message: - def __init__(self, message, proto_file): - self.method = message - self.fields = message.field - self.proto_file = proto_file +def is_repeating(field): + return field.label == 3 - # https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.descriptor - def to_java_type(self, type, field): - ### Converts a given protobuf type to the Java Equivalent: (int, or List for example) ### - primitive_list = { +def as_getter(field): + if is_repeating(field): + return f"{field.name}List" + else: + return field.json_name + +# https://developers.google.com/protocol-buffers/docs/reference/cpp/google.protobuf.descriptor +def to_java_type(type, field, output=False): + ### Converts a given protobuf type to the Java Equivalent: (int, or List for example) ### + primitive_list = { + 1: "Double", + 2: "Float", + 3: "Long", + # 4 : "uint64", + 5: "Int", + 8: "Boolean", + 9: "String", + 12: "com.google.protobuf.ByteString", + 13: "Int", # "uint32" + 17: "Int", + 18: "Long", + } + + if is_repeating(field): + primitive_map = { 1: "Double", 2: "Float", 3: "Long", - # 4 : "uint64", 5: "Int", 8: "Boolean", - 9: "String", - 12: "com.google.protobuf.ByteString", - 13: "Int", # "uint32" - 17: "Int", - 18: "Long", + 13: "Int", } + if type != 14 and type != 11: + new_type = ( + primitive_map[type] + if type in primitive_map + else primitive_list[type] + ) + else: + new_type = fix_namespace(field.type_name) - if self.is_repeating(field): - primitive_map = { - 1: "Double", - 2: "Float", - 3: "Long", - 5: "Int", - 8: "Boolean", - 13: "Int", - } - if type != 14 and type != 11: - new_type = ( - primitive_map[type] - if type in primitive_map - else primitive_list[type] - ) - else: - new_type = fix_namespace(field.type_name) + if output: + return "List<{}>".format(new_type) + else: return "Iterable<{}>".format(new_type) - if type == 14 or type == 11: # enum/message - return fix_namespace(field.type_name) + if type == 14 or type == 11: # enum/message + return fix_namespace(field.type_name) + + return primitive_list[type] + +class Message: + def __init__(self, message, proto_file): + self.method = message + self.fields = message.field + self.proto_file = proto_file + - return primitive_list[type] def as_param(self, field): optional = getattr(field, "proto3_optional") @@ -92,19 +109,16 @@ def as_param(self, field): name = "`val`" return "{}: {}{}".format( - name, self.to_java_type(field.type, field), annotation, + name, to_java_type(field.type, field), annotation, ) - def is_repeating(self, field): - return field.label == 3 - def as_setter_name(self, field): # We have a method: setXXX, so the first letter is uppercase return field[0].upper() + field[1:] def as_setter(self, field): optional = getattr(field, "proto3_optional", False) - prefix = "set" if not self.is_repeating(field) else "addAll" + prefix = "set" if not is_repeating(field) else "addAll" name = field.json_name if name == "val": name = "`val`" @@ -117,7 +131,7 @@ def getFieldSetters(self): return [(self.as_setter(f), f) for f in self.fields] def is_primitive(self, field): - return not self.is_repeating(field) and field.type not in [9, 12, 11, 14] + return not is_repeating(field) and field.type not in [9, 12, 11, 14] def as_builder(self): # we can't set fields to null, so we can't use the builder fluent syntax. @@ -183,6 +197,7 @@ def as_command_name(self): def __repr__(self): input_type_name = fix_namespace(self.method.input_type) + output_type_name = fix_namespace(self.method.output_type) input_msg = self.messages[input_type_name] out=self.get_output() name=self.method_name() @@ -203,13 +218,25 @@ def __repr__(self): return buf if ((input_type_name.endswith("Request") or len(input_msg.fields) < 2) and not contains_oneof(input_msg)): - # unroll + # unroll input pass else: - # skip unroll + # skip unroll input inv=f"(input: {input_type_name})" deser = "" + output_msg = self.messages[output_type_name] + if ( + len(output_msg.fields) == 1 + and output_msg.fields[0].type != TYPE_ENUM + ): + # unwrap single return arg + f = output_msg.fields[0] + out = to_java_type(f.type, f, output=True) + single_attribute = f".`{as_getter(f)}`" + else: + single_attribute = "" + if out == "void": return_segment = f""" {name}Raw(input.toByteArray()); @@ -219,7 +246,7 @@ def __repr__(self): out_with_colon = f": {out}" return_segment = f"""\ try {{ - return {out}.parseFrom({name}Raw(input.toByteArray())); + return {output_type_name}.parseFrom({name}Raw(input.toByteArray())){single_attribute}; }} catch (exc: com.google.protobuf.InvalidProtocolBufferException) {{ throw BackendException("protobuf parsing failed"); }}""" From 9bfe3d22c78c1da0d59151e3309a6e6f0efaa283 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 20:55:31 +1000 Subject: [PATCH 06/61] Generate translation accessors as part of the build Accessed with backend.tr.someTranslationName() --- .cargo/config.toml | 3 + .../rsdroid/BackendTranslationsTest.java | 34 ++++++ rsdroid/build.gradle | 19 ++- .../main/java/net/ankiweb/rsdroid/Backend.kt | 11 +- .../java/net/ankiweb/rsdroid/Translations.kt | 22 ++++ tools/gen-fluent-proto/gen.py | 56 --------- tools/gen-fluent-proto/readme.md | 5 - tools/genfluent/genfluent.bat | 2 + tools/genfluent/genfluent.py | 115 ++++++++++++++++++ tools/genfluent/genfluent.sh | 3 + 10 files changed, 205 insertions(+), 65 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendTranslationsTest.java create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/Translations.kt delete mode 100644 tools/gen-fluent-proto/gen.py delete mode 100644 tools/gen-fluent-proto/readme.md create mode 100644 tools/genfluent/genfluent.bat create mode 100755 tools/genfluent/genfluent.py create mode 100755 tools/genfluent/genfluent.sh diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..81d5a1758 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,3 @@ +[env] +RSLIB_FTL_ROOT = { value = "ftl/core/l10n.toml", relative = true } + diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendTranslationsTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendTranslationsTest.java new file mode 100644 index 000000000..b5ee5fada --- /dev/null +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendTranslationsTest.java @@ -0,0 +1,34 @@ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +package net.ankiweb.rsdroid; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.assertThat; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.Arrays; + +@RunWith(AndroidJUnit4.class) +public class BackendTranslationsTest extends InstrumentedTest { + + private String withoutIsolation(String s) { + return s.replace("\u2068", "").replace("\u2069", ""); + } + + @Test + public void ensureI18nWorks() { + Backend b = new Backend(Arrays.asList("en")); + assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, 10)), equalTo("Trash folder: 5 files, 10MB")); + assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, 10.0)), equalTo("Trash folder: 5 files, 10MB")); + assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, "foo")), equalTo("Trash folder: 5 files, fooMB")); + b = new Backend(Arrays.asList("fr")); + assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, 10)), equalTo("Corbeille : 5 fichiers, 10 Mo")); + } +} diff --git a/rsdroid/build.gradle b/rsdroid/build.gradle index 305cd8e94..1c359fe7f 100644 --- a/rsdroid/build.gradle +++ b/rsdroid/build.gradle @@ -41,6 +41,12 @@ android { proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } } + + sourceSets { + main { + kotlin.srcDirs += "build/generated/source/fluent" + } + } } // Consider upgrade to DSL: https://docs.gradle.org/current/userguide/plugins.html#sec:plugins_block @@ -73,6 +79,17 @@ dependencies { } +task generateTranslations(type: Exec) { + workingDir "$rootDir" + String genPath = System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows') ? 'tools\\genfluent\\genfluent.bat' : 'tools/genfluent/genfluent.sh' + if (System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows')) { + commandLine 'cmd', '/c', genPath + } else { + commandLine 'sh', '-c', genPath + } +} + +preBuild.dependsOn "generateTranslations" preBuild.dependsOn "cargoBuild" signing { @@ -88,4 +105,4 @@ signing { logger.warn("$message: ${hasPrivate}, ${hasPassword}, ${pk == null || "" == pk}, ${pwd == null || "" == pwd}") } -} \ No newline at end of file +} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt index 224db8d2b..c52577542 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -37,9 +37,14 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), // Set on init; unset on .close(). Access via withBackend() private var backendPointer: Long? = null private val lock = ReentrantLock() + // Only stored to satisfy .getPath() interface in SQL connection private var collectionPath: String? = null + val tr: Translations by lazy { + Translations(this) + } + fun isOpen(): Boolean { return backendPointer != null } @@ -107,7 +112,7 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), unpackResult(NativeMethods.runMethodRaw(it, service, method, input)) } } - + /** * Run the provided closure with locked access to the backend. * The backend maintains its own lock for backend commands, so this extra @@ -153,7 +158,7 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), override fun closeDatabase() { closeCollection(false) } - + override fun getPath(): String? { return collectionPath } @@ -207,7 +212,7 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), private fun performTransaction(kind: DbRequestKind) { Timber.i("Rust: transaction %s", kind) - runDbCommand(dbRequestJson(kind=kind)) + runDbCommand(dbRequestJson(kind = kind)) } @VisibleForTesting(otherwise = VisibleForTesting.NONE) diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Translations.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Translations.kt new file mode 100644 index 000000000..90b50050b --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Translations.kt @@ -0,0 +1,22 @@ +package net.ankiweb.rsdroid + +import anki.i18n.GeneratedTranslations +import anki.i18n.TranslateArgMap +import anki.i18n.TranslateArgValue +import anki.i18n.TranslateStringRequest +import anki.generic.String as GenericString + +// strip off unicode isolation markers from a translated string +// for testing purposes +fun String.withoutUnicodeIsolation(): String { + return this.replace("\u2068", "").replace("\u2069", "") +} + +class Translations(private val backend: Backend): GeneratedTranslations { + + override fun translate(module: Int, translation: Int, args: TranslateArgMap): String { + val request = TranslateStringRequest.newBuilder().putAllArgs(args).setModuleIndex(module).setMessageIndex(translation).build() + val output = backend.translateStringRaw(request.toByteArray()) + return GenericString.parseFrom(output).`val` + } +} diff --git a/tools/gen-fluent-proto/gen.py b/tools/gen-fluent-proto/gen.py deleted file mode 100644 index c0e2c0bc3..000000000 --- a/tools/gen-fluent-proto/gen.py +++ /dev/null @@ -1,56 +0,0 @@ -#!/usr/bin/env python - -# Currently unused -# Generates fluent.proto from fluent translation (ftl) files. - - -import sys -import os -from fluent.syntax import parse -from fluent.syntax import ast - -# requires: pip install fluent.syntax - -def get_identifiers(fileData): - idents = [] - for message in parse(fileData).body: - if not isinstance(message, ast.Message): - continue - idents.append(message.id.name) - sorted(idents) - return idents - -def proto_enum(idents): - buf = r"""// This file is automatically generated as part of the build process. - -syntax = "proto3"; -package BackendProto; -enum FluentString { -""" - - - for (idx, s) in enumerate(idents): - name = s.replace("-", "_").upper() - buf += " {} = {};\n".format(name, idx) - - buf += "}\n" - - return buf - -if __name__ == "__main__": - path = sys.argv[1] - buffer = "" - - for file in os.listdir(path): - if not file.endswith(".ftl"): - continue - with open(os.path.join(path, file)) as f: - for l in f.readlines(): - buffer += l + "\n" - - idents = get_identifiers(buffer) - buf = proto_enum(idents) - print(buf) - - - \ No newline at end of file diff --git a/tools/gen-fluent-proto/readme.md b/tools/gen-fluent-proto/readme.md deleted file mode 100644 index 63e94c30e..000000000 --- a/tools/gen-fluent-proto/readme.md +++ /dev/null @@ -1,5 +0,0 @@ -This tool generates `fluent.proto` from the fluent translation (ftl) files. - -**Unused. May be useful for future** - -We currently use the rust submodule. `build.rs` performs the same operation as long as it executes first. \ No newline at end of file diff --git a/tools/genfluent/genfluent.bat b/tools/genfluent/genfluent.bat new file mode 100644 index 000000000..f5e1dd0cf --- /dev/null +++ b/tools/genfluent/genfluent.bat @@ -0,0 +1,2 @@ +@ECHO OFF +python "%~dp0\genfluent.py" \ No newline at end of file diff --git a/tools/genfluent/genfluent.py b/tools/genfluent/genfluent.py new file mode 100755 index 000000000..f870195c1 --- /dev/null +++ b/tools/genfluent/genfluent.py @@ -0,0 +1,115 @@ +#!/usr/bin/env python3 +# Copyright: Ankitects Pty Ltd and contributors +# License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +from __future__ import annotations + +import json +from pathlib import Path +import subprocess +from typing import List, Literal, TypedDict +import stringcase + +def get_strings(): + output_file = Path("output.json").absolute() + subprocess.run(["cargo", "run", output_file], check=True, cwd="rslib-bridge/anki/rslib/i18n") + data = json.load(open(output_file)) + output_file.unlink() + return data + +modules = get_strings() + +def build_source(): + out = """\ +// Copyright: Ankitects Pty Ltd and contributors +// License: GNU AGPL, version 3 or later; http://www.gnu.org/licenses/agpl.html + +package anki.i18n; + +import anki.i18n.TranslateArgValue + +fun asTranslateArg(arg: Any): TranslateArgValue { + val builder = TranslateArgValue.newBuilder() + when (arg) { + is String -> builder.setStr(arg) + is Int -> builder.setNumber(arg.toDouble()) + is Double -> builder.setNumber(arg) + else -> throw Exception("invalid arg provided to translation") + } + return builder.build() +} + +// This should be either String, Double or Int +typealias TranslateArg = Any + +typealias TranslateArgMap = Map + +interface GeneratedTranslations { + fun translate(module: Int, translation: Int, args: TranslateArgMap): String; + + +""" + + out += methods() + out += "}" + + return out + +def write_source(): + source = build_source() + path = Path(f"rsdroid/build/generated/source/fluent/anki/GeneratedTranslations.kt") + if not path.parent.exists(): + path.parent.mkdir(parents=True) + open(path, "w").write(source) + + +class Variable(TypedDict): + name: str + kind: Literal["Any", "Int", "String", "Float"] + + +def methods() -> str: + out = [] + for module in modules: + for translation in module["translations"]: + key = stringcase.camelcase(translation["key"].replace("-", "_")) + arg_types = get_arg_types(translation["variables"]) + args = get_args(translation["variables"]) + doc = translation["text"] + out.append( + f""" + /** {doc} */ + fun {key}({arg_types}): String {{ + return translate({module["index"]}, {translation["index"]}, mapOf({args})) + }} +""" + ) + + return "\n".join(out) + "\n" + + +def get_arg_types(args: list[Variable]) -> str: + + return ", ".join( + [f"`{stringcase.camelcase(arg['name'])}`: {arg_kind(arg)}" for arg in args] + ) + + +def arg_kind(arg: Variable) -> str: + if arg["kind"] == "Int": + return "Int" + elif arg["kind"] == "Any": + return "TranslateArg" + elif arg["kind"] == "Float": + return "Double" + else: + return "String" + + +def get_args(args: list[Variable]) -> str: + return ", ".join( + [f'"{arg["name"]}" to asTranslateArg(`{stringcase.camelcase(arg["name"])}`)' for arg in args] + ) + + +write_source() diff --git a/tools/genfluent/genfluent.sh b/tools/genfluent/genfluent.sh new file mode 100755 index 000000000..7313efeff --- /dev/null +++ b/tools/genfluent/genfluent.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +./tools/genfluent/genfluent.py From 3a3719735092d0d9f49ef9a6059dea40f0ce2a33 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 15:28:53 +1000 Subject: [PATCH 07/61] Add i18n submodule in new path --- .gitmodules | 3 +++ ftl/core | 1 + 2 files changed, 4 insertions(+) create mode 160000 ftl/core diff --git a/.gitmodules b/.gitmodules index cefc90f14..dc8e5a905 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = rslib-bridge/anki url = https://github.com/ankidroid/anki branch = ankidroid-2-1-34 +[submodule "ftl/core"] + path = ftl/core + url = https://github.com/ankitects/anki-core-i18n.git diff --git a/ftl/core b/ftl/core new file mode 160000 index 000000000..fc15171cf --- /dev/null +++ b/ftl/core @@ -0,0 +1 @@ +Subproject commit fc15171cf1f81b63574fceff74767d62ab7779d0 From 179da19c2cd010fcb7ded9abee5b4684c7f3e25b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 18:09:30 +1000 Subject: [PATCH 08/61] Automatically update submodule as part of i18n build - Extracted from pinned sha in upstream repo - Maintainer needs to commit change to submodule --- ftl/core | 2 +- tools/genfluent/genfluent.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ftl/core b/ftl/core index fc15171cf..460c0d295 160000 --- a/ftl/core +++ b/ftl/core @@ -1 +1 @@ -Subproject commit fc15171cf1f81b63574fceff74767d62ab7779d0 +Subproject commit 460c0d2954e40513e6cb65b206fed6412cee5817 diff --git a/tools/genfluent/genfluent.py b/tools/genfluent/genfluent.py index f870195c1..ad844eca2 100755 --- a/tools/genfluent/genfluent.py +++ b/tools/genfluent/genfluent.py @@ -9,6 +9,16 @@ import subprocess from typing import List, Literal, TypedDict import stringcase +import re + +def ensure_i18n_module_correct(): + reg = re.compile(r'(\s+)(\S+_(?:commit|zip_csum)) = "(.*)"') + for line in open("rslib-bridge/anki/repos.bzl").readlines(): + if m := reg.match(line): + (indent, key, commit) = m.groups() + if key == "core_i18n_commit": + subprocess.run(["git", "checkout", commit], cwd="ftl/core", check=True) + break def get_strings(): output_file = Path("output.json").absolute() @@ -111,5 +121,5 @@ def get_args(args: list[Variable]) -> str: [f'"{arg["name"]}" to asTranslateArg(`{stringcase.camelcase(arg["name"])}`)' for arg in args] ) - +ensure_i18n_module_correct() write_source() From 96299a3a5dd0849d1a4995c45a228fb1850e3926 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 15:42:16 +1000 Subject: [PATCH 09/61] Update Rust version in build scripts --- .github/workflows/linux_build.yml | 2 +- .github/workflows/macos_build.yml | 2 +- .github/workflows/publish_library.yml | 2 +- .github/workflows/publish_testing.yml | 2 +- .github/workflows/robolectric_build.yml | 2 +- .github/workflows/windows_build.yml | 2 +- .github/workflows/windows_pure_build.yml | 2 +- docs/ENVIRONMENT.md | 6 +++--- docs/easy-testing.md | 2 +- tools/doctor.sh | 6 +++--- 10 files changed, 14 insertions(+), 14 deletions(-) diff --git a/.github/workflows/linux_build.yml b/.github/workflows/linux_build.yml index 094d247a2..a72ed5a5b 100644 --- a/.github/workflows/linux_build.yml +++ b/.github/workflows/linux_build.yml @@ -41,7 +41,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1.0.6 with: - toolchain: 1.54.0 + toolchain: 1.58.1 override: true components: rustfmt diff --git a/.github/workflows/macos_build.yml b/.github/workflows/macos_build.yml index c565fb0e6..5f956615d 100644 --- a/.github/workflows/macos_build.yml +++ b/.github/workflows/macos_build.yml @@ -39,7 +39,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1.0.6 with: - toolchain: 1.54.0 + toolchain: 1.58.1 override: true components: rustfmt diff --git a/.github/workflows/publish_library.yml b/.github/workflows/publish_library.yml index 74fbd5f73..39379229d 100644 --- a/.github/workflows/publish_library.yml +++ b/.github/workflows/publish_library.yml @@ -33,7 +33,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1.0.6 with: - toolchain: 1.54.0 + toolchain: 1.58.1 override: true components: rustfmt diff --git a/.github/workflows/publish_testing.yml b/.github/workflows/publish_testing.yml index 40f6b2b3f..387d5f95b 100644 --- a/.github/workflows/publish_testing.yml +++ b/.github/workflows/publish_testing.yml @@ -31,7 +31,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1.0.6 with: - toolchain: 1.54.0 + toolchain: 1.58.1 override: true components: rustfmt diff --git a/.github/workflows/robolectric_build.yml b/.github/workflows/robolectric_build.yml index 2602061bf..75db424e9 100644 --- a/.github/workflows/robolectric_build.yml +++ b/.github/workflows/robolectric_build.yml @@ -56,7 +56,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1.0.6 with: - toolchain: 1.54.0 + toolchain: 1.58.1 override: true components: rustfmt diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index 97be6f767..62f9a5955 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -58,7 +58,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1.0.6 with: - toolchain: 1.54.0 + toolchain: 1.58.1 override: true components: rustfmt diff --git a/.github/workflows/windows_pure_build.yml b/.github/workflows/windows_pure_build.yml index c85bdb1e3..7c3d99d7a 100644 --- a/.github/workflows/windows_pure_build.yml +++ b/.github/workflows/windows_pure_build.yml @@ -36,7 +36,7 @@ jobs: - name: Install Rust uses: actions-rs/toolchain@v1.0.6 with: - toolchain: 1.54.0 + toolchain: 1.58.1 override: true components: rustfmt diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index bc2fa1e99..d7932d0e6 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -61,11 +61,11 @@ You should open Android Studio and use the Tools --> SDK Manager to download the Install rust via [rustup](https://rustup.rs/) -Configure rust to use version 1.54.0 since current does not work yet [#168](https://github.com/ankidroid/Anki-Android-Backend/issues/168) +Install the Rust version the desktop code uses (later is probably fine too): ```bash -rustup install 1.54.0 -rustup default 1.54.0 +rustup install 1.58.1 +rustup default 1.58.1 ``` #### Android targets diff --git a/docs/easy-testing.md b/docs/easy-testing.md index 0ebbdcda8..fd7989768 100644 --- a/docs/easy-testing.md +++ b/docs/easy-testing.md @@ -13,7 +13,7 @@ Install NDK: Install Rust: -- rustup install 1.54.0 +- rustup install 1.58.1 - rustup target add x86_64-linux-android - sudo ln -sf /usr/bin/gcc /usr/bin/x86_64--unknown-linux-gnu-gcc diff --git a/tools/doctor.sh b/tools/doctor.sh index 3460ef8a6..6ffc61a42 100755 --- a/tools/doctor.sh +++ b/tools/doctor.sh @@ -62,9 +62,9 @@ else fi -cecho $lgray "Installing rust 1.54.0" # nightly" - temporarily using 1.54.0 - see #168 -rustup install 1.54.0 #nightly -rustup default 1.54.0 +cecho $lgray "Installing rust 1.58.1" +rustup install 1.58.1 +rustup default 1.58.1 cecho $lgray "Adding rust android targets" From d05c0e9a9017cb2eef63b3d2107cfc402e1e4542 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 15:46:15 +1000 Subject: [PATCH 10/61] Mention stringcase requirement; add to CI --- .github/workflows/linux_build.yml | 96 ++++--- .github/workflows/macos_build.yml | 199 +++++++------ .github/workflows/publish_library.yml | 106 +++---- .github/workflows/publish_testing.yml | 4 + .github/workflows/robolectric_build.yml | 345 +++++++++++------------ .github/workflows/windows_build.yml | 161 ++++++----- .github/workflows/windows_pure_build.yml | 97 ++++--- docs/ENVIRONMENT.md | 5 +- docs/easy-testing.md | 5 +- 9 files changed, 515 insertions(+), 503 deletions(-) diff --git a/.github/workflows/linux_build.yml b/.github/workflows/linux_build.yml index a72ed5a5b..fb8ba9eec 100644 --- a/.github/workflows/linux_build.yml +++ b/.github/workflows/linux_build.yml @@ -15,60 +15,64 @@ jobs: timeout-minutes: 80 runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v2 - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + - name: Fetch submodules + run: git submodule update --init --recursive --remote --force - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 - # COULD_BE_BETTER: Consider turning this into a GitHub action - help the wider community - # NDK install (unzipping) is really noisy - silence the log spam with grep, while keeping errors - - name: Install NDK (silent) - run: .github/scripts/install_ndk.sh 22.0.7026061 + # COULD_BE_BETTER: Consider turning this into a GitHub action - help the wider community + # NDK install (unzipping) is really noisy - silence the log spam with grep, while keeping errors + - name: Install NDK (silent) + run: .github/scripts/install_ndk.sh 22.0.7026061 - - name: Install linker - run: .github/scripts/linux_install_x86_64-unknown-linux-gnu-gcc.sh + - name: Install linker + run: .github/scripts/linux_install_x86_64-unknown-linux-gnu-gcc.sh - # install cargo - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.58.1 - override: true - components: rustfmt + # install cargo + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_targets.sh + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_targets.sh - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Rust Cache - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - rslib-bridge/target - key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-rust-v1-assembleRelease - ${{ runner.os }}-rust-v1 + - name: Install Python libs + run: | + pip3 install stringcase - - name: Build - run: ./gradlew clean assembleRelease -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain # assembleAndroidTest + - name: Rust Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rslib-bridge/target + key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-rust-v1-assembleRelease + ${{ runner.os }}-rust-v1 - # Our publish workflow (publish_library.yaml) is on Ubuntu and needs javadocs (#57) - - name: Test Javadoc - run: ./gradlew :rsdroid:androidJavadocs -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + - name: Build + run: ./gradlew clean assembleRelease -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain # assembleAndroidTest + + # Our publish workflow (publish_library.yaml) is on Ubuntu and needs javadocs (#57) + - name: Test Javadoc + run: ./gradlew :rsdroid:androidJavadocs -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain diff --git a/.github/workflows/macos_build.yml b/.github/workflows/macos_build.yml index 5f956615d..e48de01dc 100644 --- a/.github/workflows/macos_build.yml +++ b/.github/workflows/macos_build.yml @@ -16,72 +16,71 @@ jobs: runs-on: macos-latest timeout-minutes: 80 steps: - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK - run: .github/scripts/install_ndk.sh 22.0.7026061 - - - name: Test NDK - run: echo "NDK set to $ANDROID_NDK_HOME" - - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.58.1 - override: true - components: rustfmt - - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_targets.sh - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install & Test Protobuf Compiler - run: | - pip3 install protobuf - python3 .github/scripts/protoc_gen_deps.py - - - name: Rust Cache - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - rslib-bridge/target - key: ${{ runner.os }}-rust-v1-assembleDebug-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-rust-v1-assembleDebug - ${{ runner.os }}-rust-v1 - - - name: Build - run: ./gradlew clean assembleDebug -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: Build Instrumented Test APKs - run: ./gradlew rsdroid-instrumented:assembleDebug rsdroid-instrumented:assembleAndroidTest -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: Upload APKs as Artifact - uses: actions/upload-artifact@v2 - with: - name: rsdroid-instrumented - if-no-files-found: error - path: rsdroid-instrumented/build/outputs/apk - + - uses: actions/checkout@v2 + + - name: Fetch submodules + run: git submodule update --init --recursive --remote --force + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK + run: .github/scripts/install_ndk.sh 22.0.7026061 + + - name: Test NDK + run: echo "NDK set to $ANDROID_NDK_HOME" + + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt + + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_targets.sh + + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install & Test Protobuf Compiler + run: | + pip3 install protobuf stringcase + python3 .github/scripts/protoc_gen_deps.py + + - name: Rust Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rslib-bridge/target + key: ${{ runner.os }}-rust-v1-assembleDebug-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-rust-v1-assembleDebug + ${{ runner.os }}-rust-v1 + + - name: Build + run: ./gradlew clean assembleDebug -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: Build Instrumented Test APKs + run: ./gradlew rsdroid-instrumented:assembleDebug rsdroid-instrumented:assembleAndroidTest -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: Upload APKs as Artifact + uses: actions/upload-artifact@v2 + with: + name: rsdroid-instrumented + if-no-files-found: error + path: rsdroid-instrumented/build/outputs/apk test: needs: build @@ -93,37 +92,37 @@ jobs: api-level: [21] arch: [x86, x86_64] # arm and arm64 are not supported by reactivecircus/android-emulator-runner steps: - - uses: actions/checkout@v2 - name: Checkout - - # COULD_BE_BETTER: This may not be needed - tiny speed penalty - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - name: Download APKs - uses: actions/download-artifact@v2 - with: - name: rsdroid-instrumented - path: rsdroid-instrumented/build/outputs/apk - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK - run: .github/scripts/install_ndk.sh 22.0.7026061 - - - name: run tests - uses: reactivecircus/android-emulator-runner@v2 - timeout-minutes: 30 - with: - api-level: ${{ matrix.api-level }} - target: default - arch: ${{ matrix.arch }} - profile: Nexus 6 - script: ./gradlew rsdroid-instrumented:connectedCheck -x rsdroid-instrumented:packageDebugAndroidTest -x rsdroid-instrumented:packageDebug + - uses: actions/checkout@v2 + name: Checkout + + # COULD_BE_BETTER: This may not be needed - tiny speed penalty + - name: Fetch submodules + run: git submodule update --init --recursive --remote --force + + - name: Download APKs + uses: actions/download-artifact@v2 + with: + name: rsdroid-instrumented + path: rsdroid-instrumented/build/outputs/apk + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK + run: .github/scripts/install_ndk.sh 22.0.7026061 + + - name: run tests + uses: reactivecircus/android-emulator-runner@v2 + timeout-minutes: 30 + with: + api-level: ${{ matrix.api-level }} + target: default + arch: ${{ matrix.arch }} + profile: Nexus 6 + script: ./gradlew rsdroid-instrumented:connectedCheck -x rsdroid-instrumented:packageDebugAndroidTest -x rsdroid-instrumented:packageDebug diff --git a/.github/workflows/publish_library.yml b/.github/workflows/publish_library.yml index 39379229d..6179e6b1f 100644 --- a/.github/workflows/publish_library.yml +++ b/.github/workflows/publish_library.yml @@ -9,54 +9,58 @@ jobs: publish: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK - run: .github/scripts/install_ndk.sh 22.0.7026061 - - - name: Install linker - run: .github/scripts/linux_install_x86_64-unknown-linux-gnu-gcc.sh - - # install cargo - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.58.1 - override: true - components: rustfmt - - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_targets.sh - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Build - run: ./gradlew clean assembleRelease -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: Publish AAR to Maven - env: - ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} - ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - SONATYPE_NEXUS_USERNAME: david-allison-1 - SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - run: | - ./gradlew rsdroid:uploadArchives -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: ℹ️Additional Release Instructions (requires human interaction) - run: echo "Sign in to https://oss.sonatype.org/#stagingRepositories , close the repsository, then release it" \ No newline at end of file + - uses: actions/checkout@v2 + + - name: Fetch submodules + run: git submodule update --init --recursive --remote --force + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK + run: .github/scripts/install_ndk.sh 22.0.7026061 + + - name: Install linker + run: .github/scripts/linux_install_x86_64-unknown-linux-gnu-gcc.sh + + # install cargo + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt + + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_targets.sh + + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install Python libs + run: | + pip3 install stringcase + + - name: Build + run: ./gradlew clean assembleRelease -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: Publish AAR to Maven + env: + ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} + ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SONATYPE_NEXUS_USERNAME: david-allison-1 + SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + run: | + ./gradlew rsdroid:uploadArchives -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: ℹ️Additional Release Instructions (requires human interaction) + run: echo "Sign in to https://oss.sonatype.org/#stagingRepositories , close the repsository, then release it" diff --git a/.github/workflows/publish_testing.yml b/.github/workflows/publish_testing.yml index 387d5f95b..2818c9966 100644 --- a/.github/workflows/publish_testing.yml +++ b/.github/workflows/publish_testing.yml @@ -35,6 +35,10 @@ jobs: override: true components: rustfmt + - name: Install Python libs + run: | + pip3 install stringcase + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - name: Install Rust Targets run: .github/scripts/install_rust_robolectric_targets.sh diff --git a/.github/workflows/robolectric_build.yml b/.github/workflows/robolectric_build.yml index 75db424e9..258e534f5 100644 --- a/.github/workflows/robolectric_build.yml +++ b/.github/workflows/robolectric_build.yml @@ -15,105 +15,104 @@ jobs: runs-on: macos-latest timeout-minutes: 80 steps: -# - name: Configure Mac OS environment variables - # gnu tar for cache issue: https://github.com/actions/cache/issues/403 - # error[E0463]: can't find crate for `serde_derive` which `serde` depends on - # --> /Users/runner/work/Anki-Android-Backend/Anki-Android-Backend/anki/rslib/src/decks/schema11.rs:30:9 - - # ::add-path has now been deprectated - # echo "::add-path::/usr/local/opt/gnu-tar/libexec/gnubin" - -# run: | -# echo "/usr/local/opt/gnu-tar/libexec/gnubin" >> $GITHUB_PATH - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK (r22 - 22.0.7026061) - run: .github/scripts/install_ndk.sh 22.0.7026061 - -# TODO: Needs investigation. This seemed to work before adding gnubin/v1, just with serde broken. -# Now it doesn't seem to be picked up by rust. -# - name: Cache Rust dependencies -# uses: actions/cache@v1.0.1 -# with: -# path: rslib-bridge/target -# key: ${{ runner.OS }}-build-v1-${{ hashFiles('**/Cargo.lock') }} -# restore-keys: | -# ${{ runner.OS }}-build-v1- - - # install cargo - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.58.1 - override: true - components: rustfmt - - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_robolectric_targets.sh - - - name: Install x86_64-w64-mingw32-gcc - run: brew install mingw-w64 && x86_64-w64-mingw32-gcc -v - - - name: Install x86_64-unknown-linux-gnu - run: | - brew tap SergioBenitez/osxct - brew install x86_64-unknown-linux-gnu - x86_64-unknown-linux-gnu-gcc -v - - - name: Rust Cache - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - rslib-bridge/target - key: ${{ runner.os }}-rust-v1-rsdroid-testing:build-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-rust-v1-rsdroid-testing:build - ${{ runner.os }}-rust-v1 - - - name: Build JAR - run: | - export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc - export ANKIDROID_MACOS_CC=cc - export RUST_DEBUG=1 - export RUST_BACKTRACE=1 - export RUST_LOG=trace - export NO_CROSS=true - ./gradlew clean rsdroid-testing:build -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain --warning-mode all - - - name: Check Compiled Libraries - run: > - cd rsdroid-testing/assets && - ../../.github/scripts/check_robolectric_assets.sh - - - - name: Upload rsdroid-testing JAR as artifact - uses: actions/upload-artifact@v2 - with: - name: rsdroid-testing - if-no-files-found: error - path: rsdroid-testing/build/libs/ - - - name: Upload fluent.proto - uses: actions/upload-artifact@v2 - with: - name: anki-proto - if-no-files-found: error - path: rslib-bridge/anki/proto/ + # - name: Configure Mac OS environment variables + # gnu tar for cache issue: https://github.com/actions/cache/issues/403 + # error[E0463]: can't find crate for `serde_derive` which `serde` depends on + # --> /Users/runner/work/Anki-Android-Backend/Anki-Android-Backend/anki/rslib/src/decks/schema11.rs:30:9 + + # ::add-path has now been deprectated + # echo "::add-path::/usr/local/opt/gnu-tar/libexec/gnubin" + + # run: | + # echo "/usr/local/opt/gnu-tar/libexec/gnubin" >> $GITHUB_PATH + - uses: actions/checkout@v2 + + - name: Fetch submodules + run: git submodule update --init --recursive --remote --force + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK (r22 - 22.0.7026061) + run: .github/scripts/install_ndk.sh 22.0.7026061 + + # TODO: Needs investigation. This seemed to work before adding gnubin/v1, just with serde broken. + # Now it doesn't seem to be picked up by rust. + # - name: Cache Rust dependencies + # uses: actions/cache@v1.0.1 + # with: + # path: rslib-bridge/target + # key: ${{ runner.OS }}-build-v1-${{ hashFiles('**/Cargo.lock') }} + # restore-keys: | + # ${{ runner.OS }}-build-v1- + + # install cargo + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt + + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_robolectric_targets.sh + + - name: Install x86_64-w64-mingw32-gcc + run: brew install mingw-w64 && x86_64-w64-mingw32-gcc -v + + - name: Install x86_64-unknown-linux-gnu + run: | + brew tap SergioBenitez/osxct + brew install x86_64-unknown-linux-gnu + x86_64-unknown-linux-gnu-gcc -v + + - name: Rust Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rslib-bridge/target + key: ${{ runner.os }}-rust-v1-rsdroid-testing:build-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-rust-v1-rsdroid-testing:build + ${{ runner.os }}-rust-v1 + + - name: Build JAR + run: | + export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc + export ANKIDROID_MACOS_CC=cc + export RUST_DEBUG=1 + export RUST_BACKTRACE=1 + export RUST_LOG=trace + export NO_CROSS=true + ./gradlew clean rsdroid-testing:build -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain --warning-mode all + + - name: Check Compiled Libraries + run: > + cd rsdroid-testing/assets && + ../../.github/scripts/check_robolectric_assets.sh + + - name: Upload rsdroid-testing JAR as artifact + uses: actions/upload-artifact@v2 + with: + name: rsdroid-testing + if-no-files-found: error + path: rsdroid-testing/build/libs/ + + - name: Upload fluent.proto + uses: actions/upload-artifact@v2 + with: + name: anki-proto + if-no-files-found: error + path: rslib-bridge/anki/proto/ test: needs: build @@ -124,77 +123,77 @@ jobs: os: [ubuntu-latest, windows-latest, macos-latest] fail-fast: false steps: - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - uses: actions/download-artifact@v2 - name: Download Artifact - with: - name: rsdroid-testing - path: rsdroid-testing/build/libs - - - uses: actions/download-artifact@v2 - name: Download fluent.proto - with: - name: anki-proto - path: rslib-bridge/anki/proto/ - - - name: Ensure fluent.proto exists - if: matrix.os != 'windows-latest' - run: | - if [ ! -f rslib-bridge/anki/proto/fluent.proto ]; then - echo "fluent.proto not generated" - exit 1 - fi - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK (silent) - if: matrix.os != 'windows-latest' - run: .github/scripts/install_ndk.sh 22.0.7026061 - - - name: Install NDK (Windows - silent) - if: matrix.os == 'windows-latest' - run: | - Write-Host "NDK Install Started" - (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null - Write-Host "NDK Install Completed" - - - name: Install Python Setuptools - if: matrix.os == 'ubuntu-latest' - run: sudo apt-get install python3-setuptools - - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install & Test Protobuf Compiler - if: matrix.os != 'windows-latest' - run: | - pip3 install protobuf - python3 .github/scripts/protoc_gen_deps.py - - - name: Install & Test Protobuf Compiler (win) - if: matrix.os == 'windows-latest' - run: | - Set-Alias -Name "python3" -Value "python" - pip3 install protobuf - python3 .github/scripts/protoc_gen_deps.py - - - name: Run Tests - if: matrix.os != 'windows-latest' - run: ./gradlew rsdroid:test -x jar -x cargoBuildArm -x cargoBuildX86 -x cargoBuildArm64 -x cargoBuildX86_64 - - - name: Run Tests (win) - if: matrix.os == 'windows-latest' - run: ./gradlew rsdroid:test -x jar -x cargoBuildArm -x cargoBuildX86 -x cargoBuildArm64 -x cargoBuildX86_64 + - uses: actions/checkout@v2 + + - name: Fetch submodules + run: git submodule update --init --recursive --remote --force + + - uses: actions/download-artifact@v2 + name: Download Artifact + with: + name: rsdroid-testing + path: rsdroid-testing/build/libs + + - uses: actions/download-artifact@v2 + name: Download fluent.proto + with: + name: anki-proto + path: rslib-bridge/anki/proto/ + + - name: Ensure fluent.proto exists + if: matrix.os != 'windows-latest' + run: | + if [ ! -f rslib-bridge/anki/proto/fluent.proto ]; then + echo "fluent.proto not generated" + exit 1 + fi + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK (silent) + if: matrix.os != 'windows-latest' + run: .github/scripts/install_ndk.sh 22.0.7026061 + + - name: Install NDK (Windows - silent) + if: matrix.os == 'windows-latest' + run: | + Write-Host "NDK Install Started" + (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null + Write-Host "NDK Install Completed" + + - name: Install Python Setuptools + if: matrix.os == 'ubuntu-latest' + run: sudo apt-get install python3-setuptools + + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install & Test Protobuf Compiler + if: matrix.os != 'windows-latest' + run: | + pip3 install protobuf stringcase + python3 .github/scripts/protoc_gen_deps.py + + - name: Install & Test Protobuf Compiler (win) + if: matrix.os == 'windows-latest' + run: | + Set-Alias -Name "python3" -Value "python" + pip3 install protobuf stringcase + python3 .github/scripts/protoc_gen_deps.py + + - name: Run Tests + if: matrix.os != 'windows-latest' + run: ./gradlew rsdroid:test -x jar -x cargoBuildArm -x cargoBuildX86 -x cargoBuildArm64 -x cargoBuildX86_64 + + - name: Run Tests (win) + if: matrix.os == 'windows-latest' + run: ./gradlew rsdroid:test -x jar -x cargoBuildArm -x cargoBuildX86 -x cargoBuildArm64 -x cargoBuildX86_64 diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index 62f9a5955..39a3ecd09 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -15,96 +15,95 @@ jobs: timeout-minutes: 80 runs-on: windows-latest steps: + - uses: actions/checkout@v2 - - uses: actions/checkout@v2 + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 + # TODO: Ignore lines like we do in the Unix scripts, rather than all of them + - name: Install NDK (silent) + run: | + Write-Host "NDK Install Started" + Write-Host "ANDROID_HOME - $Env:ANDROID_HOME" + Write-Host "ANDROID_SDK_ROOT - $Env:ANDROID_SDK_ROOT" + (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null + Write-Host "NDK Install Completed" - # TODO: Ignore lines like we do in the Unix scripts, rather than all of them - - name: Install NDK (silent) - run: | - Write-Host "NDK Install Started" - Write-Host "ANDROID_HOME - $Env:ANDROID_HOME" - Write-Host "ANDROID_SDK_ROOT - $Env:ANDROID_SDK_ROOT" - (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null - Write-Host "NDK Install Completed" + # bzip2-sys does not build when the value of CC_armv7-linux-androideabi contains a space + # Setting this variable is a pain, as it contains a dash + # It's easier to just move the directory and fix the environment variables to point to the new NDK + # we change Program Files (x86) to "ProgramFiles" + # TODO: Move the setting of environment variables here (permanently) + - name: Move SDK to directory with no spaces + run: | + New-Item -ItemType directory -Path 'C:\ProgramFiles\Android' + Get-ChildItem "$Env:ANDROID_SDK_ROOT" + Move-Item -Path "$Env:ANDROID_SDK_ROOT" -Destination 'C:\ProgramFiles\Android' -ErrorAction Stop -Force + Get-ChildItem 'C:\ProgramFiles\Android\android-sdk' - # bzip2-sys does not build when the value of CC_armv7-linux-androideabi contains a space - # Setting this variable is a pain, as it contains a dash - # It's easier to just move the directory and fix the environment variables to point to the new NDK - # we change Program Files (x86) to "ProgramFiles" - # TODO: Move the setting of environment variables here (permanently) - - name: Move SDK to directory with no spaces - run: | - New-Item -ItemType directory -Path 'C:\ProgramFiles\Android' - Get-ChildItem "$Env:ANDROID_SDK_ROOT" - Move-Item -Path "$Env:ANDROID_SDK_ROOT" -Destination 'C:\ProgramFiles\Android' -ErrorAction Stop -Force - Get-ChildItem 'C:\ProgramFiles\Android\android-sdk' + - name: Debug Env + run: | + $Env:ANDROID_HOME - - name: Debug Env - run: | - $Env:ANDROID_HOME - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + - name: Fetch submodules + run: git submodule update --init --recursive --remote --force - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.58.1 - override: true - components: rustfmt + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 4 targets. - - name: Install Rust Targets - run: | - rustup target add armv7-linux-androideabi - rustup target add i686-linux-android - rustup target add aarch64-linux-android - rustup target add x86_64-linux-android + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 4 targets. + - name: Install Rust Targets + run: | + rustup target add armv7-linux-androideabi + rustup target add i686-linux-android + rustup target add aarch64-linux-android + rustup target add x86_64-linux-android - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Install & Test Protobuf - run: | - Set-Alias -Name "python3" -Value "python" - pip3 install protobuf - python3 .github/scripts/protoc_gen_deps.py + - name: Install & Test Protobuf + run: | + Set-Alias -Name "python3" -Value "python" + pip3 install protobuf stringcase + python3 .github/scripts/protoc_gen_deps.py - - name: Rust Cache - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - rslib-bridge/target - key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-rust-v1-assembleRelease - ${{ runner.os }}-rust-v1 + - name: Rust Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rslib-bridge/target + key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-rust-v1-assembleRelease + ${{ runner.os }}-rust-v1 - # -Dorg.gradle.daemon=false has been removed - # -Dorg.gradle.console=plain has been removed - # env: - # ANDROID_HOME: "C:\\ProgramFiles\\Android\\android-sdk" - # ANDROID_NDK_HOME: "C:\\ProgramFiles\\Android\\android-sdk\\ndk-bundle" - # ANDROID_NDK_PATH: "C:\\ProgramFiles\\Android\\android-sdk\\ndk-bundle" - # ANDROID_SDK_ROOT: "C:\\ProgramFiles\\Android\\android-sdk" - - name: Build - run: | - $Env:ANDROID_HOME = "C:\ProgramFiles\Android\android-sdk" - $Env:ANDROID_NDK_HOME = "C:\ProgramFiles\Android\android-sdk\ndk-bundle" - $Env:ANDROID_NDK_PATH = "C:\ProgramFiles\Android\android-sdk\ndk-bundle" - $Env:ANDROID_SDK_ROOT = "C:\ProgramFiles\Android\android-sdk" - $env:ANDROID_HOME - ./gradlew clean assembleRelease -DtestBuildType=release # assembleAndroidTest + # -Dorg.gradle.daemon=false has been removed + # -Dorg.gradle.console=plain has been removed + # env: + # ANDROID_HOME: "C:\\ProgramFiles\\Android\\android-sdk" + # ANDROID_NDK_HOME: "C:\\ProgramFiles\\Android\\android-sdk\\ndk-bundle" + # ANDROID_NDK_PATH: "C:\\ProgramFiles\\Android\\android-sdk\\ndk-bundle" + # ANDROID_SDK_ROOT: "C:\\ProgramFiles\\Android\\android-sdk" + - name: Build + run: | + $Env:ANDROID_HOME = "C:\ProgramFiles\Android\android-sdk" + $Env:ANDROID_NDK_HOME = "C:\ProgramFiles\Android\android-sdk\ndk-bundle" + $Env:ANDROID_NDK_PATH = "C:\ProgramFiles\Android\android-sdk\ndk-bundle" + $Env:ANDROID_SDK_ROOT = "C:\ProgramFiles\Android\android-sdk" + $env:ANDROID_HOME + ./gradlew clean assembleRelease -DtestBuildType=release # assembleAndroidTest diff --git a/.github/workflows/windows_pure_build.yml b/.github/workflows/windows_pure_build.yml index 7c3d99d7a..e30f06400 100644 --- a/.github/workflows/windows_pure_build.yml +++ b/.github/workflows/windows_pure_build.yml @@ -11,61 +11,60 @@ jobs: build: runs-on: windows-latest steps: - - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + - uses: actions/checkout@v2 - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 + - name: Fetch submodules + run: git submodule update --init --recursive --remote --force - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 - - name: Install NDK (Windows - silent) - if: matrix.os == 'windows-latest' - run: | - Write-Host "NDK Install Started" - (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null - Write-Host "NDK Install Completed" + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.58.1 - override: true - components: rustfmt + - name: Install NDK (Windows - silent) + if: matrix.os == 'windows-latest' + run: | + Write-Host "NDK Install Started" + (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null + Write-Host "NDK Install Completed" - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 4 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_targets.sh + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt - - name: Install Protoc - uses: arduino/setup-protoc@v1 - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 4 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_targets.sh - - name: Install & Test Protobuf - run: | - Set-Alias -Name "python3" -Value "python" - pip3 install protobuf - python3 .github/scripts/protoc_gen_deps.py + - name: Install Protoc + uses: arduino/setup-protoc@v1 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - - name: Rust Cache - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - rslib-bridge/target - key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} - restore-keys: | - ${{ runner.os }}-rust-v1-assembleRelease - ${{ runner.os }}-rust-v1 + - name: Install & Test Protobuf + run: | + Set-Alias -Name "python3" -Value "python" + pip3 install protobuf stringcase + python3 .github/scripts/protoc_gen_deps.py - - name: Build - run: ./gradlew clean assembleRelease -DtestBuildType=release \ No newline at end of file + - name: Rust Cache + uses: actions/cache@v2 + with: + path: | + ~/.cargo/registry + ~/.cargo/git + rslib-bridge/target + key: ${{ runner.os }}-rust-v1-assembleRelease-${{ hashFiles('rslib-bridge/**/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-rust-v1-assembleRelease + ${{ runner.os }}-rust-v1 + + - name: Build + run: ./gradlew clean assembleRelease -DtestBuildType=release diff --git a/docs/ENVIRONMENT.md b/docs/ENVIRONMENT.md index d7932d0e6..e4dda9e5b 100644 --- a/docs/ENVIRONMENT.md +++ b/docs/ENVIRONMENT.md @@ -114,12 +114,13 @@ cargo install cross --git https://github.com/rust-embedded/cross --tag v0.2.1 - macOS this should be `brew install python` - linux you may need to make sure `python` exists instead of just `python3`, for example `sudo apt install python-is-python3` -#### Install protobuf package +#### Install python packages ```bash -pip install protobuf +pip install protobuf stringcase ``` + ## Make sure it works Basic commands to build and test things may be taken from the `./github/workflows` scripts. diff --git a/docs/easy-testing.md b/docs/easy-testing.md index fd7989768..f29117796 100644 --- a/docs/easy-testing.md +++ b/docs/easy-testing.md @@ -20,7 +20,10 @@ Install Rust: Install protobuf: - Install protobuf with your package manager -- pip install protobuf, or see the venv section below + +Install Python packages + +- pip install protobuf stringcase, or see the venv section below ## Limit build to x86_64 From 93e90e82184b46fabf2bcc90336d3a3fe370b10b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 16:44:51 +1000 Subject: [PATCH 11/61] Use the pinned submodule commit, not branch head --- .github/workflows/linux_build.yml | 2 +- .github/workflows/macos_build.yml | 4 +- .github/workflows/publish_library.yml | 2 +- .github/workflows/publish_testing.yml | 162 +++++++++++------------ .github/workflows/robolectric_build.yml | 4 +- .github/workflows/windows_build.yml | 2 +- .github/workflows/windows_pure_build.yml | 2 +- 7 files changed, 89 insertions(+), 89 deletions(-) diff --git a/.github/workflows/linux_build.yml b/.github/workflows/linux_build.yml index fb8ba9eec..a764590b5 100644 --- a/.github/workflows/linux_build.yml +++ b/.github/workflows/linux_build.yml @@ -18,7 +18,7 @@ jobs: - uses: actions/checkout@v2 - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + run: git submodule update --init --recursive - name: Configure JDK 1.11 uses: actions/setup-java@v3 diff --git a/.github/workflows/macos_build.yml b/.github/workflows/macos_build.yml index e48de01dc..c37af7774 100644 --- a/.github/workflows/macos_build.yml +++ b/.github/workflows/macos_build.yml @@ -19,7 +19,7 @@ jobs: - uses: actions/checkout@v2 - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + run: git submodule update --init --recursive - name: Configure JDK 1.11 uses: actions/setup-java@v3 @@ -97,7 +97,7 @@ jobs: # COULD_BE_BETTER: This may not be needed - tiny speed penalty - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + run: git submodule update --init --recursive - name: Download APKs uses: actions/download-artifact@v2 diff --git a/.github/workflows/publish_library.yml b/.github/workflows/publish_library.yml index 6179e6b1f..f3aae0aee 100644 --- a/.github/workflows/publish_library.yml +++ b/.github/workflows/publish_library.yml @@ -12,7 +12,7 @@ jobs: - uses: actions/checkout@v2 - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + run: git submodule update --init --recursive - name: Configure JDK 1.11 uses: actions/setup-java@v3 diff --git a/.github/workflows/publish_testing.yml b/.github/workflows/publish_testing.yml index 2818c9966..6009fcfe2 100644 --- a/.github/workflows/publish_testing.yml +++ b/.github/workflows/publish_testing.yml @@ -10,84 +10,84 @@ jobs: publish: runs-on: macos-latest steps: - - uses: actions/checkout@v2 - - - name: Fetch submodules - run: git submodule update --init --recursive --remote --force - - - name: Configure JDK 1.11 - uses: actions/setup-java@v3 - with: - distribution: "adopt" - java-version: "11" # minimum for Android API31 - - - name: Install Android Command Line Tools - uses: android-actions/setup-android@v2 - - - name: Install NDK - run: .github/scripts/install_ndk.sh 22.0.7026061 - - # install cargo - - name: Install Rust - uses: actions-rs/toolchain@v1.0.6 - with: - toolchain: 1.58.1 - override: true - components: rustfmt - - - name: Install Python libs - run: | - pip3 install stringcase - - # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - - name: Install Rust Targets - run: .github/scripts/install_rust_robolectric_targets.sh - - - name: Install x86_64-w64-mingw32-gcc - run: brew install mingw-w64 && x86_64-w64-mingw32-gcc -v - - - name: Install x86_64-unknown-linux-gnu - run: | - brew tap SergioBenitez/osxct - brew install x86_64-unknown-linux-gnu - x86_64-unknown-linux-gnu-gcc -v - - - name: Build JAR - run: | - export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc - export ANKIDROID_MACOS_CC=cc - export RUST_DEBUG=1 - export RUST_BACKTRACE=1 - export RUST_LOG=trace - export NO_CROSS=true - ./gradlew clean rsdroid-testing:build -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: Check Compiled Libraries - run: > - cd rsdroid-testing/assets && - ../../.github/scripts/check_robolectric_assets.sh - - - name: Upload rsdroid-testing JAR as artifact - uses: actions/upload-artifact@v2 - with: - name: rsdroid-testing - if-no-files-found: error - path: rsdroid-testing/build/libs - - - name: Publish JAR to Maven - env: - ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} - ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} - SONATYPE_NEXUS_USERNAME: david-allison-1 - SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} - run: | - export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc - export ANKIDROID_MACOS_CC=cc - export RUST_DEBUG=1 - export RUST_BACKTRACE=1 - export RUST_LOG=trace - export NO_CROSS=true - ./gradlew rsdroid-testing:uploadArchives -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain - - - name: ℹ️Additional Release Instructions (requires human interaction) - run: echo "Sign in to https://oss.sonatype.org/#stagingRepositories , close the repsository, then release it" \ No newline at end of file + - uses: actions/checkout@v2 + + - name: Fetch submodules + run: git submodule update --init --recursive + + - name: Configure JDK 1.11 + uses: actions/setup-java@v3 + with: + distribution: "adopt" + java-version: "11" # minimum for Android API31 + + - name: Install Android Command Line Tools + uses: android-actions/setup-android@v2 + + - name: Install NDK + run: .github/scripts/install_ndk.sh 22.0.7026061 + + # install cargo + - name: Install Rust + uses: actions-rs/toolchain@v1.0.6 + with: + toolchain: 1.58.1 + override: true + components: rustfmt + + - name: Install Python libs + run: | + pip3 install stringcase + + # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. + - name: Install Rust Targets + run: .github/scripts/install_rust_robolectric_targets.sh + + - name: Install x86_64-w64-mingw32-gcc + run: brew install mingw-w64 && x86_64-w64-mingw32-gcc -v + + - name: Install x86_64-unknown-linux-gnu + run: | + brew tap SergioBenitez/osxct + brew install x86_64-unknown-linux-gnu + x86_64-unknown-linux-gnu-gcc -v + + - name: Build JAR + run: | + export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc + export ANKIDROID_MACOS_CC=cc + export RUST_DEBUG=1 + export RUST_BACKTRACE=1 + export RUST_LOG=trace + export NO_CROSS=true + ./gradlew clean rsdroid-testing:build -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: Check Compiled Libraries + run: > + cd rsdroid-testing/assets && + ../../.github/scripts/check_robolectric_assets.sh + + - name: Upload rsdroid-testing JAR as artifact + uses: actions/upload-artifact@v2 + with: + name: rsdroid-testing + if-no-files-found: error + path: rsdroid-testing/build/libs + + - name: Publish JAR to Maven + env: + ORG_GRADLE_PROJECT_SIGNING_PRIVATE_KEY: ${{ secrets.SIGNING_PRIVATE_KEY }} + ORG_GRADLE_PROJECT_SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} + SONATYPE_NEXUS_USERNAME: david-allison-1 + SONATYPE_NEXUS_PASSWORD: ${{ secrets.SONATYPE_NEXUS_PASSWORD }} + run: | + export ANKIDROID_LINUX_CC=x86_64-unknown-linux-gnu-gcc + export ANKIDROID_MACOS_CC=cc + export RUST_DEBUG=1 + export RUST_BACKTRACE=1 + export RUST_LOG=trace + export NO_CROSS=true + ./gradlew rsdroid-testing:uploadArchives -Dorg.gradle.project.macCC=$ANKIDROID_MACOS_CC -DtestBuildType=debug -Dorg.gradle.daemon=false -Dorg.gradle.console=plain + + - name: ℹ️Additional Release Instructions (requires human interaction) + run: echo "Sign in to https://oss.sonatype.org/#stagingRepositories , close the repsository, then release it" diff --git a/.github/workflows/robolectric_build.yml b/.github/workflows/robolectric_build.yml index 258e534f5..1ed4a98d9 100644 --- a/.github/workflows/robolectric_build.yml +++ b/.github/workflows/robolectric_build.yml @@ -28,7 +28,7 @@ jobs: - uses: actions/checkout@v2 - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + run: git submodule update --init --recursive - name: Configure JDK 1.11 uses: actions/setup-java@v3 @@ -126,7 +126,7 @@ jobs: - uses: actions/checkout@v2 - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + run: git submodule update --init --recursive - uses: actions/download-artifact@v2 name: Download Artifact diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index 39a3ecd09..9d1d76a1f 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -52,7 +52,7 @@ jobs: $Env:ANDROID_HOME - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + run: git submodule update --init --recursive - name: Install Rust uses: actions-rs/toolchain@v1.0.6 diff --git a/.github/workflows/windows_pure_build.yml b/.github/workflows/windows_pure_build.yml index e30f06400..ee98bb1f0 100644 --- a/.github/workflows/windows_pure_build.yml +++ b/.github/workflows/windows_pure_build.yml @@ -14,7 +14,7 @@ jobs: - uses: actions/checkout@v2 - name: Fetch submodules - run: git submodule update --init --recursive --remote --force + run: git submodule update --init --recursive - name: Configure JDK 1.11 uses: actions/setup-java@v3 From c8111eea967a32a266c5941b04449828a43b830f Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 18:56:34 +1000 Subject: [PATCH 12/61] Remove fluent.proto upload/download step; some tweaks to the docs --- .github/workflows/robolectric_build.yml | 21 ---------- README.md | 42 ++++++++++--------- docs/OVERVIEW.md | 54 ++++++++++++------------- 3 files changed, 48 insertions(+), 69 deletions(-) diff --git a/.github/workflows/robolectric_build.yml b/.github/workflows/robolectric_build.yml index 1ed4a98d9..9f5e208ca 100644 --- a/.github/workflows/robolectric_build.yml +++ b/.github/workflows/robolectric_build.yml @@ -107,13 +107,6 @@ jobs: if-no-files-found: error path: rsdroid-testing/build/libs/ - - name: Upload fluent.proto - uses: actions/upload-artifact@v2 - with: - name: anki-proto - if-no-files-found: error - path: rslib-bridge/anki/proto/ - test: needs: build runs-on: ${{matrix.os}} @@ -134,20 +127,6 @@ jobs: name: rsdroid-testing path: rsdroid-testing/build/libs - - uses: actions/download-artifact@v2 - name: Download fluent.proto - with: - name: anki-proto - path: rslib-bridge/anki/proto/ - - - name: Ensure fluent.proto exists - if: matrix.os != 'windows-latest' - run: | - if [ ! -f rslib-bridge/anki/proto/fluent.proto ]; then - echo "fluent.proto not generated" - exit 1 - fi - - name: Configure JDK 1.11 uses: actions/setup-java@v3 with: diff --git a/README.md b/README.md index 77414d348..0b7c37614 100644 --- a/README.md +++ b/README.md @@ -4,24 +4,28 @@ Adapter allowing AnkiDroid to leverage Anki Desktop's Rust-based business logic ## Why? -* Removes the need to port Anki Desktop's business logic to Java - * 100% compatibility and no bugs - * Rust should provide a speed increase - * An upgrade for AnkiDroid should only require moving to a later commit in a submodule - * Saves massive amount of AnkiDroid developer time & effort - * Allows Anki Desktop to iterate faster - * We can quickly port changes upstream, which will benefit the ecosystem -* Insulates Anki-Android users from the complexity of installing multiple toolchains - * The Rust/Python/cross-compilation toolchain is much more complex than downloading Android Studio - * A separate repository means we keep a low barrier to entry for new contributors +- Removes the need to port Anki Desktop's business logic to Java + - 100% compatibility and no bugs + - Rust should provide a speed increase + - An upgrade for AnkiDroid should only require moving to a later commit in a submodule + - Saves massive amount of AnkiDroid developer time & effort + - Allows Anki Desktop to iterate faster + - We can quickly port changes upstream, which will benefit the ecosystem +- Insulates Anki-Android users from the complexity of installing multiple toolchains + - The Rust/Python/cross-compilation toolchain is much more complex than downloading Android Studio + - A separate repository means we keep a low barrier to entry for new contributors ## How to use it in a project +Pre-built version: + ```gradle implementation "io.github.david-allison-1:anki-android-backend:0.1.11" testImplementation "io.github.david-allison-1:anki-android-backend-testing:0.1.11" ``` +See ./docs for info on building a version for testing. + ## Folders `/anki/` - git submodule containing the Anki Rust Codebase, used both for building into `.so` files, and to obtain the current `.proto` files for use in Java codegen @@ -40,16 +44,14 @@ This is defined as an application to allow instrumented tests to be run against ## Implementation -* Points to a fixed commit of `anki` - * Currently as a fork in `david-allison-1/anki` - * Modified to use a submodule for translations so we have a reproducible build - * Modifications to the library so we do not need to update to database schema 15 for version 1 -* References `backend.proto` and `fluent.proto` which define RPC service calls to the anki backend -* Python script to auto-generate the Java interface/backend to the RPC mechanism. Invoked via gradle. -* Android Library which contains the rust based `.so` under (x86, x86-64, arm, arm64) - * Implements `android.database.sqlite`, redirecting SQL to the rust library - * Exposes RPC calls to Rust via a clean Java interface (`net.ankiweb.rsdroid.RustBackend`) -* Testing library to allow the above to be usable under Robolectric +- Points to a fixed commit of `ankidroid/anki` + - Modifications to the library so we do not need to update to database schema 15 for version 1 +- References `rslib-bridge/anki/proto/anki/*.proto` which define RPC service calls to the anki backend +- Python script to auto-generate the Java interface/backend to the RPC mechanism. Invoked via gradle. +- Android Library which contains the rust based `.so` under (x86, x86-64, arm, arm64) + - Implements `android.database.sqlite`, redirecting SQL to the rust library + - Exposes RPC calls to Rust via a clean Java interface (`net.ankiweb.rsdroid.Backend`) +- Testing library to allow the above to be usable under Robolectric ## Additional Information diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index dcac172c0..3aff168b3 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -6,8 +6,8 @@ Anki-Android-Backend uses Rust, Java and a little Python. Since setting up a Rus There are two aspects of this library: -* `rsdroid.so` - A rust library which contains `anki/rslib` and a JNI bridge (`rslib-bridge`) -* rsdroid.aar - A java library with `rsdroid.so` and handles command processing plus acts as an adapter for database access +- `rsdroid.so` - A rust library which contains `anki/rslib` and a JNI bridge (`rslib-bridge`) +- rsdroid.aar - A java library with `rsdroid.so` and handles command processing plus acts as an adapter for database access ### Protocol Buffers @@ -15,8 +15,6 @@ Serialisation over the JNI boundary happens mostly via **Protocol Buffers**. The source files are stored in `anki/proto`. We perform RPC over JNI, rather than depending on HTTP. -`fluent.proto` is a special-case, and is generated when the Rust library is built (`anki/rslib/build.rs`) - Protobuf service generation is handled on the rust side via `anki/rslib/build.rs` and on the java side via `tools/protoc-gen/protoc-gen.py`. ### Main usages @@ -45,9 +43,9 @@ There is a small amount of code in the consuming app Anki-Android to use this li Java/Rust interface to the backend. `RustDroidBackend` wraps the implementation of the Rust. -* Allows a testable comparison between the Java and the Rust during the conversion - * Allows a pure Java conversion afterwards (if deemed appropriate due to AGPL concerns) -* Encapsulates all access to the Rust, allowing the implementation to later be swapped out within one file. +- Allows a testable comparison between the Java and the Rust during the conversion + - Allows a pure Java conversion afterwards (if deemed appropriate due to AGPL concerns) +- Encapsulates all access to the Rust, allowing the implementation to later be swapped out within one file. ### Anki-Android-Backend @@ -69,17 +67,17 @@ It contains a method per RPC method defined in `backend.proto` It is responsible for: -* Converting parameters from Java types to protobufs -* Executing a command -* Ensuring that the result was not an error -* Deserializing and returning data (if applicable) +- Converting parameters from Java types to protobufs +- Executing a command +- Ensuring that the result was not an error +- Deserializing and returning data (if applicable) ##### RustBackendImpl Example RustBackendImpl is generated by `gen/protoc-gen/protoc-gen.py` and is not checked into source control. ```java - public Backend.SearchCardsOut searchCards(@Nullable java.lang.String search, @Nullable Backend.SortOrder order) { + public Backend.SearchCardsOut searchCards(@Nullable java.lang.String search, @Nullable Backend.SortOrder order) { byte[] result = null; try { Backend.SearchCardsIn.Builder builder = Backend.SearchCardsIn.newBuilder(); @@ -105,10 +103,10 @@ BackendV1Impl extends `RustBackendImpl`. It is responsible for: -* Maintaining a pointer to the collection (to be passed into the Rust to identify the collection) -* Accessing methods in the Rust which are not generated from Protobufs - * JSON serialization for database inputs - * Collection opening/closing +- Maintaining a pointer to the collection (to be passed into the Rust to identify the collection) +- Accessing methods in the Rust which are not generated from Protobufs + - JSON serialization for database inputs + - Collection opening/closing #### NativeMethods @@ -122,19 +120,19 @@ All public methods callable from the Java are available here. Responsible for: -* Handling JNI specific concerns (conversions from Java to Rust primitives) -* Calling methods in `anki/rslib` -* Defining the interface callable by `NativeMethods` -* Converting a provided `backendPointer` into a Collection object -* Serialization of outputs and deserialization of inputs -* Handling panics and converting them to errors +- Handling JNI specific concerns (conversions from Java to Rust primitives) +- Calling methods in `anki/rslib` +- Defining the interface callable by `NativeMethods` +- Converting a provided `backendPointer` into a Collection object +- Serialization of outputs and deserialization of inputs +- Handling panics and converting them to errors ## Database Access We need to use the Rust for database access as: -* We need an open collection to perform most commands in rslib -* An open collection obtains a lock on the database - access can only be made through the Rust. +- We need an open collection to perform most commands in rslib +- An open collection obtains a lock on the database - access can only be made through the Rust. So, we implement `SupportSQLiteOpenHelper.Factory` and related classes. @@ -144,10 +142,10 @@ Anki's rust code does not stream database results, all results are currently obt This is not a significant problem, as: -* Rust is not confined by the Java heap limit -* Most results are small -* In time, we will move most data processing to the Rust, removing the need to deserialize data -* Java has been converted to use protobuf serialization (vs Anki Desktop using JSON), this significantly reduces memory usage. +- Rust is not confined by the Java heap limit +- Most results are small +- In time, we will move most data processing to the Rust, removing the need to deserialize data +- Java has been converted to use protobuf serialization (vs Anki Desktop using JSON), this significantly reduces memory usage. #### LimitOffsetSQLiteCursor From c938d3ec952b5a0e502c5cc0bbe889cf1dd2ee12 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 20:03:32 +1000 Subject: [PATCH 13/61] Install+upgrade protobuf --- .github/workflows/linux_build.yml | 2 +- .github/workflows/macos_build.yml | 2 +- .github/workflows/publish_library.yml | 2 +- .github/workflows/publish_testing.yml | 2 +- .github/workflows/robolectric_build.yml | 4 ++-- .github/workflows/windows_build.yml | 2 +- .github/workflows/windows_pure_build.yml | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/.github/workflows/linux_build.yml b/.github/workflows/linux_build.yml index a764590b5..0ee8baf07 100644 --- a/.github/workflows/linux_build.yml +++ b/.github/workflows/linux_build.yml @@ -56,7 +56,7 @@ jobs: - name: Install Python libs run: | - pip3 install stringcase + pip3 install --upgrade protobuf stringcase - name: Rust Cache uses: actions/cache@v2 diff --git a/.github/workflows/macos_build.yml b/.github/workflows/macos_build.yml index c37af7774..4093b4f52 100644 --- a/.github/workflows/macos_build.yml +++ b/.github/workflows/macos_build.yml @@ -54,7 +54,7 @@ jobs: - name: Install & Test Protobuf Compiler run: | - pip3 install protobuf stringcase + pip3 install --upgrade protobuf stringcase python3 .github/scripts/protoc_gen_deps.py - name: Rust Cache diff --git a/.github/workflows/publish_library.yml b/.github/workflows/publish_library.yml index f3aae0aee..63bfaf1f7 100644 --- a/.github/workflows/publish_library.yml +++ b/.github/workflows/publish_library.yml @@ -48,7 +48,7 @@ jobs: - name: Install Python libs run: | - pip3 install stringcase + pip3 install --upgrade protobuf stringcase - name: Build run: ./gradlew clean assembleRelease -DtestBuildType=release -Dorg.gradle.daemon=false -Dorg.gradle.console=plain diff --git a/.github/workflows/publish_testing.yml b/.github/workflows/publish_testing.yml index 6009fcfe2..721f1b2d0 100644 --- a/.github/workflows/publish_testing.yml +++ b/.github/workflows/publish_testing.yml @@ -37,7 +37,7 @@ jobs: - name: Install Python libs run: | - pip3 install stringcase + pip3 install --upgrade protobuf stringcase # actions-rs only accepts "target" (although a "targets" param to be added in v2). We need 7 targets. - name: Install Rust Targets diff --git a/.github/workflows/robolectric_build.yml b/.github/workflows/robolectric_build.yml index 9f5e208ca..f5027a595 100644 --- a/.github/workflows/robolectric_build.yml +++ b/.github/workflows/robolectric_build.yml @@ -159,14 +159,14 @@ jobs: - name: Install & Test Protobuf Compiler if: matrix.os != 'windows-latest' run: | - pip3 install protobuf stringcase + pip3 install --upgrade protobuf stringcase python3 .github/scripts/protoc_gen_deps.py - name: Install & Test Protobuf Compiler (win) if: matrix.os == 'windows-latest' run: | Set-Alias -Name "python3" -Value "python" - pip3 install protobuf stringcase + pip3 install --upgrade protobuf stringcase python3 .github/scripts/protoc_gen_deps.py - name: Run Tests diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index 9d1d76a1f..57d79c063 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -77,7 +77,7 @@ jobs: - name: Install & Test Protobuf run: | Set-Alias -Name "python3" -Value "python" - pip3 install protobuf stringcase + pip3 install --upgrade protobuf stringcase python3 .github/scripts/protoc_gen_deps.py - name: Rust Cache diff --git a/.github/workflows/windows_pure_build.yml b/.github/workflows/windows_pure_build.yml index ee98bb1f0..56b08048e 100644 --- a/.github/workflows/windows_pure_build.yml +++ b/.github/workflows/windows_pure_build.yml @@ -51,7 +51,7 @@ jobs: - name: Install & Test Protobuf run: | Set-Alias -Name "python3" -Value "python" - pip3 install protobuf stringcase + pip3 install --upgrade protobuf stringcase python3 .github/scripts/protoc_gen_deps.py - name: Rust Cache From 3476441dfe57f193c1e84f5609a8ff50ac166052 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 20:50:55 +1000 Subject: [PATCH 14/61] Update rslib commit --- rslib-bridge/anki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib-bridge/anki b/rslib-bridge/anki index 50d0af6ac..665d81c48 160000 --- a/rslib-bridge/anki +++ b/rslib-bridge/anki @@ -1 +1 @@ -Subproject commit 50d0af6ac11c770332390928ef088a8fe33db02c +Subproject commit 665d81c48001290cb4c6d2ce203afa83d923f34f From fc2e373406a6187712a41ed3b8f46a49a1ca4033 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 21:27:40 +1000 Subject: [PATCH 15/61] Setup linker symlink on Linux roboelectric test --- .github/workflows/robolectric_build.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/robolectric_build.yml b/.github/workflows/robolectric_build.yml index f5027a595..db218d373 100644 --- a/.github/workflows/robolectric_build.yml +++ b/.github/workflows/robolectric_build.yml @@ -147,6 +147,10 @@ jobs: (. sdkmanager.bat --install "ndk;22.0.7026061" --sdk_root="$Env:ANDROID_SDK_ROOT") | out-null Write-Host "NDK Install Completed" + - name: Install linker + if: matrix.os == 'ubuntu-latest' + run: .github/scripts/linux_install_x86_64-unknown-linux-gnu-gcc.sh + - name: Install Python Setuptools if: matrix.os == 'ubuntu-latest' run: sudo apt-get install python3-setuptools From 1408efda71513aca915bf15f571d6c0aac9c460c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 16 Jun 2022 21:31:01 +1000 Subject: [PATCH 16/61] Bump version number --- README.md | 4 ++-- docs/easy-testing.md | 4 ++-- gradle.properties | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 0b7c37614..f1cc70368 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ Adapter allowing AnkiDroid to leverage Anki Desktop's Rust-based business logic Pre-built version: ```gradle - implementation "io.github.david-allison-1:anki-android-backend:0.1.11" - testImplementation "io.github.david-allison-1:anki-android-backend-testing:0.1.11" + implementation "io.github.david-allison-1:anki-android-backend:0.1.12" + testImplementation "io.github.david-allison-1:anki-android-backend-testing:0.1.12" ``` See ./docs for info on building a version for testing. diff --git a/docs/easy-testing.md b/docs/easy-testing.md index f29117796..7d5b246b6 100644 --- a/docs/easy-testing.md +++ b/docs/easy-testing.md @@ -109,11 +109,11 @@ index 2a2d94034..b21c7caff 100644 - implementation "io.github.david-allison-1:anki-android-backend:$ankidroid_backend_version" - testImplementation "io.github.david-allison-1:anki-android-backend-testing:$ankidroid_backend_version" - // implementation files("../../Anki-Android-Backend/rsdroid/build/outputs/aar/rsdroid-release.aar") -- // testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-0.1.11.jar") +- // testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-0.1.12.jar") + // implementation "io.github.david-allison-1:anki-android-backend:$ankidroid_backend_version" + // testImplementation "io.github.david-allison-1:anki-android-backend-testing:$ankidroid_backend_version" + implementation files("../../Anki-Android-Backend/rsdroid/build/outputs/aar/rsdroid-release.aar") -+ testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-0.1.11.jar") ++ testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-0.1.12.jar") // On Windows, you can use something like // implementation files("C:\\GitHub\\Rust-Test\\rsdroid\\build\\outputs\\aar\\rsdroid-release.aar") diff --git a/gradle.properties b/gradle.properties index 887c4f4c5..6695c3d75 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ android.useAndroidX=true android.enableJetifier=false GROUP=io.github.david-allison-1 -VERSION_NAME=0.1.11 +VERSION_NAME=0.1.12 POM_INCEPTION_YEAR=2020 From 10d6fa0ba2077fddb0704d8b6ff45230ab27f0ee Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 17 Jun 2022 11:42:27 +1000 Subject: [PATCH 17/61] Remove unused JsonDatabaseCursor --- .../database/AnkiJsonDatabaseCursor.kt | 166 ------------------ 1 file changed, 166 deletions(-) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.kt diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.kt deleted file mode 100644 index 0542d9815..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiJsonDatabaseCursor.kt +++ /dev/null @@ -1,166 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database - -import org.json.JSONArray -import org.json.JSONException - -abstract class AnkiJsonDatabaseCursor(private val backend: Session, protected val query: String, protected val bindArgs: Array) : AnkiDatabaseCursor() { - protected var results: JSONArray? = null - private var columnMapping: Array? = null - - // There's no need to close this cursor. - override fun isClosed(): Boolean { - return true - } - - override fun getColumnCount(): Int { - return if (results!!.length() == 0) { - 0 - } else { - try { - results!!.getJSONArray(0).length() - } catch (e: JSONException) { - 0 - } - } - } - - override fun getString(columnIndex: Int): String? { - return try { - rowAtCurrentPosition.getString(columnIndex) - } catch (e: Exception) { - throw RuntimeException(e) - } - } - - override fun getShort(columnIndex: Int): Short { - return try { - rowAtCurrentPosition.getInt(columnIndex).toShort() - } catch (e: Exception) { - throw RuntimeException(e) - } - } - - override fun getInt(columnIndex: Int): Int { - return try { - val rowAtCurrentPosition = rowAtCurrentPosition - if (rowAtCurrentPosition.isNull(columnIndex)) { - 0 - } else rowAtCurrentPosition.getInt(columnIndex) - } catch (e: Exception) { - throw RuntimeException(e) - } - } - - override fun getLong(columnIndex: Int): Long { - return try { - val rowAtCurrentPosition = rowAtCurrentPosition - if (rowAtCurrentPosition.isNull(columnIndex)) { - 0 - } else rowAtCurrentPosition.getLong(columnIndex) - } catch (e: Exception) { - throw RuntimeException(e) - } - } - - override fun getFloat(columnIndex: Int): Float { - return try { - val rowAtCurrentPosition = rowAtCurrentPosition - if (rowAtCurrentPosition.isNull(columnIndex)) { - 0.0f - } else rowAtCurrentPosition.getDouble(columnIndex).toFloat() - } catch (e: Exception) { - throw RuntimeException(e) - } - } - - override fun getDouble(columnIndex: Int): Double { - return try { - if (rowAtCurrentPosition.isNull(columnIndex)) { - 0.0 - } else { - rowAtCurrentPosition.getDouble(columnIndex) - } - } catch (e: Exception) { - throw RuntimeException(e) - } - } - - override fun isNull(columnIndex: Int): Boolean { - return try { - rowAtCurrentPosition.isNull(columnIndex) - } catch (e: Exception) { - throw RuntimeException(e) - } - } - - override fun getColumnIndex(columnName: String): Int { - try { - val names = columnNames - for (i in names.indices) { - if (columnName == names[i]) { - return i - } - } - } catch (e: Exception) { - return -1 - } - return -1 - } - - @Throws(IllegalArgumentException::class) - override fun getColumnIndexOrThrow(columnName: String): Int { - try { - val names = columnNames - for (i in names.indices) { - if (columnName == names[i]) { - return i - } - } - } catch (e: Exception) { - throw IllegalArgumentException(e) - } - throw IllegalArgumentException(String.format("Could not find column '%s'", columnName)) - } - - override fun getColumnName(columnIndex: Int): String { - return columnNamesInternal[columnIndex] - } - - override fun getColumnNames(): Array { - return columnNamesInternal - } - - private val columnNamesInternal: Array - private get() { - if (columnMapping == null) { - columnMapping = backend.getColumnNames(query) - checkNotNull(columnMapping) { "unable to obtain column mapping" } - } - return columnMapping!! - } - - @get:Throws(JSONException::class) - protected abstract val rowAtCurrentPosition: JSONArray - protected fun fullQuery(query: String, bindArgs: Array?): JSONArray { - return backend.fullQuery(query, bindArgs) - } - - override fun getType(columnIndex: Int): Int { - throw NotImplementedException() - } -} \ No newline at end of file From cfd268562a8e3fab4d23f7541eca7ca97da2df0e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 17 Jun 2022 12:51:29 +1000 Subject: [PATCH 18/61] Fix warnings and some lints --- .../net/ankiweb/rsdroid/ExceptionTest.java | 2 +- .../main/java/net/ankiweb/rsdroid/Backend.kt | 13 +++++---- .../net/ankiweb/rsdroid/BackendException.kt | 28 +++++++++++++------ .../net/ankiweb/rsdroid/BackendFactory.kt | 3 +- .../net/ankiweb/rsdroid/BackendFatalError.kt | 27 ------------------ .../java/net/ankiweb/rsdroid/NativeMethods.kt | 1 + .../rsdroid/RustBackendFailedException.kt | 4 +-- .../java/net/ankiweb/rsdroid/RustCleanup.kt | 5 +--- .../ankiweb/rsdroid/RustCleanupCollection.kt | 5 +--- .../java/net/ankiweb/rsdroid/Translations.kt | 5 ++-- .../rsdroid/database/AnkiDatabaseCursor.kt | 5 ++-- .../database/NotImplementedException.kt | 4 +-- .../rsdroid/database/RustSQLiteStatement.kt | 2 +- .../database/RustSupportSQLiteDatabase.kt | 22 +++++++-------- .../database/RustSupportSQLiteOpenHelper.kt | 2 +- .../RustV11SupportSQLiteOpenHelper.kt | 9 +++--- .../RustVNextSupportSQLiteOpenHelper.kt | 9 +++--- .../ankiweb/rsdroid/database/SQLHandler.kt | 1 + .../database/StreamingProtobufSQLiteCursor.kt | 16 +++++------ .../BackendInvalidInputException.kt | 1 - .../exceptions/BackendJsonException.kt | 3 +- tools/protoc-gen/protoc-gen.py | 2 ++ 22 files changed, 75 insertions(+), 94 deletions(-) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.kt diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java index 2bbd35c2f..430b874f6 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java @@ -77,7 +77,7 @@ public static java.util.Collection initParameters() { { BackendForTesting.ErrorType.NetworkError, BackendNetworkException.class}, { BackendForTesting.ErrorType.FilteredDeckError, BackendDeckIsFilteredException.class }, { BackendForTesting.ErrorType.Existing, BackendExistingException.class }, - { BackendForTesting.ErrorType.FatalError, BackendFatalError.class }, + { BackendForTesting.ErrorType.FatalError, BackendException.BackendFatalError.class }, { BackendForTesting.ErrorType.Interrupted, BackendInterruptedException.class}, { BackendForTesting.ErrorType.InvalidInput, BackendInvalidInputException.class}, { BackendForTesting.ErrorType.IoError, BackendIoException.class}, diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt index c52577542..c0ed1a9f4 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -88,7 +88,7 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), */ override fun openCollection(collectionPath: String, mediaFolderPath: String, mediaDbPath: String, logPath: String, forceSchema11: Boolean) { try { - super.openCollection(collectionPath, mediaFolderPath, mediaDbPath, logPath, forceSchema11) + super.openCollection(collectionPath, mediaFolderPath, mediaDbPath, logPath, forceSchema11) } catch (exc: BackendException.BackendDbException) { throw exc.toSQLiteException("db open") } @@ -164,10 +164,10 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), } @CheckResult - override fun fullQuery(sql: String, bindArgs: Array?): JSONArray { + override fun fullQuery(query: String, bindArgs: Array?): JSONArray { return try { - Timber.i("Rust: SQL query: '%s'", sql) - fullQueryInternal(sql, bindArgs) + Timber.i("Rust: SQL query: '%s'", query) + fullQueryInternal(query, bindArgs) } catch (e: JSONException) { throw RuntimeException(e) } @@ -216,8 +216,9 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), } @VisibleForTesting(otherwise = VisibleForTesting.NONE) - override fun setPageSize(pageSizeInBytes: Long) { - super.setPageSize(pageSizeInBytes) + @Suppress("PARAMETER_NAME_CHANGED_ON_OVERRIDE") + override fun setPageSize(pageSizeBytes: Long) { + super.setPageSize(pageSizeBytes) } override fun getColumnNames(sql: String): Array { diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.kt index 61e1582b2..eb0f44b74 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendException.kt @@ -36,23 +36,23 @@ open class BackendException : RuntimeException { error = null } - open fun toSQLiteException(query: String?): RuntimeException { + open fun toSQLiteException(query: String): RuntimeException { val message = String.format(Locale.ROOT, "error while compiling: \"%s\": %s", query, this.localizedMessage) return SQLiteException(message, this) } class BackendDbException(error: BackendError) : BackendException(error) { - override fun toSQLiteException(query: String?): RuntimeException { + override fun toSQLiteException(query: String): RuntimeException { val message = this.localizedMessage if (message == null) { val outMessage = String.format(Locale.ROOT, "Unknown error while compiling: \"%s\"", query) return SQLiteException(outMessage, this) } if (message.contains("InvalidParameterCount")) { - val p = Pattern.compile("InvalidParameterCount\\((\\d*), (\\d*)\\)").matcher(this.message) + val p = Pattern.compile("InvalidParameterCount\\((\\d*), (\\d*)\\)").matcher(message) if (p.find()) { - val givenParams = p.group(1).toInt() - val expectedParams = p.group(2).toInt() + val givenParams = p.group(1)!!.toInt() + val expectedParams = p.group(2)!!.toInt() val errorMessage = String.format(Locale.ROOT, "Cannot bind argument at index %d because the index is out of range. The statement has %d parameters.", givenParams, expectedParams) return IllegalArgumentException(errorMessage, this) } @@ -95,14 +95,17 @@ open class BackendException : RuntimeException { } } + class BackendSearchException(error: BackendError) : BackendException(error) + class BackendFatalError(error: BackendError) : BackendException(error) + companion object { fun fromError(error: BackendError): BackendException { - when (error.kind) { + when (error.kind!!) { BackendError.Kind.DB_ERROR -> return BackendDbException.fromDbError(error) BackendError.Kind.JSON_ERROR -> return BackendJsonException(error) BackendError.Kind.SYNC_AUTH_ERROR -> return BackendSyncAuthFailedException(error) BackendError.Kind.SYNC_OTHER_ERROR -> return BackendSyncException(error) - BackendError.Kind.FATAL_ERROR -> throw BackendFatalError(error.localized) + BackendError.Kind.FATAL_ERROR -> return BackendFatalError(error) BackendError.Kind.EXISTS -> return BackendExistingException(error) BackendError.Kind.FILTERED_DECK_ERROR -> return BackendDeckIsFilteredException(error) BackendError.Kind.INTERRUPTED -> return BackendInterruptedException(error) @@ -112,12 +115,19 @@ open class BackendException : RuntimeException { BackendError.Kind.NETWORK_ERROR -> return BackendNetworkException(error) BackendError.Kind.TEMPLATE_PARSE -> return BackendTemplateException.fromTemplateError(error) BackendError.Kind.IO_ERROR -> return BackendIoException(error) + BackendError.Kind.SEARCH_ERROR -> return BackendSearchException(error) + + BackendError.Kind.UNDO_EMPTY -> return BackendException(error) + BackendError.Kind.CUSTOM_STUDY_ERROR -> return BackendException(error) + BackendError.Kind.IMPORT_ERROR -> return BackendException(error) + BackendError.Kind.DELETED -> return BackendException(error) + BackendError.Kind.CARD_TYPE_ERROR -> return BackendException(error) + BackendError.Kind.UNRECOGNIZED -> return BackendException(error) } - return BackendException(error) } fun fromException(ex: Exception?): RuntimeException { return RuntimeException(ex) } } -} \ No newline at end of file +} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt index 23221e27d..279c13b58 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt @@ -20,6 +20,7 @@ import androidx.sqlite.db.SupportSQLiteOpenHelper abstract class BackendFactory // Force users to go through getInstance - for now we need to handle the backend failure protected constructor() { private var backend: Backend? = null + @Synchronized fun getBackend(): Backend { if (backend == null) { @@ -34,7 +35,7 @@ protected constructor() { @JvmStatic @RustCleanup("Use BackendV[11/Next]Factory") @Throws(RustBackendFailedException::class) - open fun createInstance(): BackendFactory { + fun createInstance(): BackendFactory { return BackendV11Factory.createInstance() } } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.kt deleted file mode 100644 index fa23fe535..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFatalError.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid - -/** A Java implementation of a rust "panic". - * - * rsdroid-bridge is not yet unwind-safe. If a panic occurs, we may be in an incoherent state - * - * But, we don't want the rust to panic. This causes a native exception, which will kill AnkiDroid - * before ACRA can send an exception report to the crash reporting server. - * - * This error delays the panic in a form that ACRA can catch, log, then crash more gracefully with. - */ -class BackendFatalError(message: String?) : Error(message) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt index acae2992c..862b6c13b 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt @@ -36,6 +36,7 @@ object NativeMethods { @CheckResult external fun runMethodRaw(backendPointer: Long, service: Int, method: Int, args: ByteArray?): Array? + @CheckResult external fun openBackend(data: ByteArray?): Array? external fun closeBackend(backendPointer: Long) diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.kt index d5d75266e..4cb0e3477 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.kt @@ -22,6 +22,6 @@ package net.ankiweb.rsdroid "Once we prove this to be incorrect (or fix the bugs), we could remove this and assume that" + "rsdroid will always load without issue") class RustBackendFailedException : Exception { - constructor(error: Throwable?) : super(error) {} - constructor(message: String?) : super(message) {} + constructor(error: Throwable?) : super(error) + constructor(message: String?) : super(message) } \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.kt index 7673ac26e..38950a4a3 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanup.kt @@ -15,16 +15,13 @@ */ package net.ankiweb.rsdroid -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - /** * Specifies that the provided class requires attention during the Rust conversion * These act as TODOs and should be audited before a production release is produced * After the Rust conversion is completed, this class should be deleted. */ @JvmRepeatable(RustCleanupCollection::class) -@Retention(RetentionPolicy.SOURCE) +@kotlin.annotation.Retention(AnnotationRetention.SOURCE) annotation class RustCleanup( /** Context and rationale for the cleanup, and the action which will be taken */ val value: String) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.kt index e84caf599..6a9f36e0f 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustCleanupCollection.kt @@ -15,12 +15,9 @@ */ package net.ankiweb.rsdroid -import java.lang.annotation.Retention -import java.lang.annotation.RetentionPolicy - /** A collection of RustCleanup attributes. Not to be used directly * Allows multiple instances of @RustCleanup on a class * See [java.lang.annotation.Repeatable]. */ -@Retention(RetentionPolicy.SOURCE) +@kotlin.annotation.Retention(AnnotationRetention.SOURCE) annotation class RustCleanupCollection(vararg val value: RustCleanup) \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Translations.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Translations.kt index 90b50050b..f06c80b3a 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Translations.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Translations.kt @@ -2,8 +2,9 @@ package net.ankiweb.rsdroid import anki.i18n.GeneratedTranslations import anki.i18n.TranslateArgMap -import anki.i18n.TranslateArgValue import anki.i18n.TranslateStringRequest +import kotlin.Int +import kotlin.String import anki.generic.String as GenericString // strip off unicode isolation markers from a translated string @@ -12,7 +13,7 @@ fun String.withoutUnicodeIsolation(): String { return this.replace("\u2068", "").replace("\u2069", "") } -class Translations(private val backend: Backend): GeneratedTranslations { +class Translations(private val backend: Backend) : GeneratedTranslations { override fun translate(module: Int, translation: Int, args: TranslateArgMap): String { val request = TranslateStringRequest.newBuilder().putAllArgs(args).setModuleIndex(module).setMessageIndex(translation).build() diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.kt index f9e652f3d..b29aff135 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiDatabaseCursor.kt @@ -43,6 +43,7 @@ abstract class AnkiDatabaseCursor : Cursor { abstract override fun getCount(): Int abstract override fun getPosition(): Int abstract override fun getColumnIndex(columnName: String): Int + @Throws(IllegalArgumentException::class) abstract override fun getColumnIndexOrThrow(columnName: String): Int abstract override fun getColumnName(columnIndex: Int): String @@ -100,7 +101,7 @@ abstract class AnkiDatabaseCursor : Cursor { throw NotImplementedException() } - abstract override fun moveToPosition(position: Int): Boolean + abstract override fun moveToPosition(nextPositionGlobal: Int): Boolean override fun registerContentObserver(observer: ContentObserver) { Timber.w("Not implemented: registerContentObserver - shouldn't matter unless requery() is called") } @@ -149,5 +150,5 @@ abstract class AnkiDatabaseCursor : Cursor { } protected val lastPosition: Int - protected get() = count - 1 + get() = count - 1 } \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.kt index 7add3a8c4..c2444bd50 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/NotImplementedException.kt @@ -16,8 +16,8 @@ package net.ankiweb.rsdroid.database class NotImplementedException : RuntimeException { - constructor(message: String?) : super(message) {} - constructor() {} + constructor(message: String?) : super(message) + constructor() companion object { /** A method which we should implement */ diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.kt index 75066321b..53068f529 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSQLiteStatement.kt @@ -94,7 +94,7 @@ class RustSQLiteStatement(private val database: RustSupportSQLiteDatabase, priva private fun max(integerSet: Set): Int { var max = -1 for (i in integerSet) { - max = Math.max(max, i) + max = kotlin.math.max(max, i) } return max } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt index ca0a1708d..ae0496fc2 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt @@ -81,11 +81,11 @@ class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportS } override fun getVersion(): Int { - throw NotImplementedException.Companion.todo() + throw NotImplementedException.todo() } override fun setVersion(version: Int) { - throw NotImplementedException.Companion.todo() + throw NotImplementedException.todo() } override fun query(query: String): Cursor { @@ -97,11 +97,11 @@ class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportS } override fun query(query: SupportSQLiteQuery): Cursor { - throw NotImplementedException.Companion.todo() + throw NotImplementedException.todo() } override fun query(query: SupportSQLiteQuery, cancellationSignal: CancellationSignal): Cursor { - throw NotImplementedException.Companion.todo() + throw NotImplementedException.todo() } @Throws(SQLException::class) @@ -113,7 +113,7 @@ class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportS sql.append(table) sql.append('(') var bindArgs: Array? = null - val size = if (values != null && values.size() > 0) values.size() else 0 + val size = values.size() if (size > 0) { bindArgs = arrayOfNulls(size) var i = 0 @@ -139,7 +139,7 @@ class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportS override fun update(table: String, conflictAlgorithm: Int, values: ContentValues, whereClause: String?, whereArgs: Array?): Int { // taken from SQLiteDatabase class. - require(!(values == null || values.size() == 0)) { "Empty values" } + require(values.size() > 0) { "Empty values" } val sql = StringBuilder(120) sql.append("UPDATE ") sql.append(CONFLICT_VALUES[conflictAlgorithm]) @@ -183,7 +183,7 @@ class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportS override fun needUpgrade(newVersion: Int): Boolean { // needed for metaDB, but not for Anki DB - throw NotImplementedException.Companion.todo() + throw NotImplementedException.todo() } override fun getPath(): String? { @@ -199,11 +199,11 @@ class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportS } override fun isDatabaseIntegrityOk(): Boolean { - val pragma_integrity_check = query("pragma integrity_check") - if (!pragma_integrity_check.moveToFirst()) { + val pragmaIntegrityCheck = query("pragma integrity_check") + if (!pragmaIntegrityCheck.moveToFirst()) { return false } - val value = pragma_integrity_check.getString(0) + val value = pragmaIntegrityCheck.getString(0) return "ok" == value } @@ -231,7 +231,7 @@ class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportS /** Helper methods */ private val session: Session - private get() = sessionFactory.get() + get() = sessionFactory.get()!! /** Confirmed that the below are not used for our code */ override fun delete(table: String, whereClause: String, whereArgs: Array): Int { diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt index 25cd9603f..cbdb176b0 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt @@ -17,8 +17,8 @@ package net.ankiweb.rsdroid.database import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper -import net.ankiweb.rsdroid.BackendFactory import net.ankiweb.rsdroid.Backend +import net.ankiweb.rsdroid.BackendFactory abstract class RustSupportSQLiteOpenHelper : SupportSQLiteOpenHelper { protected val configuration: SupportSQLiteOpenHelper.Configuration? diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt index 6b700ac53..36a9f9f86 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt @@ -17,17 +17,16 @@ package net.ankiweb.rsdroid.database import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper +import net.ankiweb.rsdroid.Backend import net.ankiweb.rsdroid.BackendFactory import net.ankiweb.rsdroid.BackendUtils.openAnkiDroidCollection -import net.ankiweb.rsdroid.Backend -import net.ankiweb.rsdroid.BackendUtils import timber.log.Timber class RustV11SupportSQLiteOpenHelper : RustSupportSQLiteOpenHelper { - constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) : super(configuration, backendFactory) {} - constructor(backend: Backend) : super(backend) {} + constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) : super(configuration, backendFactory) + constructor(backend: Backend) : super(backend) - override fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase? { + override fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase { Timber.d("createRustSupportSQLiteDatabase") return if (configuration != null) { val backend = backendFactory!!.getBackend() diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt index 6bfa60394..188fb0a03 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt @@ -17,17 +17,16 @@ package net.ankiweb.rsdroid.database import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper -import net.ankiweb.rsdroid.BackendFactory import net.ankiweb.rsdroid.Backend -import net.ankiweb.rsdroid.BackendUtils +import net.ankiweb.rsdroid.BackendFactory import net.ankiweb.rsdroid.BackendUtils.openAnkiDroidCollection import timber.log.Timber class RustVNextSupportSQLiteOpenHelper : RustSupportSQLiteOpenHelper { - constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) : super(configuration, backendFactory) {} - constructor(backend: Backend) : super(backend) {} + constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) : super(configuration, backendFactory) + constructor(backend: Backend) : super(backend) - override fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase? { + override fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase { Timber.d("createRustSupportSQLiteDatabase") return if (configuration != null) { val backend = backendFactory!!.getBackend() diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt index 5b25625fd..2bd37a50d 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/SQLHandler.kt @@ -25,6 +25,7 @@ interface SQLHandler { fun fullQuery(query: String): JSONArray { return fullQuery(query, null) } + fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int fun insertForId(sql: String, bindArgs: Array?): Long fun beginTransaction() diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.kt index 2a6592fef..3dbd6de6f 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursor.kt @@ -51,7 +51,7 @@ open class StreamingProtobufSQLiteCursor( /**The current index into the collection or rows */ private val sliceStartIndex: Int - private get() = results!!.startIndex.toInt() + get() = results!!.startIndex.toInt() private fun loadPage(startingAtIndex: Long) { try { @@ -60,7 +60,7 @@ open class StreamingProtobufSQLiteCursor( positionInSlice = if (startingAtIndex == -1L) -1 else 0 check(results!!.sequenceNumber == sequenceNumber) { "rsdroid does not currently handle nested cursor-based queries. Please change the code to avoid holding a reference to the query, or implement the functionality in rsdroid" } } catch (e: BackendException) { - throw e.toSQLiteException(query)!! + throw e.toSQLiteException(query) } } @@ -123,7 +123,7 @@ open class StreamingProtobufSQLiteCursor( } private val columnNamesInternal: Array - private get() { + get() { if (columnMapping == null) { columnMapping = backend.getColumnNames(query) checkNotNull(columnMapping) { "unable to obtain column mapping" } @@ -143,8 +143,8 @@ open class StreamingProtobufSQLiteCursor( val field = getFieldAtIndex(columnIndex) return when (field.dataCase) { DataCase.BLOBVALUE -> throw SQLiteException("unknown error (code 0): Unable to convert BLOB to string") - DataCase.LONGVALUE -> java.lang.Long.toString(field.longValue) - DataCase.DOUBLEVALUE -> java.lang.Double.toString(field.doubleValue) + DataCase.LONGVALUE -> field.longValue.toString() + DataCase.DOUBLEVALUE -> field.doubleValue.toString() DataCase.STRINGVALUE -> field.stringValue DataCase.DATA_NOT_SET -> null else -> throw IllegalStateException("Unknown data case: " + field.dataCase) @@ -218,7 +218,7 @@ open class StreamingProtobufSQLiteCursor( } protected val rowAtCurrentPosition: Row - protected get() { + get() { val result = results!!.result val rowCount = currentSliceRowCount if (positionInSlice < 0 || positionInSlice >= rowCount) { @@ -232,7 +232,7 @@ open class StreamingProtobufSQLiteCursor( } protected val currentSliceRowCount: Int - protected get() = results!!.result.rowsCount + get() = results!!.result.rowsCount private fun strtoll(stringValue: String): Long { return try { @@ -256,7 +256,7 @@ open class StreamingProtobufSQLiteCursor( sequenceNumber = results!!.sequenceNumber rowCount = results!!.rowCount } catch (e: BackendException) { - throw e.toSQLiteException(query)!! + throw e.toSQLiteException(query) } } } \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.kt index b0e17da76..b2f530e0b 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendInvalidInputException.kt @@ -28,7 +28,6 @@ import net.ankiweb.rsdroid.BackendException open class BackendInvalidInputException(error: BackendError?) : BackendException(error!!) { class BackendCollectionAlreadyOpenException(error: BackendError?) : BackendInvalidInputException(error) class BackendCollectionNotOpenException(error: BackendError?) : BackendInvalidInputException(error) - class BackendSearchException(error: BackendError?) : BackendInvalidInputException(error) companion object { fun fromInvalidInputError(error: BackendError): BackendInvalidInputException { when (error.localized) { diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.kt index 4b67fbade..b429be095 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/exceptions/BackendJsonException.kt @@ -19,6 +19,5 @@ import anki.backend.BackendError import net.ankiweb.rsdroid.BackendException class BackendJsonException : BackendException { - constructor(error: BackendError?) : super(error!!) {} - constructor(message: String?) : super(message) {} + constructor(error: BackendError?) : super(error!!) } \ No newline at end of file diff --git a/tools/protoc-gen/protoc-gen.py b/tools/protoc-gen/protoc-gen.py index 96e5b091c..415e0901c 100755 --- a/tools/protoc-gen/protoc-gen.py +++ b/tools/protoc-gen/protoc-gen.py @@ -305,6 +305,8 @@ def generate_code(request, response): """ /* Auto-generated from the .proto files in AnkiDroidBackend. */ +@file:Suppress("NAME_SHADOWING") + package anki.backend; import com.google.protobuf.InvalidProtocolBufferException; From 2c7cdaaf74927cbef67e17724386be3467776010 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 17 Jun 2022 13:05:32 +1000 Subject: [PATCH 19/61] Tweak version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 6695c3d75..266f79bfa 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ android.useAndroidX=true android.enableJetifier=false GROUP=io.github.david-allison-1 -VERSION_NAME=0.1.12 +VERSION_NAME=0.1.12-anki2.1.53 POM_INCEPTION_YEAR=2020 From 0e489f20bb21fb511e8828e9787ccb0d35927a33 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 17 Jun 2022 13:31:55 +1000 Subject: [PATCH 20/61] Update Rust libs --- rslib-bridge/Cargo.lock | 389 ++++++++++++++++------------------------ 1 file changed, 153 insertions(+), 236 deletions(-) diff --git a/rslib-bridge/Cargo.lock b/rslib-bridge/Cargo.lock index b37833a69..6558a0244 100644 --- a/rslib-bridge/Cargo.lock +++ b/rslib-bridge/Cargo.lock @@ -49,7 +49,7 @@ dependencies = [ "anki_i18n", "async-trait", "blake3", - "bytes 1.1.0", + "bytes", "chrono", "coarsetime", "flate2", @@ -121,15 +121,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.32" +version = "1.0.57" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b602bfe940d21c130f3895acd65221e8a61270debe89d628b9cb4e3ccb8569b" +checksum = "08f9b8508dccb7687a1d6c4ce66b2b0ecef467c94667de27d8d7fe1f8d2a9cdc" [[package]] name = "arc-swap" -version = "0.4.7" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d25d88fd6b8041580a654f9d0c581a047baee2b3efee13275f2fc392fc75034" +checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f" [[package]] name = "arrayref" @@ -148,9 +148,9 @@ dependencies = [ [[package]] name = "arrayvec" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cff77d8686867eceff3105329d4698d96c2391c176d5d03adc90c7389162b5b8" +checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" [[package]] name = "arrayvec" @@ -182,9 +182,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.0.0" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "base64" @@ -194,9 +194,9 @@ checksum = "904dfeac50f3cdaba28fc6f57fdcddb75f49ed61346676a78c4ffe55877802fd" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "blake3" @@ -207,7 +207,7 @@ dependencies = [ "arrayref", "arrayvec 0.7.2", "cc", - "cfg-if 1.0.0", + "cfg-if", "constant_time_eq", "digest", ] @@ -229,15 +229,9 @@ checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" [[package]] name = "byteorder" -version = "1.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "08c48aae112d48ed9f069b33538ea9e3e90aa263cfa3d1c24309612b1f7472de" - -[[package]] -name = "bytes" -version = "0.5.6" +version = "1.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" @@ -260,12 +254,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" -[[package]] -name = "cfg-if" -version = "0.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" - [[package]] name = "cfg-if" version = "1.0.0" @@ -281,7 +269,7 @@ dependencies = [ "libc", "num-integer", "num-traits", - "time 0.1.43", + "time 0.1.44", "winapi", ] @@ -299,11 +287,11 @@ dependencies = [ [[package]] name = "combine" -version = "4.2.1" +version = "4.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e5ef862b2df927249f4e2bdc29c1bd13a33105f900884b0c32acdf32aff584" +checksum = "2a604e93b79d1808327a6fca85a6f2d69de66461e7620f5a4cbf5fb4d1d7c948" dependencies = [ - "bytes 0.5.6", + "bytes", "memchr", ] @@ -331,103 +319,31 @@ checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] name = "crc32fast" -version = "1.2.0" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" dependencies = [ - "cfg-if 0.1.10", -] - -[[package]] -name = "crossbeam" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69323bff1fb41c635347b8ead484a5ca6c3f11914d784170b158d8449ab07f8e" -dependencies = [ - "cfg-if 0.1.10", - "crossbeam-channel 0.4.4", - "crossbeam-deque", - "crossbeam-epoch", - "crossbeam-queue", - "crossbeam-utils 0.7.2", + "cfg-if", ] [[package]] name = "crossbeam-channel" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b153fe7cbef478c567df0f972e02e6d736db11affe43dfc9c56a9374d1adfb87" -dependencies = [ - "crossbeam-utils 0.7.2", - "maybe-uninit", -] - -[[package]] -name = "crossbeam-channel" -version = "0.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aaa7bd5fb665c6864b5f963dd9097905c54125909c7aa94c9e18507cdbe6c53" -dependencies = [ - "cfg-if 1.0.0", - "crossbeam-utils 0.8.8", -] - -[[package]] -name = "crossbeam-deque" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c20ff29ded3204c5106278a81a38f4b482636ed4fa1e6cfbeef193291beb29ed" -dependencies = [ - "crossbeam-epoch", - "crossbeam-utils 0.7.2", - "maybe-uninit", -] - -[[package]] -name = "crossbeam-epoch" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "058ed274caafc1f60c4997b5fc07bf7dc7cca454af7c6e81edffe5f33f70dace" -dependencies = [ - "autocfg", - "cfg-if 0.1.10", - "crossbeam-utils 0.7.2", - "lazy_static", - "maybe-uninit", - "memoffset", - "scopeguard", -] - -[[package]] -name = "crossbeam-queue" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "774ba60a54c213d409d5353bda12d49cd68d14e45036a285234c8d6f91f92570" -dependencies = [ - "cfg-if 0.1.10", - "crossbeam-utils 0.7.2", - "maybe-uninit", -] - -[[package]] -name = "crossbeam-utils" -version = "0.7.2" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8" +checksum = "4c02a4d71819009c192cf4872265391563fd6a84c81ff2c0f2a7026ca4c1d85c" dependencies = [ - "autocfg", - "cfg-if 0.1.10", - "lazy_static", + "cfg-if", + "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.8" +version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf124c720b7686e3c2663cf54062ab0f68a88af2fb6a030e87e30bf721fcb38" +checksum = "8ff1f980957787286a554052d03c7aee98d99cc32e09f6d45f0a814133c87978" dependencies = [ - "cfg-if 1.0.0", - "lazy_static", + "cfg-if", + "once_cell", ] [[package]] @@ -457,7 +373,7 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "dirs-sys-next", ] @@ -484,7 +400,7 @@ version = "0.8.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9852635589dc9f9ea1b6fe9f05b50ef208c85c834a562f0c6abb1c475736ec2b" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -694,9 +610,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.4" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "501466ecc8a30d1d3b7fc9229b122b2ce8ed6e9d9223f1138d4babb253e51817" +checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803" dependencies = [ "typenum", "version_check", @@ -713,13 +629,13 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9be70c98951c83b8d2f8f60d7065fa6d5146873094452a1008da8c2f1e4205ad" +checksum = "4eb1a864a501629691edf6c15a593b7a51eebaa1e8468e9ddc623de7c9b58ec6" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", - "wasi 0.10.0+wasi-snapshot-preview1", + "wasi 0.11.0+wasi-snapshot-preview1", ] [[package]] @@ -728,7 +644,7 @@ version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37a82c6d637fc9515a4694bbf1cb2457b79d81ce52b3108bdeea58b07dd34a57" dependencies = [ - "bytes 1.1.0", + "bytes", "fnv", "futures-core", "futures-sink", @@ -750,29 +666,35 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" + [[package]] name = "hashlink" version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" dependencies = [ - "hashbrown", + "hashbrown 0.11.2", ] [[package]] name = "heck" -version = "0.3.1" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20564e78d53d2bb135c343b3f47714a56af2061f1c928fdb541dc7b9fdd94205" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" dependencies = [ "unicode-segmentation", ] [[package]] name = "hermit-abi" -version = "0.1.15" +version = "0.1.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3deed196b6e7f9e44a2ae8d94225d80302d81208b1bb673fd21fe634645c85a9" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" dependencies = [ "libc", ] @@ -805,13 +727,13 @@ checksum = "e9025058dae765dee5070ec375f591e2ba14638c63feff74f13805a72e523163" [[package]] name = "http" -version = "0.2.1" +version = "0.2.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d569972648b2c512421b5f2a405ad6ac9666547189d0c5477a3f200f3e02f9" +checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ - "bytes 0.5.6", + "bytes", "fnv", - "itoa 0.4.6", + "itoa 1.0.2", ] [[package]] @@ -820,7 +742,7 @@ version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5f38f16d184e36f2408a55281cd658ecbd3ca05cce6d6510a176eca393e26d1" dependencies = [ - "bytes 1.1.0", + "bytes", "http", "pin-project-lite", ] @@ -843,7 +765,7 @@ version = "0.14.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" dependencies = [ - "bytes 1.1.0", + "bytes", "futures-channel", "futures-core", "futures-util", @@ -909,12 +831,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "1.8.2" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6012d540c5baa3589337a98ce73408de9b5a25ec9fc2c6fd6be8f0d39e0ca5a" +checksum = "6c6392766afd7964e2531940894cffe4bd8d7d17dbc3c1c4857040fd4b33bdb3" dependencies = [ "autocfg", - "hashbrown", + "hashbrown 0.12.1", ] [[package]] @@ -929,7 +851,7 @@ version = "0.1.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", ] [[package]] @@ -969,9 +891,9 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6f3ad7b9d11a0c00842ff8de1b60ee58661048eb8049ed33c73594f359d7e6" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" [[package]] name = "itoa" @@ -1010,9 +932,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.57" +version = "0.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "671a26f820db17c2a2750743f1dd03bafd15b98c9f30c7c2628c024c05d73397" +checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" dependencies = [ "wasm-bindgen", ] @@ -1029,9 +951,9 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6607c62aa161d23d17a9072cc5da0be67cdfc89d3afb1e8d9c842bebc2525ffe" dependencies = [ - "arrayvec 0.5.1", + "arrayvec 0.5.2", "bitflags", - "cfg-if 1.0.0", + "cfg-if", "ryu", "static_assertions", ] @@ -1055,20 +977,21 @@ dependencies = [ [[package]] name = "lock_api" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +checksum = "327fa5b6a6940e4699ec49a9beae1ea4845c6bab9314e4f84ac68742139d8c53" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.11" +version = "0.4.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fabed175da42fed1fa0746b0ea71f412aa9d35e76e95e59b192c64b9dc2bf8b" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" dependencies = [ - "cfg-if 0.1.10", + "cfg-if", ] [[package]] @@ -1103,27 +1026,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" -[[package]] -name = "maybe-uninit" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" - [[package]] name = "memchr" version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" -[[package]] -name = "memoffset" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c198b026e1bbf08a937e94c6c60f9ec4a2267f5b0d2eec9c1b21b061ce2be55f" -dependencies = [ - "autocfg", -] - [[package]] name = "mime" version = "0.3.16" @@ -1169,9 +1077,9 @@ dependencies = [ [[package]] name = "multimap" -version = "0.8.1" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8883adfde9756c1d30b0f519c9b8c502a94b41ac62f696453c37c7fc0a958ce" +checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" [[package]] name = "new_debug_unreachable" @@ -1202,7 +1110,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bafe4179722c2894288ee77a9f044f02811c86af699344c498b0840c698a2465" dependencies = [ "arrayvec 0.4.12", - "itoa 0.4.6", + "itoa 0.4.8", ] [[package]] @@ -1217,9 +1125,9 @@ dependencies = [ [[package]] name = "num-traits" -version = "0.2.12" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac267bcc07f48ee5f8935ab0d24f316fb722d7a1292e2913f0cc196b29ffd611" +checksum = "578ede34cf02f8924ab9447f50c28075b4d3e5b269972345e7e0372b38c6cdcd" dependencies = [ "autocfg", ] @@ -1292,7 +1200,7 @@ version = "0.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09a279cbf25cb0757810394fbc1e359949b59e348145c643a939a525692e6929" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "libc", "redox_syscall", "smallvec", @@ -1417,9 +1325,9 @@ checksum = "1df8c4ec4b0627e53bdf214615ad287367e482558cf84b109250b37464dc03ae" [[package]] name = "ppv-lite86" -version = "0.2.8" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "237a5ed80e274dbc66f86bd59c1e25edc039660be53194b5fe0a482e0f2612ea" +checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" [[package]] name = "precomputed-hash" @@ -1439,9 +1347,9 @@ dependencies = [ [[package]] name = "proc-macro-hack" -version = "0.5.18" +version = "0.5.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99c605b9a0adc77b7211c6b1f722dcb613d68d66859a44f3d485a6da332b0598" +checksum = "dbf0c48bc1d91375ae5c3cd81e3722dff1abcf81a30960240640d223f59fe0e5" [[package]] name = "proc-macro-nested" @@ -1464,7 +1372,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "444879275cb4fd84958b1a1d5420d15e6fcf7c235fe47f053c9c2a80aceb6001" dependencies = [ - "bytes 1.1.0", + "bytes", "prost-derive", ] @@ -1474,7 +1382,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62941722fb675d463659e49c4f3fe1fe792ff24fe5bbaa9c08cd3b98a1c354f5" dependencies = [ - "bytes 1.1.0", + "bytes", "heck", "itertools", "lazy_static", @@ -1507,7 +1415,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "534b7a0e836e3c482d2693070f982e39e7611da9695d4d1f5a4b186b51faef0a" dependencies = [ - "bytes 1.1.0", + "bytes", "prost", ] @@ -1525,9 +1433,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.7" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa563d17ecb180e500da1cfd2b028310ac758de548efdd203e18f283af693f37" +checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1" dependencies = [ "proc-macro2", ] @@ -1614,7 +1522,7 @@ version = "0.11.3" source = "git+https://github.com/ankitects/reqwest.git?rev=7591444614de02b658ddab125efba7b2bb4e2335#7591444614de02b658ddab125efba7b2bb4e2335" dependencies = [ "base64", - "bytes 1.1.0", + "bytes", "encoding_rs", "futures-core", "futures-util", @@ -1734,9 +1642,9 @@ checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" [[package]] name = "ryu" -version = "1.0.5" +version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e" +checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" [[package]] name = "same-file" @@ -1775,9 +1683,9 @@ dependencies = [ [[package]] name = "security-framework" -version = "2.3.1" +version = "2.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23a2ac85147a3a11d77ecf1bc7166ec0b92febfa4461c37944e180f319ece467" +checksum = "2dc14f172faf8a0194a3aded622712b0de276821addc574fa54fc0a1167e10dc" dependencies = [ "bitflags", "core-foundation", @@ -1890,9 +1798,18 @@ dependencies = [ [[package]] name = "sha1" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2579985fda508104f7587689507983eadd6a6e84dd35d6d115361f530916fa0d" +checksum = "c1da05c97445caa12d05e848c4a4fcbbea29e748ac28f7e80e9b010392063770" +dependencies = [ + "sha1_smol", +] + +[[package]] +name = "sha1_smol" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae1a47186c03a32177042e55dbc5fd5aee900b8e0069a8d70fba96a9375cd012" [[package]] name = "siphasher" @@ -1902,9 +1819,9 @@ checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de" [[package]] name = "slab" -version = "0.4.2" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c111b5bd5695e56cffe5129854aa230b39c93a305372fdbb2668ca2394eea9f8" +checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" [[package]] name = "slog" @@ -1918,7 +1835,7 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "766c59b252e62a34651412870ff55d8c4e6d04df19b43eecb2703e417b097ffe" dependencies = [ - "crossbeam-channel 0.5.4", + "crossbeam-channel", "slog", "take_mut", "thread_local", @@ -1941,9 +1858,9 @@ dependencies = [ [[package]] name = "slog-scope" -version = "4.3.0" +version = "4.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c44c89dd8b0ae4537d1ae318353eaf7840b4869c536e31c41e963d1ea523ee6" +checksum = "2f95a4b4c3274cd2869549da82b57ccc930859bdbf5bcea0424bc5f140b3c786" dependencies = [ "arc-swap", "lazy_static", @@ -1952,11 +1869,10 @@ dependencies = [ [[package]] name = "slog-stdlog" -version = "4.0.0" +version = "4.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be4d87903baf655da2d82bc3ac3f7ef43868c58bf712b3a661fda72009304c23" +checksum = "6706b2ace5bbae7291d3f8d2473e2bfab073ccd7d03670946197aec98471fa3e" dependencies = [ - "crossbeam", "log", "slog", "slog-scope", @@ -2086,7 +2002,7 @@ version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "fastrand", "libc", "redox_syscall", @@ -2138,20 +2054,21 @@ dependencies = [ [[package]] name = "thread_local" -version = "1.0.1" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d40c6d1b69745a6ec6fb1ca717914848da4b44ae29d9b3080cbee91d72a69b14" +checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] name = "time" -version = "0.1.43" +version = "0.1.44" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" +checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255" dependencies = [ "libc", + "wasi 0.10.0+wasi-snapshot-preview1", "winapi", ] @@ -2175,9 +2092,9 @@ checksum = "42657b1a6f4d817cda8e7a0ace261fe0cc946cf3a80314390b22cc61ae080792" [[package]] name = "tinystr" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "707151f004e8db265b83b1c7509d6c3b4c2c2bc8696113cbe0a8e595c2fdbd3b" +checksum = "29738eedb4388d9ea620eeab9384884fc3f06f586a2eddb56bedc5885126c7c1" [[package]] name = "tinyvec" @@ -2200,7 +2117,7 @@ version = "1.19.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" dependencies = [ - "bytes 1.1.0", + "bytes", "libc", "memchr", "mio", @@ -2249,7 +2166,7 @@ version = "0.6.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "36943ee01a6d67977dd3f84a5a1d2efeb4ada3a1ae771cadfaa535d9d9fc6507" dependencies = [ - "bytes 1.1.0", + "bytes", "futures-core", "futures-sink", "log", @@ -2263,7 +2180,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" dependencies = [ - "bytes 1.1.0", + "bytes", "futures-core", "futures-sink", "pin-project-lite", @@ -2273,18 +2190,18 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.6" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc92d160b1eef40665be3a05630d003936a3bc7da7421277846c2613e92c71a" +checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" dependencies = [ "serde", ] [[package]] name = "tower-service" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e987b6bf443f4b5b3b6f38704195592cca41c5bb7aedd3c3693c7081f8289860" +checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" [[package]] name = "tracing" @@ -2292,7 +2209,7 @@ version = "0.1.35" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "pin-project-lite", "tracing-core", ] @@ -2429,9 +2346,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.0" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d22af068fba1eb5edcb4aea19d382b2a3deb4c8f9d475c589b6ada9e0fd493ee" +checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" [[package]] name = "unicode-normalization" @@ -2444,9 +2361,9 @@ dependencies = [ [[package]] name = "unicode-segmentation" -version = "1.6.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83e153d1053cbb5a118eeff7fd5be06ed99153f00dbcd8ae310c5fb2b22edc0" +checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99" [[package]] name = "unicode-width" @@ -2496,21 +2413,21 @@ dependencies = [ [[package]] name = "vcpkg" -version = "0.2.10" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6454029bf181f092ad1b853286f23e2c507d8e8194d01d92da4a55c274a5508c" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] name = "version_check" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.3.1" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d" +checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" dependencies = [ "same-file", "winapi", @@ -2541,11 +2458,11 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27370197c907c55e3f1a9fbe26f44e937fe6451368324e009cba39e139dc08ad" +checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "serde", "serde_json", "wasm-bindgen-macro", @@ -2553,9 +2470,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "53e04185bfa3a779273da532f5025e33398409573f348985af9a1cbf3774d3f4" +checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" dependencies = [ "bumpalo", "lazy_static", @@ -2568,11 +2485,11 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.30" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f741de44b75e14c35df886aff5f1eb73aa114fa5d4d00dcd37b5e01259bf3b2" +checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" dependencies = [ - "cfg-if 1.0.0", + "cfg-if", "js-sys", "wasm-bindgen", "web-sys", @@ -2580,9 +2497,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17cae7ff784d7e83a2fe7611cfe766ecf034111b49deb850a3dc7699c08251f5" +checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2590,9 +2507,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "99ec0dc7a4756fffc231aab1b9f2f578d23cd391390ab27f952ae0c9b3ece20b" +checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" dependencies = [ "proc-macro2", "quote", @@ -2603,15 +2520,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.80" +version = "0.2.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d554b7f530dee5964d9a9468d95c1f8b8acae4f282807e7d27d4b03099a46744" +checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" [[package]] name = "web-sys" -version = "0.3.57" +version = "0.3.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b17e741662c70c8bd24ac5c5b18de314a2c26c32bf8346ee1e6f53de919c283" +checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" dependencies = [ "js-sys", "wasm-bindgen", @@ -2740,7 +2657,7 @@ dependencies = [ "crc32fast", "flate2", "thiserror", - "time 0.1.43", + "time 0.1.44", ] [[package]] From 70999d5b0b643c3da6f17e4cab5779b3b7b4d568 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jun 2022 00:38:27 +1000 Subject: [PATCH 21/61] Interface tidy-ups - Reduce the numbers of factories, and provide some helper routines in AnkiSupportSQLiteDatabase to create Rust-based and Framework-based instances. - BackendFactory now provides static methods to create a backend, and supports configuring the default languages and schema version ( which the AnkiAndroid code will use) - Backend instances have their schema version set at creation, instead of collection open. --- .../ankiweb/rsdroid/BackendDisposalTests.java | 4 +- .../ankiweb/rsdroid/BackendForTesting.java | 18 ++--- .../net/ankiweb/rsdroid/BackendSlowTests.java | 4 +- .../rsdroid/BackendTranslationsTest.java | 4 +- .../net/ankiweb/rsdroid/ExceptionTest.java | 10 ++- .../rsdroid/RustDatabaseIntegrationTests.java | 2 +- .../rsdroid/ankiutil/InstrumentedTest.java | 25 ++----- .../StreamingProtobufSQLiteCursorTest.java | 2 +- .../testutils/DatabaseComparison.java | 45 ++++++------ rsdroid/build.gradle | 1 + .../main/java/net/ankiweb/rsdroid/Backend.kt | 20 +++--- .../net/ankiweb/rsdroid/BackendFactory.kt | 68 ++++++++++++++----- .../java/net/ankiweb/rsdroid/BackendUtils.kt | 10 --- .../net/ankiweb/rsdroid/BackendV11Factory.kt | 38 ----------- .../ankiweb/rsdroid/BackendVNextFactory.kt | 40 ----------- .../java/net/ankiweb/rsdroid/NativeMethods.kt | 23 ++++--- .../rsdroid/RustBackendFailedException.kt | 27 -------- .../database/AnkiSupportSQLiteDatabase.kt | 59 ++++++++++++++++ .../database/RustSupportSQLiteDatabase.kt | 27 ++++---- .../database/RustSupportSQLiteOpenHelper.kt | 67 ------------------ .../RustV11SQLiteOpenHelperFactory.kt | 28 -------- .../RustV11SupportSQLiteOpenHelper.kt | 39 ----------- .../RustVNextSQLiteOpenHelperFactory.kt | 28 -------- .../RustVNextSupportSQLiteOpenHelper.kt | 40 ----------- .../net/ankiweb/CollectionCreationTest.java | 9 +-- .../src/test/java/net/ankiweb/TestUtil.java | 30 -------- 26 files changed, 205 insertions(+), 463 deletions(-) delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.kt create mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiSupportSQLiteDatabase.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.kt delete mode 100644 rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt delete mode 100644 rsdroid/src/test/java/net/ankiweb/TestUtil.java diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java index 036f489fc..0a8219cc4 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendDisposalTests.java @@ -21,7 +21,7 @@ import net.ankiweb.rsdroid.ankiutil.DatabaseUtil; import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; -import net.ankiweb.rsdroid.database.RustV11SupportSQLiteOpenHelper; +import net.ankiweb.rsdroid.database.AnkiSupportSQLiteDatabase; import org.junit.Ignore; import org.junit.Test; @@ -42,7 +42,7 @@ public void testDisposalDoesNotLeak() throws IOException { Timber.d("Iteration %d", i); try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { - SupportSQLiteDatabase db = new RustV11SupportSQLiteOpenHelper(backend).getWritableDatabase(); + SupportSQLiteDatabase db = AnkiSupportSQLiteDatabase.withRustBackend(BackendFactory.getBackend(getContext())); int count = DatabaseUtil.queryScalar(db, "select count(*) from revlog"); } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java index ad9439cf0..a36a55677 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendForTesting.java @@ -16,21 +16,21 @@ package net.ankiweb.rsdroid; +import android.content.Context; + +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; +import java.util.Arrays; + public class BackendForTesting extends Backend { - BackendForTesting() { - super(); + public BackendForTesting(@NonNull Context context, @NonNull Iterable langs, boolean legacySchema) { + super(context, langs, legacySchema); } - public static BackendForTesting create() { - try { - NativeMethods.ensureSetup(); - } catch (RustBackendFailedException e) { - throw new RuntimeException(e); - } - return new BackendForTesting(); + public static BackendForTesting create(@NonNull Context context) { + return new BackendForTesting(context, Arrays.asList("en"), true); } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java index d29805a55..8d0f124f7 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendSlowTests.java @@ -21,7 +21,7 @@ import androidx.sqlite.db.SupportSQLiteDatabase; import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; -import net.ankiweb.rsdroid.database.RustV11SupportSQLiteOpenHelper; +import net.ankiweb.rsdroid.database.AnkiSupportSQLiteDatabase; import org.junit.Ignore; import org.junit.Test; @@ -142,7 +142,7 @@ public void ensureSQLIsStreamed() throws IOException { */ try (Backend backend = super.getBackend("initial_version_2_12_1.anki2")) { - SupportSQLiteDatabase db = new RustV11SupportSQLiteOpenHelper(backend).getWritableDatabase(); + SupportSQLiteDatabase db = AnkiSupportSQLiteDatabase.withRustBackend(backend); db.query("create table tmp (id varchar)"); diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendTranslationsTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendTranslationsTest.java index b5ee5fada..6eaa690a8 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendTranslationsTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/BackendTranslationsTest.java @@ -24,11 +24,11 @@ private String withoutIsolation(String s) { @Test public void ensureI18nWorks() { - Backend b = new Backend(Arrays.asList("en")); + Backend b = BackendFactory.getBackend(getContext()); assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, 10)), equalTo("Trash folder: 5 files, 10MB")); assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, 10.0)), equalTo("Trash folder: 5 files, 10MB")); assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, "foo")), equalTo("Trash folder: 5 files, fooMB")); - b = new Backend(Arrays.asList("fr")); + b = BackendFactory.getBackend(getContext(), Arrays.asList("fr")); assertThat(withoutIsolation(b.getTr().mediaCheckTrashCount(5, 10)), equalTo("Corbeille : 5 fichiers, 10 Mo")); } } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java index 430b874f6..c7a57d5c7 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ExceptionTest.java @@ -40,6 +40,10 @@ import static org.junit.Assert.fail; +import android.content.Context; + +import androidx.test.platform.app.InstrumentationRegistry; + @RunWith(Parameterized.class) public class ExceptionTest { @@ -92,7 +96,11 @@ public static java.util.Collection initParameters() { @Before public void errorProducesNamedException() { - backend = BackendForTesting.create(); + backend = BackendForTesting.create(getContext()); + } + + protected Context getContext() { + return InstrumentationRegistry.getInstrumentation().getTargetContext(); } @Test diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java index 6a8f575f3..fa44d06ea 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/RustDatabaseIntegrationTests.java @@ -96,7 +96,7 @@ private RustSupportSQLiteDatabase getDatabase() { try { Backend backendV1 = getBackend(fileName); boolean readOnly = false; - return new RustSupportSQLiteDatabase(backendV1, readOnly); + return new RustSupportSQLiteDatabase(backendV1); } catch (Exception e) { throw new RuntimeException(e); } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java index ac08ba5ce..70ac9f1cb 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/ankiutil/InstrumentedTest.java @@ -23,10 +23,7 @@ import androidx.test.platform.app.InstrumentationRegistry; import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.BackendUtils; import net.ankiweb.rsdroid.Backend; -import net.ankiweb.rsdroid.NativeMethods; -import net.ankiweb.rsdroid.RustBackendFailedException; import net.ankiweb.rsdroid.exceptions.BackendInvalidInputException; import org.jetbrains.annotations.NotNull; @@ -57,12 +54,6 @@ public void before() { Timber.uprootAll(); Timber.plant(new Timber.DebugTree()); */ - - try { - NativeMethods.ensureSetup(); - } catch (RustBackendFailedException e) { - throw new RuntimeException(e); - } } @After @@ -127,18 +118,14 @@ protected Backend getBackend(String fileName) { @NotNull protected Backend getBackendFromPath(String path) { - Backend backendV1 = getClosedBackend(); - backendV1.setPageSize(TEST_PAGE_SIZE); - BackendUtils.openAnkiDroidCollection(backendV1, path, true); - this.backendList.add(backendV1); - return backendV1; + Backend backend = getClosedBackend(); + backend.setPageSize(TEST_PAGE_SIZE); + backend.openCollection(path); + backendList.add(backend); + return backend; } protected Backend getClosedBackend() { - try { - return BackendFactory.createInstance().getBackend(); - } catch (RustBackendFailedException e) { - throw new RuntimeException(e); - } + return BackendFactory.getBackend(getContext()); } } diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java index 00e81a6ae..951243fb8 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/StreamingProtobufSQLiteCursorTest.java @@ -62,7 +62,7 @@ public void testPaging() throws IOException { } private SupportSQLiteDatabase getWritableDatabase(Backend backend) { - return new RustV11SupportSQLiteOpenHelper(backend).getWritableDatabase(); + return AnkiSupportSQLiteDatabase.withRustBackend(backend); } @Test diff --git a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/testutils/DatabaseComparison.java b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/testutils/DatabaseComparison.java index 5e6aa96bd..be1f7575f 100644 --- a/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/testutils/DatabaseComparison.java +++ b/rsdroid-instrumented/src/androidTest/java/net/ankiweb/rsdroid/database/testutils/DatabaseComparison.java @@ -19,12 +19,11 @@ import androidx.annotation.NonNull; import androidx.sqlite.db.SupportSQLiteDatabase; import androidx.sqlite.db.SupportSQLiteOpenHelper; -import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory; +import net.ankiweb.rsdroid.Backend; import net.ankiweb.rsdroid.BackendFactory; import net.ankiweb.rsdroid.ankiutil.InstrumentedTest; -import net.ankiweb.rsdroid.RustBackendFailedException; -import net.ankiweb.rsdroid.database.RustV11SQLiteOpenHelperFactory; +import net.ankiweb.rsdroid.database.AnkiSupportSQLiteDatabase; import org.junit.Before; import org.junit.runners.Parameterized; @@ -40,7 +39,7 @@ public class DatabaseComparison extends InstrumentedTest { @Parameterized.Parameters(name = "{0}") public static java.util.Collection initParameters() { // This does one run with schedVersion injected as 1, and one run as 2 - return Arrays.asList(new Object[][] { { DatabaseType.FRAMEWORK }, { DatabaseType.RUST } }); + return Arrays.asList(new Object[][]{{DatabaseType.FRAMEWORK}, {DatabaseType.RUST_LEGACY}, {DatabaseType.RUST_NEW}}); } @Before @@ -60,23 +59,17 @@ protected boolean handleSetupException(Exception e) { } protected SupportSQLiteDatabase getDatabase() { - SupportSQLiteOpenHelper.Configuration config = SupportSQLiteOpenHelper.Configuration.builder(getContext()) - .callback(new DefaultCallback()) - .name(getDatabasePath()) - .build(); - switch (schedVersion) { - case RUST: - BackendFactory mBackendFactory; - try { - mBackendFactory = BackendFactory.createInstance(); - } catch (RustBackendFailedException e) { - throw new RuntimeException(e); - } - // This throws on corruption - return new RustV11SQLiteOpenHelperFactory(mBackendFactory).create(config).getWritableDatabase(); + case RUST_LEGACY: + Backend backend = BackendFactory.getBackend(getContext(), Arrays.asList("en"), true); + backend.openCollection(getDatabasePath()); + return AnkiSupportSQLiteDatabase.withRustBackend(backend); + case RUST_NEW: + Backend backend2 = BackendFactory.getBackend(getContext(), Arrays.asList("en"), false); + backend2.openCollection(getDatabasePath()); + return AnkiSupportSQLiteDatabase.withRustBackend(backend2); case FRAMEWORK: - return new FrameworkSQLiteOpenHelperFactory().create(config).getWritableDatabase(); + return AnkiSupportSQLiteDatabase.withFramework(getContext(), getDatabasePath()); } throw new IllegalStateException(); } @@ -85,9 +78,13 @@ protected String getDatabasePath() { // TODO: look into this - null should work try { switch (schedVersion) { - case RUST: return ":memory:"; - case FRAMEWORK: return null; - default: return null; + case RUST_LEGACY: + case RUST_NEW: + return ":memory:"; + case FRAMEWORK: + return null; + default: + return null; } } catch (NullPointerException ex) { throw new IllegalStateException("Class is not annotated with @RunWith(Parameterized.class)", ex); @@ -96,11 +93,11 @@ protected String getDatabasePath() { public enum DatabaseType { FRAMEWORK, - RUST + RUST_LEGACY, + RUST_NEW, } - private static class DefaultCallback extends SupportSQLiteOpenHelper.Callback { public DefaultCallback() { super(1); diff --git a/rsdroid/build.gradle b/rsdroid/build.gradle index 1c359fe7f..d342dd1c7 100644 --- a/rsdroid/build.gradle +++ b/rsdroid/build.gradle @@ -70,6 +70,7 @@ dependencies { api "com.google.protobuf:protobuf-java:${rootProject.ext.protobufVersion}" implementation "androidx.sqlite:sqlite:${rootProject.ext.sqliteVersion}" implementation 'com.jakewharton.timber:timber:5.0.1' + implementation 'androidx.sqlite:sqlite-framework:2.2.0' testImplementation 'junit:junit:4.13.2' testImplementation "org.robolectric:robolectric:4.8.1" diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt index c0ed1a9f4..5d2a0d975 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -15,6 +15,7 @@ */ package net.ankiweb.rsdroid +import android.content.Context import androidx.annotation.CheckResult import androidx.annotation.VisibleForTesting import anki.ankidroid.DBResponse @@ -24,6 +25,7 @@ import anki.backend.GeneratedBackend import anki.generic.Int64 import com.google.protobuf.ByteString import com.google.protobuf.InvalidProtocolBufferException +import net.ankiweb.rsdroid.database.NotImplementedException import net.ankiweb.rsdroid.database.SQLHandler import org.json.JSONArray import org.json.JSONException @@ -33,14 +35,11 @@ import java.io.Closeable import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock -open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), SQLHandler, Closeable { +open class Backend(val context: Context, langs: Iterable = listOf("en"), val legacySchema: Boolean = true) : GeneratedBackend(), SQLHandler, Closeable { // Set on init; unset on .close(). Access via withBackend() private var backendPointer: Long? = null private val lock = ReentrantLock() - // Only stored to satisfy .getPath() interface in SQL connection - private var collectionPath: String? = null - val tr: Translations by lazy { Translations(this) } @@ -49,16 +48,16 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), return backendPointer != null } - fun openCollection(collectionPath: String, forceSchema11: Boolean = true) { - openCollection(collectionPath, "", "", "", forceSchema11) + fun openCollection(collectionPath: String) { + openCollection(collectionPath, "", "", "", legacySchema) } /** * Open a backend instance, loading the shared library if not already loaded. */ init { - NativeMethods.ensureSetup() Timber.i("Opening rust backend with lang=$langs") + NativeMethods.ensureSetup(context) val input = BackendInit.newBuilder() .addAllPreferredLangs(langs) .build() @@ -92,7 +91,6 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), } catch (exc: BackendException.BackendDbException) { throw exc.toSQLiteException("db open") } - this.collectionPath = collectionPath } /** @@ -100,7 +98,6 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), */ override fun closeCollection(downgradeToSchema11: Boolean) { cancelAllProtoQueries() - collectionPath = null super.closeCollection(downgradeToSchema11) } @@ -156,11 +153,11 @@ open class Backend(langs: Iterable = listOf("en")) : GeneratedBackend(), // other DB methods override fun closeDatabase() { - closeCollection(false) + throw NotImplementedException("should close collection, not db") } override fun getPath(): String? { - return collectionPath + throw NotImplementedException() } @CheckResult @@ -244,7 +241,6 @@ enum class DbRequestKind { Begin, Commit, Rollback, - ExecuteMany } /** diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt index 279c13b58..cd88bbd48 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt @@ -13,30 +13,62 @@ * You should have received a copy of the GNU General Public License along with * this program. If not, see . */ + package net.ankiweb.rsdroid -import androidx.sqlite.db.SupportSQLiteOpenHelper +import android.content.Context +import java.util.* -abstract class BackendFactory // Force users to go through getInstance - for now we need to handle the backend failure -protected constructor() { - private var backend: Backend? = null +typealias CustomBackendCreator = (context: Context, languages: Iterable, legacySchema: Boolean) -> Backend - @Synchronized - fun getBackend(): Backend { - if (backend == null) { - backend = Backend() - } - return backend!! +object BackendFactory { + /** + * If enabled, collections are upgraded to the latest schema version on open, and different + * code paths are used to access the collection, eg the major 'col' classes: models, decks, dconf, + * conf, tags are replaced with updated variants. + * + * UNSTABLE: DO NOT USE THIS ON A COLLECTION YOU CARE ABOUT. + */ + @JvmStatic + var defaultLegacySchema: Boolean = true + + /** + * The language(es) the backend uses for translations. + */ + var defaultLanguages: Iterable = listOf("en") + + @JvmStatic + private var backendForTesting: CustomBackendCreator? = null + + @JvmStatic + @JvmOverloads + fun getBackend(context: Context, languages: Iterable? = null, legacySchema: Boolean? = null): Backend { + val langs = languages ?: defaultLanguages + val legacy = legacySchema ?: defaultLegacySchema + return backendForTesting?.invoke(context, langs, legacy) ?: Backend( + context, + langs, + legacy + ) } - abstract val sQLiteOpener: SupportSQLiteOpenHelper.Factory? + @JvmStatic + fun setDefaultLanguagesFromLocales(locales: Iterable) { + defaultLanguages = locales.map { localeToBackendCode(it) } + } - companion object { - @JvmStatic - @RustCleanup("Use BackendV[11/Next]Factory") - @Throws(RustBackendFailedException::class) - fun createInstance(): BackendFactory { - return BackendV11Factory.createInstance() + private fun localeToBackendCode(locale: Locale): String { + // TODO: this needs checking that all language codes match the ones + // shown here: https://i18n.ankiweb.net/teams/ + return when (locale.language) { + Locale("heb").language -> "he" + Locale("yue").language -> "zh-TW" + else -> locale.language } } -} \ No newline at end of file + + /** Allows overriding the returned backend for unit tests */ + fun setOverride(creator: CustomBackendCreator?) { + backendForTesting = creator + } +} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt index dd78a4119..819a9d700 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendUtils.kt @@ -16,16 +16,6 @@ package net.ankiweb.rsdroid object BackendUtils { - /** - * - * @throws android.database.sqlite.SQLiteDatabaseCorruptException If database is corrupt - */ - // fixme: call backend directly - @JvmStatic - fun openAnkiDroidCollection(backendV1: Backend, path: String?, forceSchema11: Boolean) { - backendV1.openCollection(path ?: ":memory:", forceSchema11 = forceSchema11) - } - val ankiCommitHash: String get() = BuildConfig.ANKI_COMMIT_HASH } \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt deleted file mode 100644 index 72e356421..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendV11Factory.kt +++ /dev/null @@ -1,38 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid - -import androidx.sqlite.db.SupportSQLiteOpenHelper -import net.ankiweb.rsdroid.NativeMethods.ensureSetup -import net.ankiweb.rsdroid.database.RustV11SQLiteOpenHelperFactory - -class BackendV11Factory : BackendFactory() { - override val sQLiteOpener: SupportSQLiteOpenHelper.Factory - get() = RustV11SQLiteOpenHelperFactory(this) - - companion object { - /** - * Obtains an instance of BackendFactory which will connect to rsdroid. - * Each call will generate a separate instance which can handle a new Anki collection - */ - @RustV1Cleanup("RustBackendFailedException may be moved to a more appropriate location") - @Throws(RustBackendFailedException::class) - fun createInstance(): BackendV11Factory { - ensureSetup() - return BackendV11Factory() - } - } -} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.kt deleted file mode 100644 index a986cd11a..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendVNextFactory.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid - -import androidx.sqlite.db.SupportSQLiteOpenHelper -import net.ankiweb.rsdroid.NativeMethods.ensureSetup -import net.ankiweb.rsdroid.RustBackendFailedException -import net.ankiweb.rsdroid.database.RustVNextSQLiteOpenHelperFactory - -/** A factory for the latest version of the backend (v16 currently) */ -class BackendVNextFactory : BackendFactory() { - override val sQLiteOpener: SupportSQLiteOpenHelper.Factory - get() = RustVNextSQLiteOpenHelperFactory(this) - - companion object { - /** - * Obtains an instance of BackendFactory which will connect to rsdroid. - * Each call will generate a separate instance which can handle a new Anki collection - */ - @RustV1Cleanup("RustBackendFailedException may be moved to a more appropriate location") - @Throws(RustBackendFailedException::class) - fun createInstance(): BackendVNextFactory { - ensureSetup() - return BackendVNextFactory() - } - } -} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt index 862b6c13b..7d78787c9 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt @@ -1,34 +1,39 @@ package net.ankiweb.rsdroid +import android.content.Context import android.os.Build +import android.system.Os import androidx.annotation.CheckResult -import net.ankiweb.rsdroid.RustBackendFailedException +import java.lang.RuntimeException object NativeMethods { private var hasSetUp = false - private var setupException: RustBackendFailedException? = null val isRoboUnitTest: Boolean get() = "robolectric" == Build.FINGERPRINT @JvmStatic @Synchronized - @Throws(RustBackendFailedException::class) - fun ensureSetup() { + fun ensureSetup(context: Context) { if (hasSetUp) { - if (setupException != null) { - throw setupException!! - } return } + + // Prevent sqlite throwing error 6410 due to the lack of /tmp + if (!isRoboUnitTest) { + val dir = context.cacheDir + Os.setenv("TMPDIR", dir.path, false) + } + try { System.loadLibrary("rsdroid") } catch (e: UnsatisfiedLinkError) { if (!isRoboUnitTest) { - setupException = RustBackendFailedException(e) - throw setupException!! + throw RuntimeException("backend load failed") } // In Robolectric, assume setup works (setupException == null) if the library throws. // As the library is loaded at a later time (or a failure will be quickly found). + + // fixme: is this roboelectric special case still required? } finally { hasSetUp = true } diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.kt deleted file mode 100644 index 4cb0e3477..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/RustBackendFailedException.kt +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid - -@RustV1Cleanup("This exists to force implementers to handle a `rsdroid failed to load` case" + - "as I do not trust our ~16k target devices will all export the appropriate" + - "functions allowing for rsdroid to be loaded." + - "This exists to ensure that there is a valid (working) fallback for V1 of the rust conversion" + - "Once we prove this to be incorrect (or fix the bugs), we could remove this and assume that" + - "rsdroid will always load without issue") -class RustBackendFailedException : Exception { - constructor(error: Throwable?) : super(error) - constructor(message: String?) : super(message) -} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiSupportSQLiteDatabase.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiSupportSQLiteDatabase.kt new file mode 100644 index 000000000..24ae1b880 --- /dev/null +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/AnkiSupportSQLiteDatabase.kt @@ -0,0 +1,59 @@ +/* + * Copyright (c) 2022 Ankitects Pty Ltd + * + * 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 net.ankiweb.rsdroid.database + +import android.content.Context +import androidx.sqlite.db.SupportSQLiteDatabase +import androidx.sqlite.db.SupportSQLiteOpenHelper +import androidx.sqlite.db.framework.FrameworkSQLiteOpenHelperFactory +import net.ankiweb.rsdroid.Backend + +/** + * Helper routines for constructing Rust-backed and Framework-backed + * SupportSQLiteDatabases. + */ +abstract class AnkiSupportSQLiteDatabase { + companion object { + /** + * Wrap a Rust backend connection (which provides an SQL interface). + * Caller is responsible for opening&closing the database. + */ + @JvmStatic + fun withRustBackend(backend: Backend): SupportSQLiteDatabase { + return RustSupportSQLiteDatabase(backend) + } + + /** + * Open a connection using the Android framework. + * If path is not provided, an in-memory database is opened. + */ + @JvmStatic + @JvmOverloads + fun withFramework(context: Context, path: String?, dbCallback: SupportSQLiteOpenHelper.Callback? = null): SupportSQLiteDatabase { + val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) + .name(path) + .callback(dbCallback ?: DefaultDbCallback(1)) + .build() + return FrameworkSQLiteOpenHelperFactory().create(configuration).writableDatabase + } + } + + open class DefaultDbCallback(version: Int) : SupportSQLiteOpenHelper.Callback(version) { + override fun onCreate(db: SupportSQLiteDatabase) {} + override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {} + override fun onCorruption(db: SupportSQLiteDatabase) {} + } +} diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt index ae0496fc2..c7fcf00c1 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteDatabase.kt @@ -48,16 +48,23 @@ import androidx.sqlite.db.SupportSQLiteStatement import net.ankiweb.rsdroid.Backend import java.util.* -class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportSQLiteDatabase { +/** + * A wrapper that allows the Rust backend to act like a standard Android + * database. Key differences: + * - the backend must be instructed to open the collection by the caller + * - .close() is a no-op - you should call .close() on the collection (or + * backend directly in testing) instead + * - isOpen and getPath return dummy data + */ +class RustSupportSQLiteDatabase(backend: Backend) : SupportSQLiteDatabase { private val sessionFactory: ThreadLocal - private val isReadOnly: Boolean - private var isOpen: Boolean + override fun isReadOnly(): Boolean { - return isReadOnly + return false } override fun isOpen(): Boolean { - return isOpen + return true } override fun compileStatement(sql: String): SupportSQLiteStatement { @@ -186,8 +193,8 @@ class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportS throw NotImplementedException.todo() } - override fun getPath(): String? { - return session.getPath() + override fun getPath(): String { + return "" } override fun isWriteAheadLoggingEnabled(): Boolean { @@ -208,8 +215,7 @@ class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportS } override fun close() { - isOpen = false - session.closeDatabase() + // no-op } /* Not part of interface */ @@ -304,9 +310,6 @@ class RustSupportSQLiteDatabase(backend: Backend?, readOnly: Boolean) : SupportS } init { - requireNotNull(backend) { "backend was null" } sessionFactory = SessionThreadLocal(backend) - isReadOnly = readOnly - isOpen = true } } \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt deleted file mode 100644 index cbdb176b0..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustSupportSQLiteOpenHelper.kt +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database - -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import net.ankiweb.rsdroid.Backend -import net.ankiweb.rsdroid.BackendFactory - -abstract class RustSupportSQLiteOpenHelper : SupportSQLiteOpenHelper { - protected val configuration: SupportSQLiteOpenHelper.Configuration? - protected val backend: Backend? - protected var backendFactory: BackendFactory? = null - protected var database: SupportSQLiteDatabase? = null - - constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) { - this.configuration = configuration - this.backendFactory = backendFactory - backend = null - } - - constructor(backend: Backend) { - check(backend.isOpen()) { "Backend should be open" } - this.backend = backend - configuration = null - } - - override fun getDatabaseName(): String? { - return backend?.getPath() - ?: if (configuration != null) { - configuration.name - } else { - throw IllegalStateException("Class invalid: no config or backend") - } - } - - override fun setWriteAheadLoggingEnabled(enabled: Boolean) { - throw NotImplementedException() - } - - override fun getWritableDatabase(): SupportSQLiteDatabase { - if (database == null) { - database = createRustSupportSQLiteDatabase(false) - } - return database!! - } - - override fun getReadableDatabase(): SupportSQLiteDatabase { - throw NotImplementedException("Not supported by Rust - requires open collection") - } - - override fun close() {} - protected abstract fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase? -} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.kt deleted file mode 100644 index b5fbb7616..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SQLiteOpenHelperFactory.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database - -import androidx.sqlite.db.SupportSQLiteOpenHelper -import net.ankiweb.rsdroid.BackendFactory - -/** - * Implementation of [SupportSQLiteOpenHelper.Factory] using the Anki Desktop backend - */ -class RustV11SQLiteOpenHelperFactory(private val backendFactory: BackendFactory) : SupportSQLiteOpenHelper.Factory { - override fun create(configuration: SupportSQLiteOpenHelper.Configuration): SupportSQLiteOpenHelper { - return RustV11SupportSQLiteOpenHelper(configuration, backendFactory) - } -} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt deleted file mode 100644 index 36a9f9f86..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustV11SupportSQLiteOpenHelper.kt +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database - -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import net.ankiweb.rsdroid.Backend -import net.ankiweb.rsdroid.BackendFactory -import net.ankiweb.rsdroid.BackendUtils.openAnkiDroidCollection -import timber.log.Timber - -class RustV11SupportSQLiteOpenHelper : RustSupportSQLiteOpenHelper { - constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) : super(configuration, backendFactory) - constructor(backend: Backend) : super(backend) - - override fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase { - Timber.d("createRustSupportSQLiteDatabase") - return if (configuration != null) { - val backend = backendFactory!!.getBackend() - openAnkiDroidCollection(backend, configuration.name, true) - RustSupportSQLiteDatabase(backend, readOnly) - } else { - RustSupportSQLiteDatabase(backend, readOnly) - } - } -} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.kt deleted file mode 100644 index ec0ce202c..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSQLiteOpenHelperFactory.kt +++ /dev/null @@ -1,28 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database - -import androidx.sqlite.db.SupportSQLiteOpenHelper -import net.ankiweb.rsdroid.BackendFactory - -/** - * Implementation of [SupportSQLiteOpenHelper.Factory] using the Anki Desktop backend - */ -class RustVNextSQLiteOpenHelperFactory(private val backendFactory: BackendFactory) : SupportSQLiteOpenHelper.Factory { - override fun create(configuration: SupportSQLiteOpenHelper.Configuration): SupportSQLiteOpenHelper { - return RustVNextSupportSQLiteOpenHelper(configuration, backendFactory) - } -} \ No newline at end of file diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt deleted file mode 100644 index 188fb0a03..000000000 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/database/RustVNextSupportSQLiteOpenHelper.kt +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.database - -import androidx.sqlite.db.SupportSQLiteDatabase -import androidx.sqlite.db.SupportSQLiteOpenHelper -import net.ankiweb.rsdroid.Backend -import net.ankiweb.rsdroid.BackendFactory -import net.ankiweb.rsdroid.BackendUtils.openAnkiDroidCollection -import timber.log.Timber - -class RustVNextSupportSQLiteOpenHelper : RustSupportSQLiteOpenHelper { - constructor(configuration: SupportSQLiteOpenHelper.Configuration, backendFactory: BackendFactory?) : super(configuration, backendFactory) - constructor(backend: Backend) : super(backend) - - override fun createRustSupportSQLiteDatabase(readOnly: Boolean): SupportSQLiteDatabase { - Timber.d("createRustSupportSQLiteDatabase") - return if (configuration != null) { - val backend = backendFactory!!.getBackend() - // openCollection opens and upgrades the collection - openAnkiDroidCollection(backend, configuration.name, false) - RustSupportSQLiteDatabase(backend, readOnly) - } else { - RustSupportSQLiteDatabase(backend, readOnly) - } - } -} \ No newline at end of file diff --git a/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java b/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java index ca6f1ff50..b7671d33d 100644 --- a/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java +++ b/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java @@ -10,8 +10,9 @@ import androidx.test.core.app.ApplicationProvider; import androidx.test.ext.junit.runners.AndroidJUnit4; +import net.ankiweb.rsdroid.Backend; import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.database.RustV11SupportSQLiteOpenHelper; +import net.ankiweb.rsdroid.database.AnkiSupportSQLiteDatabase; import net.ankiweb.rsdroid.testing.RustBackendLoader; import org.junit.Before; @@ -38,13 +39,13 @@ public void ensureCollectionCreatedIsValid() { // We use this routine in AnkiDroid to create the collection, therefore we need to ensure // that the database is valid, open, and the values returned match how the Java used to work - BackendFactory backendV1 = TestUtil.getBackendFactory(); - String path = new File(getTargetContext().getFilesDir(), "collection.anki2").getAbsolutePath(); Configuration config = getConfiguration(path); - SupportSQLiteDatabase database = new RustV11SupportSQLiteOpenHelper(config, backendV1).getWritableDatabase(); + Backend backend = BackendFactory.getBackend(getTargetContext()); + backend.openCollection(":memory:"); + SupportSQLiteDatabase database = AnkiSupportSQLiteDatabase.withRustBackend(backend); database.beginTransaction(); try { diff --git a/rsdroid/src/test/java/net/ankiweb/TestUtil.java b/rsdroid/src/test/java/net/ankiweb/TestUtil.java deleted file mode 100644 index d063ddb45..000000000 --- a/rsdroid/src/test/java/net/ankiweb/TestUtil.java +++ /dev/null @@ -1,30 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb; - -import net.ankiweb.rsdroid.BackendFactory; -import net.ankiweb.rsdroid.RustBackendFailedException; - -public class TestUtil { - public static BackendFactory getBackendFactory() { - try { - return BackendFactory.createInstance(); - } catch (RustBackendFailedException e) { - throw new RuntimeException(e); - } - } -} From 8b2d06effebda702cca1a0e80cf3183cacabca1e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 18 Jun 2022 19:44:58 +1000 Subject: [PATCH 22/61] Bump version --- gradle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gradle.properties b/gradle.properties index 266f79bfa..c31fd4862 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ android.useAndroidX=true android.enableJetifier=false GROUP=io.github.david-allison-1 -VERSION_NAME=0.1.12-anki2.1.53 +VERSION_NAME=0.1.13-anki2.1.53 POM_INCEPTION_YEAR=2020 From 80b60f70ab42f9a375dde697be76f83c2a29b70a Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jun 2022 15:08:41 +1000 Subject: [PATCH 23/61] Update easy-testing instructions --- README.md | 4 ++-- docs/easy-testing.md | 21 ++++++++++++++++++--- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index f1cc70368..e8c38ada0 100644 --- a/README.md +++ b/README.md @@ -20,8 +20,8 @@ Adapter allowing AnkiDroid to leverage Anki Desktop's Rust-based business logic Pre-built version: ```gradle - implementation "io.github.david-allison-1:anki-android-backend:0.1.12" - testImplementation "io.github.david-allison-1:anki-android-backend-testing:0.1.12" + implementation "io.github.david-allison-1:anki-android-backend:0.1.13-anki2.1.53" + testImplementation "io.github.david-allison-1:anki-android-backend-testing:0.1.13-anki2.1.53" ``` See ./docs for info on building a version for testing. diff --git a/docs/easy-testing.md b/docs/easy-testing.md index 7d5b246b6..b883b2f20 100644 --- a/docs/easy-testing.md +++ b/docs/easy-testing.md @@ -99,7 +99,7 @@ Tell gradle to load the compiled .aar and .jar files from disk: ```diff diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle -index 2a2d94034..b21c7caff 100644 +index c1a697f64..07b34189d 100644 --- a/AnkiDroid/build.gradle +++ b/AnkiDroid/build.gradle @@ -271,10 +271,10 @@ dependencies { @@ -109,15 +109,30 @@ index 2a2d94034..b21c7caff 100644 - implementation "io.github.david-allison-1:anki-android-backend:$ankidroid_backend_version" - testImplementation "io.github.david-allison-1:anki-android-backend-testing:$ankidroid_backend_version" - // implementation files("../../Anki-Android-Backend/rsdroid/build/outputs/aar/rsdroid-release.aar") -- // testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-0.1.12.jar") +- // testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-${ankidroid_backend_version}.jar") + // implementation "io.github.david-allison-1:anki-android-backend:$ankidroid_backend_version" + // testImplementation "io.github.david-allison-1:anki-android-backend-testing:$ankidroid_backend_version" + implementation files("../../Anki-Android-Backend/rsdroid/build/outputs/aar/rsdroid-release.aar") -+ testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-0.1.12.jar") ++ testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-${ankidroid_backend_version}.jar") // On Windows, you can use something like // implementation files("C:\\GitHub\\Rust-Test\\rsdroid\\build\\outputs\\aar\\rsdroid-release.aar") +diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.java b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.java +index 3cfd04408..525490e8f 100644 +--- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.java ++++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.java +@@ -142,6 +142,7 @@ public class AnkiDroidApp extends Application { + */ + @Override + public void onCreate() { ++ BackendFactory.setDefaultLegacySchema(false); + super.onCreate(); + if (sInstance != null) { + Timber.i("onCreate() called multiple times"); ``` +Also make sure ext.ankidroid_backend_version in AnkiDroid/build.gradle matches the version +of the backend you're testing. + After making the change, force a gradle sync, and then you should be able to build and run the project on an x86_64 emulator/device, and run unit tests. From 5499f2f9c9ba5b4851e8cbf79189e08969b5af3d Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jun 2022 11:43:42 +1000 Subject: [PATCH 24/61] Convert RustBackendLoader to Kotlin --- rsdroid-testing/build.gradle | 20 ++ .../rsdroid/testing/RustBackendLoader.java | 181 ------------------ .../rsdroid/testing/RustBackendLoader.kt | 168 ++++++++++++++++ 3 files changed, 188 insertions(+), 181 deletions(-) delete mode 100644 rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.java create mode 100644 rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt diff --git a/rsdroid-testing/build.gradle b/rsdroid-testing/build.gradle index 8641b8fca..3bfd53e2a 100644 --- a/rsdroid-testing/build.gradle +++ b/rsdroid-testing/build.gradle @@ -15,6 +15,7 @@ */ apply plugin: 'java-library' +apply plugin: 'kotlin' apply plugin: 'signing' apply plugin: 'com.vanniktech.maven.publish' @@ -22,6 +23,7 @@ dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) // obtaining the OS implementation 'org.apache.commons:commons-exec:1.3' + implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" } jar { @@ -214,4 +216,22 @@ signing { logger.warn("$message: ${hasPrivate}, ${hasPassword}, ${pk == null || "" == pk}, ${pwd == null || "" == pwd}") } +} +buildscript { + repositories { + mavenCentral() + } + dependencies { + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" + } +} +compileKotlin { + kotlinOptions { + jvmTarget = "1.8" + } +} +compileTestKotlin { + kotlinOptions { + jvmTarget = "1.8" + } } \ No newline at end of file diff --git a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.java b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.java deleted file mode 100644 index 1d4140499..000000000 --- a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Copyright (c) 2020 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 net.ankiweb.rsdroid.testing; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.io.OutputStream; -import java.util.HashMap; - -import org.apache.commons.exec.OS; - -/** - * Loads a librsdroid.so alternative to allow testing of rsdroid under a Robolectric-based environment - */ -public class RustBackendLoader { - - private static boolean alreadyLoaded; - private static final HashMap FILENAME_TO_PATH_CACHE = new HashMap<>(); - - public static boolean PRINT_DEBUG = false; - - /** - * Allows unit testing rsdroid under Robolectric
- * Loads (via {@link Runtime#load(String)}) a librsdroid.so alternative compiled for the current operating system.

- * - * This call is cached and is a no-op if called multiple times. - * - * @throws IllegalStateException OS is not Windows, Linux or macOS - * @throws RuntimeException Failure when extracting library to load - * @throws UnsatisfiedLinkError The library could not be loaded - */ - public static void init() { - if (!alreadyLoaded) { - - // This should help diagnose some issues, - print("loading rsdroid-testing for: " + System.getProperty("os.name")); - - if (OS.isFamilyWindows()) { - load("rsdroid", ".dll"); - } else if (OS.isFamilyMac()) { - load("librsdroid", ".dylib"); - } else if (OS.isFamilyUnix()) { - load("librsdroid", ".so"); - } else { - String osName = System.getProperty("os.name"); - throw new IllegalStateException(String.format("Could not determine OS Type for: '%s'", osName)); - } - - alreadyLoaded = true; - } - } - - private static void print(String message) { - if (PRINT_DEBUG) { - System.out.println(message); - } - } - - /** - * Allows unit testing rsdroid under Robolectric
- * Loads (via {@link Runtime#load(String)}) a librsdroid.so alternative compiled for the current operating system.

- * - * @param filePath A full path to the compiled .dll/.dylib/.so - */ - public static void loadRsdroid(String filePath) { - if (alreadyLoaded) { - return; - } - - loadPath(filePath); - alreadyLoaded = true; - } - - /** - * loads a named file in the jar via {@link Runtime#load(String)} - * - * @param fileName The name of the file in the jar - * @param extension The extension of the file in the jar - * - * @throws UnsatisfiedLinkError The library could not be loaded - * @throws RuntimeException Failure when extracting library to load - */ - private static void load(String fileName, String extension) { - String path; - try { - path = getPathFromResourceStream(fileName, extension); - } catch (IOException e) { - throw new RuntimeException(e); - } - - loadPath(path); - } - - private static void loadPath(String path) { - try { - Runtime.getRuntime().load(path); - } catch (UnsatisfiedLinkError e) { - if (!new File(path).exists()) { - FileNotFoundException exception = new FileNotFoundException("Extracted file was not found. Maybe the temp folder was deleted. Please try again: '" + path + "'"); - throw new RuntimeException(exception); - } - if (e.getMessage() == null || !e.getMessage().contains("already loaded in another classloader")) { - throw e; - } - } - } - - /** - * Extracts a named file from a JAR and saves it to a temp folder - * - * @param fileName The name of the file in the jar - * @param extension The extension of the file in the jar - * @return A path (on disk) to the extracted file from the JAR - * @throws IllegalStateException The named file did not exist in the jar. - * @throws IOException Error copying the file to the filesystem - */ - private static String getPathFromResourceStream(String fileName, String extension) throws IOException { - // TODO: Ensure that this is reasonably handled without too much copying. - // Note: this will leave some data in the temp folder. - String fullFilename = fileName + extension; - - // maintain a cache to the files so we reduce IO activity if a file has already been extracted. - if (FILENAME_TO_PATH_CACHE.containsKey(fullFilename)) { - return FILENAME_TO_PATH_CACHE.get(fullFilename); - } - - String path = File.createTempFile(fileName, extension).getAbsolutePath(); - File targetFile = new File(path); - - // If our temp file already exists, return it - // Likely a logical impossibility due to the implementation of createTempFile - if (targetFile.exists() && targetFile.length() > 0) { - return path; - } - - try (InputStream rsdroid = RustBackendLoader.class.getClassLoader().getResourceAsStream(fullFilename)) { - if (rsdroid == null) { - throw new IllegalStateException("Could not find " + fullFilename); - } - - try (OutputStream outStream = convertToOutputStream(targetFile)) { - byte[] buffer = new byte[8 * 1024]; - int bytesRead; - while ((bytesRead = rsdroid.read(buffer)) != -1) { - outStream.write(buffer, 0, bytesRead); - } - } - } - - FILENAME_TO_PATH_CACHE.put(fullFilename, path); - - return path; - } - - private static OutputStream convertToOutputStream(File targetFile) throws IOException { - OutputStream outStream; - try { - outStream = new FileOutputStream(targetFile); - } catch (Exception e) { - throw new IOException("Could not open output file: {}", e); - } - return outStream; - } -} \ No newline at end of file diff --git a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt new file mode 100644 index 000000000..932f1131a --- /dev/null +++ b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt @@ -0,0 +1,168 @@ +/* + * Copyright (c) 2020 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 net.ankiweb.rsdroid.testing + +import org.apache.commons.exec.OS +import java.io.* +import java.lang.Exception +import java.lang.IllegalStateException +import java.lang.RuntimeException +import java.util.HashMap +import kotlin.Throws + +/** + * Loads a librsdroid.so alternative to allow testing of rsdroid under a Robolectric-based environment + */ +object RustBackendLoader { + private var alreadyLoaded = false + private val FILENAME_TO_PATH_CACHE = HashMap() + var PRINT_DEBUG = false + + /** + * Allows unit testing rsdroid under Robolectric

+ * Loads (via [Runtime.load]) a librsdroid.so alternative compiled for the current operating system.



+ * + * This call is cached and is a no-op if called multiple times. + * + * @throws IllegalStateException OS is not Windows, Linux or macOS + * @throws RuntimeException Failure when extracting library to load + * @throws UnsatisfiedLinkError The library could not be loaded + */ + @JvmStatic + fun init() { + if (!alreadyLoaded) { + + // This should help diagnose some issues, + print("loading rsdroid-testing for: " + System.getProperty("os.name")) + if (OS.isFamilyWindows()) { + load("rsdroid", ".dll") + } else if (OS.isFamilyMac()) { + load("librsdroid", ".dylib") + } else if (OS.isFamilyUnix()) { + load("librsdroid", ".so") + } else { + val osName = System.getProperty("os.name") + throw IllegalStateException(String.format("Could not determine OS Type for: '%s'", osName)) + } + alreadyLoaded = true + } + } + + private fun print(message: String) { + if (PRINT_DEBUG) { + println(message) + } + } + + /** + * Allows unit testing rsdroid under Robolectric

+ * Loads (via [Runtime.load]) a librsdroid.so alternative compiled for the current operating system.



+ * + * @param filePath A full path to the compiled .dll/.dylib/.so + */ + fun loadRsdroid(filePath: String) { + if (alreadyLoaded) { + return + } + loadPath(filePath) + alreadyLoaded = true + } + + /** + * loads a named file in the jar via [Runtime.load] + * + * @param fileName The name of the file in the jar + * @param extension The extension of the file in the jar + * + * @throws UnsatisfiedLinkError The library could not be loaded + * @throws RuntimeException Failure when extracting library to load + */ + private fun load(fileName: String, extension: String) { + val path: String? + path = try { + getPathFromResourceStream(fileName, extension) + } catch (e: IOException) { + throw RuntimeException(e) + } + loadPath(path!!) + } + + private fun loadPath(path: String) { + try { + Runtime.getRuntime().load(path) + } catch (e: UnsatisfiedLinkError) { + if (!File(path).exists()) { + val exception = FileNotFoundException("Extracted file was not found. Maybe the temp folder was deleted. Please try again: '$path'") + throw RuntimeException(exception) + } + if (e.message == null || !e.message!!.contains("already loaded in another classloader")) { + throw e + } + } + } + + /** + * Extracts a named file from a JAR and saves it to a temp folder + * + * @param fileName The name of the file in the jar + * @param extension The extension of the file in the jar + * @return A path (on disk) to the extracted file from the JAR + * @throws IllegalStateException The named file did not exist in the jar. + * @throws IOException Error copying the file to the filesystem + */ + @Throws(IOException::class) + private fun getPathFromResourceStream(fileName: String, extension: String): String? { + // TODO: Ensure that this is reasonably handled without too much copying. + // Note: this will leave some data in the temp folder. + val fullFilename = fileName + extension + + // maintain a cache to the files so we reduce IO activity if a file has already been extracted. + if (FILENAME_TO_PATH_CACHE.containsKey(fullFilename)) { + return FILENAME_TO_PATH_CACHE[fullFilename] + } + val path = File.createTempFile(fileName, extension).absolutePath + val targetFile = File(path) + + // If our temp file already exists, return it + // Likely a logical impossibility due to the implementation of createTempFile + if (targetFile.exists() && targetFile.length() > 0) { + return path + } + RustBackendLoader::class.java.classLoader!!.getResourceAsStream(fullFilename).use { rsdroid -> + checkNotNull(rsdroid) { "Could not find $fullFilename" } + convertToOutputStream(targetFile).use { outStream -> + val buffer = ByteArray(8 * 1024) + var bytesRead: Int + while (rsdroid.read(buffer).also { bytesRead = it } != -1) { + outStream.write(buffer, 0, bytesRead) + } + } + } + FILENAME_TO_PATH_CACHE[fullFilename] = path + return path + } + + @Throws(IOException::class) + private fun convertToOutputStream(targetFile: File): OutputStream { + val outStream: OutputStream + outStream = try { + FileOutputStream(targetFile) + } catch (e: Exception) { + throw IOException("Could not open output file: {}", e) + } + return outStream + } +} \ No newline at end of file From ec0226b6c588dffca1f22437739ddd8e01bc8f76 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jun 2022 12:45:59 +1000 Subject: [PATCH 25/61] Fix /tmp filling up with librsdroid copies Closes #175 --- .../rsdroid/testing/RustBackendLoader.kt | 52 ++++++------------- 1 file changed, 15 insertions(+), 37 deletions(-) diff --git a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt index 932f1131a..5eb6df46e 100644 --- a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt +++ b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt @@ -20,6 +20,7 @@ import java.io.* import java.lang.Exception import java.lang.IllegalStateException import java.lang.RuntimeException +import java.security.MessageDigest import java.util.HashMap import kotlin.Throws @@ -91,13 +92,8 @@ object RustBackendLoader { * @throws RuntimeException Failure when extracting library to load */ private fun load(fileName: String, extension: String) { - val path: String? - path = try { - getPathFromResourceStream(fileName, extension) - } catch (e: IOException) { - throw RuntimeException(e) - } - loadPath(path!!) + val path = getPathFromResourceStream(fileName, extension) + loadPath(path) } private fun loadPath(path: String) { @@ -124,45 +120,27 @@ object RustBackendLoader { * @throws IOException Error copying the file to the filesystem */ @Throws(IOException::class) - private fun getPathFromResourceStream(fileName: String, extension: String): String? { + private fun getPathFromResourceStream(fileName: String, extension: String): String { // TODO: Ensure that this is reasonably handled without too much copying. // Note: this will leave some data in the temp folder. val fullFilename = fileName + extension // maintain a cache to the files so we reduce IO activity if a file has already been extracted. if (FILENAME_TO_PATH_CACHE.containsKey(fullFilename)) { - return FILENAME_TO_PATH_CACHE[fullFilename] + return FILENAME_TO_PATH_CACHE[fullFilename]!! } - val path = File.createTempFile(fileName, extension).absolutePath - val targetFile = File(path) - // If our temp file already exists, return it - // Likely a logical impossibility due to the implementation of createTempFile - if (targetFile.exists() && targetFile.length() > 0) { - return path - } - RustBackendLoader::class.java.classLoader!!.getResourceAsStream(fullFilename).use { rsdroid -> - checkNotNull(rsdroid) { "Could not find $fullFilename" } - convertToOutputStream(targetFile).use { outStream -> - val buffer = ByteArray(8 * 1024) - var bytesRead: Int - while (rsdroid.read(buffer).also { bytesRead = it } != -1) { - outStream.write(buffer, 0, bytesRead) - } - } + val bytes = RustBackendLoader::class.java.classLoader!!.getResourceAsStream(fullFilename).use { stream -> + stream.readAllBytes() } - FILENAME_TO_PATH_CACHE[fullFilename] = path - return path - } - - @Throws(IOException::class) - private fun convertToOutputStream(targetFile: File): OutputStream { - val outStream: OutputStream - outStream = try { - FileOutputStream(targetFile) - } catch (e: Exception) { - throw IOException("Could not open output file: {}", e) + val checksum = MessageDigest.getInstance("SHA-1").digest(bytes).joinToString("") { "%02x".format(it) } + val expectedFile = File(System.getProperty("java.io.tmpdir"), "$fileName-$checksum$extension") + if (!expectedFile.exists()) { + val tempFile = File.createTempFile(fileName, extension) + tempFile.writeBytes(bytes) + tempFile.renameTo(expectedFile) } - return outStream + FILENAME_TO_PATH_CACHE[fullFilename] = expectedFile.path + return expectedFile.absolutePath } } \ No newline at end of file From 2ed5c23522107dc8a39f4aead45b196ae57c6dab Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jun 2022 13:32:59 +1000 Subject: [PATCH 26/61] Stream library to avoid Windows test OOM I'm surprised it can't handle a 45MB alloc. Switched to a streaming approach, that requires reading the file twice in the initial write case. --- .../rsdroid/testing/RustBackendLoader.kt | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt index 5eb6df46e..e62ce890f 100644 --- a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt +++ b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt @@ -130,17 +130,36 @@ object RustBackendLoader { return FILENAME_TO_PATH_CACHE[fullFilename]!! } - val bytes = RustBackendLoader::class.java.classLoader!!.getResourceAsStream(fullFilename).use { stream -> - stream.readAllBytes() + + val buffer = ByteArray(8 * 1024) + val checksum = withStream(fullFilename) { stream -> + val digest = MessageDigest.getInstance("SHA-1") + var bytesRead: Int + while (stream.read(buffer).also { bytesRead = it } != -1) { + digest.update(buffer, 0, bytesRead) + } + digest.digest().joinToString("") { "%02x".format(it) } } - val checksum = MessageDigest.getInstance("SHA-1").digest(bytes).joinToString("") { "%02x".format(it) } val expectedFile = File(System.getProperty("java.io.tmpdir"), "$fileName-$checksum$extension") if (!expectedFile.exists()) { val tempFile = File.createTempFile(fileName, extension) - tempFile.writeBytes(bytes) + tempFile.outputStream().use { outStream -> + withStream(fullFilename) { inStream -> + var bytesRead: Int + while (inStream.read(buffer).also { bytesRead = it } != -1) { + outStream.write(buffer, 0, bytesRead) + } + } + outStream.flush() + outStream.close() + } tempFile.renameTo(expectedFile) } FILENAME_TO_PATH_CACHE[fullFilename] = expectedFile.path return expectedFile.absolutePath } + + private fun withStream(fullFilename: String, func: (InputStream) -> T): T { + return func(RustBackendLoader::class.java.classLoader!!.getResourceAsStream(fullFilename)) + } } \ No newline at end of file From ef3ddaea2ecf6b07005e156e6216ee55b4e10fbc Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jun 2022 15:26:42 +1000 Subject: [PATCH 27/61] Fix extraction of panic strings --- rslib-bridge/src/lib.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/rslib-bridge/src/lib.rs b/rslib-bridge/src/lib.rs index c681c51cb..63ec16f2d 100644 --- a/rslib-bridge/src/lib.rs +++ b/rslib-bridge/src/lib.rs @@ -83,9 +83,7 @@ where { let result = match catch_unwind(AssertUnwindSafe(func)) { Ok(result) => result, - Err(panic) => Err(panic_to_anki_error(&panic) - .into_protobuf(tr) - .encode_to_vec()), + Err(panic) => Err(panic_to_anki_error(panic).into_protobuf(tr).encode_to_vec()), }; pack_result(result, env) } @@ -114,12 +112,15 @@ fn pack_result(result: Result, Vec>, env: &JNIEnv) -> jarray { outer_array } -fn panic_to_anki_error(s: &(dyn Any + Send)) -> AnkiError { - if let Some(msg) = s.downcast_ref::() { - AnkiError::FatalError(msg.to_string()) - } else { - // The TypeId (the only thing you can reasonably get from Any) doesn't carry the type name - // Confirm an as_ref() rather than a borrow was passed in here. - AnkiError::FatalError("panic with unknown info".to_string()) - } +fn panic_to_anki_error(panic: Box) -> AnkiError { + AnkiError::FatalError( + match panic.downcast_ref::<&'static str>() { + Some(msg) => *msg, + None => match panic.downcast_ref::() { + Some(msg) => msg.as_str(), + None => "unknown panic", + }, + } + .to_string(), + ) } From 7794f3f85e7478b1b722ceb5563edc2223964a95 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jun 2022 15:48:14 +1000 Subject: [PATCH 28/61] Add buildinfo.txt file The sync code expects to find the version number in a buildinfo.txt file. For now, check it into source control so that Bazel is only required when bumping the anki dependency (or you could edit the file manually) --- .cargo/config.toml | 3 ++- rslib-bridge/buildinfo.txt | 2 ++ tools/get-buildinfo.sh | 7 +++++++ 3 files changed, 11 insertions(+), 1 deletion(-) create mode 100755 rslib-bridge/buildinfo.txt create mode 100755 tools/get-buildinfo.sh diff --git a/.cargo/config.toml b/.cargo/config.toml index 81d5a1758..89e95af7f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,4 @@ [env] RSLIB_FTL_ROOT = { value = "ftl/core/l10n.toml", relative = true } - +BUILDINFO = { value = "rslib-bridge/buildinfo.txt", relative = true } +BAZEL = "1" diff --git a/rslib-bridge/buildinfo.txt b/rslib-bridge/buildinfo.txt new file mode 100755 index 000000000..71ecf6174 --- /dev/null +++ b/rslib-bridge/buildinfo.txt @@ -0,0 +1,2 @@ +STABLE_VERSION 2.1.53 +STABLE_BUILDHASH 665d81c4 diff --git a/tools/get-buildinfo.sh b/tools/get-buildinfo.sh new file mode 100755 index 000000000..b38a84505 --- /dev/null +++ b/tools/get-buildinfo.sh @@ -0,0 +1,7 @@ +#!/bin/bash +# +# The buildinfo.txt file should be generated as part of the build, +# but for now we'll just check it into source control. + +(cd rslib-bridge/anki && bazel build -c opt buildinfo.txt) +cp rslib-bridge/anki/.bazel/bin/buildinfo.txt rslib-bridge \ No newline at end of file From 1bcd7e77d5c0c5f6763afc0ec9959cfba5bff2c8 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jun 2022 17:05:35 +1000 Subject: [PATCH 29/61] Skip outer lock for routines that do not touch the DB --- .../src/main/java/net/ankiweb/rsdroid/Backend.kt | 13 ++++++++++++- tools/protoc-gen/protoc-gen.py | 9 ++++++++- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt index 5d2a0d975..a6ea0fb4b 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -102,7 +102,8 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), } /** - * All backend methods (except for backend init/close) flow through this. + * All backend methods (except for backend init/close, and those explicitly + * excluded from the mutex) flow through this. */ override fun runMethodRaw(service: Int, method: Int, input: ByteArray): ByteArray { return withBackend { @@ -110,6 +111,16 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), } } + /** + * Translations, progress fetching, and media sync do not require an outer lock. + */ + override fun runMethodRawNoLock(service: Int, method: Int, input: ByteArray): ByteArray { + if (backendPointer == null) { + throw BackendException("Backend has been closed") + } + return unpackResult(NativeMethods.runMethodRaw(backendPointer!!, service, method, input)) + } + /** * Run the provided closure with locked access to the backend. * The backend maintains its own lock for backend commands, so this extra diff --git a/tools/protoc-gen/protoc-gen.py b/tools/protoc-gen/protoc-gen.py index 415e0901c..85d472e1e 100755 --- a/tools/protoc-gen/protoc-gen.py +++ b/tools/protoc-gen/protoc-gen.py @@ -206,10 +206,15 @@ def __repr__(self): method=self.command_num deser=self.messages[input_type_name].as_builder() + if name in ("latestProgress", "syncMedia", "translateString"): + raw_method = "runMethodRawNoLock" + else: + raw_method = "runMethodRaw" + buf = f""" @Throws(BackendException::class) fun {name}Raw(input: ByteArray): ByteArray {{ - return runMethodRaw({service}, {method}, input); + return {raw_method}({service}, {method}, input); }} """ @@ -318,6 +323,8 @@ def generate_code(request, response): @Throws(BackendException::class) protected abstract fun runMethodRaw(service: Int, method: Int, input: ByteArray): ByteArray; +@Throws(BackendException::class) +protected abstract fun runMethodRawNoLock(service: Int, method: Int, input: ByteArray): ByteArray; """ ] From 1ef44847be49611d8179f7634731323187cfb995 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jun 2022 20:08:29 +1000 Subject: [PATCH 30/61] Send logs from the backend to the Android log Closes #24 --- rslib-bridge/Cargo.lock | 31 ++++++++++++++++++++++++++ rslib-bridge/Cargo.toml | 5 ++++- rslib-bridge/src/lib.rs | 6 ++++- rslib-bridge/src/logging.rs | 44 +++++++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 2 deletions(-) create mode 100644 rslib-bridge/src/logging.rs diff --git a/rslib-bridge/Cargo.lock b/rslib-bridge/Cargo.lock index 6558a0244..c418c857c 100644 --- a/rslib-bridge/Cargo.lock +++ b/rslib-bridge/Cargo.lock @@ -41,6 +41,24 @@ dependencies = [ "url", ] +[[package]] +name = "android_log-sys" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85965b6739a430150bdd138e2374a98af0c3ee0d030b3bb7fc3bddff58d0102e" + +[[package]] +name = "android_logger" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b74b7ddf197de32e415d197aa21c1c0cb36e01e4794fd801302280ac7847ee02" +dependencies = [ + "android_log-sys", + "env_logger", + "log", + "once_cell", +] + [[package]] name = "anki" version = "0.0.0" @@ -403,6 +421,16 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "log", + "regex", +] + [[package]] name = "error-chain" version = "0.12.4" @@ -1574,11 +1602,13 @@ dependencies = [ name = "rsdroid" version = "0.1.0" dependencies = [ + "android_logger", "anki", "itertools", "jni", "lazy_static", "lexical-core", + "log", "num_enum", "prost", "prost-build", @@ -1586,6 +1616,7 @@ dependencies = [ "serde", "serde_derive", "serde_json", + "slog", ] [[package]] diff --git a/rslib-bridge/Cargo.toml b/rslib-bridge/Cargo.toml index db28ba8bd..a8a7c9745 100644 --- a/rslib-bridge/Cargo.toml +++ b/rslib-bridge/Cargo.toml @@ -23,9 +23,12 @@ lexical-core = "0.7.5" # picked bundled - TODO: Is this correct? rusqlite = { version = "0.26.0", features = ["trace", "functions", "collation", "bundled"] } +android_logger = "0.11.0" +log = "0.4.17" +slog = "2.7.0" [features] no-android = [] [build-dependencies] -prost-build = "0.9" \ No newline at end of file +prost-build = "0.9" diff --git a/rslib-bridge/src/lib.rs b/rslib-bridge/src/lib.rs index 63ec16f2d..9dc568583 100644 --- a/rslib-bridge/src/lib.rs +++ b/rslib-bridge/src/lib.rs @@ -12,14 +12,18 @@ use prost::Message; use std::any::Any; use std::panic::{catch_unwind, AssertUnwindSafe}; +mod logging; + #[no_mangle] pub unsafe extern "C" fn Java_net_ankiweb_rsdroid_NativeMethods_openBackend( env: JNIEnv, _: JClass, args: jbyteArray, ) -> jarray { + let logger = logging::setup_logging(); + let input = env.convert_byte_array(args).unwrap(); - let result = init_backend(&input, None) + let result = init_backend(&input, Some(logger)) .map(|backend| { let backend_ptr = Box::into_raw(Box::new(backend)) as i64; Int64 { val: backend_ptr }.encode_to_vec() diff --git a/rslib-bridge/src/logging.rs b/rslib-bridge/src/logging.rs new file mode 100644 index 000000000..bcf2de6cf --- /dev/null +++ b/rslib-bridge/src/logging.rs @@ -0,0 +1,44 @@ +//! A simple adaptor that takes log messages from the backend and sends them to +//! the Android logs. + +use android_logger::Config; +use log::Level; +use slog::*; +use std::{fmt, result}; + +pub struct AndroidSerializer; + +impl Serializer for AndroidSerializer { + fn emit_arguments(&mut self, key: Key, val: &fmt::Arguments<'_>) -> Result { + log::debug!("{}={}", key, val); + Ok(()) + } +} + +pub struct AndroidDrain; + +impl Drain for AndroidDrain { + type Ok = (); + type Err = (); + + fn log( + &self, + record: &Record<'_>, + values: &OwnedKVList, + ) -> result::Result { + log::debug!("{}", record.msg()); + + record + .kv() + .serialize(record, &mut AndroidSerializer) + .unwrap(); + values.serialize(record, &mut AndroidSerializer).unwrap(); + + Ok(()) + } +} + +pub(crate) fn setup_logging() -> Logger { + android_logger::init_once(Config::default().with_min_level(Level::Debug)); + Logger::root(AndroidDrain {}.fuse(), slog_o!()) +} From c9903eb0194c41bc2e4d668b71a10e902a74426a Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jun 2022 21:46:57 +1000 Subject: [PATCH 31/61] Reduce logging verbosity Large collections dump thousands of lines in the logs when showing the deck picker. --- .../main/java/net/ankiweb/rsdroid/Backend.kt | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt index a6ea0fb4b..3c3ca18ec 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -56,7 +56,7 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), * Open a backend instance, loading the shared library if not already loaded. */ init { - Timber.i("Opening rust backend with lang=$langs") + Timber.d("Opening rust backend with lang=$langs") NativeMethods.ensureSetup(context) val input = BackendInit.newBuilder() .addAllPreferredLangs(langs) @@ -70,7 +70,7 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), * Close the backend, and any open collection. This object can not be used after this. */ override fun close() { - Timber.i("Closing rust backend") + Timber.d("Closing rust backend") lock.withLock { // Must be checked inside lock to avoid race if (backendPointer != null) { @@ -174,7 +174,7 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), @CheckResult override fun fullQuery(query: String, bindArgs: Array?): JSONArray { return try { - Timber.i("Rust: SQL query: '%s'", query) + Timber.d("Rust: SQL query: '%s'", query) fullQueryInternal(query, bindArgs) } catch (e: JSONException) { throw RuntimeException(e) @@ -188,18 +188,17 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), } override fun insertForId(sql: String, bindArgs: Array?): Long { - Timber.i("Rust: sql insert %s", sql) + Timber.d("Rust: sql insert %s", sql) return super.insertForId(dbRequestJson(sql, bindArgs)) } override fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int { - Timber.i("Rust: executeGetRowsAffected %s", sql) + Timber.d("Rust: executeGetRowsAffected %s", sql) return runDbCommandForRowCount(dbRequestJson(sql, bindArgs)).toInt() } /* Begin Protobuf-based database streaming methods (#6) */ override fun fullQueryProto(query: String, bindArgs: Array?): DBResponse { - Timber.e("Rust: fullQueryProto %s", query) return runDbCommandProto(dbRequestJson(query, bindArgs)) } @@ -209,17 +208,15 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), } override fun cancelCurrentProtoQuery(sequenceNumber: Int) { - Timber.d("cancelCurrentProtoQuery") flushQuery(sequenceNumber) } override fun cancelAllProtoQueries() { - Timber.d("cancelAllProtoQueries") flushAllQueries() } private fun performTransaction(kind: DbRequestKind) { - Timber.i("Rust: transaction %s", kind) + Timber.d("Rust: transaction %s", kind) runDbCommand(dbRequestJson(kind = kind)) } @@ -230,7 +227,7 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), } override fun getColumnNames(sql: String): Array { - Timber.i("Rust: getColumnNames %s", sql) + Timber.d("Rust: getColumnNames %s", sql) return getColumnNamesFromQuery(sql).toTypedArray() } } From 9794ba9075eabe7c4660386f65b959c9f701c385 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 19 Jun 2022 22:41:07 +1000 Subject: [PATCH 32/61] Enable media handling in new backend --- .../main/java/net/ankiweb/rsdroid/Backend.kt | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt index 3c3ca18ec..2f932b903 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -32,6 +32,7 @@ import org.json.JSONException import org.json.JSONObject import timber.log.Timber import java.io.Closeable +import java.io.File import java.util.concurrent.locks.ReentrantLock import kotlin.concurrent.withLock @@ -49,7 +50,21 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), } fun openCollection(collectionPath: String) { - openCollection(collectionPath, "", "", "", legacySchema) + val (mediaFolder, mediaDb) = if (legacySchema || collectionPath == ":memory:") { + listOf("", "") + } else { + listOf(collectionPath.replace(".anki2", ".media"), + collectionPath.replace(".anki2", ".media.db")) + } + openCollection(collectionPath, mediaFolder, mediaDb, "", legacySchema) + } + + /** Forces a full media check on next sync. Only valid with new backend. */ + fun removeMediaDb(colPath: String) { + val file = File(colPath.replace(".anki2", ".media.db")) + if (file.exists()) { + file.delete() + } } /** From c05e6d1a18fdc0c7b506f57bf0f7bdd34fa96ecc Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 21 Jun 2022 20:24:41 +1000 Subject: [PATCH 33/61] Tweaks for Robolectric case - Add an ineffective lock to ensureSetup() - Document why it doesn't work, and why the error needs to be swallowed in loadPath - Don't attempt to load the library normally when running under Robolectric --- .../rsdroid/testing/RustBackendLoader.kt | 37 +++++++++++++------ .../java/net/ankiweb/rsdroid/NativeMethods.kt | 20 +++------- .../net/ankiweb/CollectionCreationTest.java | 2 +- 3 files changed, 32 insertions(+), 27 deletions(-) diff --git a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt index e62ce890f..cd06871c4 100644 --- a/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt +++ b/rsdroid-testing/src/main/java/net/ankiweb/rsdroid/testing/RustBackendLoader.kt @@ -17,7 +17,6 @@ package net.ankiweb.rsdroid.testing import org.apache.commons.exec.OS import java.io.* -import java.lang.Exception import java.lang.IllegalStateException import java.lang.RuntimeException import java.security.MessageDigest @@ -28,7 +27,7 @@ import kotlin.Throws * Loads a librsdroid.so alternative to allow testing of rsdroid under a Robolectric-based environment */ object RustBackendLoader { - private var alreadyLoaded = false + private var hasSetUp = false private val FILENAME_TO_PATH_CACHE = HashMap() var PRINT_DEBUG = false @@ -38,14 +37,22 @@ object RustBackendLoader { * * This call is cached and is a no-op if called multiple times. * + * Note the @Synchronized label is misleading - see the docs for loadPath() + * * @throws IllegalStateException OS is not Windows, Linux or macOS * @throws RuntimeException Failure when extracting library to load * @throws UnsatisfiedLinkError The library could not be loaded */ @JvmStatic - fun init() { - if (!alreadyLoaded) { - + @Synchronized + fun ensureSetup(customPath: String?) { + if (hasSetUp) { + return; + } + if (customPath != null) { + print("loading rsdroid-testing with path $customPath") + loadRsdroid(customPath) + } else { // This should help diagnose some issues, print("loading rsdroid-testing for: " + System.getProperty("os.name")) if (OS.isFamilyWindows()) { @@ -58,8 +65,8 @@ object RustBackendLoader { val osName = System.getProperty("os.name") throw IllegalStateException(String.format("Could not determine OS Type for: '%s'", osName)) } - alreadyLoaded = true } + hasSetUp = true } private fun print(message: String) { @@ -74,12 +81,8 @@ object RustBackendLoader { * * @param filePath A full path to the compiled .dll/.dylib/.so */ - fun loadRsdroid(filePath: String) { - if (alreadyLoaded) { - return - } + private fun loadRsdroid(filePath: String) { loadPath(filePath) - alreadyLoaded = true } /** @@ -96,6 +99,15 @@ object RustBackendLoader { loadPath(path) } + /** + * Subtle behaviour alert: while the routine that calls this is protected with a + * @Synchronized attribution, the lock it uses is based on the classloader that is + * active at the time. JUnit and Robolectric will alter the classloader for different tests + * (eg some do not use Robolectric's classloader at all, and other tests like BindingAndroidTest + * will use multiple classloader instances due to the use of @Config). This means this code + * is not guaranteed to execute only once, and after the first invocation, an "already loaded" + * error will be thrown by Java, which we have to swallow. + */ private fun loadPath(path: String) { try { Runtime.getRuntime().load(path) @@ -106,6 +118,8 @@ object RustBackendLoader { } if (e.message == null || !e.message!!.contains("already loaded in another classloader")) { throw e + } else { + // native library loaded by a different classloader in the same process } } } @@ -130,7 +144,6 @@ object RustBackendLoader { return FILENAME_TO_PATH_CACHE[fullFilename]!! } - val buffer = ByteArray(8 * 1024) val checksum = withStream(fullFilename) { stream -> val digest = MessageDigest.getInstance("SHA-1") diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt index 7d78787c9..b8b143f20 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt @@ -18,25 +18,17 @@ object NativeMethods { return } - // Prevent sqlite throwing error 6410 due to the lack of /tmp if (!isRoboUnitTest) { + // Prevent sqlite throwing error 6410 due to the lack of /tmp val dir = context.cacheDir Os.setenv("TMPDIR", dir.path, false) - } - - try { + // Then load library System.loadLibrary("rsdroid") - } catch (e: UnsatisfiedLinkError) { - if (!isRoboUnitTest) { - throw RuntimeException("backend load failed") - } - // In Robolectric, assume setup works (setupException == null) if the library throws. - // As the library is loaded at a later time (or a failure will be quickly found). - - // fixme: is this roboelectric special case still required? - } finally { - hasSetUp = true + } else { + // Test harness will load the library for us. } + + hasSetUp = true } @CheckResult diff --git a/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java b/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java index b7671d33d..a3f52554b 100644 --- a/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java +++ b/rsdroid/src/test/java/net/ankiweb/CollectionCreationTest.java @@ -31,7 +31,7 @@ public class CollectionCreationTest { @Before public void loadLibrary() { - RustBackendLoader.init(); + RustBackendLoader.ensureSetup(null); } @Test From 2780dfbeb2c6bd2ad723d9939d28089625829348 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 21 Jun 2022 16:35:35 +1000 Subject: [PATCH 34/61] Make it easier to switch between local testing and full build --- build-current.sh | 14 ++++++ docs/easy-testing.md | 93 +++++++----------------------------- rsdroid-testing/build.gradle | 28 ++++++----- rsdroid/build.gradle | 9 +++- 4 files changed, 56 insertions(+), 88 deletions(-) create mode 100755 build-current.sh diff --git a/build-current.sh b/build-current.sh new file mode 100755 index 000000000..c61e53e51 --- /dev/null +++ b/build-current.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# +# Builds for the current architecture only. See docs/easy-testing.md +# + +set -e + +export CURRENT_ONLY=true + +# build simulator/device package +./gradlew assembleRelease + +# build library for Robolectric tests +CARGO_TARGET_DIR=target NO_CROSS=true ./gradlew rsdroid-testing:build diff --git a/docs/easy-testing.md b/docs/easy-testing.md index b883b2f20..d339f2f07 100644 --- a/docs/easy-testing.md +++ b/docs/easy-testing.md @@ -1,5 +1,8 @@ # Testing changes with AnkiDroid on an X86_64 sim (Linux) +A similar approach may work on Mac and Windows, but this has only been tested on Linux +so far. + ## Setup Make sure you can build AnkiDroid first. @@ -15,7 +18,7 @@ Install Rust: - rustup install 1.58.1 - rustup target add x86_64-linux-android -- sudo ln -sf /usr/bin/gcc /usr/bin/x86_64--unknown-linux-gnu-gcc +- sudo ln -sf /usr/bin/gcc /usr/bin/x86_64-unknown-linux-gnu-gcc Install protobuf: @@ -25,40 +28,6 @@ Install Python packages - pip install protobuf stringcase, or see the venv section below -## Limit build to x86_64 - -So you don't need to install cross compilers, patch the sources to -only build the x86_64 image for the aar file and jar: - -```diff -diff --git a/rsdroid/build.gradle b/rsdroid/build.gradle -index bc3a401..7c69a5b 100644 ---- a/rsdroid/build.gradle -+++ b/rsdroid/build.gradle -@@ -72,7 +72,7 @@ dependencies { - - } - --preBuild.dependsOn "cargoBuild" -+preBuild.dependsOn "cargoBuildX86_64" - - signing { - def hasPrivate = project.hasProperty('SIGNING_PRIVATE_KEY') -diff --git a/rsdroid-testing/build.gradle b/rsdroid-testing/build.gradle -index 8641b8f..694212e 100644 ---- a/rsdroid-testing/build.gradle -+++ b/rsdroid-testing/build.gradle -@@ -181,8 +181,6 @@ task copyWindowsOutput(type: Copy) { - // TODO: check for cargo - // check for targets: x86_64-apple-darwin, x86_64-pc-windows-gnu, TODO: Linux - --processResources.dependsOn preBuildWindows --processResources.dependsOn copyWindowsOutput - // To fix: "toolchain 'nightly-x86_64-unknown-linux-gnu' is not installed" - // execute in bash: rustup toolchain install nightly-x86_64-unknown-linux-gnu - // "linker `x86_64-unknown-linux-gnu-gcc` not found" -``` - ## Using a custom python venv If you don't want to `pip install protobuf` globally, you can @@ -86,53 +55,27 @@ Two files need to be built: ``` export ANDROID_SDK_ROOT=$HOME/Android/Sdk export PATH=$HOME/Android/Sdk/cmdline-tools/latest/bin/:$PATH -./gradlew assembleRelease -NO_CROSS=true ./gradlew rsdroid-testing:build +./build-current.sh ``` -If your environment is set up to override the default -Rust output location, you must also set unset CARGO_TARGET_DIR. - ## Modify AnkiDroid to use built library -Tell gradle to load the compiled .aar and .jar files from disk: +Tell gradle to load the compiled .aar and .jar files from disk by editing local.properties +in the AnkiDroid repo, and adding the following line: -```diff -diff --git a/AnkiDroid/build.gradle b/AnkiDroid/build.gradle -index c1a697f64..07b34189d 100644 ---- a/AnkiDroid/build.gradle -+++ b/AnkiDroid/build.gradle -@@ -271,10 +271,10 @@ dependencies { - // - switch the commented and uncommented lines below - // - run a gradle sync - -- implementation "io.github.david-allison-1:anki-android-backend:$ankidroid_backend_version" -- testImplementation "io.github.david-allison-1:anki-android-backend-testing:$ankidroid_backend_version" -- // implementation files("../../Anki-Android-Backend/rsdroid/build/outputs/aar/rsdroid-release.aar") -- // testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-${ankidroid_backend_version}.jar") -+ // implementation "io.github.david-allison-1:anki-android-backend:$ankidroid_backend_version" -+ // testImplementation "io.github.david-allison-1:anki-android-backend-testing:$ankidroid_backend_version" -+ implementation files("../../Anki-Android-Backend/rsdroid/build/outputs/aar/rsdroid-release.aar") -+ testImplementation files("../../Anki-Android-Backend/rsdroid-testing/build/libs/rsdroid-testing-${ankidroid_backend_version}.jar") - - // On Windows, you can use something like - // implementation files("C:\\GitHub\\Rust-Test\\rsdroid\\build\\outputs\\aar\\rsdroid-release.aar") -diff --git a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.java b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.java -index 3cfd04408..525490e8f 100644 ---- a/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.java -+++ b/AnkiDroid/src/main/java/com/ichi2/anki/AnkiDroidApp.java -@@ -142,6 +142,7 @@ public class AnkiDroidApp extends Application { - */ - @Override - public void onCreate() { -+ BackendFactory.setDefaultLegacySchema(false); - super.onCreate(); - if (sInstance != null) { - Timber.i("onCreate() called multiple times"); +``` +local_backend=true +``` + +If you also want to test out the new schema code paths that make greater use of the backend, +add the following line (be warned, do not use this on a collection you care about yet): + +``` +legacy_schema=false ``` Also make sure ext.ankidroid_backend_version in AnkiDroid/build.gradle matches the version of the backend you're testing. -After making the change, force a gradle sync, and then you should be able to build -and run the project on an x86_64 emulator/device, and run unit tests. +After making the change, you should be able to build and run the project on an x86_64 +emulator/device, and run unit tests. diff --git a/rsdroid-testing/build.gradle b/rsdroid-testing/build.gradle index 3bfd53e2a..56cb1b041 100644 --- a/rsdroid-testing/build.gradle +++ b/rsdroid-testing/build.gradle @@ -183,18 +183,22 @@ task copyWindowsOutput(type: Copy) { // TODO: check for cargo // check for targets: x86_64-apple-darwin, x86_64-pc-windows-gnu, TODO: Linux -processResources.dependsOn preBuildWindows -processResources.dependsOn copyWindowsOutput -// To fix: "toolchain 'nightly-x86_64-unknown-linux-gnu' is not installed" -// execute in bash: rustup toolchain install nightly-x86_64-unknown-linux-gnu -// "linker `x86_64-unknown-linux-gnu-gcc` not found" -// sudo ln -s /usr/bin/cc /usr/local/bin/x86_64-unknown-linux-gnu-gcc - -// rustup target add x86_64-pc-windows-gnu --toolchain nightly -// brew install mingw-w64 && x86_64-w64-mingw32-gcc -v -processResources.dependsOn preBuildLinux -processResources.dependsOn copyLinuxOutput - +Boolean wantAllPlatforms = System.getenv("CURRENT_ONLY") != "true" + +if (wantAllPlatforms || org.gradle.internal.os.OperatingSystem.current().isWindows()) { + processResources.dependsOn preBuildWindows + processResources.dependsOn copyWindowsOutput +} +if (wantAllPlatforms || org.gradle.internal.os.OperatingSystem.current().isLinux()) { + // rustup target add x86_64-pc-windows-gnu --toolchain nightly + // brew install mingw-w64 && x86_64-w64-mingw32-gcc -v + // To fix: "toolchain 'nightly-x86_64-unknown-linux-gnu' is not installed" + // execute in bash: rustup toolchain install nightly-x86_64-unknown-linux-gnu + // "linker `x86_64-unknown-linux-gnu-gcc` not found" + // sudo ln -s /usr/bin/cc /usr/local/bin/x86_64-unknown-linux-gnu-gcc + processResources.dependsOn preBuildLinux + processResources.dependsOn copyLinuxOutput +} if (org.gradle.internal.os.OperatingSystem.current().isMacOsX()) { // due to restrictions on downloading the MacOS SDK, we can only build for MacOS on MacOS // > Install a reasonable number of copies of the Apple Software on Apple-branded computers diff --git a/rsdroid/build.gradle b/rsdroid/build.gradle index d342dd1c7..74d4a8e22 100644 --- a/rsdroid/build.gradle +++ b/rsdroid/build.gradle @@ -91,7 +91,14 @@ task generateTranslations(type: Exec) { } preBuild.dependsOn "generateTranslations" -preBuild.dependsOn "cargoBuild" + +Boolean wantAllPlatforms = System.getenv("CURRENT_ONLY") != "true" + +if (wantAllPlatforms) { + preBuild.dependsOn "cargoBuild" +} else { + preBuild.dependsOn "cargoBuildX86_64" +} signing { def hasPrivate = project.hasProperty('SIGNING_PRIVATE_KEY') From 57769a92bea24aeae167b31db9183f7d234b500d Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 21 Jun 2022 21:15:35 +1000 Subject: [PATCH 35/61] Make it easier to use a venv --- .gitignore | 3 +++ docs/easy-testing.md | 17 +++++------------ rslib-bridge/anki | 2 +- tools/genfluent/genfluent.sh | 3 ++- tools/protoc-gen/protoc-gen.sh | 4 +++- tools/setup-python | 11 +++++++++++ 6 files changed, 25 insertions(+), 15 deletions(-) create mode 100644 tools/setup-python diff --git a/.gitignore b/.gitignore index 781c28d33..fe10f09c3 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,6 @@ AnkiDroid/ACRA-INSTALLATION .DS_Store .externalNativeBuild .cxx + +# optional symlink to venv +python diff --git a/docs/easy-testing.md b/docs/easy-testing.md index d339f2f07..b308d7346 100644 --- a/docs/easy-testing.md +++ b/docs/easy-testing.md @@ -31,18 +31,11 @@ Install Python packages ## Using a custom python venv If you don't want to `pip install protobuf` globally, you can -switch to a venv: - -```diff -diff --git a/tools/protoc-gen/protoc-gen.sh b/tools/protoc-gen/protoc-gen.sh -index d4039ec..3ac5d29 100755 ---- a/tools/protoc-gen/protoc-gen.sh -+++ b/tools/protoc-gen/protoc-gen.sh -@@ -1,2 +1,3 @@ - #!/bin/bash --./tools/protoc-gen/protoc-gen.py -+ -+$HOME/Local/python/misc/bin/python3 ./tools/protoc-gen/protoc-gen.py +symlink the python bin from a venv into `python` at the top of +the project folder: + +``` +$ ln -sf /path/to/venv/bin/python python ``` ## Build diff --git a/rslib-bridge/anki b/rslib-bridge/anki index 665d81c48..72bbc414c 160000 --- a/rslib-bridge/anki +++ b/rslib-bridge/anki @@ -1 +1 @@ -Subproject commit 665d81c48001290cb4c6d2ce203afa83d923f34f +Subproject commit 72bbc414cea07b2a481ad58f8f6ba252befab8ff diff --git a/tools/genfluent/genfluent.sh b/tools/genfluent/genfluent.sh index 7313efeff..16551d4d9 100755 --- a/tools/genfluent/genfluent.sh +++ b/tools/genfluent/genfluent.sh @@ -1,3 +1,4 @@ #!/bin/bash -./tools/genfluent/genfluent.py +. tools/setup-python +$PYTHON ./tools/genfluent/genfluent.py diff --git a/tools/protoc-gen/protoc-gen.sh b/tools/protoc-gen/protoc-gen.sh index d4039ecae..63912015a 100755 --- a/tools/protoc-gen/protoc-gen.sh +++ b/tools/protoc-gen/protoc-gen.sh @@ -1,2 +1,4 @@ #!/bin/bash -./tools/protoc-gen/protoc-gen.py + +. tools/setup-python +$PYTHON ./tools/protoc-gen/protoc-gen.py diff --git a/tools/setup-python b/tools/setup-python new file mode 100644 index 000000000..f4d651cf2 --- /dev/null +++ b/tools/setup-python @@ -0,0 +1,11 @@ +#!/bin/bash +# +# If `python` in the project folder exists, use it instead of the global python. +# + +if [ -e python ]; then + PYTHON=$(readlink $(pwd)/python) +else + PYTHON=$(which python3) +fi + From 2768c4279d42397b7247f815d888e574f4a1abbc Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 21 Jun 2022 17:10:49 +1000 Subject: [PATCH 36/61] Rename easy-testing, tweak docs Closes #198 --- README.md | 11 +++-------- build-current.sh | 2 +- docs/{easy-testing.md => TESTING.md} | 15 ++++++++++----- 3 files changed, 14 insertions(+), 14 deletions(-) rename docs/{easy-testing.md => TESTING.md} (88%) diff --git a/README.md b/README.md index e8c38ada0..bf3e498bd 100644 --- a/README.md +++ b/README.md @@ -17,14 +17,9 @@ Adapter allowing AnkiDroid to leverage Anki Desktop's Rust-based business logic ## How to use it in a project -Pre-built version: - -```gradle - implementation "io.github.david-allison-1:anki-android-backend:0.1.13-anki2.1.53" - testImplementation "io.github.david-allison-1:anki-android-backend-testing:0.1.13-anki2.1.53" -``` - -See ./docs for info on building a version for testing. +AnkiDroid uses a pre-built version of this library, and includes it in AnkiDroid/build.gradle. +To build a local version of this library and tell AnkiDroid to use it, please see the instructions +in docs/TESTING.md ## Folders diff --git a/build-current.sh b/build-current.sh index c61e53e51..971110c22 100755 --- a/build-current.sh +++ b/build-current.sh @@ -1,6 +1,6 @@ #!/bin/bash # -# Builds for the current architecture only. See docs/easy-testing.md +# Builds for the current architecture only. See docs/TESTING.md # set -e diff --git a/docs/easy-testing.md b/docs/TESTING.md similarity index 88% rename from docs/easy-testing.md rename to docs/TESTING.md index b308d7346..7d90eaa2f 100644 --- a/docs/easy-testing.md +++ b/docs/TESTING.md @@ -1,5 +1,8 @@ # Testing changes with AnkiDroid on an X86_64 sim (Linux) +It is possible to limit the build to the current architecture, to avoid having +to use Docker or to cross-compile for multiple platforms. + A similar approach may work on Mac and Windows, but this has only been tested on Linux so far. @@ -24,11 +27,7 @@ Install protobuf: - Install protobuf with your package manager -Install Python packages - -- pip install protobuf stringcase, or see the venv section below - -## Using a custom python venv +## Optional Python venv If you don't want to `pip install protobuf` globally, you can symlink the python bin from a venv into `python` at the top of @@ -38,6 +37,12 @@ the project folder: $ ln -sf /path/to/venv/bin/python python ``` +## Install Python packages + +``` +pip install protobuf stringcase +``` + ## Build Two files need to be built: From 4c3eea41779ad6323027cd25bf0e97289a9d2fc3 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 21 Jun 2022 19:07:28 +1000 Subject: [PATCH 37/61] Use zip of desktop artifacts in build Instead of checking buildinfo.txt and the web files into this repo, we should either generate them as part of the build, or rely on a pinned pre-generated file. The latter is the easier short-term solution, as it requires minimal updates to CI, and won't balloon out the build times. It's more work when bumping versions though, so in the long time AD may want to consider adopting Bazel in this repo once this code has sufficiently proven itself. There's probably room for improvement in the download/unpack task - I'll leave that for someone with more Gradle experience. I am not able to test the artifact build/upload from the repo fork, so have temporarily placed the generated file on ankiweb. --- .cargo/config.toml | 2 +- docs/UPDATING.md | 8 ++++++++ rsdroid/build.gradle | 23 ++++++++++++++++++++++- rslib-bridge/buildinfo.txt | 2 -- 4 files changed, 31 insertions(+), 4 deletions(-) create mode 100644 docs/UPDATING.md delete mode 100755 rslib-bridge/buildinfo.txt diff --git a/.cargo/config.toml b/.cargo/config.toml index 89e95af7f..6a50f5a93 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,4 @@ [env] RSLIB_FTL_ROOT = { value = "ftl/core/l10n.toml", relative = true } -BUILDINFO = { value = "rslib-bridge/buildinfo.txt", relative = true } +BUILDINFO = { value = "rsdroid/build/generated/anki_artifacts/buildinfo.txt", relative = true } BAZEL = "1" diff --git a/docs/UPDATING.md b/docs/UPDATING.md new file mode 100644 index 000000000..149af6b81 --- /dev/null +++ b/docs/UPDATING.md @@ -0,0 +1,8 @@ +# Updating to a newer Anki version + +- Take the changes in the current ankidroid-xxx branch in the rslib-bridge/anki + repo, and rebase them over the latest desktop release. +- Use the GitHub action in that repo to generate a new release artifact, which includes + build hash and web files. +- Update artifactZipLocation in rsdroid/build.grade to point to the new artifact +- Test things build locally with ./build-current.sh diff --git a/rsdroid/build.gradle b/rsdroid/build.gradle index 74d4a8e22..75edfb681 100644 --- a/rsdroid/build.gradle +++ b/rsdroid/build.gradle @@ -45,10 +45,31 @@ android { sourceSets { main { kotlin.srcDirs += "build/generated/source/fluent" + resources { + srcDirs "src/main/resources", "build/generated/anki_artifacts" + } } } } +def artifactZipUrl = "https://apps.ankiweb.net/downloads/anki_artifacts.zip" +def artifactZipLocation = "build/artifacts.zip" +def artifactExtractLocation = "build/generated/anki_artifacts" + +task downloadAndPrepareArtifacts { + doLast { + def file = new File("rsdroid/" + artifactZipLocation) + new URL(artifactZipUrl).withInputStream{ i -> file.withOutputStream{ it << i }} + new File(artifactExtractLocation).deleteDir() + } +} + +tasks.register('unpackAnkiArtifacts', Copy) { + dependsOn(downloadAndPrepareArtifacts) + from zipTree(artifactZipLocation) + into artifactExtractLocation +} + // Consider upgrade to DSL: https://docs.gradle.org/current/userguide/plugins.html#sec:plugins_block apply plugin: "org.mozilla.rust-android-gradle.rust-android" @@ -77,7 +98,6 @@ dependencies { testImplementation 'androidx.test:core:1.4.0' testImplementation "androidx.test.ext:junit:${rootProject.ext.androidxTestJunitVersion}" testImplementation project(path: ':rsdroid-testing') - } task generateTranslations(type: Exec) { @@ -94,6 +114,7 @@ preBuild.dependsOn "generateTranslations" Boolean wantAllPlatforms = System.getenv("CURRENT_ONLY") != "true" +preBuild.dependsOn("unpackAnkiArtifacts") if (wantAllPlatforms) { preBuild.dependsOn "cargoBuild" } else { diff --git a/rslib-bridge/buildinfo.txt b/rslib-bridge/buildinfo.txt deleted file mode 100755 index 71ecf6174..000000000 --- a/rslib-bridge/buildinfo.txt +++ /dev/null @@ -1,2 +0,0 @@ -STABLE_VERSION 2.1.53 -STABLE_BUILDHASH 665d81c4 From 60a4233beb21de74f12c672291486898fb11c777 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 21 Jun 2022 19:44:56 +1000 Subject: [PATCH 38/61] Revert "Use zip of desktop artifacts in build" This reverts commit 2828381051599ed845502de926e3f0cb1f7571a1. This has a few issues that need resolving before it's usable: - Need to tell Gradle that unpackAnkiArtifacts should be run prior to the cargo lines - downloadAndPrepareArtifacts chokes on clean build because rsdroid/build does not exist; probably needs creating if missing --- .cargo/config.toml | 2 +- docs/UPDATING.md | 8 -------- rsdroid/build.gradle | 23 +---------------------- rslib-bridge/buildinfo.txt | 2 ++ 4 files changed, 4 insertions(+), 31 deletions(-) delete mode 100644 docs/UPDATING.md create mode 100755 rslib-bridge/buildinfo.txt diff --git a/.cargo/config.toml b/.cargo/config.toml index 6a50f5a93..89e95af7f 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,4 @@ [env] RSLIB_FTL_ROOT = { value = "ftl/core/l10n.toml", relative = true } -BUILDINFO = { value = "rsdroid/build/generated/anki_artifacts/buildinfo.txt", relative = true } +BUILDINFO = { value = "rslib-bridge/buildinfo.txt", relative = true } BAZEL = "1" diff --git a/docs/UPDATING.md b/docs/UPDATING.md deleted file mode 100644 index 149af6b81..000000000 --- a/docs/UPDATING.md +++ /dev/null @@ -1,8 +0,0 @@ -# Updating to a newer Anki version - -- Take the changes in the current ankidroid-xxx branch in the rslib-bridge/anki - repo, and rebase them over the latest desktop release. -- Use the GitHub action in that repo to generate a new release artifact, which includes - build hash and web files. -- Update artifactZipLocation in rsdroid/build.grade to point to the new artifact -- Test things build locally with ./build-current.sh diff --git a/rsdroid/build.gradle b/rsdroid/build.gradle index 75edfb681..74d4a8e22 100644 --- a/rsdroid/build.gradle +++ b/rsdroid/build.gradle @@ -45,31 +45,10 @@ android { sourceSets { main { kotlin.srcDirs += "build/generated/source/fluent" - resources { - srcDirs "src/main/resources", "build/generated/anki_artifacts" - } } } } -def artifactZipUrl = "https://apps.ankiweb.net/downloads/anki_artifacts.zip" -def artifactZipLocation = "build/artifacts.zip" -def artifactExtractLocation = "build/generated/anki_artifacts" - -task downloadAndPrepareArtifacts { - doLast { - def file = new File("rsdroid/" + artifactZipLocation) - new URL(artifactZipUrl).withInputStream{ i -> file.withOutputStream{ it << i }} - new File(artifactExtractLocation).deleteDir() - } -} - -tasks.register('unpackAnkiArtifacts', Copy) { - dependsOn(downloadAndPrepareArtifacts) - from zipTree(artifactZipLocation) - into artifactExtractLocation -} - // Consider upgrade to DSL: https://docs.gradle.org/current/userguide/plugins.html#sec:plugins_block apply plugin: "org.mozilla.rust-android-gradle.rust-android" @@ -98,6 +77,7 @@ dependencies { testImplementation 'androidx.test:core:1.4.0' testImplementation "androidx.test.ext:junit:${rootProject.ext.androidxTestJunitVersion}" testImplementation project(path: ':rsdroid-testing') + } task generateTranslations(type: Exec) { @@ -114,7 +94,6 @@ preBuild.dependsOn "generateTranslations" Boolean wantAllPlatforms = System.getenv("CURRENT_ONLY") != "true" -preBuild.dependsOn("unpackAnkiArtifacts") if (wantAllPlatforms) { preBuild.dependsOn "cargoBuild" } else { diff --git a/rslib-bridge/buildinfo.txt b/rslib-bridge/buildinfo.txt new file mode 100755 index 000000000..71ecf6174 --- /dev/null +++ b/rslib-bridge/buildinfo.txt @@ -0,0 +1,2 @@ +STABLE_VERSION 2.1.53 +STABLE_BUILDHASH 665d81c4 From 1a2afad3f6e1f2fd69dafc7d701e32c04613992b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Tue, 21 Jun 2022 20:22:37 +1000 Subject: [PATCH 39/61] Remove Python 3.8 syntax, allowing CI build to work --- tools/genfluent/genfluent.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tools/genfluent/genfluent.py b/tools/genfluent/genfluent.py index ad844eca2..ae6aaf29d 100755 --- a/tools/genfluent/genfluent.py +++ b/tools/genfluent/genfluent.py @@ -14,7 +14,8 @@ def ensure_i18n_module_correct(): reg = re.compile(r'(\s+)(\S+_(?:commit|zip_csum)) = "(.*)"') for line in open("rslib-bridge/anki/repos.bzl").readlines(): - if m := reg.match(line): + m = reg.match(line) + if m: (indent, key, commit) = m.groups() if key == "core_i18n_commit": subprocess.run(["git", "checkout", commit], cwd="ftl/core", check=True) From 41cc1f5ac7c5b81107ff6ae4de7169ae81d3abdd Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 22 Jun 2022 00:13:16 +1000 Subject: [PATCH 40/61] Exclude awaitBackupCompletion from mutex --- tools/protoc-gen/protoc-gen.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/protoc-gen/protoc-gen.py b/tools/protoc-gen/protoc-gen.py index 85d472e1e..7c6a71b28 100755 --- a/tools/protoc-gen/protoc-gen.py +++ b/tools/protoc-gen/protoc-gen.py @@ -206,7 +206,7 @@ def __repr__(self): method=self.command_num deser=self.messages[input_type_name].as_builder() - if name in ("latestProgress", "syncMedia", "translateString"): + if name in ("latestProgress", "syncMedia", "translateString", "awaitBackupCompletion"): raw_method = "runMethodRawNoLock" else: raw_method = "runMethodRaw" From dd19004e538f5c4603b02b90b23e3473fdf5b2b5 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 22 Jun 2022 15:27:41 +1000 Subject: [PATCH 41/61] Log stdout/stderr to ease println() debugging Android sends it to /dev/null by default --- rslib-bridge/Cargo.lock | 22 ++++++++++++++++++++++ rslib-bridge/Cargo.toml | 1 + rslib-bridge/src/logging.rs | 33 +++++++++++++++++++++++++++++++++ 3 files changed, 56 insertions(+) diff --git a/rslib-bridge/Cargo.lock b/rslib-bridge/Cargo.lock index c418c857c..eeab8cf17 100644 --- a/rslib-bridge/Cargo.lock +++ b/rslib-bridge/Cargo.lock @@ -461,6 +461,17 @@ dependencies = [ "instant", ] +[[package]] +name = "filedescriptor" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7199d965852c3bac31f779ef99cbb4537f80e952e2d6aa0ffeb30cce00f4f46e" +dependencies = [ + "libc", + "thiserror", + "winapi", +] + [[package]] name = "fixedbitset" version = "0.4.1" @@ -636,6 +647,16 @@ dependencies = [ "slab", ] +[[package]] +name = "gag" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a713bee13966e9fbffdf7193af71d54a6b35a0bb34997cd6c9519ebeb5005972" +dependencies = [ + "filedescriptor", + "tempfile", +] + [[package]] name = "generic-array" version = "0.14.5" @@ -1604,6 +1625,7 @@ version = "0.1.0" dependencies = [ "android_logger", "anki", + "gag", "itertools", "jni", "lazy_static", diff --git a/rslib-bridge/Cargo.toml b/rslib-bridge/Cargo.toml index a8a7c9745..1002959c3 100644 --- a/rslib-bridge/Cargo.toml +++ b/rslib-bridge/Cargo.toml @@ -26,6 +26,7 @@ rusqlite = { version = "0.26.0", features = ["trace", "functions", "collation", android_logger = "0.11.0" log = "0.4.17" slog = "2.7.0" +gag = "1.0.0" [features] no-android = [] diff --git a/rslib-bridge/src/logging.rs b/rslib-bridge/src/logging.rs index bcf2de6cf..3c8b9530b 100644 --- a/rslib-bridge/src/logging.rs +++ b/rslib-bridge/src/logging.rs @@ -1,11 +1,17 @@ //! A simple adaptor that takes log messages from the backend and sends them to //! the Android logs. +//! It also captures stdout/stderr output, and feeds it to logcat, to make it +//! easier to debug issues with dbg!()/println!() use android_logger::Config; use log::Level; use slog::*; +use std::io::{BufRead, BufReader}; +use std::time::Duration; use std::{fmt, result}; +use gag::BufferRedirect; + pub struct AndroidSerializer; impl Serializer for AndroidSerializer { @@ -38,7 +44,34 @@ impl Drain for AndroidDrain { } } +fn redirect_io() -> Result<()> { + monitor_io_handle(BufferRedirect::stdout()?); + monitor_io_handle(BufferRedirect::stderr()?); + Ok(()) +} + +fn monitor_io_handle(handle: BufferRedirect) { + let mut handle = BufReader::new(handle); + + std::thread::spawn(move || { + let mut buf = String::new(); + loop { + buf.truncate(0); + match handle.read_line(&mut buf) { + Ok(0) => { + // currently EOF + std::thread::sleep(Duration::from_secs(1)); + } + Ok(_) => log::debug!("{}", buf), + Err(err) => log::debug!("stdio err: {}", err), + } + } + }); +} + pub(crate) fn setup_logging() -> Logger { + // failure is expected after the first backend invocation + let _ = redirect_io(); android_logger::init_once(Config::default().with_min_level(Level::Debug)); Logger::root(AndroidDrain {}.fuse(), slog_o!()) } From f65cd9999812015d2c2bebbf3d3407c0e12a8d28 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 22 Jun 2022 20:54:32 +1000 Subject: [PATCH 42/61] Add script to run tests on current platform/sim --- test-current.sh | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100755 test-current.sh diff --git a/test-current.sh b/test-current.sh new file mode 100755 index 000000000..21b93d6c8 --- /dev/null +++ b/test-current.sh @@ -0,0 +1,6 @@ +#!/bin/bash + +set -e + +NO_CROSS=true CURRENT_ONLY=true ./gradlew rsdroid:test rsdroid-instrumented:assembleDebugAndroidTest +adb shell am instrument -w -e debug false net.ankiweb.rsdroid.instrumented.test/androidx.test.runner.AndroidJUnitRunner From 4ce057b320c0a834a740d5a3ecf512bab36dfe42 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Wed, 22 Jun 2022 20:57:25 +1000 Subject: [PATCH 43/61] Update rslib for backup fix --- rslib-bridge/anki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib-bridge/anki b/rslib-bridge/anki index 72bbc414c..66d3cbe41 160000 --- a/rslib-bridge/anki +++ b/rslib-bridge/anki @@ -1 +1 @@ -Subproject commit 72bbc414cea07b2a481ad58f8f6ba252befab8ff +Subproject commit 66d3cbe41018766411c38577d56671922d798d56 From 1760ab986e0768bd2dee9cf2e06cb82240fb66e6 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 23 Jun 2022 15:21:57 +1000 Subject: [PATCH 44/61] Ignore s_glBindAttribLocation lines from sim --- rslib-bridge/src/logging.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/rslib-bridge/src/logging.rs b/rslib-bridge/src/logging.rs index 3c8b9530b..a69054ac4 100644 --- a/rslib-bridge/src/logging.rs +++ b/rslib-bridge/src/logging.rs @@ -62,13 +62,26 @@ fn monitor_io_handle(handle: BufferRedirect) { // currently EOF std::thread::sleep(Duration::from_secs(1)); } - Ok(_) => log::debug!("{}", buf), + Ok(_) => { + if !should_ignore_line(&buf) { + log::debug!("{}", buf) + } + } Err(err) => log::debug!("stdio err: {}", err), } } }); } +fn should_ignore_line(buf: &str) -> bool { + // quieten simulator noise + if buf.starts_with("s_glBindAttribLocation") { + true + } else { + false + } +} + pub(crate) fn setup_logging() -> Logger { // failure is expected after the first backend invocation let _ = redirect_io(); From 75e19bdbb369f2710faca2fe41fb383f43948f3a Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 23 Jun 2022 14:52:29 +1000 Subject: [PATCH 45/61] Warn when executing SQL statements on main thread; remove other logging If they block due to a concurrent operation, the UI will freeze until it completes, so these operations should be moved to a backend thread over time (in many cases, there's a backend operation that can be used instead) --- .../main/java/net/ankiweb/rsdroid/Backend.kt | 21 +++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt index 2f932b903..f1f038397 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -16,6 +16,7 @@ package net.ankiweb.rsdroid import android.content.Context +import android.os.Looper import androidx.annotation.CheckResult import androidx.annotation.VisibleForTesting import anki.ankidroid.DBResponse @@ -189,7 +190,6 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), @CheckResult override fun fullQuery(query: String, bindArgs: Array?): JSONArray { return try { - Timber.d("Rust: SQL query: '%s'", query) fullQueryInternal(query, bindArgs) } catch (e: JSONException) { throw RuntimeException(e) @@ -198,27 +198,28 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), @Throws(JSONException::class) private fun fullQueryInternal(sql: String, bindArgs: Array?): JSONArray { + checkMainThread(sql) val output = runDbCommand(dbRequestJson(sql, bindArgs)).toStringUtf8() return JSONArray(output) } override fun insertForId(sql: String, bindArgs: Array?): Long { - Timber.d("Rust: sql insert %s", sql) + checkMainThread(sql) return super.insertForId(dbRequestJson(sql, bindArgs)) } override fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int { - Timber.d("Rust: executeGetRowsAffected %s", sql) + checkMainThread(sql) return runDbCommandForRowCount(dbRequestJson(sql, bindArgs)).toInt() } /* Begin Protobuf-based database streaming methods (#6) */ override fun fullQueryProto(query: String, bindArgs: Array?): DBResponse { + checkMainThread(query) return runDbCommandProto(dbRequestJson(query, bindArgs)) } override fun getNextSlice(startIndex: Long, sequenceNumber: Int): DBResponse { - Timber.d("Rust: getNextSlice %d", startIndex) return getNextResultPage(sequenceNumber, startIndex) } @@ -231,7 +232,6 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), } private fun performTransaction(kind: DbRequestKind) { - Timber.d("Rust: transaction %s", kind) runDbCommand(dbRequestJson(kind = kind)) } @@ -242,9 +242,18 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), } override fun getColumnNames(sql: String): Array { - Timber.d("Rust: getColumnNames %s", sql) return getColumnNamesFromQuery(sql).toTypedArray() } + + private fun checkMainThread(query: String) { + try { + if (Looper.getMainLooper().isCurrentThread) { + Timber.w("SQL on UI thread: %s", query) + } + } catch (exc: NoSuchMethodError) { + // running outside Android + } + } } /** From 6955d8de2b99e49a7d59a9331a0ee9d7667a4207 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Thu, 23 Jun 2022 17:47:10 +1000 Subject: [PATCH 46/61] Report backend requests that happen on UI thread Reveals deck list's renderPage() invokes a lot of SQL queries, and isCurrentlySelectedDeck() queries the current deck each time. --- .../main/java/net/ankiweb/rsdroid/Backend.kt | 41 +++++++++++++++---- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt index f1f038397..9646fc200 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -57,6 +57,7 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), listOf(collectionPath.replace(".anki2", ".media"), collectionPath.replace(".anki2", ".media.db")) } + checkMainThreadOp() openCollection(collectionPath, mediaFolder, mediaDb, "", legacySchema) } @@ -72,6 +73,7 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), * Open a backend instance, loading the shared library if not already loaded. */ init { + checkMainThreadOp() Timber.d("Opening rust backend with lang=$langs") NativeMethods.ensureSetup(context) val input = BackendInit.newBuilder() @@ -86,6 +88,7 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), * Close the backend, and any open collection. This object can not be used after this. */ override fun close() { + checkMainThreadOp() Timber.d("Closing rust backend") lock.withLock { // Must be checked inside lock to avoid race @@ -122,6 +125,7 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), * excluded from the mutex) flow through this. */ override fun runMethodRaw(service: Int, method: Int, input: ByteArray): ByteArray { + checkMainThreadOp() return withBackend { unpackResult(NativeMethods.runMethodRaw(it, service, method, input)) } @@ -198,24 +202,24 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), @Throws(JSONException::class) private fun fullQueryInternal(sql: String, bindArgs: Array?): JSONArray { - checkMainThread(sql) + checkMainThreadSQL(sql) val output = runDbCommand(dbRequestJson(sql, bindArgs)).toStringUtf8() return JSONArray(output) } override fun insertForId(sql: String, bindArgs: Array?): Long { - checkMainThread(sql) + checkMainThreadSQL(sql) return super.insertForId(dbRequestJson(sql, bindArgs)) } override fun executeGetRowsAffected(sql: String, bindArgs: Array?): Int { - checkMainThread(sql) + checkMainThreadSQL(sql) return runDbCommandForRowCount(dbRequestJson(sql, bindArgs)).toInt() } /* Begin Protobuf-based database streaming methods (#6) */ override fun fullQueryProto(query: String, bindArgs: Array?): DBResponse { - checkMainThread(query) + checkMainThreadSQL(query) return runDbCommandProto(dbRequestJson(query, bindArgs)) } @@ -244,14 +248,37 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), override fun getColumnNames(sql: String): Array { return getColumnNamesFromQuery(sql).toTypedArray() } + + private fun checkMainThreadOp() { + checkMainThread { + val stackTraceElements = Thread.currentThread().stackTrace + val firstElem = stackTraceElements.filter { + val klass = it.className + for (text in listOf("rsdroid", "libanki", "java.lang", "dalvik", "anki.backend")) { + if (text in klass) { + return@filter false + } + } + true + }.first() + Timber.w("Op on UI thread: %s", firstElem) + } + } + - private fun checkMainThread(query: String) { + private fun checkMainThreadSQL(query: String) { + checkMainThread { + Timber.w("SQL on UI thread: %s", query) + } + } + + private fun checkMainThread(func: () -> Unit) { try { if (Looper.getMainLooper().isCurrentThread) { - Timber.w("SQL on UI thread: %s", query) + func() } } catch (exc: NoSuchMethodError) { - // running outside Android + // running outside Android, or old API } } } From bd6547745609f2ec57a6626ba7607f78b654c5d8 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Fri, 24 Jun 2022 19:14:46 +1000 Subject: [PATCH 47/61] Log caller for SQL statements --- .../src/main/java/net/ankiweb/rsdroid/Backend.kt | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt index 9646fc200..83da1ba3d 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/Backend.kt @@ -60,7 +60,7 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), checkMainThreadOp() openCollection(collectionPath, mediaFolder, mediaDb, "", legacySchema) } - + /** Forces a full media check on next sync. Only valid with new backend. */ fun removeMediaDb(colPath: String) { val file = File(colPath.replace(".anki2", ".media.db")) @@ -249,12 +249,13 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), return getColumnNamesFromQuery(sql).toTypedArray() } - private fun checkMainThreadOp() { + private fun checkMainThreadOp(sql: String? = null) { checkMainThread { val stackTraceElements = Thread.currentThread().stackTrace val firstElem = stackTraceElements.filter { val klass = it.className - for (text in listOf("rsdroid", "libanki", "java.lang", "dalvik", "anki.backend")) { + for (text in listOf("rsdroid", "libanki", "java.lang", "dalvik", "anki.backend", + "DatabaseChangeDecorator")) { if (text in klass) { return@filter false } @@ -262,14 +263,15 @@ open class Backend(val context: Context, langs: Iterable = listOf("en"), true }.first() Timber.w("Op on UI thread: %s", firstElem) + sql?.let { + Timber.w("%s", sql) + } } } - + private fun checkMainThreadSQL(query: String) { - checkMainThread { - Timber.w("SQL on UI thread: %s", query) - } + checkMainThreadOp(query) } private fun checkMainThread(func: () -> Unit) { From 5d291167109aaee679f5eeff5709066622f6db14 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sat, 25 Jun 2022 12:16:48 +1000 Subject: [PATCH 48/61] Fix oneof detection in code generation script --- tools/protoc-gen/protoc-gen.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tools/protoc-gen/protoc-gen.py b/tools/protoc-gen/protoc-gen.py index 7c6a71b28..b5f3e666b 100755 --- a/tools/protoc-gen/protoc-gen.py +++ b/tools/protoc-gen/protoc-gen.py @@ -346,10 +346,7 @@ def generate_code(request, response): f.content = "\n".join(file_contents) def contains_oneof(msg): - for field in msg.fields: - if field.oneof_index: - return True - return False + return msg.method.oneof_decl if __name__ == "__main__": # Read request message from stdin From 21a43f5698610b1a1a037c6250697dde25cbe245 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 26 Jun 2022 10:58:21 +1000 Subject: [PATCH 49/61] Support customizing log level (see follow-up commit in AnkiDroid repo) --- rslib-bridge/Cargo.lock | 1 + rslib-bridge/Cargo.toml | 1 + rslib-bridge/src/logging.rs | 15 ++++++++++++--- 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/rslib-bridge/Cargo.lock b/rslib-bridge/Cargo.lock index eeab8cf17..117197916 100644 --- a/rslib-bridge/Cargo.lock +++ b/rslib-bridge/Cargo.lock @@ -1639,6 +1639,7 @@ dependencies = [ "serde_derive", "serde_json", "slog", + "slog-envlogger", ] [[package]] diff --git a/rslib-bridge/Cargo.toml b/rslib-bridge/Cargo.toml index 1002959c3..eb3eb42cc 100644 --- a/rslib-bridge/Cargo.toml +++ b/rslib-bridge/Cargo.toml @@ -27,6 +27,7 @@ android_logger = "0.11.0" log = "0.4.17" slog = "2.7.0" gag = "1.0.0" +slog-envlogger = "2.2.0" [features] no-android = [] diff --git a/rslib-bridge/src/logging.rs b/rslib-bridge/src/logging.rs index a69054ac4..8fce2de40 100644 --- a/rslib-bridge/src/logging.rs +++ b/rslib-bridge/src/logging.rs @@ -3,7 +3,7 @@ //! It also captures stdout/stderr output, and feeds it to logcat, to make it //! easier to debug issues with dbg!()/println!() -use android_logger::Config; +use android_logger::{Config, FilterBuilder}; use log::Level; use slog::*; use std::io::{BufRead, BufReader}; @@ -85,6 +85,15 @@ fn should_ignore_line(buf: &str) -> bool { pub(crate) fn setup_logging() -> Logger { // failure is expected after the first backend invocation let _ = redirect_io(); - android_logger::init_once(Config::default().with_min_level(Level::Debug)); - Logger::root(AndroidDrain {}.fuse(), slog_o!()) + + let filter = format!( + "{},rsdroid::logging=debug", + std::env::var("RUST_LOG").unwrap_or_else(|_| "error".into()) + ); + android_logger::init_once( + Config::default() + .with_min_level(Level::Debug) + .with_filter(FilterBuilder::new().parse(&filter).build()), + ); + Logger::root(slog_envlogger::new(AndroidDrain {}).fuse(), slog_o!()) } From 8c024b512bc2cf3e2ca0c86cf6a4b45bbed2b3cc Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Sun, 26 Jun 2022 13:50:46 +1000 Subject: [PATCH 50/61] Update OVERVIEW.md I tried using the site to import the docs/source files, but it told me it wasn't a valid file type, so I've just removed the diagrams for now. --- docs/OVERVIEW.md | 210 ++++++++++-------- .../java/net/ankiweb/rsdroid/NativeMethods.kt | 4 +- 2 files changed, 123 insertions(+), 91 deletions(-) diff --git a/docs/OVERVIEW.md b/docs/OVERVIEW.md index 3aff168b3..15f64e8a4 100644 --- a/docs/OVERVIEW.md +++ b/docs/OVERVIEW.md @@ -1,131 +1,163 @@ # Overview -## Introduction and Basic Flow +Anki-Android-Backend uses Rust, Java and a little Python. Since setting up a Rust environment is somewhat complex, having a separate library encourages drive-by contributions to the main app by keeping a low barrier to entry for Anki-Android. -Anki-Android-Backend uses Rust, Java and a little Python. Since setting up a Rust environment is complex, having a separate library encourages drive-by contributions to the main app by keeping a low barrier to entry for Anki-Android. +This repo is comprised of two main components: -There are two aspects of this library: +- `rslib-bridge` is a small Rust project that gets compiled into a shared library, which Java code can import. Its API consists + of only three different functions: `openBackend()`, `closeBackend()`, and `runMethodRaw()`. When one of these functions is called by + Java code, `rslib-bridge` takes care of converting the Java objects to a native Rust representation, and then passes the call + on to Anki's Rust backend, which gets included in `rslib-bridge` via the `rslib-bridge/anki[/rslib]` submodule. +- `rsdroid` is a Kotlin library that provides a friendly interface to the backend code. The bulk of its code is automatically + generated from the service definitions in `rslib-bridge/anki/proto/anki`. `rsdroid` also provides an adaptor to the Rust + database functionality, so that the Rust backend can be used in place of the standard Android SQLite library. -- `rsdroid.so` - A rust library which contains `anki/rslib` and a JNI bridge (`rslib-bridge`) -- rsdroid.aar - A java library with `rsdroid.so` and handles command processing plus acts as an adapter for database access +## Protocol Buffers -### Protocol Buffers +The Rust backend uses Protocol Buffers to define available methods, +and the structure of data each method takes and receives. The files can +be found in `rslib-bridge/anki/proto/anki`. -Serialisation over the JNI boundary happens mostly via **Protocol Buffers**. +For example: -The source files are stored in `anki/proto`. We perform RPC over JNI, rather than depending on HTTP. - -Protobuf service generation is handled on the rust side via `anki/rslib/build.rs` and on the java side via `tools/protoc-gen/protoc-gen.py`. - -### Main usages +``` +service StatsService { + rpc GetGraphPreferences(generic.Empty) returns (GraphPreferences); +} +... +message GraphPreferences { + enum Weekday { + SUNDAY = 0; + MONDAY = 1; + FRIDAY = 5; + SATURDAY = 6; + } + Weekday calendar_first_day_of_week = 1; + bool card_counts_separate_inactive = 2; + bool browser_links_supported = 3; + bool future_due_show_backlog = 4; +} +``` -See the components section for additional details +Every method takes a _request_ structure, and returns a _response_ structure. +In the case of GetGraphPreferences, which doesn't need any input arguments, +we use the _Empty_ structure which is a message that contains no fields. -### Command Setup +When Java code wants to invoke a method on the backend, it first creates the +input request, and then encodes it into a ByteArray. It passes the method +id and the input bytes into the backend's `runMethodRaw()`, and the backend +returns another ByteArray which can be decoded into the output message. -![](https://www.planttext.com/api/plantuml/img/ZLLDZzem4BtxLupsi0Af8FNAeThjqWE7TYMazj9MaUDCmKkmhV44YNzVEmci7Q8bkIJnl3VpvYjvyYo9csCjf69Bq5tPiuV68maNS5ff9mt3jl6y9gkhhr8Tq5GHD3mJfMrC9UaC3y_ce1VFfehMHMz-o1psXpzPrtxCyElpvkZgIvXBX1GOhX-IzGc_8-zjvPTlyYHx_PaXqKM-rkMY95tjCDSJpfVa8xV5T924AKD6EQF5HK8q5MMlS4UsSP3cBuI8vOJ5bzighi0wD2-shb6njcOjMPRIuyn9tiz1t116dAn04Kf6M9UmCQ6xHY4ymWxvwcq-wYXjGQyawcvXxv8wAI833yX1WJKd98Q81RRWoBAzuIIT_23UQrQHZPaB4RlQ5VQrPAaDEAiDXvhEh45WVSHvYqa3vF5MWCOtXFthR1IVJKqdCCTdWCX8PcKCdvX7_1DoGzTSnWDaAJWahprdZ3XpQNs21dYltiGiqsupVOBopFZxxzIS1-46IMVm2fMj44AGoT1sakw1eun2NNKKSIMxvPl4j5HqErGOMpIk2aztYI47KD90YV5hMSMfbqgXzs4PwWdTxseyeUi9yAFvVjcZEi2_y1L78ajtyLlqpXgFeFrbcPVVZexFopzcsqtcwLB0E6JnBWOEsz-4U0fluN_o7m00) +Doing this manually for each method would be a pain, so we use code generation +instead. `tools/protoc-gen/protoc-gen.py` reads the Protobuf files, and +automatically generates methods for us into a GeneratedBackend.kt file. Eg: -Source: `docs/sources/sequence_open_collection.plantuml`. Created with [planttext](https://www.planttext.com/) +``` + @Throws(BackendException::class) + fun getGraphPreferencesRaw(input: ByteArray): ByteArray { + return runMethodRaw(service=10, method=2, input); + } -### Process command + @Throws(BackendException::class) + open fun getGraphPreferences(): anki.stats.GraphPreferences { + val builder = anki.generic.Empty.newBuilder(); + val input = builder.build(); + return anki.stats.GraphPreferences.parseFrom( + getGraphPreferencesRaw(input.toByteArray())); + } +``` -![](https://www.planttext.com/api/plantuml/img/bPH1ZzCm48Nl_XMZFQNIjg9mAnBQ1VQ0n7802Gvq5JcnTsis6KVZSKibVZpZ90Kd1R7aK4MUzxqPFpjLLu4rSMmRfMls1CCpUGyGWoNLYSxLhjF8y346ValUcTUwVhHeacY-fYeVqMWwmiKrFhhbDPfKNOxbYudXkFXv_QxjcfFRoIWNolD1izlRMyixRyBgczxhSSn98MjFeN7LiY9d7koqhQolA2IsrmoIZDGo-9JeTGb8fR8Q9tmW7pl8jwbK2WsMhywpsa2m_DxNkhbr6Dc6BpPmiNws0ANEnAF1Fza1_JErWThZtXA3ansmXuuy-pamIMy3zhkjfS4RtxOQJT4nNSBwnILKHxPVxnOlrKIV3B88KyU_SPaiKNcE6w28vOYMYGX5ngfS-mJsUUaGBVs7nQU3ute77cNaBHfRUsCbj2xo5YNpHj9lxbTDohziXmCe3t82MoJBaH1yP16d-z5dl4NvZ2oH_9wMpYQOn3RQ53TjnySVwKBT97enJssMzN2wlNywttxtJqCqkleN0eMx1zwHF-1Pn_dD_EtHpveyzbALGCsu2wNKbGZbl-Kd) +## Backend -Source: `docs/sources/sequence_method_call.plantuml`. Created with [planttext](https://www.planttext.com/) +The main class that AnkiDroid uses is called `Backend`. It includes: -## Components +- all the generated method definitions from `GeneratedBackend.kt` +- a `tr` property that exposes all the backend translations +- helper methods for DB access -### Anki-Android +When `Backend` is instantiated, it uses `NativeMethods.kt:openBackend()` to +create a handle to a backend instance. `Backend.close()` takes care of +also closing the backend instance, closing the collection if open, and +freeing up memory. -There is a small amount of code in the consuming app Anki-Android to use this library +Each backend instance supports a single open collection, so multiple +backend instances need to be created if you need to have multiple collections +open in parallel. Switching between collections does not require multiple +instances - you can close one collection and then open another with the +same backend instance. -#### DroidBackend/RustDroidBackend +`Backend` also wraps the majority of method calls in a mutex, which prevents +any backend call from being made while a transaction is active on another +thread. -Java/Rust interface to the backend. `RustDroidBackend` wraps the implementation of the Rust. +## BackendFactory -- Allows a testable comparison between the Java and the Rust during the conversion - - Allows a pure Java conversion afterwards (if deemed appropriate due to AGPL concerns) -- Encapsulates all access to the Rust, allowing the implementation to later be swapped out within one file. +`BackendFactory.getBackend()` is used by AnkiDroid to get a `Backend` instance +with translations set to the language currently configured by the user. -### Anki-Android-Backend +The returned backend has a boolean legacySchema property, which is +set to `BackendFactory.defaultLegacySchema`. -#### RustBackend (interface) +When set to the default of true, collections are not upgraded on opening, +and they remain in the legacy schema 11 format that AnkiDroid's legacy +code expects. Many backend methods require the schema to be updated, so +when this setting is enabled, the backend can effectively only be used +for DB access, and a few routines that do not require an open collection +to operate. -An interface of the commands allowed to be sent to the Rust. Allows for easy mocking, and method discovery. +When set to false, collections are upgraded to the latest schema on opening, +allowing full use of backend functionality. This setting has not received +much testing yet. To change it, see TESTING.md -##### RustBackend Example +## Error handling -RustBackend is generated by `gen/protoc-gen/protoc-gen.py` and is not checked into source control. +The API `rslib-bridge` exposes is defined in `rsdroid`'s NativeMethods.kt: -```java - Backend.RenderCardOut renderUncommittedCard(@Nullable Backend.Note note, int cardOrd, @Nullable com.google.protobuf.ByteString template, boolean fillEmpty); +``` + external fun runMethodRaw(backendPointer: Long, service: Int, method: Int, args: ByteArray): Array? + external fun openBackend(data: ByteArray): Array? + external fun closeBackend(backendPointer: Long) ``` -#### RustBackendImpl - -It contains a method per RPC method defined in `backend.proto` - -It is responsible for: - -- Converting parameters from Java types to protobufs -- Executing a command -- Ensuring that the result was not an error -- Deserializing and returning data (if applicable) +When the backend returns data, it needs to be able to return either the data, +or an error message. This is done with nested arrays: `Array?` is +`[valid_data_or_null, error_data_or_null]`. The `Backend` class takes care of +this for us, extracting the error message, decoding it into a protobuf message and +then a native BackendException, and throwing it. The outer array is declared +as nullable to account for rare cases where an array can't be allocated. -##### RustBackendImpl Example +## Usage in AnkiDroid -RustBackendImpl is generated by `gen/protoc-gen/protoc-gen.py` and is not checked into source control. +When a collection is opened with `Storage.collection()`, a `Backend` instance +is created (or reused if provided), and stored in `Collection.backend`. As +`Collection` is initialized, it calls `Storage.openDB(path, backend)` which +creates a `DB` instance that delegates database calls to the provided backend. -```java - public Backend.SearchCardsOut searchCards(@Nullable java.lang.String search, @Nullable Backend.SortOrder order) { - byte[] result = null; - try { - Backend.SearchCardsIn.Builder builder = Backend.SearchCardsIn.newBuilder(); - if (search != null) { builder.setSearch(search); } - if (order != null) { builder.setOrder(order); } - Backend.SearchCardsIn protobuf = builder.build(); +If `defaultLegacySchema` is false, a `CollectionV16` subclass of `Collection` +is returned instead. It contains changes to work with the new backend methods, +such as requesting a list of decks from the backend instead of directly trying +to query them via SQL, eg: - Pointer backendPointer = ensureBackend(); - result = NativeMethods.executeCommand(backendPointer.toJni(), 9, protobuf.toByteArray()); - Backend.SearchCardsOut message = Backend.SearchCardsOut.parseFrom(result); - validateMessage(result, message); - return message; - } catch (InvalidProtocolBufferException ex) { - validateResult(result); - throw BackendException.fromException(ex); +``` + override fun all_names_and_ids(skip_empty_default: Boolean, include_filtered: Boolean): List { + return backend.getDeckNames(skip_empty_default, include_filtered).map { + entry -> + DeckNameId(entry.name, entry.id) } } ``` -#### BackendV1Impl - -BackendV1Impl extends `RustBackendImpl`. - -It is responsible for: - -- Maintaining a pointer to the collection (to be passed into the Rust to identify the collection) -- Accessing methods in the Rust which are not generated from Protobufs - - JSON serialization for database inputs - - Collection opening/closing - -#### NativeMethods - -Method definitions to access `rslib-bridge` - -### rslib-bridge - -#### lib.rs - -All public methods callable from the Java are available here. +V16 corresponds to the schema version that was current when this code was originally +written, and is not technically correct anymore. The DecksV16/ModelsV16/etc classes +also use a few layers of indirection, as they were written at a time when it wasn't +clear whether AnkiDroid would be using the Rust backend or not. -Responsible for: +## Usage in unit tests -- Handling JNI specific concerns (conversions from Java to Rust primitives) -- Calling methods in `anki/rslib` -- Defining the interface callable by `NativeMethods` -- Converting a provided `backendPointer` into a Collection object -- Serialization of outputs and deserialization of inputs -- Handling panics and converting them to errors +AnkiDroid's unit tests run with Robolectric, and to use the backend inside Robolectric, a separate build of rslib-bridge is required. This is handled +by `rsdroid-testing`, which takes care of compiling `rslib-bridge` correctly, +and provides a `RustBackendLoader.kt` file which AnkiDroid's unit tests call +into. ## Database Access diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt index b8b143f20..7b63d7896 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/NativeMethods.kt @@ -32,9 +32,9 @@ object NativeMethods { } @CheckResult - external fun runMethodRaw(backendPointer: Long, service: Int, method: Int, args: ByteArray?): Array? + external fun runMethodRaw(backendPointer: Long, service: Int, method: Int, args: ByteArray): Array? @CheckResult - external fun openBackend(data: ByteArray?): Array? + external fun openBackend(data: ByteArray): Array? external fun closeBackend(backendPointer: Long) } \ No newline at end of file From 442db0a6fde50d8cb4e38832fda4a0ec7a8dbcaf Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 27 Jun 2022 11:01:11 +1000 Subject: [PATCH 51/61] Enable Kotlin extensions to generated protobuf Makes working with messages easier in the AnkiDroid repo --- rsdroid/build.gradle | 8 +++++++- rsdroid/proto.gradle | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/rsdroid/build.gradle b/rsdroid/build.gradle index 74d4a8e22..fd7bb4002 100644 --- a/rsdroid/build.gradle +++ b/rsdroid/build.gradle @@ -67,7 +67,7 @@ dependencies { implementation fileTree(dir: "libs", include: ["*.jar", '*.so']) implementation "androidx.appcompat:appcompat:${rootProject.ext.appcompatVersion}" // Protobuf is part of the ABI, so include it as a compile/api dependency. - api "com.google.protobuf:protobuf-java:${rootProject.ext.protobufVersion}" + api "com.google.protobuf:protobuf-kotlin:${rootProject.ext.protobufVersion}" implementation "androidx.sqlite:sqlite:${rootProject.ext.sqliteVersion}" implementation 'com.jakewharton.timber:timber:5.0.1' implementation 'androidx.sqlite:sqlite-framework:2.2.0' @@ -80,6 +80,12 @@ dependencies { } +tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompile).all { + kotlinOptions { + freeCompilerArgs = ["-opt-in=kotlin.RequiresOptIn"] + } +} + task generateTranslations(type: Exec) { workingDir "$rootDir" String genPath = System.getProperty('os.name').toLowerCase(Locale.ROOT).contains('windows') ? 'tools\\genfluent\\genfluent.bat' : 'tools/genfluent/genfluent.sh' diff --git a/rsdroid/proto.gradle b/rsdroid/proto.gradle index ef1b4ae94..f988acf30 100644 --- a/rsdroid/proto.gradle +++ b/rsdroid/proto.gradle @@ -44,6 +44,9 @@ protobuf { // which is confusing and of no use to us // option "lite" } + kotlin { + + } } task.plugins { anki { } From cd1f797088aa329d773b3d9d1282828578268ddf Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 27 Jun 2022 11:12:41 +1000 Subject: [PATCH 52/61] Add two more language maps --- rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt index cd88bbd48..7edccfe27 100644 --- a/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt +++ b/rsdroid/src/main/java/net/ankiweb/rsdroid/BackendFactory.kt @@ -63,6 +63,8 @@ object BackendFactory { return when (locale.language) { Locale("heb").language -> "he" Locale("yue").language -> "zh-TW" + Locale("ind").language -> "id" + Locale("tgl").language -> "tl" else -> locale.language } } From 43fa4dc657db8bd0472b620a888c0ea3e286c2d4 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 27 Jun 2022 12:00:20 +1000 Subject: [PATCH 53/61] Check Bazel is installed --- tools/get-buildinfo.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tools/get-buildinfo.sh b/tools/get-buildinfo.sh index b38a84505..e542655d8 100755 --- a/tools/get-buildinfo.sh +++ b/tools/get-buildinfo.sh @@ -3,5 +3,12 @@ # The buildinfo.txt file should be generated as part of the build, # but for now we'll just check it into source control. +if ! bazel --version > /dev/null 2>&1; then + echo "bazel: command not found. Please install Bazelisk. Your distro may have it," + echo "or you can fetch the binary from https://github.com/bazelbuild/bazelisk/releases" + echo "and rename it to /usr/local/bin/bazel" + exit 1 +fi + (cd rslib-bridge/anki && bazel build -c opt buildinfo.txt) cp rslib-bridge/anki/.bazel/bin/buildinfo.txt rslib-bridge \ No newline at end of file From c6bf63b8630fbbc48ad8642385caa273fcd6389e Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 27 Jun 2022 12:07:56 +1000 Subject: [PATCH 54/61] Clarify venv+pip --- docs/TESTING.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/TESTING.md b/docs/TESTING.md index 7d90eaa2f..9a95dcfc6 100644 --- a/docs/TESTING.md +++ b/docs/TESTING.md @@ -29,9 +29,9 @@ Install protobuf: ## Optional Python venv -If you don't want to `pip install protobuf` globally, you can -symlink the python bin from a venv into `python` at the top of -the project folder: +If you don't want to `pip install protobuf stringcase` globally, +you can do so in a venv, and then symlink the python bin from the +venv into `python` at the top of the project folder: ``` $ ln -sf /path/to/venv/bin/python python From d5c26f4671d42f8c1d23e17a0beb623cc40ea255 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 27 Jun 2022 12:30:52 +1000 Subject: [PATCH 55/61] Use connectedAndroidTest https://github.com/ankidroid/Anki-Android-Backend/issues/209#issuecomment-1166690348 --- test-current.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test-current.sh b/test-current.sh index 21b93d6c8..b8a496011 100755 --- a/test-current.sh +++ b/test-current.sh @@ -2,5 +2,5 @@ set -e -NO_CROSS=true CURRENT_ONLY=true ./gradlew rsdroid:test rsdroid-instrumented:assembleDebugAndroidTest -adb shell am instrument -w -e debug false net.ankiweb.rsdroid.instrumented.test/androidx.test.runner.AndroidJUnitRunner +NO_CROSS=true CURRENT_ONLY=true ./gradlew rsdroid:test rsdroid-instrumented:connectedAndroidTest + From d70ded01fe0e5396ce70196d9890749a9df1d96c Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 27 Jun 2022 12:32:45 +1000 Subject: [PATCH 56/61] Add stringcase to doctor --- tools/doctor.sh | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/doctor.sh b/tools/doctor.sh index 6ffc61a42..e7631244f 100755 --- a/tools/doctor.sh +++ b/tools/doctor.sh @@ -90,4 +90,10 @@ if [[ $(pip3 install protobuf) ]]; then else echo -e "Try installing with python 3.7" error_echo "Failed installing Protobuf python libraries" +fi + +if [[ $(pip3 install stringcase) ]]; then + ok_echo "Stringcase is installed" +else + error_echo "Failed installing stringcase python library" fi \ No newline at end of file From 979695c3440a752a147d8e3466723b92d64c84e8 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 27 Jun 2022 12:38:52 +1000 Subject: [PATCH 57/61] Remove unused branch label from .gitmodules https://github.com/ankidroid/Anki-Android-Backend/pull/202#discussion_r906913799 --- .gitmodules | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitmodules b/.gitmodules index dc8e5a905..feca11298 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,6 @@ [submodule "anki"] path = rslib-bridge/anki url = https://github.com/ankidroid/anki - branch = ankidroid-2-1-34 [submodule "ftl/core"] path = ftl/core url = https://github.com/ankitects/anki-core-i18n.git From 3e3f0af3092241092b2905d3e9f8909574722431 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 27 Jun 2022 12:41:47 +1000 Subject: [PATCH 58/61] Update rslib commit to Mike's rebase --- rslib-bridge/anki | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rslib-bridge/anki b/rslib-bridge/anki index 66d3cbe41..6ffa055db 160000 --- a/rslib-bridge/anki +++ b/rslib-bridge/anki @@ -1 +1 @@ -Subproject commit 66d3cbe41018766411c38577d56671922d798d56 +Subproject commit 6ffa055dbb1716d3b00552f3379501a20368ee6f From b1114d8e5394df918b35e7c2d3554dace573ccae Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 27 Jun 2022 16:53:05 +1000 Subject: [PATCH 59/61] Update to Anki 2.1.54 --- ftl/core | 2 +- gradle.properties | 2 +- rslib-bridge/Cargo.lock | 39 +++++++++++++++++++++++++++++++++++++++ rslib-bridge/anki | 2 +- rslib-bridge/src/lib.rs | 2 +- 5 files changed, 43 insertions(+), 4 deletions(-) diff --git a/ftl/core b/ftl/core index 460c0d295..40963d9a2 160000 --- a/ftl/core +++ b/ftl/core @@ -1 +1 @@ -Subproject commit 460c0d2954e40513e6cb65b206fed6412cee5817 +Subproject commit 40963d9a29703c756a47bac8e0e7e5174a286c86 diff --git a/gradle.properties b/gradle.properties index c31fd4862..519138e63 100644 --- a/gradle.properties +++ b/gradle.properties @@ -19,7 +19,7 @@ android.useAndroidX=true android.enableJetifier=false GROUP=io.github.david-allison-1 -VERSION_NAME=0.1.13-anki2.1.53 +VERSION_NAME=0.1.14-anki2.1.54 POM_INCEPTION_YEAR=2020 diff --git a/rslib-bridge/Cargo.lock b/rslib-bridge/Cargo.lock index 117197916..672472fe3 100644 --- a/rslib-bridge/Cargo.lock +++ b/rslib-bridge/Cargo.lock @@ -70,6 +70,7 @@ dependencies = [ "bytes", "chrono", "coarsetime", + "csv", "flate2", "fluent", "fluent-bundle", @@ -239,6 +240,18 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.10.0" @@ -374,6 +387,26 @@ dependencies = [ "typenum", ] +[[package]] +name = "csv" +version = "1.1.6" +source = "git+https://github.com/ankitects/rust-csv.git?rev=1c9d3aab6f79a7d815c69f925a46a4590c115f90#1c9d3aab6f79a7d815c69f925a46a4590c115f90" +dependencies = [ + "bstr", + "csv-core", + "itoa 1.0.2", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "git+https://github.com/ankitects/rust-csv.git?rev=1c9d3aab6f79a7d815c69f925a46a4590c115f90#1c9d3aab6f79a7d815c69f925a46a4590c115f90" +dependencies = [ + "memchr", +] + [[package]] name = "digest" version = "0.10.3" @@ -1550,6 +1583,12 @@ dependencies = [ "regex-syntax", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "regex-syntax" version = "0.6.26" diff --git a/rslib-bridge/anki b/rslib-bridge/anki index 6ffa055db..2f7a02029 160000 --- a/rslib-bridge/anki +++ b/rslib-bridge/anki @@ -1 +1 @@ -Subproject commit 6ffa055dbb1716d3b00552f3379501a20368ee6f +Subproject commit 2f7a0202976b3a400542b0a9bf5312c0e633472a diff --git a/rslib-bridge/src/lib.rs b/rslib-bridge/src/lib.rs index 9dc568583..2a52ed06b 100644 --- a/rslib-bridge/src/lib.rs +++ b/rslib-bridge/src/lib.rs @@ -1,6 +1,6 @@ #![allow(clippy::missing_safety_doc)] -use anki::backend_proto::{backend_error, BackendError, Int64}; +use anki::pb::{backend_error, BackendError, Int64}; use jni::objects::{JClass, JObject}; use jni::sys::{jarray, jbyteArray, jint, jlong}; use jni::JNIEnv; From 8714da7e03b855251066e6213c95583f83942bc5 Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 27 Jun 2022 16:01:32 +1000 Subject: [PATCH 60/61] Fetch new changes before trying to switch commits --- tools/genfluent/genfluent.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/genfluent/genfluent.py b/tools/genfluent/genfluent.py index ae6aaf29d..25fc5062c 100755 --- a/tools/genfluent/genfluent.py +++ b/tools/genfluent/genfluent.py @@ -18,6 +18,7 @@ def ensure_i18n_module_correct(): if m: (indent, key, commit) = m.groups() if key == "core_i18n_commit": + subprocess.run(["git", "fetch"], cwd="ftl/core", check=True) subprocess.run(["git", "checkout", commit], cwd="ftl/core", check=True) break From bf699dce912f48b59fa24673a7f6260d8f9c236b Mon Sep 17 00:00:00 2001 From: Damien Elmes Date: Mon, 27 Jun 2022 20:29:13 +1000 Subject: [PATCH 61/61] Bump protobuf version https://github.com/ankidroid/Anki-Android/pull/11711 --- build.gradle | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index b625f655e..07d538c9d 100644 --- a/build.gradle +++ b/build.gradle @@ -6,7 +6,7 @@ buildscript { compileSdkVersion = 31 targetSdkVersion = 30 minSdkVersion = 21 - protobufVersion = "3.19.3" + protobufVersion = "3.21.2" appcompatVersion = "1.3.1" androidxTestJunitVersion = "1.1.3" sqliteVersion = "2.2.0"