diff --git a/app/build.gradle b/app/build.gradle deleted file mode 100644 index 1a495153..00000000 --- a/app/build.gradle +++ /dev/null @@ -1,292 +0,0 @@ -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply plugin: 'kotlin-kapt' -apply plugin: "androidx.navigation.safeargs.kotlin" -apply plugin: 'com.mikepenz.aboutlibraries.plugin' -apply plugin: 'com.google.gms.google-services' // Google Services Gradle plugin -apply plugin: 'com.google.firebase.crashlytics' // Apply the Crashlytics Gradle plugin -apply plugin: 'com.google.dagger.hilt.android' -apply plugin: 'com.google.devtools.ksp' -apply plugin: 'org.jetbrains.kotlin.plugin.compose' - -android { - - namespace = "org.phenoapps.intercross" - - compileSdkVersion 36 - - signingConfigs { - - playStoreConfig { - Properties keystoreProps = new Properties() - - if (file('keystore.config').exists()) { - keystoreProps.load(new FileInputStream(file('keystore.config'))) - keyAlias keystoreProps['keyAlias'] - keyPassword keystoreProps['keyPassword'] - storePassword keystoreProps['storePassword'] - storeFile file('intercross.keystore.jks') - } - } - } - - buildFeatures { - dataBinding = true - buildConfig = true - viewBinding = true - compose true - } - - defaultConfig { - applicationId "org.phenoapps.intercross" - minSdkVersion 23 - targetSdkVersion 36 - versionCode 203 - versionName "2.0.3" - vectorDrawables.useSupportLibrary true - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - multiDexEnabled true - manifestPlaceholders = ['appAuthRedirectScheme': "fieldbook"] - - javaCompileOptions { - annotationProcessorOptions { - arguments += [ - "room.schemaLocation":"$projectDir/schemas".toString(), - "room.incremental":"true", - "room.expandProjection":"true"] - } - } - } - - buildTypes { - - debug { - clean - debuggable true - manifestPlaceholders = [crashlyticsCollectionEnabled: "false"] - applicationIdSuffix ".debug" - } - - release { - clean - minifyEnabled true - proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), "proguard-rules.pro" - debuggable false - jniDebuggable false - signingConfig signingConfigs.playStoreConfig //Add your own signing config - pseudoLocalesEnabled false - zipAlignEnabled true - shrinkResources true - } - } - - compileOptions { - sourceCompatibility JavaVersion.VERSION_17 - targetCompatibility JavaVersion.VERSION_17 - } - - kotlinOptions { - jvmTarget = JavaVersion.VERSION_17.toString() - } - - repositories { - mavenCentral() - google() - maven { - url 'https://jitpack.io' - } - } -} - -dependencies { - - def lifecycle_version = "2.10.0" - - def nav_version = "2.8.6" - - def espresso_version = "3.7.0" - - def mockito_version = "5.15.2" - - def room_lifecycle_version = "2.8.4" - - def multidex_version = "2.0.1" - - def firebase_version = "21.1.1" - - implementation fileTree(include: ['*.jar', '*.aar'], dir: 'libs') - - kapt 'com.android.databinding:compiler:3.1.4' - ksp "androidx.room:room-compiler:$room_lifecycle_version" - - implementation 'com.github.phenoapps:phenolib:v0.9.54' - - implementation 'com.google.android.exoplayer:exoplayer:2.19.1' - implementation 'com.squareup.okhttp3:okhttp:4.12.0' - - coreLibraryDesugaring "com.android.tools:desugar_jdk_libs:2.1.5" - - - //uvc camera java native libs - implementation("com.serenegiant:common:2.12.4") - - //brapi and required dependencies - implementation 'org.brapi:brapi-java-client:2.1.0' - //necessary when building phenolib locally - implementation "com.squareup.okhttp:okhttp:2.7.5" - implementation("com.squareup.okhttp3:logging-interceptor:4.12.0") - implementation("io.gsonfire:gson-fire:1.8.4") //required v1 - - //Navigation tests - androidTestImplementation "androidx.navigation:navigation-testing:$nav_version" - - testImplementation 'junit:junit:4.13.2' - implementation 'androidx.test.ext:junit:1.3.0' - - // Mockito - testImplementation ("org.mockito:mockito-core:$mockito_version") - testImplementation ("org.mockito:mockito-android:$mockito_version") - - // Core library - implementation 'androidx.test:core:1.7.0' - -// -// // AndroidJUnitRunner and JUnit Rules -// androidTestImplementation 'androidx.test:runner:1.2.0' -// androidTestImplementation 'androidx.test:rules:1.2.0' -// -// // Assertions -// androidTestImplementation 'androidx.test.ext:junit:1.1.1' -// androidTestImplementation 'androidx.test.ext:truth:1.2.0' -// androidTestImplementation 'com.google.truth:truth:0.42' -// - // Espresso dependencies - androidTestImplementation "androidx.test.espresso:espresso-core:$espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-contrib:$espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-intents:$espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-accessibility:$espresso_version" - androidTestImplementation "androidx.test.espresso:espresso-web:$espresso_version" - androidTestImplementation "androidx.test.espresso.idling:idling-concurrent:$espresso_version" - - androidTestImplementation "androidx.test.espresso:espresso-idling-resource:$espresso_version" -// -// // Optional -- Hamcrest library -// androidTestImplementation 'org.hamcrest:hamcrest-library:1.3' -// // Optional -- UI testing with Espresso -// androidTestImplementation 'androidx.test.espresso:espresso-core:3.2.0' -// // Optional -- UI testing with UI Automator -// androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' -// androidTestImplementation 'androidx.test:runner:1.2.0' -// androidTestImplementation 'androidx.test:rules:1.2.0' -// -// debugImplementation "androidx.fragment:fragment-testing:1.2.5" - - implementation "androidx.fragment:fragment-ktx:1.8.9" - - //Kotlin Extension Modules - // ViewModel - implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycle_version" - // LiveData - implementation "androidx.lifecycle:lifecycle-livedata-ktx:$lifecycle_version" - // Lifecycles only (without ViewModel or LiveData) - implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" - // Saved state module for ViewModel - implementation "androidx.lifecycle:lifecycle-viewmodel-savedstate:$lifecycle_version" - // alternately - if using Java8, use the following instead of lifecycle-compiler - implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" - - implementation "androidx.room:room-ktx:$room_lifecycle_version" - - //androidx - implementation 'androidx.preference:preference-ktx:1.2.1' - implementation 'androidx.recyclerview:recyclerview:1.4.0' - implementation "androidx.appcompat:appcompat:1.7.1" - implementation 'androidx.constraintlayout:constraintlayout:2.2.1' - implementation 'androidx.core:core-ktx:1.17.0' - implementation "androidx.room:room-runtime:$room_lifecycle_version" - implementation 'androidx.legacy:legacy-support-core-utils:1.0.0' - implementation 'androidx.appcompat:appcompat:1.7.1' - implementation "androidx.collection:collection-ktx:1.5.0" - implementation "androidx.activity:activity-ktx:1.12.2" - implementation "androidx.fragment:fragment-ktx:1.8.9" - implementation 'androidx.legacy:legacy-support-v13:1.0.0' - implementation 'androidx.vectordrawable:vectordrawable:1.2.0' - implementation 'androidx.legacy:legacy-support-core-utils:1.0.0' - - implementation "androidx.navigation:navigation-runtime-ktx:2.9.6" - implementation "androidx.navigation:navigation-fragment-ktx:2.9.6" - implementation "androidx.navigation:navigation-ui-ktx:2.9.6" - implementation "androidx.hilt:hilt-navigation-compose:1.3.0" - -// implementation 'org.apache.poi:poi:4.1.2' -// implementation 'org.apache.poi:poi-ooxml:4.1.2' - - //zebra dependency - implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0' - - implementation 'com.journeyapps:zxing-android-embedded:4.3.0' - - implementation 'com.google.code.gson:gson:2.11.0' - implementation 'com.google.android.material:material:1.13.0' - - implementation "androidx.multidex:multidex:$multidex_version" - - //jetbrains - implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:1.9.0" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.9.0' - - //github repositories - implementation 'com.github.PhilJay:MPAndroidChart:v3.1.0' - implementation 'com.github.MFlisar:changelog:1.1.7' - implementation 'com.mikepenz:aboutlibraries-core:8.3.0' - implementation 'com.mikepenz:aboutlibraries:8.3.0' - implementation 'com.github.evrencoskun:TableView:v0.8.9.4' - implementation 'com.github.ByteHamster:SearchPreference:v2.0.0' - -// Dont update the next lib - implementation 'com.github.daniel-stoneuk:material-about-library:2.4.2' - - //firebase - implementation 'com.google.firebase:firebase-crash:16.2.1' - implementation "com.google.firebase:firebase-core:$firebase_version" -// Recommended: Add the Firebase SDK for Google Analytics. - implementation 'com.google.firebase:firebase-analytics-ktx:22.5.0' - implementation "com.google.firebase:firebase-crashlytics:20.0.3" - - implementation 'net.openid:appauth:0.11.1' - implementation 'com.google.guava:listenablefuture:9999.0-empty-to-avoid-conflict-with-guava' //google messed up some packages, this package is temporary until the issue is fixed (Feb 2021) - - // hilt - implementation "com.google.dagger:hilt-android:2.56.2" - kapt "com.google.dagger:hilt-compiler:2.56.2" - - implementation 'pub.devrel:easypermissions:3.0.0' - - //AppIntro - implementation 'com.github.AppIntro:AppIntro:6.3.1' - - // Compose - def composeBom = platform('androidx.compose:compose-bom:2025.12.01') - implementation composeBom - androidTestImplementation composeBom - - implementation 'androidx.activity:activity-compose:1.12.2' - - // Material Design 3 - implementation 'androidx.compose.material3:material3' - implementation "androidx.compose.material3:material3-window-size-class:1.4.0" - implementation 'androidx.compose.material3.adaptive:adaptive:1.3.0-alpha05' - - // foundational components - implementation 'androidx.compose.foundation:foundation' - implementation 'androidx.compose.ui:ui' - - // Android Studio Preview support - implementation 'androidx.compose.ui:ui-tooling-preview' - debugImplementation 'androidx.compose.ui:ui-tooling' -} - -kapt { - //used in hilt documentation says 'Allow references to generated code" - correctErrorTypes true -} \ No newline at end of file diff --git a/app/build.gradle.kts b/app/build.gradle.kts new file mode 100644 index 00000000..d9760ca2 --- /dev/null +++ b/app/build.gradle.kts @@ -0,0 +1,258 @@ +import java.io.FileInputStream +import java.util.Properties + + +plugins { + alias(libs.plugins.android.application) + alias(libs.plugins.navigation.safeargs) + alias(libs.plugins.aboutlibraries) + alias(libs.plugins.google.services) + alias(libs.plugins.firebase.crashlytics) + alias(libs.plugins.hilt) + alias(libs.plugins.ksp) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.screenshot) +} + +kotlin { + jvmToolchain(21) +} + +android { + namespace = "org.phenoapps.intercross" + compileSdk = libs.versions.compileSdk.get().toInt() + + signingConfigs { + create("playStoreConfig") { + val keystorePropsFile = file("keystore.config") + if (keystorePropsFile.exists()) { + val keystoreProps = Properties().apply { + load(FileInputStream(keystorePropsFile)) + } + keyAlias = keystoreProps["keyAlias"] as String + keyPassword = keystoreProps["keyPassword"] as String + storePassword = keystoreProps["storePassword"] as String + storeFile = file("intercross.keystore.jks") + } + } + } + + buildFeatures { + buildConfig = true + compose = true + resValues = true + } + + defaultConfig { + applicationId = "org.phenoapps.intercross" + minSdk = libs.versions.minSdk.get().toInt() + targetSdk = libs.versions.targetSdk.get().toInt() + versionCode = libs.versions.versionCode.get().toInt() + versionName = libs.versions.versionName.get() + vectorDrawables.useSupportLibrary = true + testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" + testNamespace = "org.phenoapps.intercross.test" + manifestPlaceholders["appAuthRedirectScheme"] = "intercross" + + ksp { + arg("room.schemaLocation", "$projectDir/schemas") + } + + resValue("string", "brapi_account_type", "org.phenoapps.brapi.org.phenoapps.intercross") + } + + buildTypes { + release { + isMinifyEnabled = true + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + isDebuggable = false + isJniDebuggable = false + signingConfig = signingConfigs.getByName("playStoreConfig") + isPseudoLocalesEnabled = false + isShrinkResources = true + } + + debug { + isDebuggable = true + applicationIdSuffix = ".debug" + manifestPlaceholders["crashlyticsCollectionEnabled"] = "false" + proguardFiles( + getDefaultProguardFile("proguard-android-optimize.txt"), + "proguard-rules.pro" + ) + resValue("string", "brapi_account_type", "org.phenoapps.brapi.org.phenoapps.intercross.debug") + } + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 + } + + lint { + abortOnError = false + disable += "MissingTranslation" + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } + + experimentalProperties["android.experimental.enableScreenshotTest"] = true +} + +dependencies { + // Local libs + implementation(fileTree(mapOf("include" to listOf("*.jar", "*.aar"), "dir" to "libs"))) + implementation(libs.screenshot.validation.api) + + // BrAPI Provider module + implementation(project(":brapi-provider")) + + // Room + ksp(libs.androidx.room.compiler) + implementation(libs.androidx.room.runtime) + implementation(libs.androidx.room.ktx) + + // Desugar + coreLibraryDesugaring(libs.desugar.jdk.libs) + + // AndroidX Core + implementation(libs.androidx.core.ktx) + implementation(libs.androidx.appcompat) + implementation(libs.androidx.activity.ktx) + implementation(libs.androidx.fragment.ktx) + implementation(libs.androidx.constraintlayout) + implementation(libs.androidx.recyclerview) + implementation(libs.androidx.preference.ktx) + implementation(libs.androidx.collection.ktx) + implementation(libs.androidx.vectordrawable) + implementation(libs.androidx.legacy.support.core.utils) + implementation(libs.androidx.legacy.support.v13) + + // Lifecycle + implementation(libs.androidx.lifecycle.viewmodel.ktx) + implementation(libs.androidx.lifecycle.livedata.ktx) + implementation(libs.androidx.lifecycle.runtime.ktx) + implementation(libs.androidx.lifecycle.viewmodel.savedstate) + implementation(libs.androidx.lifecycle.common.java8) + + // Navigation + implementation(libs.androidx.navigation.fragment.ktx) + implementation(libs.androidx.navigation.ui.ktx) + implementation(libs.androidx.navigation.runtime.ktx) + + // Compose + implementation(platform(libs.androidx.compose.bom)) + androidTestImplementation(platform(libs.androidx.compose.bom)) + implementation(libs.androidx.activity.compose) + implementation(libs.androidx.compose.foundation) + implementation(libs.androidx.compose.foundation.layout) + implementation(libs.androidx.compose.ui) + implementation(libs.androidx.compose.ui.tooling.preview) + debugImplementation(libs.androidx.compose.ui.tooling) + implementation(libs.androidx.compose.material3) + implementation(libs.androidx.compose.material3.windowSizeClass) + implementation(libs.androidx.compose.material3.adaptive) + implementation(libs.androidx.compose.material.icons.extended) + implementation(libs.lazytable) + implementation(libs.androidx.hilt.navigation.compose) + + // Hilt + implementation(libs.hilt.android) + ksp(libs.hilt.compiler) + + // Firebase + implementation(platform(libs.firebase.bom)) + implementation(libs.firebase.analytics) + implementation(libs.firebase.crashlytics) + + // Google / Material + implementation(libs.google.material) + implementation(libs.google.gson) + implementation(libs.google.guava.listenablefuture) + + // Networking + implementation(libs.okhttp3) + implementation(libs.okhttp3.logging.interceptor) + implementation(libs.okhttp2) + implementation(libs.gsonfire) + + // Media + implementation(libs.exoplayer) + + // UVC camera + implementation(libs.serenegiant.common) + + // BrAPI + implementation(libs.brapi.java.client) + + // Zebra / Jackson + implementation(libs.jackson.databind) + + // QR / Barcode + implementation(libs.zxing.android.embedded) // QR code generation in CrossListItem + implementation(libs.mlkit.barcode.scanning) + implementation(libs.camerax.camera2) + implementation(libs.camerax.lifecycle) + implementation(libs.camerax.view) + + // Coroutines + implementation(libs.kotlin.coroutines.core) + implementation(libs.kotlin.coroutines.android) + + // Third-party UI + implementation(libs.mpandroidchart) + implementation(libs.changelog) + implementation(libs.aboutlibraries.core) + implementation(libs.aboutlibraries) + implementation(libs.tableview) + implementation(libs.searchpreference) + // Don't update the next lib + implementation(libs.material.about.library) + + // Auth + implementation(libs.appauth) + + // Permissions + implementation(libs.easypermissions) + + // AppIntro + implementation(libs.appintro) + + // PhenoLib + implementation(libs.phenolib) + + // Testing + testImplementation(libs.junit) + testImplementation(libs.kotest.property) + testImplementation(libs.kotest.runner.junit5) + testImplementation(libs.robolectric) + implementation(libs.androidx.test.ext.junit) + implementation(libs.androidx.test.core) + testImplementation(libs.mockito.core) + testImplementation(libs.mockito.android) + + // Espresso + androidTestImplementation(libs.espresso.core) + androidTestImplementation(libs.espresso.contrib) + androidTestImplementation(libs.espresso.intents) + androidTestImplementation(libs.espresso.accessibility) + androidTestImplementation(libs.espresso.web) + androidTestImplementation(libs.espresso.idling.concurrent) + androidTestImplementation(libs.espresso.idling.resource) + + // Navigation testing + androidTestImplementation(libs.androidx.navigation.testing) + + // Screenshot Testing + screenshotTestImplementation(libs.screenshot.validation.api) + screenshotTestImplementation(libs.androidx.compose.ui.tooling) +} + +apply(from = "../gradle/screenshots.gradle.kts") diff --git a/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/3.json b/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/3.json new file mode 100644 index 00000000..69fda1d0 --- /dev/null +++ b/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/3.json @@ -0,0 +1,438 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "cea50807ded802b922e1950728a86e94", + "entities": [ + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `mom` TEXT NOT NULL, `dad` TEXT NOT NULL, `name` TEXT NOT NULL, `date` TEXT NOT NULL, `person` TEXT NOT NULL, `experiment` TEXT NOT NULL, `type` INTEGER NOT NULL, `sex` INTEGER NOT NULL, `eid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "eventDbId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "femaleObsUnitDbId", + "columnName": "mom", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleObsUnitDbId", + "columnName": "dad", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readableName", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "person", + "columnName": "person", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "experiment", + "columnName": "experiment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sex", + "columnName": "sex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "eid" + ] + }, + "indices": [ + { + "name": "index_events_codeId", + "unique": true, + "columnNames": [ + "codeId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_events_codeId` ON `${TABLE_NAME}` (`codeId`)" + } + ] + }, + { + "tableName": "parents", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `sex` INTEGER NOT NULL, `selected` INTEGER NOT NULL, `pid` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `isPoly` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "codeId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sex", + "columnName": "sex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selected", + "columnName": "selected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "pid", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPoly", + "columnName": "isPoly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pid" + ] + }, + "indices": [ + { + "name": "index_parents_codeId", + "unique": true, + "columnNames": [ + "codeId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_parents_codeId` ON `${TABLE_NAME}` (`codeId`)" + } + ] + }, + { + "tableName": "wishlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`femaleDbId` TEXT NOT NULL, `maleDbId` TEXT NOT NULL, `femaleName` TEXT NOT NULL, `maleName` TEXT NOT NULL, `wishType` TEXT NOT NULL, `wishMin` INTEGER NOT NULL, `wishMax` INTEGER, `wid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "femaleDbId", + "columnName": "femaleDbId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleDbId", + "columnName": "maleDbId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "femaleName", + "columnName": "femaleName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleName", + "columnName": "maleName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wishType", + "columnName": "wishType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wishMin", + "columnName": "wishMin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wishMax", + "columnName": "wishMax", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "wid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "wid" + ] + }, + "indices": [ + { + "name": "index_wishlist_femaleDbId_maleDbId_wishType", + "unique": true, + "columnNames": [ + "femaleDbId", + "maleDbId", + "wishType" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_wishlist_femaleDbId_maleDbId_wishType` ON `${TABLE_NAME}` (`femaleDbId`, `maleDbId`, `wishType`)" + } + ] + }, + { + "tableName": "settings", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`isPattern` INTEGER NOT NULL, `isUUID` INTEGER NOT NULL, `startFrom` INTEGER NOT NULL, `isAutoIncrement` INTEGER NOT NULL, `pad` INTEGER NOT NULL, `number` INTEGER NOT NULL, `prefix` TEXT NOT NULL, `suffix` TEXT NOT NULL, `allowBlank` INTEGER NOT NULL, `order` INTEGER NOT NULL, `collectData` INTEGER NOT NULL, `sid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "isPattern", + "columnName": "isPattern", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isUUID", + "columnName": "isUUID", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startFrom", + "columnName": "startFrom", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "isAutoIncrement", + "columnName": "isAutoIncrement", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "pad", + "columnName": "pad", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "number", + "columnName": "number", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "prefix", + "columnName": "prefix", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "suffix", + "columnName": "suffix", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "allowBlank", + "columnName": "allowBlank", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "order", + "columnName": "order", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "collectData", + "columnName": "collectData", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "sid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "sid" + ] + } + }, + { + "tableName": "pollen_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `name` TEXT NOT NULL, `maleId` INTEGER, `gid` INTEGER PRIMARY KEY AUTOINCREMENT, `selected` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "codeId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleId", + "columnName": "maleId", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "gid", + "affinity": "INTEGER" + }, + { + "fieldPath": "selected", + "columnName": "selected", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "gid" + ] + } + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`property` TEXT NOT NULL, `defaultValue` INTEGER, `mid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "property", + "columnName": "property", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultValue", + "columnName": "defaultValue", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "mid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "mid" + ] + }, + "indices": [ + { + "name": "index_metadata_property", + "unique": true, + "columnNames": [ + "property" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_property` ON `${TABLE_NAME}` (`property`)" + } + ] + }, + { + "tableName": "metaValues", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eid` INTEGER NOT NULL, `metaId` INTEGER NOT NULL, `value` INTEGER, `mvId` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "eid", + "columnName": "eid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metaId", + "columnName": "metaId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "mvId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "mvId" + ] + }, + "indices": [ + { + "name": "index_metaValues_eid_metaId", + "unique": true, + "columnNames": [ + "eid", + "metaId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metaValues_eid_metaId` ON `${TABLE_NAME}` (`eid`, `metaId`)" + } + ] + } + ], + "views": [ + { + "viewName": "WishlistView", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT DISTINCT femaleDbId as momId, femaleName as momName, maleDbId as dadId, maleName as dadName, wishMin, wishMax, wishType,\n\t(SELECT COUNT(*) \n\tFROM events as child\n\tWHERE (w.femaleDbId = child.mom and ((w.maleDbId = child.dad) or (child.dad = \"blank\" and w.maleDbId = \"-1\")))) as wishProgress\nfrom wishlist as w" + } + ], + "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, 'cea50807ded802b922e1950728a86e94')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/5.json b/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/5.json new file mode 100644 index 00000000..b02eb39a --- /dev/null +++ b/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/5.json @@ -0,0 +1,355 @@ +{ + "formatVersion": 1, + "database": { + "version": 5, + "identityHash": "ea9807ed1ca313c4664f074bea4df9d3", + "entities": [ + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `mom` TEXT NOT NULL, `dad` TEXT NOT NULL, `name` TEXT NOT NULL, `date` TEXT NOT NULL, `person` TEXT NOT NULL, `experiment` TEXT NOT NULL, `type` INTEGER NOT NULL, `sex` INTEGER NOT NULL, `eid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "eventDbId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "femaleObsUnitDbId", + "columnName": "mom", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleObsUnitDbId", + "columnName": "dad", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readableName", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "person", + "columnName": "person", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "experiment", + "columnName": "experiment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sex", + "columnName": "sex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "eid" + ] + }, + "indices": [ + { + "name": "index_events_codeId", + "unique": true, + "columnNames": [ + "codeId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_events_codeId` ON `${TABLE_NAME}` (`codeId`)" + } + ] + }, + { + "tableName": "parents", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `sex` INTEGER NOT NULL, `selected` INTEGER NOT NULL, `pid` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `isPoly` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "codeId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sex", + "columnName": "sex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selected", + "columnName": "selected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "pid", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPoly", + "columnName": "isPoly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pid" + ] + }, + "indices": [ + { + "name": "index_parents_codeId", + "unique": true, + "columnNames": [ + "codeId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_parents_codeId` ON `${TABLE_NAME}` (`codeId`)" + } + ] + }, + { + "tableName": "wishlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`femaleDbId` TEXT NOT NULL, `maleDbId` TEXT NOT NULL, `femaleName` TEXT NOT NULL, `maleName` TEXT NOT NULL, `wishType` TEXT NOT NULL, `wishMin` INTEGER NOT NULL, `wishMax` INTEGER, `wid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "femaleDbId", + "columnName": "femaleDbId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleDbId", + "columnName": "maleDbId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "femaleName", + "columnName": "femaleName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleName", + "columnName": "maleName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wishType", + "columnName": "wishType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wishMin", + "columnName": "wishMin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wishMax", + "columnName": "wishMax", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "wid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "wid" + ] + }, + "indices": [ + { + "name": "index_wishlist_femaleDbId_maleDbId_wishType", + "unique": true, + "columnNames": [ + "femaleDbId", + "maleDbId", + "wishType" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_wishlist_femaleDbId_maleDbId_wishType` ON `${TABLE_NAME}` (`femaleDbId`, `maleDbId`, `wishType`)" + } + ] + }, + { + "tableName": "pollen_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `name` TEXT NOT NULL, `maleId` INTEGER, `gid` INTEGER PRIMARY KEY AUTOINCREMENT, `selected` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "codeId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleId", + "columnName": "maleId", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "gid", + "affinity": "INTEGER" + }, + { + "fieldPath": "selected", + "columnName": "selected", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "gid" + ] + } + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`property` TEXT NOT NULL, `defaultValue` INTEGER, `mid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "property", + "columnName": "property", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultValue", + "columnName": "defaultValue", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "mid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "mid" + ] + }, + "indices": [ + { + "name": "index_metadata_property", + "unique": true, + "columnNames": [ + "property" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_property` ON `${TABLE_NAME}` (`property`)" + } + ] + }, + { + "tableName": "metaValues", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eid` INTEGER NOT NULL, `metaId` INTEGER NOT NULL, `value` INTEGER, `mvId` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "eid", + "columnName": "eid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metaId", + "columnName": "metaId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "mvId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "mvId" + ] + }, + "indices": [ + { + "name": "index_metaValues_eid_metaId", + "unique": true, + "columnNames": [ + "eid", + "metaId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metaValues_eid_metaId` ON `${TABLE_NAME}` (`eid`, `metaId`)" + } + ] + } + ], + "views": [ + { + "viewName": "WishlistView", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT DISTINCT femaleDbId as momId, femaleName as momName, maleDbId as dadId, maleName as dadName, wishMin, wishMax, wishType,\n\t(SELECT COUNT(*) \n\tFROM events as child\n\tWHERE (w.femaleDbId = child.mom and ((w.maleDbId = child.dad) or (child.dad = \"blank\" and w.maleDbId = \"-1\")))) as wishProgress\nfrom wishlist as w" + } + ], + "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, 'ea9807ed1ca313c4664f074bea4df9d3')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/6.json b/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/6.json new file mode 100644 index 00000000..ef19aa33 --- /dev/null +++ b/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/6.json @@ -0,0 +1,360 @@ +{ + "formatVersion": 1, + "database": { + "version": 6, + "identityHash": "423883fb9ebf66e658465004e680af77", + "entities": [ + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `mom` TEXT NOT NULL, `dad` TEXT NOT NULL, `name` TEXT NOT NULL, `date` TEXT NOT NULL, `person` TEXT NOT NULL, `experiment` TEXT NOT NULL, `type` INTEGER NOT NULL, `sex` INTEGER NOT NULL, `eid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "eventDbId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "femaleObsUnitDbId", + "columnName": "mom", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleObsUnitDbId", + "columnName": "dad", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readableName", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "person", + "columnName": "person", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "experiment", + "columnName": "experiment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sex", + "columnName": "sex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "eid" + ] + }, + "indices": [ + { + "name": "index_events_codeId", + "unique": true, + "columnNames": [ + "codeId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_events_codeId` ON `${TABLE_NAME}` (`codeId`)" + } + ] + }, + { + "tableName": "parents", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `sex` INTEGER NOT NULL, `selected` INTEGER NOT NULL, `pid` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `isPoly` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "codeId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sex", + "columnName": "sex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selected", + "columnName": "selected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "pid", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPoly", + "columnName": "isPoly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pid" + ] + }, + "indices": [ + { + "name": "index_parents_codeId", + "unique": true, + "columnNames": [ + "codeId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_parents_codeId` ON `${TABLE_NAME}` (`codeId`)" + } + ] + }, + { + "tableName": "wishlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`femaleDbId` TEXT NOT NULL, `maleDbId` TEXT NOT NULL, `femaleName` TEXT NOT NULL, `maleName` TEXT NOT NULL, `wishType` TEXT NOT NULL, `wishMin` INTEGER NOT NULL, `wishMax` INTEGER, `wid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "femaleDbId", + "columnName": "femaleDbId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleDbId", + "columnName": "maleDbId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "femaleName", + "columnName": "femaleName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleName", + "columnName": "maleName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wishType", + "columnName": "wishType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wishMin", + "columnName": "wishMin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wishMax", + "columnName": "wishMax", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "wid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "wid" + ] + }, + "indices": [ + { + "name": "index_wishlist_femaleDbId_maleDbId_wishType", + "unique": true, + "columnNames": [ + "femaleDbId", + "maleDbId", + "wishType" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_wishlist_femaleDbId_maleDbId_wishType` ON `${TABLE_NAME}` (`femaleDbId`, `maleDbId`, `wishType`)" + } + ] + }, + { + "tableName": "pollen_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `name` TEXT NOT NULL, `maleId` INTEGER, `gid` INTEGER PRIMARY KEY AUTOINCREMENT, `selected` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "codeId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleId", + "columnName": "maleId", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "gid", + "affinity": "INTEGER" + }, + { + "fieldPath": "selected", + "columnName": "selected", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "gid" + ] + } + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`property` TEXT NOT NULL, `defaultValue` INTEGER, `icon` TEXT, `mid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "property", + "columnName": "property", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultValue", + "columnName": "defaultValue", + "affinity": "INTEGER" + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT" + }, + { + "fieldPath": "id", + "columnName": "mid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "mid" + ] + }, + "indices": [ + { + "name": "index_metadata_property", + "unique": true, + "columnNames": [ + "property" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_property` ON `${TABLE_NAME}` (`property`)" + } + ] + }, + { + "tableName": "metaValues", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eid` INTEGER NOT NULL, `metaId` INTEGER NOT NULL, `value` INTEGER, `mvId` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "eid", + "columnName": "eid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metaId", + "columnName": "metaId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "mvId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "mvId" + ] + }, + "indices": [ + { + "name": "index_metaValues_eid_metaId", + "unique": true, + "columnNames": [ + "eid", + "metaId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metaValues_eid_metaId` ON `${TABLE_NAME}` (`eid`, `metaId`)" + } + ] + } + ], + "views": [ + { + "viewName": "WishlistView", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT DISTINCT femaleDbId as momId, femaleName as momName, maleDbId as dadId, maleName as dadName, wishMin, wishMax, wishType,\n\t(SELECT COUNT(*) \n\tFROM events as child\n\tWHERE (w.femaleDbId = child.mom and ((w.maleDbId = child.dad) or (child.dad = \"blank\" and w.maleDbId = \"-1\")))) as wishProgress\nfrom wishlist as w" + } + ], + "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, '423883fb9ebf66e658465004e680af77')" + ] + } +} \ No newline at end of file diff --git a/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/7.json b/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/7.json new file mode 100644 index 00000000..9ed425ea --- /dev/null +++ b/app/schemas/org.phenoapps.intercross.data.IntercrossDatabase/7.json @@ -0,0 +1,366 @@ +{ + "formatVersion": 1, + "database": { + "version": 7, + "identityHash": "423593f89ebe4ad2d003e8217df75489", + "entities": [ + { + "tableName": "events", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `mom` TEXT NOT NULL, `dad` TEXT NOT NULL, `name` TEXT NOT NULL, `date` TEXT NOT NULL, `person` TEXT NOT NULL, `experiment` TEXT NOT NULL, `type` INTEGER NOT NULL, `sex` INTEGER NOT NULL, `archived` INTEGER NOT NULL, `eid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "eventDbId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "femaleObsUnitDbId", + "columnName": "mom", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleObsUnitDbId", + "columnName": "dad", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "readableName", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "timestamp", + "columnName": "date", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "person", + "columnName": "person", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "experiment", + "columnName": "experiment", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "type", + "columnName": "type", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "sex", + "columnName": "sex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "archived", + "columnName": "archived", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "eid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "eid" + ] + }, + "indices": [ + { + "name": "index_events_codeId", + "unique": true, + "columnNames": [ + "codeId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_events_codeId` ON `${TABLE_NAME}` (`codeId`)" + } + ] + }, + { + "tableName": "parents", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `sex` INTEGER NOT NULL, `selected` INTEGER NOT NULL, `pid` INTEGER PRIMARY KEY AUTOINCREMENT, `name` TEXT NOT NULL, `isPoly` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "codeId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "sex", + "columnName": "sex", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "selected", + "columnName": "selected", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "id", + "columnName": "pid", + "affinity": "INTEGER" + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "isPoly", + "columnName": "isPoly", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "pid" + ] + }, + "indices": [ + { + "name": "index_parents_codeId", + "unique": true, + "columnNames": [ + "codeId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_parents_codeId` ON `${TABLE_NAME}` (`codeId`)" + } + ] + }, + { + "tableName": "wishlist", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`femaleDbId` TEXT NOT NULL, `maleDbId` TEXT NOT NULL, `femaleName` TEXT NOT NULL, `maleName` TEXT NOT NULL, `wishType` TEXT NOT NULL, `wishMin` INTEGER NOT NULL, `wishMax` INTEGER, `wid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "femaleDbId", + "columnName": "femaleDbId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleDbId", + "columnName": "maleDbId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "femaleName", + "columnName": "femaleName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleName", + "columnName": "maleName", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wishType", + "columnName": "wishType", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "wishMin", + "columnName": "wishMin", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wishMax", + "columnName": "wishMax", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "wid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "wid" + ] + }, + "indices": [ + { + "name": "index_wishlist_femaleDbId_maleDbId_wishType", + "unique": true, + "columnNames": [ + "femaleDbId", + "maleDbId", + "wishType" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_wishlist_femaleDbId_maleDbId_wishType` ON `${TABLE_NAME}` (`femaleDbId`, `maleDbId`, `wishType`)" + } + ] + }, + { + "tableName": "pollen_groups", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`codeId` TEXT NOT NULL, `name` TEXT NOT NULL, `maleId` INTEGER, `gid` INTEGER PRIMARY KEY AUTOINCREMENT, `selected` INTEGER NOT NULL)", + "fields": [ + { + "fieldPath": "codeId", + "columnName": "codeId", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "name", + "columnName": "name", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "maleId", + "columnName": "maleId", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "gid", + "affinity": "INTEGER" + }, + { + "fieldPath": "selected", + "columnName": "selected", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "gid" + ] + } + }, + { + "tableName": "metadata", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`property` TEXT NOT NULL, `defaultValue` INTEGER, `icon` TEXT, `mid` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "property", + "columnName": "property", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "defaultValue", + "columnName": "defaultValue", + "affinity": "INTEGER" + }, + { + "fieldPath": "icon", + "columnName": "icon", + "affinity": "TEXT" + }, + { + "fieldPath": "id", + "columnName": "mid", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "mid" + ] + }, + "indices": [ + { + "name": "index_metadata_property", + "unique": true, + "columnNames": [ + "property" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metadata_property` ON `${TABLE_NAME}` (`property`)" + } + ] + }, + { + "tableName": "metaValues", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`eid` INTEGER NOT NULL, `metaId` INTEGER NOT NULL, `value` INTEGER, `mvId` INTEGER PRIMARY KEY AUTOINCREMENT)", + "fields": [ + { + "fieldPath": "eid", + "columnName": "eid", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "metaId", + "columnName": "metaId", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "value", + "columnName": "value", + "affinity": "INTEGER" + }, + { + "fieldPath": "id", + "columnName": "mvId", + "affinity": "INTEGER" + } + ], + "primaryKey": { + "autoGenerate": true, + "columnNames": [ + "mvId" + ] + }, + "indices": [ + { + "name": "index_metaValues_eid_metaId", + "unique": true, + "columnNames": [ + "eid", + "metaId" + ], + "orders": [], + "createSql": "CREATE UNIQUE INDEX IF NOT EXISTS `index_metaValues_eid_metaId` ON `${TABLE_NAME}` (`eid`, `metaId`)" + } + ] + } + ], + "views": [ + { + "viewName": "WishlistView", + "createSql": "CREATE VIEW `${VIEW_NAME}` AS SELECT DISTINCT femaleDbId as momId, femaleName as momName, maleDbId as dadId, maleName as dadName, wishMin, wishMax, wishType,\n\t(SELECT COUNT(*) \n\tFROM events as child\n\tWHERE (w.femaleDbId = child.mom and ((w.maleDbId = child.dad) or (child.dad = \"blank\" and w.maleDbId = \"-1\")))) as wishProgress\nfrom wishlist as w" + } + ], + "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, '423593f89ebe4ad2d003e8217df75489')" + ] + } +} \ No newline at end of file diff --git a/app/src/androidTest/java/org/phenoapps/intercross/test/NavigationTests.kt b/app/src/androidTest/java/org/phenoapps/intercross/test/NavigationTests.kt deleted file mode 100644 index 8a20fe3b..00000000 --- a/app/src/androidTest/java/org/phenoapps/intercross/test/NavigationTests.kt +++ /dev/null @@ -1,94 +0,0 @@ -package org.phenoapps.intercross.test - -import android.content.Intent -import android.view.Gravity -import androidx.navigation.NavController -import androidx.test.espresso.Espresso.onView -import androidx.test.espresso.action.ViewActions.* -import androidx.test.espresso.assertion.ViewAssertions.matches -import androidx.test.espresso.contrib.DrawerActions -import androidx.test.espresso.contrib.DrawerMatchers.isClosed -import androidx.test.espresso.contrib.NavigationViewActions -import androidx.test.espresso.matcher.ViewMatchers.withId -import androidx.test.rule.ActivityTestRule -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.mockito.Mockito.mock -import org.phenoapps.intercross.activities.MainActivity -import org.phenoapps.intercross.R -import java.util.* - -class NavigationTests { - - private lateinit var maleString: String - private lateinit var femaleString: String - private lateinit var crossString: String - - @get:Rule - var activityRule: ActivityTestRule - = ActivityTestRule(MainActivity::class.java) - @Before - fun initValidString() { - - maleString = UUID.randomUUID().toString() - - femaleString = UUID.randomUUID().toString() - - crossString = UUID.randomUUID().toString() - } - - private fun openNavDrawer() { - - onView(withId(R.id.drawer_layout)) - .check(matches(isClosed(Gravity.LEFT))) - .perform(DrawerActions.open()) - } - - @Test - fun testEventsToParentsFragment() { - - // Create a TestNavHostController - val navController = mock(NavController::class.java) - - activityRule.launchActivity(Intent()) - -// // Create a graphical FragmentScenario for the TitleScreen -// val titleScenario = launchFragmentInContainer() -// -// // Set the NavController property on the fragment -// titleScenario.onFragment { fragment -> -// Navigation.setViewNavController(fragment.requireView(), navController) -// } - - openNavDrawer() - - // Verify that performing a click changes the NavController’s state - onView(withId(R.id.nvView)) - .perform(NavigationViewActions.navigateTo(R.id.action_nav_parents)) - - assert(navController.currentDestination?.id == R.id.parents_fragment) - - } - - @Test - fun testEventsFragment() { - - // Type text and then press the button. - onView(withId(R.id.firstText)) - .perform(typeText(femaleString), closeSoftKeyboard()) - - onView(withId(R.id.secondText)) - .perform(typeText(maleString), closeSoftKeyboard()) - - onView(withId(R.id.firstText)) - .perform(typeText(crossString), closeSoftKeyboard()) - - onView(withId(R.id.saveButton)).perform(click()) - -// // Check that the text was changed. -// onView(withId(R.id.textToBeChanged)) -// .check(matches(withText(stringToBetyped))) - - } -} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8e51eaff..7713ffa3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,5 +1,6 @@ - + @@ -8,6 +9,8 @@ + + @@ -19,18 +22,11 @@ android:icon="@mipmap/ic_launcher" android:label="@string/app_name" android:supportsRtl="true" - android:theme="@style/AppTheme"> + android:theme="@style/AppTheme" + android:usesCleartextTraffic="true"> - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/java/androidx/core/content/MyFileProvider.java b/app/src/main/java/androidx/core/content/MyFileProvider.java deleted file mode 100644 index 0843631f..00000000 --- a/app/src/main/java/androidx/core/content/MyFileProvider.java +++ /dev/null @@ -1,844 +0,0 @@ -/* - * Copyright (C) 2013 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. - */ - -package androidx.core.content; - -import android.content.ContentProvider; -import android.content.ContentValues; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.content.pm.ProviderInfo; -import android.content.res.XmlResourceParser; -import android.database.Cursor; -import android.database.MatrixCursor; -import android.net.Uri; -import android.os.Build; -import android.os.Environment; -import android.os.ParcelFileDescriptor; -import android.provider.OpenableColumns; -import android.text.TextUtils; -import android.webkit.MimeTypeMap; - -import androidx.annotation.GuardedBy; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import org.xmlpull.v1.XmlPullParserException; - -import java.io.File; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.util.HashMap; -import java.util.Map; - -import static org.xmlpull.v1.XmlPullParser.END_DOCUMENT; -import static org.xmlpull.v1.XmlPullParser.START_TAG; - -/** - * FileProvider is a special subclass of {@link ContentProvider} that facilitates secure sharing - * of files associated with an app by creating a content:// {@link Uri} for a file - * instead of a file:/// {@link Uri}. - *

