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 @@
-
-
+
+
diff --git a/app/src/main/res/xml/backup_rules.xml b/app/src/main/res/xml/backup_rules.xml
new file mode 100644
index 0000000..6ca565f
--- /dev/null
+++ b/app/src/main/res/xml/backup_rules.xml
@@ -0,0 +1,11 @@
+
+
+
+
+
diff --git a/app/src/main/res/xml/data_extraction_rules.xml b/app/src/main/res/xml/data_extraction_rules.xml
new file mode 100644
index 0000000..2c95934
--- /dev/null
+++ b/app/src/main/res/xml/data_extraction_rules.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
new file mode 100644
index 0000000..9fe6a77
--- /dev/null
+++ b/app/src/main/res/xml/preferences.xml
@@ -0,0 +1,17 @@
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/test/java/com/vaudibert/canidrive/data/ConvertersTest.kt b/app/src/test/java/com/vaudibert/canidrive/data/ConvertersTest.kt
new file mode 100644
index 0000000..134fbcd
--- /dev/null
+++ b/app/src/test/java/com/vaudibert/canidrive/data/ConvertersTest.kt
@@ -0,0 +1,22 @@
+package com.vaudibert.canidrive.data
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import java.util.*
+
+class ConvertersTest {
+ @Test
+ fun `fromTimestamp converts timestamp to correct Date`() {
+ val timestamp = 1678615200000L // 2023-03-12 10:00:00 UTC
+ val date = Converters.fromTimestamp(timestamp)
+ assertEquals(timestamp, date.time)
+ }
+
+ @Test
+ fun `dateToTimestamp converts Date to correct timestamp`() {
+ val timestamp = 1678615200000L
+ val date = Date(timestamp)
+ val result = Converters.dateToTimestamp(date)
+ assertEquals(timestamp, result)
+ }
+}
diff --git a/app/src/test/java/com/vaudibert/canidrive/domain/DigestionServiceTest.kt b/app/src/test/java/com/vaudibert/canidrive/domain/DigestionServiceTest.kt
index 28f0617..d5bfcae 100644
--- a/app/src/test/java/com/vaudibert/canidrive/domain/DigestionServiceTest.kt
+++ b/app/src/test/java/com/vaudibert/canidrive/domain/DigestionServiceTest.kt
@@ -2,6 +2,7 @@ package com.vaudibert.canidrive.domain
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.IngestedDrink
import com.vaudibert.canidrive.domain.drink.IngestionService
import com.vaudibert.canidrive.domain.drink.PresetDrink
@@ -12,7 +13,6 @@ import org.junit.jupiter.api.Test
import java.util.*
internal class DigestionServiceTest {
-
private val precision = 0.0001
private lateinit var body: PhysicalBody
@@ -24,20 +24,22 @@ internal class DigestionServiceTest {
PresetDrink(
"beer",
500.0,
- 5.0
+ 5.0,
),
- now
+ now,
)
}
@BeforeEach
fun before() {
body = PhysicalBody()
- body.sex = "MALE"
+ body.sex = Sex.MALE
body.weight = 100.0
- ingestionService = IngestionService {
- preset: PresetDrink, ingestionTime: Date -> IngestedDrink(preset.name, preset.volume, preset.degree, ingestionTime)
- }
+ ingestionService =
+ IngestionService {
+ preset: PresetDrink, ingestionTime: Date ->
+ IngestedDrink(preset.name, preset.volume, preset.degree, ingestionTime)
+ }
digestionService = DigestionService(body, ingestionService)
}
@@ -77,5 +79,4 @@ internal class DigestionServiceTest {
assertEquals(body.decreaseFactor, instantRate - laterRate, precision)
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/test/java/com/vaudibert/canidrive/domain/DrinkerStatusServiceTest.kt b/app/src/test/java/com/vaudibert/canidrive/domain/DrinkerStatusServiceTest.kt
new file mode 100644
index 0000000..767289e
--- /dev/null
+++ b/app/src/test/java/com/vaudibert/canidrive/domain/DrinkerStatusServiceTest.kt
@@ -0,0 +1,61 @@
+package com.vaudibert.canidrive.domain
+
+import com.vaudibert.canidrive.domain.digestion.DigestionService
+import com.vaudibert.canidrive.domain.drivelaw.DriveLawService
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+import org.mockito.Mockito.*
+import java.util.*
+
+class DrinkerStatusServiceTest {
+ @Test
+ fun `status returns can drive when alcohol rate is below limit`() {
+ val digestionService = mock(DigestionService::class.java)
+ val driveLawService = mock(DriveLawService::class.java)
+
+ `when`(driveLawService.driveLimit()).thenReturn(0.5)
+ `when`(driveLawService.defaultLimit).thenReturn(0.0)
+ `when`(digestionService.alcoholRateAt(any(Date::class.java))).thenReturn(0.4)
+ `when`(digestionService.timeToReachLimit(anyDouble())).thenReturn(Date(1000L))
+
+ val service = DrinkerStatusService(digestionService, driveLawService)
+ val status = service.status()
+
+ assertTrue(status.isSafeToDrive)
+ assertEquals(0.4, status.alcoholRate, 0.01)
+ }
+
+ @Test
+ fun `status returns cannot drive when alcohol rate is above limit`() {
+ val digestionService = mock(DigestionService::class.java)
+ val driveLawService = mock(DriveLawService::class.java)
+
+ `when`(driveLawService.driveLimit()).thenReturn(0.5)
+ `when`(driveLawService.defaultLimit).thenReturn(0.0)
+ `when`(digestionService.alcoholRateAt(any(Date::class.java))).thenReturn(0.8)
+ `when`(digestionService.timeToReachLimit(anyDouble())).thenReturn(Date(1000L))
+
+ val service = DrinkerStatusService(digestionService, driveLawService)
+ val status = service.status()
+
+ assertFalse(status.isSafeToDrive)
+ assertEquals(0.8, status.alcoholRate, 0.01)
+ }
+
+ @Test
+ fun `status returns can drive when alcohol rate equals limit`() {
+ val digestionService = mock(DigestionService::class.java)
+ val driveLawService = mock(DriveLawService::class.java)
+
+ `when`(driveLawService.driveLimit()).thenReturn(0.5)
+ `when`(driveLawService.defaultLimit).thenReturn(0.0)
+ `when`(digestionService.alcoholRateAt(any(Date::class.java))).thenReturn(0.5)
+ `when`(digestionService.timeToReachLimit(anyDouble())).thenReturn(Date(1000L))
+
+ val service = DrinkerStatusService(digestionService, driveLawService)
+ val status = service.status()
+
+ assertTrue(status.isSafeToDrive)
+ assertEquals(0.5, status.alcoholRate, 0.01)
+ }
+}
diff --git a/app/src/test/java/com/vaudibert/canidrive/domain/DriveLawServiceTest.kt b/app/src/test/java/com/vaudibert/canidrive/domain/DriveLawServiceTest.kt
index 43c36cc..fed5823 100644
--- a/app/src/test/java/com/vaudibert/canidrive/domain/DriveLawServiceTest.kt
+++ b/app/src/test/java/com/vaudibert/canidrive/domain/DriveLawServiceTest.kt
@@ -10,18 +10,19 @@ import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
internal class DriveLawServiceTest {
-
- private var defaultNamer = { s:String -> s}
+ private var defaultNamer = { s: String -> s }
private var countryList: List = emptyList()
private val defaultDriveLaw =
DriveLaw("")
- private val aDriveLaw = DriveLaw(
- "A-first", 0.7,
- YoungLimit(0.15, 654321),
- ProfessionalLimit(0.23)
- )
+ private val aDriveLaw =
+ DriveLaw(
+ "A-first",
+ 0.7,
+ YoungLimit(0.15, "testExplanation"),
+ ProfessionalLimit(0.23),
+ )
private val zDriveLaw =
DriveLaw("Z-last")
@@ -35,7 +36,7 @@ internal class DriveLawServiceTest {
defaultNamer,
"other",
countryList,
- defaultDriveLaw
+ defaultDriveLaw,
)
}
@@ -44,7 +45,6 @@ internal class DriveLawServiceTest {
assertEquals(defaultDriveLaw, driveLawService.driveLaw)
}
-
@Test
fun `Drive laws are sorted by country name (with given namer)`() {
val countryNames = driveLawService.getListOfCountriesWithFlags()
@@ -78,7 +78,7 @@ internal class DriveLawServiceTest {
assertEquals(aDriveLaw, a)
assertEquals(defaultDriveLaw, default)
}
-
+
@Test
fun `Drive limit is regular one by default`() {
driveLawService.select("A-first")
@@ -101,5 +101,4 @@ internal class DriveLawServiceTest {
assertEquals(0.23, driveLawService.driveLimit())
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/test/java/com/vaudibert/canidrive/domain/PhysicalBodyTest.kt b/app/src/test/java/com/vaudibert/canidrive/domain/PhysicalBodyTest.kt
index 4cbf607..7c03ea4 100644
--- a/app/src/test/java/com/vaudibert/canidrive/domain/PhysicalBodyTest.kt
+++ b/app/src/test/java/com/vaudibert/canidrive/domain/PhysicalBodyTest.kt
@@ -1,15 +1,15 @@
package com.vaudibert.canidrive.domain
import com.vaudibert.canidrive.domain.digestion.PhysicalBody
+import com.vaudibert.canidrive.domain.digestion.Sex
import org.junit.jupiter.api.Assertions.assertEquals
import org.junit.jupiter.api.Test
internal class PhysicalBodyTest {
-
@Test
fun `A 100kg male drinker shall have eff-weight 70, decrease 0,1`() {
val male = PhysicalBody()
- male.sex = "MALE"
+ male.sex = Sex.MALE
male.weight = 100.0
assertEquals(70.0, male.effectiveWeight)
@@ -19,7 +19,7 @@ internal class PhysicalBodyTest {
@Test
fun `A 50kg female drinker shall have eff-weight 30, decrease 0,085`() {
val female = PhysicalBody()
- female.sex = "FEMALE"
+ female.sex = Sex.FEMALE
female.weight = 50.0
assertEquals(30.0, female.effectiveWeight)
@@ -29,15 +29,15 @@ internal class PhysicalBodyTest {
@Test
fun `Changing sex impacts effectiveWeight and decreaseFactor`() {
val changer = PhysicalBody()
- changer.sex = "MALE"
+ changer.sex = Sex.MALE
changer.weight = 100.0
- changer.sex="FEMALE"
+ changer.sex = Sex.FEMALE
assertEquals(60.0, changer.effectiveWeight)
assertEquals(0.085, changer.decreaseFactor)
- changer.sex="MALE"
+ changer.sex = Sex.MALE
assertEquals(70.0, changer.effectiveWeight)
assertEquals(0.1, changer.decreaseFactor)
@@ -58,11 +58,11 @@ internal class PhysicalBodyTest {
@Test
fun `Tolerance impacts only decreaseFactor for a given sex`() {
val female = PhysicalBody()
- female.sex = "FEMALE"
+ female.sex = Sex.FEMALE
female.weight = 50.0
val male = PhysicalBody()
- male.sex = "MALE"
+ male.sex = Sex.MALE
male.weight = 100.0
assertEquals(0.085, female.decreaseFactor)
@@ -72,7 +72,5 @@ internal class PhysicalBodyTest {
assertEquals(0.1, male.decreaseFactor)
male.alcoholTolerance = 1.0
assertEquals(0.15, male.decreaseFactor)
-
}
-
-}
\ No newline at end of file
+}
diff --git a/app/src/test/java/com/vaudibert/canidrive/domain/drink/IngestedDrinkTest.kt b/app/src/test/java/com/vaudibert/canidrive/domain/drink/IngestedDrinkTest.kt
new file mode 100644
index 0000000..2f9d63f
--- /dev/null
+++ b/app/src/test/java/com/vaudibert/canidrive/domain/drink/IngestedDrinkTest.kt
@@ -0,0 +1,24 @@
+package com.vaudibert.canidrive.domain.drink
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+import java.util.*
+
+class IngestedDrinkTest {
+ @Test
+ fun `alcoholMass is correctly calculated`() {
+ // formula: degree/100 * volume * ALCOHOL_DENSITY(0.8)
+
+ // 500ml of 5% beer -> 0.05 * 500 * 0.8 = 20g
+ val beer = IngestedDrink("Beer", 500.0, 5.0, Date())
+ assertEquals(20.0, beer.alcoholMass(), 0.01)
+
+ // 130ml of 12% wine -> 0.12 * 130 * 0.8 = 12.48g
+ val wine = IngestedDrink("Wine", 130.0, 12.0, Date())
+ assertEquals(12.48, wine.alcoholMass(), 0.01)
+
+ // 30ml of 40% strong alcohol -> 0.4 * 30 * 0.8 = 9.6g
+ val strong = IngestedDrink("Strong", 30.0, 40.0, Date())
+ assertEquals(9.6, strong.alcoholMass(), 0.01)
+ }
+}
diff --git a/app/src/test/java/com/vaudibert/canidrive/domain/drink/PresetDrinkServiceTest.kt b/app/src/test/java/com/vaudibert/canidrive/domain/drink/PresetDrinkServiceTest.kt
index c9c9fdf..70572d1 100644
--- a/app/src/test/java/com/vaudibert/canidrive/domain/drink/PresetDrinkServiceTest.kt
+++ b/app/src/test/java/com/vaudibert/canidrive/domain/drink/PresetDrinkServiceTest.kt
@@ -1,29 +1,27 @@
package com.vaudibert.canidrive.domain.drink
-import org.junit.Test
import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
import java.util.*
internal class PresetDrinkServiceTest {
-
private lateinit var presetService: PresetDrinkService
- private lateinit var exportPreset: List
+ private val exportPreset: List get() = presetService.presetsFlow.value
private lateinit var testIIngestor: TestIIngestCapable
private val presetA = PresetDrink("A", 10.0, 10.0, 1)
- private val presetB = PresetDrink("B", 20.0, 20.0,2)
+ private val presetB = PresetDrink("B", 20.0, 20.0, 2)
private val presetC = PresetDrink("C", 30.0, 30.0, 3)
private var presetPopulation = listOf(presetA, presetB, presetC)
private fun setupBasic() {
- presetService = PresetDrinkService {
- name: String, volume: Double, degree: Double ->
- PresetDrink(name, volume, degree)
- }
- presetService.onPresetsChanged = {
- exportPreset = it
- }
+ presetService =
+ PresetDrinkService {
+ name: String, volume: Double, degree: Double ->
+ PresetDrink(name, volume, degree)
+ }
+
testIIngestor = TestIIngestCapable()
presetService.ingestionService = testIIngestor
}
@@ -99,4 +97,4 @@ internal class PresetDrinkServiceTest {
assertEquals("D", selected?.name)
assertEquals(0, selected?.count)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/test/java/com/vaudibert/canidrive/domain/drink/TestIIngestCapable.kt b/app/src/test/java/com/vaudibert/canidrive/domain/drink/TestIIngestCapable.kt
index 117fed4..185eef8 100644
--- a/app/src/test/java/com/vaudibert/canidrive/domain/drink/TestIIngestCapable.kt
+++ b/app/src/test/java/com/vaudibert/canidrive/domain/drink/TestIIngestCapable.kt
@@ -3,10 +3,13 @@ package com.vaudibert.canidrive.domain.drink
import java.util.*
class TestIIngestCapable : IIngestCapable {
- val ingestedList : MutableList = emptyList().toMutableList()
+ val ingestedList: MutableList = emptyList().toMutableList()
- override fun ingest(preset: PresetDrink, ingestionTime: Date) {
+ override fun ingest(
+ preset: PresetDrink,
+ ingestionTime: Date,
+ ) {
val ingested = IngestedDrink(preset.name, preset.volume, preset.degree, ingestionTime)
ingestedList.add(ingested)
}
-}
\ No newline at end of file
+}
diff --git a/app/src/test/java/com/vaudibert/canidrive/ui/repository/DriveLawsTest.kt b/app/src/test/java/com/vaudibert/canidrive/ui/repository/DriveLawsTest.kt
new file mode 100644
index 0000000..2d9a6cc
--- /dev/null
+++ b/app/src/test/java/com/vaudibert/canidrive/ui/repository/DriveLawsTest.kt
@@ -0,0 +1,36 @@
+package com.vaudibert.canidrive.data.repository
+
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Test
+
+class DriveLawsTest {
+ @Test
+ fun `check drive laws BAC limits`() {
+ // These values should all be in g/L.
+ val expectedLimits =
+ mapOf(
+ "AL" to 0.1, "AT" to 0.5, "BY" to 0.3, "BE" to 0.5, "BA" to 0.3, "BG" to 0.5,
+ "HR" to 0.5, "CY" to 0.5, "CZ" to 0.0, "DK" to 0.5, "EE" to 0.19, "FI" to 0.5,
+ "FR" to 0.5, "GE" to 0.2, "DE" to 0.5, "GI" to 0.5, "GR" to 0.5, "HU" to 0.0,
+ "IS" to 0.2, "IE" to 0.5, "IT" to 0.5, "LV" to 0.5, "LT" to 0.4, "LU" to 0.5,
+ "MT" to 0.8, "MD" to 0.3, "ME" to 0.3, "NL" to 0.5, "NO" to 0.2, "PL" to 0.2,
+ "PT" to 0.5, "RO" to 0.0, "RU" to 0.356, "RS" to 0.2, "SK" to 0.0, "SI" to 0.5,
+ "ES" to 0.5, "SE" to 0.2, "CH" to 0.5, "UA" to 0.2, "GB" to 0.8, "CA" to 0.8,
+ "US" to 0.8, "CN" to 0.2, "HK" to 0.5, "JP" to 0.3, "KR" to 0.3, "TW" to 0.3,
+ "IN" to 0.3, "NP" to 0.0, "PK" to 0.0, "LK" to 0.6, "ID" to 0.0, "LA" to 0.8,
+ "MY" to 0.8, "PH" to 0.5, "SG" to 0.8, "TH" to 0.5, "VN" to 0.0, "AM" to 0.4,
+ "IR" to 0.0, "IL" to 0.24, "JO" to 0.5, "KW" to 0.0, "SA" to 0.0, "AE" to 0.0,
+ "TR" to 0.5,
+ )
+
+ for (law in DriveLaws.list) {
+ val countryCode = law.countryCode
+ if (countryCode.isEmpty()) continue
+
+ val expected = expectedLimits[countryCode]
+ requireNotNull(expected) { "Missing expected limit for country $countryCode" }
+
+ assertEquals(expected, law.limit, 0.001, "Incorrect limit for country $countryCode")
+ }
+ }
+}
diff --git a/build.gradle.kts b/build.gradle.kts
index c3e5187..8a0eca0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -1,25 +1,7 @@
-buildscript {
- repositories {
- google()
- jcenter()
- mavenCentral()
- }
- dependencies {
- classpath("com.android.tools.build:gradle:4.0.1")
- classpath(kotlin("gradle-plugin", version = "1.3.61"))
- classpath("androidx.navigation:navigation-safe-args-gradle-plugin:2.1.0")
- }
-}
-
-allprojects {
- repositories {
- google()
- jcenter()
- }
-}
-
-tasks {
- val clean by registering(Delete::class) {
- delete(rootProject.buildDir)
- }
+plugins {
+ id("com.android.application") version "8.13.2" apply false
+ id("org.jetbrains.kotlin.android") version "2.0.21" apply false
+ id("com.google.devtools.ksp") version "2.0.21-1.0.28" apply false
+ id("androidx.navigation.safeargs.kotlin") version "2.8.5" apply false
+ id("org.jlleitschuh.gradle.ktlint") version "12.1.0" apply false
}
diff --git a/build_output.txt b/build_output.txt
new file mode 100644
index 0000000..9bada7b
--- /dev/null
+++ b/build_output.txt
@@ -0,0 +1,53 @@
+> Task :app:preBuild UP-TO-DATE
+> Task :app:preDebugBuild UP-TO-DATE
+> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
+> Task :app:checkKotlinGradlePluginConfigurationErrors SKIPPED
+> Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE
+> Task :app:generateDebugResValues UP-TO-DATE
+> Task :app:generateDebugResources UP-TO-DATE
+> Task :app:mergeDebugResources UP-TO-DATE
+> Task :app:packageDebugResources UP-TO-DATE
+> Task :app:processDebugNavigationResources UP-TO-DATE
+> Task :app:parseDebugLocalResources UP-TO-DATE
+> Task :app:dataBindingGenBaseClassesDebug UP-TO-DATE
+> Task :app:generateDebugBuildConfig UP-TO-DATE
+> Task :app:generateSafeArgsDebug UP-TO-DATE
+> Task :app:checkDebugAarMetadata UP-TO-DATE
+> Task :app:compileDebugNavigationResources UP-TO-DATE
+> Task :app:mapDebugSourceSetPaths UP-TO-DATE
+> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
+> Task :app:extractDeepLinksDebug UP-TO-DATE
+> Task :app:processDebugMainManifest UP-TO-DATE
+> Task :app:processDebugManifest UP-TO-DATE
+> Task :app:processDebugManifestForPackage UP-TO-DATE
+> Task :app:processDebugResources UP-TO-DATE
+> Task :app:kspDebugKotlin UP-TO-DATE
+> Task :app:compileDebugKotlin UP-TO-DATE
+> Task :app:javaPreCompileDebug UP-TO-DATE
+> Task :app:compileDebugJavaWithJavac UP-TO-DATE
+> Task :app:mergeDebugShaders UP-TO-DATE
+> Task :app:compileDebugShaders NO-SOURCE
+> Task :app:generateDebugAssets UP-TO-DATE
+> Task :app:mergeDebugAssets UP-TO-DATE
+> Task :app:compressDebugAssets UP-TO-DATE
+> Task :app:desugarDebugFileDependencies UP-TO-DATE
+> Task :app:dexBuilderDebug UP-TO-DATE
+> Task :app:mergeDebugGlobalSynthetics UP-TO-DATE
+> Task :app:processDebugJavaRes UP-TO-DATE
+> Task :app:mergeDebugJavaResource UP-TO-DATE
+> Task :app:checkDebugDuplicateClasses UP-TO-DATE
+> Task :app:mergeExtDexDebug UP-TO-DATE
+> Task :app:mergeLibDexDebug UP-TO-DATE
+> Task :app:mergeProjectDexDebug UP-TO-DATE
+> Task :app:mergeDebugJniLibFolders UP-TO-DATE
+> Task :app:mergeDebugNativeLibs NO-SOURCE
+> Task :app:stripDebugDebugSymbols NO-SOURCE
+> Task :app:validateSigningDebug UP-TO-DATE
+> Task :app:writeDebugAppMetadata UP-TO-DATE
+> Task :app:writeDebugSigningConfigVersions UP-TO-DATE
+> Task :app:packageDebug UP-TO-DATE
+> Task :app:createDebugApkListingFileRedirect UP-TO-DATE
+> Task :app:assembleDebug UP-TO-DATE
+
+BUILD SUCCESSFUL in 1s
+41 actionable tasks: 41 up-to-date
diff --git a/build_output_dark_mode.txt b/build_output_dark_mode.txt
new file mode 100644
index 0000000..791bd90
--- /dev/null
+++ b/build_output_dark_mode.txt
@@ -0,0 +1,53 @@
+> Task :app:preBuild UP-TO-DATE
+> Task :app:preDebugBuild UP-TO-DATE
+> Task :app:mergeDebugNativeDebugMetadata NO-SOURCE
+> Task :app:checkKotlinGradlePluginConfigurationErrors SKIPPED
+> Task :app:dataBindingMergeDependencyArtifactsDebug UP-TO-DATE
+> Task :app:generateDebugResValues UP-TO-DATE
+> Task :app:generateDebugResources UP-TO-DATE
+> Task :app:mergeDebugResources UP-TO-DATE
+> Task :app:packageDebugResources UP-TO-DATE
+> Task :app:processDebugNavigationResources UP-TO-DATE
+> Task :app:parseDebugLocalResources UP-TO-DATE
+> Task :app:dataBindingGenBaseClassesDebug UP-TO-DATE
+> Task :app:generateDebugBuildConfig UP-TO-DATE
+> Task :app:generateSafeArgsDebug UP-TO-DATE
+> Task :app:checkDebugAarMetadata UP-TO-DATE
+> Task :app:compileDebugNavigationResources UP-TO-DATE
+> Task :app:mapDebugSourceSetPaths UP-TO-DATE
+> Task :app:createDebugCompatibleScreenManifests UP-TO-DATE
+> Task :app:extractDeepLinksDebug UP-TO-DATE
+> Task :app:processDebugMainManifest UP-TO-DATE
+> Task :app:processDebugManifest UP-TO-DATE
+> Task :app:processDebugManifestForPackage UP-TO-DATE
+> Task :app:processDebugResources UP-TO-DATE
+> Task :app:kspDebugKotlin UP-TO-DATE
+> Task :app:compileDebugKotlin UP-TO-DATE
+> Task :app:javaPreCompileDebug UP-TO-DATE
+> Task :app:compileDebugJavaWithJavac UP-TO-DATE
+> Task :app:mergeDebugShaders UP-TO-DATE
+> Task :app:compileDebugShaders NO-SOURCE
+> Task :app:generateDebugAssets UP-TO-DATE
+> Task :app:mergeDebugAssets UP-TO-DATE
+> Task :app:compressDebugAssets UP-TO-DATE
+> Task :app:desugarDebugFileDependencies UP-TO-DATE
+> Task :app:dexBuilderDebug UP-TO-DATE
+> Task :app:mergeDebugGlobalSynthetics UP-TO-DATE
+> Task :app:processDebugJavaRes UP-TO-DATE
+> Task :app:mergeDebugJavaResource UP-TO-DATE
+> Task :app:checkDebugDuplicateClasses UP-TO-DATE
+> Task :app:mergeExtDexDebug UP-TO-DATE
+> Task :app:mergeLibDexDebug UP-TO-DATE
+> Task :app:mergeProjectDexDebug UP-TO-DATE
+> Task :app:mergeDebugJniLibFolders UP-TO-DATE
+> Task :app:mergeDebugNativeLibs NO-SOURCE
+> Task :app:stripDebugDebugSymbols NO-SOURCE
+> Task :app:validateSigningDebug UP-TO-DATE
+> Task :app:writeDebugAppMetadata UP-TO-DATE
+> Task :app:writeDebugSigningConfigVersions UP-TO-DATE
+> Task :app:packageDebug UP-TO-DATE
+> Task :app:createDebugApkListingFileRedirect UP-TO-DATE
+> Task :app:assembleDebug UP-TO-DATE
+
+BUILD SUCCESSFUL in 954ms
+41 actionable tasks: 41 up-to-date
diff --git a/gradle.properties b/gradle.properties
index 23339e0..bc3c070 100644
--- a/gradle.properties
+++ b/gradle.properties
@@ -15,7 +15,7 @@ org.gradle.jvmargs=-Xmx1536m
# Android operating system, and which are packaged with your app's APK
# https://developer.android.com/topic/libraries/support-library/androidx-rn
android.useAndroidX=true
-# Automatically convert third-party libraries to use AndroidX
-android.enableJetifier=true
+# Jetifier is no longer needed with pure AndroidX dependencies
+# android.enableJetifier=true
# Kotlin code style for this project: "official" or "obsolete":
kotlin.code.style=official
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 7a28483..02767eb 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -1,6 +1,5 @@
-#Sun Oct 11 17:12:52 CEST 2020
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-6.1.1-all.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip
diff --git a/gradlew b/gradlew
index cccdd3d..782aa8f 100755
--- a/gradlew
+++ b/gradlew
@@ -1,78 +1,122 @@
-#!/usr/bin/env sh
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
##############################################################################
-##
-## Gradle start up script for UN*X
-##
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced functionality shells will NOT work,
+# because this script at least requires all of the following
+# POSIX://p shell features:
+# * functions;
+# * expansions «$var», «## {var}», «## {var:-default}», «## {var:+alt}»,
+# «## {var%pattern}», «$@» and «$*»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX://p shell, so it avoid extensions
+# provided by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" style of «sed» and «tr» has been avoided, because
+# they do vary.
+#
+# The "ed" and "ex" commands have been avoided too, because their
+# syntax varies noticeably, although the "vi" command is part of POSIX.
+#
+# (3) Avoid special characters in file names, passwords and other
+# user-controlled strings used in these scripts.
+#
+# (4) Review the main page for this script for advice on
+# http://www.gradle.org/docs/
+#
##############################################################################
# Attempt to set APP_HOME
+
# Resolve links: $0 may be a link
-PRG="$0"
-# Need this for relative symlinks.
-while [ -h "$PRG" ] ; do
- ls=`ls -ld "$PRG"`
- link=`expr "$ls" : '.*-> \(.*\)$'`
- if expr "$link" : '/.*' > /dev/null; then
- PRG="$link"
- else
- PRG=`dirname "$PRG"`"/$link"
- fi
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
done
-SAVED="`pwd`"
-cd "`dirname \"$PRG\"`/" >/dev/null
-APP_HOME="`pwd -P`"
-cd "$SAVED" >/dev/null
-
-APP_NAME="Gradle"
-APP_BASE_NAME=`basename "$0"`
-# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
-DEFAULT_JVM_OPTS=""
+# This is normally unused
+# shellcheck disable=SC2034
+APP_BASE_NAME=${0##*/}
+# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
+APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
-MAX_FD="maximum"
+MAX_FD=maximum
warn () {
echo "$*"
-}
+} >&2
die () {
echo
echo "$*"
echo
exit 1
-}
+} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
-case "`uname`" in
- CYGWIN* )
- cygwin=true
- ;;
- Darwin* )
- darwin=true
- ;;
- MINGW* )
- msys=true
- ;;
- NONSTOP* )
- nonstop=true
- ;;
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
- JAVACMD="$JAVA_HOME/jre/sh/java"
+ JAVACMD=$JAVA_HOME/jre/sh/java
else
- JAVACMD="$JAVA_HOME/bin/java"
+ JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
@@ -81,92 +125,86 @@ Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
- JAVACMD="java"
- which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+ JAVACMD=java
+ if ! command -v java >/dev/null 2>&1
+ then
+ die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
+ fi
fi
# Increase the maximum file descriptors if we can.
-if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
- MAX_FD_LIMIT=`ulimit -H -n`
- if [ $? -eq 0 ] ; then
- if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
- MAX_FD="$MAX_FD_LIMIT"
- fi
- ulimit -n $MAX_FD
- if [ $? -ne 0 ] ; then
- warn "Could not set maximum file descriptor limit: $MAX_FD"
- fi
- else
- warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
- fi
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
+ # shellcheck disable=SC2039,SC3045
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
fi
-# For Darwin, add options to specify how the application appears in the dock
-if $darwin; then
- GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
-fi
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
-# For Cygwin, switch paths to Windows format before running java
-if $cygwin ; then
- APP_HOME=`cygpath --path --mixed "$APP_HOME"`
- CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
- JAVACMD=`cygpath --unix "$JAVACMD"`
-
- # We build the pattern for arguments to be converted via cygpath
- ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
- SEP=""
- for dir in $ROOTDIRSRAW ; do
- ROOTDIRS="$ROOTDIRS$SEP$dir"
- SEP="|"
- done
- OURCYGPATTERN="(^($ROOTDIRS))"
- # Add a user-defined pattern to the cygpath arguments
- if [ "$GRADLE_CYGPATTERN" != "" ] ; then
- OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
- fi
# Now convert the arguments - kludge to limit ourselves to /bin/sh
- i=0
- for arg in "$@" ; do
- CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
- CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
-
- if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
- eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
- else
- eval `echo args$i`="\"$arg\""
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#)}; t=/${t%%/*} # looks like a POSIX://p filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
fi
- i=$((i+1))
+ # Roll the args list around exactly as many times as the number of
+ # temporary values we need to pass arguments get to here.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
done
- case $i in
- (0) set -- ;;
- (1) set -- "$args0" ;;
- (2) set -- "$args0" "$args1" ;;
- (3) set -- "$args0" "$args1" "$args2" ;;
- (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
- (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
- (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
- (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
- (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
- (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
- esac
fi
-# Escape application args
-save () {
- for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
- echo " "
-}
-APP_ARGS=$(save "$@")
-
-# Collect all arguments for the java command, following the shell quoting and substitution rules
-eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
-# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong
-if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then
- cd "$(dirname "$0")"
-fi
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Collect all arguments for the java command:
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, GRADLE_OPTS, and optsEnvironmentVar are not://p
+# temporary values we need to pass arguments.
+# * To://p avoid://p://p://p collisions://p, we://p use://p://p "{@Q}" instead of "$@"
+# * expand://p://p arguments://p from://p build://p.gradle etc.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xeli" is set, since we may be sourced.
+unset CDPATH
exec "$JAVACMD" "$@"
diff --git a/settings.gradle.kts b/settings.gradle.kts
index ea5de87..4d7624d 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -1,2 +1,18 @@
+pluginManagement {
+ repositories {
+ google()
+ mavenCentral()
+ gradlePluginPortal()
+ }
+}
+
+dependencyResolutionManagement {
+ repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
include(":app")
-rootProject.name="CanIDrive"
+rootProject.name = "CanIDrive"