diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c8c79bd3..a4a7c888 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -34,7 +34,7 @@ jobs: run: yes | $ANDROID_HOME/cmdline-tools/latest/bin/sdkmanager --licenses || true - name: Run UI Tests - run: bash ./gradlew allDevicesFdroidDebugAndroidTest -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true -Pandroid.experimental.testOptions.managedDevices.maxConcurrentDevices=1 -Pandroid.experimental.testOptions.managedDevices.setupTimeoutMinutes=180 + run: bash ./gradlew pixel2api30FdroidDebugAndroidTest -Pandroid.testoptions.manageddevices.emulator.gpu=swiftshader_indirect -Pandroid.experimental.testOptions.managedDevices.emulator.showKernelLogging=true -Pandroid.experimental.testOptions.managedDevices.maxConcurrentDevices=1 -Pandroid.experimental.testOptions.managedDevices.setupTimeoutMinutes=180 - name: Assemble App Debug APK run: ./gradlew assembleDebug diff --git a/app/build.gradle b/app/build.gradle index 523b5346..79d397bd 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,6 +6,7 @@ apply plugin: 'com.android.application' apply plugin: "androidx.navigation.safeargs" apply plugin: 'com.mikepenz.aboutlibraries.plugin' apply plugin: 'org.jetbrains.kotlin.android' +apply plugin: 'androidx.room' android { compileSdk 35 @@ -17,12 +18,6 @@ android { versionName "1.2.12" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunnerArgument "notAnnotation", "androidx.test.filters.FlakyTest" - javaCompileOptions { - annotationProcessorOptions { - arguments = ["room.schemaLocation": - "$projectDir/schemas".toString()] - } - } } buildTypes { release { @@ -56,6 +51,9 @@ android { applicationVariants.configureEach { variant -> variant.resValue "string", "versionName", variant.versionName } + room { + schemaDirectory "$projectDir/schemas" + } testOptions { animationsDisabled = true managedDevices { @@ -65,6 +63,11 @@ android { apiLevel = 30 systemImageSource = "aosp-atd" } + pixel8api35 { + device = "Pixel 8" + apiLevel = 35 + systemImageSource = "aosp-atd" + } } } } @@ -93,7 +96,6 @@ dependencies { runtimeOnly group: 'com.google.android.material', name: 'material', version: '1.12.0' implementation 'androidx.constraintlayout:constraintlayout:2.2.0' - def nav_version = "2.8.4" implementation "androidx.navigation:navigation-fragment:$nav_version" implementation "androidx.navigation:navigation-ui:$nav_version" @@ -101,7 +103,6 @@ dependencies { implementation "androidx.viewpager2:viewpager2:1.1.0" implementation 'id.zelory:compressor:2.1.1' // 3.0.0 does not work with Java - def room_version = "2.6.1" implementation "androidx.room:room-runtime:$room_version" annotationProcessor "androidx.room:room-compiler:$room_version" testImplementation "androidx.room:room-testing:$room_version" diff --git a/app/schemas/com.flauschcode.broccoli.BroccoliDatabase/2.json b/app/schemas/com.flauschcode.broccoli.BroccoliDatabase/2.json new file mode 100644 index 00000000..ae99ccaa --- /dev/null +++ b/app/schemas/com.flauschcode.broccoli.BroccoliDatabase/2.json @@ -0,0 +1,244 @@ +{ + "formatVersion": 1, + "database": { + "version": 2, + "identityHash": "01b8721f1ac5a34ff8cafe8c4f516546", + "entities": [ + { + "tableName": "recipes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recipeId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `title` TEXT, `imageName` TEXT, `description` TEXT, `servings` TEXT, `preparationTime` TEXT, `source` TEXT, `ingredients` TEXT, `directions` TEXT, `notes` TEXT, `favorite` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "recipeId", + "columnName": "recipeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "imageName", + "columnName": "imageName", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "servings", + "columnName": "servings", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "preparationTime", + "columnName": "preparationTime", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ingredients", + "columnName": "ingredients", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "directions", + "columnName": "directions", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "notes", + "columnName": "notes", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "favorite", + "columnName": "favorite", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "recipeId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`categoryId` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, `name` TEXT)", + "fields": [ + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "categoryId" + ] + }, + "indices": [], + "foreignKeys": [] + }, + { + "tableName": "recipes_with_categories", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`recipeId` INTEGER NOT NULL, `categoryId` INTEGER NOT NULL, PRIMARY KEY(`recipeId`, `categoryId`), FOREIGN KEY(`recipeId`) REFERENCES `recipes`(`recipeId`) ON UPDATE NO ACTION ON DELETE CASCADE , FOREIGN KEY(`categoryId`) REFERENCES `categories`(`categoryId`) ON UPDATE NO ACTION ON DELETE CASCADE )", + "fields": [ + { + "fieldPath": "recipeId", + "columnName": "recipeId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "categoryId", + "columnName": "categoryId", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "recipeId", + "categoryId" + ] + }, + "indices": [ + { + "name": "index_recipes_with_categories_recipeId", + "unique": false, + "columnNames": [ + "recipeId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_recipes_with_categories_recipeId` ON `${TABLE_NAME}` (`recipeId`)" + }, + { + "name": "index_recipes_with_categories_categoryId", + "unique": false, + "columnNames": [ + "categoryId" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `index_recipes_with_categories_categoryId` ON `${TABLE_NAME}` (`categoryId`)" + } + ], + "foreignKeys": [ + { + "table": "recipes", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "recipeId" + ], + "referencedColumns": [ + "recipeId" + ] + }, + { + "table": "categories", + "onDelete": "CASCADE", + "onUpdate": "NO ACTION", + "columns": [ + "categoryId" + ], + "referencedColumns": [ + "categoryId" + ] + } + ] + }, + { + "ftsVersion": "FTS4", + "ftsOptions": { + "tokenizer": "unicode61", + "tokenizerArgs": [ + "tokenchars=#" + ], + "contentTable": "recipes", + "languageIdColumnName": "", + "matchInfo": "FTS4", + "notIndexedColumns": [], + "prefixSizes": [], + "preferredOrder": "ASC" + }, + "contentSyncTriggers": [ + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipes_fts_BEFORE_UPDATE BEFORE UPDATE ON `recipes` BEGIN DELETE FROM `recipes_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipes_fts_BEFORE_DELETE BEFORE DELETE ON `recipes` BEGIN DELETE FROM `recipes_fts` WHERE `docid`=OLD.`rowid`; END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipes_fts_AFTER_UPDATE AFTER UPDATE ON `recipes` BEGIN INSERT INTO `recipes_fts`(`docid`, `title`, `description`, `source`, `ingredients`) VALUES (NEW.`rowid`, NEW.`title`, NEW.`description`, NEW.`source`, NEW.`ingredients`); END", + "CREATE TRIGGER IF NOT EXISTS room_fts_content_sync_recipes_fts_AFTER_INSERT AFTER INSERT ON `recipes` BEGIN INSERT INTO `recipes_fts`(`docid`, `title`, `description`, `source`, `ingredients`) VALUES (NEW.`rowid`, NEW.`title`, NEW.`description`, NEW.`source`, NEW.`ingredients`); END" + ], + "tableName": "recipes_fts", + "createSql": "CREATE VIRTUAL TABLE IF NOT EXISTS `${TABLE_NAME}` USING FTS4(`title` TEXT, `description` TEXT, `source` TEXT, `ingredients` TEXT, tokenize=unicode61 `tokenchars=#`, content=`recipes`)", + "fields": [ + { + "fieldPath": "title", + "columnName": "title", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "description", + "columnName": "description", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "source", + "columnName": "source", + "affinity": "TEXT", + "notNull": false + }, + { + "fieldPath": "ingredients", + "columnName": "ingredients", + "affinity": "TEXT", + "notNull": false + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [] + }, + "indices": [], + "foreignKeys": [] + } + ], + "views": [], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '01b8721f1ac5a34ff8cafe8c4f516546')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/com/flauschcode/broccoli/CRUDIntegrationTest.java b/app/src/androidTest/java/com/flauschcode/broccoli/CRUDIntegrationTest.java index ffe83d26..8bbb23d4 100644 --- a/app/src/androidTest/java/com/flauschcode/broccoli/CRUDIntegrationTest.java +++ b/app/src/androidTest/java/com/flauschcode/broccoli/CRUDIntegrationTest.java @@ -17,7 +17,6 @@ import androidx.test.espresso.accessibility.AccessibilityChecks; import androidx.test.espresso.matcher.ViewMatchers; import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.FlakyTest; import com.flauschcode.broccoli.util.RecyclerViewAssertions; import com.flauschcode.broccoli.util.RecyclerViewMatcher; @@ -28,7 +27,6 @@ import org.junit.runner.RunWith; @RunWith(AndroidJUnit4.class) -@FlakyTest public class CRUDIntegrationTest { private ActivityScenario scenario; diff --git a/app/src/androidTest/java/com/flauschcode/broccoli/ImportingIntegrationTest.java b/app/src/androidTest/java/com/flauschcode/broccoli/ImportingIntegrationTest.java index 11c21b9c..7bdbbddc 100644 --- a/app/src/androidTest/java/com/flauschcode/broccoli/ImportingIntegrationTest.java +++ b/app/src/androidTest/java/com/flauschcode/broccoli/ImportingIntegrationTest.java @@ -16,7 +16,6 @@ import androidx.test.espresso.accessibility.AccessibilityChecks; import androidx.test.espresso.matcher.ViewMatchers; import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.FlakyTest; import com.flauschcode.broccoli.recipe.crud.CreateAndEditRecipeActivity; @@ -38,7 +37,6 @@ */ @RunWith(AndroidJUnit4.class) -@FlakyTest public class ImportingIntegrationTest { private ActivityScenario scenario; @@ -53,60 +51,6 @@ public void tearDown() { scenario.close(); } - @Test - public void import_new_recipe_from_chefkoch() { - Intent intent = new Intent(ApplicationProvider.getApplicationContext(), CreateAndEditRecipeActivity.class); - intent.setAction(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_TEXT, "https://www.chefkoch.de/rezepte/3212051478029180/Vegane-Chocolate-Chip-Cookies.html"); - scenario = launch(intent); - - onView(isRoot()).perform(waitForView(withText("Vegane Chocolate Chip Cookies"), 10000)); - - onView(ViewMatchers.withId(R.id.new_title)).check(matches(withText("Vegane Chocolate Chip Cookies"))); - onView(withId(R.id.new_source)).check(matches(withText("https://www.chefkoch.de/rezepte/3212051478029180/Vegane-Chocolate-Chip-Cookies.html"))); - onView(withId(R.id.new_servings)).check(matches(withText("1"))); - onView(withId(R.id.new_preparation_time)).check(matches(withText("35m"))); - onView(withId(R.id.new_description)).check(matches(withSubstring("Vegane Chocolate Chip Cookies - außen kross, innen weich, lecker und vegan, ergibt 35 Stück."))); - onView(withId(R.id.new_ingredients)).check(matches(withText("20 g Chiasamen\n50 ml Wasser\n190 g Butterersatz oder Margarine, vegan\n200 g Zucker , braun, alternativ Rohrzucker\n2 TL Rübensirup , alternativ Melasse, Ahornsirup oder Agavendicksaft\n2 Pck. Vanillezucker\n300 g Weizenmehl oder Dinkelmehl, oder gemischt\n4 g Natron\nn. B. Salz\n200 g Blockschokolade , zartbitter oder Schokotröpfchen"))); - onView(withId(R.id.new_directions)).check(matches(withText("Den Backofen auf 180 °C Umluft vorheizen. Die Chiasamen und das Wasser in einer kleinen Schüssel vermengen und ca. 10 Minuten quellen lassen.\n\nEin Backblech mit Backpapier auslegen. Vegane Butter bzw. Margarine und Zucker mit den Schneebesen des Rührgeräts cremig verrühren. Dann die gequollenen Chiasamen, den Zuckerrübensirup und beide Päckchen Vanillezucker dazugeben und weiter rühren. Unter weiterem Rühren jetzt zuerst das Mehl hinzugeben und anschließend Natron sowie Salz. Alternativ - oder falls der Teig zu zäh ist - kann alles auch mit den Händen verknetet werden. Abschließend die Schokotröpfchen bzw. die gehackte Blockschokolade untermischen.\n\nDen nun fertigen Teig mit einem Esslöffel oder Eisportionierer klecksweise im Abstand von etwa 5 - 6 cm auf das Backpapier geben. Die Teigkleckse können - müssen jedoch nicht - mit einem Löffel noch etwas rund geformt und flach gedrückt werden.\n\nDie Cookies bei 180 °C Umluft maximal 15 Minuten backen, da sie sonst zu fest werden."))); - } - - @Test - public void import_new_recipe_via_yoast_plugin() { - Intent intent = new Intent(ApplicationProvider.getApplicationContext(), CreateAndEditRecipeActivity.class); - intent.setAction(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_TEXT, "https://stilettosandsprouts.de/vegane-fenchel-pasta/"); - scenario = launch(intent); - - onView(isRoot()).perform(waitForView(withText("Vegane Fenchel-Pasta"), 10000)); - - onView(withId(R.id.new_title)).check(matches(withText("Vegane Fenchel-Pasta"))); - onView(withId(R.id.new_source)).check(matches(withText("https://stilettosandsprouts.de/vegane-fenchel-pasta/"))); - onView(withId(R.id.new_servings)).check(matches(withText("2"))); - onView(withId(R.id.new_preparation_time)).check(matches(withText("10m"))); - onView(withId(R.id.new_description)).check(matches(withSubstring("Pasta mit geröstetem Fenchel und Zitrone – ein herrlich leichtes Pastagericht, in nur 10 Minuten fertig zubereitet."))); - onView(withId(R.id.new_ingredients)).check(matches(withText("1 Knolle Fenchel\n1 Bio-Zitrone\n1 Knoblauchzehe, geschält und fein gehackt\n1 Schalotte, geschält und fein gehackt\n1 Handvoll Petersilie, frisch, fein gehackt\n5 EL Semmelbrösel\nOlivenöl\nSalz & Pfeffer\n250 g Pasta (z.B. Linguini)"))); - onView(withId(R.id.new_directions)).check(matches(withText("Vom Fenchel die oberen grünen Stängel entfernen. Dabei unbedingt das Fenchelgrün aufbewahren. Das kommt später an die Pasta ran. Die Knolle halbieren, den Strunk in der Mitte keilförmig entfernen und nun die zwei Hälften in dünne Streifen schneiden. Die Fenchel-Streifen waschen und gut abtrocknen.\nDie Pasta nach Packungsanweisung garen.\nIn der Zwischenzeit in einer großen Pfanne Olivenöl erhitzen und den Fenchel darin mit etwas Salz ca. 8 Minuten lang anrösten.\nIn einer kleinen Pfanne etwas Olivenöl erhitzen, Knoblauch- und Schalottenwürfel darin mit etwas Salz glasig andünsten. Zitronenabrieb, Petersilie und die Semmelbrösel hinzugeben und alles ca. 4 Minuten vorsichtig anrösten bis die Semmelbrösel leicht angebräunt sind. Die Mischung vom Herd nehmen.\nDie fertige Pasta abgießen. Dabei etwa ein halbes Wasserglas der Kochflüssigkeit auffangen. Abgetropfte Pasta zum Fenchel geben. Die Semmelbröselmischung dazugeben. Kochflüssigkeit nach Belieben dazugeben, damit die Pasta schön glänzend wird. Pasta ordentlich salzen und pfeffern und nach Geschmack Zitrone hinzugeben. Den Saft einer halben Zitrone verträgt das Gericht mindestens. Mit einem guten Schuss Olivenöl und mit Fenchelgrün bestreut servieren."))); - } - - @Test - public void import_new_recipe_with_arrified_json() { - Intent intent = new Intent(ApplicationProvider.getApplicationContext(), CreateAndEditRecipeActivity.class); - intent.setAction(Intent.ACTION_SEND); - intent.putExtra(Intent.EXTRA_TEXT, "https://www.gutekueche.de/fladenbrot-grundrezept-rezept-1673"); - scenario = launch(intent); - - onView(isRoot()).perform(waitForView(withText("Fladenbrot Grundrezept"), 10000)); - - onView(withId(R.id.new_title)).check(matches(withText("Fladenbrot Grundrezept"))); - onView(withId(R.id.new_source)).check(matches(withText("https://www.gutekueche.de/fladenbrot-grundrezept-rezept-1673"))); - onView(withId(R.id.new_servings)).check(matches(withText("4"))); - onView(withId(R.id.new_preparation_time)).check(matches(withText("25m"))); - onView(withId(R.id.new_description)).check(matches(withSubstring("Dieses Grundrezept für Fladenbrot ohne Hefe passt zu vielen Gerichten. Das einfache und schnelle Rezept ist sehr variabel."))); - onView(withId(R.id.new_ingredients)).check(matches(withText("200 g Mehl, Type 550\n3 EL Olivenöl (oder Pflanzenöl)\n3 EL Olivenöl (oder Pflanzenöl)\n1 Prise Salz\n100 ml Wasser"))); - onView(withId(R.id.new_directions)).check(matches(withText("1. Zuerst das Mehl in eine Schüssel geben, Salz, Wasser und Olivenöl dazugeben und alle Zutaten zu einem Teig verkneten - am besten mit der Hand.\n2. Dann den Teig für 10 Minuten quellen lassen und erneut für 5 Minuten kneten, so dass ein glatter Teig entsteht.\n3. Später aus dem Teig 4 dünne Fladen formen, eine gusseiserne Pfanne (=unbeschichtet) ohne Fett erhitzen und die Teigfladen darin nacheinander backen bis sich die ersten braunen Flecken zeigen.\n4. Im Anschluss die Fladen wenden und auch auf der anderen Seite backen."))); - } - @Test public void import_new_recipe_from_flauschcode() { Intent intent = new Intent(ApplicationProvider.getApplicationContext(), CreateAndEditRecipeActivity.class); diff --git a/app/src/androidTest/java/com/flauschcode/broccoli/SeasonsIntegrationTest.java b/app/src/androidTest/java/com/flauschcode/broccoli/SeasonsIntegrationTest.java index e64e96f2..9f86c1fa 100644 --- a/app/src/androidTest/java/com/flauschcode/broccoli/SeasonsIntegrationTest.java +++ b/app/src/androidTest/java/com/flauschcode/broccoli/SeasonsIntegrationTest.java @@ -33,7 +33,6 @@ import androidx.test.espresso.accessibility.AccessibilityChecks; import androidx.test.espresso.matcher.ViewMatchers; import androidx.test.ext.junit.runners.AndroidJUnit4; -import androidx.test.filters.FlakyTest; import com.flauschcode.broccoli.util.RecyclerViewAssertions; import com.flauschcode.broccoli.util.RecyclerViewMatcher; @@ -49,7 +48,6 @@ import java.util.Set; @RunWith(AndroidJUnit4.class) -@FlakyTest public class SeasonsIntegrationTest { private ActivityScenario scenario; diff --git a/app/src/androidTest/java/com/flauschcode/broccoli/recipe/CreateAndEditRecipeActivityTest.java b/app/src/androidTest/java/com/flauschcode/broccoli/recipe/CreateAndEditRecipeActivityTest.java index 090895bf..61a32ef9 100644 --- a/app/src/androidTest/java/com/flauschcode/broccoli/recipe/CreateAndEditRecipeActivityTest.java +++ b/app/src/androidTest/java/com/flauschcode/broccoli/recipe/CreateAndEditRecipeActivityTest.java @@ -81,7 +81,7 @@ public class CreateAndEditRecipeActivityTest { private ActivityScenario scenario; - private ArgumentCaptor recipeCaptor = ArgumentCaptor.forClass(Recipe.class); + private final ArgumentCaptor recipeCaptor = ArgumentCaptor.forClass(Recipe.class); private static final Recipe LAUCHKUCHEN = RecipeTestUtil.createLauchkuchen(); private static final Recipe LAUCHKUCHEN_SAVED = RecipeTestUtil.createdAlreadySavedLauchkuchen(); @@ -144,6 +144,7 @@ public void save_new_recipe() throws IOException { onView(withId(android.R.id.content)).perform(swipeUp()); // scrollTo() does not work for NestedScrollViews onView(withId(R.id.new_ingredients)).perform(closeSoftKeyboard(), typeText(LAUCHKUCHEN.getIngredients())); // it seems not to be possible to make Espresso type the enter key in a deterministic way onView(withId(R.id.new_directions)).perform(closeSoftKeyboard(), typeText(LAUCHKUCHEN.getDirections())); + onView(withId(R.id.new_notes)).perform(closeSoftKeyboard(), typeText(LAUCHKUCHEN.getNotes())); onView(withId(R.id.button_save_recipe)).perform(click()); // TODO find out why there sometimes is such a long wait @@ -157,6 +158,7 @@ public void save_new_recipe() throws IOException { assertThat(recipe.getPreparationTime(), is(LAUCHKUCHEN.getPreparationTime())); assertThat(recipe.getIngredients(), is(LAUCHKUCHEN.getIngredients())); assertThat(recipe.getDirections(), is(LAUCHKUCHEN.getDirections())); + assertThat(recipe.getNotes(), is(LAUCHKUCHEN.getNotes())); assertThat(recipe.getImageName(), startsWith("12345.jpg")); assertThat(recipe.getCategories().size(), is(1)); assertThat(recipe.getCategories().get(0), is(categoryHauptgerichte)); @@ -179,6 +181,7 @@ public void edit_recipe(){ onView(withId(R.id.new_preparation_time)).check(matches(withText(LAUCHKUCHEN_SAVED.getPreparationTime()))); onView(withId(R.id.new_ingredients)).check(matches(withText(LAUCHKUCHEN_SAVED.getIngredients()))); onView(withId(R.id.new_directions)).check(matches(withText(LAUCHKUCHEN_SAVED.getDirections()))); + onView(withId(R.id.new_notes)).check(matches(withText(LAUCHKUCHEN_SAVED.getNotes()))); onView(withId(R.id.new_servings)).perform(replaceText("1 Portion")); onView(withId(R.id.button_save_recipe)).perform(click()); diff --git a/app/src/androidTest/java/com/flauschcode/broccoli/seasons/SeasonalCalendarHolderTest.java b/app/src/androidTest/java/com/flauschcode/broccoli/seasons/SeasonalCalendarHolderTest.java index b868874f..e05aff20 100644 --- a/app/src/androidTest/java/com/flauschcode/broccoli/seasons/SeasonalCalendarHolderTest.java +++ b/app/src/androidTest/java/com/flauschcode/broccoli/seasons/SeasonalCalendarHolderTest.java @@ -3,6 +3,7 @@ import androidx.preference.PreferenceManager; import androidx.test.espresso.accessibility.AccessibilityChecks; import androidx.test.ext.junit.runners.AndroidJUnit4; +import androidx.test.filters.FlakyTest; import com.flauschcode.broccoli.BroccoliApplication; @@ -25,6 +26,7 @@ import static org.hamcrest.collection.IsEmptyCollection.empty; @RunWith(AndroidJUnit4.class) +@FlakyTest public class SeasonalCalendarHolderTest { private SeasonalCalendarHolder holder; diff --git a/app/src/androidTest/java/com/flauschcode/broccoli/util/RecipeTestUtil.java b/app/src/androidTest/java/com/flauschcode/broccoli/util/RecipeTestUtil.java index 9cebe272..d3d8934c 100644 --- a/app/src/androidTest/java/com/flauschcode/broccoli/util/RecipeTestUtil.java +++ b/app/src/androidTest/java/com/flauschcode/broccoli/util/RecipeTestUtil.java @@ -15,6 +15,7 @@ public static Recipe createLauchkuchen() { recipe.setPreparationTime("50 Minuten"); recipe.setIngredients("500g Mehl\n2 Stangen Lauch"); recipe.setDirections("1. Lauch schnippeln und Teig machen.\n2. Kochen und backen."); + recipe.setNotes("Ein paar Anmerkungen zum Lauchkuchen."); recipe.getCategories().add(new Category("Hauptgerichte")); recipe.getCategories().add(new Category("Gebackenes")); return recipe; @@ -39,6 +40,7 @@ public static Recipe createNusskuchen() { recipe.setPreparationTime("1 Stunde"); recipe.setIngredients("500g Mehl\nviel Schokolade"); recipe.setDirections("1. Teig machen.\n2. Backen.\n 3. Schokolade dazu."); + recipe.setNotes("Ein paar Anmerkungen zum Nusskuchen."); return recipe; } diff --git a/app/src/main/java/com/flauschcode/broccoli/BroccoliDatabase.java b/app/src/main/java/com/flauschcode/broccoli/BroccoliDatabase.java index a15d9c03..069f9b4c 100644 --- a/app/src/main/java/com/flauschcode/broccoli/BroccoliDatabase.java +++ b/app/src/main/java/com/flauschcode/broccoli/BroccoliDatabase.java @@ -1,9 +1,7 @@ package com.flauschcode.broccoli; -import android.content.Context; - +import androidx.room.AutoMigration; import androidx.room.Database; -import androidx.room.Room; import androidx.room.RoomDatabase; import com.flauschcode.broccoli.category.Category; @@ -13,20 +11,21 @@ import com.flauschcode.broccoli.recipe.RecipeCategoryAssociation; import com.flauschcode.broccoli.recipe.RecipeDAO; -@Database(entities = {CoreRecipe.class, Category.class, RecipeCategoryAssociation.class, CoreRecipeFts.class}, version = 1) +@Database( + version = 2, + entities = { + CoreRecipe.class, + Category.class, + RecipeCategoryAssociation.class, + CoreRecipeFts.class + }, + autoMigrations = { + @AutoMigration(from = 1, to = 2) + } +) public abstract class BroccoliDatabase extends RoomDatabase { - private static BroccoliDatabase broccoliDatabase; - - public abstract RecipeDAO getRecipeDAO(); - public abstract CategoryDAO getCategoryDAO(); - - public static synchronized BroccoliDatabase get(Context context) { - if (broccoliDatabase == null) { - broccoliDatabase = Room.databaseBuilder(context.getApplicationContext(), BroccoliDatabase.class, "broccoli") - .build(); - } - return broccoliDatabase; - } + public abstract RecipeDAO recipeDAO(); + public abstract CategoryDAO categoryDAO(); } diff --git a/app/src/main/java/com/flauschcode/broccoli/category/Category.java b/app/src/main/java/com/flauschcode/broccoli/category/Category.java index 0e6a23fb..6cbc4520 100644 --- a/app/src/main/java/com/flauschcode/broccoli/category/Category.java +++ b/app/src/main/java/com/flauschcode/broccoli/category/Category.java @@ -5,14 +5,13 @@ import androidx.room.PrimaryKey; import com.fasterxml.jackson.annotation.JsonIgnore; -import com.flauschcode.broccoli.BroccoliApplication; - -import com.flauschcode.broccoli.R; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import java.io.Serializable; import java.util.Objects; @Entity(tableName = "categories") +@JsonIgnoreProperties(ignoreUnknown = true) public class Category implements Serializable { @PrimaryKey(autoGenerate = true) diff --git a/app/src/main/java/com/flauschcode/broccoli/di/DatabaseModule.java b/app/src/main/java/com/flauschcode/broccoli/di/DatabaseModule.java index e97a6b61..bff8004b 100644 --- a/app/src/main/java/com/flauschcode/broccoli/di/DatabaseModule.java +++ b/app/src/main/java/com/flauschcode/broccoli/di/DatabaseModule.java @@ -16,7 +16,7 @@ @Module public class DatabaseModule { - private BroccoliDatabase database; + private final BroccoliDatabase database; private static final String DB_NAME = "broccoli"; @@ -34,13 +34,13 @@ BroccoliDatabase database () { @Provides @Singleton RecipeDAO recipeDAO(BroccoliDatabase database) { - return database.getRecipeDAO(); + return database.recipeDAO(); } @Provides @Singleton CategoryDAO categoryDAO(BroccoliDatabase database) { - return database.getCategoryDAO(); + return database.categoryDAO(); } } diff --git a/app/src/main/java/com/flauschcode/broccoli/recipe/CoreRecipe.java b/app/src/main/java/com/flauschcode/broccoli/recipe/CoreRecipe.java index ac56c1af..7c72e739 100644 --- a/app/src/main/java/com/flauschcode/broccoli/recipe/CoreRecipe.java +++ b/app/src/main/java/com/flauschcode/broccoli/recipe/CoreRecipe.java @@ -21,6 +21,8 @@ public class CoreRecipe implements Serializable { private String ingredients = ""; private String directions = ""; + private String notes = ""; + private boolean favorite = false; public long getRecipeId() { @@ -95,6 +97,14 @@ public void setDirections(String directions) { this.directions = directions; } + public String getNotes() { + return notes; + } + + public void setNotes(String notes) { + this.notes = notes; + } + public boolean isFavorite() { return favorite; } @@ -108,20 +118,12 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; CoreRecipe that = (CoreRecipe) o; - return recipeId == that.recipeId && - favorite == that.favorite && - Objects.equals(title, that.title) && - Objects.equals(imageName, that.imageName) && - Objects.equals(description, that.description) && - Objects.equals(servings, that.servings) && - Objects.equals(preparationTime, that.preparationTime) && - Objects.equals(source, that.source) && - Objects.equals(ingredients, that.ingredients) && - Objects.equals(directions, that.directions); + return recipeId == that.recipeId && favorite == that.favorite && Objects.equals(title, that.title) && Objects.equals(imageName, that.imageName) && Objects.equals(description, that.description) && Objects.equals(servings, that.servings) && Objects.equals(preparationTime, that.preparationTime) && Objects.equals(source, that.source) && Objects.equals(ingredients, that.ingredients) && Objects.equals(directions, that.directions) && Objects.equals(notes, that.notes); } @Override public int hashCode() { - return Objects.hash(recipeId, title, imageName, description, servings, preparationTime, source, ingredients, directions, favorite); + return Objects.hash(recipeId, title, imageName, description, servings, preparationTime, source, ingredients, directions, notes, favorite); } + } diff --git a/app/src/main/java/com/flauschcode/broccoli/recipe/Recipe.java b/app/src/main/java/com/flauschcode/broccoli/recipe/Recipe.java index e5a64bf3..8fdde7ce 100644 --- a/app/src/main/java/com/flauschcode/broccoli/recipe/Recipe.java +++ b/app/src/main/java/com/flauschcode/broccoli/recipe/Recipe.java @@ -8,6 +8,7 @@ import androidx.room.Relation; import com.fasterxml.jackson.annotation.JsonIgnore; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.flauschcode.broccoli.category.Category; import com.flauschcode.broccoli.BR; @@ -17,6 +18,7 @@ import java.util.List; import java.util.Objects; +@JsonIgnoreProperties(ignoreUnknown = true) public class Recipe extends BaseObservable implements Serializable { @Embedded @@ -135,6 +137,14 @@ public void setDirections(String directions) { this.coreRecipe.setDirections(directions); } + public String getNotes() { + return coreRecipe.getNotes(); + } + + public void setNotes(String notes) { + this.coreRecipe.setNotes(notes); + } + public boolean isFavorite() { return coreRecipe.isFavorite(); } diff --git a/app/src/main/java/com/flauschcode/broccoli/recipe/RecipeDAO.java b/app/src/main/java/com/flauschcode/broccoli/recipe/RecipeDAO.java index d0d2964a..db2b5b3c 100644 --- a/app/src/main/java/com/flauschcode/broccoli/recipe/RecipeDAO.java +++ b/app/src/main/java/com/flauschcode/broccoli/recipe/RecipeDAO.java @@ -33,7 +33,7 @@ public interface RecipeDAO { LiveData> findAll(List favorite); @Transaction - @Query(" SELECT recipes.recipeId, title, imageName, description, servings, preparationTime, source, ingredients, directions, favorite FROM recipes INNER JOIN recipes_with_categories ON recipes.recipeId = recipes_with_categories.recipeId WHERE recipes_with_categories.categoryId = :categoryId ORDER BY title COLLATE NOCASE") + @Query(" SELECT recipes.* FROM recipes INNER JOIN recipes_with_categories ON recipes.recipeId = recipes_with_categories.recipeId WHERE recipes_with_categories.categoryId = :categoryId ORDER BY title COLLATE NOCASE") LiveData> filterBy(long categoryId); @Transaction @@ -41,7 +41,7 @@ public interface RecipeDAO { LiveData> searchFor(String term, List favorite); @Transaction - @Query("SELECT recipes.recipeId, recipes.title, imageName, recipes.description, servings, preparationTime, recipes.source, recipes.ingredients, directions, favorite FROM recipes JOIN recipes_fts ON (recipes.recipeId = recipes_fts.docid) INNER JOIN recipes_with_categories ON recipes.recipeId = recipes_with_categories.recipeId WHERE recipes_with_categories.categoryId = :categoryId AND recipes_fts MATCH :term ORDER BY SUBSTR(OFFSETS(recipes_fts), 1, 1), recipes.title COLLATE NOCASE") + @Query("SELECT recipes.* FROM recipes JOIN recipes_fts ON (recipes.recipeId = recipes_fts.docid) INNER JOIN recipes_with_categories ON recipes.recipeId = recipes_with_categories.recipeId WHERE recipes_with_categories.categoryId = :categoryId AND recipes_fts MATCH :term ORDER BY SUBSTR(OFFSETS(recipes_fts), 1, 1), recipes.title COLLATE NOCASE") LiveData> filterByAndSearchFor(long categoryId, String term); @Transaction diff --git a/app/src/main/java/com/flauschcode/broccoli/recipe/sharing/ShareableRecipeBuilder.java b/app/src/main/java/com/flauschcode/broccoli/recipe/sharing/ShareableRecipeBuilder.java index 97457e43..8f4deb0f 100644 --- a/app/src/main/java/com/flauschcode/broccoli/recipe/sharing/ShareableRecipeBuilder.java +++ b/app/src/main/java/com/flauschcode/broccoli/recipe/sharing/ShareableRecipeBuilder.java @@ -69,6 +69,10 @@ private String toPlainText(Recipe recipe) { stringBuilder.append("\n"); } + if(!"".equals(recipe.getNotes())) { + stringBuilder.append(getNotesString()).append(":\n").append(recipe.getNotes()).append("\n\n"); + } + stringBuilder.append(getSharedWithString()); return stringBuilder.toString(); @@ -94,6 +98,11 @@ private String getDirectionsString() { return application.getString(R.string.directions); } + private String getNotesString() { + return application.getString(R.string.notes); + } + + private String getSharedWithString() { return application.getString(R.string.shared_with, application.getString(R.string.store_url)); } diff --git a/app/src/main/res/drawable/ic_notes_24dp.xml b/app/src/main/res/drawable/ic_notes_24dp.xml new file mode 100644 index 00000000..23e25296 --- /dev/null +++ b/app/src/main/res/drawable/ic_notes_24dp.xml @@ -0,0 +1,13 @@ + + + + + diff --git a/app/src/main/res/layout/activity_new_recipe.xml b/app/src/main/res/layout/activity_new_recipe.xml index 29789be5..c68267b6 100644 --- a/app/src/main/res/layout/activity_new_recipe.xml +++ b/app/src/main/res/layout/activity_new_recipe.xml @@ -228,6 +228,22 @@ + + + + + + diff --git a/app/src/main/res/layout/content_recipe_details.xml b/app/src/main/res/layout/content_recipe_details.xml index b5a4beba..b1056f0e 100644 --- a/app/src/main/res/layout/content_recipe_details.xml +++ b/app/src/main/res/layout/content_recipe_details.xml @@ -215,6 +215,29 @@ + + + + + + + + diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index abf2aa84..cbb8941d 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -20,6 +20,7 @@ Zeit Zutaten Anleitung + Notizen Rezept-Foto Foto ändern Foto entfernen diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index db608220..bb9f01a0 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -23,6 +23,7 @@ Tiempo Ingredientes Instrucciones + Notas Foto de la receta Cambiar foto Eliminar la foto diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85a34fa9..460cbf34 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,6 +28,7 @@ Time Ingredients Directions + Notes Recipe photo Change photo diff --git a/app/src/test/java/com/flauschcode/broccoli/recipe/sharing/ShareableRecipeBuilderTest.java b/app/src/test/java/com/flauschcode/broccoli/recipe/sharing/ShareableRecipeBuilderTest.java index d4c75cea..03c8d492 100644 --- a/app/src/test/java/com/flauschcode/broccoli/recipe/sharing/ShareableRecipeBuilderTest.java +++ b/app/src/test/java/com/flauschcode/broccoli/recipe/sharing/ShareableRecipeBuilderTest.java @@ -34,35 +34,40 @@ public class ShareableRecipeBuilderTest { @InjectMocks private ShareableRecipeBuilder shareableRecipeBuilder; - private static final String PLAIN_TEXT_RECIPE_FULL = "LAUCHKUCHEN\n" + - "\n" + - "Servings: 4 Portionen\n" + - "Preparation time: 1h\n" + - "Source: www.flauschhaus.org\n" + - "\n" + - "Das ist toll!\n" + - "\n" + - "Ingredients:\n" + - "- 500g Mehl\n" + - "- 100g Margarine\n" + - "\n" + - "Directions:\n" + - "1. Erst dies.\n" + - "2. Dann das.\n" + - "\n" + - "Shared with BROCCOLI_URL"; - - private static final String PLAIN_TEXT_RECIPE_MINIMAL = "LAUCHKUCHEN\n" + - "\n" + - "Ingredients:\n" + - "- 500g Mehl\n" + - "- 100g Margarine\n" + - "\n" + - "Directions:\n" + - "1. Erst dies.\n" + - "2. Dann das.\n" + - "\n" + - "Shared with BROCCOLI_URL"; + private static final String PLAIN_TEXT_RECIPE_FULL = """ + LAUCHKUCHEN + + Servings: 4 Portionen + Preparation time: 1h + Source: www.flauschhaus.org + + Das ist toll! + + Ingredients: + - 500g Mehl + - 100g Margarine + + Directions: + 1. Erst dies. + 2. Dann das. + + Notes: + Ein paar Anmerkungen zum Lauchkuchen. + + Shared with BROCCOLI_URL"""; + + private static final String PLAIN_TEXT_RECIPE_MINIMAL = """ + LAUCHKUCHEN + + Ingredients: + - 500g Mehl + - 100g Margarine + + Directions: + 1. Erst dies. + 2. Dann das. + + Shared with BROCCOLI_URL"""; @Before public void setUp() { @@ -71,6 +76,7 @@ public void setUp() { when(application.getString(R.string.source)).thenReturn("Source"); when(application.getString(R.string.ingredients)).thenReturn("Ingredients"); when(application.getString(R.string.directions)).thenReturn("Directions"); + when(application.getString(R.string.notes)).thenReturn("Notes"); when(application.getString(R.string.store_url)).thenReturn("BROCCOLI_URL"); when(application.getString(R.string.shared_with, "BROCCOLI_URL")).thenReturn("Shared with BROCCOLI_URL"); } @@ -85,6 +91,7 @@ public void to_plain_text_full() { recipe.setSource("www.flauschhaus.org"); recipe.setIngredients("- 500g Mehl\n - 100g Margarine "); recipe.setDirections(" 1. Erst dies. \n 2. Dann das. "); + recipe.setNotes("Ein paar Anmerkungen zum Lauchkuchen."); recipe.setImageName("image/bla.jpg"); when(recipeImageService.getUri("image/bla.jpg")).thenReturn(imageUri); diff --git a/build.gradle b/build.gradle index b4e6eb8d..bb662327 100644 --- a/build.gradle +++ b/build.gradle @@ -3,6 +3,8 @@ buildscript { ext { kotlin_version = '1.8.21' + nav_version = '2.8.5' + room_version = "2.6.1" } repositories { google() @@ -17,10 +19,9 @@ buildscript { // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files - def nav_version = '2.5.3' classpath "androidx.navigation:navigation-safe-args-gradle-plugin:$nav_version" - classpath "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin:11.2.1" + classpath "androidx.room:room-gradle-plugin:$room_version" } }