- * A content URI allows you to grant read and write access using - * temporary access permissions. When you create an {@link Intent} containing - * a content URI, in order to send the content URI - * to a client app, you can also call {@link Intent#setFlags(int) Intent.setFlags()} to add - * permissions. These permissions are available to the client app for as long as the stack for - * a receiving {@link android.app.Activity} is active. For an {@link Intent} going to a - * {@link android.app.Service}, the permissions are available as long as the - * {@link android.app.Service} is running. - *

- * In comparison, to control access to a file:/// {@link Uri} you have to modify the - * file system permissions of the underlying file. The permissions you provide become available to - * any app, and remain in effect until you change them. This level of access is - * fundamentally insecure. - *

- * The increased level of file access security offered by a content URI - * makes FileProvider a key part of Android's security infrastructure. - *

- * This overview of FileProvider includes the following topics: - *

- *
    - *
  1. Defining a FileProvider
  2. - *
  3. Specifying Available Files
  4. - *
  5. Retrieving the Content URI for a File
  6. - *
  7. Granting Temporary Permissions to a URI
  8. - *
  9. Serving a Content URI to Another App
  10. - *
- *

Defining a FileProvider

- *

- * Since the default functionality of FileProvider includes content URI generation for files, you - * don't need to define a subclass in code. Instead, you can include a FileProvider in your app - * by specifying it entirely in XML. To specify the FileProvider component itself, add a - * <provider> - * element to your app manifest. Set the android:name attribute to - * androidx.core.content.FileProvider. Set the android:authorities - * attribute to a URI authority based on a domain you control; for example, if you control the - * domain mydomain.com you should use the authority - * com.mydomain.fileprovider. Set the android:exported attribute to - * false; the FileProvider does not need to be public. Set the - * android:grantUriPermissions attribute to true, to allow you - * to grant temporary access to files. For example: - *

- *<manifest>
- *    ...
- *    <application>
- *        ...
- *        <provider
- *            android:name="androidx.core.content.FileProvider"
- *            android:authorities="com.mydomain.fileprovider"
- *            android:exported="false"
- *            android:grantUriPermissions="true">
- *            ...
- *        </provider>
- *        ...
- *    </application>
- *</manifest>
- *

- * If you want to override any of the default behavior of FileProvider methods, extend - * the FileProvider class and use the fully-qualified class name in the android:name - * attribute of the <provider> element. - *

Specifying Available Files

- * A FileProvider can only generate a content URI for files in directories that you specify - * beforehand. To specify a directory, specify the its storage area and path in XML, using child - * elements of the <paths> element. - * For example, the following paths element tells FileProvider that you intend to - * request content URIs for the images/ subdirectory of your private file area. - *
- *<paths xmlns:android="http://schemas.android.com/apk/res/android">
- *    <files-path name="my_images" path="images/"/>
- *    ...
- *</paths>
- *
- *

- * The <paths> element must contain one or more of the following child elements: - *

- *
- *
- *
- *<files-path name="name" path="path" />
- *
- *
- *
- * Represents files in the files/ subdirectory of your app's internal storage - * area. This subdirectory is the same as the value returned by {@link Context#getFilesDir() - * Context.getFilesDir()}. - *
- *
- *
- *<cache-path name="name" path="path" />
- *
- *
- *
- * Represents files in the cache subdirectory of your app's internal storage area. The root path - * of this subdirectory is the same as the value returned by {@link Context#getCacheDir() - * getCacheDir()}. - *
- *
- *
- *<external-path name="name" path="path" />
- *
- *
- *
- * Represents files in the root of the external storage area. The root path of this subdirectory - * is the same as the value returned by - * {@link Environment#getExternalStorageDirectory() Environment.getExternalStorageDirectory()}. - *
- *
- *
- *<external-files-path name="name" path="path" />
- *
- *
- *
- * Represents files in the root of your app's external storage area. The root path of this - * subdirectory is the same as the value returned by - * {@code Context#getExternalFilesDir(String) Context.getExternalFilesDir(null)}. - *
- *
- *
- *<external-cache-path name="name" path="path" />
- *
- *
- *
- * Represents files in the root of your app's external cache area. The root path of this - * subdirectory is the same as the value returned by - * {@link Context#getExternalCacheDir() Context.getExternalCacheDir()}. - *
- *
- *
- *<external-media-path name="name" path="path" />
- *
- *
- *
- * Represents files in the root of your app's external media area. The root path of this - * subdirectory is the same as the value returned by the first result of - * {@link Context#getExternalMediaDirs() Context.getExternalMediaDirs()}. - *

Note: this directory is only available on API 21+ devices.

- *
- *
- *

- * These child elements all use the same attributes: - *

- *
- *
- * name="name" - *
- *
- * A URI path segment. To enforce security, this value hides the name of the subdirectory - * you're sharing. The subdirectory name for this value is contained in the - * path attribute. - *
- *
- * path="path" - *
- *
- * The subdirectory you're sharing. While the name attribute is a URI path - * segment, the path value is an actual subdirectory name. Notice that the - * value refers to a subdirectory, not an individual file or files. You can't - * share a single file by its file name, nor can you specify a subset of files using - * wildcards. - *
- *
- *

- * You must specify a child element of <paths> for each directory that contains - * files for which you want content URIs. For example, these XML elements specify two directories: - *

- *<paths xmlns:android="http://schemas.android.com/apk/res/android">
- *    <files-path name="my_images" path="images/"/>
- *    <files-path name="my_docs" path="docs/"/>
- *</paths>
- *
- *

- * Put the <paths> element and its children in an XML file in your project. - * For example, you can add them to a new file called res/xml/file_paths.xml. - * To link this file to the FileProvider, add a - * <meta-data> element - * as a child of the <provider> element that defines the FileProvider. Set the - * <meta-data> element's "android:name" attribute to - * android.support.FILE_PROVIDER_PATHS. Set the element's "android:resource" attribute - * to @xml/file_paths (notice that you don't specify the .xml - * extension). For example: - *

- *<provider
- *    android:name="androidx.core.content.FileProvider"
- *    android:authorities="com.mydomain.fileprovider"
- *    android:exported="false"
- *    android:grantUriPermissions="true">
- *    <meta-data
- *        android:name="android.support.FILE_PROVIDER_PATHS"
- *        android:resource="@xml/file_paths" />
- *</provider>
- *
- *

Generating the Content URI for a File

- *

- * To share a file with another app using a content URI, your app has to generate the content URI. - * To generate the content URI, create a new {@link File} for the file, then pass the {@link File} - * to {@link #getUriForFile(Context, String, File) getUriForFile()}. You can send the content URI - * returned by {@link #getUriForFile(Context, String, File) getUriForFile()} to another app in an - * {@link android.content.Intent}. The client app that receives the content URI can open the file - * and access its contents by calling - * {@link android.content.ContentResolver#openFileDescriptor(Uri, String) - * ContentResolver.openFileDescriptor} to get a {@link ParcelFileDescriptor}. - *

- * For example, suppose your app is offering files to other apps with a FileProvider that has the - * authority com.mydomain.fileprovider. To get a content URI for the file - * default_image.jpg in the images/ subdirectory of your internal storage - * add the following code: - *

- *File imagePath = new File(Context.getFilesDir(), "images");
- *File newFile = new File(imagePath, "default_image.jpg");
- *Uri contentUri = getUriForFile(getContext(), "com.mydomain.fileprovider", newFile);
- *
- * As a result of the previous snippet, - * {@link #getUriForFile(Context, String, File) getUriForFile()} returns the content URI - * content://com.mydomain.fileprovider/my_images/default_image.jpg. - *

Granting Temporary Permissions to a URI

- * To grant an access permission to a content URI returned from - * {@link #getUriForFile(Context, String, File) getUriForFile()}, do one of the following: - *
    - *
  • - * Call the method - * {@link Context#grantUriPermission(String, Uri, int) - * Context.grantUriPermission(package, Uri, mode_flags)} for the content:// - * {@link Uri}, using the desired mode flags. This grants temporary access permission for the - * content URI to the specified package, according to the value of the - * the mode_flags parameter, which you can set to - * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION}, {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} - * or both. The permission remains in effect until you revoke it by calling - * {@link Context#revokeUriPermission(Uri, int) revokeUriPermission()} or until the device - * reboots. - *
  • - *
  • - * Put the content URI in an {@link Intent} by calling {@link Intent#setData(Uri) setData()}. - *
  • - *
  • - * Next, call the method {@link Intent#setFlags(int) Intent.setFlags()} with either - * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} or - * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION} or both. - *
  • - *
  • - * Finally, send the {@link Intent} to - * another app. Most often, you do this by calling - * {@link android.app.Activity#setResult(int, android.content.Intent) setResult()}. - *

    - * Permissions granted in an {@link Intent} remain in effect while the stack of the receiving - * {@link android.app.Activity} is active. When the stack finishes, the permissions are - * automatically removed. Permissions granted to one {@link android.app.Activity} in a client - * app are automatically extended to other components of that app. - *

    - *
  • - *
- *

Serving a Content URI to Another App

- *

- * There are a variety of ways to serve the content URI for a file to a client app. One common way - * is for the client app to start your app by calling - * {@link android.app.Activity#startActivityForResult(Intent, int, Bundle) startActivityResult()}, - * which sends an {@link Intent} to your app to start an {@link android.app.Activity} in your app. - * In response, your app can immediately return a content URI to the client app or present a user - * interface that allows the user to pick a file. In the latter case, once the user picks the file - * your app can return its content URI. In both cases, your app returns the content URI in an - * {@link Intent} sent via {@link android.app.Activity#setResult(int, Intent) setResult()}. - *

- *

- * You can also put the content URI in a {@link android.content.ClipData} object and then add the - * object to an {@link Intent} you send to a client app. To do this, call - * {@link Intent#setClipData(ClipData) Intent.setClipData()}. When you use this approach, you can - * add multiple {@link android.content.ClipData} objects to the {@link Intent}, each with its own - * content URI. When you call {@link Intent#setFlags(int) Intent.setFlags()} on the {@link Intent} - * to set temporary access permissions, the same permissions are applied to all of the content - * URIs. - *

- *

- * Note: The {@link Intent#setClipData(ClipData) Intent.setClipData()} method is - * only available in platform version 16 (Android 4.1) and later. If you want to maintain - * compatibility with previous versions, you should send one content URI at a time in the - * {@link Intent}. Set the action to {@link Intent#ACTION_SEND} and put the URI in data by calling - * {@link Intent#setData setData()}. - *

- *

More Information

- *

- * To learn more about FileProvider, see the Android training class - * Sharing Files Securely with URIs. - *

- */ -public class MyFileProvider extends ContentProvider { - private static final String[] COLUMNS = { - OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE }; - - private static final String - META_DATA_FILE_PROVIDER_PATHS = "android.support.FILE_PROVIDER_PATHS"; - - private static final String TAG_ROOT_PATH = "root-path"; - private static final String TAG_FILES_PATH = "files-path"; - private static final String TAG_CACHE_PATH = "cache-path"; - private static final String TAG_EXTERNAL = "external-path"; - private static final String TAG_EXTERNAL_FILES = "external-files-path"; - private static final String TAG_EXTERNAL_CACHE = "external-cache-path"; - private static final String TAG_EXTERNAL_MEDIA = "external-media-path"; - - private static final String ATTR_NAME = "name"; - private static final String ATTR_PATH = "path"; - - private static final File DEVICE_ROOT = new File("/"); - - @GuardedBy("sCache") - private static HashMap sCache = new HashMap(); - - private androidx.core.content.MyFileProvider.PathStrategy mStrategy; - - /** - * The default FileProvider implementation does not need to be initialized. If you want to - * override this method, you must provide your own subclass of FileProvider. - */ - @Override - public boolean onCreate() { - return true; - } - - /** - * After the FileProvider is instantiated, this method is called to provide the system with - * information about the provider. - * - * @param context A {@link Context} for the current component. - * @param info A {@link ProviderInfo} for the new provider. - */ - @Override - public void attachInfo(@NonNull Context context, @NonNull ProviderInfo info) { - super.attachInfo(context, info); - - // Sanity check our security - if (info.exported) { - throw new SecurityException("Provider must not be exported"); - } - if (!info.grantUriPermissions) { - throw new SecurityException("Provider must grant uri permissions"); - } - - mStrategy = getPathStrategy(context, info.authority); - } - - /** - * Return a content URI for a given {@link File}. Specific temporary - * permissions for the content URI can be set with - * {@link Context#grantUriPermission(String, Uri, int)}, or added - * to an {@link Intent} by calling {@link Intent#setData(Uri) setData()} and then - * {@link Intent#setFlags(int) setFlags()}; in both cases, the applicable flags are - * {@link Intent#FLAG_GRANT_READ_URI_PERMISSION} and - * {@link Intent#FLAG_GRANT_WRITE_URI_PERMISSION}. A FileProvider can only return a - * content {@link Uri} for file paths defined in their <paths> - * meta-data element. See the Class Overview for more information. - * - * @param context A {@link Context} for the current component. - * @param authority The authority of a {@link androidx.core.content.FileProvider} defined in a - * {@code } element in your app's manifest. - * @param file A {@link File} pointing to the filename for which you want a - * content {@link Uri}. - * @return A content URI for the file. - * @throws IllegalArgumentException When the given {@link File} is outside - * the paths supported by the provider. - */ - public static Uri getUriForFile(@NonNull Context context, @NonNull String authority, - @NonNull File file) { - final androidx.core.content.MyFileProvider.PathStrategy strategy = getPathStrategy(context, authority); - return strategy.getUriForFile(file); - } - - /** - * Use a content URI returned by - * {@link #getUriForFile(Context, String, File) getUriForFile()} to get information about a file - * managed by the FileProvider. - * FileProvider reports the column names defined in {@link android.provider.OpenableColumns}: - *
    - *
  • {@link android.provider.OpenableColumns#DISPLAY_NAME}
  • - *
  • {@link android.provider.OpenableColumns#SIZE}
  • - *
- * For more information, see - * {@link ContentProvider#query(Uri, String[], String, String[], String) - * ContentProvider.query()}. - * - * @param uri A content URI returned by {@link #getUriForFile}. - * @param projection The list of columns to put into the {@link Cursor}. If null all columns are - * included. - * @param selection Selection criteria to apply. If null then all data that matches the content - * URI is returned. - * @param selectionArgs An array of {@link java.lang.String}, containing arguments to bind to - * the selection parameter. The query method scans selection from left to - * right and iterates through selectionArgs, replacing the current "?" character in - * selection with the value at the current position in selectionArgs. The - * values are bound to selection as {@link java.lang.String} values. - * @param sortOrder A {@link java.lang.String} containing the column name(s) on which to sort - * the resulting {@link Cursor}. - * @return A {@link Cursor} containing the results of the query. - * - */ - @Override - public Cursor query(@NonNull Uri uri, @Nullable String[] projection, @Nullable String selection, - @Nullable String[] selectionArgs, - @Nullable String sortOrder) { - // ContentProvider has already checked granted permissions - final File file = mStrategy.getFileForUri(uri); - - if (projection == null) { - projection = COLUMNS; - } - - String[] cols = new String[projection.length]; - Object[] values = new Object[projection.length]; - int i = 0; - for (String col : projection) { - if (OpenableColumns.DISPLAY_NAME.equals(col)) { - cols[i] = OpenableColumns.DISPLAY_NAME; - values[i++] = file.getName(); - } else if (OpenableColumns.SIZE.equals(col)) { - cols[i] = OpenableColumns.SIZE; - values[i++] = file.length(); - } - } - - cols = copyOf(cols, i); - values = copyOf(values, i); - - final MatrixCursor cursor = new MatrixCursor(cols, 1); - cursor.addRow(values); - return cursor; - } - - /** - * Returns the MIME type of a content URI returned by - * {@link #getUriForFile(Context, String, File) getUriForFile()}. - * - * @param uri A content URI returned by - * {@link #getUriForFile(Context, String, File) getUriForFile()}. - * @return If the associated file has an extension, the MIME type associated with that - * extension; otherwise application/octet-stream. - */ - @Override - public String getType(@NonNull Uri uri) { - // ContentProvider has already checked granted permissions - final File file = mStrategy.getFileForUri(uri); - - final int lastDot = file.getName().lastIndexOf('.'); - if (lastDot >= 0) { - final String extension = file.getName().substring(lastDot + 1); - final String mime = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension); - if (mime != null) { - return mime; - } - } - - return "application/octet-stream"; - } - - /** - * By default, this method throws an {@link java.lang.UnsupportedOperationException}. You must - * subclass FileProvider if you want to provide different functionality. - */ - @Override - public Uri insert(@NonNull Uri uri, ContentValues values) { - throw new UnsupportedOperationException("No external inserts"); - } - - /** - * By default, this method throws an {@link java.lang.UnsupportedOperationException}. You must - * subclass FileProvider if you want to provide different functionality. - */ - @Override - public int update(@NonNull Uri uri, ContentValues values, @Nullable String selection, - @Nullable String[] selectionArgs) { - throw new UnsupportedOperationException("No external updates"); - } - - /** - * Deletes the file associated with the specified content URI, as - * returned by {@link #getUriForFile(Context, String, File) getUriForFile()}. Notice that this - * method does not throw an {@link java.io.IOException}; you must check its return value. - * - * @param uri A content URI for a file, as returned by - * {@link #getUriForFile(Context, String, File) getUriForFile()}. - * @param selection Ignored. Set to {@code null}. - * @param selectionArgs Ignored. Set to {@code null}. - * @return 1 if the delete succeeds; otherwise, 0. - */ - @Override - public int delete(@NonNull Uri uri, @Nullable String selection, - @Nullable String[] selectionArgs) { - // ContentProvider has already checked granted permissions - final File file = mStrategy.getFileForUri(uri); - return file.delete() ? 1 : 0; - } - - /** - * By default, FileProvider automatically returns the - * {@link ParcelFileDescriptor} for a file associated with a content:// - * {@link Uri}. To get the {@link ParcelFileDescriptor}, call - * {@link android.content.ContentResolver#openFileDescriptor(Uri, String) - * ContentResolver.openFileDescriptor}. - * - * To override this method, you must provide your own subclass of FileProvider. - * - * @param uri A content URI associated with a file, as returned by - * {@link #getUriForFile(Context, String, File) getUriForFile()}. - * @param mode Access mode for the file. May be "r" for read-only access, "rw" for read and - * write access, or "rwt" for read and write access that truncates any existing file. - * @return A new {@link ParcelFileDescriptor} with which you can access the file. - */ - @Override - public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) - throws FileNotFoundException { - // ContentProvider has already checked granted permissions - final File file = mStrategy.getFileForUri(uri); - final int fileMode = modeToMode(mode); - return ParcelFileDescriptor.open(file, fileMode); - } - - /** - * Return {@link androidx.core.content.FileProvider.PathStrategy} for given authority, either by parsing or - * returning from cache. - */ - private static androidx.core.content.MyFileProvider.PathStrategy getPathStrategy(Context context, String authority) { - androidx.core.content.MyFileProvider.PathStrategy strat; - synchronized (sCache) { - strat = sCache.get(authority); - if (strat == null) { - try { - strat = parsePathStrategy(context, authority); - } catch (IOException e) { - throw new IllegalArgumentException( - "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e); - } catch (XmlPullParserException e) { - throw new IllegalArgumentException( - "Failed to parse " + META_DATA_FILE_PROVIDER_PATHS + " meta-data", e); - } - sCache.put(authority, strat); - } - } - return strat; - } - - /** - * Parse and return {@link androidx.core.content.FileProvider.PathStrategy} for given authority as defined in - * {@link #META_DATA_FILE_PROVIDER_PATHS} {@code }. - * - * @see #getPathStrategy(Context, String) - */ - private static androidx.core.content.MyFileProvider.PathStrategy parsePathStrategy(Context context, String authority) - throws IOException, XmlPullParserException { - final androidx.core.content.MyFileProvider.SimplePathStrategy strat = new androidx.core.content.MyFileProvider.SimplePathStrategy(authority); - - final ProviderInfo info = context.getPackageManager() - .resolveContentProvider(authority, PackageManager.GET_META_DATA); - if (info == null) { - throw new IllegalArgumentException( - "Couldn't find meta-data for provider with authority " + authority); - } - - final XmlResourceParser in = info.loadXmlMetaData( - context.getPackageManager(), META_DATA_FILE_PROVIDER_PATHS); - if (in == null) { - throw new IllegalArgumentException( - "Missing " + META_DATA_FILE_PROVIDER_PATHS + " meta-data"); - } - - int type; - while ((type = in.next()) != END_DOCUMENT) { - if (type == START_TAG) { - final String tag = in.getName(); - - final String name = in.getAttributeValue(null, ATTR_NAME); - String path = in.getAttributeValue(null, ATTR_PATH); - - File target = null; - if (TAG_ROOT_PATH.equals(tag)) { - target = DEVICE_ROOT; - } else if (TAG_FILES_PATH.equals(tag)) { - target = context.getFilesDir(); - } else if (TAG_CACHE_PATH.equals(tag)) { - target = context.getCacheDir(); - } else if (TAG_EXTERNAL.equals(tag)) { - target = Environment.getExternalStorageDirectory(); - } else if (TAG_EXTERNAL_FILES.equals(tag)) { - File[] externalFilesDirs = ContextCompat.getExternalFilesDirs(context, null); - if (externalFilesDirs.length > 0) { - target = externalFilesDirs[0]; - } - } else if (TAG_EXTERNAL_CACHE.equals(tag)) { - File[] externalCacheDirs = ContextCompat.getExternalCacheDirs(context); - if (externalCacheDirs.length > 0) { - target = externalCacheDirs[0]; - } - } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP - && TAG_EXTERNAL_MEDIA.equals(tag)) { - File[] externalMediaDirs = context.getExternalMediaDirs(); - if (externalMediaDirs.length > 0) { - target = externalMediaDirs[0]; - } - } - - if (target != null) { - strat.addRoot(name, buildPath(target, path)); - } - } - } - - return strat; - } - - /** - * Strategy for mapping between {@link File} and {@link Uri}. - *

- * Strategies must be symmetric so that mapping a {@link File} to a - * {@link Uri} and then back to a {@link File} points at the original - * target. - *

- * Strategies must remain consistent across app launches, and not rely on - * dynamic state. This ensures that any generated {@link Uri} can still be - * resolved if your process is killed and later restarted. - * - * @see androidx.core.content.FileProvider.SimplePathStrategy - */ - interface PathStrategy { - /** - * Return a {@link Uri} that represents the given {@link File}. - */ - Uri getUriForFile(File file); - - /** - * Return a {@link File} that represents the given {@link Uri}. - */ - File getFileForUri(Uri uri); - } - - /** - * Strategy that provides access to files living under a narrow whitelist of - * filesystem roots. It will throw {@link SecurityException} if callers try - * accessing files outside the configured roots. - *

- * For example, if configured with - * {@code addRoot("myfiles", context.getFilesDir())}, then - * {@code context.getFileStreamPath("foo.txt")} would map to - * {@code content://myauthority/myfiles/foo.txt}. - */ - static class SimplePathStrategy implements androidx.core.content.MyFileProvider.PathStrategy { - private final String mAuthority; - private final HashMap mRoots = new HashMap(); - - SimplePathStrategy(String authority) { - mAuthority = authority; - } - - /** - * Add a mapping from a name to a filesystem root. The provider only offers - * access to files that live under configured roots. - */ - void addRoot(String name, File root) { - if (TextUtils.isEmpty(name)) { - throw new IllegalArgumentException("Name must not be empty"); - } - - try { - // Resolve to canonical path to keep path checking fast - root = root.getCanonicalFile(); - } catch (IOException e) { - throw new IllegalArgumentException( - "Failed to resolve canonical path for " + root, e); - } - - mRoots.put(name, root); - } - - @Override - public Uri getUriForFile(File file) { - String path; - try { - path = file.getCanonicalPath(); - } catch (IOException e) { - throw new IllegalArgumentException("Failed to resolve canonical path for " + file); - } - - // Find the most-specific root path - Map.Entry mostSpecific = null; - for (Map.Entry root : mRoots.entrySet()) { - final String rootPath = root.getValue().getPath(); - if (path.startsWith(rootPath) && (mostSpecific == null - || rootPath.length() > mostSpecific.getValue().getPath().length())) { - mostSpecific = root; - } - } - - if (mostSpecific == null) { - throw new IllegalArgumentException( - "Failed to find configured root that contains " + path); - } - - // Start at first char of path under root - final String rootPath = mostSpecific.getValue().getPath(); - if (rootPath.endsWith("/")) { - path = path.substring(rootPath.length()); - } - else if (path.length() == rootPath.length()) { - path = rootPath; - } - else { - path = path.substring(rootPath.length() + 1); - } - - // Encode the tag and path separately - path = Uri.encode(mostSpecific.getKey()) + '/' + Uri.encode(path, "/"); - return new Uri.Builder().scheme("content") - .authority(mAuthority).encodedPath(path).build(); - } - - @Override - public File getFileForUri(Uri uri) { - String path = uri.getEncodedPath(); - - final int splitIndex = path.indexOf('/', 1); - final String tag = Uri.decode(path.substring(1, splitIndex)); - path = Uri.decode(path.substring(splitIndex + 1)); - - final File root = mRoots.get(tag); - if (root == null) { - throw new IllegalArgumentException("Unable to find configured root for " + uri); - } - - File file = new File(root, path); - try { - file = file.getCanonicalFile(); - } catch (IOException e) { - throw new IllegalArgumentException("Failed to resolve canonical path for " + file); - } - - if (!file.getPath().startsWith(root.getPath())) { - throw new SecurityException("Resolved path jumped beyond configured root"); - } - - return file; - } - } - - /** - * Copied from ContentResolver.java - */ - private static int modeToMode(String mode) { - int modeBits; - if ("r".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_READ_ONLY; - } else if ("w".equals(mode) || "wt".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY - | ParcelFileDescriptor.MODE_CREATE - | ParcelFileDescriptor.MODE_TRUNCATE; - } else if ("wa".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_WRITE_ONLY - | ParcelFileDescriptor.MODE_CREATE - | ParcelFileDescriptor.MODE_APPEND; - } else if ("rw".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_READ_WRITE - | ParcelFileDescriptor.MODE_CREATE; - } else if ("rwt".equals(mode)) { - modeBits = ParcelFileDescriptor.MODE_READ_WRITE - | ParcelFileDescriptor.MODE_CREATE - | ParcelFileDescriptor.MODE_TRUNCATE; - } else { - throw new IllegalArgumentException("Invalid mode: " + mode); - } - return modeBits; - } - - private static File buildPath(File base, String... segments) { - File cur = base; - for (String segment : segments) { - if (segment != null) { - cur = new File(cur, segment); - } - } - return cur; - } - - private static String[] copyOf(String[] original, int newLength) { - final String[] result = new String[newLength]; - System.arraycopy(original, 0, result, 0, newLength); - return result; - } - - private static Object[] copyOf(Object[] original, int newLength) { - final Object[] result = new Object[newLength]; - System.arraycopy(original, 0, result, 0, newLength); - return result; - } -} - diff --git a/app/src/main/java/org/phenoapps/intercross/activities/BrapiAddAccountActivity.kt b/app/src/main/java/org/phenoapps/intercross/activities/BrapiAddAccountActivity.kt new file mode 100644 index 00000000..48e573ff --- /dev/null +++ b/app/src/main/java/org/phenoapps/intercross/activities/BrapiAddAccountActivity.kt @@ -0,0 +1,69 @@ +package org.phenoapps.intercross.activities + +import android.content.Intent +import android.os.Bundle +import androidx.activity.compose.setContent +import androidx.appcompat.app.AppCompatActivity +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.core.content.edit +import androidx.preference.PreferenceManager +import dagger.hilt.android.AndroidEntryPoint +import org.phenoapps.brapi.account.BrapiAccountRepository +import org.phenoapps.intercross.ui.settings.AddBrapiAccountStepper +import org.phenoapps.intercross.util.KeyUtil +import javax.inject.Inject + +@AndroidEntryPoint +class BrapiAddAccountActivity : AppCompatActivity() { + + @Inject + lateinit var accountRepository: BrapiAccountRepository + + @Inject + lateinit var keyUtil: KeyUtil + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + setContent { + Box(Modifier.fillMaxSize().padding(16.dp)) { + AddBrapiAccountStepper( + onDismiss = { finish() }, + onAuthorize = { url, displayName, oidcUrl, oidcFlow, clientId, oidcScope, version -> + accountRepository.addAccountConfig( + serverUrl = url, + displayName = displayName, + oidcUrl = oidcUrl, + oidcFlow = oidcFlow, + oidcClientId = clientId, + oidcScope = oidcScope, + brapiVersion = version, + ) + accountRepository.setActiveAccount(url) + val prefs = PreferenceManager.getDefaultSharedPreferences(this@BrapiAddAccountActivity) + prefs.edit { + putString(keyUtil.brapiOidc, oidcUrl) + putString(keyUtil.brapiFlow, oidcFlow) + putString(keyUtil.brapiClient, clientId) + putString(keyUtil.brapiScope, oidcScope) + } + val intent = Intent(this@BrapiAddAccountActivity, BrapiAuthActivity::class.java).apply { + putExtra(BrapiAuthActivity.EXTRA_SERVER_URL, url) + putExtra(BrapiAuthActivity.EXTRA_OIDC_URL, oidcUrl) + putExtra(BrapiAuthActivity.EXTRA_OIDC_FLOW, oidcFlow) + putExtra(BrapiAuthActivity.EXTRA_OIDC_CLIENT_ID, clientId) + putExtra(BrapiAuthActivity.EXTRA_OIDC_SCOPE, oidcScope) + putExtra(BrapiAuthActivity.EXTRA_BRAPI_VERSION, version) + } + startActivity(intent) + finish() + }, + ) + } + } + } +} diff --git a/app/src/main/java/org/phenoapps/intercross/activities/BrapiAuthActivity.kt b/app/src/main/java/org/phenoapps/intercross/activities/BrapiAuthActivity.kt new file mode 100644 index 00000000..e8b88c02 --- /dev/null +++ b/app/src/main/java/org/phenoapps/intercross/activities/BrapiAuthActivity.kt @@ -0,0 +1,392 @@ +package org.phenoapps.intercross.activities + +import android.app.PendingIntent +import android.content.ActivityNotFoundException +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.net.Uri +import android.os.Bundle +import android.util.Log +import android.view.View +import android.widget.Toast +import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.widget.Toolbar +import androidx.preference.PreferenceManager +import net.openid.appauth.AppAuthConfiguration +import net.openid.appauth.AuthorizationException +import net.openid.appauth.AuthorizationRequest +import net.openid.appauth.AuthorizationResponse +import net.openid.appauth.AuthorizationService +import net.openid.appauth.AuthorizationServiceConfiguration +import net.openid.appauth.ResponseTypeValues +import net.openid.appauth.TokenResponse +import androidx.core.content.edit +import androidx.core.net.toUri +import dagger.hilt.android.AndroidEntryPoint +import org.phenoapps.brapi.account.BrapiTokenStoreResult +import org.phenoapps.intercross.R +import org.phenoapps.intercross.util.BrapiAccountHelper +import org.phenoapps.intercross.util.InsetHandler +import org.phenoapps.intercross.util.KeyUtil +import org.phenoapps.intercross.util.OpenAuthConfigUtil +import javax.inject.Inject + +@AndroidEntryPoint +class BrapiAuthActivity : AppCompatActivity() { + + companion object { + private const val TAG = "BrapiAuthActivity" + const val EXTRA_SERVER_URL = "brapi_extra_server_url" + const val EXTRA_OIDC_URL = "brapi_extra_oidc_url" + const val EXTRA_OIDC_FLOW = "brapi_extra_oidc_flow" + const val EXTRA_OIDC_CLIENT_ID = "brapi_extra_oidc_client_id" + const val EXTRA_OIDC_SCOPE = "brapi_extra_oidc_scope" + const val EXTRA_BRAPI_VERSION = "brapi_extra_brapi_version" + } + + @Inject + lateinit var accountHelper: BrapiAccountHelper + + @Inject + lateinit var keyUtil: KeyUtil + + private lateinit var redirectUri: String + private var launchServerUrl: String = "" + private var launchOidcUrl: String = "" + private var launchOidcFlow: String = "" + private var launchOidcClientId: String = "" + private var launchOidcScope: String = "" + private var launchBrapiVersion: String = "" + + private lateinit var authUtil: OpenAuthConfigUtil + private var activityStarting = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_brapi_auth) + + val toolbar = findViewById(R.id.toolbar) + setSupportActionBar(toolbar) + supportActionBar?.apply { + title = getString(R.string.brapi_auth_title) + setDisplayHomeAsUpEnabled(true) + setHomeButtonEnabled(true) + } + + val rootView = findViewById(android.R.id.content) + InsetHandler.setupStandardInsets(rootView, toolbar) + + val prefs = PreferenceManager.getDefaultSharedPreferences(this) + authUtil = OpenAuthConfigUtil(this, prefs) + + redirectUri = getString(R.string.brapi_redirect_uri) + if (savedInstanceState != null) { + launchServerUrl = savedInstanceState.getString(EXTRA_SERVER_URL, "") + launchOidcUrl = savedInstanceState.getString(EXTRA_OIDC_URL, "") + launchOidcFlow = savedInstanceState.getString(EXTRA_OIDC_FLOW, "") + launchOidcClientId = savedInstanceState.getString(EXTRA_OIDC_CLIENT_ID, "") + launchOidcScope = savedInstanceState.getString(EXTRA_OIDC_SCOPE, "") + launchBrapiVersion = savedInstanceState.getString(EXTRA_BRAPI_VERSION, "") + } else { + launchServerUrl = intent?.getStringExtra(EXTRA_SERVER_URL) + ?: prefs.getString(keyUtil.brapiUrl, "") ?: "" + launchOidcUrl = intent?.getStringExtra(EXTRA_OIDC_URL) + ?: prefs.getString(keyUtil.brapiOidc, "") ?: "" + launchOidcFlow = intent?.getStringExtra(EXTRA_OIDC_FLOW) + ?: prefs.getString(keyUtil.brapiFlow, "") ?: "" + launchOidcClientId = intent?.getStringExtra(EXTRA_OIDC_CLIENT_ID) + ?: prefs.getString(keyUtil.brapiClient, getString(R.string.brapi_oidc_clientid_default)) ?: "" + launchOidcScope = intent?.getStringExtra(EXTRA_OIDC_SCOPE) + ?: prefs.getString(keyUtil.brapiScope, "") ?: "" + launchBrapiVersion = intent?.getStringExtra(EXTRA_BRAPI_VERSION) ?: "" + } + if (launchOidcClientId.isEmpty()) launchOidcClientId = getString(R.string.brapi_oidc_clientid_default) + if (launchOidcFlow.isEmpty()) launchOidcFlow = getString(R.string.pref_brapi_oidc_flow_implicit) + activityStarting = true + + // Start auth only when not returning from a deep link or AppAuth result. + if (!hasAuthResult()) { + if (isImplicitFlow(launchOidcFlow)) { + authorizeBrAPIImplicit(prefs) + } else { + authorizeBrAPICode(prefs) + } + } + } + + override fun onNewIntent(intent: Intent) { + super.onNewIntent(intent) + setIntent(intent) + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + outState.putString(EXTRA_SERVER_URL, launchServerUrl) + outState.putString(EXTRA_OIDC_URL, launchOidcUrl) + outState.putString(EXTRA_OIDC_FLOW, launchOidcFlow) + outState.putString(EXTRA_OIDC_CLIENT_ID, launchOidcClientId) + outState.putString(EXTRA_OIDC_SCOPE, launchOidcScope) + outState.putString(EXTRA_BRAPI_VERSION, launchBrapiVersion) + } + + override fun onResume() { + super.onResume() + + if (activityStarting) { + activityStarting = false + handleAuthResultIfPresent() + return + } + + if (!handleAuthResultIfPresent()) { + intent?.data = null + finish() + } + } + + private fun authorizeBrAPIImplicit(prefs: SharedPreferences) { + prefs.edit { putString(keyUtil.brapiToken, null) } + + val clientId = launchOidcClientId.ifEmpty { getString(R.string.brapi_oidc_clientid_default) } + val scope = launchOidcScope + val implicitRedirectUri = getString(R.string.brapi_implicit_redirect_uri).toUri() + + try { + authUtil.getAuthServiceConfiguration({ config, err -> + if (err != null || config == null) { + Log.e(TAG, "Failed to fetch OIDC config", err) + authError(err ?: Exception("No config")) + return@getAuthServiceConfiguration + } + try { + requestAuthorization(config, clientId, ResponseTypeValues.TOKEN, implicitRedirectUri, scope) + } catch (e: Exception) { + authError(e) + } + }, launchOidcUrl) + } catch (e: Exception) { + authError(e) + } + } + + private fun authorizeBrAPICode(prefs: SharedPreferences) { + prefs.edit { putString(keyUtil.brapiToken, null) } + + val clientId = launchOidcClientId.ifEmpty { getString(R.string.brapi_oidc_clientid_default) } + val scope = launchOidcScope + val codeRedirectUri = redirectUri.toUri() + + try { + authUtil.getAuthServiceConfiguration({ config, err -> + if (err != null || config == null) { + Log.e(TAG, "Failed to fetch OIDC config", err) + authError(err ?: Exception("No config")) + return@getAuthServiceConfiguration + } + try { + requestAuthorization(config, clientId, ResponseTypeValues.CODE, codeRedirectUri, scope) + } catch (e: Exception) { + authError(e) + } + }, launchOidcUrl) + } catch (e: Exception) { + authError(e) + } + } + + private fun requestAuthorization( + serviceConfig: AuthorizationServiceConfiguration, + clientId: String, + responseType: String, + redirectUri: Uri, + scope: String, + ) { + val authRequestBuilder = AuthorizationRequest.Builder( + serviceConfig, + clientId, + responseType, + redirectUri, + ) + + if (scope.trim().isNotEmpty()) { + authRequestBuilder.setScope("$scope openid") + } else { + authRequestBuilder.setScopes("openid") + } + + val authRequest = authRequestBuilder.setPrompt("login").build() + val authService = getAuthorizationService() + + val responseIntent = Intent(this, BrapiAuthActivity::class.java).apply { + addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) + putExtra(EXTRA_SERVER_URL, launchServerUrl) + putExtra(EXTRA_OIDC_URL, launchOidcUrl) + putExtra(EXTRA_OIDC_FLOW, launchOidcFlow) + putExtra(EXTRA_OIDC_CLIENT_ID, launchOidcClientId) + putExtra(EXTRA_OIDC_SCOPE, launchOidcScope) + putExtra(EXTRA_BRAPI_VERSION, launchBrapiVersion) + } + + authService.performAuthorizationRequest( + authRequest, + PendingIntent.getActivity( + this, + 0, + responseIntent, + PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE, + ), + ) + } + + /** + * Best available human-readable cause for an auth failure, or empty when nothing was reported. + * + * AppAuth puts the provider's own wording in errorDescription and the OAuth error code in + * error; both are absent for transport-level failures, where the exception message is all + * there is. + */ + private fun describeAuthFailure(ex: Exception?): String { + if (ex == null) return "" + + if (ex is AuthorizationException) { + val errorDesc: String? = ex.errorDescription + if (!errorDesc.isNullOrEmpty()) { + return errorDesc + } + val errorCode: String? = ex.error + if (!errorCode.isNullOrEmpty()) { + return errorCode + } + val cause = ex.cause + if (cause != null && cause.message != null) { + return cause.message!! + } + } + + return ex.message ?: "" + } + + private fun authError(ex: Exception?) { + // Clear our data from our deep link so the app doesn't think it is + // coming from a deep link if it is coming from deep link on pause and resume. + + intent.data = null + + Log.e("BrAPI", "Error starting BrAPI auth", ex) + + val reason = describeAuthFailure(ex) + val message = if (reason.isEmpty()) { + getString(R.string.brapi_auth_error_starting) + } else { + getString(R.string.brapi_auth_error_starting_reason, reason) + } + + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + setResult(RESULT_CANCELED) + finish() + } + + private fun authSuccess(accessToken: String?, idToken: String?) { + if (accessToken == null) { + authError(null) + return + } + val serverUrl = launchServerUrl.ifEmpty { getString(R.string.brapi_base_url_default) } + val stored = accountHelper.storeToken(serverUrl, accessToken, idToken) + + // Clear our data from our deep link so the app doesn't think it is + // coming from a deep link if it is coming from deep link on pause and resume. + intent.data = null + + // The provider signed us in either way, but only STORED means the server now has an + // account of its own here. Saying "authorization successful" for the other outcomes would + // promise a server card that never appears. + val message = when (stored) { + BrapiTokenStoreResult.STORED -> R.string.brapi_auth_success + BrapiTokenStoreResult.ALREADY_SHARED -> R.string.brapi_auth_success_already_shared + BrapiTokenStoreResult.ACCOUNT_UNAVAILABLE -> R.string.brapi_auth_success_no_account + } + + Log.d("BrAPI", "Auth successful, token stored: $stored") + Toast.makeText(this, message, Toast.LENGTH_LONG).show() + setResult(RESULT_OK) + finish() + } + + private fun getAuthorizationService(): AuthorizationService { + val builder = AppAuthConfiguration.Builder() + builder.setConnectionBuilder(authUtil.getConnectionBuilder()) + return AuthorizationService(this, builder.build()) + } + + private fun handleAuthResultIfPresent(): Boolean { + val ex = AuthorizationException.fromIntent(intent) + val response = AuthorizationResponse.fromIntent(intent) + val data = intent?.data + + return when { + ex != null -> { + authError(ex) + true + } + response != null || data != null -> { + checkBrapiAuth(data) + true + } + else -> false + } + } + + private fun hasAuthResult(): Boolean = + intent?.data != null || + AuthorizationException.fromIntent(intent) != null || + AuthorizationResponse.fromIntent(intent) != null + + fun checkBrapiAuth(data: Uri?) { + val authService = getAuthorizationService() + val ex = AuthorizationException.fromIntent(intent) + val response = AuthorizationResponse.fromIntent(intent) + + if (ex != null) { + authError(ex) + return + } + + if (response?.authorizationCode != null) { + authService.performTokenRequest(response.createTokenExchangeRequest()) { tokenResponse: TokenResponse?, tokenEx: AuthorizationException? -> + if (tokenResponse?.accessToken != null) { + authSuccess(tokenResponse.accessToken!!, tokenResponse.idToken) + } else { + authError(tokenEx) + } + } + return + } + + if (response?.accessToken != null) { + authSuccess(response.accessToken!!, null) + return + } + + // Fallback: parse access_token from fragment + if (data == null) { + authError(null) + return + } + val modifiedData = data.toString().replaceFirst("#", "?").toUri() + var token = modifiedData.getQueryParameter("access_token") + if (token == null) { + authError(null) + return + } + if (token.startsWith("Bearer ")) { + token = token.removePrefix("Bearer ") + } + authSuccess(token, null) + } + + private fun isImplicitFlow(flow: String): Boolean = + flow == getString(R.string.pref_brapi_oidc_flow_implicit) + || flow.contains("implicit", ignoreCase = true) +} diff --git a/app/src/main/java/org/phenoapps/intercross/activities/MainActivity.kt b/app/src/main/java/org/phenoapps/intercross/activities/MainActivity.kt index e9a5d29c..1b16c309 100644 --- a/app/src/main/java/org/phenoapps/intercross/activities/MainActivity.kt +++ b/app/src/main/java/org/phenoapps/intercross/activities/MainActivity.kt @@ -1,32 +1,21 @@ package org.phenoapps.intercross.activities import android.content.Intent -import android.content.res.Configuration import android.net.Uri import android.os.Bundle import android.os.Handler import android.os.Looper import android.view.MenuItem -import android.view.View import android.widget.Toast import androidx.activity.OnBackPressedCallback import androidx.activity.enableEdgeToEdge import androidx.activity.result.contract.ActivityResultContracts +import androidx.activity.compose.setContent import androidx.activity.viewModels import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AppCompatActivity -import androidx.appcompat.widget.Toolbar -import androidx.core.view.ViewCompat -import androidx.core.view.WindowCompat -import androidx.core.view.WindowInsetsCompat -import androidx.core.view.updatePadding -import androidx.databinding.DataBindingUtil import androidx.lifecycle.lifecycleScope -import androidx.navigation.NavController import androidx.preference.PreferenceManager -import com.bytehamster.lib.preferencesearch.SearchPreferenceFragment -import com.bytehamster.lib.preferencesearch.SearchPreferenceResult -import com.bytehamster.lib.preferencesearch.SearchPreferenceResultListener import dagger.hilt.android.AndroidEntryPoint import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -40,7 +29,6 @@ import org.phenoapps.intercross.data.MetaValuesRepository import org.phenoapps.intercross.data.MetadataRepository import org.phenoapps.intercross.data.ParentsRepository import org.phenoapps.intercross.data.PollenGroupRepository -import org.phenoapps.intercross.data.SettingsRepository import org.phenoapps.intercross.data.WishlistRepository import org.phenoapps.intercross.data.models.CrossType import org.phenoapps.intercross.data.models.Event @@ -48,41 +36,36 @@ import org.phenoapps.intercross.data.models.Meta import org.phenoapps.intercross.data.models.MetadataValues import org.phenoapps.intercross.data.models.Parent import org.phenoapps.intercross.data.models.PollenGroup -import org.phenoapps.intercross.data.models.Settings import org.phenoapps.intercross.data.models.Wishlist import org.phenoapps.intercross.data.viewmodels.EventListViewModel import org.phenoapps.intercross.data.viewmodels.MetaValuesViewModel import org.phenoapps.intercross.data.viewmodels.MetadataViewModel import org.phenoapps.intercross.data.viewmodels.ParentsListViewModel import org.phenoapps.intercross.data.viewmodels.PollenGroupListViewModel -import org.phenoapps.intercross.data.viewmodels.SettingsViewModel import org.phenoapps.intercross.data.viewmodels.WishlistViewModel import org.phenoapps.intercross.data.viewmodels.factory.EventsListViewModelFactory import org.phenoapps.intercross.data.viewmodels.factory.MetaValuesViewModelFactory import org.phenoapps.intercross.data.viewmodels.factory.MetadataViewModelFactory import org.phenoapps.intercross.data.viewmodels.factory.ParentsListViewModelFactory import org.phenoapps.intercross.data.viewmodels.factory.PollenGroupListViewModelFactory -import org.phenoapps.intercross.data.viewmodels.factory.SettingsViewModelFactory import org.phenoapps.intercross.data.viewmodels.factory.WishlistViewModelFactory -import org.phenoapps.intercross.databinding.ActivityMainBinding -import org.phenoapps.intercross.fragments.EventsFragmentDirections import org.phenoapps.intercross.fragments.ImportSampleDialogFragment -import org.phenoapps.intercross.fragments.preferences.PreferencesFragment +import org.phenoapps.intercross.ui.app.IntercrossApp +import org.phenoapps.intercross.ui.app.IntercrossAppActions +import org.phenoapps.intercross.ui.app.BrapiMode import org.phenoapps.intercross.util.DateUtil -import org.phenoapps.intercross.util.Dialogs import org.phenoapps.intercross.util.ExportUtil import org.phenoapps.intercross.util.FileUtil import org.phenoapps.intercross.util.KeyUtil -import org.phenoapps.intercross.util.SnackbarQueue +import org.phenoapps.intercross.util.CrossIdSettings import org.phenoapps.intercross.util.VerifyPersonHelper import org.phenoapps.utils.BaseDocumentTreeUtil import java.io.File import javax.inject.Inject import androidx.core.content.edit -import androidx.navigation.findNavController @AndroidEntryPoint -class MainActivity : AppCompatActivity(), SearchPreferenceResultListener { +class MainActivity : AppCompatActivity() { // private val mFirebaseAnalytics by lazy { // FirebaseAnalytics.getInstance(this) @@ -116,10 +99,6 @@ class MainActivity : AppCompatActivity(), SearchPreferenceResultListener { MetaValuesViewModelFactory(MetaValuesRepository.getInstance(mDatabase.metaValuesDao())) } - private val settingsModel: SettingsViewModel by viewModels { - SettingsViewModelFactory(SettingsRepository.getInstance(mDatabase.settingsDao())) - } - private val metadataViewModel: MetadataViewModel by viewModels { MetadataViewModelFactory(MetadataRepository.getInstance(mDatabase.metadataDao())) } @@ -141,7 +120,9 @@ class MainActivity : AppCompatActivity(), SearchPreferenceResultListener { try { - FileUtil(this).exportCrossesToFile(nonNullUri, mEvents, mParents, mGroups, mMetadata, mMetaValues) + val parentIds = (mEvents.map { it.maleObsUnitDbId } + mEvents.map { it.femaleObsUnitDbId }) + val filteredParents = mParents.filter { it.codeId in parentIds } + FileUtil(this).exportCrossesToFile(nonNullUri, mEvents, filteredParents, mGroups, mMetadata, mMetaValues) } catch (e: Exception) { @@ -295,17 +276,7 @@ class MainActivity : AppCompatActivity(), SearchPreferenceResultListener { IntercrossDatabase.getInstance(this) } - private lateinit var mSnackbar: SnackbarQueue - - private lateinit var mBinding: ActivityMainBinding - - private lateinit var mNavController: NavController - - private var preferencesFragment: PreferencesFragment? = null - - fun setPreferencesFragment(fragment: PreferencesFragment?) { - preferencesFragment = fragment - } + private var navigateToBrapiProjects: ((Int) -> Unit)? = null private fun writeStream(file: File, resourceId: Int) { @@ -389,18 +360,17 @@ class MainActivity : AppCompatActivity(), SearchPreferenceResultListener { val introIntent = Intent(this, AppIntroActivity::class.java) appIntroLauncher.launch(introIntent) - settingsModel.insert( - Settings().apply { - isUUID = true - } - ) + CrossIdSettings.save(mPref, CrossIdSettings(isUUID = true)) lifecycleScope.launch { withContext(Dispatchers.IO) { - for (property in arrayOf(getString(R.string.metadata_fruits), - getString(R.string.metadata_flowers), getString(R.string.metadata_seeds))) { + for ((property, icon) in arrayOf( + getString(R.string.metadata_fruits) to "🍎", + getString(R.string.metadata_flowers) to "🌸", + getString(R.string.metadata_seeds) to "🌱", + )) { metadataViewModel.insert( - Meta(property) + Meta(property, icon = icon) ) } } @@ -431,12 +401,6 @@ class MainActivity : AppCompatActivity(), SearchPreferenceResultListener { enableEdgeToEdge() super.onCreate(savedInstanceState) - window.apply { - WindowCompat.getInsetsController(this, decorView).apply { - isAppearanceLightStatusBars = false - } - } - verifyPersonHelper.updateAskedSinceOpened() firstRunSetup() @@ -445,62 +409,26 @@ class MainActivity : AppCompatActivity(), SearchPreferenceResultListener { setupDirs() - mBinding = DataBindingUtil.setContentView(this@MainActivity, - R.layout.activity_main - ) - - setWindowInsetListener() - - supportActionBar.apply { - title = "" - this?.let { - it.themedContext - setDisplayHomeAsUpEnabled(true) - setHomeButtonEnabled(true) - } - } - - mSnackbar = SnackbarQueue() - - mNavController = findNavController(R.id.nav_fragment) - - onBackPressedDispatcher.addCallback(this, backCallback) - - // toolbar for search screen - supportFragmentManager.addOnBackStackChangedListener { - val currentFragment = supportFragmentManager.findFragmentById(android.R.id.list_container) - if (currentFragment is SearchPreferenceFragment) { - setBackButtonToolbar() - supportActionBar?.title = getString(R.string.settings_label) - supportActionBar?.show() - } - } - startObservers() - mBinding.mainTb.setNavigationOnClickListener { - onBackPressedDispatcher.onBackPressed() + setContent { + org.phenoapps.intercross.ui.theme.AppTheme { + IntercrossApp( + actions = IntercrossAppActions( + launchImportLocal = { importedFileContent.launch("*/*") }, + launchExportLocal = { + val defaultFileNamePrefix = getString(R.string.default_crosses_export_file_name) + exportCrossesFile.launch("${defaultFileNamePrefix}_${DateUtil().getTime()}.csv") + }, + onHomeBack = { backCallback.handleOnBackPressed() }, + onNavigateToBrapiProjects = { navigateToBrapiProjects = it }, + onNavigateToProfileSettings = { navigateToProfileSettings = it }, + ), + ) + } } } - fun setBackButtonToolbar() { - setSupportActionBar(mBinding.mainTb) - - supportActionBar?.title = null - supportActionBar?.setDisplayHomeAsUpEnabled(true) - supportActionBar?.setDisplayShowHomeEnabled(true) - supportActionBar?.hide() - } - - fun setToolbar() { - setSupportActionBar(mBinding.mainTb) - - supportActionBar?.title = null - supportActionBar?.setDisplayHomeAsUpEnabled(false) - supportActionBar?.setDisplayShowHomeEnabled(false) - supportActionBar?.show() - } - private fun startObservers() { eventsModel.events.observe(this) { @@ -549,150 +477,31 @@ class MainActivity : AppCompatActivity(), SearchPreferenceResultListener { } } - private fun showExportDialog() { - - val defaultFileNamePrefix = getString(R.string.default_crosses_export_file_name) - - with(AlertDialog.Builder(this@MainActivity)) { - - setSingleChoiceItems(arrayOf("CSV", "Database"), 0) { dialog, which -> - - when (which) { - - 0 -> exportCrossesFile.launch("${defaultFileNamePrefix}_${DateUtil().getTime()}.csv") - - // 1 -> exportDatabase.launch("intercross.zip") - - } - - dialog.dismiss() + private val backCallback = object : OnBackPressedCallback(true) { + override fun handleOnBackPressed() { + if (doubleBackToExitPressedOnce) { + finish() + return } - setTitle(R.string.export) - - show() - } - } - - fun launchImport() { - - //if (mAuthPref.getString(mKeyUtil.brapiKeys.brapiTokenKey, null) != null) { - //show a dialog asking user to import from local file or brapi - //TODO -// AlertDialog.Builder(this) -// .setSingleChoiceItems(arrayOf("Local", "BrAPI"), 0) { dialog, which -> -// when (which) { -// //import file from local directory -// 0 -> importedFileContent?.launch("*/*") -// -// //start brapi import fragment -// 1 -> mNavController.navigate(CrossCountFragmentDirections.globalActionToWishlistImport()) -// -// } -// -// dialog.dismiss() -// } -// .show() - //} else { - importedFileContent.launch("*/*") - //} - } - - fun startExport(fileName: String) { - exportUtil.exportCrosses(eventsModel, mEvents, mParents, mGroups, mMetadata, mMetaValues, fileName) - } - - fun showExportDialog(onDismiss: () -> Unit) { - - //TODO - //val tokenCheck = mAuthPref.getString(mKeyUtil.brapiKeys.brapiTokenKey, null) - val importCheck = mPref.getString(mKeyUtil.brapiHasBeenImported, null) - val defaultFileNamePrefix = getString(R.string.default_crosses_export_file_name) - - if (importCheck != null) { //(tokenCheck != null || importCheck != null) { - - AlertDialog.Builder(this) - .setTitle(R.string.dialog_export_title) - .setSingleChoiceItems(arrayOf("Local", "BrAPI"), 0) { dialog, which -> - when (which) { - 0 -> { - exportCrossesFile.launch("${defaultFileNamePrefix}_${DateUtil().getTime()}.csv") - } - else -> { - mNavController.navigate(R.id.global_action_to_brapi_export) - } - } + doubleBackToExitPressedOnce = true + Toast.makeText( + this@MainActivity, + getString(R.string.press_back_again_to_exit), + Toast.LENGTH_SHORT + ).show() - dialog.dismiss() - } - .setOnDismissListener { - onDismiss() - } - .show() - - onDismiss() - - } else { - exportCrossesFile.launch("${defaultFileNamePrefix}_${DateUtil().getTime()}.csv") - } - } - - fun navigateToLastSummaryFragment() { - - val lastSummaryFragment = PreferenceManager.getDefaultSharedPreferences(this@MainActivity) - .getString("last_visited_summary", "summary") - - /*** - * Prioritize navigation to summary fragment, otherwise pick the last chosen view using preferences - * The key "last_visited_summary" is updated at the start of each respective fragment. - */ - when (lastSummaryFragment) { - - "summary" -> { - if (mEvents.isNotEmpty()) mNavController.navigate(EventsFragmentDirections.actionToCrossTrackerFragment()) - else if(mWishlist.isNotEmpty()) mNavController.navigate(EventsFragmentDirections.actionToWishlistFragment()) - else Dialogs.notify(AlertDialog.Builder(this@MainActivity), - getString(R.string.summary_and_wishlist_empty)) - } - "crossblock" -> { - if (mWishlist.isNotEmpty()) mNavController.navigate(EventsFragmentDirections.actionToCrossblock()) - else if (mEvents.isNotEmpty()) mNavController.navigate(EventsFragmentDirections.actionToCrossTrackerFragment()) - else Dialogs.notify(AlertDialog.Builder(this@MainActivity), - getString(R.string.summary_and_wishlist_empty)) - } - "wishlist" -> { - if (mWishlist.isNotEmpty()) mNavController.navigate(EventsFragmentDirections.actionToWishlistFragment()) - else if (mEvents.isNotEmpty()) mNavController.navigate(EventsFragmentDirections.actionToCrossTrackerFragment()) - else Dialogs.notify(AlertDialog.Builder(this@MainActivity), - getString(R.string.summary_and_wishlist_empty)) - } + Handler(Looper.getMainLooper()).postDelayed( + { doubleBackToExitPressedOnce = false }, + 2000 + ) } } - private val backCallback = object : OnBackPressedCallback(true) { - override fun handleOnBackPressed() { - val destId = mNavController.currentDestination?.id - - if (destId == R.id.events_fragment) { - if (doubleBackToExitPressedOnce) { // exits the app - finish() - return - } - - doubleBackToExitPressedOnce = true - Toast.makeText(this@MainActivity, "Press back again to exit", Toast.LENGTH_SHORT).show() - - Handler(Looper.getMainLooper()).postDelayed( - { doubleBackToExitPressedOnce = false }, - 2000 - ) - return - } + private var navigateToProfileSettings: (() -> Unit)? = null - // for any other fragment, just pop the fragment - val popped = mNavController.popBackStack() - if (!popped) finish() - } + fun navigateToProfileSettings() { + navigateToProfileSettings?.invoke() } override fun onOptionsItemSelected(item: MenuItem): Boolean { @@ -704,12 +513,6 @@ class MainActivity : AppCompatActivity(), SearchPreferenceResultListener { return super.onOptionsItemSelected(item) } - override fun onSearchResultClicked(result: SearchPreferenceResult) { - Handler().post { // handle in preferencesFragment - preferencesFragment?.onSearchResultClicked(result) - } - } - // private fun savePersonAndExperiment(person: String, experiment: String) { // val editor = mPref.edit() // editor.putString(mKeyUtil.profPersonKey, person) @@ -723,83 +526,4 @@ class MainActivity : AppCompatActivity(), SearchPreferenceResultListener { // return Pair(person, experiment) // } - private fun setWindowInsetListener() { - val leftScrim = mBinding.cameraScrimLeft - val rightScrim = mBinding.cameraScrimRight - - ViewCompat.setOnApplyWindowInsetsListener(mBinding.root) { _, windowInsets -> - val insets = windowInsets.getInsets( - WindowInsetsCompat.Type.displayCutout() or WindowInsetsCompat.Type.systemBars() - ) - - when (resources.configuration.orientation) { - Configuration.ORIENTATION_LANDSCAPE -> { - when { // when the camera is on the left - insets.left > 0 -> { - leftScrim.visibility = View.VISIBLE - leftScrim.layoutParams.width = insets.left - leftScrim.requestLayout() - - rightScrim.visibility = View.GONE - - mBinding.root.setPadding(0, 0, insets.right, 0) - } - insets.right > 0 -> { // when the camera is on the right - rightScrim.visibility = View.VISIBLE - rightScrim.layoutParams.width = insets.right - rightScrim.requestLayout() - - leftScrim.visibility = View.GONE - mBinding.root.setPadding(insets.left, 0, 0, 0) - } - } - } - else -> { // portrait mode - leftScrim.visibility = View.GONE - rightScrim.visibility= View.GONE - mBinding.root.setPadding(0, 0, 0, 0) - } - } - - mBinding.mainTb.updatePadding(top = insets.top) - - windowInsets - } - } - - fun applyFragmentInsets(root: View, toolbar: Toolbar?) { - ViewCompat.setOnApplyWindowInsetsListener(root) { _, windowInsets -> - val insets = windowInsets.getInsets( - WindowInsetsCompat.Type.systemBars() or - WindowInsetsCompat.Type.displayCutout() - ) - - toolbar?.updatePadding(top = insets.top) - - windowInsets - } - } - - /** - * use this for fragments without a bottom nav bar - */ - fun applyBottomInsets(root: View) { - ViewCompat.setOnApplyWindowInsetsListener(root) { _, windowInsets -> - val systemInsets = windowInsets.getInsets( - WindowInsetsCompat.Type.systemBars() or - WindowInsetsCompat.Type.displayCutout() - ) - - val imeInsets = windowInsets.getInsets(WindowInsetsCompat.Type.ime()) - - // if keyboard is visible, use ime insets, otherwise use system insets - val bottomInsets = - if (imeInsets.bottom > 0) imeInsets.bottom - else systemInsets.bottom - - root.updatePadding(bottom = bottomInsets) - - windowInsets - } - } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/BindingAdapters.kt b/app/src/main/java/org/phenoapps/intercross/adapters/BindingAdapters.kt deleted file mode 100644 index d262cdfd..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/BindingAdapters.kt +++ /dev/null @@ -1,84 +0,0 @@ -package org.phenoapps.intercross.adapters - -import android.view.ViewGroup -import android.widget.EditText -import android.widget.ImageView -import android.widget.TextView -import androidx.databinding.BindingAdapter -import org.phenoapps.intercross.R -import org.phenoapps.intercross.data.models.CrossType -import org.phenoapps.intercross.data.models.Event -import org.phenoapps.intercross.data.models.Settings -import org.phenoapps.intercross.util.AsyncLoadBarcode -import java.util.* - -@BindingAdapter("setImageByCrossType") -fun bindCrossTypeImage(view: ImageView, event: Event?) { - - if (event == null) { - - view.setImageResource(R.drawable.ic_cross_unknown) - - } else { - - view.setImageResource(when(event.type) { - - CrossType.BIPARENTAL -> R.drawable.ic_cross_biparental - - CrossType.POLY -> R.drawable.ic_cross_poly - - CrossType.SELF -> R.drawable.ic_cross_self - - CrossType.OPEN -> R.drawable.ic_cross_open - - CrossType.UNKNOWN -> R.drawable.ic_cross_unknown - - }) - } -} - -@BindingAdapter("setQRCode") -fun bindQRCodeImage(view: ImageView, code: String?) { - - code?.let { - - view.tag = code - - AsyncLoadBarcode(view, code).execute(code) - } -} - -@BindingAdapter("layoutMarginStart") -fun bindLayoutMarginStart(view: TextView, spacing: Float) { - - view.layoutParams = (view.layoutParams as ViewGroup.MarginLayoutParams).apply { - - marginStart = spacing.toInt() - - } -} - -@BindingAdapter("setCrossId") -fun bindCrossId(view: EditText, settings: Settings?) { - - settings?.let { - - with (it) { - - when { - - isPattern -> { - - view.setText(pattern) - - } - - isUUID -> { - - view.setText(UUID.randomUUID().toString()) - - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/CrossBlockTableViewAdapter.java b/app/src/main/java/org/phenoapps/intercross/adapters/CrossBlockTableViewAdapter.java deleted file mode 100644 index 525e5085..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/CrossBlockTableViewAdapter.java +++ /dev/null @@ -1,273 +0,0 @@ -package org.phenoapps.intercross.adapters; - -import android.graphics.Color; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.LinearLayout; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import com.evrencoskun.tableview.adapter.AbstractTableAdapter; -import com.evrencoskun.tableview.adapter.recyclerview.holder.AbstractSorterViewHolder; -import com.evrencoskun.tableview.adapter.recyclerview.holder.AbstractViewHolder; -import org.jetbrains.annotations.NotNull; -import org.phenoapps.intercross.R; -import org.phenoapps.intercross.fragments.CrossBlockFragment; - -/** - * https://github.com/evrencoskun/TableView/wiki - */ -public class CrossBlockTableViewAdapter extends AbstractTableAdapter { - - static class CellViewHolder extends AbstractSorterViewHolder { - - final TextView textView; - final LinearLayout linearLayout; - - public CellViewHolder(View itemView) { - super(itemView); - - linearLayout = itemView.findViewById(R.id.list_item_table_cell_container); - textView = itemView.findViewById(R.id.list_item_table_cell_tv); - } - } - - static class RowHeaderViewHolder extends AbstractViewHolder { - - final TextView textView; - final LinearLayout linearLayout; - - public RowHeaderViewHolder(@NonNull View itemView) { - super(itemView); - - linearLayout = itemView.findViewById(R.id.list_item_table_cell_container); - textView = itemView.findViewById(R.id.list_item_table_cell_tv); - } - } - - static class ColumnHeaderViewHolder extends AbstractViewHolder { - - final TextView textView; - final LinearLayout linearLayout; - - public ColumnHeaderViewHolder(@NonNull View itemView) { - super(itemView); - - linearLayout = itemView.findViewById(R.id.list_item_table_cell_container); - textView = itemView.findViewById(R.id.list_item_table_cell_tv); - } - } - - /** - * This is where you create your custom Cell ViewHolder. This method is called when Cell - * RecyclerView of the TableView needs a new RecyclerView.ViewHolder of the given type to - * represent an item. - * - * @param viewType : This value comes from #getCellItemViewType method to support different type - * of viewHolder as a Cell item. - * - * @see #getCellItemViewType(int); - */ - @NotNull - @Override - public AbstractViewHolder onCreateCellViewHolder(ViewGroup parent, int viewType) { - // Get cell xml layout - View layout = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.list_item_table_cell, parent, false); - // Create a Custom ViewHolder for a Cell item. - return new CellViewHolder(layout); - } - - /** - * That is where you set Cell View Model data to your custom Cell ViewHolder. This method is - * Called by Cell RecyclerView of the TableView to display the data at the specified position. - * This method gives you everything you need about a cell item. - * - * @param holder : This is one of your cell ViewHolders that was created on - * ```onCreateCellViewHolder``` method. In this example, we have created - * "MyCellViewHolder" holder. - * @param cellItemModel : This is the cell view model located on this X and Y position. In this - * example, the model class is "Cell". - * @param columnPosition : This is the X (Column) position of the cell item. - * @param rowPosition : This is the Y (Row) position of the cell item. - * - * @see #onCreateCellViewHolder(ViewGroup, int); - */ - @Override - public void onBindCellViewHolder(@NotNull AbstractViewHolder holder, CrossBlockFragment.CellData cellItemModel, int - columnPosition, int rowPosition) { - - // Get the holder to update cell item text - CellViewHolder viewHolder = (CellViewHolder) holder; - - if (cellItemModel != null) { - if (cellItemModel.getFid().isEmpty() && cellItemModel.getMid().isEmpty()) { - viewHolder.linearLayout.setBackgroundColor(Color.WHITE); - } else viewHolder.linearLayout.setBackgroundColor(cellItemModel.getProgressColor()); - } - - // If your TableView should have auto resize for cells & columns. - // Then you should consider the below lines. Otherwise, you can ignore them. - - // It is necessary to remeasure itself. - viewHolder.linearLayout.getLayoutParams().width = LinearLayout.LayoutParams.WRAP_CONTENT; - viewHolder.linearLayout.requestLayout(); - viewHolder.textView.requestLayout(); - } - - /** - * This is where you create your custom Column Header ViewHolder. This method is called when - * Column Header RecyclerView of the TableView needs a new RecyclerView.ViewHolder of the given - * type to represent an item. - * - * @param viewType : This value comes from "getColumnHeaderItemViewType" method to support - * different type of viewHolder as a Column Header item. - * - * @see #getColumnHeaderItemViewType(int); - */ - @NotNull - @Override - public AbstractViewHolder onCreateColumnHeaderViewHolder(ViewGroup parent, int viewType) { - - // Get Column Header xml Layout - View layout = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.list_item_table_column_header, parent, false); - - // Create a ColumnHeader ViewHolder - return new ColumnHeaderViewHolder(layout); - } - - /** - * That is where you set Column Header View Model data to your custom Column Header ViewHolder. - * This method is Called by ColumnHeader RecyclerView of the TableView to display the data at - * the specified position. This method gives you everything you need about a column header - * item. - * - * @param holder : This is one of your column header ViewHolders that was created on - * ```onCreateColumnHeaderViewHolder``` method. In this example we have created - * "MyColumnHeaderViewHolder" holder. - * @param columnHeaderItemModel : This is the column header view model located on this X position. In this - * example, the model class is "ColumnHeader". - * @param position : This is the X (Column) position of the column header item. - * - * @see #onCreateColumnHeaderViewHolder(ViewGroup, int) ; - */ - @Override - public void onBindColumnHeaderViewHolder(@NotNull AbstractViewHolder holder, - CrossBlockFragment.CellData columnHeaderItemModel, - int position) { - - // Get the holder to update cell item text - ColumnHeaderViewHolder columnHeaderViewHolder = (ColumnHeaderViewHolder) holder; - - if (columnHeaderItemModel != null) { - columnHeaderViewHolder.textView.setText(columnHeaderItemModel.getText()); - - columnHeaderViewHolder.itemView.setOnClickListener(v -> { - Toast.makeText(v.getContext(), columnHeaderItemModel.getText(), Toast.LENGTH_SHORT).show(); - }); - } - - // If your TableView should have auto resize for cells & columns. - // Then you should consider the below lines. Otherwise, you can ignore them. - - // It is necessary to remeasure itself. - columnHeaderViewHolder.linearLayout.getLayoutParams().width = LinearLayout.LayoutParams.WRAP_CONTENT; - columnHeaderViewHolder.linearLayout.requestLayout(); - columnHeaderViewHolder.textView.requestLayout(); - } - - /** - * This is where you create your custom Row Header ViewHolder. This method is called when - * Row Header RecyclerView of the TableView needs a new RecyclerView.ViewHolder of the given - * type to represent an item. - * - * @param viewType : This value comes from "getRowHeaderItemViewType" method to support - * different type of viewHolder as a row Header item. - * - * @see #getRowHeaderItemViewType(int); - */ - @NotNull - @Override - public AbstractViewHolder onCreateRowHeaderViewHolder(ViewGroup parent, int viewType) { - - // Get Row Header xml Layout - View layout = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.list_item_table_cell, parent, false); - - // Create a Row Header ViewHolder - return new RowHeaderViewHolder(layout); - } - - - /** - * That is where you set Row Header View Model data to your custom Row Header ViewHolder. This - * method is Called by RowHeader RecyclerView of the TableView to display the data at the - * specified position. This method gives you everything you need about a row header item. - * - * @param holder : This is one of your row header ViewHolders that was created on - * ```onCreateRowHeaderViewHolder``` method. In this example, we have created - * "MyRowHeaderViewHolder" holder. - * @param rowHeaderItemModel : This is the row header view model located on this Y position. In this - * example, the model class is "RowHeader". - * @param position : This is the Y (row) position of the row header item. - * - * @see #onCreateRowHeaderViewHolder(ViewGroup, int) ; - */ - @Override - public void onBindRowHeaderViewHolder(@NotNull AbstractViewHolder holder, CrossBlockFragment.CellData rowHeaderItemModel, int - position) { - - // Get the holder to update row header item text - RowHeaderViewHolder rowHeaderViewHolder = (RowHeaderViewHolder) holder; - - if (rowHeaderItemModel != null) { - rowHeaderViewHolder.textView.setText(rowHeaderItemModel.getText()); - - rowHeaderViewHolder.itemView.setOnClickListener(v -> { - Toast.makeText(v.getContext(), rowHeaderItemModel.getText(), Toast.LENGTH_SHORT).show(); - }); - } - - rowHeaderViewHolder.linearLayout.getLayoutParams().width = LinearLayout.LayoutParams.WRAP_CONTENT; - rowHeaderViewHolder.linearLayout.requestLayout(); - rowHeaderViewHolder.textView.requestLayout(); - } - - @NotNull - @Override - public View onCreateCornerView(ViewGroup parent) { - // Get Corner xml layout - return LayoutInflater.from(parent.getContext()) - .inflate(R.layout.list_item_corner, parent, false); - } - - @Override - public int getColumnHeaderItemViewType(int columnPosition) { - // The unique ID for this type of column header item - // If you have different items for Cell View by X (Column) position, - // then you should fill this method to be able create different - // type of ColumnViewHolder on "onCreateColumnViewHolder" - return 0; - } - - @Override - public int getRowHeaderItemViewType(int rowPosition) { - // The unique ID for this type of row header item - // If you have different items for Row Header View by Y (Row) position, - // then you should fill this method to be able create different - // type of RowHeaderViewHolder on "onCreateRowHeaderViewHolder" - return 0; - } - - @Override - public int getCellItemViewType(int columnPosition) { - // The unique ID for this type of cell item - // If you have different items for Cell View by X (Column) position, - // then you should fill this method to be able create different - // type of CellViewHolder on "onCreateCellViewHolder" - return 0; - } - } \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/CrossTrackerAdapter.kt b/app/src/main/java/org/phenoapps/intercross/adapters/CrossTrackerAdapter.kt deleted file mode 100644 index a8f0d746..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/CrossTrackerAdapter.kt +++ /dev/null @@ -1,261 +0,0 @@ -package org.phenoapps.intercross.adapters - -import android.content.Context -import android.view.LayoutInflater -import android.view.MotionEvent -import android.view.View -import android.view.ViewGroup -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import androidx.cardview.widget.CardView -import androidx.constraintlayout.helper.widget.Flow -import androidx.constraintlayout.widget.ConstraintLayout -import androidx.core.content.ContextCompat -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import com.google.android.material.chip.Chip -import com.google.android.material.progressindicator.LinearProgressIndicator -import org.phenoapps.intercross.R -import org.phenoapps.intercross.fragments.CrossTrackerFragment -import org.phenoapps.intercross.fragments.CrossTrackerFragment.ListEntry -import org.phenoapps.intercross.fragments.CrossTrackerFragment.PlannedCrossData -import org.phenoapps.intercross.interfaces.CrossController -import org.phenoapps.intercross.util.DateUtil -import org.phenoapps.intercross.util.WishProgressColorUtil -import androidx.core.view.isVisible - -class CrossTrackerAdapter( - private val crossController: CrossController, -) : ListAdapter( - DiffCallback() - ) { - - inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val crossCount: TextView = view.findViewById(R.id.cross_count) - val femaleParent: TextView = view.findViewById(R.id.female_parent) - val maleParent: TextView = view.findViewById(R.id.male_parent) - val personChip: Chip = view.findViewById(R.id.person_chip) - val dateChip: Chip = view.findViewById(R.id.date_chip) - - val progressSection: LinearLayout = view.findViewById(R.id.wish_progress_section) - val progressStatusIcon: ImageView = view.findViewById(R.id.wish_progress_status) - val progressBar: LinearProgressIndicator = view.findViewById(R.id.wish_progress_bar) - - init { - val crossCardView: CardView = view.findViewById(R.id.cross_card) - var isLongPress = false - - // intercept touch events to prevent person, date and wishlist progress chip ripple - crossCardView.setOnTouchListener{ _, event -> - if (event.action == MotionEvent.ACTION_UP) { - if (!isLongPress) { - view.performClick() - handleClick(layoutPosition) - } - isLongPress = false - } - true - } - } - } - - private fun handleClick(position: Int) { - if (position != RecyclerView.NO_POSITION) { - when (val item = currentList[position]) { - is PlannedCrossData -> { - crossController.onCrossClicked(item.maleId, item.femaleId) - } - else -> { - crossController.onCrossClicked(item.male, item.female) - } - } - } - } - - // Create new views (invoked by the layout manager) - override fun onCreateViewHolder(viewGroup: ViewGroup, viewType: Int): ViewHolder { - // Create a new view, which defines the UI of the list item - val view = LayoutInflater.from(viewGroup.context) - .inflate(R.layout.list_item_cross_tracker, viewGroup, false) - - return ViewHolder(view) - } - - // Replace the contents of a view (invoked by the layout manager) - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - - // Get element from your dataset at this position and replace the - // contents of the view with that element - with(currentList[position]) { - viewHolder.apply { - crossCount.text = count - femaleParent.text = female - maleParent.text = male - - personChip.apply { - text = if (persons.isNotEmpty()) { // show only the first person if it exists - if (persons.size == 1 ) persons.first().name else "" - } else "" - setChipIconResource( - if (persons.size > 1) R.drawable.ic_person_multiple - else R.drawable.ic_person - ) - visibility = if (persons.isNotEmpty()) View.VISIBLE else View.GONE - setOnClickListener { - crossController.onPersonChipClicked(persons, (crossCount.text as String).toInt()) - } - } - - dateChip.apply { // show only the first date if it exists - text = if (dates.isNotEmpty()) { - if (dates.size == 1) DateUtil().getFormattedDate(dates.first().date) else "" - } else "" - setChipIconResource( - if (dates.size > 1) R.drawable.ic_calendar_multiple - else R.drawable.ic_calendar - ) - visibility = if (dates.isNotEmpty()) View.VISIBLE else View.GONE - setOnClickListener { - crossController.onDateChipClicked(dates) - } - } - } - - when (this) { - is CrossTrackerFragment.UnplannedCrossData -> hideProgressBar(viewHolder) - is PlannedCrossData -> setWishlistProgress(viewHolder, this) - } - } - } - - private fun hideProgressBar(viewHolder: ViewHolder) { - viewHolder.apply { - progressSection.visibility = View.GONE - } - clearWishChipsFromFlow(viewHolder) - updateWishChips(viewHolder, null) - } - - private fun setWishlistProgress(viewHolder: ViewHolder, planned: PlannedCrossData) { - val ctx = viewHolder.itemView.context - val wishes = planned.wishes - - clearWishChipsFromFlow(viewHolder) - - if (wishes.isEmpty()) { - hideProgressBar(viewHolder) - updateWishChips(viewHolder, null) - return - } - - viewHolder.itemView.findViewById(R.id.chips_flow) - val constraintLayout = viewHolder.itemView.findViewById(R.id.root_constraint_layout) - - val chipIds = mutableListOf() - - // add wish chips - wishes.forEachIndexed { index, wish -> - val chipId = View.generateViewId() - val chip = Chip(ctx).apply { - id = chipId - text = "${wish.progress}/${wish.min}" - setChipIconResource(getWishTypeIcon(wish.wishType, ctx)) - isClickable = true - isCheckable = false - layoutParams = ViewGroup.LayoutParams( - ViewGroup.LayoutParams.WRAP_CONTENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - setOnClickListener { crossController.onWishlistProgressChipClicked(wish) } - } - - constraintLayout.addView(chip) - chipIds.add(chipId) - } - - updateWishChips(viewHolder, chipIds) - - // find WishlistItem with highest progress in terms of % - val highestProgressWish = wishes.maxBy { it.progress.toDouble() / it.min.toDouble() } - val bestPercent = ((highestProgressWish.progress.toDouble() / highestProgressWish.min.toDouble()) * 100).toInt().coerceAtMost(100) - - val color = WishProgressColorUtil().getProgressColor(ctx, highestProgressWish.progress, highestProgressWish.min, highestProgressWish.max) - - viewHolder.apply { - progressSection.visibility = View.VISIBLE - - // set progress icon if ANY wish meets min - val anyComplete = planned.wishes.any { it.progress >= it.min } - progressStatusIcon.setImageDrawable( - ContextCompat.getDrawable( - ctx, - if (anyComplete) R.drawable.ic_wishes_complete else R.drawable.ic_wishes_incomplete - ) - ) - - progressBar.apply { - max = 100 - progress = bestPercent - setIndicatorColor(color) - visibility = View.VISIBLE - } - } - } - - private fun updateWishChips(viewHolder: ViewHolder, wishChipIds: List?) { - val flowLayout = viewHolder.itemView.findViewById(R.id.chips_flow) - - val allIds = mutableListOf() - - if (viewHolder.personChip.isVisible) allIds.add(R.id.person_chip) - - if (viewHolder.dateChip.isVisible) allIds.add(R.id.date_chip) - - wishChipIds?.let { allIds.addAll(wishChipIds) } - - flowLayout.setReferencedIds(allIds.toIntArray()) - } - - private fun clearWishChipsFromFlow(viewHolder: ViewHolder) { - val constraintLayout = viewHolder.itemView.findViewById(R.id.root_constraint_layout) - - // remove any previously added wish chips - val childrenToRemove = mutableListOf() - for (i in 0 until constraintLayout.childCount) { - val child = constraintLayout.getChildAt(i) - if (child is Chip && child.id != R.id.person_chip && child.id != R.id.date_chip) { - childrenToRemove.add(child) - } - } - - childrenToRemove.forEach { constraintLayout.removeView(it) } - } - - private fun getWishTypeIcon(wishType: String, context: Context): Int { - return when (wishType) { - context.getString(R.string.metadata_fruits) -> R.drawable.ic_fruit - context.getString(R.string.metadata_seeds) -> R.drawable.ic_seed - context.getString(R.string.metadata_flowers) -> R.drawable.ic_flower - context.getString(R.string.literal_cross) -> R.drawable.ic_cross - else -> R.drawable.ic_sprout - } - } - - class DiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: ListEntry, newItem: ListEntry) = - oldItem.male == newItem.male && oldItem.female == newItem.female && oldItem.getType() == newItem.getType() - - override fun areContentsTheSame(oldItem: ListEntry, newItem: ListEntry): Boolean { - if (oldItem.count != newItem.count) return false - if (oldItem.persons != newItem.persons) return false - if (oldItem.dates != newItem.dates) return false - if (oldItem is PlannedCrossData && newItem is PlannedCrossData) { - if (oldItem.wishes.sortedBy { it.wishType } != newItem.wishes.sortedBy { it.wishType }) return false - } - return true - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/EventsAdapter.kt b/app/src/main/java/org/phenoapps/intercross/adapters/EventsAdapter.kt deleted file mode 100644 index 762fe5d3..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/EventsAdapter.kt +++ /dev/null @@ -1,85 +0,0 @@ -package org.phenoapps.intercross.adapters - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.Observer -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import org.phenoapps.intercross.R -import org.phenoapps.intercross.data.models.Event -import org.phenoapps.intercross.data.viewmodels.EventListViewModel -import org.phenoapps.intercross.databinding.ListItemEventsBinding -import org.phenoapps.intercross.interfaces.EventClickListener -import org.phenoapps.intercross.util.DateUtil - - -class EventsAdapter( - private val owner: LifecycleOwner, - private val viewModel: EventListViewModel, - private val eventClickListener: EventClickListener) : ListAdapter(Event.Companion.DiffCallback()) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - - return EventViewHolder(DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.list_item_events, parent, false)) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - - getItem(position).let { event -> - - with(holder as EventViewHolder) { - - viewModel.parents.observe(owner, Observer { data -> - - data?.let { parents -> - - val actualParents = parents.filter { it.dad == event.maleObsUnitDbId && it.mom == event.femaleObsUnitDbId } - - if (actualParents.isNotEmpty()) { - - bind(event, actualParents[0].momReadable, actualParents[0].dadReadable, eventClickListener) - - } - } - }) - } - } - } - - class EventViewHolder(private val binding: ListItemEventsBinding - ) : RecyclerView.ViewHolder(binding.root) { - - fun bind(event: Event, mom: String, dad: String, eventClickListener: EventClickListener) { - - with(binding) { - - event.id?.let { key -> - - this.event = event - - this.male = dad - - this.female = mom - - if ("_" in event.timestamp) { - - this.timestamp = DateUtil().getEntireTimestamp(event.timestamp) - - } else this.timestamp = event.timestamp - - clickListener = View.OnClickListener { - - it?.let { - eventClickListener.onEventClick(key) - } - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/FileAdapter.kt b/app/src/main/java/org/phenoapps/intercross/adapters/FileAdapter.kt deleted file mode 100644 index d3a63c2d..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/FileAdapter.kt +++ /dev/null @@ -1,53 +0,0 @@ -package org.phenoapps.intercross.adapters - -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.TextView -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import org.phenoapps.intercross.R -import org.phenoapps.intercross.dialogs.FileExploreDialogFragment.Item - -class FileAdapter(private val onItemClick: (Item) -> Unit) : - ListAdapter(DiffCallback()) { - - inner class ViewHolder(view: View) : RecyclerView.ViewHolder(view) { - val textView: TextView = view.findViewById(android.R.id.text1) - } - - // Create new views (invoked by the layout manager) - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - // Create a new view, which defines the UI of the list item - val view = LayoutInflater.from(parent.context) - .inflate(R.layout.custom_dialog_item_select, parent, false) - return ViewHolder(view) - } - - // Replace the contents of a view (invoked by the layout manager) - override fun onBindViewHolder(viewHolder: ViewHolder, position: Int) { - val item = getItem(position) - viewHolder.textView.apply { - text = item.file - setCompoundDrawablesWithIntrinsicBounds(item.icon, 0, 0, 0) - - // add margin between image and text (support various screen - // densities) - val dp5 = (5 * resources.displayMetrics.density + 0.5f).toInt() - compoundDrawablePadding = dp5 - } - - viewHolder.itemView.setOnClickListener { onItemClick(item) } - } - - private class DiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: Item, newItem: Item) = oldItem == newItem - - override fun areContentsTheSame(oldItem: Item, newItem: Item) = - oldItem.file == newItem.file && - oldItem.isDir == newItem.isDir && - oldItem.icon == newItem.icon - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/MetadataAdapter.kt b/app/src/main/java/org/phenoapps/intercross/adapters/MetadataAdapter.kt deleted file mode 100644 index b30cd1bf..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/MetadataAdapter.kt +++ /dev/null @@ -1,106 +0,0 @@ -package org.phenoapps.intercross.adapters - -import android.view.LayoutInflater -import android.view.ViewGroup -import android.view.inputmethod.InputMethodManager -import androidx.core.widget.addTextChangedListener -import androidx.databinding.DataBindingUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import org.phenoapps.intercross.R -import org.phenoapps.intercross.adapters.models.MetadataModel -import org.phenoapps.intercross.databinding.ListItemMetadataBinding -import org.phenoapps.intercross.interfaces.MetadataManager - - -class MetadataAdapter(val imm: InputMethodManager?, val listener: MetadataManager) : - ListAdapter(MetadataModel.Companion.DiffCallback()) { - - private var mFirstLoad = true - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder { - - return MetadataViewHolder(DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.list_item_metadata, parent, false)) - } - - override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) { - - getItem(position).let { meta -> - - with(holder as MetadataViewHolder) { - - bind(meta) - - } - } - } - - inner class MetadataViewHolder(private val binding: ListItemMetadataBinding) : RecyclerView.ViewHolder(binding.root) { - - fun bind(meta: MetadataModel) { - - with(binding) { - fun validateInputForMaxInt(newInput: String, previousValue: String): String { - if (newInput.isNotEmpty()) { - val longValue = newInput.toLongOrNull() - - if (longValue != null && longValue > Int.MAX_VALUE) { // retain old value - listItemMetadataEditText.setText(previousValue) - listItemMetadataEditText.setSelection(previousValue.length) - listItemMetadataTextLayout.error = listItemMetadataTextLayout.context.getString( - R.string.error_value_exceeds_max, - Int.MAX_VALUE - ) - return previousValue - } else { // valid input - listItemMetadataTextLayout.error = null - return newInput - } - } else { // input is empty = valid - listItemMetadataTextLayout.error = null - return "" - } - } - - listItemMetadataEditText.setText(meta.value) - - if (mFirstLoad) { - listItemMetadataEditText.requestFocus() - imm?.toggleSoftInput(InputMethodManager.SHOW_FORCED, InputMethodManager.HIDE_IMPLICIT_ONLY) - mFirstLoad = false - } - - listItemMetadataTextLayout.hint = meta.property - - var previousValidValue = meta.value - - listItemMetadataEditText.addTextChangedListener { - - val currentInput = listItemMetadataEditText.text.toString() - - // update the variable to hold validated input - previousValidValue = validateInputForMaxInt(currentInput, previousValidValue) - - val intValue = previousValidValue.toIntOrNull() - - listener.onMetadataUpdated(meta.property, intValue) - - } - - listItemMetadataEditText.setOnFocusChangeListener { _, hasFocus -> // clear any errors when focus shifts - if (!hasFocus) { - listItemMetadataTextLayout.error = null - } - } - - value = if (meta.value == "null") "" else meta.value - - property = meta.property - - hint = meta.property - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/ParentsAdapter.kt b/app/src/main/java/org/phenoapps/intercross/adapters/ParentsAdapter.kt deleted file mode 100644 index d070ed8a..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/ParentsAdapter.kt +++ /dev/null @@ -1,88 +0,0 @@ -package org.phenoapps.intercross.adapters - -import android.graphics.Color -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import org.phenoapps.intercross.R -import org.phenoapps.intercross.data.models.BaseParent -import org.phenoapps.intercross.data.models.Parent -import org.phenoapps.intercross.data.models.PollenGroup -import org.phenoapps.intercross.data.viewmodels.ParentsListViewModel -import org.phenoapps.intercross.data.viewmodels.PollenGroupListViewModel -import org.phenoapps.intercross.databinding.ListItemSelectableParentRowBinding - -class ParentsAdapter(private val listModel: ParentsListViewModel, - private val groupModel: PollenGroupListViewModel) - : ListAdapter(BaseParent.Companion.DiffCallback()) { - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - - return ViewHolder( - - DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.list_item_selectable_parent_row, parent, false - ) - ) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - - getItem(position).let { item -> - - with(holder) { - - bind(item) - } - } - } - - inner class ViewHolder(private val binding: ListItemSelectableParentRowBinding) - : RecyclerView.ViewHolder(binding.root) { - - fun bind(p: BaseParent) { - - with(binding) { - - if (p is Parent) { - - name = p.name - - checked = p.selected - - } else if (p is PollenGroup) { - - this.textField.setBackgroundColor(Color.parseColor("#42FF5722")) - - name = p.name - - checked = p.selected - } - - linearLayout.setOnClickListener { - - doneCheckBox.isChecked=!doneCheckBox.isChecked - - if (p is Parent) { - - listModel.update(p.apply { - - selected = doneCheckBox.isChecked - - }) - } else if (p is PollenGroup) { - - groupModel.update(p.apply { - - selected = doneCheckBox.isChecked - - }) - } - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/SimpleListAdapter.kt b/app/src/main/java/org/phenoapps/intercross/adapters/SimpleListAdapter.kt deleted file mode 100644 index 029f759a..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/SimpleListAdapter.kt +++ /dev/null @@ -1,84 +0,0 @@ -package org.phenoapps.intercross.adapters - -import android.graphics.Color -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import org.phenoapps.intercross.R -import org.phenoapps.intercross.databinding.SimpleListItemBinding -import org.phenoapps.intercross.interfaces.OnSimpleItemClicked - -/** - * Simple class to use with recycler views to submit strings - * Data supplied should be a list of pairs where the first item is a Long id and the second item is a string value - * s.a listOf(1L to "Chaney", 2L to "Trevor") - * - * Uses an onclick interface to return the pair that was clicked - */ -class SimpleListAdapter(private val listener: OnSimpleItemClicked) - : ListAdapter, SimpleListAdapter.ViewHolder>( - - //diffutil is simply string equality of the row id or the content value - object : DiffUtil.ItemCallback>() { - override fun areItemsTheSame(old: Pair, new: Pair) = old.first == new.first - override fun areContentsTheSame(old: Pair, new: Pair) = old.second == new.second - }) { - - private var mSelectedPosition = -1 - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - - return ViewHolder( - - DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.simple_list_item, parent, false - ) - ) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - - getItem(position).let { item -> - - with(holder) { - - if (mSelectedPosition == position) itemView.setBackgroundColor(Color.parseColor("#6000ee00")) - else itemView.setBackgroundResource(R.drawable.text_cell) - - holder.itemView.setOnLongClickListener { - - listener.onItemLongClicked(item) - - true - } - - holder.itemView.setOnClickListener { - - val old = mSelectedPosition - - mSelectedPosition = position - - notifyItemChanged(position) - notifyItemChanged(old) - - this@SimpleListAdapter.listener.onItemClicked(item) - } - - bind(item) - } - } - } - - inner class ViewHolder(private val binding: SimpleListItemBinding) - : RecyclerView.ViewHolder(binding.root) { - - fun bind(pair: Pair) { - - binding.name = pair.second - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/SummaryAdapter.kt b/app/src/main/java/org/phenoapps/intercross/adapters/SummaryAdapter.kt deleted file mode 100644 index 6c7ad60e..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/SummaryAdapter.kt +++ /dev/null @@ -1,68 +0,0 @@ -package org.phenoapps.intercross.adapters - -import android.view.LayoutInflater -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import org.phenoapps.intercross.R -import org.phenoapps.intercross.databinding.ListItemSummaryBinding -import org.phenoapps.intercross.fragments.SummaryFragment - -class SummaryAdapter: - ListAdapter(SummaryDiffCallback()) { - - private class SummaryDiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: SummaryFragment.ListEntry, - newItem: SummaryFragment.ListEntry): Boolean { - - return (oldItem.label == newItem.label) - } - - override fun areContentsTheSame(oldItem: SummaryFragment.ListEntry, - newItem: SummaryFragment.ListEntry): Boolean { - - return (oldItem.label == newItem.label) && (oldItem.label == newItem.label) - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - - return ViewHolder( - DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.list_item_summary, parent, false - ) - ) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - - getItem(position).let { item -> - - with(holder) { - - itemView.tag = item - - bind(item) - } - } - } - - inner class ViewHolder( - private val binding: ListItemSummaryBinding) : RecyclerView.ViewHolder(binding.root) { - - fun bind(data: SummaryFragment.ListEntry) { - - with(binding) { - - labelText = data.label - - countValue = data.value.toInt() - - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/TableViewAdapter.java b/app/src/main/java/org/phenoapps/intercross/adapters/TableViewAdapter.java deleted file mode 100644 index 7ca51f8d..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/TableViewAdapter.java +++ /dev/null @@ -1,258 +0,0 @@ -package org.phenoapps.intercross.adapters; - -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ImageView; -import android.widget.LinearLayout; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import com.evrencoskun.tableview.adapter.AbstractTableAdapter; -import com.evrencoskun.tableview.adapter.recyclerview.holder.AbstractSorterViewHolder; -import com.evrencoskun.tableview.adapter.recyclerview.holder.AbstractViewHolder; - -import org.jetbrains.annotations.NotNull; -import org.phenoapps.intercross.R; -import org.phenoapps.intercross.fragments.CrossTrackerFragment; - -/** - * https://github.com/evrencoskun/TableView/wiki - */ -public class TableViewAdapter extends AbstractTableAdapter { - - static class CellViewHolder extends AbstractSorterViewHolder { - - final TextView textView; - final ImageView imageView; - final LinearLayout linearLayout; - - public CellViewHolder(View itemView) { - super(itemView); - - linearLayout = itemView.findViewById(R.id.list_item_table_cell_container); - imageView = itemView.findViewById(R.id.list_item_table_cell_iv); - textView = itemView.findViewById(R.id.list_item_table_cell_tv); - } - } - - static class RowHeaderViewHolder extends AbstractViewHolder { - - final ImageView imageView; - - public RowHeaderViewHolder(@NonNull View itemView) { - super(itemView); - - imageView = itemView.findViewById(R.id.list_item_table_checkmark_iv); - imageView.setVisibility(View.GONE); - } - } - - /** - * This is where you create your custom Cell ViewHolder. This method is called when Cell - * RecyclerView of the TableView needs a new RecyclerView.ViewHolder of the given type to - * represent an item. - * - * @param viewType : This value comes from #getCellItemViewType method to support different type - * of viewHolder as a Cell item. - * - * @see #getCellItemViewType(int); - */ - @NotNull - @Override - public AbstractViewHolder onCreateCellViewHolder(ViewGroup parent, int viewType) { - // Get cell xml layout - View layout = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.list_item_table_cell, parent, false); - // Create a Custom ViewHolder for a Cell item. - return new CellViewHolder(layout); - } - - /** - * That is where you set Cell View Model data to your custom Cell ViewHolder. This method is - * Called by Cell RecyclerView of the TableView to display the data at the specified position. - * This method gives you everything you need about a cell item. - * - * @param holder : This is one of your cell ViewHolders that was created on - * ```onCreateCellViewHolder``` method. In this example, we have created - * "MyCellViewHolder" holder. - * @param cellItemModel : This is the cell view model located on this X and Y position. In this - * example, the model class is "Cell". - * @param columnPosition : This is the X (Column) position of the cell item. - * @param rowPosition : This is the Y (Row) position of the cell item. - * - * @see #onCreateCellViewHolder(ViewGroup, int); - */ - @Override - public void onBindCellViewHolder(@NotNull AbstractViewHolder holder, CrossTrackerFragment.CellData cellItemModel, int - columnPosition, int rowPosition) { - - // Get the holder to update cell item text - CellViewHolder viewHolder = (CellViewHolder) holder; - - if (cellItemModel != null) { - viewHolder.textView.setText(cellItemModel.getText()); - if (cellItemModel.getComplete()) { - viewHolder.textView.setVisibility(View.INVISIBLE); - viewHolder.imageView.setVisibility(View.VISIBLE); - } else { - viewHolder.textView.setVisibility(View.VISIBLE); - viewHolder.imageView.setVisibility(View.INVISIBLE); - } - } - - // If your TableView should have auto resize for cells & columns. - // Then you should consider the below lines. Otherwise, you can ignore them. - - // It is necessary to remeasure itself. - viewHolder.linearLayout.getLayoutParams().width = LinearLayout.LayoutParams.WRAP_CONTENT; - //viewHolder.imageView.requestLayout(); - //viewHolder.textView.requestLayout(); - } - - /** - * This is where you create your custom Column Header ViewHolder. This method is called when - * Column Header RecyclerView of the TableView needs a new RecyclerView.ViewHolder of the given - * type to represent an item. - * - * @param viewType : This value comes from "getColumnHeaderItemViewType" method to support - * different type of viewHolder as a Column Header item. - * - * @see #getColumnHeaderItemViewType(int); - */ - @NotNull - @Override - public AbstractViewHolder onCreateColumnHeaderViewHolder(ViewGroup parent, int viewType) { - - // Get Column Header xml Layout - View layout = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.list_item_table_cell, parent, false); - - // Create a ColumnHeader ViewHolder - return new CellViewHolder(layout); - } - - /** - * That is where you set Column Header View Model data to your custom Column Header ViewHolder. - * This method is Called by ColumnHeader RecyclerView of the TableView to display the data at - * the specified position. This method gives you everything you need about a column header - * item. - * - * @param holder : This is one of your column header ViewHolders that was created on - * ```onCreateColumnHeaderViewHolder``` method. In this example we have created - * "MyColumnHeaderViewHolder" holder. - * @param columnHeaderItemModel : This is the column header view model located on this X position. In this - * example, the model class is "ColumnHeader". - * @param position : This is the X (Column) position of the column header item. - * - * @see #onCreateColumnHeaderViewHolder(ViewGroup, int) ; - */ - @Override - public void onBindColumnHeaderViewHolder(@NotNull AbstractViewHolder holder, - CrossTrackerFragment.CellData columnHeaderItemModel, - int position) { - - // Get the holder to update cell item text - CellViewHolder columnHeaderViewHolder = (CellViewHolder) holder; - - if (columnHeaderItemModel != null) { - columnHeaderViewHolder.textView.setText(columnHeaderItemModel.getText()); - } - - // If your TableView should have auto resize for cells & columns. - // Then you should consider the below lines. Otherwise, you can ignore them. - - // It is necessary to remeasure itself. - // columnHeaderViewHolder.container.getLayoutParams().width = LinearLayout.LayoutParams.WRAP_CONTENT; - columnHeaderViewHolder.textView.requestLayout(); - } - - /** - * This is where you create your custom Row Header ViewHolder. This method is called when - * Row Header RecyclerView of the TableView needs a new RecyclerView.ViewHolder of the given - * type to represent an item. - * - * @param viewType : This value comes from "getRowHeaderItemViewType" method to support - * different type of viewHolder as a row Header item. - * - * @see #getRowHeaderItemViewType(int); - */ - @NotNull - @Override - public AbstractViewHolder onCreateRowHeaderViewHolder(ViewGroup parent, int viewType) { - - // Get Row Header xml Layout - View layout = LayoutInflater.from(parent.getContext()) - .inflate(R.layout.list_item_table_checkmark, parent, false); - - // Create a Row Header ViewHolder - return new RowHeaderViewHolder(layout); - } - - - /** - * That is where you set Row Header View Model data to your custom Row Header ViewHolder. This - * method is Called by RowHeader RecyclerView of the TableView to display the data at the - * specified position. This method gives you everything you need about a row header item. - * - * @param holder : This is one of your row header ViewHolders that was created on - * ```onCreateRowHeaderViewHolder``` method. In this example, we have created - * "MyRowHeaderViewHolder" holder. - * @param rowHeaderItemModel : This is the row header view model located on this Y position. In this - * example, the model class is "RowHeader". - * @param position : This is the Y (row) position of the row header item. - * - * @see #onCreateRowHeaderViewHolder(ViewGroup, int) ; - */ - @Override - public void onBindRowHeaderViewHolder(@NotNull AbstractViewHolder holder, CrossTrackerFragment.CellData rowHeaderItemModel, int - position) { - - // Get the holder to update row header item text - RowHeaderViewHolder rowHeaderViewHolder = (RowHeaderViewHolder) holder; - - if (rowHeaderItemModel != null) { - if (rowHeaderItemModel.getComplete()) { - rowHeaderViewHolder.itemView.setVisibility(View.VISIBLE); - } else { - rowHeaderViewHolder.itemView.setVisibility(View.INVISIBLE); - } - } - } - - @NotNull - @Override - public View onCreateCornerView(ViewGroup parent) { - // Get Corner xml layout - return LayoutInflater.from(parent.getContext()) - .inflate(R.layout.list_item_corner, parent, false); - } - - @Override - public int getColumnHeaderItemViewType(int columnPosition) { - // The unique ID for this type of column header item - // If you have different items for Cell View by X (Column) position, - // then you should fill this method to be able create different - // type of ColumnViewHolder on "onCreateColumnViewHolder" - return 0; - } - - @Override - public int getRowHeaderItemViewType(int rowPosition) { - // The unique ID for this type of row header item - // If you have different items for Row Header View by Y (Row) position, - // then you should fill this method to be able create different - // type of RowHeaderViewHolder on "onCreateRowHeaderViewHolder" - return 0; - } - - @Override - public int getCellItemViewType(int columnPosition) { - // The unique ID for this type of cell item - // If you have different items for Cell View by X (Column) position, - // then you should fill this method to be able create different - // type of CellViewHolder on "onCreateCellViewHolder" - return 0; - } - } \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/WishlistAdapter.kt b/app/src/main/java/org/phenoapps/intercross/adapters/WishlistAdapter.kt deleted file mode 100644 index 098b274c..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/WishlistAdapter.kt +++ /dev/null @@ -1,103 +0,0 @@ -package org.phenoapps.intercross.adapters - -import android.content.Context -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import androidx.databinding.DataBindingUtil -import androidx.recyclerview.widget.DiffUtil -import androidx.recyclerview.widget.ListAdapter -import androidx.recyclerview.widget.RecyclerView -import org.phenoapps.intercross.R -import org.phenoapps.intercross.databinding.ListItemWishlistRowBinding -import org.phenoapps.intercross.fragments.CrossTrackerFragment - - -/** - * This adapter class handles CrossData and WishlistFragment.ListEntry data - * CrossData holds information on two previously crossed samples, and their crosses. - * ListEntry holds wishlist specific data s.a wishMax, current. - * - * CrossData's are not rendered with checkboxes. - * - */ -class WishlistAdapter(val context: Context) : - ListAdapter(SummaryDiffCallback()) { - - private class SummaryDiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: CrossTrackerFragment.ListEntry, - newItem: CrossTrackerFragment.ListEntry): Boolean { - - return (oldItem.female == newItem.female) && (oldItem.male == newItem.male) - } - - override fun areContentsTheSame(oldItem: CrossTrackerFragment.ListEntry, - newItem: CrossTrackerFragment.ListEntry): Boolean { - - return (oldItem.female == newItem.female) && (oldItem.male == newItem.male) - } - } - - private lateinit var mParent: ViewGroup - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - - mParent = parent - - return ViewHolder( - DataBindingUtil.inflate( - LayoutInflater.from(parent.context), - R.layout.list_item_wishlist_row, parent, false - ) - ) - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - - getItem(position).let { wishitem -> - - with(holder) { - - itemView.tag = wishitem - - bind(wishitem) - } - } - } - - inner class ViewHolder( - private val binding: ListItemWishlistRowBinding) : RecyclerView.ViewHolder(binding.root) { - - fun bind(data: CrossTrackerFragment.ListEntry) { - - with(binding) { - - binding.numCrosses.isSelected = true - - maleParent = data.male - - femaleParent = data.female - - count = data.count - - if (data.count.contains("/")) { - - val tokens = data.count.split("/") - - if (tokens[0].toInt() >= tokens[1].toInt()) { - - completed = true - } - } - - //uses inner view holder to create list of crosses used for that wish item - onClick = View.OnClickListener { - - - - } - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/adapters/models/MetadataModel.kt b/app/src/main/java/org/phenoapps/intercross/adapters/models/MetadataModel.kt deleted file mode 100644 index 6d2809f5..00000000 --- a/app/src/main/java/org/phenoapps/intercross/adapters/models/MetadataModel.kt +++ /dev/null @@ -1,19 +0,0 @@ -package org.phenoapps.intercross.adapters.models - -import androidx.recyclerview.widget.DiffUtil - -data class MetadataModel(val property: String, val value: String) { - companion object { - - class DiffCallback : DiffUtil.ItemCallback() { - - override fun areItemsTheSame(oldItem: MetadataModel, newItem: MetadataModel): Boolean { - return oldItem.property== newItem.property - } - - override fun areContentsTheSame(oldItem: MetadataModel, newItem: MetadataModel): Boolean { - return oldItem.property == newItem.property && oldItem.value == newItem.value - } - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/application/ActivityModule.kt b/app/src/main/java/org/phenoapps/intercross/application/ActivityModule.kt index 7f06aa25..63759016 100644 --- a/app/src/main/java/org/phenoapps/intercross/application/ActivityModule.kt +++ b/app/src/main/java/org/phenoapps/intercross/application/ActivityModule.kt @@ -1,4 +1,4 @@ -package org.phenoapps.intercross.application; +package org.phenoapps.intercross.application import android.content.Context import android.content.SharedPreferences @@ -8,6 +8,10 @@ import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.android.qualifiers.ApplicationContext import dagger.hilt.components.SingletonComponent +import javax.inject.Singleton +import org.phenoapps.brapi.account.BrapiAccountRepository +import org.phenoapps.brapi.account.BrapiPreferenceKeys +import org.phenoapps.intercross.util.KeyUtil @Module @InstallIn(SingletonComponent::class) @@ -16,4 +20,29 @@ object ActivityModule { @Provides fun providesPreferences(@ApplicationContext context: Context): SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context) -} \ No newline at end of file + + @Provides + @Singleton + fun providesBrapiPreferenceKeys(@ApplicationContext context: Context): BrapiPreferenceKeys { + val keyUtil = KeyUtil(context) + return BrapiPreferenceKeys( + enabled = keyUtil.brapiEnabled, + baseUrl = keyUtil.brapiUrl, + displayName = keyUtil.brapiDisplayName, + accessToken = keyUtil.brapiToken, + idToken = keyUtil.brapiId, + oidcUrl = keyUtil.brapiOidcUrl, + oidcFlow = keyUtil.brapiOidcFlow, + oidcClientId = keyUtil.brapiOidcClientId, + oidcScope = keyUtil.brapiOidcScope, + ) + } + + @Provides + @Singleton + fun providesBrapiAccountRepository( + @ApplicationContext context: Context, + prefs: SharedPreferences, + keys: BrapiPreferenceKeys, + ): BrapiAccountRepository = BrapiAccountRepository(context, prefs, keys) +} diff --git a/app/src/main/java/org/phenoapps/intercross/application/Intercross.java b/app/src/main/java/org/phenoapps/intercross/application/Intercross.java deleted file mode 100644 index 6b9ddf73..00000000 --- a/app/src/main/java/org/phenoapps/intercross/application/Intercross.java +++ /dev/null @@ -1,18 +0,0 @@ -package org.phenoapps.intercross.application; - -import androidx.multidex.MultiDexApplication; - -import org.phenoapps.intercross.BuildConfig; - -import dagger.hilt.android.HiltAndroidApp; - -@HiltAndroidApp -public class Intercross extends MultiDexApplication { - - public Intercross() { - if (BuildConfig.DEBUG) { - //StrictMode.enableDefaults(); - //un-comment to enable strict warnings in logcat - } - } -} diff --git a/app/src/main/java/org/phenoapps/intercross/application/Intercross.kt b/app/src/main/java/org/phenoapps/intercross/application/Intercross.kt new file mode 100644 index 00000000..63b20f7b --- /dev/null +++ b/app/src/main/java/org/phenoapps/intercross/application/Intercross.kt @@ -0,0 +1,20 @@ +package org.phenoapps.intercross.application + +import android.app.Application +import dagger.hilt.android.HiltAndroidApp +import org.phenoapps.brapi.account.BrapiAccountRepository +import javax.inject.Inject + +@HiltAndroidApp +class Intercross : Application() { + + @Inject + lateinit var brapiAccountRepository: BrapiAccountRepository + + override fun onCreate() { + super.onCreate() + // Run one-time migration from SharedPreferences to AccountManager. + // Must run before any BrAPI UI or auth flow is accessed. + brapiAccountRepository.migrateFromPrefsIfNeeded() + } +} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/brapi/BrapiAuthDialog.java b/app/src/main/java/org/phenoapps/intercross/brapi/BrapiAuthDialog.java index 8121d6b7..679e2dd9 100644 --- a/app/src/main/java/org/phenoapps/intercross/brapi/BrapiAuthDialog.java +++ b/app/src/main/java/org/phenoapps/intercross/brapi/BrapiAuthDialog.java @@ -17,8 +17,8 @@ public class BrapiAuthDialog extends Dialog implements View.OnClickListener { private SharedPreferences preferences; - private String target; - private Context context; + private final String target; + private final Context context; public BrapiAuthDialog(@NonNull Context context, String target) { super(context); @@ -45,23 +45,17 @@ protected void onCreate(Bundle savedInstanceState) { @Override public void onClick(View view) { - //TODO -// switch (view.getId()) { -// case R.id.brapi_auth_cancel_btn: -// // Cancel -// dismiss(); -// break; -// -// case R.id.brapi_auth_btn: -// -// // Start our brapi authentication process. -// dismiss(); -// // Show our error message if it exists -// BrapiControllerResponse brapiControllerResponse = BrAPIService.authorizeBrAPI(preferences, context, target); -// processBrapiControllerMessage(brapiControllerResponse); -// break; -// -// } + int id = view.getId(); + if (id == R.id.brapi_auth_cancel_btn) { + // Cancel + dismiss(); + } else if (id == R.id.brapi_auth_btn) { + // Start our brapi authentication process. + dismiss(); + // Show our error message if it exists + BrapiControllerResponse brapiControllerResponse = BrAPIService.authorizeBrAPI(preferences, context, target); + processBrapiControllerMessage(brapiControllerResponse); + } } private void processBrapiControllerMessage(BrapiControllerResponse brapiControllerResponse) { diff --git a/app/src/main/java/org/phenoapps/intercross/brapi/BrapiInfoDialog.java b/app/src/main/java/org/phenoapps/intercross/brapi/BrapiInfoDialog.java deleted file mode 100644 index 0b168673..00000000 --- a/app/src/main/java/org/phenoapps/intercross/brapi/BrapiInfoDialog.java +++ /dev/null @@ -1,43 +0,0 @@ -package org.phenoapps.intercross.brapi; - -import android.app.Dialog; -import android.content.Context; -import android.os.Bundle; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; - -import androidx.annotation.NonNull; - -import org.phenoapps.intercross.R; - - -public class BrapiInfoDialog extends Dialog implements View.OnClickListener { - - private String bodyMessage; - - public BrapiInfoDialog(@NonNull Context context, String bodyMessage) { - super(context); - this.bodyMessage = bodyMessage; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - - setContentView(R.layout.dialog_info); - Button closeBtn = findViewById(R.id.closeBtn); - TextView bodyTextView = findViewById(R.id.body_message); - - // Set our body text - bodyTextView.setText(bodyMessage); - closeBtn.setOnClickListener(this); - } - - @Override - public void onClick(View view) { - if (view.getId() == R.id.closeBtn) { - dismiss(); - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/brapi/BrapiLoadDialog.java b/app/src/main/java/org/phenoapps/intercross/brapi/BrapiLoadDialog.java deleted file mode 100644 index 49439a9f..00000000 --- a/app/src/main/java/org/phenoapps/intercross/brapi/BrapiLoadDialog.java +++ /dev/null @@ -1,296 +0,0 @@ -package org.phenoapps.intercross.brapi; - -import android.app.Activity; -import android.app.Dialog; -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; -import android.os.Bundle; -import android.os.Handler; -import android.text.Html; -import android.util.Log; -import android.view.View; -import android.view.Window; -import android.widget.Button; -import android.widget.TextView; -import android.widget.Toast; - -import androidx.annotation.NonNull; -import androidx.arch.core.util.Function; - -import org.phenoapps.intercross.R; -import org.phenoapps.intercross.brapi.model.BrapiStudyDetails; -import org.phenoapps.intercross.brapi.service.BrAPIService; -import org.phenoapps.intercross.brapi.service.BrAPIServiceV2; - -public class BrapiLoadDialog extends Dialog implements View.OnClickListener { - - private Button saveBtn; - private BrapiStudyDetails study; - private BrapiStudyDetails studyDetails; - private BrAPIService brAPIService; - private Context context; - private Boolean studyLoadStatus = false; - private Boolean plotLoadStatus = false; - private Boolean traitLoadStatus = false; - // Creates a new thread to do importing - private Runnable importRunnable = new Runnable() { - public void run() { - new ImportRunnableTask().execute(0); - } - }; - - public BrapiLoadDialog(@NonNull Context context) { - super(context); - this.context = context; - } - - public void setSelectedStudy(BrapiStudyDetails selectedStudy) { - this.study = selectedStudy; - } - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - this.setCanceledOnTouchOutside(false); - requestWindowFeature(Window.FEATURE_NO_TITLE); - setContentView(R.layout.dialog_brapi_import); - - brAPIService = new BrAPIServiceV2(context); - saveBtn = findViewById(R.id.brapi_save_btn); - saveBtn.setOnClickListener(this); - Button cancelBtn = findViewById(R.id.brapi_cancel_btn); - cancelBtn.setOnClickListener(this); - studyDetails = new BrapiStudyDetails(); - - // Set our OK button to be disabled until we are finished loading - saveBtn.setVisibility(View.GONE); - buildStudyDetails(); - loadStudy(); - } - - private void buildStudyDetails() { - findViewById(R.id.loadingPanel).setVisibility(View.VISIBLE); -// brAPIService.getStudyDetails(study.getStudyDbId(), new Function() { -// @Override -// public Void apply(final BrapiStudyDetails study) { -// -// ((Activity) context).runOnUiThread(new Runnable() { -// @Override -// public void run() { -// BrapiStudyDetails.merge(studyDetails, study); -// loadStudy(); -// // Check if user should save yet -// studyLoadStatus = true; -// if (checkAllLoadsFinished()) { -// findViewById(R.id.loadingPanel).setVisibility(View.GONE); -// saveBtn.setVisibility(View.VISIBLE); -// resetLoadStatus(); -// } -// } -// }); -// return null; -// } -// }, new Function() { -// -// @Override -// public Void apply(final Integer code) { -// ((Activity) context).runOnUiThread(new Runnable() { -// @Override -// public void run() { -// findViewById(R.id.loadingPanel).setVisibility(View.GONE); -// Toast.makeText(context, context.getString(R.string.brapi_study_detail_error), Toast.LENGTH_LONG).show(); -// } -// }); -// return null; -// } -// }); -// -// brAPIService.getPlotDetails(study.getStudyDbId(), new Function() { -// @Override -// public Void apply(final BrapiStudyDetails study) { -// -// ((Activity) context).runOnUiThread(new Runnable() { -// @Override -// public void run() { -// BrapiStudyDetails.merge(studyDetails, study); -// loadStudy(); -// // Check if user should save yet -// plotLoadStatus = true; -// if (checkAllLoadsFinished()) { -// findViewById(R.id.loadingPanel).setVisibility(View.GONE); -// saveBtn.setVisibility(View.VISIBLE); -// resetLoadStatus(); -// } -// } -// }); -// return null; -// } -// -// }, new Function() { -// -// @Override -// public Void apply(final Integer code) { -// ((Activity) context).runOnUiThread(new Runnable() { -// @Override -// public void run() { -// findViewById(R.id.loadingPanel).setVisibility(View.GONE); -// Toast.makeText(context, context.getString(R.string.brapi_plot_detail_error), Toast.LENGTH_LONG).show(); -// } -// }); -// return null; -// } -// }); - - -// brAPIService.getTraits(study.getStudyDbId(), new Function() { -// @Override -// public Void apply(final BrapiStudyDetails study) { -// -// ((Activity) context).runOnUiThread(new Runnable() { -// @Override -// public void run() { -// BrapiStudyDetails.merge(studyDetails, study); -// loadStudy(); -// // Check if user should save yet -// traitLoadStatus = true; -// if (checkAllLoadsFinished()) { -// findViewById(R.id.loadingPanel).setVisibility(View.GONE); -// saveBtn.setVisibility(View.VISIBLE); -// resetLoadStatus(); -// } -// } -// }); -// return null; -// } -// }, new Function() { -// -// @Override -// public Void apply(final Integer code) { -// ((Activity) context).runOnUiThread(new Runnable() { -// @Override -// public void run() { -// findViewById(R.id.loadingPanel).setVisibility(View.GONE); -// Toast.makeText(context, context.getString(R.string.brapi_study_traits_error), Toast.LENGTH_LONG).show(); -// } -// }); -// return null; -// } -// }); - } - - private void loadStudy() { -// if (this.studyDetails.getStudyName() != null) -// ((TextView) findViewById(R.id.studyNameValue)).setText(this.studyDetails.getStudyName()); -// if (this.studyDetails.getStudyDescription() != null) -// ((TextView) findViewById(R.id.studyDescriptionValue)).setText(this.studyDetails.getStudyDescription()); -// if (this.studyDetails.getStudyLocation() != null) -// ((TextView) findViewById(R.id.studyLocationValue)).setText(this.studyDetails.getStudyLocation()); -// if (this.studyDetails.getNumberOfPlots() != null) -// ((TextView) findViewById(R.id.studyNumPlotsValue)).setText(this.studyDetails.getNumberOfPlots().toString()); -// if (this.studyDetails.getTraits() != null) -// ((TextView) findViewById(R.id.studyNumTraitsValue)).setText(String.valueOf(this.studyDetails.getTraits().size())); - } - - private Boolean checkAllLoadsFinished() { - return studyLoadStatus && plotLoadStatus && traitLoadStatus; - } - - private void resetLoadStatus() { - studyLoadStatus = false; - traitLoadStatus = false; - plotLoadStatus = false; - } - - @Override - public void onClick(View v) { - //TODO -// switch (v.getId()) { -// case R.id.brapi_save_btn: -// saveStudy(); -// break; -// case R.id.brapi_cancel_btn: -// dismiss(); -// break; -// default: -// break; -// } -// dismiss(); - } - - private void saveStudy() { - - // Dismiss this dialog - dismiss(); - - // Run saving task in the background so we can showing progress dialog - Handler mHandler = new Handler(); - mHandler.post(importRunnable); - - } - - // Mimics the class used in the csv field importer to run the saving - // task in a different thread from the UI thread so the app doesn't freeze up. - private class ImportRunnableTask extends AsyncTask { - - ProgressDialog dialog; - - BrapiControllerResponse brapiControllerResponse; - boolean fail; - - @Override - protected void onPreExecute() { - super.onPreExecute(); - dialog = new ProgressDialog(context); - dialog.setIndeterminate(true); - dialog.setCancelable(false); - dialog.setMessage(Html.fromHtml(context.getResources().getString(R.string.import_dialog_importing))); - dialog.show(); - } - - @Override - protected Integer doInBackground(Integer... params) { - try { - - //brapiControllerResponse = brAPIService.saveStudyDetails(studyDetails); - - } catch (Exception e) { - e.printStackTrace(); - fail = true; - } - - return 0; - } - - @Override - protected void onPostExecute(Integer result) { - if (dialog.isShowing()) - dialog.dismiss(); - - // Finish our BrAPI import activity - //removed this statement for cases when multiple studies need to be imported at once - //((Activity) context).finish(); - - // Display our message. - if (!brapiControllerResponse.status) { - if (brapiControllerResponse.message == BrAPIService.notUniqueFieldMessage) { - //Toast.makeText(context, R.string.fields_study_exists_message, Toast.LENGTH_LONG).show(); - } else if (brapiControllerResponse.message == BrAPIService.notUniqueIdMessage) { - Toast.makeText(context, R.string.import_error_unique, Toast.LENGTH_LONG).show(); - } else { - Log.e("error-ope", brapiControllerResponse.message); - Toast.makeText(context, R.string.brapi_save_field_error, Toast.LENGTH_LONG).show(); - } - } - - // This is an unhandled failed that we should not run into unless there is - // an error in the saveStudyDetails code outside of that handling. - if (fail) { - Log.e("error-opef", brapiControllerResponse.message); - Toast.makeText(context, R.string.brapi_save_field_error, Toast.LENGTH_LONG).show(); - } - - } - } - -} diff --git a/app/src/main/java/org/phenoapps/intercross/brapi/model/BrapiObservation.java b/app/src/main/java/org/phenoapps/intercross/brapi/model/BrapiObservation.java deleted file mode 100644 index 519d52e3..00000000 --- a/app/src/main/java/org/phenoapps/intercross/brapi/model/BrapiObservation.java +++ /dev/null @@ -1,145 +0,0 @@ -package org.phenoapps.intercross.brapi.model; - -import org.threeten.bp.OffsetDateTime; -import org.threeten.bp.format.DateTimeFormatter; -import org.threeten.bp.format.DateTimeParseException; - -public class BrapiObservation { - - private OffsetDateTime timestamp; - private String unitDbId; - private String variableDbId; - private String dbId; - private OffsetDateTime lastSyncedTime; - private String fieldbookDbId; - private String variableName; - - public String getFieldbookDbId() { - return fieldbookDbId; - } - - public void setFieldbookDbId(String fieldbookDbId) { - this.fieldbookDbId = fieldbookDbId; - } - - public String getDbId() { - return dbId; - } - - public void setDbId(String id) { - this.dbId = id; - } - - public OffsetDateTime getTimestamp() { - return timestamp; - } - - public void setTimestamp(OffsetDateTime timestamp) { - this.timestamp = timestamp; - } - - public void setTimestamp(String timestamp) { - this.timestamp = convertTime(timestamp); - } - - private OffsetDateTime convertTime(String time) { - OffsetDateTime converted = null; - try { - //TODO: locale - DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSSZZZZZ"); - converted = OffsetDateTime.parse(time, formatter); - } catch (DateTimeParseException e) { - e.printStackTrace(); - } finally { - return converted; - } - - } - - public OffsetDateTime getLastSyncedTime() { - return lastSyncedTime; - } - - public void setLastSyncedTime(String timestamp) { - this.lastSyncedTime = convertTime(timestamp); - } - - public Status getStatus() { - - Status status = Status.INVALID; - - if (dbId == null) { - status = Status.NEW; - } else if (lastSyncedTime == null) { - status = Status.INCOMPLETE; - } else if (dbId != null && lastSyncedTime != null && timestamp != null && timestamp.compareTo(lastSyncedTime) < 0) { - status = Status.SYNCED; - } else if (dbId != null && lastSyncedTime != null && timestamp != null && timestamp.compareTo(lastSyncedTime) > 0) { - status = Status.EDITED; - } - - return status; - } - - // Objects.equals Jdk7 - boolean objectsEquals(Object a, Object b) { - return (a == b) || (a != null && a.equals(b)); - } - - // The objects methods used were added in API 19 so they'll just - // be duplicated here to work around that - - // Objects.hash Jdk7 - int objectsHash(Object... values) { - return arraysHashCode(values); - } - - // Objects.hashCode Jdk7 - private int arraysHashCode(Object[] a) { - if (a == null) - return 0; - - int result = 1; - - for (Object element : a) - result = 31 * result + (element == null ? 0 : element.hashCode()); - - return result; - } - - public String getUnitDbId() { - return unitDbId; - } - - public void setUnitDbId(String unitDbId) { - this.unitDbId = unitDbId; - } - - public String getVariableDbId() { - return variableDbId; - } - - public void setVariableDbId(String variableDbId) { - this.variableDbId = variableDbId; - } - - public String getVariableName() { - return variableName; - } - - public void setVariableName(String variableName) { - this.variableName = variableName; - } - - public String getFieldBookDbId() { - return fieldbookDbId; - } - - public void setFieldBookDbId(String fieldbookDbId) { - this.fieldbookDbId = fieldbookDbId; - } - - public enum Status { - NEW, SYNCED, EDITED, INCOMPLETE, INVALID - } -} diff --git a/app/src/main/java/org/phenoapps/intercross/brapi/model/BrapiStudyDetails.java b/app/src/main/java/org/phenoapps/intercross/brapi/model/BrapiStudyDetails.java deleted file mode 100644 index d2203fb3..00000000 --- a/app/src/main/java/org/phenoapps/intercross/brapi/model/BrapiStudyDetails.java +++ /dev/null @@ -1,108 +0,0 @@ -package org.phenoapps.intercross.brapi.model; - -import java.util.List; - -public class BrapiStudyDetails { - private String studyDbId; - private String studyName; - private String studyDescription; - private String studyLocation; - private String commonCropName; - private Integer numberOfPlots; - private List attributes; - private List> values; -// private List traits; - - public static void merge(BrapiStudyDetails sd1, BrapiStudyDetails sd2) { - if (sd2.getStudyDbId() != null) - sd1.setStudyDbId(sd2.getStudyDbId()); - if (sd2.getStudyName() != null) - sd1.setStudyName(sd2.getStudyName()); - if (sd2.getStudyDescription() != null) - sd1.setStudyDescription(sd2.getStudyDescription()); - if (sd2.getStudyLocation() != null) - sd1.setStudyLocation(sd2.getStudyLocation()); - if (sd2.getCommonCropName() != null) - sd1.setCommonCropName(sd2.getCommonCropName()); - if (sd2.getNumberOfPlots() != null) - sd1.setNumberOfPlots(sd2.getNumberOfPlots()); - if (sd2.getAttributes() != null) - sd1.setAttributes(sd2.getAttributes()); - if (sd2.getValues() != null) - sd1.setValues(sd2.getValues()); -// if (sd2.getTraits() != null) -// sd1.setTraits(sd2.getTraits()); - } - - public String getCommonCropName() { - return commonCropName; - } - - public void setCommonCropName(String commonCropName) { - this.commonCropName = commonCropName; - } - -// public List getTraits() { -// return traits; -// } -// -// public void setTraits(List traits) { -// this.traits = traits; -// } - - public String getStudyDbId() { - return studyDbId; - } - - public void setStudyDbId(String studyDbId) { - this.studyDbId = studyDbId; - } - - public String getStudyName() { - return studyName; - } - - public void setStudyName(String studyName) { - this.studyName = studyName; - } - - public String getStudyDescription() { - return studyDescription; - } - - public void setStudyDescription(String studyDescription) { - this.studyDescription = studyDescription; - } - - public String getStudyLocation() { - return studyLocation; - } - - public void setStudyLocation(String studyLocation) { - this.studyLocation = studyLocation; - } - - public Integer getNumberOfPlots() { - return numberOfPlots; - } - - public void setNumberOfPlots(Integer numberOfPlots) { - this.numberOfPlots = numberOfPlots; - } - - public List getAttributes() { - return attributes; - } - - public void setAttributes(List attributes) { - this.attributes = attributes; - } - - public List> getValues() { - return values; - } - - public void setValues(List> values) { - this.values = values; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/brapi/model/FieldBookImage.java b/app/src/main/java/org/phenoapps/intercross/brapi/model/FieldBookImage.java deleted file mode 100644 index b8c7f1e1..00000000 --- a/app/src/main/java/org/phenoapps/intercross/brapi/model/FieldBookImage.java +++ /dev/null @@ -1,218 +0,0 @@ -package org.phenoapps.intercross.brapi.model; - -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; - -import com.google.gson.JsonArray; -import com.google.gson.JsonObject; - -import java.io.ByteArrayOutputStream; -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -public class FieldBookImage extends BrapiObservation { - - private File file; - private int width; - private int height; - private long fileSize; - private String fileName; - private String imageName; - private String mimeType; - private Bitmap missing; -// private GeoJSON location; - private Map additionalInfo; - private byte[] bytes; - - private List descriptiveOntologyTerms; - private String description; - - public FieldBookImage() { - - } - - public FieldBookImage(String filePath, Bitmap missingPhoto) { - - this.file = new File(filePath); - this.fileSize = file.length(); - this.fileName = file.getName(); - this.imageName = this.fileName; - this.missing = missingPhoto; -// this.location = new GeoJSON(); - this.additionalInfo = new HashMap<>(); - } - -// public FieldBookImage(io.swagger.client.model.Image response) { -// this.setDbId(response.getImageDbId()); -// this.setUnitDbId(response.getObservationUnitDbId()); -// this.fileName = response.getImageFileName(); -// } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - FieldBookImage that = (FieldBookImage) o; - return objectsEquals(getUnitDbId(), that.getUnitDbId()) && - objectsEquals(fileName, that.getFileName()); - } - - @Override - public int hashCode() { - return objectsHash(getUnitDbId(), fileName);//, timestamp); - } - -// public GeoJSON getLocation() { -// return location; -// } - - public int getWidth() { - return width; - } - - public int getHeight() { - return height; - } - - public long getFileSize() { - return fileSize; - } - - public String getFileName() { - return fileName; - } - - public void setFile(File file) { - this.file = file; - } - - public void setWidth(int width) { - this.width = width; - } - - public void setHeight(int height) { - this.height = height; - } - - public void setFileSize(long fileSize) { - this.fileSize = fileSize; - } - - public void setFileName(String fileName) { - this.fileName = fileName; - } - - public void setImageName(String imageName) { - this.imageName = imageName; - } - - public void setMimeType(String mimeType) { - this.mimeType = mimeType; - } - - public void setMissing(Bitmap missing) { - this.missing = missing; - } - -// public void setLocation(GeoJSON location) { -// this.location = location; -// } - - public void setBytes(byte[] bytes) { - this.bytes = bytes; - } - - public String getImageName() { - return imageName; - } - - public String getMimeType() { - return mimeType; - } - - public File getFile() { - return file; - } - - public String getCopyright() { - if(getTimestamp() != null) { - return String.valueOf(getTimestamp().getYear()); - } - return null; - } - - public Map getAdditionalInfo() { - return additionalInfo; - } - - public void setAdditionalInfo(Map additionalInfo) { - this.additionalInfo = additionalInfo; - } - - public byte[] getImageData() { - return bytes; - } - - public List getDescriptiveOntologyTerms() { - return this.descriptiveOntologyTerms; - } - - public void setDescriptiveOntologyTerms(List descriptiveOntologyTerms) { - this.descriptiveOntologyTerms = descriptiveOntologyTerms; - } - - public String getDescription() { - return this.description; - } - - public void setDescription(String description) { - this.description = description; - } - - public void loadImage() { - Bitmap bitmap; - if (!file.exists()) { - bitmap = missing; - width = missing.getWidth(); - height = missing.getHeight(); - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - bitmap.compress(Bitmap.CompressFormat.JPEG, 95, stream); - bytes = stream.toByteArray(); - fileSize = bytes.length; - } else { - BitmapFactory.Options bmOptions = new BitmapFactory.Options(); - bmOptions.inJustDecodeBounds = true; - bitmap = BitmapFactory.decodeFile(file.getAbsolutePath(), bmOptions); - width = bmOptions.outWidth; - height = bmOptions.outHeight; - bytes = new byte[(int) file.length()]; - -// try { -// ExifInterface exif = new ExifInterface(file.getAbsolutePath()); -// double latlon[] = exif.getLatLong(); -// if (latlon != null) { -// double lat = latlon[0]; -// double lon = latlon[1]; -// location.setType(GeoJSON.TypeEnum.FEATURE); -// JsonObject o = new JsonObject(); -// o.addProperty("type", "Point"); -// JsonArray a = new JsonArray(); -// a.add(lon); -// a.add(lat); -// o.add("coordinates", a); -// location.setGeometry(o); -// } -// -// FileInputStream fin = new FileInputStream(file); -// fin.read(bytes); -// } catch (IOException e) { -// } - } - - mimeType = "image/jpeg"; - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/brapi/model/Observation.java b/app/src/main/java/org/phenoapps/intercross/brapi/model/Observation.java deleted file mode 100644 index 3b7735d7..00000000 --- a/app/src/main/java/org/phenoapps/intercross/brapi/model/Observation.java +++ /dev/null @@ -1,56 +0,0 @@ -package org.phenoapps.intercross.brapi.model; - -public class Observation extends BrapiObservation { - - private String collector; - private String season; - private String studyId; - private String value; - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - Observation that = (Observation) o; - return super.objectsEquals(getUnitDbId(), that.getUnitDbId()) && - objectsEquals(getVariableDbId(), that.getVariableDbId()); - } - - @Override - public int hashCode() { - return super.objectsHash(getUnitDbId(), getVariableDbId()); - } - - public String getSeason() { - return season; - } - - public void setSeason(String season) { - this.season = season; - } - - public String getStudyId() { - return studyId; - } - - public void setStudyId(String studyId) { - this.studyId = studyId; - } - - public String getValue() { - return value; - } - - public void setValue(String value) { - this.value = value; - } - - public String getCollector() { - return collector; - } - - public void setCollector(String collector) { - this.collector = collector; - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/brapi/service/BrAPIService.java b/app/src/main/java/org/phenoapps/intercross/brapi/service/BrAPIService.java index e3c41ae4..0a7d5f6d 100644 --- a/app/src/main/java/org/phenoapps/intercross/brapi/service/BrAPIService.java +++ b/app/src/main/java/org/phenoapps/intercross/brapi/service/BrAPIService.java @@ -10,7 +10,6 @@ import android.util.Patterns; import android.widget.Toast; -import androidx.arch.core.util.Function; import androidx.preference.PreferenceManager; import org.phenoapps.intercross.Constants; @@ -18,38 +17,22 @@ import org.phenoapps.intercross.brapi.ApiError; import org.phenoapps.intercross.brapi.BrapiAuthDialog; import org.phenoapps.intercross.brapi.BrapiControllerResponse; -import org.phenoapps.intercross.brapi.model.BrapiProgram; -import org.phenoapps.intercross.brapi.model.BrapiStudyDetails; -import org.phenoapps.intercross.brapi.model.BrapiTrial; -import org.phenoapps.intercross.brapi.model.FieldBookImage; -import org.phenoapps.intercross.brapi.model.Observation; import org.phenoapps.intercross.util.KeyUtil; import java.net.MalformedURLException; import java.net.URL; -import java.util.List; -//todo create github package for a generalized service maybe also an Android Service implementation? public interface BrAPIService { - public static String exportTarget = "export"; - public static String notUniqueFieldMessage = "not_unique"; - public static String notUniqueIdMessage = "not_unique_id"; - - public static BrapiControllerResponse authorizeBrAPI(SharedPreferences sharedPreferences, Context context, String target) { + static BrapiControllerResponse authorizeBrAPI(SharedPreferences sharedPreferences, Context context, String target) { KeyUtil keyUtil = new KeyUtil(context); - SharedPreferences.Editor editor = PreferenceManager.getDefaultSharedPreferences(context).edit(); - //TODO -// editor.putString(keyUtil.getBrapiKeys().getBrapiTokenKey(), null); -// editor.apply(); if (target == null) { target = ""; } try { - //TODO parameterize this name or use Application label - String url = ""; //TODO PreferenceManager.getDefaultSharedPreferences(context).getString(keyUtil.getBrapiKeys().getBrapiUrlKey(), "") + "/brapi/authorize?display_name=Intercross&return_url=intercross://%s"; + String url = PreferenceManager.getDefaultSharedPreferences(context).getString(keyUtil.getBrapiUrl(), "") + "/brapi/authorize?display_name=Intercross&return_url=intercross://%s"; url = String.format(url, target); try { // Go to url with the default browser @@ -75,7 +58,7 @@ public static BrapiControllerResponse authorizeBrAPI(SharedPreferences sharedPre } // Returns true on successful parsing. False otherwise. - public static BrapiControllerResponse checkBrapiAuth(Activity activity) { + static BrapiControllerResponse checkBrapiAuth(Activity activity) { KeyUtil keyUtil = new KeyUtil(activity); @@ -104,13 +87,13 @@ public static BrapiControllerResponse checkBrapiAuth(Activity activity) { return new BrapiControllerResponse(false, "No access token received in response from host."); } - //TODO editor.putString(keyUtil.getBrapiKeys().getBrapiTokenKey(), token); + editor.putString(keyUtil.getBrapiToken(), token); editor.apply(); return new BrapiControllerResponse(true, activity.getString(R.string.brapi_auth_success)); } else { SharedPreferences.Editor editor = prefs.edit(); - //TODO editor.putString(keyUtil.getBrapiKeys().getBrapiTokenKey(), null); + editor.putString(keyUtil.getBrapiToken(), null); editor.apply(); return new BrapiControllerResponse(false, activity.getString(R.string.brapi_auth_deny)); @@ -123,26 +106,22 @@ public static BrapiControllerResponse checkBrapiAuth(Activity activity) { } // Helper functions for brapi configurations - public static Boolean isLoggedIn(Context context) { + static Boolean isLoggedIn(Context context) { KeyUtil keyUtil = new KeyUtil(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - String auth_token = ""; //TODO prefs.getString(keyUtil.getBrapiKeys().getBrapiTokenKey(), ""); - - if (auth_token == null || auth_token == "") { - return false; - } + String auth_token = prefs.getString(keyUtil.getBrapiToken(), ""); - return true; + return auth_token != null && auth_token != ""; } - public static Boolean hasValidBaseUrl(Context context) { + static Boolean hasValidBaseUrl(Context context) { String url = getBrapiUrl(context); return Patterns.WEB_URL.matcher(url).matches(); } - public static Boolean checkMatchBrapiUrl(Context context, String dataSource) { + static Boolean checkMatchBrapiUrl(Context context, String dataSource) { try { URL externalUrl = new URL(getBrapiUrl(context)); @@ -156,7 +135,7 @@ public static Boolean checkMatchBrapiUrl(Context context, String dataSource) { } - public static String getHostUrl(Context context) { + static String getHostUrl(Context context) { try { String brapiURL = getBrapiUrl(context); URL externalUrl = new URL(brapiURL); @@ -167,19 +146,19 @@ public static String getHostUrl(Context context) { } } - public static String getBrapiUrl(Context context) { + static String getBrapiUrl(Context context) { KeyUtil keyUtil = new KeyUtil(context); SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - String baseURL = ""; //TODO prefs.getString(keyUtil.getBrapiKeys().getBrapiUrlKey(), "https://test-server.brapi.org"); + String baseURL = prefs.getString(keyUtil.getBrapiUrl(), "https://test-server.brapi.org"); String path = Constants.BRAPI_PATH_V2; return baseURL + path; } - public static boolean isConnectionError(int code) { + static boolean isConnectionError(int code) { return code == 401 || code == 403 || code == 404; } - public static void handleConnectionError(Context context, int code) { + static void handleConnectionError(Context context, int code) { ApiError apiError = ApiError.processErrorCode(code); String toastMsg = ""; @@ -201,29 +180,4 @@ public static void handleConnectionError(Context context, int code) { } Toast.makeText(context.getApplicationContext(), toastMsg, Toast.LENGTH_LONG).show(); } - - public void getPrograms(final BrapiPaginationManager paginationManager, final Function, Void> function, final Function failFunction); - - public void getTrials(String programDbId, BrapiPaginationManager paginationManager, final Function, Void> function, final Function failFunction); - - public void getStudies(String programDbId, String trialDbId, BrapiPaginationManager paginationManager, final Function, Void> function, final Function failFunction); - - //public void getStudyDetails(final String studyDbId, final Function function, final Function failFunction); - - //public void getPlotDetails(final String studyDbId, final Function function, final Function failFunction); - - //public void getOntology(BrapiPaginationManager paginationManager, final Function, Void> function, final Function failFunction); - - public void postPhenotypes(List observations, - final Function, Void> function, - final Function failFunction); - - // will only ever have one study in current architecture - public void putObservations(List observations, - final Function, Void> function, - final Function failFunction); - - //public void getTraits(final String studyDbId, final Function function, final Function failFunction); - - //public BrapiControllerResponse saveStudyDetails(BrapiStudyDetails studyDetails); } \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/brapi/service/BrAPIServiceV2.java b/app/src/main/java/org/phenoapps/intercross/brapi/service/BrAPIServiceV2.java index 28dbbba3..a1dd9ff4 100644 --- a/app/src/main/java/org/phenoapps/intercross/brapi/service/BrAPIServiceV2.java +++ b/app/src/main/java/org/phenoapps/intercross/brapi/service/BrAPIServiceV2.java @@ -2,333 +2,107 @@ import android.app.Activity; import android.content.Context; +import android.content.SharedPreferences; +import android.util.Log; import androidx.arch.core.util.Function; +import androidx.preference.PreferenceManager; import org.brapi.client.v2.BrAPIClient; import org.brapi.client.v2.model.exceptions.ApiException; -import org.brapi.client.v2.model.queryParams.core.ProgramQueryParams; -import org.brapi.client.v2.model.queryParams.core.StudyQueryParams; -import org.brapi.client.v2.model.queryParams.core.TrialQueryParams; +import org.brapi.client.v2.model.queryParams.germplasm.CrossQueryParams; import org.brapi.client.v2.model.queryParams.germplasm.CrossingProjectQueryParams; -import org.brapi.client.v2.model.queryParams.germplasm.GermplasmQueryParams; import org.brapi.client.v2.model.queryParams.germplasm.PlannedCrossQueryParams; -import org.brapi.client.v2.model.queryParams.phenotype.ObservationUnitQueryParams; -import org.brapi.client.v2.model.queryParams.phenotype.VariableQueryParams; import org.brapi.client.v2.modules.core.ProgramsApi; -import org.brapi.client.v2.modules.core.StudiesApi; -import org.brapi.client.v2.modules.core.TrialsApi; import org.brapi.client.v2.modules.germplasm.CrossesApi; import org.brapi.client.v2.modules.germplasm.CrossingProjectsApi; import org.brapi.client.v2.modules.germplasm.GermplasmApi; -import org.brapi.client.v2.modules.phenotype.ImagesApi; import org.brapi.client.v2.modules.phenotype.ObservationUnitsApi; -import org.brapi.client.v2.modules.phenotype.ObservationVariablesApi; -import org.brapi.client.v2.modules.phenotype.ObservationsApi; import org.brapi.v2.model.BrAPIMetadata; -import org.brapi.v2.model.TimeAdapter; -import org.brapi.v2.model.core.BrAPIProgram; -import org.brapi.v2.model.core.BrAPIStudy; -import org.brapi.v2.model.core.BrAPITrial; -import org.brapi.v2.model.core.request.BrAPIStudySearchRequest; -import org.brapi.v2.model.core.request.BrAPITrialSearchRequest; -import org.brapi.v2.model.core.response.BrAPIProgramListResponse; -import org.brapi.v2.model.core.response.BrAPIStudyListResponse; -import org.brapi.v2.model.core.response.BrAPIStudySingleResponse; -import org.brapi.v2.model.core.response.BrAPITrialListResponse; import org.brapi.v2.model.germ.BrAPICross; import org.brapi.v2.model.germ.BrAPICrossingProject; -import org.brapi.v2.model.germ.BrAPIGermplasm; import org.brapi.v2.model.germ.BrAPIPlannedCross; import org.brapi.v2.model.germ.response.BrAPICrossesListResponse; import org.brapi.v2.model.germ.response.BrAPICrossingProjectsListResponse; -import org.brapi.v2.model.germ.response.BrAPIGermplasmListResponse; import org.brapi.v2.model.germ.response.BrAPIPlannedCrossesListResponse; -import org.brapi.v2.model.pheno.BrAPIImage; -import org.brapi.v2.model.pheno.BrAPIObservation; -import org.brapi.v2.model.pheno.BrAPIObservationUnit; -import org.brapi.v2.model.pheno.BrAPIObservationUnitLevelRelationship; -import org.brapi.v2.model.pheno.BrAPIObservationUnitPosition; -import org.brapi.v2.model.pheno.BrAPIPositionCoordinateTypeEnum; -import org.brapi.v2.model.pheno.BrAPIScaleValidValuesCategories; -import org.brapi.v2.model.pheno.request.BrAPIObservationUnitSearchRequest; -import org.brapi.v2.model.pheno.response.BrAPIImageListResponse; -import org.brapi.v2.model.pheno.response.BrAPIImageSingleResponse; -import org.brapi.v2.model.pheno.response.BrAPIObservationListResponse; -import org.brapi.v2.model.pheno.response.BrAPIObservationUnitListResponse; -import org.brapi.v2.model.pheno.response.BrAPIObservationVariableListResponse; -import org.phenoapps.intercross.brapi.model.BrapiProgram; -import org.phenoapps.intercross.brapi.model.BrapiStudyDetails; -import org.phenoapps.intercross.brapi.model.BrapiTrial; -import org.phenoapps.intercross.brapi.model.FieldBookImage; -import org.phenoapps.intercross.brapi.model.Observation; +import org.phenoapps.brapi.account.BrapiAccountRepository; +import org.phenoapps.brapi.account.BrapiPreferenceKeys; import org.phenoapps.intercross.util.KeyUtil; import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; import java.util.List; import java.util.Map; -import static com.google.android.gms.common.util.CollectionUtils.listOf; +public class BrAPIServiceV2 implements BrAPIService { -import com.google.gson.JsonElement; -import com.google.gson.JsonObject; -import com.google.gson.JsonPrimitive; - -public class BrAPIServiceV2 implements BrAPIService{ - - private final Context context; - private final ImagesApi imagesApi; - private final StudiesApi studiesApi; + private final static String TAG = "BrAPI V2"; private final ProgramsApi programsApi; - private final TrialsApi trialsApi; private final CrossingProjectsApi crossingProjectsApi; private final CrossesApi crossesApi; private final GermplasmApi germplasmApi; - private final ObservationsApi observationsApi; - private final ObservationUnitsApi observationUnitsApi; - private final ObservationVariablesApi traitsApi; - private KeyUtil mKeyUtil; + public final ObservationUnitsApi observationUnitsApi; + + private final KeyUtil mKeyUtil; public BrAPIServiceV2(Context context) { - this.context = context; this.mKeyUtil = new KeyUtil(context); - // Make timeout longer. Set it to 60 seconds for now - BrAPIClient apiClient = new BrAPIClient(BrAPIService.getBrapiUrl(context), 60000); -// try { -// //TODO -//// apiClient.authenticate(t -> context.getSharedPreferences("Settings", 0) -//// .getString(mKeyUtil.getBrapiKeys().getBrapiTokenKey(), null)); -// } catch (ApiException e) { -// e.printStackTrace(); -// } - this.imagesApi = new ImagesApi(apiClient); - this.studiesApi = new StudiesApi(apiClient); - this.programsApi = new ProgramsApi(apiClient); - this.trialsApi = new TrialsApi(apiClient); - this.crossingProjectsApi = new CrossingProjectsApi(apiClient); - this.germplasmApi = new GermplasmApi(apiClient); - this.crossesApi = new CrossesApi(apiClient); - this.traitsApi = new ObservationVariablesApi(apiClient); - this.observationsApi = new ObservationsApi(apiClient); - this.observationUnitsApi = new ObservationUnitsApi(apiClient); - } - private void updatePageInfo(BrapiPaginationManager paginationManager, BrAPIMetadata metadata){ - if(paginationManager.getContext() != null) { //null check for JUnits - ((Activity) paginationManager.getContext()) - .runOnUiThread(() -> paginationManager.updatePageInfo(metadata.getPagination().getTotalPages())); - } - } - - public void postImageMetaData(FieldBookImage image, - final Function function, - final Function failFunction) { - try { - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { - @Override - public void onSuccess(BrAPIImageListResponse imageResponse, int i, Map> map) { - final BrAPIImage response = imageResponse.getResult().getData().get(0); - function.apply(mapToImage(response)); - } - - @Override - public void onFailure(ApiException error, int statusCode, Map> responseHeaders) { - failFunction.apply(error.getCode()); - } - }; - - BrAPIImage request = mapImage(image); - imagesApi.imagesPostAsync(Arrays.asList(request), callback); - - } catch (ApiException e) { - failFunction.apply(e.getCode()); - e.printStackTrace(); - } - - } - - private BrAPIImage mapImage(FieldBookImage image) { - BrAPIImage request = new BrAPIImage(); - JsonObject object = new JsonObject(); - Map info = image.getAdditionalInfo(); - if (info != null) { - for (Map.Entry entry : info.entrySet()) { - JsonElement element = new JsonPrimitive(entry.getValue()); - object.add(entry.getKey(), element); - } - } - request.setAdditionalInfo(object); - request.setCopyright(image.getCopyright()); - request.setDescription(image.getDescription()); - request.setDescriptiveOntologyTerms(image.getDescriptiveOntologyTerms()); - request.setImageFileName(image.getFileName()); - request.setImageFileSize((int) image.getFileSize()); - request.setImageHeight(image.getHeight()); - request.setImageName(image.getImageName()); - request.setImageWidth(image.getWidth()); - request.setMimeType(image.getMimeType()); - request.setObservationUnitDbId(image.getUnitDbId()); - // TODO fix these - //request.setImageLocation(image.getLocation()); - request.setImageTimeStamp(TimeAdapter.convertFrom(image.getTimestamp())); - return request; - } + // Make timeout longer. Set it to 60 seconds for now + BrAPIClient apiClient = new BrAPIClient(BrAPIService.getBrapiUrl(context), 60*1000*10); - private FieldBookImage mapToImage(BrAPIImage image) { - FieldBookImage request = new FieldBookImage(); - Map info = new HashMap<>(); - JsonObject object = image.getAdditionalInfo(); - if (object != null) { - for (Map.Entry entry : object.entrySet()) { - info.put(entry.getKey(), entry.getValue().getAsString()); - } - } - request.setAdditionalInfo(info); - request.setDescription(image.getDescription()); - request.setDescriptiveOntologyTerms(image.getDescriptiveOntologyTerms()); - request.setFileName(image.getImageFileName()); - request.setFileSize((int) image.getImageFileSize()); - request.setHeight(image.getImageHeight()); - request.setImageName(image.getImageName()); - request.setWidth(image.getImageWidth()); - request.setMimeType(image.getMimeType()); - request.setUnitDbId(image.getObservationUnitDbId()); - // TODO fix these - //request.setLocation(image.getImageLocation()); - request.setTimestamp(TimeAdapter.convertFrom(image.getImageTimeStamp())); - return request; - } + String token = getTokenFromRepository(context); - public void putImageContent(FieldBookImage image, - final Function function, - final Function failFunction) { try { - - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { - @Override - public void onSuccess(BrAPIImageSingleResponse imageResponse, int i, Map> map) { - final BrAPIImage response = imageResponse.getResult(); - function.apply(mapToImage(response)); - } - - @Override - public void onFailure(ApiException error, int i, Map> map) { - failFunction.apply(error.getCode()); - } - }; - - imagesApi.imagesImageDbIdImagecontentPutAsync(image.getDbId(), image.getImageData(), callback); - + apiClient.authenticate(t -> token); } catch (ApiException e) { - failFunction.apply(e.getCode()); - e.printStackTrace(); + Log.e(TAG, "Authentication error", e); } + this.programsApi = new ProgramsApi(apiClient); + this.crossingProjectsApi = new CrossingProjectsApi(apiClient); + this.germplasmApi = new GermplasmApi(apiClient); + this.crossesApi = new CrossesApi(apiClient); + this.observationUnitsApi = new ObservationUnitsApi(apiClient); } - public void putImage(FieldBookImage image, - final Function function, - final Function failFunction) { - try { - - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { - @Override - public void onSuccess(BrAPIImageSingleResponse imageResponse, int i, Map> map) { - final BrAPIImage response = imageResponse.getResult(); - function.apply(mapToImage(response)); - } - - @Override - public void onFailure(ApiException error, int i, Map> map) { - failFunction.apply(error.getCode()); - } - }; - - BrAPIImage request = mapImage(image); - imagesApi.imagesImageDbIdPutAsync(image.getDbId(), request, callback); - - } catch (ApiException e) { - failFunction.apply(e.getCode()); - e.printStackTrace(); - } - - } - - public void getPrograms(final BrapiPaginationManager paginationManager, - final Function, Void> function, - final Function failFunction) { - Integer initPage = paginationManager.getPage(); + /** + * Retrieves the BrAPI access token, trying BrapiAccountRepository first + * and falling back to SharedPreferences if the repository is unavailable. + */ + private static String getTokenFromRepository(Context context) { try { - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { - @Override - public void onSuccess(BrAPIProgramListResponse programsResponse, int i, Map> map) { - // Cancel processing if the page that was processed is not the page - // that we are currently on. For Example: User taps "Next Page" before brapi call returns data - if (initPage.equals(paginationManager.getPage())) { - updatePageInfo(paginationManager, programsResponse.getMetadata()); - List programList = programsResponse.getResult().getData(); - function.apply(mapPrograms(programList)); - } - } - - @Override - public void onFailure(ApiException error, int i, Map> map) { - failFunction.apply(error.getCode()); - } - }; - ProgramQueryParams queryParams = new ProgramQueryParams(); - queryParams.page(paginationManager.getPage()).pageSize(paginationManager.getPageSize()); - programsApi.programsGetAsync(queryParams, callback); - } catch (ApiException e) { - failFunction.apply(e.getCode()); - e.printStackTrace(); - } - } - - private List mapPrograms(List programList) { - List brapiPrograms = new ArrayList<>(); - if (programList != null) { - for (BrAPIProgram program : programList) { - BrapiProgram brapiProgram = new BrapiProgram(); - brapiProgram.setProgramName(program.getProgramName()); - brapiProgram.setProgramDbId(program.getProgramDbId()); - brapiPrograms.add(brapiProgram); + KeyUtil keyUtil = new KeyUtil(context); + SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); + BrapiPreferenceKeys keys = new BrapiPreferenceKeys( + keyUtil.getBrapiEnabled(), + keyUtil.getBrapiUrl(), + keyUtil.getBrapiDisplayName(), + keyUtil.getBrapiToken(), + keyUtil.getBrapiId(), + "v2", + keyUtil.getBrapiOidcUrl(), + keyUtil.getBrapiOidcFlow(), + keyUtil.getBrapiOidcClientId(), + keyUtil.getBrapiOidcScope() + ); + BrapiAccountRepository repository = new BrapiAccountRepository(context, prefs, keys); + String token = repository.peekToken(); + if (token != null && !token.isEmpty()) { + return token; } + } catch (Exception e) { + Log.e(TAG, "Failed to get token from repository, falling back to SharedPreferences", e); } - return brapiPrograms; + // Fallback to SharedPreferences + KeyUtil keyUtil = new KeyUtil(context); + return PreferenceManager.getDefaultSharedPreferences(context).getString(keyUtil.getBrapiToken(), ""); } - public void getTrials(String programDbId, BrapiPaginationManager paginationManager, - final Function, Void> function, - final Function failFunction) { - Integer initPage = paginationManager.getPage(); - try { - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { - @Override - public void onSuccess(BrAPITrialListResponse trialsResponse, int i, Map> map) { - // Cancel processing if the page that was processed is not the page - // that we are currently on. For Example: User taps "Next Page" before brapi call returns data - if (initPage.equals(paginationManager.getPage())) { - updatePageInfo(paginationManager, trialsResponse.getMetadata()); - List trialList = trialsResponse.getResult().getData(); - function.apply(mapTrials(trialList)); - } - } - - @Override - public void onFailure(ApiException error, int i, Map> map) { - failFunction.apply(error.getCode()); - } - }; - TrialQueryParams queryParams = new TrialQueryParams(); - queryParams.programDbId(programDbId).page(paginationManager.getPage()).pageSize(paginationManager.getPageSize()); - trialsApi.trialsGetAsync(queryParams, callback); - } catch (ApiException e) { - failFunction.apply(e.getCode()); - e.printStackTrace(); + private void updatePageInfo(BrapiPaginationManager paginationManager, BrAPIMetadata metadata){ + if(paginationManager.getContext() != null) { //null check for JUnits + ((Activity) paginationManager.getContext()) + .runOnUiThread(() -> paginationManager.update(metadata.getPagination())); } } @@ -386,35 +160,6 @@ public void onFailure(ApiException error, int statusCode, Map observations, final Function, Void> function, - final Function failFunction) { - try { - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { - @Override - public void onSuccess(BrAPIObservationUnitListResponse response, int i, Map> map) { - - List units = new ArrayList<>(); - for(BrAPIObservationUnit obs: response.getResult().getData()){ - units.add(obs); - } - - function.apply(units); - } - - @Override - public void onFailure(ApiException error, int statusCode, Map> responseHeaders) { - failFunction.apply(error.getCode()); - } - }; - - observationUnitsApi.observationunitsPostAsync(observations, callback); - - } catch (ApiException e) { - failFunction.apply(e.getCode()); - e.printStackTrace(); - } - } - public void postCrossingProject(String name, final Function, Void> function, final Function failFunction) { try { @@ -445,23 +190,23 @@ public void onFailure(ApiException error, int statusCode, Map, Void> function, - final Function failFunction) { + public void getPlannedCrosses(String crossProjectDbId, BrapiPaginationManager paginationManager, + final Function, Void> function, + final Function failFunction) { Integer initPage = paginationManager.getPage(); try { - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { + BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { @Override - public void onSuccess(BrAPIGermplasmListResponse response, int i, Map> map) { + public void onSuccess(BrAPIPlannedCrossesListResponse response, int i, Map> map) { // Cancel processing if the page that was processed is not the page // that we are currently on. For Example: User taps "Next Page" before brapi call returns data if (initPage.equals(paginationManager.getPage())) { updatePageInfo(paginationManager, response.getMetadata()); - List germs = response.getResult().getData(); - function.apply(germs); + List crosses = response.getResult().getData(); + function.apply(crosses); } } @@ -471,10 +216,12 @@ public void onFailure(ApiException error, int i, Map> map) } }; - GermplasmQueryParams request = new GermplasmQueryParams(); + PlannedCrossQueryParams request = new PlannedCrossQueryParams(); request.page(paginationManager.getPage()).pageSize(paginationManager.getPageSize()); - germplasmApi.germplasmGetAsync(request, callback); + //.crossingProjectDbId(crossProjectDbId) + + crossesApi.plannedcrossesGetAsync(request, callback); } catch (ApiException e) { failFunction.apply(e.getCode()); @@ -482,23 +229,20 @@ public void onFailure(ApiException error, int i, Map> map) } } - - public void getPlannedCrosses(String crossProjectDbId, BrapiPaginationManager paginationManager, - final Function, Void> function, + public void getCrosses(String crossProjectDbId, BrapiPaginationManager paginationManager, + final Function, Void> function, final Function failFunction) { Integer initPage = paginationManager.getPage(); try { - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { + BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { @Override - public void onSuccess(BrAPIPlannedCrossesListResponse response, int i, Map> map) { - // Cancel processing if the page that was processed is not the page - // that we are currently on. For Example: User taps "Next Page" before brapi call returns data + public void onSuccess(BrAPICrossesListResponse response, int i, Map> map) { if (initPage.equals(paginationManager.getPage())) { updatePageInfo(paginationManager, response.getMetadata()); - List crosses = response.getResult().getData(); + List crosses = response.getResult().getData(); function.apply(crosses); } } @@ -509,12 +253,9 @@ public void onFailure(ApiException error, int i, Map> map) } }; - PlannedCrossQueryParams request = new PlannedCrossQueryParams(); - request.page(paginationManager.getPage()).pageSize(paginationManager.getPageSize()); - - //.crossingProjectDbId(crossProjectDbId) - - crossesApi.plannedcrossesGetAsync(request, callback); + CrossQueryParams params = new CrossQueryParams(); + params.crossingProjectDbId(crossProjectDbId).page(paginationManager.getPage()).pageSize(paginationManager.getPageSize()); + crossesApi.crossesGetAsync(params, callback); } catch (ApiException e) { failFunction.apply(e.getCode()); @@ -530,8 +271,6 @@ public void getCrossingProjects(BrapiPaginationManager paginationManager, BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { @Override public void onSuccess(BrAPICrossingProjectsListResponse projectsListResponse, int i, Map> map) { - // Cancel processing if the page that was processed is not the page - // that we are currently on. For Example: User taps "Next Page" before brapi call returns data if (initPage.equals(paginationManager.getPage())) { updatePageInfo(paginationManager, projectsListResponse.getMetadata()); List projects = projectsListResponse.getResult().getData(); @@ -555,121 +294,15 @@ public void onFailure(ApiException error, int i, Map> map) } } - public void searchTrials(List programDbIds, BrapiPaginationManager paginationManager, - final Function, Void> function, - final Function failFunction) { - Integer initPage = paginationManager.getPage(); - try { - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { - @Override - public void onSuccess(BrAPITrialListResponse trialsResponse, int i, Map> map) { - // Cancel processing if the page that was processed is not the page - // that we are currently on. For Example: User taps "Next Page" before brapi call returns data - if (initPage.equals(paginationManager.getPage())) { - updatePageInfo(paginationManager, trialsResponse.getMetadata()); - List trialList = trialsResponse.getResult().getData(); - function.apply(mapTrials(trialList)); - } - } - - @Override - public void onFailure(ApiException error, int i, Map> map) { - failFunction.apply(error.getCode()); - } - }; - - BrAPITrialSearchRequest request = new BrAPITrialSearchRequest(); - request.programDbIds(programDbIds).page(paginationManager.getPage()).pageSize(paginationManager.getPageSize()); - - trialsApi.searchTrialsPostAsync(request, callback); - } catch (ApiException e) { - failFunction.apply(e.getCode()); - e.printStackTrace(); - } - } - - public void searchObservationUnits(List observationUnitIds, BrapiPaginationManager paginationManager, - final Function, Void> function, - final Function failFunction) { - Integer initPage = paginationManager.getPage(); - try { - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { - @Override - public void onSuccess(BrAPIObservationUnitListResponse response, int i, Map> map) { - // Cancel processing if the page that was processed is not the page - // that we are currently on. For Example: User taps "Next Page" before brapi call returns data - if (initPage.equals(paginationManager.getPage())) { - updatePageInfo(paginationManager, response.getMetadata()); - List list = response.getResult().getData(); - function.apply(list); - } - } - - @Override - public void onFailure(ApiException error, int i, Map> map) { - failFunction.apply(error.getCode()); - } - }; - - BrAPIObservationUnitSearchRequest request = new BrAPIObservationUnitSearchRequest(); - request.observationUnitDbIds(observationUnitIds).page(paginationManager.getPage()).pageSize(paginationManager.getPageSize()); - - observationUnitsApi.searchObservationunitsPostAsync(request, callback); - - } catch (ApiException e) { - failFunction.apply(e.getCode()); - e.printStackTrace(); - } - } - - private List mapStudies(List studies) { - List brapiStudies = new ArrayList<>(); - if (studies != null) { - for (BrAPIStudy study : studies) { - BrapiStudyDetails brapiStudy = new BrapiStudyDetails(); - brapiStudy.setStudyName(study.getStudyName()); - brapiStudy.setStudyDbId(study.getStudyDbId()); - brapiStudies.add(brapiStudy); - } - } - return brapiStudies; - } - - private List mapTrials(List trialList) { - List brapiTrials = new ArrayList<>(); - if (trialList != null) { - for (BrAPITrial trial : trialList) { - BrapiTrial brapiTrial = new BrapiTrial(); - String name = trial.getTrialName(); - brapiTrial.setTrialName(name); - brapiTrial.setTrialDbId(trial.getTrialDbId()); - brapiTrials.add(brapiTrial); - } - } - return brapiTrials; - } - - public void getStudies(String programDbId, String trialDbId, BrapiPaginationManager paginationManager, - final Function, Void> function, - final Function failFunction) { - Integer initPage = paginationManager.getPage(); + public void getCrossingProject(String crossProjectDbId, + final Function, Void> function, + final Function failFunction) { try { - - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { + BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { @Override - public void onSuccess(BrAPIStudyListResponse studiesResponse, int i, Map> map) { - // Cancel processing if the page that was processed is not the page - // that we are currently on. For Example: User taps "Next Page" before brapi call returns data - if (initPage.equals(paginationManager.getPage())) { - updatePageInfo(paginationManager, studiesResponse.getMetadata()); - final List studies = new ArrayList<>(); - final List studySummaryList = studiesResponse.getResult().getData(); - for (BrAPIStudy studySummary : studySummaryList) { - studies.add(mapStudy(studySummary)); - } - - function.apply(studies); - } + public void onSuccess(BrAPICrossingProjectsListResponse projectsListResponse, int i, Map> map) { + List projects = projectsListResponse.getResult().getData(); + function.apply(projects); } @Override @@ -678,133 +311,9 @@ public void onFailure(ApiException error, int i, Map> map) } }; - StudyQueryParams queryParams = new StudyQueryParams(); - queryParams.active("true").programDbId(programDbId).trialDbId(trialDbId).page(paginationManager.getPage()).pageSize(paginationManager.getPageSize()); - studiesApi.studiesGetAsync(queryParams, callback); - - } catch (ApiException e) { - failFunction.apply(e.getCode()); - e.printStackTrace(); - } - - } - - private BrapiStudyDetails mapStudy(BrAPIStudy study) { - BrapiStudyDetails studyDetails = new BrapiStudyDetails(); - studyDetails.setStudyDbId(study.getStudyDbId()); - studyDetails.setStudyName(study.getStudyName()); - studyDetails.setCommonCropName(study.getCommonCropName()); - studyDetails.setStudyDescription(study.getStudyDescription()); - studyDetails.setStudyLocation(study.getLocationName()); - return studyDetails; - } - - private String getRowColStr(BrAPIPositionCoordinateTypeEnum type) { - if(null != type){ - switch (type) { - case PLANTED_INDIVIDUAL: - case GRID_COL: - case MEASURED_COL: - case LATITUDE: - return "Column"; - case PLANTED_ROW: - case GRID_ROW: - case MEASURED_ROW: - case LONGITUDE: - return "Row"; - } - } - return null; - } - - private Observation mapToObservation(BrAPIObservation obs){ - Observation newObservation = new Observation(); - newObservation.setDbId(obs.getObservationDbId()); - newObservation.setUnitDbId(obs.getObservationUnitDbId()); - newObservation.setVariableDbId(obs.getObservationVariableDbId()); - return newObservation; - } - - public void postPhenotypes(List observations, - final Function, Void> function, - final Function failFunction) { - try { - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { - @Override - public void onSuccess(BrAPIObservationListResponse phenotypesResponse, int i, Map> map) { - List newObservations = new ArrayList<>(); - for(BrAPIObservation obs: phenotypesResponse.getResult().getData()){ - newObservations.add(mapToObservation(obs)); - } - - function.apply(newObservations); - } - - @Override - public void onFailure(ApiException error, int statusCode, Map> responseHeaders) { - failFunction.apply(error.getCode()); - } - }; - - List request = new ArrayList<>(); - - for (Observation observation : observations) { - BrAPIObservation newObservation = new BrAPIObservation(); - newObservation.setCollector(observation.getCollector().trim()); - newObservation.setObservationTimeStamp(TimeAdapter.convertFrom(observation.getTimestamp())); - newObservation.setObservationUnitDbId(observation.getUnitDbId()); - newObservation.setStudyDbId(observation.getStudyId()); - newObservation.setObservationVariableDbId(observation.getVariableDbId()); - newObservation.setObservationVariableName(observation.getVariableName()); - newObservation.setValue(observation.getValue()); - - request.add(newObservation); - } - - observationsApi.observationsPostAsync(request, callback); - - } catch (ApiException e) { - failFunction.apply(e.getCode()); - e.printStackTrace(); - } - } - - public void putObservations(List observations, - final Function, Void> function, - final Function failFunction) { - try { - - BrapiV2ApiCallBack callback = new BrapiV2ApiCallBack() { - @Override - public void onSuccess(BrAPIObservationListResponse observationsResponse, int i, Map> map) { - List newObservations = new ArrayList<>(); - for(BrAPIObservation obs: observationsResponse.getResult().getData()){ - newObservations.add(mapToObservation(obs)); - } - function.apply(newObservations); - } - - @Override - public void onFailure(ApiException error, int statusCode, Map> responseHeaders) { - failFunction.apply(error.getCode()); - } - }; - - Map request = new HashMap<>(); - - for (Observation obs : observations) { - BrAPIObservation o = new BrAPIObservation(); - o.setCollector(obs.getCollector().trim()); - o.setObservationDbId(obs.getDbId()); - o.setObservationTimeStamp(TimeAdapter.convertFrom(obs.getTimestamp())); - o.setObservationUnitDbId(obs.getUnitDbId()); - o.setObservationVariableDbId(obs.getVariableDbId()); - o.setValue(obs.getValue()); - - request.put(obs.getDbId(), o); - } - - observationsApi.observationsPutAsync(request, callback); + CrossingProjectQueryParams request = new CrossingProjectQueryParams(); + request.crossingProjectDbId(crossProjectDbId); + crossingProjectsApi.crossingprojectsGetAsync(request, callback); } catch (ApiException e) { failFunction.apply(e.getCode()); diff --git a/app/src/main/java/org/phenoapps/intercross/brapi/service/BrapiPaginationManager.java b/app/src/main/java/org/phenoapps/intercross/brapi/service/BrapiPaginationManager.java index d7ab5b1e..df02027d 100644 --- a/app/src/main/java/org/phenoapps/intercross/brapi/service/BrapiPaginationManager.java +++ b/app/src/main/java/org/phenoapps/intercross/brapi/service/BrapiPaginationManager.java @@ -1,35 +1,21 @@ package org.phenoapps.intercross.brapi.service; -import android.app.Activity; import android.content.Context; import android.util.Log; -import android.view.View; -import android.widget.Button; -import android.widget.TextView; -import org.phenoapps.intercross.R; -import org.phenoapps.intercross.util.KeyUtil; +import org.brapi.v2.model.BrAPIPagination; public class BrapiPaginationManager { + private static final String TAG = "BrAPI Pager"; + private Integer currentPage = 0; private Integer totalPages = 1; private Integer pageSize; - - private Button nextBtn; - private Button prevBtn; - private TextView pageIndicator; - private Context context; - - private KeyUtil mKeyUtil; + private final Context context; public BrapiPaginationManager(Context context){ this.context = context; - this.mKeyUtil = new KeyUtil(context); - // Make our prev and next buttons invisible - nextBtn = ((Activity)context).findViewById(R.id.next); - prevBtn = ((Activity)context).findViewById(R.id.prev); - pageIndicator = ((Activity)context).findViewById(R.id.page_indicator); reset(); } @@ -43,10 +29,6 @@ public BrapiPaginationManager(Integer page, Integer pageSize){ } public void reset() { - // Initially make next and prev gone until we know there are more than 1 page. - - if (nextBtn != null) nextBtn.setVisibility(View.INVISIBLE); - if (prevBtn != null) prevBtn.setVisibility(View.INVISIBLE); //set defaults currentPage = 0; totalPages = 1; @@ -83,47 +65,17 @@ public Integer getPageSize() { return pageSize; } - public void updatePageInfo(Integer totalPages) { - //BrAPI metadata was setting totalPages to 0. PageMan. default should be 1. - this.totalPages = Math.max(1, totalPages); - refreshPageIndicator(); + public Integer getTotalPages() { + return totalPages; } - public void refreshPageIndicator() { - if (pageIndicator != null) pageIndicator.setText(String.format("Page %d of %d", currentPage + 1, totalPages)); - - // Determine our button visibility. Not necessary if we only have 1 page. - determineBtnVisibility(); - } - - public void determineBtnVisibility() { - // Determine what buttons should be visible - if (currentPage <= 0) { - if (prevBtn != null) prevBtn.setVisibility(View.INVISIBLE); - } else { - if (prevBtn != null) prevBtn.setVisibility(View.VISIBLE); - } - - if (currentPage >= (totalPages - 1)) { - if (nextBtn != null) nextBtn.setVisibility(View.INVISIBLE); - } else { - if (nextBtn != null) nextBtn.setVisibility(View.VISIBLE); - } - } - - public void setNewPage(int id) { - if(R.id.prev == id){ - if (currentPage > 0){ - currentPage = currentPage - 1; - }else { - currentPage = 0; - } - }else if (R.id.next == id){ - if (currentPage < totalPages - 1){ - currentPage = currentPage + 1; - }else { - currentPage = totalPages - 1; - } + public void update(BrAPIPagination pagination) { + //BrAPI metadata was setting totalPages to 0. PageMan. default should be 1. + this.totalPages = 1; + try { + this.totalPages = Math.max(1, pagination.getTotalPages()); + } catch (Exception e) { + Log.e(TAG, "Pagination update failed.", e); } } diff --git a/app/src/main/java/org/phenoapps/intercross/data/BaseRepository.kt b/app/src/main/java/org/phenoapps/intercross/data/BaseRepository.kt index aa43d3b2..b49ddee9 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/BaseRepository.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/BaseRepository.kt @@ -4,7 +4,7 @@ import kotlinx.coroutines.Dispatchers.IO import kotlinx.coroutines.withContext import org.phenoapps.intercross.data.dao.BaseDao -open class BaseRepository constructor(private val dao: BaseDao) { +open class BaseRepository(private val dao: BaseDao) { suspend fun insert(vararg items: T) { diff --git a/app/src/main/java/org/phenoapps/intercross/data/EventsRepository.kt b/app/src/main/java/org/phenoapps/intercross/data/EventsRepository.kt index 55efef52..36a2f2d2 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/EventsRepository.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/EventsRepository.kt @@ -11,6 +11,8 @@ class EventsRepository fun selectAll() = eventsDao.selectAll() + fun selectArchived() = eventsDao.selectArchived() + fun getParentCount() = eventsDao.getParentCount() fun getAllParents() = eventsDao.getAllParents() @@ -43,6 +45,18 @@ class EventsRepository } } + suspend fun archiveByIds(ids: List) { + withContext(IO) { + eventsDao.archiveByIds(ids) + } + } + + suspend fun unarchiveByIds(ids: List) { + withContext(IO) { + eventsDao.unarchiveByIds(ids) + } + } + fun insert(event: Event): Long = eventsDao.insertEvent(event) fun loadCrosses() = eventsDao.selectAllLive() @@ -56,4 +70,4 @@ class EventsRepository .also { instance = it } } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/phenoapps/intercross/data/IntercrossDatabase.kt b/app/src/main/java/org/phenoapps/intercross/data/IntercrossDatabase.kt index cacaec82..9a71b15e 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/IntercrossDatabase.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/IntercrossDatabase.kt @@ -5,24 +5,26 @@ import androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase import org.phenoapps.intercross.data.dao.* import org.phenoapps.intercross.data.migrations.MigrationV2MetaData import org.phenoapps.intercross.data.migrations.MigrationV3WishlistView +import org.phenoapps.intercross.data.migrations.MigrationV4UniqueWishType +import org.phenoapps.intercross.data.migrations.MigrationV5DropSettings +import org.phenoapps.intercross.data.migrations.MigrationV6AddMetaIcon +import org.phenoapps.intercross.data.migrations.MigrationV7ArchiveEvents import org.phenoapps.intercross.data.models.* +import kotlin.jvm.java @Database(entities = [Event::class, Parent::class, - Wishlist::class, Settings::class, PollenGroup::class, + Wishlist::class, PollenGroup::class, Meta::class, MetadataValues::class], - views = [WishlistView::class], version = 3, exportSchema = true) + views = [WishlistView::class], version = 7, exportSchema = true) @TypeConverters(Converters::class) abstract class IntercrossDatabase : RoomDatabase() { abstract fun eventsDao(): EventsDao abstract fun parentsDao(): ParentsDao abstract fun wishlistDao(): WishlistDao - abstract fun settingsDao(): SettingsDao abstract fun pollenGroupDao(): PollenGroupDao abstract fun metadataDao(): MetadataDao abstract fun metaValuesDao(): MetaValuesDao @@ -46,8 +48,12 @@ abstract class IntercrossDatabase : RoomDatabase() { return Room.databaseBuilder(ctx, IntercrossDatabase::class.java, DATABASE_NAME) .addMigrations(MigrationV2MetaData()) //v1 -> v2 migration added JSON based metadata .addMigrations(MigrationV3WishlistView()) // v2 -> v3 migration for WishlistView + .addMigrations(MigrationV4UniqueWishType()) // v3 -> v4 migration for unique wishlist type + .addMigrations(MigrationV5DropSettings(ctx)) // v4 -> v5 migration drops settings table + .addMigrations(MigrationV6AddMetaIcon()) // v5 -> v6 migration adds icon to metadata + .addMigrations(MigrationV7ArchiveEvents()) // v6 -> v7 migration adds event archiving .setJournalMode(JournalMode.TRUNCATE) //truncate mode makes it easier to export/import database w/o having to manage WAL files. .build() } } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/phenoapps/intercross/data/SettingsRepository.kt b/app/src/main/java/org/phenoapps/intercross/data/SettingsRepository.kt deleted file mode 100644 index 6d85b710..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/SettingsRepository.kt +++ /dev/null @@ -1,21 +0,0 @@ -package org.phenoapps.intercross.data - -import org.phenoapps.intercross.data.dao.SettingsDao -import org.phenoapps.intercross.data.models.Settings - -class SettingsRepository private constructor( - private val settingsDao: SettingsDao): BaseRepository(settingsDao) { - - fun getSettings() = settingsDao.getSettings() - - companion object { - @Volatile private var instance: SettingsRepository? = null - - fun getInstance(settingsDao: SettingsDao) = - instance ?: synchronized(this) { - instance ?: SettingsRepository(settingsDao) - .also { instance = it } - } - } - -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/WishlistRepository.kt b/app/src/main/java/org/phenoapps/intercross/data/WishlistRepository.kt index b59bf77f..0acb344d 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/WishlistRepository.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/WishlistRepository.kt @@ -30,6 +30,8 @@ class WishlistRepository private constructor( fun getAll() = wishlistDao.getAll() + fun getByParents(femaleId: String, maleId: String) = wishlistDao.getByParents(femaleId, maleId) + fun getWishes() = wishlistDao.getWishes() fun getAllCounts() = wishlistDao.getAllCounts() diff --git a/app/src/main/java/org/phenoapps/intercross/data/dao/EventsDao.kt b/app/src/main/java/org/phenoapps/intercross/data/dao/EventsDao.kt index f8940318..96054009 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/dao/EventsDao.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/dao/EventsDao.kt @@ -21,16 +21,19 @@ interface EventsDao : BaseDao { @Query("SELECT * FROM events WHERE events.eid == :eid") suspend fun getEvent(eid: Long?): Event - @Query("SELECT * FROM events ORDER BY date DESC") + @Query("SELECT * FROM events WHERE archived = 0 ORDER BY date DESC") fun selectAll(): LiveData> + @Query("SELECT * FROM events WHERE archived = 1 ORDER BY date DESC") + fun selectArchived(): LiveData> + @Query(""" SELECT DISTINCT x.mom, female.name as "momReadable", x.dad, male.name as "dadReadable", MAX(x.person) as "person", MAX(x.date) as "date", (SELECT COUNT(*) FROM events as y - WHERE y.mom = x.mom and y.dad = x.dad) as count + WHERE y.archived = 0 and y.mom = x.mom and y.dad = x.dad) as count FROM events as x, parents as male, parents as female - WHERE x.dad = male.codeId and x.mom = female.codeId + WHERE x.archived = 0 and x.dad = male.codeId and x.mom = female.codeId GROUP BY x.mom, "momReadable", x.dad, "dadReadable" """) fun getParentCount(): LiveData> @@ -40,7 +43,7 @@ interface EventsDao : BaseDao { x.person as "person", x.date as "date", COUNT(*) as count FROM events as x, parents as male, parents as female - WHERE x.dad = male.codeId and x.mom = female.codeId + WHERE x.archived = 0 and x.dad = male.codeId and x.mom = female.codeId GROUP BY x.mom, "momReadable", x.dad, "dadReadable", x.person, x.date """) fun getAllParents(): LiveData> @@ -79,6 +82,12 @@ interface EventsDao : BaseDao { @Query("DELETE FROM events WHERE events.eid = :eid") suspend fun deleteById(eid: Long) + @Query("UPDATE events SET archived = 1 WHERE eid IN (:ids)") + suspend fun archiveByIds(ids: List) + + @Query("UPDATE events SET archived = 0 WHERE eid IN (:ids)") + suspend fun unarchiveByIds(ids: List) + @Query("SELECT DISTINCT x.codeId FROM events as x WHERE x.codeId = :code") fun getEventsWithCode(code: String): List @@ -103,4 +112,4 @@ interface EventsDao : BaseDao { @Insert(onConflict = OnConflictStrategy.IGNORE) fun insertEvent(event: Event): Long -} \ No newline at end of file +} diff --git a/app/src/main/java/org/phenoapps/intercross/data/dao/SettingsDao.kt b/app/src/main/java/org/phenoapps/intercross/data/dao/SettingsDao.kt deleted file mode 100644 index 589381b9..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/dao/SettingsDao.kt +++ /dev/null @@ -1,13 +0,0 @@ -package org.phenoapps.intercross.data.dao - -import androidx.lifecycle.LiveData -import androidx.room.Dao -import androidx.room.Query -import org.phenoapps.intercross.data.models.Settings - -@Dao -interface SettingsDao: BaseDao { - - @Query("SELECT * FROM settings LIMIT 1") - fun getSettings(): LiveData -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/dao/WishlistDao.kt b/app/src/main/java/org/phenoapps/intercross/data/dao/WishlistDao.kt index dd0f4197..49a8d96f 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/dao/WishlistDao.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/dao/WishlistDao.kt @@ -2,6 +2,8 @@ package org.phenoapps.intercross.data.dao import androidx.lifecycle.LiveData import androidx.room.Dao +import androidx.room.Insert +import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction import org.phenoapps.intercross.data.models.Wishlist @@ -13,6 +15,9 @@ interface WishlistDao : BaseDao { @Query("SELECT * FROM wishlist ORDER BY wishlist.wishMin DESC") fun getAll(): LiveData> + @Query("SELECT * FROM wishlist WHERE femaleDbId = :femaleId AND maleDbId = :maleId") + fun getByParents(femaleId: String, maleId: String): LiveData> + @Query("SELECT * FROM wishlistView ORDER BY wishlistView.wishProgress DESC") fun getAllCounts(): LiveData> @@ -52,4 +57,7 @@ interface WishlistDao : BaseDao { @Transaction @Query("DELETE FROM wishlist") fun drop() + + @Insert(onConflict = OnConflictStrategy.REPLACE) + override fun insert(vararg items: Wishlist) } \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/fts/CrossesDatabase.kt b/app/src/main/java/org/phenoapps/intercross/data/fts/CrossesDatabase.kt deleted file mode 100644 index 39a2683a..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/fts/CrossesDatabase.kt +++ /dev/null @@ -1,42 +0,0 @@ -package org.phenoapps.intercross.data.fts - -import android.content.Context -import androidx.room.Room -import androidx.room.RoomDatabase -import org.phenoapps.intercross.data.fts.dao.CrossesDao -import org.phenoapps.intercross.data.fts.dao.CrossesFtsDao -import org.phenoapps.intercross.data.fts.tables.Crosses -import org.phenoapps.intercross.data.fts.tables.CrossesFts - -@androidx.room.Database(entities = [Crosses::class, CrossesFts::class], version = 1, - exportSchema = true) -abstract class CrossesDatabase : RoomDatabase() { - - abstract fun crossesDao(): CrossesDao - abstract fun crossesFtsDao(): CrossesFtsDao - - companion object { - - fun getInstance(ctx: Context): CrossesDatabase { - - return buildDatabase(ctx) - - } - - fun getInMemoryInstance(ctx: Context): CrossesDatabase { - - return buildInMemoryDatabase(ctx) - - } - - private fun buildInMemoryDatabase(ctx: Context): CrossesDatabase { - return Room.inMemoryDatabaseBuilder(ctx, CrossesDatabase::class.java) - .build() - } - - private fun buildDatabase(ctx: Context): CrossesDatabase { - return Room.databaseBuilder(ctx, CrossesDatabase::class.java, "crosses_fts.db") - .build() - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/fts/dao/CrossesDao.kt b/app/src/main/java/org/phenoapps/intercross/data/fts/dao/CrossesDao.kt deleted file mode 100644 index 75a1b5ee..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/fts/dao/CrossesDao.kt +++ /dev/null @@ -1,18 +0,0 @@ -package org.phenoapps.intercross.data.fts.dao - -import androidx.room.* -import org.phenoapps.intercross.data.fts.tables.Crosses - -@Dao -interface CrossesDao { - - @Update - fun update(vararg e: Crosses): Int - - @Insert(onConflict = OnConflictStrategy.REPLACE) - fun insert(vararg items: Crosses) - - @Delete - fun delete(vararg e: Crosses): Int - -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/fts/dao/CrossesFtsDao.kt b/app/src/main/java/org/phenoapps/intercross/data/fts/dao/CrossesFtsDao.kt deleted file mode 100644 index cbbd85ad..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/fts/dao/CrossesFtsDao.kt +++ /dev/null @@ -1,20 +0,0 @@ -package org.phenoapps.intercross.data.fts.dao - -import androidx.lifecycle.LiveData -import androidx.room.* -import org.phenoapps.intercross.data.fts.models.RankedCrosses - -@Dao -interface CrossesFtsDao { - - @Query(""" - SELECT crosses.*, matchinfo(crosses_fts, "pcx") as "matchInfo" - FROM crosses - JOIN crosses_fts ON crosses_fts.rowid = crosses.rowid - WHERE crosses_fts MATCH :query - """) - fun search(query: String): LiveData> - - @Query("""INSERT INTO crosses_fts (crosses_fts) VALUES ("rebuild")""") - fun rebuild() -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/fts/models/RankedCrosses.kt b/app/src/main/java/org/phenoapps/intercross/data/fts/models/RankedCrosses.kt deleted file mode 100644 index d7fc23bf..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/fts/models/RankedCrosses.kt +++ /dev/null @@ -1,31 +0,0 @@ -package org.phenoapps.intercross.data.fts.models - -import androidx.room.Embedded -import org.phenoapps.intercross.data.fts.tables.Crosses - -/** - * Return object of the search query. Match info contains FTS data for matching indices to query terms. - */ -data class RankedCrosses( - @Embedded - val cross: Crosses, - val matchInfo: ByteArray -) { - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as RankedCrosses - - if (cross != other.cross) return false - if (!matchInfo.contentEquals(other.matchInfo)) return false - - return true - } - - override fun hashCode(): Int { - var result = cross.hashCode() - result = 31 * result + matchInfo.contentHashCode() - return result - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/fts/tables/Crosses.kt b/app/src/main/java/org/phenoapps/intercross/data/fts/tables/Crosses.kt deleted file mode 100644 index e4a0225a..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/fts/tables/Crosses.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.phenoapps.intercross.data.fts.tables - -import androidx.room.Entity -import androidx.room.PrimaryKey - -@Entity(tableName = "crosses") -data class Crosses( - @PrimaryKey - val rowid: Long, - val crossId: String, - val femaleId: String, - val maleId: String, - val femaleName: String?, - val maleName: String?, - val date: String, -) \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/fts/tables/CrossesFts.kt b/app/src/main/java/org/phenoapps/intercross/data/fts/tables/CrossesFts.kt deleted file mode 100644 index 3f2fb7ec..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/fts/tables/CrossesFts.kt +++ /dev/null @@ -1,15 +0,0 @@ -package org.phenoapps.intercross.data.fts.tables - -import androidx.room.Entity -import androidx.room.Fts4 - -@Entity(tableName = "crosses_fts") -@Fts4(contentEntity = Crosses::class) -data class CrossesFts( - val crossId: String, - val femaleId: String, - val maleId: String, - val femaleName: String?, - val maleName: String?, - val date: String, -) \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/fts/viewmodels/CrossesViewModel.kt b/app/src/main/java/org/phenoapps/intercross/data/fts/viewmodels/CrossesViewModel.kt deleted file mode 100644 index 308980a8..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/fts/viewmodels/CrossesViewModel.kt +++ /dev/null @@ -1,34 +0,0 @@ -package org.phenoapps.intercross.data.fts.viewmodels - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.liveData -import androidx.lifecycle.viewModelScope -import kotlinx.coroutines.withContext -import org.phenoapps.intercross.data.dao.EventsDao -import org.phenoapps.intercross.data.fts.dao.CrossesDao -import org.phenoapps.intercross.data.fts.dao.CrossesFtsDao -import org.phenoapps.intercross.data.fts.models.RankedCrosses -import org.phenoapps.intercross.data.fts.tables.Crosses -import org.phenoapps.intercross.data.models.Event - -class CrossesViewModel( - private val crossesDao: CrossesDao, - private val fts: CrossesFtsDao -): ViewModel() { - - private fun sanitizeQuery(query: String) = query.replace("\"", "\"\"") - - fun searchResult(query: String) = fts.search("*${sanitizeQuery(query)}*") - - //replace all - in timestamps with _ because it is an fts operator - //inserts crossess into the fts database, links events with their parents (whether commutative or not) - fun insert(commutative: Boolean, events: List, parents: List) { - crossesDao.insert(*events.map { cross -> - val parent = if (!commutative) parents.find { it.dad == cross.maleObsUnitDbId && it.mom == cross.femaleObsUnitDbId } - else parents.find { (it.dad == cross.maleObsUnitDbId && it.mom == cross.femaleObsUnitDbId) - || (it.mom == cross.maleObsUnitDbId && it.dad == cross.femaleObsUnitDbId)} - Crosses(cross.id ?: -1L, cross.eventDbId, cross.femaleObsUnitDbId, cross.maleObsUnitDbId, - parent?.momReadable, parent?.dadReadable, cross.timestamp.replace("-", "_")) - }.toTypedArray()) - } -} diff --git a/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV4UniqueWishType.kt b/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV4UniqueWishType.kt new file mode 100644 index 00000000..1787867f --- /dev/null +++ b/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV4UniqueWishType.kt @@ -0,0 +1,31 @@ +package org.phenoapps.intercross.data.migrations + +import android.database.sqlite.SQLiteException +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class MigrationV4UniqueWishType : Migration(3, 4) { + + override fun migrate(db: SupportSQLiteDatabase) { + with(db) { + try { + beginTransaction() + + createWishTypeIndex() + + setTransactionSuccessful() + } catch (e: SQLiteException) { + e.printStackTrace() + } finally { + endTransaction() + } + } + } + + private fun SupportSQLiteDatabase.createWishTypeIndex() { + execSQL( + "CREATE UNIQUE INDEX IF NOT EXISTS index_intercross_maleDbId_femaleDbId_wishType " + + "ON wishlist(maleDbId, femaleDbId, wishType)" + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV5DropSettings.kt b/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV5DropSettings.kt new file mode 100644 index 00000000..38bcc365 --- /dev/null +++ b/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV5DropSettings.kt @@ -0,0 +1,44 @@ +package org.phenoapps.intercross.data.migrations + +import android.content.Context +import androidx.preference.PreferenceManager +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase +import org.phenoapps.intercross.util.CrossIdSettings + +/** + * Migration from v4 to v5: moves Settings table data into SharedPreferences + * and drops the settings table. + */ +class MigrationV5DropSettings(private val context: Context) : Migration(4, 5) { + + override fun migrate(db: SupportSQLiteDatabase) { + // Read existing settings before dropping + val prefs = PreferenceManager.getDefaultSharedPreferences(context) + try { + db.query("SELECT * FROM settings LIMIT 1").use { cursor -> + if (cursor.moveToFirst()) { + val isPattern = cursor.getInt(cursor.getColumnIndexOrThrow("isPattern")) == 1 + val isUUID = cursor.getInt(cursor.getColumnIndexOrThrow("isUUID")) == 1 + val isAutoIncrement = cursor.getInt(cursor.getColumnIndexOrThrow("isAutoIncrement")) == 1 + val pad = cursor.getInt(cursor.getColumnIndexOrThrow("pad")) + val number = cursor.getInt(cursor.getColumnIndexOrThrow("number")) + val prefix = cursor.getString(cursor.getColumnIndexOrThrow("prefix")) ?: "" + val suffix = cursor.getString(cursor.getColumnIndexOrThrow("suffix")) ?: "" + CrossIdSettings.save( + prefs, + CrossIdSettings(isPattern, isUUID, isAutoIncrement, pad, number, prefix, suffix) + ) + } + } + } catch (_: Exception) { + // If the table doesn't exist or is empty, just proceed with defaults + } + db.execSQL("DROP TABLE IF EXISTS settings") + + // Fix stale index from MigrationV4 that has wrong column order/name + db.execSQL("DROP INDEX IF EXISTS index_intercross_maleDbId_femaleDbId_wishType") + // Ensure the correct index exists + db.execSQL("CREATE UNIQUE INDEX IF NOT EXISTS index_wishlist_femaleDbId_maleDbId_wishType ON wishlist(femaleDbId, maleDbId, wishType)") + } +} diff --git a/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV6AddMetaIcon.kt b/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV6AddMetaIcon.kt new file mode 100644 index 00000000..389f347c --- /dev/null +++ b/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV6AddMetaIcon.kt @@ -0,0 +1,27 @@ +package org.phenoapps.intercross.data.migrations + +import android.database.sqlite.SQLiteException +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class MigrationV6AddMetaIcon : Migration(5, 6) { + + override fun migrate(db: SupportSQLiteDatabase) { + with(db) { + try { + beginTransaction() + + execSQL("ALTER TABLE metadata ADD COLUMN icon TEXT") + execSQL("UPDATE metadata SET icon = '🍎' WHERE LOWER(property) = 'fruits'") + execSQL("UPDATE metadata SET icon = '🌸' WHERE LOWER(property) = 'flowers'") + execSQL("UPDATE metadata SET icon = '🌱' WHERE LOWER(property) = 'seeds'") + + setTransactionSuccessful() + } catch (e: SQLiteException) { + e.printStackTrace() + } finally { + endTransaction() + } + } + } +} diff --git a/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV7ArchiveEvents.kt b/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV7ArchiveEvents.kt new file mode 100644 index 00000000..0cf6a437 --- /dev/null +++ b/app/src/main/java/org/phenoapps/intercross/data/migrations/MigrationV7ArchiveEvents.kt @@ -0,0 +1,24 @@ +package org.phenoapps.intercross.data.migrations + +import android.database.sqlite.SQLiteException +import androidx.room.migration.Migration +import androidx.sqlite.db.SupportSQLiteDatabase + +class MigrationV7ArchiveEvents : Migration(6, 7) { + + override fun migrate(db: SupportSQLiteDatabase) { + with(db) { + try { + beginTransaction() + + execSQL("ALTER TABLE events ADD COLUMN archived INTEGER NOT NULL DEFAULT 0") + + setTransactionSuccessful() + } catch (e: SQLiteException) { + e.printStackTrace() + } finally { + endTransaction() + } + } + } +} diff --git a/app/src/main/java/org/phenoapps/intercross/data/models/BaseParent.kt b/app/src/main/java/org/phenoapps/intercross/data/models/BaseParent.kt index f7c778b2..9ee738ce 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/models/BaseParent.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/models/BaseParent.kt @@ -10,13 +10,27 @@ open class BaseParent: BaseTable() { class DiffCallback : DiffUtil.ItemCallback() { override fun areItemsTheSame(oldItem: BaseParent, newItem: BaseParent): Boolean { - return oldItem.hashCode() == newItem.hashCode() + val oldCode = (oldItem as? Parent)?.codeId ?: (oldItem as? PollenGroup)?.codeId + val newCode = (newItem as? Parent)?.codeId ?: (newItem as? PollenGroup)?.codeId + return oldCode == newCode } override fun areContentsTheSame(oldItem: BaseParent, newItem: BaseParent): Boolean { - return oldItem.hashCode() == newItem.hashCode() + val oldSelected = (oldItem as? Parent)?.selected ?: (oldItem as? PollenGroup)?.selected + val newSelected = (newItem as? Parent)?.selected ?: (newItem as? PollenGroup)?.selected + return oldItem.hashCode() == newItem.hashCode() && oldSelected == newSelected } } } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + return true + } + + override fun hashCode(): Int { + return javaClass.hashCode() + } } diff --git a/app/src/main/java/org/phenoapps/intercross/data/models/Event.kt b/app/src/main/java/org/phenoapps/intercross/data/models/Event.kt index 096f46e7..1b89611e 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/models/Event.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/models/Event.kt @@ -38,6 +38,9 @@ data class Event( var sex: Int = -1, //by default sex is unknown + @ColumnInfo(name = "archived") + var archived: Boolean = false, + @ColumnInfo(name = "eid") @PrimaryKey(autoGenerate = true) var id: Long? = null): BaseTable() { @@ -91,4 +94,4 @@ data class Event( return "$eventDbId,$femaleObsUnitDbId,$maleObsUnitDbId::$group::$malesRepr,$timestamp,$person,$experiment,${CrossType.POLY}" } -} \ No newline at end of file +} diff --git a/app/src/main/java/org/phenoapps/intercross/data/models/Meta.kt b/app/src/main/java/org/phenoapps/intercross/data/models/Meta.kt index 789fbadd..6a3b4c5f 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/models/Meta.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/models/Meta.kt @@ -20,6 +20,9 @@ data class Meta( @ColumnInfo(name = "defaultValue") var defaultValue: Int? = null, + @ColumnInfo(name = "icon") + var icon: String? = null, + @ColumnInfo(name = "mid") @PrimaryKey(autoGenerate = true) var id: Long? = null): BaseTable() \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/models/Settings.kt b/app/src/main/java/org/phenoapps/intercross/data/models/Settings.kt deleted file mode 100644 index 033e2f0e..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/models/Settings.kt +++ /dev/null @@ -1,30 +0,0 @@ -package org.phenoapps.intercross.data.models - -import androidx.room.ColumnInfo -import androidx.room.Entity -import androidx.room.Ignore -import androidx.room.PrimaryKey - -@Entity(tableName = "settings") -data class Settings ( - - var isPattern: Boolean = false, - var isUUID: Boolean = true, - var startFrom: Boolean = false, - var isAutoIncrement: Boolean = false, - var pad: Int = 0, - var number: Int = 1, - var prefix: String = String(), - var suffix: String = String(), - var allowBlank: Boolean = false, - var order: Int = 0, - var collectData: Boolean = false -) { - - @ColumnInfo(name = "sid") - @PrimaryKey(autoGenerate = true) - var id: Long? = null - - @Ignore - val pattern = prefix + number.toString().padStart(pad, '0') + suffix -} diff --git a/app/src/main/java/org/phenoapps/intercross/data/models/Wishlist.kt b/app/src/main/java/org/phenoapps/intercross/data/models/Wishlist.kt index 8fd4541d..231cf94a 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/models/Wishlist.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/models/Wishlist.kt @@ -2,9 +2,13 @@ package org.phenoapps.intercross.data.models import androidx.room.ColumnInfo import androidx.room.Entity +import androidx.room.Index import androidx.room.PrimaryKey -@Entity(tableName = "wishlist") +@Entity(tableName = "wishlist", + indices = [ + Index(value = ["femaleDbId", "maleDbId", "wishType"], unique = true) + ]) data class Wishlist(var femaleDbId: String, var maleDbId: String, var femaleName: String=femaleDbId, diff --git a/app/src/main/java/org/phenoapps/intercross/data/viewmodels/EventListViewModel.kt b/app/src/main/java/org/phenoapps/intercross/data/viewmodels/EventListViewModel.kt index b44c1398..63fb6aa0 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/viewmodels/EventListViewModel.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/viewmodels/EventListViewModel.kt @@ -28,6 +28,24 @@ class EventListViewModel(private val eventRepo: EventsRepository): BaseViewModel } } + fun archiveByIds(ids: List) { + + viewModelScope.launch { + + eventRepo.archiveByIds(ids) + + } + } + + fun unarchiveByIds(ids: List) { + + viewModelScope.launch { + + eventRepo.unarchiveByIds(ids) + + } + } + fun getRowid(e: Event): Long = eventRepo.getRowid(e) fun insert(item: Event): Long = eventRepo.insert(item) @@ -38,5 +56,7 @@ class EventListViewModel(private val eventRepo: EventsRepository): BaseViewModel val events = eventRepo.selectAll() + val archivedEvents = eventRepo.selectArchived() + val metadata = eventRepo.getMetadata() } diff --git a/app/src/main/java/org/phenoapps/intercross/data/viewmodels/LabelTemplateViewModel.kt b/app/src/main/java/org/phenoapps/intercross/data/viewmodels/LabelTemplateViewModel.kt new file mode 100644 index 00000000..dd5ca67e --- /dev/null +++ b/app/src/main/java/org/phenoapps/intercross/data/viewmodels/LabelTemplateViewModel.kt @@ -0,0 +1,453 @@ +package org.phenoapps.intercross.data.viewmodels + +import android.content.Context +import android.content.SharedPreferences +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import androidx.core.content.edit +import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope +import dagger.hilt.android.lifecycle.HiltViewModel +import dagger.hilt.android.qualifiers.ApplicationContext +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import org.phenoapps.intercross.R +import org.phenoapps.intercross.util.KeyUtil +import org.phenoapps.intercross.util.LabelMediaType +import org.phenoapps.intercross.util.LabelPrinterDpiDetector +import org.phenoapps.intercross.util.LabelTemplateConfig +import org.phenoapps.intercross.util.LabelTemplateStore +import org.phenoapps.intercross.util.LabelTemplateType +import org.phenoapps.intercross.util.ZplStringReplacer +import org.phenoapps.intercross.util.ZplFormatter +import org.phenoapps.intercross.util.ZplTemplate +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import java.util.Locale +import javax.inject.Inject + +/** + * Immutable UI state snapshot for the label template editor screen. + * + * All fields are serializable-friendly types; non-UI helpers such as preview bitmaps + * are provided to the Compose UI for rendering only. + * + */ +data class LabelTemplateUiState( + val config: LabelTemplateConfig = LabelTemplateConfig(), + val savedTemplates: List = emptyList(), + val selectedTemplateName: String? = null, + val activeTemplateName: String = "", + val deviceName: String = "", + val printerDetails: String = "", + val isDetectingDpi: Boolean = false, + val previewBitmap: Bitmap? = null, + val isRenderingPreview: Boolean = false, + val previewMessage: String? = null, + val message: String? = null, +) { + val zplPreview: String + get() = config.sanitizedForSave().toZpl() +} + +@HiltViewModel +class LabelTemplateViewModel @Inject constructor( + @ApplicationContext private val context: Context, + private val prefs: SharedPreferences, +) : ViewModel() { + + private val keyUtil = KeyUtil(context) + private val previewClient = OkHttpClient() + + private val _uiState = MutableStateFlow(createInitialState()) + val uiState: StateFlow = _uiState.asStateFlow() + + init { + detectDpi() + } + + fun updateConfig(config: LabelTemplateConfig) { + val previousConfig = _uiState.value.config + if (previousConfig.mediaType != config.mediaType) { + updatePrinterMediaType(config.media) + } + if (previousConfig.widthInches != config.widthInches) { + updatePrinterWidth(config.widthDots) + } + if (previousConfig.heightInches != config.heightInches) { + updatePrinterHeight(config.heightDots) + } + + if (previousConfig.type != config.type) { + // When switching types, auto-select the first available template of the new type + val firstOfNewType = _uiState.value.savedTemplates.firstOrNull { it.type == config.type } + if (firstOfNewType != null) { + _uiState.update { + it.copy( + config = firstOfNewType, + selectedTemplateName = firstOfNewType.name, + activeTemplateName = activeTemplateNameFor(firstOfNewType.type), + previewBitmap = null, + previewMessage = null, + message = null, + ) + } + return + } + } + + val updatedConfig = if ( + previousConfig.type != config.type && + ZplStringReplacer.hasPlaceholdersForOtherType(config.toZpl(), config.type) + ) { + config.copy(rawZpl = null) + } else { + config + } + _uiState.update { + it.copy( + config = updatedConfig, + selectedTemplateName = selectedTemplateNameFor(updatedConfig), + activeTemplateName = activeTemplateNameFor(updatedConfig.type), + previewBitmap = null, + previewMessage = null, + message = null, + ) + } + } + + fun selectSavedTemplate(name: String, type: LabelTemplateType) { + val template = _uiState.value.savedTemplates.firstOrNull { it.name == name && it.type == type } + ?: ZplTemplate.getDefaultTemplates(context) + .firstOrNull { it.displayName == name && it.type == type } + ?.let { + LabelTemplateConfig( + name = it.displayName, + rawZpl = it.zplCode, + labelType = it.type.name, + ) + } + ?: return + _uiState.update { + it.copy( + config = template, + selectedTemplateName = template.name, + activeTemplateName = activeTemplateNameFor(template.type), + previewBitmap = null, + previewMessage = null, + message = null, + ) + } + } + + fun saveCurrentTemplate() { + val saved = _uiState.value.config.sanitizedForSave() + val templates = LabelTemplateStore.upsert(prefs, keyUtil.labelTemplatesKey, saved) + setActiveTemplate(saved) + _uiState.update { + it.copy( + config = saved, + savedTemplates = templates.withBuiltInZpl(), + selectedTemplateName = saved.name, + activeTemplateName = saved.name, + previewBitmap = null, + previewMessage = null, + message = context.getString(R.string.label_template_saved_selected, saved.name), + ) + } + } + + fun useCurrentTemplate() { + val active = _uiState.value.config.sanitizedForSave() + setActiveTemplate(active) + _uiState.update { + it.copy( + config = active, + activeTemplateName = active.name, + previewBitmap = null, + previewMessage = null, + message = context.getString(R.string.label_template_selected, active.name), + ) + } + } + + fun importRawZpl(rawZpl: String, displayName: String?) { + val importedName = displayName + ?.substringAfterLast("/") + ?.substringBeforeLast(".") + ?.takeIf { it.isNotBlank() } + ?: context.getString(R.string.imported_zpl_name) + _uiState.update { + it.copy( + config = it.config.copy(name = importedName, rawZpl = ZplFormatter.readable(rawZpl)), + selectedTemplateName = importedName, + previewBitmap = null, + previewMessage = null, + message = context.getString(R.string.imported_zpl_message), + ) + } + } + + fun renderPreview() { + if (_uiState.value.isRenderingPreview) return + + val config = _uiState.value.config.sanitizedForSave() + _uiState.update { + it.copy( + isRenderingPreview = true, + previewBitmap = null, + previewMessage = null, + message = null, + ) + } + + viewModelScope.launch { + val result = withContext(Dispatchers.IO) { + runCatching { renderLabelaryPreview(config) } + } + + _uiState.update { state -> + result.fold( + onSuccess = { bitmap -> + state.copy( + previewBitmap = bitmap, + isRenderingPreview = false, + previewMessage = null, + ) + }, + onFailure = { + state.copy( + isRenderingPreview = false, + previewMessage = context.getString(R.string.label_preview_failed), + ) + }, + ) + } + } + } + + fun clearMessage() { + _uiState.update { it.copy(message = null) } + } + + fun detectDpi() { + val deviceName = prefs.getString(keyUtil.printerDeviceNameKey, "").orEmpty() + _uiState.update { + it.copy( + deviceName = deviceName, + isDetectingDpi = true, + message = if (deviceName.isBlank()) { + context.getString(R.string.choose_printer_before_dpi) + } else { + null + }, + ) + } + + if (deviceName.isBlank()) { + _uiState.update { it.copy(isDetectingDpi = false) } + return + } + + viewModelScope.launch { + val result = withContext(Dispatchers.IO) { + LabelPrinterDpiDetector.detect(context, deviceName) + } + + _uiState.update { state -> + val updatedConfig = result.dpi?.let { dpi -> + var config = state.config.copy( + dpi = dpi, + ) + result.printWidthDots?.let { widthDots -> + config = config.copy(widthInches = widthDots / dpi.toFloat()) + } + result.labelLengthDots?.let { heightDots -> + config = config.copy(heightInches = heightDots / dpi.toFloat()) + } + result.mediaType?.let { mType -> + config = config.copy(mediaType = LabelMediaType.fromSgd(mType).name) + } + config + } ?: state.config + state.copy( + config = updatedConfig, + printerDetails = result.summary(context), + isDetectingDpi = false, + message = result.message, + ) + } + } + } + + fun updatePrinterMediaType(mediaType: LabelMediaType) { + val deviceName = _uiState.value.deviceName + if (deviceName.isBlank()) return + + viewModelScope.launch(Dispatchers.IO) { + LabelPrinterDpiDetector.update(context, deviceName, mediaType) + } + } + + fun updatePrinterWidth(widthDots: Float) { + val deviceName = _uiState.value.deviceName + if (deviceName.isBlank()) return + + viewModelScope.launch(Dispatchers.IO) { + LabelPrinterDpiDetector.updateWidth(context, deviceName, widthDots) + } + } + + fun updatePrinterHeight(heightDots: Float) { + val deviceName = _uiState.value.deviceName + if (deviceName.isBlank()) return + + viewModelScope.launch(Dispatchers.IO) { + LabelPrinterDpiDetector.updateHeight(context, deviceName, heightDots) + } + } + + private fun setActiveTemplate(template: LabelTemplateConfig) { + prefs.edit { + putString(keyUtil.zplTemplateKey, template.name) + putString(keyUtil.zplCodeKey, template.toZpl()) + when (template.type) { + LabelTemplateType.CROSS -> { + putString(keyUtil.crossZplTemplateKey, template.name) + putString(keyUtil.crossZplCodeKey, template.toZpl()) + } + LabelTemplateType.PARENT -> { + putString(keyUtil.parentZplTemplateKey, template.name) + putString(keyUtil.parentZplCodeKey, template.toZpl()) + } + } + } + } + + private fun selectedTemplateNameFor(config: LabelTemplateConfig): String? { + return config.name.takeIf { name -> + _uiState.value.savedTemplates.any { template -> + template.type == config.type && template.name == name + } + } + } + + private fun createInitialState(): LabelTemplateUiState { + val savedTemplates = LabelTemplateStore.load(prefs, keyUtil.labelTemplatesKey) + val allTemplates = savedTemplates.withBuiltInZpl() + val activeTemplateName = prefs.getString(keyUtil.zplTemplateKey, "").orEmpty() + val activeZpl = prefs.getString(keyUtil.zplCodeKey, "").orEmpty() + val defaultName = context.getString(R.string.label_designer_default_name) + val noneName = context.getString(R.string.none) + val initialConfig = allTemplates.firstOrNull { it.name == activeTemplateName } + ?: activeZpl.takeIf { it.isNotBlank() }?.let { + LabelTemplateConfig( + name = activeTemplateName + .takeUnless { name -> name.isBlank() || name.equals(noneName, ignoreCase = true) } + ?: defaultName, + rawZpl = it, + ) + } + ?: allTemplates.firstOrNull { it.type == LabelTemplateType.CROSS } + ?: allTemplates.firstOrNull() + ?: LabelTemplateConfig(name = defaultName) + + return LabelTemplateUiState( + config = initialConfig, + savedTemplates = allTemplates, + selectedTemplateName = initialConfig.name, + activeTemplateName = activeTemplateNameFor(initialConfig.type).ifBlank { activeTemplateName }, + deviceName = prefs.getString(keyUtil.printerDeviceNameKey, "").orEmpty(), + ) + } + + private fun builtInZpl(): List { + return ZplTemplate.getDefaultTemplates(context).map { + LabelTemplateConfig( + name = it.displayName, + rawZpl = it.zplCode, + labelType = it.type.name, + ) + } + } + + private fun List.withBuiltInZpl(): List { + return (this + builtInZpl()) + .distinctBy { "${it.type.name}:${it.name.lowercase()}" } + .sortedBy { it.name.lowercase() } + } + + private fun activeTemplateNameFor(type: LabelTemplateType): String { + return when (type) { + LabelTemplateType.CROSS -> prefs.getString(keyUtil.crossZplTemplateKey, "").orEmpty() + LabelTemplateType.PARENT -> prefs.getString(keyUtil.parentZplTemplateKey, "").orEmpty() + } + } + + private fun renderLabelaryPreview(config: LabelTemplateConfig): Bitmap { + val url = "https://api.labelary.com/v1/printers/${config.dpi.toDpmm()}dpmm" + + "/labels/${config.widthInches.toLabelarySize()}x${config.heightInches.toLabelarySize()}/0/" + val request = Request.Builder() + .url(url) + .addHeader("Accept", "image/png") + .post(previewZpl(config).toRequestBody("application/x-www-form-urlencoded".toMediaType())) + .build() + + previewClient.newCall(request).execute().use { response -> + if (!response.isSuccessful) error("Preview request failed: ${response.code}") + val bytes = response.body?.bytes() ?: ByteArray(0) + return BitmapFactory.decodeByteArray(bytes, 0, bytes.size) + ?: error("Preview response was not an image") + } + } + + private fun previewZpl(config: LabelTemplateConfig): String { + val replacements = when (config.type) { + LabelTemplateType.CROSS -> mapOf( + "{crossId}" to "CROSS-001", + "{readableName}" to "Female A x Male B", + "{femaleId}" to "female-id-123", + "{maleId}" to "male-id-456", + "{date}" to "2026-05-18", + "{timestamp}" to "2026-05-18_14-46-00", + "{person}" to "Collector", + "{experiment}" to "Experiment", + "{type}" to "Biparental", + "{qrCrossId}" to "QA,CROSS-001", + "{femaleName}" to "FEMALE-A", + "{maleName}" to "MALE-B" + ) + LabelTemplateType.PARENT -> mapOf( + "{parentId}" to "PARENT-001", + "{parentName}" to "Parent A", + "{parentType}" to "female", + ) + } + + return replacements.entries.fold(config.toZpl()) { zpl, (placeholder, value) -> + zpl.replace(placeholder, value) + } + } + + private fun Int.toDpmm(): Int { + return when { + this >= 600 -> 24 + this >= 300 -> 12 + this >= 203 -> 8 + else -> 6 + } + } + + private fun Float.toLabelarySize(): String { + return String.format(Locale.US, "%.2f", this) + .trimEnd('0') + .trimEnd('.') + } +} diff --git a/app/src/main/java/org/phenoapps/intercross/data/viewmodels/SettingsViewModel.kt b/app/src/main/java/org/phenoapps/intercross/data/viewmodels/SettingsViewModel.kt deleted file mode 100644 index c4af1ee2..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/viewmodels/SettingsViewModel.kt +++ /dev/null @@ -1,10 +0,0 @@ -package org.phenoapps.intercross.data.viewmodels - -import org.phenoapps.intercross.data.SettingsRepository -import org.phenoapps.intercross.data.models.Settings - -class SettingsViewModel internal constructor(repo: SettingsRepository) : BaseViewModel(repo) { - - val settings = repo.getSettings() - -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/data/viewmodels/WishlistViewModel.kt b/app/src/main/java/org/phenoapps/intercross/data/viewmodels/WishlistViewModel.kt index b61568fd..6c409a43 100644 --- a/app/src/main/java/org/phenoapps/intercross/data/viewmodels/WishlistViewModel.kt +++ b/app/src/main/java/org/phenoapps/intercross/data/viewmodels/WishlistViewModel.kt @@ -27,6 +27,8 @@ class WishlistViewModel internal constructor(private val repo: WishlistRepositor val commutativeCrossblock = repo.getCommutativeCrossblock() + fun getByParents(femaleId: String, maleId: String) = repo.getByParents(femaleId, maleId) + fun deleteAll() { viewModelScope.launch { diff --git a/app/src/main/java/org/phenoapps/intercross/data/viewmodels/factory/SettingsViewModelFactory.kt b/app/src/main/java/org/phenoapps/intercross/data/viewmodels/factory/SettingsViewModelFactory.kt deleted file mode 100644 index 552a880e..00000000 --- a/app/src/main/java/org/phenoapps/intercross/data/viewmodels/factory/SettingsViewModelFactory.kt +++ /dev/null @@ -1,16 +0,0 @@ -package org.phenoapps.intercross.data.viewmodels.factory - -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider -import org.phenoapps.intercross.data.SettingsRepository -import org.phenoapps.intercross.data.viewmodels.SettingsViewModel - -class SettingsViewModelFactory(private val repo: SettingsRepository -) : ViewModelProvider.Factory { - - @SuppressWarnings("UNCHECKED_CAST") - override fun create(modelClass: Class): T { - - return SettingsViewModel(repo) as T - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/dialogs/FileExploreDialogFragment.kt b/app/src/main/java/org/phenoapps/intercross/dialogs/FileExploreDialogFragment.kt deleted file mode 100644 index bfa59304..00000000 --- a/app/src/main/java/org/phenoapps/intercross/dialogs/FileExploreDialogFragment.kt +++ /dev/null @@ -1,218 +0,0 @@ -package org.phenoapps.intercross.dialogs - -import org.phenoapps.intercross.adapters.FileAdapter -import android.app.AlertDialog -import android.app.Dialog -import android.net.Uri -import android.os.Bundle -import android.view.View -import android.widget.ProgressBar -import androidx.documentfile.provider.DocumentFile -import androidx.fragment.app.DialogFragment -import androidx.lifecycle.lifecycleScope -import androidx.recyclerview.widget.LinearLayoutManager -import androidx.recyclerview.widget.RecyclerView -import org.phenoapps.intercross.R -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.MainScope -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext -import androidx.core.net.toUri - -class FileExploreDialogFragment : DialogFragment(), CoroutineScope by MainScope() { - - companion object { - const val TAG = "FileExploreDialogFragment" - } - - private var mainListView: RecyclerView? = null - private var progressBar: ProgressBar? = null - - // Stores names of traversed directories - private val str = ArrayList() - - // Check if the first level of the directory structure is the one showing - private var firstLvl = true - private var fileList = mutableListOf() - private var chosenFile: String? = null - - private var onFileSelectedListener: ((Uri) -> Unit)? = null - - // updates whenever we traverse a directory, loads the current level's files - private var path: DocumentFile? = null - - private var exclude: Array? = null - private var include: Array? = null - - // Wrapper class to hold file data - class Item(var file: String, var isDir: Boolean, var icon: Int) { - override fun toString(): String { - return file - } - } - - private val mComparator = java.util.Comparator { f1: Item, f2: Item -> - if (f1.isDir && !f2.isDir) { - // Directory before non-directory - return@Comparator -1 - } else if (!f1.isDir && f2.isDir) { - // Non-directory after directory - return@Comparator 1 - } else { - // Alphabetic order otherwise - return@Comparator f1.file.compareTo(f2.file, ignoreCase = true) - } - } - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val inflater = activity?.layoutInflater - val view = inflater?.inflate(R.layout.dialog_file_explorer, null) - - mainListView = view?.findViewById(R.id.fileRecyclerView) - progressBar = view?.findViewById(R.id.progressBar) - - val fileUri = arguments?.getString(getString(R.string.path))?.toUri() - ?: return showPathError() - - path = DocumentFile.fromTreeUri(requireContext(), fileUri) - exclude = arguments?.getStringArray(getString(R.string.exclude)) - include = arguments?.getStringArray(getString(R.string.include)) - val dialogTitle = arguments?.getString(getString(R.string.dialog_title)) - - val builder = AlertDialog.Builder(context) - .setTitle(dialogTitle) - .setView(view) - - if (path != null) { - loadFilesProgress(path) - } - - setupRecyclerView() - - return builder.create() - } - - private fun showPathError() = - AlertDialog.Builder(context) - .setTitle(R.string.error) - .setMessage(R.string.error_opening_path_message) - .create() - - private fun setupRecyclerView() { - // set the click action for each type of item - val fileAdapter = FileAdapter { item -> - chosenFile = item.file - val file = path?.findFile(chosenFile!!) - if (file != null && file.exists() && file.isDirectory) { - firstLvl = false - - // Adds chosen directory to list - str.add(chosenFile) - fileList.clear() - path = file - - loadFilesProgress(file) - - mainListView?.adapter?.notifyDataSetChanged() - } else if (chosenFile.equals(getString(R.string.activity_file_explorer_up_directory_name)) && (file == null || !file.exists())) { - // for "up" dummy folder - - // present directory removed from list - str.removeAt(str.size - 1) - - // path modified to exclude present directory - path = path?.parentFile - - fileList.clear() - - // if there are no more directories in the list, then - // its the first level - if (str.isEmpty()) { - firstLvl = true - } - - loadFilesProgress(path) - return@FileAdapter - } else if (file != null && file.isFile) { - onFileSelectedListener?.invoke(file.uri) - dismiss() - } - - - } - - mainListView?.adapter = fileAdapter - mainListView?.layoutManager = LinearLayoutManager(context) - } - - private fun loadFilesProgress(path: DocumentFile?) { - progressBar?.visibility = View.VISIBLE - mainListView?.visibility = View.INVISIBLE - - lifecycleScope.launch(Dispatchers.IO) { - try { - loadFileList(path) - } catch (e: Exception) { - e.printStackTrace() - } - - withContext(Dispatchers.Main) { - progressBar?.visibility = View.GONE - mainListView?.visibility = View.VISIBLE - (mainListView?.adapter as? FileAdapter)?.apply { - submitList(fileList) - notifyDataSetChanged() - } - } - } - } - - - private fun loadFileList(path: DocumentFile?) { - if (path?.exists() == true) { - val files = path.listFiles() - val excludedExtensions = exclude?.toList() ?: emptyList() - val includedExtensions = include?.toList() ?: emptyList() - - for (file in files) { - val name = file.name ?: continue - - // Skip asset files - if (name.contains(".intercross")) continue - - // Check extensions only for files - val extension = name.substringAfterLast('.', "").lowercase() - if (extension in excludedExtensions) continue - - // only add files with included extensions - if (includedExtensions.contains(extension) || file.isDirectory) { - val icon = when { - name.lowercase().endsWith(".csv") -> R.drawable.ic_file_csv - name.endsWith(".xls") -> R.drawable.ic_file_xls - name.endsWith(".zip") -> R.drawable.ic_file_zip - file.isDirectory -> R.drawable.ic_file_directory - else -> R.drawable.ic_file_generic - } - fileList.add(Item(name, file.isDirectory, icon)) - } - } - - fileList.sortWith(mComparator) - - if (!firstLvl) { - fileList.add( - 0, Item( - getString(R.string.activity_file_explorer_up_directory_name), - true, - R.drawable.ic_file_up_dir - ) - ) - } - } - } - - fun setOnFileSelectedListener(listener: (Uri) -> Unit) { - onFileSelectedListener = listener - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/dialogs/ListAddDialog.kt b/app/src/main/java/org/phenoapps/intercross/dialogs/ListAddDialog.kt deleted file mode 100644 index 33b20f16..00000000 --- a/app/src/main/java/org/phenoapps/intercross/dialogs/ListAddDialog.kt +++ /dev/null @@ -1,87 +0,0 @@ -package org.phenoapps.intercross.dialogs - -import android.app.Activity -import android.app.AlertDialog -import android.app.Dialog -import android.os.Bundle -import android.view.View -import android.view.ViewGroup -import android.view.WindowManager -import android.widget.AdapterView -import android.widget.ArrayAdapter -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.ListView -import android.widget.TextView -import androidx.fragment.app.DialogFragment -import org.phenoapps.intercross.R - -class ListAddDialog( - private val activity: Activity, - private val dialogTitle: String, - private val items: Array, - private val icons: IntArray, - private val onItemClickListener: AdapterView.OnItemClickListener -) : DialogFragment() { - - override fun onCreateDialog(savedInstanceState: Bundle?): Dialog { - val layout = LinearLayout(activity).apply { - orientation = LinearLayout.VERTICAL - setPadding(16, 16, 16, 16) - } - - val listView = ListView(activity).apply { - layoutParams = LinearLayout.LayoutParams( - ViewGroup.LayoutParams.MATCH_PARENT, - ViewGroup.LayoutParams.WRAP_CONTENT - ) - } - layout.addView(listView) - - val adapter = ListAddAdapter(activity, items, icons) - listView.adapter = adapter - - val builder = AlertDialog.Builder(activity) - .setTitle(dialogTitle) - .setCancelable(true) - .setView(layout) - .setPositiveButton(getString(R.string.dialog_cancel)) { dialogInterface, _ -> dialogInterface.dismiss() } - - val dialog = builder.create() - dialog.show() - - dialog.window?.let { window -> - window.attributes = window.attributes.apply { - width = WindowManager.LayoutParams.MATCH_PARENT - height = WindowManager.LayoutParams.WRAP_CONTENT - } - } - - listView.setOnItemClickListener { parent, view, position, id -> - onItemClickListener.onItemClick(parent, view, position, id) - dialog.dismiss() - } - - return dialog - } - - private inner class ListAddAdapter( - private val context: Activity, - private val values: Array, - private val icons: IntArray - ) : ArrayAdapter(context, R.layout.list_item_dialog_with_icon, values) { - - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = convertView ?: context.layoutInflater.inflate( - R.layout.list_item_dialog_with_icon, - parent, - false - ) - - view.findViewById(R.id.icon)?.setImageResource(icons[position]) - view.findViewById(R.id.spinnerTarget)?.text = values[position] - - return view - } - } -} \ No newline at end of file diff --git a/app/src/main/java/org/phenoapps/intercross/dialogs/MetadataCreatorDialog.kt b/app/src/main/java/org/phenoapps/intercross/dialogs/MetadataCreatorDialog.kt deleted file mode 100644 index 159d3cbc..00000000 --- a/app/src/main/java/org/phenoapps/intercross/dialogs/MetadataCreatorDialog.kt +++ /dev/null @@ -1,52 +0,0 @@ -package org.phenoapps.intercross.dialogs - -import android.app.Dialog -import android.content.Context -import android.os.Bundle -import android.widget.* -import org.phenoapps.intercross.R -import org.phenoapps.intercross.interfaces.MetadataManager - -/** - * A cancelable dialog for creating a new metadata property with a default value. - * Metadata property must not be blank, while metadata default value can be an integer or null (empty string) - * Updating the database with the new properties is handled by the MetadataManager implemented in the accompanied fragment. - */ -class MetadataCreatorDialog(private val ctx: Context, private val listener: MetadataManager) : Dialog(ctx, R.style.Dialog) { - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - setContentView(R.layout.dialog_metadata_creator) - - setCanceledOnTouchOutside(true) - - setTitle(R.string.dialog_metadata_title) - - setupUi() - - } - - private fun setupUi() { - - val property = findViewById(R.id.dialog_metadata_property_edit_text) - - val value = findViewById(R.id.dialog_metadata_value_edit_text) - - findViewById