diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e3e6a41 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,4 @@ +[*.{kt,kts}] +ktlint_standard_no-wildcard-imports = disabled +ktlint_standard_property-naming = disabled +ktlint_standard_no-consecutive-comments = disabled diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml new file mode 100644 index 0000000..a8bd578 --- /dev/null +++ b/.github/workflows/android.yml @@ -0,0 +1,33 @@ +name: Android CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: set up JDK 17 + uses: actions/setup-java@v4 + with: + java-version: '17' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Ktlint Check + run: ./gradlew ktlintCheck --no-daemon + + - name: Run Unit Tests + run: ./gradlew testDebugUnitTest --no-daemon + + - name: Build Debug APK + run: ./gradlew assembleDebug --no-daemon diff --git a/.idea/AndroidProjectSystem.xml b/.idea/AndroidProjectSystem.xml new file mode 100644 index 0000000..4a53bee --- /dev/null +++ b/.idea/AndroidProjectSystem.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/compiler.xml b/.idea/compiler.xml index fb7f4a8..b86273d 100644 --- a/.idea/compiler.xml +++ b/.idea/compiler.xml @@ -1,6 +1,6 @@ - + \ No newline at end of file diff --git a/.idea/deploymentTargetSelector.xml b/.idea/deploymentTargetSelector.xml new file mode 100644 index 0000000..b268ef3 --- /dev/null +++ b/.idea/deploymentTargetSelector.xml @@ -0,0 +1,10 @@ + + + + + + + + + \ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml index e9969a1..639c779 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -4,17 +4,15 @@ diff --git a/.idea/migrations.xml b/.idea/migrations.xml new file mode 100644 index 0000000..f8051a6 --- /dev/null +++ b/.idea/migrations.xml @@ -0,0 +1,10 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 6199cc2..d15a481 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,9 +1,7 @@ - - - - - \ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..16660f1 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,17 @@ + + + + + + \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..0f646ba --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,6 @@ +{ + "chat.tools.terminal.autoApprove": { + "mkdir": true, + "mv": true + } +} \ No newline at end of file diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..7128faa --- /dev/null +++ b/TODO.md @@ -0,0 +1,149 @@ +# TODO – CanIDrive + +Items marked ~~like this~~ were fixed in the initial implementation pass. + +--- + +## CRITICAL – Bugs / Crashes ✓ All Fixed + +- [x] ~~**Fix `substring(0, 4)` crash on small alcohol rates**~~ – `DriveFragment.kt`: replaced with `String.format("%.2f g/L", rate)`. +- [x] ~~**Fix `toDouble()` crash on invalid input**~~ – `DrinkerFragment.kt`: replaced with `toDoubleOrNull()` + fallback. +- [x] ~~**Fix Saint Patrick's Day logic (wrong month + wrong field)**~~ – `TimeServiceAndroid.kt`: migrated to `Calendar` with `MONTH`/`DAY_OF_MONTH`. +- [x] ~~**Fix `equals/hashCode` contract violation**~~ – `PresetDrink.kt`: removed `count` from `hashCode()` to match `equals()`. + +--- + +## HIGH – Data Integrity + +- [x] ~~**Fix destructive database migration**~~ – `MainRepository.kt`: migration `1→2` fixed and re-enabled. Added `NOT NULL DEFAULT ''` on `name` column, preserved the table rename, and added the missing `PresetDrinkEntity` table creation. `fallbackToDestructiveMigration()` removed. +- [x] ~~**Fix unit inconsistency for US drive laws**~~ – `DriveLaws.kt`: USA is `0.8` while Canada is `0.08`. Verify all ~60 country entries against official sources and standardize to g/L. +- [x] ~~**Validate all country BAC limits**~~ – Add a unit test that asserts expected BAC limit per country against a reference table to prevent accidental data corruption. + +--- + +## HIGH – Architecture + +- [x] ~~**Introduce ViewModels**~~ – No `ViewModel` is used anywhere. Fragments access `CanIDrive.instance.mainRepository` directly. Create `DriveViewModel`, `DrinkerViewModel`, `AddDrinkViewModel` with `viewModelScope`-bound coroutines. +- [x] ~~**Introduce Dependency Injection (Hilt or Koin)**~~ – Replace the global `CanIDrive` Application singleton with a proper DI framework. Improves testability and reduces coupling. +- [x] ~~**Move repositories out of `ui` package**~~ – `MainRepository`, `DrinkRepository`, `DigestionRepository`, `DriveLawRepository` are in `ui.repository`. Moved to `data.repository`. +- [x] ~~**Remove Android resource dependencies from domain layer**~~ – `DriveLaw.kt` holds `explanationId: Int` (an `R.string` reference). The domain should be pure Kotlin; resolve resource IDs in the UI layer only. +- [x] ~~**Fix Law of Demeter violations**~~ – `IngestedDrinksAdapter.kt`: `CanIDrive.instance.mainRepository.drinkRepository.ingestionService` is 4 levels deep. Expose needed services directly via constructor injection. + +--- + +## HIGH – Legal / Safety + +- [x] ~~**Add legal disclaimer**~~ – Disclaimer AlertDialog shown on first launch (`SplashFragment.kt`). Accepted state persisted in SharedPreferences. + +--- + +## HIGH – Security + +- [x] ~~**Enable minification/obfuscation for release builds**~~ – `app/build.gradle.kts`: `isMinifyEnabled = true` + `isShrinkResources = true` enabled for release. +- [x] ~~**Use `EncryptedSharedPreferences` for health data**~~ – All three repositories (`DigestionRepository`, `DriveLawRepository`, `MainRepository`) now use `EncryptedSharedPreferences` (AES256-GCM) via `androidx.security:security-crypto:1.1.0-alpha06`. +- [x] ~~**Review `allowBackup`**~~ – Added `android:fullBackupContent="@xml/backup_rules"` (API 23–30) and `android:dataExtractionRules="@xml/data_extraction_rules"` (API 31+) to exclude SharedPreferences from cloud backup while keeping the Room database backed up. + +--- + +## MEDIUM – Performance + +- [x] **Replace `ListView` with `RecyclerView`** – `constraint_content_drive_history.xml` and `constraint_content_add_drink_presets.xml`: migrate to `RecyclerView` + `ListAdapter` + `DiffUtil` for better scroll performance. +- [x] **Implement ViewHolder pattern in adapters** – `PresetDrinksAdapter.kt` and `IngestedDrinksAdapter.kt` extend `BaseAdapter` and inflate a new view on every `getView()` call, ignoring `convertView`. This causes excessive GC. Migrate to `RecyclerView.Adapter`. +- [x] ~~**Fix LiveData observer leak in `PresetDrinksAdapter`**~~ – Moved `liveSelectedPreset` observation from `getView()` into `init` block. State cached in a field. +- [x] **Cancel coroutines properly** – `DrinkRepository.kt`: `Job()` and manual `CoroutineScope` are never cancelled → memory/coroutine leaks. Use `viewModelScope` after ViewModel introduction, or implement `Closeable`. + +--- + +## MEDIUM – Deprecated APIs + +- [x] ~~**Replace deprecated `Date.month` / `Date.day`**~~ – `TimeServiceAndroid.kt`: migrated to `Calendar`. +- [x] ~~**Replace `String.toUpperCase()` without Locale**~~ – `DriveLawService.kt`: replaced with `uppercase(Locale.ROOT)`. +- [x] **Replace `toggleSoftInput(SHOW_FORCED, 0)`** – `KeyboardUtils.kt`: deprecated since API 31. Use `WindowInsetsController.show(WindowInsetsCompat.Type.ime())` instead. +- [x] **Replace `android:tint` with `app:tint`** in relevant `ImageView` XML attributes for backward compat with `AppCompatImageView` (affects `fragment_drive_status.xml` and `constraint_content_drinker_country.xml`). + +--- + +## MEDIUM – Code Quality + +- [x] ~~**Replace magic strings for sex with `Sex` enum**~~ – Created `Sex.kt` enum in `domain.digestion`. Updated `PhysicalBody`, `DigestionRepository`, `DrinkerFragment`, all test files. +- [x] ~~**Move `KeyboardUtils` to `ui.util` package**~~ – Moved to `ui/util/KeyboardUtils.kt`, updated all 3 import sites. +- [x] ~~**Remove unused `volume` and `degree` variables**~~ – `AddDrinkFragment.kt`. +- [x] **Replace mutable lambda callbacks with `Flow`/`LiveData`** – `PhysicalBody.onUpdate`, `DriveLawService.onCustomLimitCallback`, `PresetDrinkService.onPresetsChanged`: fragile `var` lambdas. Replace with `StateFlow` or `LiveData`. +- [x] **Separate `DriveLaws.kt` data from Android resources** – 361-line file mixes raw BAC data with `R.string.*` references. Consider loading from a bundled JSON asset file and resolving string IDs in a mapper class. + +--- + +## MEDIUM – Testing + +- [x] ~~**Fix JUnit 4/5 mixing**~~ – `PresetDrinkServiceTest.kt`: replaced `org.junit.Test` with `org.junit.jupiter.api.Test`. +- [x] **Add tests for `DrinkerStatusService`** – No tests for the status aggregation logic (sober / can drive / cannot drive). +- [ ] **Add tests for `IngestionService`** – Only indirectly covered via `DigestionServiceTest`. +- [x] **Add tests for `IngestedDrink.alcoholMass()`** – Critical BAC calculation has no direct unit test. +- [x] **Add tests for Room `Converters`** – `Date` ↔ `Long` conversion is untested; a regression could silently corrupt all timestamps. +- [ ] **Add tests for `TimeServiceAndroid`** – A test would have caught the original Saint Patrick's Day bug. +- [ ] **Add repository tests** – No repository layer has any tests. +- [ ] **Add UI / integration tests (Espresso)** – Zero UI tests exist. At minimum, test the happy path: set profile → add drink → verify status changes. +- [x] ~~**Validate `DriveLaws` data in tests**~~ – Assert expected BAC limits for a subset of countries as a regression guard. + +--- + +## LOW – Accessibility + +- [x] ~~**Move hardcoded `contentDescription` to string resources**~~ – `item_preset_drink.xml`, `item_past_drink.xml` now use `@string/content_desc_*`. +- [x] ~~**Add `contentDescription` to all FloatingActionButtons**~~ – All FABs in `fragment_drive_status.xml`, `fragment_drinker.xml`, `fragment_add_preset.xml` now have descriptions. +- [x] ~~**Fix splash screen text size unit**~~ – `fragment_splash.xml`: changed from `20pt` to `40sp`. +- [ ] **Add explicit text labels for drive status** – `DriveFragment.kt`: status communicated only by color/icon. Add a visible text like "Safe to drive" / "Do NOT drive" for colorblind users and screen readers. +- [x] **Fix suppressed `LabelFor` warning** – `constraint_content_drinker_country.xml`: `tools:ignore="LabelFor"` on the custom limit `EditText`. Add a proper ``. + +--- + +## LOW – Internationalization + +- [x] ~~**Remove hardcoded `"average"` text**~~ – `constraint_content_drinker_pickers.xml`: replaced with `@string/alcohol_tolerance_medium`. +- [x] ~~**Remove redundant `translatable="false"` keys from Italian strings**~~ – Cleaned `values-it-rIT/strings.xml`. +- [x] **Hardcoded unit string `"g/L"`** – `constraint_content_drinker_country.xml`: move to string resource; consider supporting g/dL, mg/100mL, ‰ per country selection. +- [x] **Use locale-aware number formatting** – `DecimalFormat("0.#")` ignores locale decimal separator conventions. Use `NumberFormat.getInstance(locale)`. + +--- + +## LOW – UI/UX Improvements + +- [ ] **Replace custom splash with Android 12+ SplashScreen API** – `SplashFragment.kt`: artificial 1-second `postDelayed`. Use the `androidx.core:core-splashscreen` library for the system splash. +- [x] **Add deletion confirmation dialogs** – Drink delete buttons (presets and history) have no confirmation. Accidental taps permanently delete data. Show an `AlertDialog` before deletion. +- [ ] **Extend weight picker range** – `DrinkerFragment.kt`: weight limited to `[30..150]` kg in 5 kg steps. Add free-form `EditText` input or extend range to 250 kg. +- [x] ~~**Implement Settings screen**~~ – `menu_main.xml` has a Settings item but `MainActivity.kt` returns `true` without navigating. Wire up a `PreferenceFragment` or custom settings screen. Implementation added with `SettingsFragment.kt` and `nav_graph.xml` integration. +- [ ] **Add Up/Back button in Toolbar** – Navigation relies on FABs; Toolbar has no back/up button. Non-standard Android UX pattern. +- [ ] **Support dark mode** – No `values-night/` resources exist. Implement Material Design dark theme. + +--- + +## LOW – Build & Configuration + +- [x] ~~**Move `kotlin-reflect` to `testImplementation`**~~ – Only needed at test time. +- [x] ~~**Remove `legacy-support-v4` dependency**~~ – Unnecessary with `minSdk = 21` + AndroidX. +- [x] ~~**Disable Jetifier**~~ – `gradle.properties`: `android.enableJetifier` commented out; not needed with pure AndroidX deps. +- [ ] **Update Room to latest stable** – Current: `2.6.1`. Update to `2.7.x+`. +- [ ] **Add CI/CD pipeline** – No GitHub Actions or CI config. Add automated build, unit test, and lint checks on each push. +- [ ] **Add `ktlint` or `detekt`** – No static analysis or formatting enforcement configured. + +--- + +## FEATURE IDEAS + +- [ ] **Notification / alarm when user can drive again** – Calculate time until BAC drops below legal limit; schedule a `WorkManager` notification. +- [ ] **Home screen widget** – Quick-glance widget showing current BAC status and estimated time remaining. +- [ ] **BAC evolution graph** – Chart the alcohol rate over time (e.g., MPAndroidChart or Compose charts). +- [ ] **Food intake tracking** – Food slows alcohol absorption. Let user mark whether they ate before/during drinking to improve estimate accuracy. +- [ ] **Multiple BAC unit support** – Support g/dL, mg/100mL, ‰, and % BAC; auto-select or let user choose. +- [ ] **Data export / import** – Export drink history as CSV or JSON; re-import on a new device. +- [ ] **Bulk history deletion** – "Clear all history" or date-range deletion. +- [ ] **Favorite / recent drinks shortcuts** – One-tap re-add of last consumed drink from the main screen. +- [ ] **Hydration reminders** – Suggest drinking water between alcoholic drinks. +- [ ] **Taxi / rideshare quick link** – One-tap shortcut to a taxi or rideshare app when user cannot drive. +- [ ] **Onboarding flow** – First-launch tutorial explaining profile setup and how BAC estimation works. +- [ ] **Material Design 3 / Material You** – Migrate to Material 3 components and support dynamic color theming. +- [ ] **Jetpack Compose migration** – Migrate XML layouts to Compose for a modern, declarative UI. +- [ ] **Multi-user profiles** – Track multiple people (e.g., a group of friends, designated driver checking on passengers). +- [ ] **Weekly/monthly statistics** – Dashboard showing consumption trends over time. +- [ ] **Wear OS companion app** – Quick BAC status check from a smartwatch. +- [ ] **Health Connect / Google Fit integration** – Sync BAC and consumption data with health platforms. diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 452fbea..eccbe6a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,102 +1,110 @@ plugins { id("com.android.application") - id("kotlin-android") - id("kotlin-android-extensions") + id("org.jetbrains.kotlin.android") id("androidx.navigation.safeargs.kotlin") - id("kotlin-kapt") + id("com.google.devtools.ksp") + id("org.jlleitschuh.gradle.ktlint") } android { - compileSdkVersion(30) + namespace = "com.vaudibert.canidrive" + compileSdk = 35 + defaultConfig { applicationId = "com.vaudibert.canidrive" - minSdkVersion(19) - targetSdkVersion(30) - versionCode = 22 - versionName = "0.2.9" + minSdk = 21 + targetSdk = 35 + versionCode = 23 + versionName = "0.3.0" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - kapt { - arguments { - arg("room.schemaLocation", "$projectDir/schemas") - } + + ksp { + arg("room.schemaLocation", "$projectDir/schemas") } } + buildTypes { - getByName("release") { - isMinifyEnabled = false + release { + isMinifyEnabled = true + isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } + + buildFeatures { + viewBinding = true + buildConfig = true + } + compileOptions { - sourceCompatibility.isJava8 - targetCompatibility.isJava8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } - (kotlinOptions as org.jetbrains.kotlin.gradle.dsl.KotlinJvmOptions).apply { - jvmTarget = JavaVersion.VERSION_1_8.toString() + + kotlinOptions { + jvmTarget = "17" } } dependencies { - implementation(fileTree(mapOf("dir" to "libs", "include" to listOf("*.jar")))) implementation("com.h6ah4i.android.widget.verticalseekbar:verticalseekbar:1.0.0") // Kotlin - implementation(kotlin("stdlib-jdk8", version = "1.3.61")) - implementation("androidx.core:core-ktx:1.3.2") + implementation("androidx.core:core-ktx:1.15.0") + implementation("androidx.core:core-splashscreen:1.0.1") // Android - implementation("androidx.appcompat:appcompat:1.2.0") - implementation("androidx.constraintlayout:constraintlayout:2.0.2") - implementation("com.google.android.material:material:1.2.1") - implementation("androidx.legacy:legacy-support-v4:1.0.0") + implementation("androidx.appcompat:appcompat:1.7.0") + implementation("androidx.constraintlayout:constraintlayout:2.2.0") + implementation("com.google.android.material:material:1.12.0") // JUnit - testImplementation("org.junit.jupiter:junit-jupiter-api:5.5.1") - testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.5.1") - androidTestImplementation("androidx.test:runner:1.3.0") - androidTestImplementation("androidx.test.espresso:espresso-core:3.3.0") + testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.4") + testRuntimeOnly("org.junit.jupiter:junit-jupiter-engine:5.11.4") + androidTestImplementation("androidx.test:runner:1.6.2") + androidTestImplementation("androidx.test:rules:1.6.1") + androidTestImplementation("androidx.test.ext:junit:1.2.1") + androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") // Kotlin needs for testing - implementation(kotlin("reflect")) + testImplementation(kotlin("reflect")) testImplementation(kotlin("test")) - testImplementation(kotlin("test-junit")) + testImplementation("org.mockito:mockito-core:5.11.0") // Navigation (Kotlin) - val navigationVersion = "2.2.0" + val navigationVersion = "2.8.5" implementation("androidx.navigation:navigation-fragment-ktx:$navigationVersion") implementation("androidx.navigation:navigation-ui-ktx:$navigationVersion") - // ViewModel and LiveData - val lifecycleVersion = "2.2.0" - implementation("androidx.lifecycle:lifecycle-extensions:$lifecycleVersion") + val lifecycleVersion = "2.8.7" implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion") implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion") - kapt("androidx.lifecycle:lifecycle-compiler:$lifecycleVersion") - // optional - ReactiveStreams support for LiveData - // For Kotlin use lifecycle-reactivestreams-ktx implementation("androidx.lifecycle:lifecycle-reactivestreams-ktx:$lifecycleVersion") // optional - Test helpers for LiveData - val coreTestingVersion = "2.1.0" - testImplementation("androidx.arch.core:core-testing:$coreTestingVersion") - + testImplementation("androidx.arch.core:core-testing:2.2.0") // Room - val roomVersion = "2.2.3" + val roomVersion = "2.6.1" implementation("androidx.room:room-runtime:$roomVersion") - kapt("androidx.room:room-compiler:$roomVersion") - // For Kotlin use kapt instead of annotationProcessor - - // optional - Kotlin Extensions and Coroutines support for Room + ksp("androidx.room:room-compiler:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion") + // Preferences + implementation("androidx.preference:preference-ktx:1.2.1") + + // Security – encrypted SharedPreferences + implementation("androidx.security:security-crypto:1.1.0-alpha06") + + // Koin for Dependency Injection + val koinVersion = "3.5.3" + implementation("io.insert-koin:koin-android:$koinVersion") } -tasks.withType < Test > { - // Use the native JUnit support of Gradle. +tasks.withType { useJUnitPlatform() } diff --git a/app/src/androidTest/java/com/vaudibert/canidrive/HappyPathUITest.kt b/app/src/androidTest/java/com/vaudibert/canidrive/HappyPathUITest.kt new file mode 100644 index 0000000..6136960 --- /dev/null +++ b/app/src/androidTest/java/com/vaudibert/canidrive/HappyPathUITest.kt @@ -0,0 +1,47 @@ +package com.vaudibert.canidrive + +import androidx.test.espresso.Espresso.onView +import androidx.test.espresso.action.ViewActions.clearText +import androidx.test.espresso.action.ViewActions.click +import androidx.test.espresso.action.ViewActions.closeSoftKeyboard +import androidx.test.espresso.action.ViewActions.typeText +import androidx.test.espresso.assertion.ViewAssertions.matches +import androidx.test.espresso.matcher.ViewMatchers.isDisplayed +import androidx.test.espresso.matcher.ViewMatchers.withId +import androidx.test.espresso.matcher.ViewMatchers.withText +import androidx.test.ext.junit.rules.ActivityScenarioRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.vaudibert.canidrive.ui.MainActivity +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class HappyPathUITest { + @get:Rule + val activityRule = ActivityScenarioRule(MainActivity::class.java) + + @Test + fun testHappyPathAddDrink() { + // App starts. Disclaimer shows up on first launch. + onView(withText("I understand")).perform(click()) + + // We land on DrinkerFragment because it's first launch + onView(withId(R.id.editTextWeight)).perform(clearText(), typeText("80"), closeSoftKeyboard()) + onView(withId(R.id.radioMale)).perform(click()) + onView(withId(R.id.buttonValidateDrinker)).perform(click()) + + // We should be on DriveFragment + onView(withId(R.id.buttonAddDrink)).perform(click()) + + // AddDrinkFragment + onView(withId(R.id.buttonAddPreset)).perform(click()) + + // Added a default preset, we should be back on DriveFragment + // The alcohol rate view should be visible indicating > 0 BAC. + onView(withId(R.id.textViewAlcoholRate)).check(matches(isDisplayed())) + + // Ensure that label explicit text is also present + onView(withId(R.id.textViewDriveStatusLabel)).check(matches(isDisplayed())) + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6fcdfd9..062af75 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,10 +1,11 @@ - + + android:theme="@style/Theme.App.Starting"> diff --git a/app/src/main/assets/drive_laws.json b/app/src/main/assets/drive_laws.json new file mode 100644 index 0000000..bcc8e55 --- /dev/null +++ b/app/src/main/assets/drive_laws.json @@ -0,0 +1,14 @@ +[ + { + "countryCode": "AL", + "limit": 0.1, + "youngLimit": null, + "professionalLimit": null + }, + { + "countryCode": "AT", + "limit": 0.5, + "youngLimit": null, + "professionalLimit": null + } +] \ No newline at end of file diff --git a/app/src/main/java/com/vaudibert/canidrive/data/Converters.kt b/app/src/main/java/com/vaudibert/canidrive/data/Converters.kt index 701ab8e..5ef9935 100644 --- a/app/src/main/java/com/vaudibert/canidrive/data/Converters.kt +++ b/app/src/main/java/com/vaudibert/canidrive/data/Converters.kt @@ -7,7 +7,6 @@ import java.util.* * Static object required for room automatic conversion. */ object Converters { - @TypeConverter @JvmStatic fun fromTimestamp(value: Long): Date { @@ -19,5 +18,4 @@ object Converters { fun dateToTimestamp(date: Date): Long { return date.time } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/data/DrinkDatabase.kt b/app/src/main/java/com/vaudibert/canidrive/data/DrinkDatabase.kt index b6ab560..730f0fe 100644 --- a/app/src/main/java/com/vaudibert/canidrive/data/DrinkDatabase.kt +++ b/app/src/main/java/com/vaudibert/canidrive/data/DrinkDatabase.kt @@ -7,13 +7,14 @@ import androidx.room.TypeConverters @Database( entities = [ IngestedDrinkEntity::class, - PresetDrinkEntity::class + PresetDrinkEntity::class, ], version = 2, - exportSchema = true + exportSchema = true, ) @TypeConverters(Converters::class) abstract class DrinkDatabase : RoomDatabase() { abstract fun ingestedDrinkDao(): IngestedDrinkDao + abstract fun presetDrinkDao(): PresetDrinkDao } diff --git a/app/src/main/java/com/vaudibert/canidrive/data/IngestedDrinkDao.kt b/app/src/main/java/com/vaudibert/canidrive/data/IngestedDrinkDao.kt index c5bcd69..cfa2900 100644 --- a/app/src/main/java/com/vaudibert/canidrive/data/IngestedDrinkDao.kt +++ b/app/src/main/java/com/vaudibert/canidrive/data/IngestedDrinkDao.kt @@ -16,4 +16,4 @@ interface IngestedDrinkDao { @Delete(entity = IngestedDrinkEntity::class) suspend fun remove(ingestedDrink: IngestedDrink) -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/data/IngestedDrinkEntity.kt b/app/src/main/java/com/vaudibert/canidrive/data/IngestedDrinkEntity.kt index e895199..6c058c3 100644 --- a/app/src/main/java/com/vaudibert/canidrive/data/IngestedDrinkEntity.kt +++ b/app/src/main/java/com/vaudibert/canidrive/data/IngestedDrinkEntity.kt @@ -8,18 +8,17 @@ import java.util.* @Entity class IngestedDrinkEntity( - @PrimaryKey(autoGenerate = true) var uid : Long, - ingestionTime : Date, - volume : Double, - name : String, - degree: Double - ) : IIngestedDrink, IngestedDrink(name, volume, degree, ingestionTime) { - - constructor(uid: Long, ingested : IngestedDrink) : this( + @PrimaryKey(autoGenerate = true) var uid: Long, + ingestionTime: Date, + volume: Double, + name: String, + degree: Double, +) : IIngestedDrink, IngestedDrink(name, volume, degree, ingestionTime) { + constructor(uid: Long, ingested: IngestedDrink) : this( uid, ingested.ingestionTime, ingested.volume, ingested.name, - ingested.degree) - -} \ No newline at end of file + ingested.degree, + ) +} diff --git a/app/src/main/java/com/vaudibert/canidrive/data/PresetDrinkDao.kt b/app/src/main/java/com/vaudibert/canidrive/data/PresetDrinkDao.kt index 8f6d204..8b11c2b 100644 --- a/app/src/main/java/com/vaudibert/canidrive/data/PresetDrinkDao.kt +++ b/app/src/main/java/com/vaudibert/canidrive/data/PresetDrinkDao.kt @@ -6,7 +6,7 @@ import com.vaudibert.canidrive.domain.drink.PresetDrink @Dao interface PresetDrinkDao { @Insert(entity = PresetDrinkEntity::class) - suspend fun insert(presetDrink: PresetDrink) : Long + suspend fun insert(presetDrink: PresetDrink): Long @Query("SELECT COUNT(uid) from presetdrinkentity ORDER BY count DESC") suspend fun count(): Int @@ -22,4 +22,4 @@ interface PresetDrinkDao { @Query("SELECT * from presetdrinkentity") suspend fun getAll(): List -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/data/PresetDrinkEntity.kt b/app/src/main/java/com/vaudibert/canidrive/data/PresetDrinkEntity.kt index d353868..2d4063b 100644 --- a/app/src/main/java/com/vaudibert/canidrive/data/PresetDrinkEntity.kt +++ b/app/src/main/java/com/vaudibert/canidrive/data/PresetDrinkEntity.kt @@ -7,12 +7,14 @@ import com.vaudibert.canidrive.domain.drink.PresetDrink @Entity class PresetDrinkEntity( - @PrimaryKey(autoGenerate = true) var uid : Long, + @PrimaryKey(autoGenerate = true) var uid: Long, name: String, volume: Double, degree: Double, - count: Int -): IPresetDrink, PresetDrink(name, volume, degree, count) { - - constructor(uid: Long, presetDrink: PresetDrink) : this(uid, presetDrink.name, presetDrink.volume, presetDrink.degree, presetDrink.count) -} \ No newline at end of file + count: Int, +) : IPresetDrink, PresetDrink(name, volume, degree, count) { + constructor( + uid: Long, + presetDrink: PresetDrink, + ) : this(uid, presetDrink.name, presetDrink.volume, presetDrink.degree, presetDrink.count) +} diff --git a/app/src/main/java/com/vaudibert/canidrive/data/repository/DigestionRepository.kt b/app/src/main/java/com/vaudibert/canidrive/data/repository/DigestionRepository.kt new file mode 100644 index 0000000..f2ad07f --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/data/repository/DigestionRepository.kt @@ -0,0 +1,87 @@ +package com.vaudibert.canidrive.data.repository + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.vaudibert.canidrive.R +import com.vaudibert.canidrive.domain.digestion.DigestionService +import com.vaudibert.canidrive.domain.digestion.PhysicalBody +import com.vaudibert.canidrive.domain.digestion.Sex +import com.vaudibert.canidrive.domain.drink.IIngestedDrinkProvider +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +/** + * Repository holding the drinker and driveLaw instances. + * + * It establishes the link between the drinker absolute state and the drive law limits that depend + * on the country selection. + * + * It is responsible for retrieving all saved values with the given : + * - sharedPreferences instance for : + * - weight + * - sex + * - country code (= drive law) + * - init flag (= user configuration already validated once) + * - drinkDao for past consumed drinks. + */ +class DigestionRepository(context: Context, drinkProvider: IIngestedDrinkProvider) { + // Main instance to link + val body = PhysicalBody() + + val digestionService = DigestionService(body, drinkProvider) + + val toleranceLevels = + listOf( + context.getString(R.string.alcohol_tolerance_low), + context.getString(R.string.alcohol_tolerance_medium), + context.getString(R.string.alcohol_tolerance_high), + ) + + private val _liveDrinker = MutableLiveData() + val liveDrinker: LiveData + get() = _liveDrinker + + init { + val masterKey = + MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + val sharedPref = + EncryptedSharedPreferences.create( + context, + context.getString(R.string.user_preferences), + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + + val weight = sharedPref.getFloat(context.getString(R.string.user_weight), 70F).toDouble() + val sex = + Sex.fromString( + sharedPref.getString(context.getString(R.string.user_sex), "OTHER") ?: "OTHER", + ) + val tolerance = sharedPref.getFloat(context.getString(R.string.user_tolerance), 0.0F).toDouble() + + body.sex = sex + body.weight = weight + body.alcoholTolerance = tolerance + + CoroutineScope(Dispatchers.IO).launch { + body.bodyState.collect { state -> + sharedPref + .edit() + .putString(context.getString(R.string.user_sex), state.sex.name) + .putFloat(context.getString(R.string.user_weight), state.weight.toFloat()) + .putFloat("USER_TOLERANCE", state.alcoholTolerance.toFloat()) + .apply() + _liveDrinker.postValue(body) + } + } + + _liveDrinker.value = body + } +} diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DrinkRepository.kt b/app/src/main/java/com/vaudibert/canidrive/data/repository/DrinkRepository.kt similarity index 53% rename from app/src/main/java/com/vaudibert/canidrive/ui/repository/DrinkRepository.kt rename to app/src/main/java/com/vaudibert/canidrive/data/repository/DrinkRepository.kt index c274834..20da4fa 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DrinkRepository.kt +++ b/app/src/main/java/com/vaudibert/canidrive/data/repository/DrinkRepository.kt @@ -1,4 +1,4 @@ -package com.vaudibert.canidrive.ui.repository +package com.vaudibert.canidrive.data.repository import android.content.Context import android.util.Log @@ -16,10 +16,10 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Job import kotlinx.coroutines.launch +import java.io.Closeable import java.util.* -class DrinkRepository(context: Context, drinkDatabase: DrinkDatabase) { - +class DrinkRepository(context: Context, drinkDatabase: DrinkDatabase) : Closeable { private val _livePastDrinks = MutableLiveData>() val livePastDrinks: LiveData> get() = _livePastDrinks @@ -38,68 +38,67 @@ class DrinkRepository(context: Context, drinkDatabase: DrinkDatabase) { private val daoJob = Job() private val uiScope = CoroutineScope(Dispatchers.Main + daoJob) - private val presetMaker: (String, Double, Double) -> PresetDrinkEntity = { - name:String, volume:Double, degree:Double -> - val newPreset = PresetDrink(name, volume, degree) - val newPresetEntity = PresetDrinkEntity(-1, newPreset) - uiScope.launch { - newPresetEntity.uid = presetDrinkDao.insert(newPreset) - } - newPresetEntity + name: String, volume: Double, degree: Double -> + val newPreset = PresetDrink(name, volume, degree) + val newPresetEntity = PresetDrinkEntity(-1, newPreset) + uiScope.launch { + newPresetEntity.uid = presetDrinkDao.insert(newPreset) + } + newPresetEntity } val presetService = PresetDrinkService(presetMaker) private val ingestor: (PresetDrinkEntity, Date) -> IngestedDrinkEntity = { - preset : PresetDrinkEntity, ingestionTime : Date -> - val newIngested = IngestedDrink(preset.name, preset.volume, preset.degree, ingestionTime) - val newIngestedEntity = IngestedDrinkEntity(-1, newIngested) - uiScope.launch { newIngestedEntity.uid = ingestedDrinkDao.insert(newIngested) } - newIngestedEntity + preset: PresetDrinkEntity, ingestionTime: Date -> + val newIngested = IngestedDrink(preset.name, preset.volume, preset.degree, ingestionTime) + val newIngestedEntity = IngestedDrinkEntity(-1, newIngested) + uiScope.launch { newIngestedEntity.uid = ingestedDrinkDao.insert(newIngested) } + newIngestedEntity } val ingestionService = IngestionService(ingestor) // TODO : move this default data in the database init ? - private val defaultPresetDrink = mutableListOf( - PresetDrink( - context.getString(R.string.preset_red_wine), - 130.0, - 13.0 - ), - PresetDrink( - context.getString(R.string.preset_light_beer), - 250.0, - 4.5 - ), - PresetDrink( - context.getString(R.string.preset_light_beer), - 500.0, - 4.5 - ), - PresetDrink( - context.getString(R.string.preset_triple_beer), - 330.0, - 9.0 - ), - PresetDrink( - context.getString(R.string.preset_soft_cider), - 250.0, - 2.5 - ), - PresetDrink( - context.getString(R.string.preset_martini), - 80.0, - 17.0 - ), - PresetDrink( - context.getString(R.string.preset_whisky), - 80.0, - 30.0 + private val defaultPresetDrink = + mutableListOf( + PresetDrink( + context.getString(R.string.preset_red_wine), + 130.0, + 13.0, + ), + PresetDrink( + context.getString(R.string.preset_light_beer), + 250.0, + 4.5, + ), + PresetDrink( + context.getString(R.string.preset_light_beer), + 500.0, + 4.5, + ), + PresetDrink( + context.getString(R.string.preset_triple_beer), + 330.0, + 9.0, + ), + PresetDrink( + context.getString(R.string.preset_soft_cider), + 250.0, + 2.5, + ), + PresetDrink( + context.getString(R.string.preset_martini), + 80.0, + 17.0, + ), + PresetDrink( + context.getString(R.string.preset_whisky), + 80.0, + 30.0, + ), ) - ) - init { @@ -118,12 +117,10 @@ class DrinkRepository(context: Context, drinkDatabase: DrinkDatabase) { } } - presetService.onPresetsChanged = { - _livePresetDrinks.postValue(it) - } - - presetService.onSelectUpdated = { - _liveSelectedPreset.postValue(it) + ingestionService.onAdded = { + uiScope.launch { + ingestedDrinkDao.insert(it) + } } // Get all preset drinks @@ -135,14 +132,32 @@ class DrinkRepository(context: Context, drinkDatabase: DrinkDatabase) { presetService.populate(presetDrinkDao.getAll()) } - presetService.onPresetRemoved = { - uiScope.launch { + uiScope.launch { + presetService.presetsFlow.collect { + _livePresetDrinks.postValue(it) + } + } + + uiScope.launch { + presetService.selectedPresetFlow.collect { + _liveSelectedPreset.postValue(it) + } + } + + uiScope.launch { + presetService.presetRemovedFlow.collect { presetDrinkDao.remove(it) } } - presetService.onPresetUpdated = { - uiScope.launch { + uiScope.launch { + presetService.presetAddedFlow.collect { + presetDrinkDao.insert(it) + } + } + + uiScope.launch { + presetService.presetUpdatedFlow.collect { presetDrinkDao.update(it) } } @@ -151,4 +166,7 @@ class DrinkRepository(context: Context, drinkDatabase: DrinkDatabase) { presetService.ingestionService = ingestionService } -} \ No newline at end of file + override fun close() { + daoJob.cancel() + } +} diff --git a/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLawRepository.kt b/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLawRepository.kt new file mode 100644 index 0000000..43b8ed9 --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLawRepository.kt @@ -0,0 +1,110 @@ +package com.vaudibert.canidrive.data.repository + +import android.content.Context +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import com.vaudibert.canidrive.R +import com.vaudibert.canidrive.domain.drivelaw.DriveLaw +import com.vaudibert.canidrive.domain.drivelaw.DriveLawService +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.drop +import kotlinx.coroutines.launch +import java.util.* + +class DriveLawRepository(private val context: Context) { + val driveLawService = + DriveLawService( + { code: String -> Locale("", code).displayCountry }, + context.getString(R.string.other), + DriveLaws.loadLaws(context), + DriveLaws.default, + ) + + private val _liveDriveLaw = MutableLiveData() + private val _liveIsYoung = MutableLiveData() + private val _liveIsProfessional = MutableLiveData() + private val _liveCustomCountryLimit = MutableLiveData() + + val liveDriveLaw: LiveData + get() = _liveDriveLaw + val liveIsYoung: LiveData + get() = _liveIsYoung + val liveIsProfessional: LiveData + get() = _liveIsProfessional + val liveCustomCountryLimit: LiveData + get() = _liveCustomCountryLimit + + init { + val masterKey = + MasterKey.Builder(context) + .setKeyScheme(MasterKey.KeyScheme.AES256_GCM) + .build() + val sharedPref = + EncryptedSharedPreferences.create( + context, + context.getString(R.string.user_preferences), + masterKey, + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + + // Initiate drive law service + driveLawService.isYoung = sharedPref.getBoolean(context.getString(R.string.user_young_driver), false) + driveLawService.isProfessional = sharedPref.getBoolean(context.getString(R.string.user_professional_driver), false) + + driveLawService.select( + sharedPref.getString(context.getString(R.string.countryCode), "") ?: "", + ) + driveLawService.customCountryLimit = sharedPref.getFloat(context.getString(R.string.customCountryLimit), 0.0F).toDouble() + + // Set collectors once drive law service initialized + CoroutineScope(Dispatchers.IO).launch { + launch { + driveLawService.driveLawFlow.drop(1).collect { law -> + sharedPref.edit() + .putString(context.getString(R.string.countryCode), law.countryCode) + .apply() + _liveDriveLaw.postValue(law) + } + } + + launch { + driveLawService.isYoungFlow.drop(1).collect { isYoung -> + sharedPref.edit() + .putBoolean(context.getString(R.string.user_young_driver), isYoung) + .apply() + _liveIsYoung.postValue(isYoung) + _liveDriveLaw.postValue(driveLawService.driveLaw) + } + } + + launch { + driveLawService.isProfessionalFlow.drop(1).collect { isProfessional -> + sharedPref.edit() + .putBoolean(context.getString(R.string.user_professional_driver), isProfessional) + .apply() + _liveIsProfessional.postValue(isProfessional) + _liveDriveLaw.postValue(driveLawService.driveLaw) + } + } + + launch { + driveLawService.customCountryLimitFlow.drop(1).collect { newLimit -> + sharedPref.edit() + .putFloat(context.getString(R.string.customCountryLimit), newLimit.toFloat()) + .apply() + _liveCustomCountryLimit.postValue(newLimit) + } + } + } + + // Initialize live datas with initial state from flows + _liveDriveLaw.value = driveLawService.driveLaw + _liveIsYoung.value = driveLawService.isYoung + _liveIsProfessional.value = driveLawService.isProfessional + _liveCustomCountryLimit.value = driveLawService.customCountryLimit + } +} diff --git a/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLaws.kt b/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLaws.kt new file mode 100644 index 0000000..b3b8caf --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLaws.kt @@ -0,0 +1,39 @@ +package com.vaudibert.canidrive.data.repository + +import android.content.Context +import com.vaudibert.canidrive.domain.drivelaw.DriveLaw +import com.vaudibert.canidrive.domain.drivelaw.ProfessionalLimit +import com.vaudibert.canidrive.domain.drivelaw.YoungLimit +import org.json.JSONArray + +object DriveLaws { + val default = DriveLaw("", 0.0) + + fun loadLaws(context: Context): List { + val jsonString = context.assets.open("drive_laws.json").bufferedReader().use { it.readText() } + val jsonArray = JSONArray(jsonString) + val list = mutableListOf() + + for (i in 0 until jsonArray.length()) { + val obj = jsonArray.getJSONObject(i) + val code = obj.getString("countryCode") + val limit = obj.getDouble("limit") + + var youngLimit: YoungLimit? = null + if (!obj.isNull("youngLimit")) { + val yObj = obj.getJSONObject("youngLimit") + youngLimit = YoungLimit(yObj.getDouble("limit"), yObj.getString("explanationName")) + } + + var profLimit: ProfessionalLimit? = null + if (!obj.isNull("professionalLimit")) { + val pObj = obj.getJSONObject("professionalLimit") + profLimit = ProfessionalLimit(pObj.getDouble("limit")) + } + + list.add(DriveLaw(code, limit, youngLimit, profLimit)) + } + + return list + } +} diff --git a/app/src/main/java/com/vaudibert/canidrive/data/repository/MainRepository.kt b/app/src/main/java/com/vaudibert/canidrive/data/repository/MainRepository.kt new file mode 100644 index 0000000..3bfa1b4 --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/data/repository/MainRepository.kt @@ -0,0 +1,73 @@ +package com.vaudibert.canidrive.data.repository + +import android.content.Context +import androidx.room.Room +import androidx.room.migration.Migration +import androidx.security.crypto.EncryptedSharedPreferences +import androidx.security.crypto.MasterKey +import androidx.sqlite.db.SupportSQLiteDatabase +import com.vaudibert.canidrive.R +import com.vaudibert.canidrive.data.DrinkDatabase +import com.vaudibert.canidrive.domain.DrinkerStatusService + +class MainRepository(private val context: Context) { + private val migration_1_2 = + object : Migration(1, 2) { + override fun migrate(database: SupportSQLiteDatabase) { + // 1. Add the `name` column with a default value for existing rows. + // NOT NULL DEFAULT '' matches the v2 schema (TEXT NOT NULL). + database.execSQL( + "ALTER TABLE DrinkEntity ADD COLUMN `name` TEXT NOT NULL DEFAULT ''", + ) + // 2. Rename the table to its new name. + database.execSQL( + "ALTER TABLE DrinkEntity RENAME TO IngestedDrinkEntity", + ) + // 3. Create the PresetDrinkEntity table that was added in v2. + database.execSQL( + "CREATE TABLE IF NOT EXISTS `PresetDrinkEntity` " + + "(`uid` INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, " + + "`name` TEXT NOT NULL, " + + "`volume` REAL NOT NULL, " + + "`degree` REAL NOT NULL, " + + "`count` INTEGER NOT NULL)", + ) + } + } + + private val drinkDatabase = + Room + .databaseBuilder(context, DrinkDatabase::class.java, "drink-database") + .addMigrations(migration_1_2) + .build() + + val drinkRepository = DrinkRepository(context, drinkDatabase) + + val digestionRepository = DigestionRepository(context, drinkRepository.ingestionService) + + val driveLawRepository = DriveLawRepository(context) + + val drinkerStatusService = + DrinkerStatusService( + digestionRepository.digestionService, + driveLawRepository.driveLawService, + ) + + private val sharedPref = + EncryptedSharedPreferences.create( + context, + context.getString(R.string.user_preferences), + MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build(), + EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV, + EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM, + ) + + // Flag for initialization, saved when set. + var init: Boolean = sharedPref.getBoolean(context.getString(R.string.user_initialized), false) + set(value) { + field = value + sharedPref.edit() + .putBoolean(context.getString(R.string.user_initialized), value) + .apply() + } +} diff --git a/app/src/main/java/com/vaudibert/canidrive/di/AppModule.kt b/app/src/main/java/com/vaudibert/canidrive/di/AppModule.kt new file mode 100644 index 0000000..f1fb8a5 --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/di/AppModule.kt @@ -0,0 +1,21 @@ +package com.vaudibert.canidrive.di + +import com.vaudibert.canidrive.data.repository.MainRepository +import org.koin.android.ext.koin.androidContext +import org.koin.androidx.viewmodel.dsl.viewModel +import org.koin.dsl.module + +val appModule = + module { + // Singletons for Repositories + single { MainRepository(androidContext()) } + single { get().drinkRepository } + single { get().digestionRepository } + single { get().driveLawRepository } + + // ViewModels + viewModel { com.vaudibert.canidrive.ui.fragment.AddDrinkViewModel(get()) } + viewModel { com.vaudibert.canidrive.ui.fragment.DrinkerViewModel(get()) } + viewModel { com.vaudibert.canidrive.ui.fragment.DriveViewModel(get()) } + viewModel { com.vaudibert.canidrive.ui.fragment.EditPresetViewModel(get()) } + } diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/DrinkerStatus.kt b/app/src/main/java/com/vaudibert/canidrive/domain/DrinkerStatus.kt index 2d7c3e1..e5c5d6e 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/DrinkerStatus.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/DrinkerStatus.kt @@ -3,8 +3,8 @@ package com.vaudibert.canidrive.domain import java.util.* data class DrinkerStatus( - val canDrive : Boolean, + val canDrive: Boolean, val alcoholRate: Double, - val canDriveDate : Date, - val soberDate : Date -) \ No newline at end of file + val canDriveDate: Date, + val soberDate: Date, +) diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/DrinkerStatusService.kt b/app/src/main/java/com/vaudibert/canidrive/domain/DrinkerStatusService.kt index ddef5ee..6d605cd 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/DrinkerStatusService.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/DrinkerStatusService.kt @@ -6,16 +6,16 @@ import java.util.* class DrinkerStatusService( private val digestionService: DigestionService, - private val driveLawService: DriveLawService + private val driveLawService: DriveLawService, ) { - fun status() : DrinkerStatus { + fun status(): DrinkerStatus { val driveLimit = driveLawService.driveLimit() val ratePresent = digestionService.alcoholRateAt(Date()) return DrinkerStatus( ratePresent <= driveLimit, ratePresent, digestionService.timeToReachLimit(driveLimit), - digestionService.timeToReachLimit(driveLawService.defaultLimit) + digestionService.timeToReachLimit(driveLawService.defaultLimit), ) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/ITimeService.kt b/app/src/main/java/com/vaudibert/canidrive/domain/ITimeService.kt index 75d59bb..71f4e4c 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/ITimeService.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/ITimeService.kt @@ -2,5 +2,6 @@ package com.vaudibert.canidrive.domain interface ITimeService { fun nowInMillis(): Long + fun isSaintPatrick(): Boolean -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/digestion/DigestionService.kt b/app/src/main/java/com/vaudibert/canidrive/domain/digestion/DigestionService.kt index 6f68420..e14dbae 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/digestion/DigestionService.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/digestion/DigestionService.kt @@ -18,9 +18,8 @@ import kotlin.math.max */ class DigestionService( private var body: PhysicalBody, - private val drinkProvider: IIngestedDrinkProvider + private val drinkProvider: IIngestedDrinkProvider, ) { - // TODO : extract a time service to avoid java.util dependency ? fun alcoholRateAt(date: Date): Double { val drinks = drinkProvider.getDrinks() @@ -32,30 +31,34 @@ class DigestionService( drinks.forEach { lastRate = newRate(lastRate, lastIngestion, it.ingestionTime) + - (it.alcoholMass() / body.effectiveWeight + (body.decreaseFactor/2)) + (it.alcoholMass() / body.effectiveWeight + (body.decreaseFactor / 2)) lastIngestion = it.ingestionTime } return newRate(lastRate, lastIngestion, date) } - fun timeToReachLimit(limit: Double) : Date { + fun timeToReachLimit(limit: Double): Date { val now = Date() val rate = alcoholRateAt(now) if (rate < limit) return now return Date( - now.time + ((rate-limit) / body.decreaseFactor* 3_600_000).toLong() + now.time + ((rate - limit) / body.decreaseFactor * 3_600_000).toLong(), ) } /** * Computes the alcohol rate since the last ingestion. */ - private fun newRate(lastRate: Double, lastIngestion: Date, now: Date) : Double { - val timeLapse = ((now.time - lastIngestion.time).toDouble() / (3600*1000)) + private fun newRate( + lastRate: Double, + lastIngestion: Date, + now: Date, + ): Double { + val timeLapse = ((now.time - lastIngestion.time).toDouble() / (3600 * 1000)) - val newRate = lastRate - (body.decreaseFactor* timeLapse) + val newRate = lastRate - (body.decreaseFactor * timeLapse) return max(0.0, newRate) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/digestion/PhysicalBody.kt b/app/src/main/java/com/vaudibert/canidrive/domain/digestion/PhysicalBody.kt index ebeb94b..30df9e8 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/digestion/PhysicalBody.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/digestion/PhysicalBody.kt @@ -1,34 +1,43 @@ package com.vaudibert.canidrive.domain.digestion +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + /** * Represents the person drinking. * The parameters such as weight and sex may change as the user adjusts the inputs. */ class PhysicalBody { - // TODO : find a more kotlin way to declare these constants - private val MALE = "MALE" - private val MALE_SEX_FACTOR = 0.7 - private val MALE_MIN_DECREASE = 0.1 - private val MALE_MAX_DECREASE = 0.15 + companion object { + private const val MALE_SEX_FACTOR = 0.7 + private const val MALE_MIN_DECREASE = 0.1 + private const val MALE_MAX_DECREASE = 0.15 + + private const val FEMALE_SEX_FACTOR = 0.6 + private const val FEMALE_MIN_DECREASE = 0.085 + private const val FEMALE_MAX_DECREASE = 0.1 + } + + data class BodyState(val sex: Sex, val weight: Double, val alcoholTolerance: Double) - private val FEMALE_SEX_FACTOR = 0.6 - private val FEMALE_MIN_DECREASE = 0.085 - private val FEMALE_MAX_DECREASE = 0.1 + private val _bodyState = MutableStateFlow(BodyState(Sex.OTHER, 80.0, 0.0)) + val bodyState: StateFlow = _bodyState.asStateFlow() - var sex = "NONE" + var sex: Sex = Sex.OTHER set(value) { field = value - effectiveWeight = weight * (if (sex == MALE) MALE_SEX_FACTOR else FEMALE_SEX_FACTOR) + effectiveWeight = weight * (if (sex == Sex.MALE) MALE_SEX_FACTOR else FEMALE_SEX_FACTOR) decreaseFactor = decreaseFactorWith(value, alcoholTolerance) - onUpdate(sex, weight, alcoholTolerance) + _bodyState.value = BodyState(sex, weight, alcoholTolerance) } var weight = 80.0 set(value) { field = value - effectiveWeight = weight * (if (sex == MALE) MALE_SEX_FACTOR else FEMALE_SEX_FACTOR) - onUpdate(sex, weight, alcoholTolerance) + effectiveWeight = weight * (if (sex == Sex.MALE) MALE_SEX_FACTOR else FEMALE_SEX_FACTOR) + _bodyState.value = BodyState(sex, weight, alcoholTolerance) } var alcoholTolerance = 0.0 @@ -36,21 +45,22 @@ class PhysicalBody { if (value in 0.0..1.0) { field = value decreaseFactor = decreaseFactorWith(sex, value) - onUpdate(sex, weight, alcoholTolerance) + _bodyState.value = BodyState(sex, weight, alcoholTolerance) } } - private fun decreaseFactorWith(sex: String, tolerance: Double): Double { - return if (sex == "MALE") - tolerance * MALE_MAX_DECREASE + (1-tolerance) * MALE_MIN_DECREASE - else - tolerance * FEMALE_MAX_DECREASE + (1-tolerance) * FEMALE_MIN_DECREASE + private fun decreaseFactorWith( + sex: Sex, + tolerance: Double, + ): Double { + return if (sex == Sex.MALE) { + tolerance * MALE_MAX_DECREASE + (1 - tolerance) * MALE_MIN_DECREASE + } else { + tolerance * FEMALE_MAX_DECREASE + (1 - tolerance) * FEMALE_MIN_DECREASE + } } - var onUpdate = { _ : String, _ : Double, _ : Double -> } - var decreaseFactor: Double = FEMALE_MIN_DECREASE - var effectiveWeight:Double = weight * FEMALE_SEX_FACTOR - -} \ No newline at end of file + var effectiveWeight: Double = weight * FEMALE_SEX_FACTOR +} diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/digestion/Sex.kt b/app/src/main/java/com/vaudibert/canidrive/domain/digestion/Sex.kt new file mode 100644 index 0000000..8c1baf3 --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/domain/digestion/Sex.kt @@ -0,0 +1,22 @@ +package com.vaudibert.canidrive.domain.digestion + +/** + * Biological sex used for BAC calculation (Widmark formula). + * The sex factor affects the volume of distribution of alcohol in the body. + */ +enum class Sex { + MALE, + FEMALE, + OTHER, + ; + + companion object { + fun fromString(value: String): Sex { + return when (value.uppercase()) { + "MALE" -> MALE + "FEMALE" -> FEMALE + else -> OTHER + } + } + } +} diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/drink/Drink.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drink/Drink.kt index 82d53b0..20ef2f4 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/drink/Drink.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/drink/Drink.kt @@ -1,3 +1,3 @@ package com.vaudibert.canidrive.domain.drink -open class Drink(val volume: Double, val degree: Double) \ No newline at end of file +open class Drink(val volume: Double, val degree: Double) diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestCapable.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestCapable.kt index ffb2bfd..889f289 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestCapable.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestCapable.kt @@ -3,5 +3,8 @@ package com.vaudibert.canidrive.domain.drink import java.util.* interface IIngestCapable { - fun ingest(preset : Preset, ingestionTime : Date) -} \ No newline at end of file + fun ingest( + preset: Preset, + ingestionTime: Date, + ) +} diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestedDrink.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestedDrink.kt index 233500c..13f63c6 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestedDrink.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestedDrink.kt @@ -7,5 +7,6 @@ interface IIngestedDrink { val volume: Double val degree: Double val ingestionTime: Date + fun alcoholMass(): Double -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestedDrinkProvider.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestedDrinkProvider.kt index cd50f9e..a9006d5 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestedDrinkProvider.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IIngestedDrinkProvider.kt @@ -1,5 +1,5 @@ package com.vaudibert.canidrive.domain.drink interface IIngestedDrinkProvider { - fun getDrinks() : List -} \ No newline at end of file + fun getDrinks(): List +} diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IPresetDrink.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IPresetDrink.kt index aea8cef..3ef2cdf 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IPresetDrink.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IPresetDrink.kt @@ -5,4 +5,4 @@ interface IPresetDrink { var volume: Double var degree: Double var count: Int -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IngestedDrink.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IngestedDrink.kt index 9c82874..5a4d025 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IngestedDrink.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IngestedDrink.kt @@ -5,62 +5,60 @@ import java.util.* const val ALCOHOL_DENSITY = 0.8 open class IngestedDrink( - override val name:String, - override val volume:Double, - override val degree:Double, - override val ingestionTime: Date + override val name: String, + override val volume: Double, + override val degree: Double, + override val ingestionTime: Date, ) : IIngestedDrink { - - override fun alcoholMass(): Double = degree/100 * volume * ALCOHOL_DENSITY + override fun alcoholMass(): Double = degree / 100 * volume * ALCOHOL_DENSITY companion object Data { - // degrees in % : 2.5 = 2.5% - val degrees = doubleArrayOf( - 0.5, - 2.5, - 3.0, - 3.5, - 4.0, - 4.5, - 5.0, - 5.5, - 6.0, - 6.5, - 7.0, - 7.5, - 8.0, - 9.0, - 10.0, - 11.0, - 12.0, - 13.0, - 14.0, - 15.0, - 17.0, - 20.0, - 25.0, - 30.0, - 40.0, - 60.0, - 80.0) + val degrees = + doubleArrayOf( + 0.5, + 2.5, + 3.0, + 3.5, + 4.0, + 4.5, + 5.0, + 5.5, + 6.0, + 6.5, + 7.0, + 7.5, + 8.0, + 9.0, + 10.0, + 11.0, + 12.0, + 13.0, + 14.0, + 15.0, + 17.0, + 20.0, + 25.0, + 30.0, + 40.0, + 60.0, + 80.0, + ) // Volumes in mL - val volumes = doubleArrayOf( - 20.0, - 30.0, - 50.0, - 80.0, - 130.0, - 200.0, - 250.0, - 330.0, - 500.0, - 750.0, - 1000.0) - + val volumes = + doubleArrayOf( + 20.0, + 30.0, + 50.0, + 80.0, + 130.0, + 200.0, + 250.0, + 330.0, + 500.0, + 750.0, + 1000.0, + ) } } - - - diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IngestionService.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IngestionService.kt index 8f22b58..ac8c340 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/drink/IngestionService.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/drink/IngestionService.kt @@ -3,29 +3,38 @@ package com.vaudibert.canidrive.domain.drink import java.util.* class IngestionService( - private val ingestFunction : (preset: Preset, ingestionTime: Date) -> Ingested + private val ingestFunction: (preset: Preset, ingestionTime: Date) -> Ingested, ) : IIngestCapable, IIngestedDrinkProvider { + private val ingestedDrinks: MutableList = mutableListOf() - private val ingestedDrinks : MutableList = mutableListOf() - - var onRemoved = { _ : Ingested -> } + var onRemoved = { _: Ingested -> } + var onAdded = { _: Ingested -> } var onIngestedChanged = { _: List -> } - override fun ingest(preset : Preset, ingestionTime : Date) { - val ingested = ingestFunction(preset, ingestionTime) + override fun ingest( + preset: Preset, + ingestionTime: Date, + ) { + val ingested = ingestFunction(preset, ingestionTime) ingestedDrinks.add(ingested) sortAndListCallBack() } override fun getDrinks(): List = ingestedDrinks - fun remove(ingested : Ingested) { + fun remove(ingested: Ingested) { ingestedDrinks.remove(ingested) onRemoved(ingested) sortAndListCallBack() } - fun populate(ingests : List) { + fun add(ingested: Ingested) { + ingestedDrinks.add(ingested) + onAdded(ingested) + sortAndListCallBack() + } + + fun populate(ingests: List) { ingestedDrinks.addAll(ingests) sortAndListCallBack() } @@ -34,4 +43,4 @@ class IngestionService( ingestedDrinks.sortBy { it.ingestionTime.time } onIngestedChanged(ingestedDrinks) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/drink/PresetDrink.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drink/PresetDrink.kt index a2c00a1..e76b26a 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/drink/PresetDrink.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/drink/PresetDrink.kt @@ -4,21 +4,20 @@ open class PresetDrink( override var name: String, override var volume: Double, override var degree: Double, - override var count: Int = 0 + override var count: Int = 0, ) : IPresetDrink { override fun equals(other: Any?): Boolean { if (other !is PresetDrink) return false return this.name == other.name && - this.volume == other.volume && - this.degree == other.degree + this.volume == other.volume && + this.degree == other.degree } override fun hashCode(): Int { var result = name.hashCode() result = 31 * result + volume.hashCode() result = 31 * result + degree.hashCode() - result = 31 * result + count return result } } diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/drink/PresetDrinkService.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drink/PresetDrinkService.kt index 614e3a2..85924bb 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/drink/PresetDrinkService.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/drink/PresetDrinkService.kt @@ -1,32 +1,51 @@ package com.vaudibert.canidrive.domain.drink +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharedFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asSharedFlow +import kotlinx.coroutines.flow.asStateFlow import java.util.* class PresetDrinkService( - private val presetMaker : (name: String, volume:Double, degree: Double) -> Preset + private val presetMaker: (name: String, volume: Double, degree: Double) -> Preset, ) { - var onPresetRemoved = { _:Preset -> } - var onPresetsChanged = { _: List -> } - var onPresetUpdated = { _:Preset -> } + private val _presetRemovedFlow = MutableSharedFlow(extraBufferCapacity = 10) + val presetRemovedFlow: SharedFlow = _presetRemovedFlow.asSharedFlow() - var onSelectUpdated = { _:Preset? -> } + private val _presetAddedFlow = MutableSharedFlow(extraBufferCapacity = 10) + val presetAddedFlow: SharedFlow = _presetAddedFlow.asSharedFlow() - var ingestionService : IIngestCapable? = null + private val _presetUpdatedFlow = MutableSharedFlow(extraBufferCapacity = 10) + val presetUpdatedFlow: SharedFlow = _presetUpdatedFlow.asSharedFlow() - var selectedPreset : Preset? = null + private val _presetsFlow = MutableStateFlow>(emptyList()) + val presetsFlow: StateFlow> = _presetsFlow.asStateFlow() + + private val _selectedPresetFlow = MutableStateFlow(null) + val selectedPresetFlow: StateFlow = _selectedPresetFlow.asStateFlow() + + var ingestionService: IIngestCapable? = null + + var selectedPreset: Preset? + get() = _selectedPresetFlow.value set(value) { - field = value - onSelectUpdated(field) + _selectedPresetFlow.value = value } - private var presetDrinks : MutableList = mutableListOf() + private var presetDrinks: MutableList = mutableListOf() - fun populate(presets : List) { + fun populate(presets: List) { presetDrinks.addAll(presets) sortAndCallbackPresets() } - fun addNewPreset(name: String, volume: Double, degree: Double) { + fun addNewPreset( + name: String, + volume: Double, + degree: Double, + ) { val newPreset = presetMaker(name, volume, degree) presetDrinks.add(newPreset) selectedPreset = newPreset @@ -35,26 +54,36 @@ class PresetDrinkService( private fun sortAndCallbackPresets() { presetDrinks.sortByDescending { it.count } - onPresetsChanged(presetDrinks) + _presetsFlow.value = presetDrinks.toList() } fun removePreset(presetDrink: Preset) { presetDrinks.remove(presetDrink) if (selectedPreset == presetDrink) selectedPreset = null - onPresetRemoved(presetDrink) + _presetRemovedFlow.tryEmit(presetDrink) sortAndCallbackPresets() } - fun updateSelectedPreset(name: String, volume: Double, degree: Double) { + fun addPreset(presetDrink: Preset) { + presetDrinks.add(presetDrink) + _presetAddedFlow.tryEmit(presetDrink) + sortAndCallbackPresets() + } + + fun updateSelectedPreset( + name: String, + volume: Double, + degree: Double, + ) { val currentSelected = selectedPreset - if (currentSelected == null) + if (currentSelected == null) { addNewPreset(name, volume, degree) - else { + } else { currentSelected.name = name currentSelected.volume = volume currentSelected.degree = degree selectedPreset = currentSelected - onPresetUpdated(currentSelected) + _presetUpdatedFlow.tryEmit(currentSelected) sortAndCallbackPresets() } } @@ -64,10 +93,9 @@ class PresetDrinkService( val preset = selectedPreset if (ingester != null && preset != null) { preset.count++ - onPresetUpdated(preset) + _presetUpdatedFlow.tryEmit(preset) sortAndCallbackPresets() ingester.ingest(preset, ingestionTime) } } - -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/drivelaw/DriveLaw.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drivelaw/DriveLaw.kt index 1c41614..fa9d51f 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/drivelaw/DriveLaw.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/drivelaw/DriveLaw.kt @@ -1,16 +1,14 @@ package com.vaudibert.canidrive.domain.drivelaw - data class DriveLaw( - val countryCode:String, - val limit:Double = 0.0, + val countryCode: String, + val limit: Double = 0.0, val youngLimit: YoungLimit? = null, - val professionalLimit: ProfessionalLimit? = null + val professionalLimit: ProfessionalLimit? = null, ) { fun isCustom() = countryCode.isEmpty() } -data class YoungLimit(val limit:Double = 0.0, val explanationId: Int) - -data class ProfessionalLimit(val limit:Double = 0.0) +data class YoungLimit(val limit: Double = 0.0, val explanationName: String = "") +data class ProfessionalLimit(val limit: Double = 0.0) diff --git a/app/src/main/java/com/vaudibert/canidrive/domain/drivelaw/DriveLawService.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drivelaw/DriveLawService.kt index c4d6862..3aad163 100644 --- a/app/src/main/java/com/vaudibert/canidrive/domain/drivelaw/DriveLawService.kt +++ b/app/src/main/java/com/vaudibert/canidrive/domain/drivelaw/DriveLawService.kt @@ -1,88 +1,106 @@ package com.vaudibert.canidrive.domain.drivelaw +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow import kotlin.math.min class DriveLawService( private val countryNamer: (countryCode: String) -> String, private val defaultName: String, countryList: List, - private val defaultDriveLaw: DriveLaw + private val defaultDriveLaw: DriveLaw, ) { val defaultLimit = 0.0 - var onCustomLimitCallback = { _:Double -> } + private val _customCountryLimitFlow = MutableStateFlow(0.0) + val customCountryLimitFlow: StateFlow = _customCountryLimitFlow.asStateFlow() - var customCountryLimit = 0.0 + var customCountryLimit: Double + get() = _customCountryLimitFlow.value set(value) { - field = value - onCustomLimitCallback(value) + _customCountryLimitFlow.value = value } - var onYoungCallback = { _:Boolean -> } + private val _isYoungFlow = MutableStateFlow(false) + val isYoungFlow: StateFlow = _isYoungFlow.asStateFlow() - var isYoung: Boolean = false + var isYoung: Boolean + get() = _isYoungFlow.value set(value) { - field = value - onYoungCallback(value) + _isYoungFlow.value = value } - var onProfessionalCallback = { _:Boolean -> } + private val _isProfessionalFlow = MutableStateFlow(false) + val isProfessionalFlow: StateFlow = _isProfessionalFlow.asStateFlow() - var isProfessional: Boolean = false + var isProfessional: Boolean + get() = _isProfessionalFlow.value set(value) { - field = value - onProfessionalCallback(value) + _isProfessionalFlow.value = value } - private val countryLaws = countryList - .sortedBy { law -> countryNamer(law.countryCode) } + private val countryLaws = + countryList + .sortedBy { law -> countryNamer(law.countryCode) } - var driveLaw = defaultDriveLaw + private val _driveLawFlow = MutableStateFlow(defaultDriveLaw) + val driveLawFlow: StateFlow = _driveLawFlow.asStateFlow() + + var driveLaw: DriveLaw + get() = _driveLawFlow.value + private set(value) { + _driveLawFlow.value = value + } fun getListOfCountriesWithFlags(): List { return countryLaws.map { law -> - if (law.countryCode == "") + if (law.countryCode == "") { defaultName - else + } else { stringToFlagEmoji(law.countryCode) + " " + countryNamer(law.countryCode) + } } } - - fun getIndexOfCurrent() = countryLaws - .indexOfFirst { - law -> law.countryCode == driveLaw.countryCode - }.coerceAtLeast(0) - - var onSelectCallback = { _:String -> } + fun getIndexOfCurrent() = + countryLaws + .indexOfFirst { + law -> + law.countryCode == driveLaw.countryCode + }.coerceAtLeast(0) fun select(countryCode: String) { driveLaw = countryLaws.find { law -> law.countryCode == countryCode } ?: defaultDriveLaw - onSelectCallback(countryCode) } fun select(position: Int) { - driveLaw = if (position !in countryLaws.indices) - defaultDriveLaw - else - countryLaws[position] - onSelectCallback(driveLaw.countryCode) + driveLaw = + if (position !in countryLaws.indices) { + defaultDriveLaw + } else { + countryLaws[position] + } } - fun driveLimit() : Double { + fun driveLimit(): Double { if (driveLaw == defaultDriveLaw) return customCountryLimit val regularLimit = driveLaw.limit - val youngLimit = if (isYoung) - driveLaw.youngLimit?.limit ?: regularLimit - else - regularLimit + val youngLimit = + if (isYoung) { + driveLaw.youngLimit?.limit ?: regularLimit + } else { + regularLimit + } - val professionalLimit = if (isProfessional) - driveLaw.professionalLimit?.limit ?: regularLimit - else - regularLimit + val professionalLimit = + if (isProfessional) { + driveLaw.professionalLimit?.limit ?: regularLimit + } else { + regularLimit + } return min(youngLimit, professionalLimit) } @@ -101,7 +119,7 @@ class DriveLawService( return twoCharString } - val countryCodeCaps = twoCharString.toUpperCase() // upper case is important because we are calculating offset + val countryCodeCaps = twoCharString.uppercase(java.util.Locale.ROOT) // upper case is important because we are calculating offset val firstLetter = Character.codePointAt(countryCodeCaps, 0) - 0x41 + 0x1F1E6 val secondLetter = Character.codePointAt(countryCodeCaps, 1) - 0x41 + 0x1F1E6 @@ -112,4 +130,4 @@ class DriveLawService( return String(Character.toChars(firstLetter)) + String(Character.toChars(secondLetter)) } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/CanIDrive.kt b/app/src/main/java/com/vaudibert/canidrive/ui/CanIDrive.kt index 51b611e..c72afc7 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/CanIDrive.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/CanIDrive.kt @@ -1,22 +1,22 @@ package com.vaudibert.canidrive.ui import android.app.Application +import com.vaudibert.canidrive.di.appModule import com.vaudibert.canidrive.domain.ITimeService -import com.vaudibert.canidrive.ui.repository.MainRepository +import org.koin.android.ext.koin.androidContext +import org.koin.android.ext.koin.androidLogger +import org.koin.core.context.startKoin class CanIDrive : Application() { - - companion object { - lateinit var instance: CanIDrive - } - - val time : ITimeService = TimeServiceAndroid() - - lateinit var mainRepository : MainRepository + val time: ITimeService = TimeServiceAndroid() override fun onCreate() { super.onCreate() - mainRepository = MainRepository(this) - instance = this + + startKoin { + androidLogger() + androidContext(this@CanIDrive) + modules(appModule) + } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/MainActivity.kt b/app/src/main/java/com/vaudibert/canidrive/ui/MainActivity.kt index f506b69..812105b 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/MainActivity.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/MainActivity.kt @@ -4,40 +4,61 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity +import androidx.appcompat.app.AppCompatDelegate +import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.navigation.findNavController +import androidx.navigation.fragment.NavHostFragment +import androidx.preference.PreferenceManager import com.google.android.material.appbar.AppBarLayout import com.vaudibert.canidrive.R -import kotlinx.android.synthetic.main.activity_main.* +import com.vaudibert.canidrive.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { - - val time = CanIDrive.instance.time + private lateinit var binding: ActivityMainBinding override fun onCreate(savedInstanceState: Bundle?) { + installSplashScreen() + val time = (application as CanIDrive).time + val sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) + val themeMode = + when (sharedPreferences.getString("theme_preference", "system")) { + "light" -> AppCompatDelegate.MODE_NIGHT_NO + "dark" -> AppCompatDelegate.MODE_NIGHT_YES + else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } + AppCompatDelegate.setDefaultNightMode(themeMode) + when { time.isSaintPatrick() -> setTheme(R.style.AppThemeSaintPatrick) else -> setTheme(R.style.AppTheme) } super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) + binding = ActivityMainBinding.inflate(layoutInflater) + setContentView(binding.root) - findNavController(R.id.nav_host_fragment) + val navHostFragment = + supportFragmentManager + .findFragmentById(R.id.nav_host_fragment) as NavHostFragment + navHostFragment.navController .addOnDestinationChangedListener { _, destination, _ -> - appBarDrinker.visibility = if (destination.id == R.id.splashFragment) - AppBarLayout.GONE - else - AppBarLayout.VISIBLE - toolbar.title = when (destination.id) { - R.id.driveFragment -> getString(R.string.can_i_drive_question) - R.id.drinkerFragment -> getString(R.string.about_you) - R.id.addDrinkFragment -> getString(R.string.select_a_drink) - R.id.addPresetFragment -> getString(R.string.add_preset_description) - else -> "" - } + binding.appBarDrinker.visibility = + if (destination.id == R.id.splashFragment) { + AppBarLayout.GONE + } else { + AppBarLayout.VISIBLE + } + binding.toolbar.title = + when (destination.id) { + R.id.driveFragment -> getString(R.string.can_i_drive_question) + R.id.drinkerFragment -> getString(R.string.about_you) + R.id.addDrinkFragment -> getString(R.string.select_a_drink) + R.id.addPresetFragment -> getString(R.string.add_preset_description) + R.id.settingsFragment -> getString(R.string.action_settings) + else -> "" + } } } - override fun onCreateOptionsMenu(menu: Menu): Boolean { // Inflate the menu; this adds items to the action bar if it is present. menuInflater.inflate(R.menu.menu_main, menu) @@ -49,7 +70,10 @@ class MainActivity : AppCompatActivity() { // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. return when (item.itemId) { - R.id.action_settings -> true + R.id.action_settings -> { + findNavController(R.id.nav_host_fragment).navigate(R.id.settingsFragment) + true + } else -> super.onOptionsItemSelected(item) } } diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/TimeServiceAndroid.kt b/app/src/main/java/com/vaudibert/canidrive/ui/TimeServiceAndroid.kt index 8a2fac2..517fcb3 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/TimeServiceAndroid.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/TimeServiceAndroid.kt @@ -4,10 +4,12 @@ import com.vaudibert.canidrive.domain.ITimeService import java.util.* class TimeServiceAndroid : ITimeService { - override fun nowInMillis() = Date().time + override fun nowInMillis() = System.currentTimeMillis() override fun isSaintPatrick(): Boolean { - val now = Date() - return now.month == 3 && (now.day in 16..18) + val calendar = Calendar.getInstance() + val month = calendar.get(Calendar.MONTH) // 0-indexed: March = 2 + val day = calendar.get(Calendar.DAY_OF_MONTH) + return month == Calendar.MARCH && day in 16..18 } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/adapter/IngestedDrinksAdapter.kt b/app/src/main/java/com/vaudibert/canidrive/ui/adapter/IngestedDrinksAdapter.kt index 7cca5ce..dcf752d 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/adapter/IngestedDrinksAdapter.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/adapter/IngestedDrinksAdapter.kt @@ -4,82 +4,86 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.BaseAdapter import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView +import androidx.recyclerview.widget.RecyclerView import com.vaudibert.canidrive.R import com.vaudibert.canidrive.data.IngestedDrinkEntity -import com.vaudibert.canidrive.ui.CanIDrive +import com.vaudibert.canidrive.domain.drink.IngestionService import java.text.DateFormat -import java.text.DecimalFormat +import java.text.NumberFormat import java.util.* class IngestedDrinksAdapter( - val context: Context - ) : BaseAdapter() { - - private var ingestedDrinkList : List = emptyList() - - private val DAY_IN_MILLIS = 3600*1000*24 + val context: Context, + private val ingestionService: IngestionService, +) : RecyclerView.Adapter() { + private var ingestedDrinkList: List = emptyList() + private val DAY_IN_MILLIS = 3600L * 1000 * 24 private val dateFormat = DateFormat.getTimeInstance(DateFormat.SHORT) + private val doubleFormat = + java.text.NumberFormat.getInstance().apply { + maximumFractionDigits = 1 + } - private val doubleFormat : DecimalFormat = DecimalFormat("0.#") - - // TODO : inject service ? - private val ingestionService = - CanIDrive.instance.mainRepository.drinkRepository.ingestionService - - private val inflater: LayoutInflater = - context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater + inner class DrinkViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val propertiesText: TextView = view.findViewById(R.id.textViewPresetDrinkProperties) + val descriptionText: TextView = view.findViewById(R.id.textViewPresetDrinkDescription) + val glassImage: ImageView = view.findViewById(R.id.imageViewPresetDrinkIcon) + val deleteButton: ImageButton = view.findViewById(R.id.buttonRemovePastDrink) + val timeText: TextView = view.findViewById(R.id.textViewPastDrinkTime) + val daysText: TextView = view.findViewById(R.id.textViewPastDays) + } - override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): DrinkViewHolder { + val inflater = LayoutInflater.from(context) + val view = inflater.inflate(R.layout.item_past_drink, parent, false) + return DrinkViewHolder(view) + } - val drinkView = inflater.inflate(R.layout.item_past_drink, parent, false) - val drink = getItem(position) + override fun onBindViewHolder( + holder: DrinkViewHolder, + position: Int, + ) { + val drink = ingestedDrinkList[position] - val propertiesText = drinkView.findViewById(R.id.textViewPresetDrinkProperties) as TextView - val descriptionText = drinkView.findViewById(R.id.textViewPresetDrinkDescription) as TextView - val glassImage = drinkView.findViewById(R.id.imageViewPresetDrinkIcon) as ImageView - val deleteButton = drinkView.findViewById(R.id.buttonRemovePastDrink) as ImageButton - val timeText = drinkView.findViewById(R.id.textViewPastDrinkTime) as TextView - val daysText = drinkView.findViewById(R.id.textViewPastDays) as TextView + holder.propertiesText.text = "${doubleFormat.format(drink.volume)} ml - ${drink.degree} %" + holder.descriptionText.text = drink.name + holder.glassImage.setImageResource(R.drawable.wine_glass) - propertiesText.text = "${doubleFormat.format(drink.volume)} ml - ${drink.degree} %" - descriptionText.text = drink.name - glassImage.setImageResource(R.drawable.wine_glass) val days: Long = (drink.ingestionTime.time / DAY_IN_MILLIS) - (Date().time / DAY_IN_MILLIS) - if (days == 0L) - daysText.visibility = TextView.GONE - else { - daysText.visibility = TextView.VISIBLE - daysText.text = "$days${context.getString(R.string.day_unit)} " + if (days == 0L) { + holder.daysText.visibility = View.GONE + } else { + holder.daysText.visibility = View.VISIBLE + holder.daysText.text = "$days${context.getString(R.string.day_unit)} " } - timeText.text = dateFormat.format(drink.ingestionTime) + holder.timeText.text = dateFormat.format(drink.ingestionTime) - deleteButton.setOnClickListener { + holder.deleteButton.setOnClickListener { ingestionService.remove(drink) + com.google.android.material.snackbar.Snackbar.make( + holder.itemView, + R.string.snackbar_drink_deleted, + com.google.android.material.snackbar.Snackbar.LENGTH_LONG, + ).setAction(R.string.snackbar_undo) { + ingestionService.add(drink) + }.show() } - - return drinkView - } - - override fun getItem(position: Int): IngestedDrinkEntity { - return ingestedDrinkList[position] - } - - override fun getItemId(position: Int): Long { - return position.toLong() } - override fun getCount(): Int { + override fun getItemCount(): Int { return ingestedDrinkList.size } - fun setDrinkList(ingestedDrinks : List) { + fun setDrinkList(ingestedDrinks: List) { ingestedDrinkList = ingestedDrinks notifyDataSetChanged() } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/adapter/PresetDrinksAdapter.kt b/app/src/main/java/com/vaudibert/canidrive/ui/adapter/PresetDrinksAdapter.kt index def3787..18c4b56 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/adapter/PresetDrinksAdapter.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/adapter/PresetDrinksAdapter.kt @@ -4,124 +4,136 @@ import android.content.Context import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.BaseAdapter import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer +import androidx.recyclerview.widget.RecyclerView import com.vaudibert.canidrive.R import com.vaudibert.canidrive.data.PresetDrinkEntity -import com.vaudibert.canidrive.ui.repository.DrinkRepository -import java.text.DecimalFormat +import com.vaudibert.canidrive.data.repository.DrinkRepository +import java.text.NumberFormat class PresetDrinksAdapter( val context: Context, private val lifecycleOwner: LifecycleOwner, private val goToAddPreset: () -> Unit, - private val drinkRepository: DrinkRepository -) : BaseAdapter() { - - private val inflater: LayoutInflater = - context.getSystemService(Context.LAYOUT_INFLATER_SERVICE) as LayoutInflater - + private val drinkRepository: DrinkRepository, +) : RecyclerView.Adapter() { private val presetService = drinkRepository.presetService - - private val doubleFormat : DecimalFormat = DecimalFormat("0.#") + private val doubleFormat = + java.text.NumberFormat.getInstance().apply { + maximumFractionDigits = 1 + } private var presetDrinks: List = emptyList() + private var selectedPreset: PresetDrinkEntity? = null + + companion object { + const val TYPE_ADD_PRESET = 0 + const val TYPE_PRESET_ITEM = 1 + } init { - drinkRepository.livePresetDrinks.observe(lifecycleOwner, Observer { - presetDrinks = it - notifyDataSetChanged() - }) + drinkRepository.livePresetDrinks.observe( + lifecycleOwner, + Observer { + presetDrinks = it + notifyDataSetChanged() + }, + ) + drinkRepository.liveSelectedPreset.observe( + lifecycleOwner, + Observer { + selectedPreset = it + notifyDataSetChanged() + }, + ) } + override fun getItemViewType(position: Int): Int { + return if (position == 0) TYPE_ADD_PRESET else TYPE_PRESET_ITEM + } - override fun getView(position: Int, convertView: View?, parent: ViewGroup?): View { - return if (position == 0) { - getAddPresetView(parent) + override fun onCreateViewHolder( + parent: ViewGroup, + viewType: Int, + ): RecyclerView.ViewHolder { + val inflater = LayoutInflater.from(context) + if (viewType == TYPE_ADD_PRESET) { + val view = inflater.inflate(R.layout.item_add_preset, parent, false) + return AddPresetViewHolder(view) } else { - getPresetView(parent, position) + val view = inflater.inflate(R.layout.item_preset_drink, parent, false) + return PresetViewHolder(view) } } - private fun getPresetView(parent: ViewGroup?, position: Int): View { - val drinkView = inflater.inflate(R.layout.item_preset_drink, parent, false) - val presetDrink = getItem(position) - - val propertiesText = drinkView.findViewById(R.id.textViewPresetDrinkProperties) as TextView - val descriptionText = - drinkView.findViewById(R.id.textViewPresetDrinkDescription) as TextView - val glassImage = drinkView.findViewById(R.id.imageViewPresetDrinkIcon) as ImageView - val deleteButton = drinkView.findViewById(R.id.buttonRemovePresetDrink) as ImageButton - - propertiesText.text = "${doubleFormat.format(presetDrink.volume)} ml - ${presetDrink.degree} %" - descriptionText.text = presetDrink.name - glassImage.setImageResource(R.drawable.wine_glass) - - drinkRepository.liveSelectedPreset.observe(lifecycleOwner, Observer { - updatePresetColor(presetDrink, drinkView, deleteButton, it) - }) - - val clickListener = { _: View -> - presetService.selectedPreset = - if (presetDrink == drinkRepository.liveSelectedPreset.value) - null - else - presetDrink - } - val longClickListener = View.OnLongClickListener { - presetService.selectedPreset = presetDrink - goToAddPreset() - true - } - propertiesText.setOnClickListener(clickListener) - propertiesText.setOnLongClickListener(longClickListener) - - descriptionText.setOnClickListener(clickListener) - descriptionText.setOnLongClickListener(longClickListener) - - glassImage.setOnClickListener(clickListener) - glassImage.setOnLongClickListener(longClickListener) - - deleteButton.setOnClickListener { - if (presetDrink != drinkRepository.liveSelectedPreset.value) return@setOnClickListener - - presetService.removePreset(presetDrink) + override fun onBindViewHolder( + holder: RecyclerView.ViewHolder, + position: Int, + ) { + if (holder is AddPresetViewHolder) { + holder.addDescriptionText.text = context.getString(R.string.add_preset_description) + holder.itemView.setOnClickListener { + presetService.selectedPreset = null + goToAddPreset() + } + } else if (holder is PresetViewHolder) { + val presetDrink = presetDrinks[position - 1] + + holder.propertiesText.text = "${doubleFormat.format(presetDrink.volume)} ml - ${presetDrink.degree} %" + holder.descriptionText.text = presetDrink.name + holder.glassImage.setImageResource(R.drawable.wine_glass) + + updatePresetColor(presetDrink, holder.itemView, holder.deleteButton, selectedPreset) + + val clickListener = { _: View -> + presetService.selectedPreset = + if (presetDrink == drinkRepository.liveSelectedPreset.value) { + null + } else { + presetDrink + } + } + val longClickListener = + View.OnLongClickListener { + presetService.selectedPreset = presetDrink + goToAddPreset() + true + } + + holder.propertiesText.setOnClickListener(clickListener) + holder.propertiesText.setOnLongClickListener(longClickListener) + + holder.descriptionText.setOnClickListener(clickListener) + holder.descriptionText.setOnLongClickListener(longClickListener) + + holder.glassImage.setOnClickListener(clickListener) + holder.glassImage.setOnLongClickListener(longClickListener) + + holder.deleteButton.setOnClickListener { + if (presetDrink != drinkRepository.liveSelectedPreset.value) return@setOnClickListener + presetService.removePreset(presetDrink) + com.google.android.material.snackbar.Snackbar.make( + holder.itemView, + R.string.snackbar_drink_deleted, + com.google.android.material.snackbar.Snackbar.LENGTH_LONG, + ).setAction(R.string.snackbar_undo) { + presetService.addPreset(presetDrink) + }.show() + } } - - return drinkView } - private fun getAddPresetView(parent: ViewGroup?): View { - val addPresetView = inflater.inflate( - R.layout.item_add_preset, - parent, - false - ) - - val addDescriptionText = addPresetView.findViewById( - R.id.textViewAddPresetDescription - ) as TextView - - addDescriptionText.text = context.getString(R.string.add_preset_description) - - addPresetView.setOnClickListener { - //selectedPreset.postValue(null) - presetService.selectedPreset = null - goToAddPreset() - } - - return addPresetView - } + override fun getItemCount(): Int = presetDrinks.size + 1 private fun updatePresetColor( drink: PresetDrinkEntity, drinkView: View, deleteButton: ImageButton, - selected: PresetDrinkEntity? + selected: PresetDrinkEntity?, ) { if (selected != null && drink == selected) { drinkView.setBackgroundResource(R.drawable.background_color_primary) @@ -132,10 +144,14 @@ class PresetDrinksAdapter( } } - override fun getItem(position: Int): PresetDrinkEntity = presetDrinks[position-1] - - override fun getItemId(position: Int): Long = position.toLong() - - override fun getCount(): Int = presetDrinks.size + 1 + inner class AddPresetViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val addDescriptionText: TextView = view.findViewById(R.id.textViewAddPresetDescription) + } -} \ No newline at end of file + inner class PresetViewHolder(view: View) : RecyclerView.ViewHolder(view) { + val propertiesText: TextView = view.findViewById(R.id.textViewPresetDrinkProperties) + val descriptionText: TextView = view.findViewById(R.id.textViewPresetDrinkDescription) + val glassImage: ImageView = view.findViewById(R.id.imageViewPresetDrinkIcon) + val deleteButton: ImageButton = view.findViewById(R.id.buttonRemovePresetDrink) + } +} diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/AddDrinkFragment.kt b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/AddDrinkFragment.kt index 7e68f09..53e0afd 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/AddDrinkFragment.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/AddDrinkFragment.kt @@ -1,6 +1,5 @@ package com.vaudibert.canidrive.ui.fragment - import android.app.Activity import android.os.Bundle import android.view.LayoutInflater @@ -8,69 +7,85 @@ import android.view.View import android.view.ViewGroup import android.widget.Button import android.widget.SeekBar +import android.widget.TextView import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController -import com.vaudibert.canidrive.KeyboardUtils +import androidx.recyclerview.widget.RecyclerView +import com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBar import com.vaudibert.canidrive.R -import com.vaudibert.canidrive.ui.CanIDrive +import com.vaudibert.canidrive.databinding.FragmentAddDrinkBinding import com.vaudibert.canidrive.ui.adapter.PresetDrinksAdapter -import kotlinx.android.synthetic.main.constraint_content_add_drink_delay_button.* -import kotlinx.android.synthetic.main.constraint_content_add_drink_presets.* -import java.util.* +import com.vaudibert.canidrive.ui.util.KeyboardUtils +import org.koin.androidx.viewmodel.ext.android.viewModel +import java.util.Date /** * Fragment to add a drink. */ class AddDrinkFragment : Fragment() { + private val viewModel: AddDrinkViewModel by viewModel() + + private var _binding: FragmentAddDrinkBinding? = null + private val binding get() = _binding!! - private var volume = 0.0 - private var degree = 0.0 private var delay: Long = 0 + // Views from included layouts + private lateinit var listViewPresetDrinks: RecyclerView + private lateinit var textViewWhenText: TextView + private lateinit var seekBarIngestionDelay: VerticalSeekBar + private lateinit var buttonValidateNewDrink: com.google.android.material.floatingactionbutton.FloatingActionButton override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_add_drink, container, false) + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentAddDrinkBinding.inflate(inflater, container, false) + return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { super.onViewCreated(view, savedInstanceState) - val drinkRepository = CanIDrive.instance.mainRepository.drinkRepository + // Initialize views from included layouts + listViewPresetDrinks = view.findViewById(R.id.listViewPresetDrinks) + textViewWhenText = view.findViewById(R.id.textViewWhenText) + seekBarIngestionDelay = view.findViewById(R.id.seekBarIngestionDelay) + buttonValidateNewDrink = view.findViewById(R.id.buttonValidateNewDrink) + + val drinkRepository = viewModel.drinkRepository val presetService = drinkRepository.presetService setDelaySeekBar() val presetDrinksAdapter = PresetDrinksAdapter( - this.context!!, + requireContext(), viewLifecycleOwner, { findNavController().navigate( - AddDrinkFragmentDirections.actionAddDrinkFragmentToAddPresetFragment() + AddDrinkFragmentDirections.actionAddDrinkFragmentToAddPresetFragment(), ) }, - drinkRepository + drinkRepository, ) listViewPresetDrinks.adapter = presetDrinksAdapter - drinkRepository.liveSelectedPreset.observe(viewLifecycleOwner, Observer { - if (it == null) + drinkRepository.liveSelectedPreset.observe(viewLifecycleOwner) { + if (it == null) { buttonValidateNewDrink.visibility = Button.INVISIBLE - else + } else { buttonValidateNewDrink.visibility = Button.VISIBLE - }) - - drinkRepository.livePresetDrinks.observe( - viewLifecycleOwner, - Observer { - presetDrinksAdapter.notifyDataSetChanged() } - ) + } + + drinkRepository.livePresetDrinks.observe(viewLifecycleOwner) { + presetDrinksAdapter.notifyDataSetChanged() + } buttonValidateNewDrink.setOnClickListener { val ingestionTime = Date(Date().time - (delay * 60000)) @@ -78,44 +93,55 @@ class AddDrinkFragment : Fragment() { KeyboardUtils.hideKeyboard(this.activity as Activity) findNavController().navigate( - AddDrinkFragmentDirections.actionAddDrinkFragmentToDriveFragment() + AddDrinkFragmentDirections.actionAddDrinkFragmentToDriveFragment(), ) } - } private fun setDelaySeekBar() { val delays = longArrayOf(0, 20, 40, 60, 90, 120, 180, 300, 480, 720, 1080, 1440) - val delayLabels = arrayOf( - getString(R.string.now), - "20min", - "40min", - "1h", - "1h30", - "2h", - "3h", - "5h", - "8h", - "12h", - "18h", - "24h" - ) + val delayLabels = + arrayOf( + getString(R.string.now), + getString(R.string.delay_20min), + getString(R.string.delay_40min), + getString(R.string.delay_1h), + getString(R.string.delay_1h30), + getString(R.string.delay_2h), + getString(R.string.delay_3h), + getString(R.string.delay_5h), + getString(R.string.delay_8h), + getString(R.string.delay_12h), + getString(R.string.delay_18h), + getString(R.string.delay_24h), + ) val levelCount = delays.size - 1 seekBarIngestionDelay.max = levelCount textViewWhenText.text = delayLabels[0] - seekBarIngestionDelay.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - textViewWhenText.text = delayLabels[progress] - delay = delays[progress] - } - - override fun onStartTrackingTouch(seekBar: SeekBar?) {} - override fun onStopTrackingTouch(seekBar: SeekBar?) {} - }) + seekBarIngestionDelay.setOnSeekBarChangeListener( + object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar?, + progress: Int, + fromUser: Boolean, + ) { + textViewWhenText.text = delayLabels[progress] + delay = delays[progress] + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) {} + + override fun onStopTrackingTouch(seekBar: SeekBar?) {} + }, + ) seekBarIngestionDelay.progress = 0 delay = delays[0] } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/AddDrinkViewModel.kt b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/AddDrinkViewModel.kt new file mode 100644 index 0000000..237db78 --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/AddDrinkViewModel.kt @@ -0,0 +1,11 @@ +package com.vaudibert.canidrive.ui.fragment + +import androidx.lifecycle.ViewModel +import com.vaudibert.canidrive.data.repository.DrinkRepository +import com.vaudibert.canidrive.data.repository.MainRepository + +class AddDrinkViewModel( + val mainRepository: MainRepository, +) : ViewModel() { + val drinkRepository: DrinkRepository = mainRepository.drinkRepository +} diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DrinkerFragment.kt b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DrinkerFragment.kt index 4dc9a7b..ea117af 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DrinkerFragment.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DrinkerFragment.kt @@ -1,28 +1,31 @@ package com.vaudibert.canidrive.ui.fragment - import android.os.Bundle import android.text.Editable import android.text.TextWatcher import android.view.LayoutInflater import android.view.View import android.view.ViewGroup -import android.widget.* -import androidx.core.widget.addTextChangedListener +import android.widget.AdapterView +import android.widget.ArrayAdapter +import android.widget.CheckBox +import android.widget.EditText +import android.widget.RadioButton +import android.widget.SeekBar +import android.widget.Spinner +import android.widget.TextView import androidx.fragment.app.Fragment -import androidx.lifecycle.observe import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController -import com.vaudibert.canidrive.KeyboardUtils import com.vaudibert.canidrive.R +import com.vaudibert.canidrive.data.repository.DigestionRepository +import com.vaudibert.canidrive.data.repository.MainRepository +import com.vaudibert.canidrive.databinding.FragmentDrinkerBinding +import com.vaudibert.canidrive.domain.digestion.Sex import com.vaudibert.canidrive.domain.drivelaw.DriveLaw import com.vaudibert.canidrive.domain.drivelaw.DriveLawService -import com.vaudibert.canidrive.ui.CanIDrive -import com.vaudibert.canidrive.ui.repository.DigestionRepository -import com.vaudibert.canidrive.ui.repository.MainRepository -import kotlinx.android.synthetic.main.constraint_content_drinker_country.* -import kotlinx.android.synthetic.main.constraint_content_drinker_pickers.* -import kotlinx.android.synthetic.main.fragment_drinker.* +import com.vaudibert.canidrive.ui.util.KeyboardUtils +import org.koin.androidx.viewmodel.ext.android.viewModel import kotlin.math.roundToInt /** @@ -30,45 +33,80 @@ import kotlin.math.roundToInt */ // TODO : split into 2 fragments : body and drive law ? class DrinkerFragment : Fragment() { + private val viewModel: DrinkerViewModel by viewModel() + + private var _binding: FragmentDrinkerBinding? = null + private val binding get() = _binding!! private var weight = 0.0 - private var sex = "NONE" + private var sex = Sex.OTHER - private lateinit var mainRepository : MainRepository - private lateinit var digestionRepository : DigestionRepository + private lateinit var mainRepository: MainRepository + private lateinit var digestionRepository: DigestionRepository private lateinit var driveLawService: DriveLawService - override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { + // Views from included layouts (constraint_content_drinker_pickers.xml) + private lateinit var editTextWeight: EditText + private lateinit var radioMale: RadioButton + private lateinit var radioFemale: RadioButton + private lateinit var radioSexOther: RadioButton + private lateinit var seekBarAlcoholTolerance: SeekBar + private lateinit var textViewAlcoholToleranceTextValue: TextView + + // Views from included layouts (constraint_content_drinker_country.xml) + private lateinit var textViewCurrentLimit: TextView + private lateinit var editTextCurrentLimit: EditText + private lateinit var spinnerCountry: Spinner + private lateinit var checkboxYoungDriver: CheckBox + private lateinit var checkboxProfessionalDriver: CheckBox - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_drinker, container, false) + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentDrinkerBinding.inflate(inflater, container, false) + return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { super.onViewCreated(view, savedInstanceState) - mainRepository = CanIDrive.instance.mainRepository - digestionRepository = mainRepository.digestionRepository + // Initialize views from included layouts + editTextWeight = view.findViewById(R.id.editTextWeight) + radioMale = view.findViewById(R.id.radioMale) + radioFemale = view.findViewById(R.id.radioFemale) + radioSexOther = view.findViewById(R.id.radioSexOther) + seekBarAlcoholTolerance = view.findViewById(R.id.seekBarAlcoholTolerance) + textViewAlcoholToleranceTextValue = view.findViewById(R.id.textViewAlcoholToleranceTextValue) - val driveLawRepository = mainRepository.driveLawRepository - driveLawService = driveLawRepository.driveLawService + textViewCurrentLimit = view.findViewById(R.id.textViewCurrentLimit) + editTextCurrentLimit = view.findViewById(R.id.editTextCurrentLimit) + spinnerCountry = view.findViewById(R.id.spinnerCountry) + checkboxYoungDriver = view.findViewById(R.id.checkboxYoungDriver) + checkboxProfessionalDriver = view.findViewById(R.id.checkboxProfessionalDriver) + + mainRepository = viewModel.mainRepository + digestionRepository = viewModel.digestionRepository + + val driveLawRepository = viewModel.driveLawRepository + driveLawService = viewModel.driveLawService setupSpinnerCountry( driveLawService - .getListOfCountriesWithFlags() + .getListOfCountriesWithFlags(), ) setupWeightPicker() setupSexPicker(digestionRepository.body.sex) - setupValidationButton(digestionRepository) - driveLawRepository.liveDriveLaw.observe(this) { driveLaw: DriveLaw -> + driveLawRepository.liveDriveLaw.observe(viewLifecycleOwner) { driveLaw: DriveLaw -> // update the limit area (custom or not) if (driveLaw.isCustom()) { textViewCurrentLimit.visibility = TextView.GONE @@ -78,13 +116,18 @@ class DrinkerFragment : Fragment() { // TODO : ugly structure for driveLimit call, move in driveLaw ? textViewCurrentLimit.text = driveLawService.driveLimit().toString() editTextCurrentLimit.visibility = TextView.GONE - KeyboardUtils.hideKeyboard(this.activity!!) + KeyboardUtils.hideKeyboard(requireActivity()) } // Update for Young driver checkbox visibility (not value) if (driveLaw.youngLimit != null) { checkboxYoungDriver.visibility = CheckBox.VISIBLE - checkboxYoungDriver.text = getString(driveLaw.youngLimit.explanationId) + val resId = resources.getIdentifier(driveLaw.youngLimit.explanationName, "string", requireActivity().packageName) + if (resId != 0) { + checkboxYoungDriver.text = getString(resId) + } else { + checkboxYoungDriver.text = driveLaw.youngLimit.explanationName + } } else { checkboxYoungDriver.visibility = CheckBox.GONE } @@ -97,10 +140,10 @@ class DrinkerFragment : Fragment() { } } - driveLawRepository.liveIsYoung.observe(this) { + driveLawRepository.liveIsYoung.observe(viewLifecycleOwner) { checkboxYoungDriver.isChecked = it } - driveLawRepository.liveIsProfessional.observe(this) { + driveLawRepository.liveIsProfessional.observe(viewLifecycleOwner) { checkboxProfessionalDriver.isChecked = it } @@ -120,52 +163,60 @@ class DrinkerFragment : Fragment() { val levelCount = digestionRepository.toleranceLevels.size - 1 seekBarAlcoholTolerance.max = levelCount - seekBarAlcoholTolerance.setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener { - override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) { - textViewAlcoholToleranceTextValue.text = digestionRepository.toleranceLevels[progress] - } + seekBarAlcoholTolerance.setOnSeekBarChangeListener( + object : SeekBar.OnSeekBarChangeListener { + override fun onProgressChanged( + seekBar: SeekBar?, + progress: Int, + fromUser: Boolean, + ) { + textViewAlcoholToleranceTextValue.text = digestionRepository.toleranceLevels[progress] + } + + override fun onStartTrackingTouch(seekBar: SeekBar?) {} - override fun onStartTrackingTouch(seekBar: SeekBar?) {} - override fun onStopTrackingTouch(seekBar: SeekBar?) {} - }) + override fun onStopTrackingTouch(seekBar: SeekBar?) {} + }, + ) seekBarAlcoholTolerance.progress = (digestionRepository.body.alcoholTolerance * levelCount).roundToInt() textViewAlcoholToleranceTextValue.text = digestionRepository.toleranceLevels[seekBarAlcoholTolerance.progress] } private fun setupValidationButton(digestionRepository: DigestionRepository) { - buttonValidateDrinker.setOnClickListener { - - digestionRepository.body.sex = when { - radioMale.isChecked -> "MALE" - radioFemale.isChecked -> "FEMALE" - else -> "OTHER" - } + binding.buttonValidateDrinker.setOnClickListener { + digestionRepository.body.sex = + when { + radioMale.isChecked -> Sex.MALE + radioFemale.isChecked -> Sex.FEMALE + else -> Sex.OTHER + } val levelCount = (digestionRepository.toleranceLevels.size - 1).coerceAtLeast(1) digestionRepository.body.alcoholTolerance = seekBarAlcoholTolerance.progress.toDouble() / - levelCount.toDouble() + levelCount.toDouble() - driveLawService.customCountryLimit = editTextCurrentLimit.text.toString().toDouble() + driveLawService.customCountryLimit = editTextCurrentLimit.text.toString().toDoubleOrNull() ?: driveLawService.customCountryLimit - var navOptions:NavOptions? = null + var navOptions: NavOptions? = null if (!mainRepository.init) { mainRepository.init = true // Specific option needed for the init of the app, the drinker is the first fragment // and needs to be cleared (take over nav_graph definition). - navOptions = NavOptions.Builder() - .setPopUpTo( - R.id.drinkerFragment, - true - ).build() + navOptions = + NavOptions.Builder() + .setPopUpTo( + R.id.drinkerFragment, + true, + ).build() } findNavController().navigate( DrinkerFragmentDirections.actionDrinkerFragmentToDriveFragment(), - navOptions + navOptions, ) } } @@ -179,87 +230,120 @@ class DrinkerFragment : Fragment() { } } - private fun setupSexPicker(recordedSex: String) { + private fun setupSexPicker(recordedSex: Sex) { sex = recordedSex when (sex) { - "MALE" -> radioMale.isChecked = true - "FEMALE" -> radioFemale.isChecked = true + Sex.MALE -> radioMale.isChecked = true + Sex.FEMALE -> radioFemale.isChecked = true else -> radioSexOther.isChecked = true } } private fun setupWeightPicker() { - val weights = intArrayOf( - 30, 35, 40, 45, 50, 55, 60, 65, 70, 75, 80, 85, 90, 95, - 100, 110, 120, 130, 140, 150 - ) - val weightLabels = weights.map { i -> i.toString() + "kg" }.toTypedArray() - numberPickerWeight.minValue = 0 - numberPickerWeight.maxValue = weights.size - 1 - numberPickerWeight.displayedValues = weightLabels + val currentWeight = digestionRepository.body.weight - numberPickerWeight.value = weights - .indexOf(digestionRepository.body.weight.toInt()) - .coerceAtLeast(0) - - numberPickerWeight.setOnValueChangedListener { _, _, newVal -> - digestionRepository.body.weight = weights[newVal].toDouble() + // Show as integer if it's a whole number, else decimal + if (currentWeight == currentWeight.toInt().toDouble()) { + editTextWeight.setText(currentWeight.toInt().toString()) + } else { + editTextWeight.setText(currentWeight.toString()) } - } - private fun setupSpinnerCountry( - countries: List - ) { - spinnerCountry.adapter = ArrayAdapter( - this.context!!, - R.layout.item_country_spinner, - countries + editTextWeight.addTextChangedListener( + object : TextWatcher { + override fun afterTextChanged(s: Editable) { + var newVal = s.toString().toDoubleOrNull() + if (newVal != null) { + // Prevent completely absurd weights if necessary, or just let it pass + if (newVal < 10.0) newVal = 10.0 + if (newVal > 500.0) newVal = 500.0 + digestionRepository.body.weight = newVal + } + } + + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int, + ) {} + + override fun onTextChanged( + s: CharSequence, + start: Int, + before: Int, + count: Int, + ) {} + }, ) + } + + private fun setupSpinnerCountry(countries: List) { + spinnerCountry.adapter = + ArrayAdapter( + requireContext(), + R.layout.item_country_spinner, + countries, + ) val initialPosition = driveLawService.getIndexOfCurrent() spinnerCountry.setSelection(initialPosition) - spinnerCountry.onItemSelectedListener = object : - AdapterView.OnItemSelectedListener { - override fun onItemSelected( - parent: AdapterView<*>, - view: View?, - position: Int, - id: Long - ) { - if (position == 0) { - // handle the other country case - val customLimit = driveLawService.customCountryLimit - updateCustomLimit(customLimit) - + spinnerCountry.onItemSelectedListener = + object : + AdapterView.OnItemSelectedListener { + override fun onItemSelected( + parent: AdapterView<*>, + view: View?, + position: Int, + id: Long, + ) { + if (position == 0) { + // handle the other country case + val customLimit = driveLawService.customCountryLimit + updateCustomLimit(customLimit) + } + driveLawService.select(position) } - driveLawService.select(position) + override fun onNothingSelected(parent: AdapterView<*>?) {} } - override fun onNothingSelected(parent: AdapterView<*>?) { } - } - - editTextCurrentLimit.addTextChangedListener(object : TextWatcher { - - override fun afterTextChanged(s: Editable) { - driveLawService.customCountryLimit = s.toString().toDouble() - } + editTextCurrentLimit.addTextChangedListener( + object : TextWatcher { + override fun afterTextChanged(s: Editable) { + s.toString().toDoubleOrNull()?.let { + driveLawService.customCountryLimit = it + } + } - override fun beforeTextChanged(s: CharSequence, start: Int, - count: Int, after: Int) { - } + override fun beforeTextChanged( + s: CharSequence, + start: Int, + count: Int, + after: Int, + ) { + } - override fun onTextChanged(s: CharSequence, start: Int, - before: Int, count: Int) { - } - }) + override fun onTextChanged( + s: CharSequence, + start: Int, + before: Int, + count: Int, + ) { + } + }, + ) } private fun updateCustomLimit(customLimit: Double) { editTextCurrentLimit.setText( - ((customLimit * 100.0).roundToInt() / 100.0).toString() + ((customLimit * 100.0).roundToInt() / 100.0).toString(), ) } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DrinkerViewModel.kt b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DrinkerViewModel.kt new file mode 100644 index 0000000..d0c49a8 --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DrinkerViewModel.kt @@ -0,0 +1,22 @@ +package com.vaudibert.canidrive.ui.fragment + +import androidx.lifecycle.ViewModel +import com.vaudibert.canidrive.data.repository.DigestionRepository +import com.vaudibert.canidrive.data.repository.DriveLawRepository +import com.vaudibert.canidrive.data.repository.MainRepository + +class DrinkerViewModel( + val mainRepository: MainRepository, +) : ViewModel() { + val digestionRepository: DigestionRepository = mainRepository.digestionRepository + val driveLawRepository: DriveLawRepository = mainRepository.driveLawRepository + val driveLawService = driveLawRepository.driveLawService + + fun updateTolerance( + progress: Int, + levelCount: Int, + ) { + val count = levelCount.coerceAtLeast(1) + digestionRepository.body.alcoholTolerance = progress.toDouble() / count.toDouble() + } +} diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DriveFragment.kt b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DriveFragment.kt index 4990b3f..a02a72f 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DriveFragment.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DriveFragment.kt @@ -1,6 +1,5 @@ package com.vaudibert.canidrive.ui.fragment - import android.os.Bundle import android.os.Handler import android.os.Looper @@ -11,16 +10,15 @@ import android.widget.LinearLayout import android.widget.TextView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController +import androidx.recyclerview.widget.RecyclerView import com.vaudibert.canidrive.R +import com.vaudibert.canidrive.data.repository.DigestionRepository +import com.vaudibert.canidrive.data.repository.DrinkRepository +import com.vaudibert.canidrive.databinding.FragmentDriveStatusBinding import com.vaudibert.canidrive.domain.DrinkerStatusService -import com.vaudibert.canidrive.ui.CanIDrive import com.vaudibert.canidrive.ui.adapter.IngestedDrinksAdapter -import com.vaudibert.canidrive.ui.repository.DigestionRepository -import com.vaudibert.canidrive.ui.repository.DrinkRepository -import kotlinx.android.synthetic.main.constraint_content_drive_history.* -import kotlinx.android.synthetic.main.fragment_drive_status.* +import org.koin.androidx.viewmodel.ext.android.viewModel import java.text.DateFormat /** @@ -30,6 +28,10 @@ import java.text.DateFormat * - what were the past drinks ? */ class DriveFragment : Fragment() { + private val viewModel: DriveViewModel by viewModel() + + private var _binding: FragmentDriveStatusBinding? = null + private val binding get() = _binding!! private lateinit var digestionRepository: DigestionRepository private lateinit var drinkRepository: DrinkRepository @@ -39,89 +41,111 @@ class DriveFragment : Fragment() { private lateinit var ingestedDrinksAdapter: IngestedDrinksAdapter + // Views from included layout (constraint_content_drive_history.xml) + private lateinit var textViewPastDrinks: TextView + private lateinit var listViewPastDrinks: RecyclerView + override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_drive_status, container, false) + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentDriveStatusBinding.inflate(inflater, container, false) + return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { super.onViewCreated(view, savedInstanceState) - val mainRepository = CanIDrive.instance.mainRepository - drinkerStatusService = mainRepository.drinkerStatusService - drinkRepository = mainRepository.drinkRepository - digestionRepository = mainRepository.digestionRepository + // Initialize views from included layouts + textViewPastDrinks = view.findViewById(R.id.textViewPastDrinks) + listViewPastDrinks = view.findViewById(R.id.listViewPastDrinks) - ingestedDrinksAdapter = IngestedDrinksAdapter(this.context!!) + drinkerStatusService = viewModel.mainRepository.drinkerStatusService + drinkRepository = viewModel.drinkRepository + digestionRepository = viewModel.digestionRepository + + ingestedDrinksAdapter = IngestedDrinksAdapter(requireContext(), drinkRepository.ingestionService) listViewPastDrinks.adapter = ingestedDrinksAdapter - drinkRepository.livePastDrinks.observe(viewLifecycleOwner, Observer { + drinkRepository.livePastDrinks.observe(viewLifecycleOwner) { textViewPastDrinks.visibility = if (it.isEmpty()) TextView.GONE else TextView.VISIBLE ingestedDrinksAdapter.setDrinkList(it.asReversed()) updateDriveStatus() - }) + } // needed for periodic update of drinker status mainHandler = Handler(Looper.getMainLooper()) - buttonToDrinker.setOnClickListener { + binding.buttonToDrinker.setOnClickListener { findNavController().navigate( - DriveFragmentDirections.actionDriveFragmentToDrinkerFragment() + DriveFragmentDirections.actionDriveFragmentToDrinkerFragment(), ) } - buttonAddDrink.setOnClickListener { + binding.buttonAddDrink.setOnClickListener { findNavController().navigate( - DriveFragmentDirections.actionDriveFragmentToAddDrinkFragment() + DriveFragmentDirections.actionDriveFragmentToAddDrinkFragment(), ) } - } + private fun updateDriveStatus() { val drinkerStatus = drinkerStatusService.status() + val label = binding.root.findViewById(R.id.textViewDriveStatusLabel) + if (drinkerStatus.alcoholRate < 0.01) { - linearAlcoholRate.visibility = LinearLayout.GONE - linearWaitToSober.visibility = LinearLayout.GONE + binding.linearAlcoholRate.visibility = LinearLayout.GONE + binding.linearWaitToSober.visibility = LinearLayout.GONE - imageCar.setColorFilter(ContextCompat.getColor(this.context!!, R.color.driveGreen)) - imageDriveStatus.setImageResource(R.drawable.ic_check_white_24dp) - imageDriveStatus.setColorFilter(ContextCompat.getColor(this.context!!, R.color.driveGreen)) - linearWaitToDrive.visibility = LinearLayout.GONE + binding.imageCar.setColorFilter(ContextCompat.getColor(requireContext(), R.color.driveGreen)) + binding.imageDriveStatus.setImageResource(R.drawable.ic_check_white_24dp) + binding.imageDriveStatus.setColorFilter(ContextCompat.getColor(requireContext(), R.color.driveGreen)) + binding.linearWaitToDrive.visibility = LinearLayout.GONE + label.text = getString(R.string.safe_to_drive) + label.setTextColor(ContextCompat.getColor(requireContext(), R.color.driveGreen)) } else { - linearAlcoholRate.visibility = LinearLayout.VISIBLE - linearWaitToSober.visibility = LinearLayout.VISIBLE - textViewAlcoholRate.text = - "${drinkerStatus.alcoholRate.toString().substring(0, 4)} g/L" + binding.linearAlcoholRate.visibility = LinearLayout.VISIBLE + binding.linearWaitToSober.visibility = LinearLayout.VISIBLE - textViewTimeToSober.text = DateFormat - .getTimeInstance(DateFormat.SHORT) - .format(drinkerStatus.soberDate) + val numberFormat = java.text.NumberFormat.getInstance() + numberFormat.maximumFractionDigits = 2 + numberFormat.minimumFractionDigits = 2 + binding.textViewAlcoholRate.text = "${numberFormat.format(drinkerStatus.alcoholRate)} ${getString(R.string.bac_unit_gl)}" + + binding.textViewTimeToSober.text = + DateFormat + .getTimeInstance(DateFormat.SHORT) + .format(drinkerStatus.soberDate) if (drinkerStatus.canDrive) { // Set status icons to drive-able - imageCar.setColorFilter(ContextCompat.getColor(this.context!!, R.color.driveGreen)) - imageDriveStatus.setImageResource(R.drawable.ic_warning_white_24dp) - imageDriveStatus.setColorFilter(ContextCompat.getColor(this.context!!, R.color.driveAmber)) - linearWaitToDrive.visibility = LinearLayout.GONE - textViewAlcoholRate.setTextColor(ContextCompat.getColor(this.context!!,R.color.driveAmber)) + binding.imageCar.setColorFilter(ContextCompat.getColor(requireContext(), R.color.driveGreen)) + binding.imageDriveStatus.setImageResource(R.drawable.ic_warning_white_24dp) + binding.imageDriveStatus.setColorFilter(ContextCompat.getColor(requireContext(), R.color.driveAmber)) + binding.linearWaitToDrive.visibility = LinearLayout.GONE + binding.textViewAlcoholRate.setTextColor(ContextCompat.getColor(requireContext(), R.color.driveAmber)) + + label.text = getString(R.string.safe_to_drive) + label.setTextColor(ContextCompat.getColor(requireContext(), R.color.driveAmber)) } else { - textViewTimeToDrive.text = DateFormat - .getTimeInstance(DateFormat.SHORT) - .format(drinkerStatus.canDriveDate) + binding.textViewTimeToDrive.text = + DateFormat + .getTimeInstance(DateFormat.SHORT) + .format(drinkerStatus.canDriveDate) // Set status icons to NOT drive-able - imageCar.setColorFilter(ContextCompat.getColor(this.context!!, R.color.driveRed)) - imageDriveStatus.setImageResource(R.drawable.ic_forbidden_white_24dp) - imageDriveStatus.setColorFilter(ContextCompat.getColor(this.context!!, R.color.driveRed)) - linearWaitToDrive.visibility = LinearLayout.VISIBLE - textViewAlcoholRate.setTextColor(ContextCompat.getColor(this.context!!,R.color.driveRed)) + binding.imageCar.setColorFilter(ContextCompat.getColor(requireContext(), R.color.driveRed)) + binding.imageDriveStatus.setImageResource(R.drawable.ic_forbidden_white_24dp) + binding.imageDriveStatus.setColorFilter(ContextCompat.getColor(requireContext(), R.color.driveRed)) + binding.linearWaitToDrive.visibility = LinearLayout.VISIBLE + binding.textViewAlcoholRate.setTextColor(ContextCompat.getColor(requireContext(), R.color.driveRed)) } - } ingestedDrinksAdapter.notifyDataSetChanged() } @@ -129,12 +153,13 @@ class DriveFragment : Fragment() { /** * Helper task to update the drive status while app is running. */ - private val updateDriveStatusTask = object : Runnable { - override fun run() { - updateDriveStatus() - mainHandler.postDelayed(this, 1000*60) + private val updateDriveStatusTask = + object : Runnable { + override fun run() { + updateDriveStatus() + mainHandler.postDelayed(this, 1000 * 60) + } } - } override fun onPause() { super.onPause() @@ -146,4 +171,9 @@ class DriveFragment : Fragment() { updateDriveStatus() mainHandler.post(updateDriveStatusTask) } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DriveViewModel.kt b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DriveViewModel.kt new file mode 100644 index 0000000..ca587f2 --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DriveViewModel.kt @@ -0,0 +1,14 @@ +package com.vaudibert.canidrive.ui.fragment + +import androidx.lifecycle.ViewModel +import com.vaudibert.canidrive.data.repository.MainRepository + +class DriveViewModel( + val mainRepository: MainRepository, +) : ViewModel() { + val digestionRepository = mainRepository.digestionRepository + val drinkRepository = mainRepository.drinkRepository + val driveLawRepository = mainRepository.driveLawRepository + val driveLawService = driveLawRepository.driveLawService + val isInit = mainRepository.init +} diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/EditPresetFragment.kt b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/EditPresetFragment.kt index 1bbed10..965a2e3 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/EditPresetFragment.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/EditPresetFragment.kt @@ -5,86 +5,103 @@ import android.os.Bundle import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import android.widget.NumberPicker import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController -import com.vaudibert.canidrive.KeyboardUtils import com.vaudibert.canidrive.R +import com.vaudibert.canidrive.databinding.FragmentAddPresetBinding import com.vaudibert.canidrive.domain.drink.IngestedDrink -import com.vaudibert.canidrive.ui.CanIDrive -import kotlinx.android.synthetic.main.fragment_add_preset.* -import kotlinx.android.synthetic.main.linear_content_add_drink_custom_pickers.* +import com.vaudibert.canidrive.ui.util.KeyboardUtils +import org.koin.androidx.viewmodel.ext.android.viewModel import java.text.DecimalFormat class EditPresetFragment : Fragment() { + private val viewModel: EditPresetViewModel by viewModel() + + private var _binding: FragmentAddPresetBinding? = null + private val binding get() = _binding!! + private var volume = 0.0 private var degree = 0.0 - private val doubleFormat : DecimalFormat = DecimalFormat("0.#") + private val doubleFormat: DecimalFormat = DecimalFormat("0.#") + + // Views from included layout (linear_content_add_drink_custom_pickers.xml) + private lateinit var numberPickerVolume: NumberPicker + private lateinit var numberPickerDegree: NumberPicker override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_add_preset, container, false) + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentAddPresetBinding.inflate(inflater, container, false) + return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { super.onViewCreated(view, savedInstanceState) - val drinkRepository = CanIDrive.instance.mainRepository.drinkRepository - val presetService = drinkRepository.presetService + // Initialize views from included layouts + numberPickerVolume = view.findViewById(R.id.numberPickerVolume) + numberPickerDegree = view.findViewById(R.id.numberPickerDegree) + + val presetService = viewModel.drinkRepository.presetService val selectedPreset = presetService.selectedPreset if (selectedPreset != null) { volume = selectedPreset.volume degree = selectedPreset.degree - editTextNewPresetName.setText(selectedPreset.name) + binding.editTextNewPresetName.setText(selectedPreset.name) } - buttonValidateNewPreset.setOnClickListener { - if (editTextNewPresetName.text.toString().isBlank()) return@setOnClickListener + binding.buttonValidateNewPreset.setOnClickListener { + if (binding.editTextNewPresetName.text.toString().isBlank()) return@setOnClickListener if (selectedPreset != null) { presetService.updateSelectedPreset( - editTextNewPresetName.text.toString(), + binding.editTextNewPresetName.text.toString(), volume, - degree + degree, ) } else { presetService.addNewPreset( - editTextNewPresetName.text.toString(), + binding.editTextNewPresetName.text.toString(), volume, - degree + degree, ) } KeyboardUtils.hideKeyboard(this.activity as Activity) findNavController().navigate( - EditPresetFragmentDirections.actionAddPresetFragmentToAddDrinkFragment() + EditPresetFragmentDirections.actionAddPresetFragmentToAddDrinkFragment(), ) } setVolumePicker() setDegreePicker() - } - private fun setDegreePicker() { - val degreeLabels = IngestedDrink.degrees.map { deg -> - "${doubleFormat.format(deg)} %" - }.toTypedArray() + val degreeLabels = + IngestedDrink.degrees.map { deg -> + "${doubleFormat.format(deg)} %" + }.toTypedArray() numberPickerDegree.minValue = 0 numberPickerDegree.maxValue = degreeLabels.size - 1 numberPickerDegree.displayedValues = degreeLabels - val indexOfDegree = IngestedDrink.degrees.indexOf(degree) - val startDegree = if (indexOfDegree < 0) - degreeLabels.size / 2 - else - indexOfDegree + val indexOfDegree = IngestedDrink.degrees.toList().indexOf(degree) + val startDegree = + if (indexOfDegree < 0) { + degreeLabels.size / 2 + } else { + indexOfDegree + } degree = IngestedDrink.degrees[startDegree] numberPickerDegree.value = startDegree numberPickerDegree.setOnValueChangedListener { _, _, newVal -> @@ -93,24 +110,33 @@ class EditPresetFragment : Fragment() { } private fun setVolumePicker() { - val volumeLabels = IngestedDrink.volumes.map { vol -> - if (vol < 1000.0) - "${doubleFormat.format(vol )} mL" - else - "${doubleFormat.format(vol / 1000.0)} L" - }.toTypedArray() + val volumeLabels = + IngestedDrink.volumes.map { vol -> + if (vol < 1000.0) { + "${doubleFormat.format(vol)} mL" + } else { + "${doubleFormat.format(vol / 1000.0)} L" + } + }.toTypedArray() numberPickerVolume.minValue = 0 numberPickerVolume.maxValue = volumeLabels.size - 1 numberPickerVolume.displayedValues = volumeLabels - val indexOfVolume = IngestedDrink.volumes.indexOf(volume) - val startVolume = if (indexOfVolume < 0) - volumeLabels.size / 2 - else - indexOfVolume + val indexOfVolume = IngestedDrink.volumes.toList().indexOf(volume) + val startVolume = + if (indexOfVolume < 0) { + volumeLabels.size / 2 + } else { + indexOfVolume + } numberPickerVolume.value = startVolume volume = IngestedDrink.volumes[startVolume] numberPickerVolume.setOnValueChangedListener { _, _, newVal -> volume = IngestedDrink.volumes[newVal] } } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/EditPresetViewModel.kt b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/EditPresetViewModel.kt new file mode 100644 index 0000000..ebc6994 --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/EditPresetViewModel.kt @@ -0,0 +1,11 @@ +package com.vaudibert.canidrive.ui.fragment + +import androidx.lifecycle.ViewModel +import com.vaudibert.canidrive.data.repository.DrinkRepository +import com.vaudibert.canidrive.data.repository.MainRepository + +class EditPresetViewModel( + val mainRepository: MainRepository, +) : ViewModel() { + val drinkRepository: DrinkRepository = mainRepository.drinkRepository +} diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/SettingsFragment.kt b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/SettingsFragment.kt new file mode 100644 index 0000000..c723b55 --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/SettingsFragment.kt @@ -0,0 +1,30 @@ +package com.vaudibert.canidrive.ui.fragment + +import android.os.Bundle +import androidx.appcompat.app.AppCompatDelegate +import androidx.preference.ListPreference +import androidx.preference.Preference +import androidx.preference.PreferenceFragmentCompat +import com.vaudibert.canidrive.R + +class SettingsFragment : PreferenceFragmentCompat() { + override fun onCreatePreferences( + savedInstanceState: Bundle?, + rootKey: String?, + ) { + setPreferencesFromResource(R.xml.preferences, rootKey) + + val themePreference: ListPreference? = findPreference("theme_preference") + themePreference?.onPreferenceChangeListener = + Preference.OnPreferenceChangeListener { _, newValue -> + val themeMode = + when (newValue as String) { + "light" -> AppCompatDelegate.MODE_NIGHT_NO + "dark" -> AppCompatDelegate.MODE_NIGHT_YES + else -> AppCompatDelegate.MODE_NIGHT_FOLLOW_SYSTEM + } + AppCompatDelegate.setDefaultNightMode(themeMode) + true + } + } +} diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/SplashFragment.kt b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/SplashFragment.kt index bf4ce78..a73378c 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/fragment/SplashFragment.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/SplashFragment.kt @@ -1,55 +1,103 @@ package com.vaudibert.canidrive.ui.fragment - +import android.content.Context import android.os.Bundle -import android.os.Handler -import android.os.Looper import android.view.LayoutInflater import android.view.View import android.view.ViewGroup +import androidx.appcompat.app.AlertDialog import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.vaudibert.canidrive.BuildConfig import com.vaudibert.canidrive.R -import com.vaudibert.canidrive.ui.CanIDrive -import kotlinx.android.synthetic.main.fragment_splash.* +import com.vaudibert.canidrive.data.repository.MainRepository +import com.vaudibert.canidrive.databinding.FragmentSplashBinding +import org.koin.android.ext.android.inject /** * The splash fragment, to display icon, version and do stuff in background. */ class SplashFragment : Fragment() { + private val mainRepository: MainRepository by inject() + + private var _binding: FragmentSplashBinding? = null + private val binding get() = _binding!! override fun onCreateView( - inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View? { - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_splash, container, false) + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle?, + ): View { + _binding = FragmentSplashBinding.inflate(inflater, container, false) + return binding.root } - override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + override fun onViewCreated( + view: View, + savedInstanceState: Bundle?, + ) { super.onViewCreated(view, savedInstanceState) - textViewVersionName.text = "v" + BuildConfig.VERSION_NAME + binding.textViewVersionName.text = "v" + BuildConfig.VERSION_NAME } override fun onResume() { + super.onResume() + + val sharedPref = + requireContext().getSharedPreferences( + getString(R.string.user_preferences), + Context.MODE_PRIVATE, + ) + val disclaimerAccepted = + sharedPref.getBoolean( + getString(R.string.disclaimer_pref_key), + false, + ) + + if (!disclaimerAccepted) { + showDisclaimerDialog() + } else { + navigateToNext() + } + } - val mainHandler = Handler(Looper.getMainLooper()) - mainHandler.postDelayed( { + private fun showDisclaimerDialog() { + if (!isAdded) return + AlertDialog.Builder(requireContext()) + .setTitle(R.string.disclaimer_title) + .setMessage(R.string.disclaimer_message) + .setCancelable(false) + .setPositiveButton(R.string.disclaimer_accept) { dialog, _ -> + val sharedPref = + requireContext().getSharedPreferences( + getString(R.string.user_preferences), + Context.MODE_PRIVATE, + ) + sharedPref.edit() + .putBoolean(getString(R.string.disclaimer_pref_key), true) + .apply() + dialog.dismiss() + navigateToNext() + } + .show() + } - val init = CanIDrive.instance.mainRepository.init + private fun navigateToNext() { + if (!isAdded) return + val init = mainRepository.init - val action = if (init) + val action = + if (init) { SplashFragmentDirections.actionSplashFragmentToDriveFragment() - else + } else { SplashFragmentDirections.actionSplashFragmentToDrinkerFragment() + } - findNavController().navigate(action) - }, 1000) - - super.onResume() + findNavController().navigate(action) } - - + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DigestionRepository.kt b/app/src/main/java/com/vaudibert/canidrive/ui/repository/DigestionRepository.kt deleted file mode 100644 index 9784fab..0000000 --- a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DigestionRepository.kt +++ /dev/null @@ -1,67 +0,0 @@ -package com.vaudibert.canidrive.ui.repository - -import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.vaudibert.canidrive.R -import com.vaudibert.canidrive.domain.digestion.DigestionService -import com.vaudibert.canidrive.domain.digestion.PhysicalBody -import com.vaudibert.canidrive.domain.drink.IIngestedDrinkProvider - -/** - * Repository holding the drinker and driveLaw instances. - * - * It establishes the link between the drinker absolute state and the drive law limits that depend - * on the country selection. - * - * It is responsible for retrieving all saved values with the given : - * - sharedPreferences instance for : - * - weight - * - sex - * - country code (= drive law) - * - init flag (= user configuration already validated once) - * - drinkDao for past consumed drinks. - */ -class DigestionRepository(context: Context, drinkProvider: IIngestedDrinkProvider) { - - // Main instance to link - val body = PhysicalBody() - - val digestionService = DigestionService(body, drinkProvider) - - val toleranceLevels = listOf( - context.getString(R.string.alcohol_tolerance_low), - context.getString(R.string.alcohol_tolerance_medium), - context.getString(R.string.alcohol_tolerance_high) - ) - - private val _liveDrinker = MutableLiveData() - val liveDrinker:LiveData - get() = _liveDrinker - - init { - val sharedPref = context.getSharedPreferences(context.getString(R.string.user_preferences), Context.MODE_PRIVATE) - - val weight = sharedPref.getFloat(context.getString(R.string.user_weight), 70F).toDouble() - val sex = sharedPref.getString(context.getString(R.string.user_sex), "NONE") ?: "NONE" - val tolerance = sharedPref.getFloat(context.getString(R.string.user_tolerance), 0.0F).toDouble() - - body.sex = sex - body.weight = weight - body.alcoholTolerance = tolerance - - body.onUpdate = { updatedSex: String, updatedWeight: Double, updatedTolerance:Double -> run { - sharedPref - .edit() - .putString(context.getString(R.string.user_sex), updatedSex) - .putFloat(context.getString(R.string.user_weight), updatedWeight.toFloat()) - .putFloat("USER_TOLERANCE", updatedTolerance.toFloat()) - .apply() - _liveDrinker.value = body - } } - - _liveDrinker.value = body - } - - -} \ No newline at end of file diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DriveLawRepository.kt b/app/src/main/java/com/vaudibert/canidrive/ui/repository/DriveLawRepository.kt deleted file mode 100644 index 4117252..0000000 --- a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DriveLawRepository.kt +++ /dev/null @@ -1,90 +0,0 @@ -package com.vaudibert.canidrive.ui.repository - -import android.content.Context -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import com.vaudibert.canidrive.R -import com.vaudibert.canidrive.domain.drivelaw.DriveLaw -import com.vaudibert.canidrive.domain.drivelaw.DriveLawService -import java.util.* - -class DriveLawRepository(private val context: Context) { - - val driveLawService = - DriveLawService( - { code: String -> Locale("", code).displayCountry }, - context.getString(R.string.other), - DriveLaws.list, - DriveLaws.default - ) - - private val _liveDriveLaw = MutableLiveData() - private val _liveIsYoung = MutableLiveData() - private val _liveIsProfessional = MutableLiveData() - private val _liveCustomCountryLimit = MutableLiveData() - - - val liveDriveLaw: LiveData - get() = _liveDriveLaw - val liveIsYoung: LiveData - get() = _liveIsYoung - val liveIsProfessional: LiveData - get() = _liveIsProfessional - val liveCustomCountryLimit: LiveData - get() = _liveCustomCountryLimit - - init { - val sharedPref = context.getSharedPreferences(context.getString(R.string.user_preferences), Context.MODE_PRIVATE) - - // Initiate drive law service - driveLawService.isYoung = sharedPref.getBoolean(context.getString(R.string.user_young_driver), false) - driveLawService.isProfessional = sharedPref.getBoolean(context.getString(R.string.user_professional_driver), false) - - driveLawService.select( - sharedPref.getString(context.getString(R.string.countryCode), "") ?: "" - ) - driveLawService.customCountryLimit= sharedPref.getFloat(context.getString(R.string.customCountryLimit), 0.0F).toDouble() - - // Set callbacks once drive law service initialized - driveLawService.onSelectCallback = { - countryCode:String -> - sharedPref.edit() - .putString(context.getString(R.string.countryCode), countryCode) - .apply() - _liveDriveLaw.value = driveLawService.driveLaw - } - - driveLawService.onYoungCallback = { - isYoung: Boolean -> - sharedPref.edit() - .putBoolean(context.getString(R.string.user_young_driver), isYoung) - .apply() - _liveIsYoung.value = driveLawService.isYoung - _liveDriveLaw.value = driveLawService.driveLaw - } - - driveLawService.onProfessionalCallback = { - isProfessional: Boolean -> - sharedPref.edit() - .putBoolean(context.getString(R.string.user_professional_driver), isProfessional) - .apply() - _liveIsProfessional.value = driveLawService.isProfessional - _liveDriveLaw.value = driveLawService.driveLaw - } - - driveLawService.onCustomLimitCallback = { - newLimit:Double -> - sharedPref.edit() - .putFloat(context.getString(R.string.customCountryLimit), newLimit.toFloat()) - .apply() - _liveCustomCountryLimit.value = driveLawService.customCountryLimit - } - - // Initialize live datas - _liveDriveLaw.value = driveLawService.driveLaw - _liveIsYoung.value = driveLawService.isYoung - _liveIsProfessional.value = driveLawService.isProfessional - _liveCustomCountryLimit.value = driveLawService.customCountryLimit - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DriveLaws.kt b/app/src/main/java/com/vaudibert/canidrive/ui/repository/DriveLaws.kt deleted file mode 100644 index c0c0abb..0000000 --- a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DriveLaws.kt +++ /dev/null @@ -1,361 +0,0 @@ -package com.vaudibert.canidrive.ui.repository - -import com.vaudibert.canidrive.R -import com.vaudibert.canidrive.domain.drivelaw.DriveLaw -import com.vaudibert.canidrive.domain.drivelaw.ProfessionalLimit -import com.vaudibert.canidrive.domain.drivelaw.YoungLimit - -object DriveLaws { - - val default = DriveLaw("", 0.0) - - // Main source : https://en.wikipedia.org/wiki/Drunk_driving_law_by_country - val list = listOf( - - // Other - default, - - // -------------------------- Europe -------------------------- - // Albania - DriveLaw("AL", 0.1), - - // Austria - DriveLaw( - "AT", 0.5, - YoungLimit( - 0.1, - R.string.less_two_years_driving - ), - professionalLimit = ProfessionalLimit( - 0.2 - ) - ), - - // Belarus - DriveLaw("BY", 0.3), - - // Belgium - DriveLaw( - "BE", - 0.5, - professionalLimit = ProfessionalLimit( - 0.2 - ) - ), - - // Bosnia & Herzegovina - DriveLaw( - "BA", 0.3, - YoungLimit(explanationId = R.string.twenty_one_young_or_less_three_years_driving), - ProfessionalLimit() - ), - - // Bulgaria - DriveLaw("BG", 0.5), - - // Croatia - DriveLaw( - "HR", 0.5, - YoungLimit(explanationId = R.string.sixteen_to_twenty_four_years_old), - ProfessionalLimit() - ), - - // Cyprus - DriveLaw("CY", 0.5), - - // Czech republic - DriveLaw("CZ"), - - //Denmark - DriveLaw("DK", 0.5), - - // Estonia - DriveLaw("EE", 0.19), - - // Finland - DriveLaw("FI", 0.5), - - // France - DriveLaw( - "FR", 0.5, - YoungLimit( - 0.2, - R.string.less_two_years_driving - ), - ProfessionalLimit(0.2) - ), - - // Georgia - DriveLaw("GE", 0.2), - - // Germany - DriveLaw( - "DE", 0.5, - YoungLimit(explanationId = R.string.twenty_one_young_or_less_two_years_driving), - ProfessionalLimit(0.2) - ), - - // Gibraltar - DriveLaw("GI", 0.5), - - // Greece - DriveLaw( - "GR", 0.5, - YoungLimit( - 0.2, - R.string.motor_cycle_or_less_two_years_driving - ), - ProfessionalLimit(0.2) - ), - - // Hungary - DriveLaw("HU"), - - // Iceland - // TODO : check data - DriveLaw("IS", 0.2), - - // Ireland - DriveLaw( - "IE", 0.5, - YoungLimit( - 0.2, - R.string.motor_cycle_or_less_two_years_driving - ), - ProfessionalLimit(0.2) - ), - - // Italy - DriveLaw( - "IT", 0.5, - YoungLimit( - 0.01, - R.string.three_years_driving - ), - ProfessionalLimit(0.01) - ), - - // Latvia - DriveLaw( - "LV", 0.5, - YoungLimit( - 0.2, - R.string.less_two_years_driving - ) - ), - - // Lithuania - DriveLaw( - "LT", 0.4, - YoungLimit(explanationId = R.string.motor_cycle_or_less_two_years_driving), - ProfessionalLimit() - ), - - // Luxemburg - DriveLaw( - "LU", 0.5, - YoungLimit( - 0.2, - R.string.less_two_years_driving - ), - ProfessionalLimit(0.2) - ), - - // Malta - DriveLaw("MT", 0.8), - - // Moldova - DriveLaw("MD", 0.3), - - // Montenegro - DriveLaw("ME", 0.3), - - // Netherlands - // TODO : check data, missing professional ? - DriveLaw( - "NL", 0.5, - YoungLimit( - 0.2, - R.string.five_years_driving - ) - ), - - // TODO : North macedonia - - // Norway - DriveLaw("NO", 0.2), - - // Poland - DriveLaw("PL", 0.2), - - // Portugal - DriveLaw( - "PT", 0.5, - YoungLimit( - 0.2, - R.string.three_years_driving - ) - ), - - // Romania - DriveLaw("RO"), - - // Russia - DriveLaw("RU", 0.356), - - // Serbia - DriveLaw( - "RS", 0.2, - YoungLimit(explanationId = R.string.motor_cycle_or_young_drivers), - ProfessionalLimit() - ), - - // Slovakia - DriveLaw("SK"), - - // Slovenia - DriveLaw( - "SI", 0.5, - YoungLimit(explanationId = R.string.three_years_driving), - ProfessionalLimit() - ), - - // Spain - DriveLaw( - "ES", 0.5, - YoungLimit( - 0.3, - R.string.less_two_years_driving - ), - ProfessionalLimit(0.2) - ), - - // Sweden - DriveLaw("SE", 0.2), - - // Switzland - DriveLaw( - "CH", 0.5, - YoungLimit(explanationId = R.string.three_years_driving) - ), - - // Ukraine - DriveLaw("UA", 0.2), - - // Great-Britain (includes scotland...) - // TODO : treat special case of scotland (not a country yet different law). - DriveLaw( - "GB", 0.8, - professionalLimit = ProfessionalLimit( - 0.8 - ) - ), - - // -------------------- Americas -------------------- - // Canada - DriveLaw("CA", 0.08), - - // USA - DriveLaw( - "US", 0.8, - YoungLimit(explanationId = R.string.twenty_one_young) - ), - - // -------------------- Asia -------------------- - // China - DriveLaw("CN"), - - // Hong-Kong - DriveLaw("HK", 0.5), - - // Japan - DriveLaw("JP", 0.3), - - // South Korea - DriveLaw("KR", 0.3), - - // Taiwan - DriveLaw("TW", 0.3), - - // India - DriveLaw("IN", 0.3), - - // Nepal - DriveLaw("NP"), - - // Pakistan - DriveLaw("PK"), - - // Sri-lanka - DriveLaw("LK", 0.6), - - // Indonesia - DriveLaw("ID"), - - // Laos - DriveLaw("LA", 0.8), - - // Malaysia - DriveLaw("MY", 0.8), - - // Philippines - DriveLaw( - "PH", 0.5, - YoungLimit( - 0.1, - R.string.motor_cycle_or_young_drivers - ), - ProfessionalLimit(0.1) - ), - - // Singapore - DriveLaw("SG", 0.8), - - // Thailand - DriveLaw( - "TH", - 0.5, - professionalLimit = ProfessionalLimit() - ), - - // Vietnam - // TODO : weird case for motorbikes : 0.5 - DriveLaw("VN"), - - // Armenia - DriveLaw("AM", 0.4), - - // Iran - DriveLaw("IR"), - - // Israel - DriveLaw( - "IL", 0.24, - YoungLimit( - 0.05, - R.string.new_driver_or_twenty_four - ), - ProfessionalLimit(0.05) - ), - - // Jordan - DriveLaw("JO", 0.5), - - // Kuwait - DriveLaw("KW"), - - // Saudi Arabia - DriveLaw("SA"), - - // United Arab Emirates - DriveLaw("AE"), - - // Turkey - DriveLaw( - "TR", - 0.5, - professionalLimit = ProfessionalLimit() - ) - - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/repository/MainRepository.kt b/app/src/main/java/com/vaudibert/canidrive/ui/repository/MainRepository.kt deleted file mode 100644 index 3f4570a..0000000 --- a/app/src/main/java/com/vaudibert/canidrive/ui/repository/MainRepository.kt +++ /dev/null @@ -1,50 +0,0 @@ -package com.vaudibert.canidrive.ui.repository - -import android.content.Context -import androidx.room.Room -import androidx.room.migration.Migration -import androidx.sqlite.db.SupportSQLiteDatabase -import com.vaudibert.canidrive.R -import com.vaudibert.canidrive.data.DrinkDatabase -import com.vaudibert.canidrive.domain.DrinkerStatusService - -class MainRepository(private val context: Context) { - - private val migration_1_2 = object : Migration(1, 2) { - override fun migrate(database: SupportSQLiteDatabase) { - database.execSQL("ALTER TABLE DrinkEntity ADD COLUMN `name` VARCHAR") - database.execSQL("ALTER TABLE DrinkEntity RENAME TO IngestedDrinkEntity") - } - } - - - private val drinkDatabase = Room - .databaseBuilder(context, DrinkDatabase::class.java, "drink-database") - //.addMigrations(migration_1_2) // commented as crashing, not priority :-( - .fallbackToDestructiveMigration() - .build() - - val drinkRepository = DrinkRepository(context, drinkDatabase) - - val digestionRepository = DigestionRepository(context, drinkRepository.ingestionService) - - val driveLawRepository = DriveLawRepository(context) - - val drinkerStatusService = DrinkerStatusService( - digestionRepository.digestionService, - driveLawRepository.driveLawService - ) - - private val sharedPref = context.getSharedPreferences(context.getString(R.string.user_preferences), Context.MODE_PRIVATE) - - - // Flag for initialization, saved when set. - var init : Boolean = sharedPref.getBoolean(context.getString(R.string.user_initialized), false) - set(value) { - field = value - sharedPref.edit() - .putBoolean(context.getString(R.string.user_initialized), value) - .apply() - } - -} \ No newline at end of file diff --git a/app/src/main/java/com/vaudibert/canidrive/KeyboardUtils.kt b/app/src/main/java/com/vaudibert/canidrive/ui/util/KeyboardUtils.kt similarity index 79% rename from app/src/main/java/com/vaudibert/canidrive/KeyboardUtils.kt rename to app/src/main/java/com/vaudibert/canidrive/ui/util/KeyboardUtils.kt index efa0afe..b15724b 100644 --- a/app/src/main/java/com/vaudibert/canidrive/KeyboardUtils.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/util/KeyboardUtils.kt @@ -1,4 +1,4 @@ -package com.vaudibert.canidrive +package com.vaudibert.canidrive.ui.util import android.R import android.app.Activity @@ -22,17 +22,17 @@ object KeyboardUtils { } fun showKeyboard(activity: Activity) { - val inputMethodManager = - activity.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager - inputMethodManager.toggleSoftInput( - InputMethodManager.SHOW_FORCED, - 0 - ) + val view = activity.currentFocus ?: activity.findViewById(android.R.id.content) + if (view != null) { + val windowInsetsController = + androidx.core.view.WindowCompat.getInsetsController(activity.window, view) + windowInsetsController.show(androidx.core.view.WindowInsetsCompat.Type.ime()) + } } fun addKeyboardVisibilityListener( rootLayout: View, - onKeyboardVisibiltyListener: OnKeyboardVisibiltyListener + onKeyboardVisibiltyListener: OnKeyboardVisibiltyListener, ) { rootLayout.viewTreeObserver.addOnGlobalLayoutListener { val r = Rect() @@ -50,4 +50,4 @@ object KeyboardUtils { interface OnKeyboardVisibiltyListener { fun onVisibilityChange(isVisible: Boolean) } -} \ No newline at end of file +} diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 43a0c30..2f68e69 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -4,12 +4,14 @@ xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" + android:fitsSystemWindows="true" tools:context="com.vaudibert.canidrive.ui.MainActivity"> @@ -17,6 +19,7 @@ android:id="@+id/appBarDrinker" android:layout_width="match_parent" android:layout_height="wrap_content" + android:fitsSystemWindows="true" android:theme="@style/AppTheme.AppBarOverlay" app:layout_constraintTop_toTopOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -33,12 +36,11 @@ - diff --git a/app/src/main/res/layout/constraint_content_add_drink_presets.xml b/app/src/main/res/layout/constraint_content_add_drink_presets.xml index 6389092..67e6b2c 100644 --- a/app/src/main/res/layout/constraint_content_add_drink_presets.xml +++ b/app/src/main/res/layout/constraint_content_add_drink_presets.xml @@ -14,13 +14,12 @@ app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> - @@ -74,7 +75,7 @@ android:layout_marginEnd="3dp" android:contentDescription="@string/forbidden_symbol" android:src="@drawable/ic_forbidden_white_24dp" - android:tint="@color/driveRed" + app:tint="@color/driveRed" app:layout_constraintEnd_toStartOf="@id/linearLimitValue" app:layout_constraintTop_toTopOf="@+id/textViewLimitUnit" /> diff --git a/app/src/main/res/layout/constraint_content_drinker_pickers.xml b/app/src/main/res/layout/constraint_content_drinker_pickers.xml index 7b830f3..a36ca81 100644 --- a/app/src/main/res/layout/constraint_content_drinker_pickers.xml +++ b/app/src/main/res/layout/constraint_content_drinker_pickers.xml @@ -25,10 +25,15 @@ android:layout_height="wrap_content" android:text="@string/weight" /> - + android:layout_height="wrap_content" + android:inputType="numberDecimal" + android:minEms="4" + android:textAlignment="center" + android:importantForAccessibility="yes" + android:hint="@string/weight" /> @@ -94,11 +99,11 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" style="@style/TextViewLabel" - android:text="average" + android:text="@string/alcohol_tolerance_medium" app:layout_constraintTop_toBottomOf="@id/linearPickerWeight" app:layout_constraintStart_toEndOf="@id/textViewAlcoholToleranceLabel" app:layout_constraintEnd_toEndOf="parent" - tools:ignore="HardcodedText" /> + /> diff --git a/app/src/main/res/layout/constraint_content_drive_history.xml b/app/src/main/res/layout/constraint_content_drive_history.xml index b377081..db069e0 100644 --- a/app/src/main/res/layout/constraint_content_drive_history.xml +++ b/app/src/main/res/layout/constraint_content_drive_history.xml @@ -18,11 +18,11 @@ /> - + app:tint="@color/driveGreen" /> + app:tint="@color/driveRed" /> + + + app:tint="@color/driveAmber" /> + app:tint="@color/driveGreen" /> @@ -85,7 +84,7 @@ android:layout_margin="5dp" android:padding="5dp" android:src="@drawable/ic_delete_white_24dp" - android:contentDescription="Delete drink button" + android:contentDescription="@string/content_desc_delete_drink" tools:ignore="HardcodedText" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" diff --git a/app/src/main/res/layout/item_preset_drink.xml b/app/src/main/res/layout/item_preset_drink.xml index c70cc56..4512e74 100644 --- a/app/src/main/res/layout/item_preset_drink.xml +++ b/app/src/main/res/layout/item_preset_drink.xml @@ -16,8 +16,7 @@ android:layout_gravity="center" android:padding="5dp" android:src="@drawable/wine_glass" - android:contentDescription="drink icon" - tools:ignore="HardcodedText" + android:contentDescription="@string/content_desc_drink_icon" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toTopOf="parent" /> @@ -55,12 +54,11 @@ android:layout_gravity="center" android:layout_margin="5dp" android:background="@drawable/background_round_button_small_primary" - android:contentDescription="Delete drink button" + android:contentDescription="@string/content_desc_delete_drink" android:padding="5dp" android:src="@drawable/ic_delete_white_24dp" android:visibility="invisible" app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintTop_toTopOf="parent" - tools:ignore="HardcodedText" /> + app:layout_constraintTop_toTopOf="parent" /> \ No newline at end of file diff --git a/app/src/main/res/navigation/nav_graph.xml b/app/src/main/res/navigation/nav_graph.xml index a1a9877..fd7d0ed 100644 --- a/app/src/main/res/navigation/nav_graph.xml +++ b/app/src/main/res/navigation/nav_graph.xml @@ -93,4 +93,8 @@ app:popUpTo="@+id/addDrinkFragment" app:popUpToInclusive="true" /> + \ No newline at end of file diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 3accb72..9e8478b 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -23,6 +23,7 @@ Motorrad oder junger Fahrer. Jünger als 21. Neuer Fahrer oder jünger als 24. + Junger Fahrer Berufskraftfahrer Land Gesundheit @@ -44,4 +45,14 @@ Leichter Apfelwein Martini Whisky + + Wichtiger Hinweis + Diese App liefert eine SCHÄTZUNG des Blutalkoholgehalts (BAK) auf Basis allgemeiner Formeln. Sie ist KEIN Ersatz für ein zertifiziertes Atemalkoholmessgerät und darf NIEMALS als rechtliche Referenz für das Fahren verwendet werden.\n\nDer tatsächliche BAK hängt von vielen Faktoren ab, die diese App nicht berücksichtigt (Medikamente, Nahrungsaufnahme, Gesundheitszustand usw.).\n\nIm Zweifel: NICHT FAHREN.\n\nMit der Nutzung dieser App erkennen Sie an, dass die Entwickler keine Haftung für Folgen aus der Nutzung übernehmen. + Verstanden + + Getränk-Symbol + Getränk löschen + Getränk hinzufügen + Profileinstellungen + Bestätigen \ No newline at end of file diff --git a/app/src/main/res/values-es-rES/strings.xml b/app/src/main/res/values-es-rES/strings.xml new file mode 100644 index 0000000..339248f --- /dev/null +++ b/app/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,58 @@ + + + Ajustes + Volumen + Grado + Cuándo + Peso + Género + HOMBRE + MUJER + OTRO + Sobre ti + ¿Puedo conducir? + Selecciona una bebida + ahora + Conductor novel + Conductor profesional + País + Menos de 2 años de experiencia al volante. + Menor de 21 años o menos de 3 años de carnet. + 16–24 años de edad. + Menor de 21 años o menos de 2 años de carnet. + Moto o menos de 2 años de carnet. + Menos de 3 años de carnet. + Menos de 5 años de carnet. + Moto o conductor novel. + Menor de 21 años. + Conductor novel o menor de 24 años. + Otro + d + Historial de bebidas + Legislación + Fisiología + Estado de conducción + Salud + baja + media + buena + Preajustes + Definir nueva bebida + Nombre de la bebida + Vino tinto + Cerveza rubia ligera + Cerveza triple + Sidra suave + Martini + Whisky + + Aviso importante + Esta aplicación proporciona una ESTIMACIÓN del nivel de alcohol en sangre (TAS) basada en fórmulas generales. NO sustituye a un alcoholímetro certificado y NUNCA debe usarse como referencia legal para conducir.\n\nEl TAS real varía en función de muchos factores no contemplados por esta aplicación (medicamentos, ingesta de alimentos, estado de salud, etc.).\n\nEn caso de duda, NO CONDUZCA.\n\nAl usar esta aplicación, usted reconoce que los desarrolladores no aceptan responsabilidad alguna por las consecuencias derivadas de su uso. + Entendido + + Icono de bebida + Eliminar bebida + Añadir una bebida + Ajustes de perfil + Validar + diff --git a/app/src/main/res/values-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 6f2ea5f..016c615 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -10,6 +10,13 @@ FEMME AUTRE À propos de vous + + Apparence + Thème + Choisir le thème + Clair + Sombre + Selon le système Choisir une boisson maintenant Puis-je conduire ? @@ -45,4 +52,31 @@ Cidre doux Martini Whisky + + Avertissement important + Cette application fournit une ESTIMATION du taux d\'alcoolémie basée sur des formules générales. Elle ne remplace PAS un éthylotest certifié et ne doit JAMAIS être utilisée comme référence légale pour la conduite.\n\nLe taux réel varie en fonction de nombreux facteurs non pris en compte par cette application (médicaments, alimentation, état de santé, etc.).\n\nEn cas de doute, NE CONDUISEZ PAS.\n\nEn utilisant cette application, vous reconnaissez que les développeurs déclinent toute responsabilité quant aux conséquences de son utilisation. + J\'ai compris + + Icône de boisson + Supprimer la boisson + Ajouter une boisson + Paramètres du profil + Valider + g/L + Boisson supprimée + ANNULER + + 20 min + 40 min + 1 h + 1h30 + 2 h + 3 h + 5 h + 8 h + 12 h + 18 h + 24 h + Apte à conduire + NE CONDUISEZ PAS \ No newline at end of file diff --git a/app/src/main/res/values-it-rIT/strings.xml b/app/src/main/res/values-it-rIT/strings.xml index ce6b64e..ef25ca4 100644 --- a/app/src/main/res/values-it-rIT/strings.xml +++ b/app/src/main/res/values-it-rIT/strings.xml @@ -1,16 +1,6 @@ - CanIDrive Impostazioni - USER_PREFS - WEIGHT - SEX - YOUNG_DRIVER - PROFESSIONAL_DRIVER - - INITIALIZED - - Volume Gradi Quando @@ -21,19 +11,11 @@ ALTRO Il tuo profilo Posso guidare? - version Seleziona una bevanda adesso - car colored with status - drive status symbol - time to wait Neopatentato Autista professionista Paese - COUNTRY_CODE - alcohol blood rate - 0.6 g/L - 12:34 Patente da meno di 2 anni Meno di 21 anni di età o patente da meno di 3 anni @@ -45,9 +27,6 @@ Motocicli o neopatentato Meno di 21 anni di età Neopatentato o meno di 24 anni di età - forbidden symbol - drop symbol - CUSTOM_COUNTRY_LIMIT Altro g Storico bevande @@ -58,7 +37,6 @@ bassa media buona - USER_TOLERANCE Predefiniti Definisci una nuova bevanda Nome della bevanda @@ -68,4 +46,14 @@ Birra Radler Martini Whisky + + Avviso importante + Questa app fornisce una STIMA del tasso alcolemico (BAC) basata su formule generali. NON sostituisce un etilometro certificato e NON deve MAI essere utilizzata come riferimento legale per la guida.\n\nIl tasso alcolemico reale varia in base a molti fattori non considerati da questa app (farmaci, assunzione di cibo, condizioni di salute, ecc.).\n\nIn caso di dubbio, NON GUIDARE.\n\nUtilizzando questa app, l\'utente riconosce che gli sviluppatori non si assumono alcuna responsabilità per le conseguenze derivanti dal suo utilizzo. + Ho capito + + Icona della bevanda + Elimina bevanda + Aggiungi una bevanda + Impostazioni profilo + Conferma diff --git a/app/src/main/res/values-night/colors.xml b/app/src/main/res/values-night/colors.xml new file mode 100644 index 0000000..8f4a1e5 --- /dev/null +++ b/app/src/main/res/values-night/colors.xml @@ -0,0 +1,18 @@ + + + + #E58A7A + #A6E58A7A + #E58A7A + + #57E45E + #A657E45E + #A9D68B + + #121212 + #E0E0E0 + + #30CA36 + #FF5252 + #FFB74D + diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..f8bd295 --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,13 @@ + + + + @string/theme_light + @string/theme_dark + @string/theme_system + + + light + dark + system + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 44b8ee5..5a79eb9 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,13 @@ FEMALE OTHER About you + + Display + Theme + Choose Theme + Light + Dark + System Default Can I Drive ? version Select a drink @@ -68,4 +75,21 @@ Soft cider Martini Whisky + + Important Disclaimer + This app provides an ESTIMATE of blood alcohol content (BAC) based on general formulas. It is NOT a substitute for a certified breathalyzer and should NEVER be used as a legal reference for driving.\n\nActual BAC varies based on many factors not accounted for by this app (medication, food intake, health conditions, etc.).\n\nWhen in doubt, DO NOT DRIVE.\n\nBy using this app, you acknowledge that the developers accept no liability for any consequences resulting from its use. + I understand + DISCLAIMER_ACCEPTED + + Drink icon + Delete drink + Add a drink + Profile settings + Validate + Safe to drive + DO NOT DRIVE + g/L + 12h + 18h + 24h diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 4a1bd8e..508ba7d 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ - - + +