From f6f9522b4d3354a31fb9f5305e9d0193524b62b7 Mon Sep 17 00:00:00 2001 From: JohanPy Date: Sat, 17 Jan 2026 16:57:02 +0100 Subject: [PATCH 01/11] feat: Upgrade Android Gradle Plugin, Kotlin, dependencies, API levels, and migrate to View Binding. --- .idea/gradle.xml | 12 +- .idea/misc.xml | 8 +- app/build.gradle.kts | 85 +++--- app/src/main/AndroidManifest.xml | 4 +- .../vaudibert/canidrive/ui/MainActivity.kt | 16 +- .../canidrive/ui/fragment/AddDrinkFragment.kt | 48 ++-- .../canidrive/ui/fragment/DrinkerFragment.kt | 91 ++++-- .../canidrive/ui/fragment/DriveFragment.kt | 80 +++--- .../ui/fragment/EditPresetFragment.kt | 45 ++- .../canidrive/ui/fragment/SplashFragment.kt | 25 +- app/src/main/res/layout/activity_main.xml | 3 +- build.gradle.kts | 29 +- gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 264 ++++++++++-------- settings.gradle.kts | 18 +- 15 files changed, 422 insertions(+), 309 deletions(-) diff --git a/.idea/gradle.xml b/.idea/gradle.xml index e9969a1..7505d8d 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,20 +1,10 @@ - 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/app/build.gradle.kts b/app/build.gradle.kts index 452fbea..05effb6 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,102 +1,95 @@ 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") } 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") { + release { isMinifyEnabled = false 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") // 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.appcompat:appcompat:1.7.0") + implementation("androidx.constraintlayout:constraintlayout:2.2.0") + implementation("com.google.android.material:material:1.12.0") implementation("androidx.legacy:legacy-support-v4:1.0.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.espresso:espresso-core:3.6.1") // Kotlin needs for testing implementation(kotlin("reflect")) testImplementation(kotlin("test")) - testImplementation(kotlin("test-junit")) // 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") - } -tasks.withType < Test > { - // Use the native JUnit support of Gradle. +tasks.withType { useJUnitPlatform() } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 6fcdfd9..4d8f825 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,6 +1,5 @@ - + 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..ed4c2a1 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/MainActivity.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/MainActivity.kt @@ -4,13 +4,14 @@ import android.os.Bundle import android.view.Menu import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity -import androidx.navigation.findNavController +import androidx.navigation.fragment.NavHostFragment 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() { + private lateinit var binding: ActivityMainBinding val time = CanIDrive.instance.time override fun onCreate(savedInstanceState: Bundle?) { @@ -19,15 +20,18 @@ class MainActivity : AppCompatActivity() { 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) + binding.appBarDrinker.visibility = if (destination.id == R.id.splashFragment) AppBarLayout.GONE else AppBarLayout.VISIBLE - toolbar.title = when (destination.id) { + 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) 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..0098a31 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 @@ -7,39 +7,54 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.Button +import android.widget.ListView import android.widget.SeekBar +import android.widget.TextView import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController +import com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBar import com.vaudibert.canidrive.KeyboardUtils import com.vaudibert.canidrive.R +import com.vaudibert.canidrive.databinding.FragmentAddDrinkBinding import com.vaudibert.canidrive.ui.CanIDrive 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 java.util.Date /** * Fragment to add a drink. */ class AddDrinkFragment : Fragment() { + 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: ListView + 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) + ): View { + _binding = FragmentAddDrinkBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // 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 = CanIDrive.instance.mainRepository.drinkRepository val presetService = drinkRepository.presetService @@ -47,7 +62,7 @@ class AddDrinkFragment : Fragment() { val presetDrinksAdapter = PresetDrinksAdapter( - this.context!!, + requireContext(), viewLifecycleOwner, { findNavController().navigate( @@ -58,19 +73,16 @@ class AddDrinkFragment : Fragment() { ) listViewPresetDrinks.adapter = presetDrinksAdapter - drinkRepository.liveSelectedPreset.observe(viewLifecycleOwner, Observer { + drinkRepository.liveSelectedPreset.observe(viewLifecycleOwner) { if (it == null) buttonValidateNewDrink.visibility = Button.INVISIBLE 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)) @@ -118,4 +130,8 @@ class AddDrinkFragment : Fragment() { delay = delays[0] } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } 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..a8fc0a2 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 @@ -7,22 +7,26 @@ 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.NumberPicker +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.databinding.FragmentDrinkerBinding 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 kotlin.math.roundToInt /** @@ -31,25 +35,56 @@ import kotlin.math.roundToInt // TODO : split into 2 fragments : body and drive law ? class DrinkerFragment : Fragment() { + private var _binding: FragmentDrinkerBinding? = null + private val binding get() = _binding!! + private var weight = 0.0 private var sex = "NONE" - 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 + // Views from included layouts (constraint_content_drinker_pickers.xml) + private lateinit var numberPickerWeight: NumberPicker + 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 + override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle? - ): View? { - - // Inflate the layout for this fragment - return inflater.inflate(R.layout.fragment_drinker, container, false) + ): View { + _binding = FragmentDrinkerBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // Initialize views from included layouts + numberPickerWeight = view.findViewById(R.id.numberPickerWeight) + 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) + + 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 = CanIDrive.instance.mainRepository digestionRepository = mainRepository.digestionRepository @@ -68,7 +103,7 @@ class DrinkerFragment : Fragment() { 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,7 +113,7 @@ 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) @@ -97,10 +132,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 } @@ -133,7 +168,7 @@ class DrinkerFragment : Fragment() { } private fun setupValidationButton(digestionRepository: DigestionRepository) { - buttonValidateDrinker.setOnClickListener { + binding.buttonValidateDrinker.setOnClickListener { digestionRepository.body.sex = when { radioMale.isChecked -> "MALE" @@ -149,7 +184,7 @@ class DrinkerFragment : Fragment() { driveLawService.customCountryLimit = editTextCurrentLimit.text.toString().toDouble() - var navOptions:NavOptions? = null + var navOptions: NavOptions? = null if (!mainRepository.init) { mainRepository.init = true @@ -212,7 +247,7 @@ class DrinkerFragment : Fragment() { countries: List ) { spinnerCountry.adapter = ArrayAdapter( - this.context!!, + requireContext(), R.layout.item_country_spinner, countries ) @@ -237,7 +272,7 @@ class DrinkerFragment : Fragment() { } - override fun onNothingSelected(parent: AdapterView<*>?) { } + override fun onNothingSelected(parent: AdapterView<*>?) {} } editTextCurrentLimit.addTextChangedListener(object : TextWatcher { @@ -246,12 +281,16 @@ class DrinkerFragment : Fragment() { driveLawService.customCountryLimit = s.toString().toDouble() } - 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 + ) { } }) } @@ -262,4 +301,8 @@ class DrinkerFragment : Fragment() { ) } + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } 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..9853afd 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 @@ -8,19 +8,18 @@ import android.view.LayoutInflater import android.view.View import android.view.ViewGroup import android.widget.LinearLayout +import android.widget.ListView import android.widget.TextView import androidx.core.content.ContextCompat import androidx.fragment.app.Fragment -import androidx.lifecycle.Observer import androidx.navigation.fragment.findNavController import com.vaudibert.canidrive.R +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 java.text.DateFormat /** @@ -31,6 +30,9 @@ import java.text.DateFormat */ class DriveFragment : Fragment() { + private var _binding: FragmentDriveStatusBinding? = null + private val binding get() = _binding!! + private lateinit var digestionRepository: DigestionRepository private lateinit var drinkRepository: DrinkRepository private lateinit var drinkerStatusService: DrinkerStatusService @@ -39,87 +41,96 @@ 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: ListView + 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) + ): View { + _binding = FragmentDriveStatusBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // Initialize views from included layouts + textViewPastDrinks = view.findViewById(R.id.textViewPastDrinks) + listViewPastDrinks = view.findViewById(R.id.listViewPastDrinks) + val mainRepository = CanIDrive.instance.mainRepository drinkerStatusService = mainRepository.drinkerStatusService drinkRepository = mainRepository.drinkRepository digestionRepository = mainRepository.digestionRepository - ingestedDrinksAdapter = IngestedDrinksAdapter(this.context!!) + ingestedDrinksAdapter = IngestedDrinksAdapter(requireContext()) 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() ) } - buttonAddDrink.setOnClickListener { + binding.buttonAddDrink.setOnClickListener { findNavController().navigate( DriveFragmentDirections.actionDriveFragmentToAddDrinkFragment() ) } } + private fun updateDriveStatus() { val drinkerStatus = drinkerStatusService.status() 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 } else { - linearAlcoholRate.visibility = LinearLayout.VISIBLE - linearWaitToSober.visibility = LinearLayout.VISIBLE - textViewAlcoholRate.text = + binding.linearAlcoholRate.visibility = LinearLayout.VISIBLE + binding.linearWaitToSober.visibility = LinearLayout.VISIBLE + binding.textViewAlcoholRate.text = "${drinkerStatus.alcoholRate.toString().substring(0, 4)} g/L" - textViewTimeToSober.text = DateFormat + 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)) } else { - textViewTimeToDrive.text = DateFormat + 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)) } } @@ -146,4 +157,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/EditPresetFragment.kt b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/EditPresetFragment.kt index 1bbed10..bd7b2af 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,33 +5,45 @@ 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 java.text.DecimalFormat class EditPresetFragment : Fragment() { + + 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) + ): View { + _binding = FragmentAddPresetBinding.inflate(inflater, container, false) + return binding.root } override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) + // Initialize views from included layouts + numberPickerVolume = view.findViewById(R.id.numberPickerVolume) + numberPickerDegree = view.findViewById(R.id.numberPickerDegree) + val drinkRepository = CanIDrive.instance.mainRepository.drinkRepository val presetService = drinkRepository.presetService val selectedPreset = presetService.selectedPreset @@ -39,21 +51,21 @@ class EditPresetFragment : Fragment() { 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 ) } else { presetService.addNewPreset( - editTextNewPresetName.text.toString(), + binding.editTextNewPresetName.text.toString(), volume, degree ) @@ -80,7 +92,7 @@ class EditPresetFragment : Fragment() { numberPickerDegree.minValue = 0 numberPickerDegree.maxValue = degreeLabels.size - 1 numberPickerDegree.displayedValues = degreeLabels - val indexOfDegree = IngestedDrink.degrees.indexOf(degree) + val indexOfDegree = IngestedDrink.degrees.toList().indexOf(degree) val startDegree = if (indexOfDegree < 0) degreeLabels.size / 2 else @@ -95,14 +107,14 @@ class EditPresetFragment : Fragment() { private fun setVolumePicker() { val volumeLabels = IngestedDrink.volumes.map { vol -> if (vol < 1000.0) - "${doubleFormat.format(vol )} mL" + "${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 indexOfVolume = IngestedDrink.volumes.toList().indexOf(volume) val startVolume = if (indexOfVolume < 0) volumeLabels.size / 2 else @@ -113,4 +125,9 @@ class EditPresetFragment : Fragment() { volume = IngestedDrink.volumes[newVal] } } + + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } 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..bb8d516 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 @@ -10,32 +10,35 @@ import android.view.ViewGroup import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.vaudibert.canidrive.BuildConfig -import com.vaudibert.canidrive.R +import com.vaudibert.canidrive.databinding.FragmentSplashBinding import com.vaudibert.canidrive.ui.CanIDrive -import kotlinx.android.synthetic.main.fragment_splash.* /** * The splash fragment, to display icon, version and do stuff in background. */ class SplashFragment : Fragment() { + 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) + ): View { + _binding = FragmentSplashBinding.inflate(inflater, container, false) + return binding.root } 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 mainHandler = Handler(Looper.getMainLooper()) - mainHandler.postDelayed( { + mainHandler.postDelayed({ val init = CanIDrive.instance.mainRepository.init @@ -46,10 +49,10 @@ class SplashFragment : Fragment() { findNavController().navigate(action) }, 1000) - - super.onResume() } - - + override fun onDestroyView() { + super.onDestroyView() + _binding = null + } } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 43a0c30..315d4f0 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -33,12 +33,11 @@ - diff --git a/build.gradle.kts b/build.gradle.kts index c3e5187..5386578 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,25 +1,6 @@ -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.7.3" 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 } diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 7a28483..efdcc4a 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.11.1-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" From ab638abd77910291b6979f881798e87053885cc7 Mon Sep 17 00:00:00 2001 From: JohanPy Date: Sat, 17 Jan 2026 17:20:26 +0100 Subject: [PATCH 02/11] Upgrade Gradle wrapper to 8.13 Bump Android Application plugin version to 8.13.2 Update Java bytecode target level to 21 --- .idea/AndroidProjectSystem.xml | 6 ++++++ .idea/compiler.xml | 2 +- .idea/deploymentTargetSelector.xml | 10 ++++++++++ .idea/gradle.xml | 8 ++++++++ .idea/migrations.xml | 10 ++++++++++ .idea/runConfigurations.xml | 17 +++++++++++++++++ build.gradle.kts | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 8 files changed, 54 insertions(+), 3 deletions(-) create mode 100644 .idea/AndroidProjectSystem.xml create mode 100644 .idea/deploymentTargetSelector.xml create mode 100644 .idea/migrations.xml create mode 100644 .idea/runConfigurations.xml 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 7505d8d..639c779 100644 --- a/.idea/gradle.xml +++ b/.idea/gradle.xml @@ -1,10 +1,18 @@ + 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/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/build.gradle.kts b/build.gradle.kts index 5386578..48101ee 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,5 +1,5 @@ plugins { - id("com.android.application") version "8.7.3" apply false + 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 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index efdcc4a..02767eb 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-all.zip From 282289f478a739d08d3c3c8960782ef298127d57 Mon Sep 17 00:00:00 2001 From: JohanPy Date: Tue, 24 Feb 2026 15:33:56 +0100 Subject: [PATCH 03/11] fix: corriger les permissions des fichiers d'images et du script gradlew --- .../en-US/images/image_1024_500_playstore.png | Bin .../android/en-US/images/phoneScreenshots/1.png | Bin .../android/en-US/images/phoneScreenshots/2.png | Bin .../android/en-US/images/phoneScreenshots/3.png | Bin .../android/en-US/images/phoneScreenshots/4.png | Bin .../android/en-US/images/phoneScreenshots/5.png | Bin .../android/en-US/images/phoneScreenshots/6.png | Bin .../fr-FR/images/image_1024_500_playstore.png | Bin .../android/fr-FR/images/phoneScreenshots/1.png | Bin .../android/fr-FR/images/phoneScreenshots/2.png | Bin .../android/fr-FR/images/phoneScreenshots/3.png | Bin .../android/fr-FR/images/phoneScreenshots/4.png | Bin .../android/fr-FR/images/phoneScreenshots/5.png | Bin gradlew | 0 14 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 fastlane/metadata/android/en-US/images/image_1024_500_playstore.png mode change 100755 => 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/1.png mode change 100755 => 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/2.png mode change 100755 => 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/3.png mode change 100755 => 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/4.png mode change 100755 => 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/5.png mode change 100755 => 100644 fastlane/metadata/android/en-US/images/phoneScreenshots/6.png mode change 100755 => 100644 fastlane/metadata/android/fr-FR/images/image_1024_500_playstore.png mode change 100755 => 100644 fastlane/metadata/android/fr-FR/images/phoneScreenshots/1.png mode change 100755 => 100644 fastlane/metadata/android/fr-FR/images/phoneScreenshots/2.png mode change 100755 => 100644 fastlane/metadata/android/fr-FR/images/phoneScreenshots/3.png mode change 100755 => 100644 fastlane/metadata/android/fr-FR/images/phoneScreenshots/4.png mode change 100755 => 100644 fastlane/metadata/android/fr-FR/images/phoneScreenshots/5.png mode change 100755 => 100644 gradlew diff --git a/fastlane/metadata/android/en-US/images/image_1024_500_playstore.png b/fastlane/metadata/android/en-US/images/image_1024_500_playstore.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/1.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/2.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/3.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/4.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/5.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png b/fastlane/metadata/android/en-US/images/phoneScreenshots/6.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/fr-FR/images/image_1024_500_playstore.png b/fastlane/metadata/android/fr-FR/images/image_1024_500_playstore.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/fr-FR/images/phoneScreenshots/1.png b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/1.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/fr-FR/images/phoneScreenshots/2.png b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/2.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/fr-FR/images/phoneScreenshots/3.png b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/3.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/fr-FR/images/phoneScreenshots/4.png b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/4.png old mode 100755 new mode 100644 diff --git a/fastlane/metadata/android/fr-FR/images/phoneScreenshots/5.png b/fastlane/metadata/android/fr-FR/images/phoneScreenshots/5.png old mode 100755 new mode 100644 diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 From e855251316565782ec6661ee0d632ba5907d23a8 Mon Sep 17 00:00:00 2001 From: JohanPy Date: Tue, 24 Feb 2026 15:39:52 +0100 Subject: [PATCH 04/11] feat: add spanish --- app/src/main/res/values-de-rDE/strings.xml | 1 + app/src/main/res/values-es-rES/strings.xml | 48 ++++++++++++++++++++++ 2 files changed, 49 insertions(+) create mode 100644 app/src/main/res/values-es-rES/strings.xml diff --git a/app/src/main/res/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 3accb72..9a4979f 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 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..03c4300 --- /dev/null +++ b/app/src/main/res/values-es-rES/strings.xml @@ -0,0 +1,48 @@ + + + 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 + From 45ffba0544a871d006a244c931c0c065c135d4ab Mon Sep 17 00:00:00 2001 From: JohanPy Date: Tue, 24 Feb 2026 16:12:04 +0100 Subject: [PATCH 05/11] feat: Implement legal disclaimer and user preferences handling - Added a disclaimer dialog in SplashFragment to inform users about BAC estimation limitations. - Persisted user acceptance of the disclaimer in SharedPreferences. - Updated DriveFragment to format alcohol rate display using String.format for better precision. - Refactored KeyboardUtils to a new package for better organization. - Introduced a Sex enum to improve clarity and type safety in BAC calculations. - Updated DigestionRepository to use the new Sex enum and handle user preferences more robustly. - Enhanced accessibility by adding content descriptions to various UI elements. - Improved internationalization by replacing hardcoded strings with resource references. - Fixed various layout issues and deprecated API usages across XML files. - Updated unit tests to reflect changes in the Sex enum and ensure proper functionality. --- .vscode/settings.json | 6 + TODO.md | 149 ++++++++++++++++++ app/build.gradle.kts | 6 +- .../domain/digestion/PhysicalBody.kt | 30 ++-- .../canidrive/domain/digestion/Sex.kt | 21 +++ .../canidrive/domain/drink/PresetDrink.kt | 1 - .../domain/drivelaw/DriveLawService.kt | 2 +- .../canidrive/ui/TimeServiceAndroid.kt | 8 +- .../ui/adapter/IngestedDrinksAdapter.kt | 2 +- .../ui/adapter/PresetDrinksAdapter.kt | 10 +- .../canidrive/ui/fragment/AddDrinkFragment.kt | 4 +- .../canidrive/ui/fragment/DrinkerFragment.kt | 22 +-- .../canidrive/ui/fragment/DriveFragment.kt | 2 +- .../ui/fragment/EditPresetFragment.kt | 2 +- .../canidrive/ui/fragment/SplashFragment.kt | 53 ++++++- .../ui/repository/DigestionRepository.kt | 9 +- .../canidrive/{ => ui/util}/KeyboardUtils.kt | 2 +- .../constraint_content_drinker_pickers.xml | 4 +- .../main/res/layout/fragment_add_preset.xml | 1 + app/src/main/res/layout/fragment_drinker.xml | 1 + .../main/res/layout/fragment_drive_status.xml | 2 + app/src/main/res/layout/fragment_splash.xml | 2 +- app/src/main/res/layout/item_past_drink.xml | 5 +- app/src/main/res/layout/item_preset_drink.xml | 8 +- app/src/main/res/values-de-rDE/strings.xml | 10 ++ app/src/main/res/values-es-rES/strings.xml | 10 ++ app/src/main/res/values-fr-rFR/strings.xml | 10 ++ app/src/main/res/values-it-rIT/strings.xml | 32 ++-- app/src/main/res/values/strings.xml | 11 ++ .../canidrive/domain/DigestionServiceTest.kt | 3 +- .../canidrive/domain/PhysicalBodyTest.kt | 15 +- .../domain/drink/PresetDrinkServiceTest.kt | 2 +- gradle.properties | 4 +- 33 files changed, 352 insertions(+), 97 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 TODO.md create mode 100644 app/src/main/java/com/vaudibert/canidrive/domain/digestion/Sex.kt rename app/src/main/java/com/vaudibert/canidrive/{ => ui/util}/KeyboardUtils.kt (97%) 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..7597607 --- /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 + +- [ ] **Fix destructive database migration** – `MainRepository.kt`: `fallbackToDestructiveMigration()` silently wipes all user data on schema changes. Implement proper Room `Migration` objects. The commented-out `migration_1_2` should be completed and uncommented. +- [ ] **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. +- [ ] **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 + +- [ ] **Introduce ViewModels** – No `ViewModel` is used anywhere. Fragments access `CanIDrive.instance.mainRepository` directly. Create `DriveViewModel`, `DrinkerViewModel`, `AddDrinkViewModel` with `viewModelScope`-bound coroutines. +- [ ] **Introduce Dependency Injection (Hilt or Koin)** – Replace the global `CanIDrive` Application singleton with a proper DI framework. Improves testability and reduces coupling. +- [ ] **Move repositories out of `ui` package** – `MainRepository`, `DrinkRepository`, `DigestionRepository`, `DriveLawRepository` are in `ui.repository`. Move to `data.repository` (persistence) and `domain.repository` (interfaces). +- [ ] **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. +- [ ] **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. +- [ ] **Use `EncryptedSharedPreferences` for health data** – `DigestionRepository.kt`: weight, sex, and alcohol tolerance are stored plain. Use `androidx.security:security-crypto` for sensitive health data. +- [ ] **Review `allowBackup`** – `AndroidManifest.xml`: `android:allowBackup="true"`. Consider `android:allowBackup="false"` or a `fullBackupContent` XML to exclude SharedPreferences. + +--- + +## MEDIUM – Performance + +- [ ] **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. +- [ ] **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. +- [ ] **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)`. +- [ ] **Replace `toggleSoftInput(SHOW_FORCED, 0)`** – `KeyboardUtils.kt`: deprecated since API 31. Use `WindowInsetsController.show(WindowInsetsCompat.Type.ime())` instead. +- [ ] **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`. +- [ ] **Replace mutable lambda callbacks with `Flow`/`LiveData`** – `PhysicalBody.onUpdate`, `DriveLawService.onCustomLimitCallback`, `PresetDrinkService.onPresetsChanged`: fragile `var` lambdas. Replace with `StateFlow` or `LiveData`. +- [ ] **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`. +- [ ] **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`. +- [ ] **Add tests for `IngestedDrink.alcoholMass()`** – Critical BAC calculation has no direct unit test. +- [ ] **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. +- [ ] **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. +- [ ] **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`. +- [ ] **Hardcoded unit string `"g/L"`** – `constraint_content_drinker_country.xml`: move to string resource; consider supporting g/dL, mg/100mL, ‰ per country selection. +- [ ] **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. +- [ ] **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. +- [ ] **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. +- [ ] **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 05effb6..2336a22 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -24,7 +24,8 @@ android { buildTypes { release { - isMinifyEnabled = false + isMinifyEnabled = true + isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } @@ -54,7 +55,6 @@ dependencies { implementation("androidx.appcompat:appcompat:1.7.0") implementation("androidx.constraintlayout:constraintlayout:2.2.0") implementation("com.google.android.material:material:1.12.0") - implementation("androidx.legacy:legacy-support-v4:1.0.0") // JUnit testImplementation("org.junit.jupiter:junit-jupiter-api:5.11.4") @@ -63,7 +63,7 @@ dependencies { androidTestImplementation("androidx.test.espresso:espresso-core:3.6.1") // Kotlin needs for testing - implementation(kotlin("reflect")) + testImplementation(kotlin("reflect")) testImplementation(kotlin("test")) // Navigation (Kotlin) 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..4ed7600 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 @@ -6,20 +6,20 @@ package com.vaudibert.canidrive.domain.digestion */ 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 - - private val FEMALE_SEX_FACTOR = 0.6 - private val FEMALE_MIN_DECREASE = 0.085 - private val FEMALE_MAX_DECREASE = 0.1 + 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 + } - 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) } @@ -27,7 +27,7 @@ class PhysicalBody { var weight = 80.0 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) onUpdate(sex, weight, alcoholTolerance) } @@ -40,14 +40,14 @@ class PhysicalBody { } } - private fun decreaseFactorWith(sex: String, tolerance: Double): Double { - return if (sex == "MALE") + 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 onUpdate = { _ : Sex, _ : Double, _ : Double -> } var decreaseFactor: Double = FEMALE_MIN_DECREASE 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..f85b1e7 --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/domain/digestion/Sex.kt @@ -0,0 +1,21 @@ +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/PresetDrink.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drink/PresetDrink.kt index a2c00a1..4578a52 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 @@ -18,7 +18,6 @@ open class PresetDrink( 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/drivelaw/DriveLawService.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drivelaw/DriveLawService.kt index c4d6862..b7be6c6 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 @@ -101,7 +101,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 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..f4f62fa 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..7ff2b2f 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 @@ -21,7 +21,7 @@ class IngestedDrinksAdapter( private var ingestedDrinkList : List = emptyList() - private val DAY_IN_MILLIS = 3600*1000*24 + private val DAY_IN_MILLIS = 3600L * 1000 * 24 private val dateFormat = DateFormat.getTimeInstance(DateFormat.SHORT) 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..9a5f479 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 @@ -31,11 +31,17 @@ class PresetDrinksAdapter( private var presetDrinks: List = emptyList() + private var selectedPreset: PresetDrinkEntity? = null + init { drinkRepository.livePresetDrinks.observe(lifecycleOwner, Observer { presetDrinks = it notifyDataSetChanged() }) + drinkRepository.liveSelectedPreset.observe(lifecycleOwner, Observer { + selectedPreset = it + notifyDataSetChanged() + }) } @@ -61,9 +67,7 @@ class PresetDrinksAdapter( descriptionText.text = presetDrink.name glassImage.setImageResource(R.drawable.wine_glass) - drinkRepository.liveSelectedPreset.observe(lifecycleOwner, Observer { - updatePresetColor(presetDrink, drinkView, deleteButton, it) - }) + updatePresetColor(presetDrink, drinkView, deleteButton, selectedPreset) val clickListener = { _: View -> presetService.selectedPreset = 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 0098a31..8745f10 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 @@ -13,7 +13,7 @@ import android.widget.TextView import androidx.fragment.app.Fragment import androidx.navigation.fragment.findNavController import com.h6ah4i.android.widget.verticalseekbar.VerticalSeekBar -import com.vaudibert.canidrive.KeyboardUtils +import com.vaudibert.canidrive.ui.util.KeyboardUtils import com.vaudibert.canidrive.R import com.vaudibert.canidrive.databinding.FragmentAddDrinkBinding import com.vaudibert.canidrive.ui.CanIDrive @@ -28,8 +28,6 @@ class AddDrinkFragment : Fragment() { 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 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 a8fc0a2..ead145a 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 @@ -19,7 +19,7 @@ import android.widget.TextView import androidx.fragment.app.Fragment import androidx.navigation.NavOptions import androidx.navigation.fragment.findNavController -import com.vaudibert.canidrive.KeyboardUtils +import com.vaudibert.canidrive.ui.util.KeyboardUtils import com.vaudibert.canidrive.R import com.vaudibert.canidrive.databinding.FragmentDrinkerBinding import com.vaudibert.canidrive.domain.drivelaw.DriveLaw @@ -39,7 +39,7 @@ class DrinkerFragment : Fragment() { 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 @@ -171,9 +171,9 @@ class DrinkerFragment : Fragment() { binding.buttonValidateDrinker.setOnClickListener { digestionRepository.body.sex = when { - radioMale.isChecked -> "MALE" - radioFemale.isChecked -> "FEMALE" - else -> "OTHER" + radioMale.isChecked -> Sex.MALE + radioFemale.isChecked -> Sex.FEMALE + else -> Sex.OTHER } val levelCount = (digestionRepository.toleranceLevels.size - 1).coerceAtLeast(1) @@ -182,7 +182,7 @@ class DrinkerFragment : Fragment() { seekBarAlcoholTolerance.progress.toDouble() / levelCount.toDouble() - driveLawService.customCountryLimit = editTextCurrentLimit.text.toString().toDouble() + driveLawService.customCountryLimit = editTextCurrentLimit.text.toString().toDoubleOrNull() ?: driveLawService.customCountryLimit var navOptions: NavOptions? = null @@ -214,12 +214,12 @@ 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 } } @@ -278,7 +278,9 @@ class DrinkerFragment : Fragment() { editTextCurrentLimit.addTextChangedListener(object : TextWatcher { override fun afterTextChanged(s: Editable) { - driveLawService.customCountryLimit = s.toString().toDouble() + s.toString().toDoubleOrNull()?.let { + driveLawService.customCountryLimit = it + } } override fun beforeTextChanged( 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 9853afd..9b66f31 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 @@ -107,7 +107,7 @@ class DriveFragment : Fragment() { binding.linearAlcoholRate.visibility = LinearLayout.VISIBLE binding.linearWaitToSober.visibility = LinearLayout.VISIBLE binding.textViewAlcoholRate.text = - "${drinkerStatus.alcoholRate.toString().substring(0, 4)} g/L" + String.format("%.2f g/L", drinkerStatus.alcoholRate) binding.textViewTimeToSober.text = DateFormat .getTimeInstance(DateFormat.SHORT) 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 bd7b2af..69f7a04 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 @@ -8,7 +8,7 @@ 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.ui.util.KeyboardUtils import com.vaudibert.canidrive.R import com.vaudibert.canidrive.databinding.FragmentAddPresetBinding import com.vaudibert.canidrive.domain.drink.IngestedDrink 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 bb8d516..63052e7 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,15 +1,18 @@ 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.databinding.FragmentSplashBinding import com.vaudibert.canidrive.ui.CanIDrive @@ -39,18 +42,54 @@ class SplashFragment : Fragment() { val mainHandler = Handler(Looper.getMainLooper()) mainHandler.postDelayed({ + if (!isAdded) return@postDelayed - val init = CanIDrive.instance.mainRepository.init + val sharedPref = requireContext().getSharedPreferences( + getString(R.string.user_preferences), Context.MODE_PRIVATE + ) + val disclaimerAccepted = sharedPref.getBoolean( + getString(R.string.disclaimer_pref_key), false + ) - val action = if (init) - SplashFragmentDirections.actionSplashFragmentToDriveFragment() - else - SplashFragmentDirections.actionSplashFragmentToDrinkerFragment() - - findNavController().navigate(action) + if (!disclaimerAccepted) { + showDisclaimerDialog() + } else { + navigateToNext() + } }, 1000) } + 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() + } + + private fun navigateToNext() { + if (!isAdded) return + val init = CanIDrive.instance.mainRepository.init + + val action = if (init) + SplashFragmentDirections.actionSplashFragmentToDriveFragment() + else + SplashFragmentDirections.actionSplashFragmentToDrinkerFragment() + + 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 index 9784fab..d466425 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DigestionRepository.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/repository/DigestionRepository.kt @@ -6,6 +6,7 @@ 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.digestion.Sex import com.vaudibert.canidrive.domain.drink.IIngestedDrinkProvider /** @@ -43,17 +44,19 @@ class DigestionRepository(context: Context, drinkProvider: IIngestedDrinkProvide 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 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 - body.onUpdate = { updatedSex: String, updatedWeight: Double, updatedTolerance:Double -> run { + body.onUpdate = { updatedSex: Sex, updatedWeight: Double, updatedTolerance:Double -> run { sharedPref .edit() - .putString(context.getString(R.string.user_sex), updatedSex) + .putString(context.getString(R.string.user_sex), updatedSex.name) .putFloat(context.getString(R.string.user_weight), updatedWeight.toFloat()) .putFloat("USER_TOLERANCE", updatedTolerance.toFloat()) .apply() 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 97% 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..dc6fdb0 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 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..08ca2f8 100644 --- a/app/src/main/res/layout/constraint_content_drinker_pickers.xml +++ b/app/src/main/res/layout/constraint_content_drinker_pickers.xml @@ -94,11 +94,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/fragment_add_preset.xml b/app/src/main/res/layout/fragment_add_preset.xml index 3418e26..64c4dc6 100644 --- a/app/src/main/res/layout/fragment_add_preset.xml +++ b/app/src/main/res/layout/fragment_add_preset.xml @@ -53,6 +53,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_check_white_24dp" + android:contentDescription="@string/content_desc_validate" android:layout_margin="24dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/fragment_drinker.xml b/app/src/main/res/layout/fragment_drinker.xml index 787c2e8..2a97a36 100644 --- a/app/src/main/res/layout/fragment_drinker.xml +++ b/app/src/main/res/layout/fragment_drinker.xml @@ -53,6 +53,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_check_white_24dp" + android:contentDescription="@string/content_desc_validate" android:layout_margin="24dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/fragment_drive_status.xml b/app/src/main/res/layout/fragment_drive_status.xml index 3451481..9456860 100644 --- a/app/src/main/res/layout/fragment_drive_status.xml +++ b/app/src/main/res/layout/fragment_drive_status.xml @@ -156,6 +156,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_add_white_24dp" + android:contentDescription="@string/content_desc_add_drink" android:layout_margin="24dp" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintBottom_toBottomOf="parent" @@ -166,6 +167,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:src="@drawable/ic_person_white_24dp" + android:contentDescription="@string/content_desc_profile" android:layout_margin="24dp" app:layout_constraintStart_toStartOf="parent" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/fragment_splash.xml b/app/src/main/res/layout/fragment_splash.xml index 287ae17..9e9a6a2 100644 --- a/app/src/main/res/layout/fragment_splash.xml +++ b/app/src/main/res/layout/fragment_splash.xml @@ -11,7 +11,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@string/can_i_drive_question" - android:textSize="20pt" + android:textSize="40sp" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintStart_toStartOf="parent" diff --git a/app/src/main/res/layout/item_past_drink.xml b/app/src/main/res/layout/item_past_drink.xml index 428c8be..b9ac4c4 100644 --- a/app/src/main/res/layout/item_past_drink.xml +++ b/app/src/main/res/layout/item_past_drink.xml @@ -44,8 +44,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_toEndOf="@+id/linearPastDrinkTime" app:layout_constraintTop_toTopOf="parent" /> @@ -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/values-de-rDE/strings.xml b/app/src/main/res/values-de-rDE/strings.xml index 9a4979f..9e8478b 100644 --- a/app/src/main/res/values-de-rDE/strings.xml +++ b/app/src/main/res/values-de-rDE/strings.xml @@ -45,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 index 03c4300..339248f 100644 --- a/app/src/main/res/values-es-rES/strings.xml +++ b/app/src/main/res/values-es-rES/strings.xml @@ -45,4 +45,14 @@ 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..602f4a8 100644 --- a/app/src/main/res/values-fr-rFR/strings.xml +++ b/app/src/main/res/values-fr-rFR/strings.xml @@ -45,4 +45,14 @@ 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 \ 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/strings.xml b/app/src/main/res/values/strings.xml index 44b8ee5..d642825 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -68,4 +68,15 @@ 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 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..1a9017c 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 @@ -33,7 +34,7 @@ internal class DigestionServiceTest { @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) 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..b5bc336 100644 --- a/app/src/test/java/com/vaudibert/canidrive/domain/PhysicalBodyTest.kt +++ b/app/src/test/java/com/vaudibert/canidrive/domain/PhysicalBodyTest.kt @@ -1,6 +1,7 @@ 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 @@ -9,7 +10,7 @@ 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 +20,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 +30,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 +59,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) 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..18c33b8 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,6 +1,6 @@ package com.vaudibert.canidrive.domain.drink -import org.junit.Test +import org.junit.jupiter.api.Test import org.junit.jupiter.api.Assertions.* import java.util.* 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 From 300bd3d8082c3c0e293f75d0f3fda5f52a352b9b Mon Sep 17 00:00:00 2001 From: JohanPy Date: Tue, 24 Feb 2026 16:31:45 +0100 Subject: [PATCH 06/11] Fix destructive database migration --- TODO.md | 2 +- .../canidrive/ui/repository/MainRepository.kt | 23 +++++++++++++++---- 2 files changed, 20 insertions(+), 5 deletions(-) diff --git a/TODO.md b/TODO.md index 7597607..3ebc4b1 100644 --- a/TODO.md +++ b/TODO.md @@ -15,7 +15,7 @@ Items marked ~~like this~~ were fixed in the initial implementation pass. ## HIGH – Data Integrity -- [ ] **Fix destructive database migration** – `MainRepository.kt`: `fallbackToDestructiveMigration()` silently wipes all user data on schema changes. Implement proper Room `Migration` objects. The commented-out `migration_1_2` should be completed and uncommented. +- [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. - [ ] **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. - [ ] **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. 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 index 3f4570a..76dfdbe 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/repository/MainRepository.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/repository/MainRepository.kt @@ -12,16 +12,31 @@ 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") + // 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) // commented as crashing, not priority :-( - .fallbackToDestructiveMigration() + .addMigrations(migration_1_2) .build() val drinkRepository = DrinkRepository(context, drinkDatabase) From 8f4e3b094c3f97ecf1751c2e9a504ac9a6f30241 Mon Sep 17 00:00:00 2001 From: JohanPy Date: Tue, 24 Feb 2026 16:39:51 +0100 Subject: [PATCH 07/11] EncryptedSharedPreferences for health data AllowBackup --- TODO.md | 4 ++-- app/build.gradle.kts | 3 +++ app/src/main/AndroidManifest.xml | 2 ++ .../ui/repository/DigestionRepository.kt | 13 ++++++++++++- .../ui/repository/DriveLawRepository.kt | 13 ++++++++++++- .../canidrive/ui/repository/MainRepository.kt | 10 +++++++++- app/src/main/res/xml/backup_rules.xml | 11 +++++++++++ app/src/main/res/xml/data_extraction_rules.xml | 17 +++++++++++++++++ 8 files changed, 68 insertions(+), 5 deletions(-) create mode 100644 app/src/main/res/xml/backup_rules.xml create mode 100644 app/src/main/res/xml/data_extraction_rules.xml diff --git a/TODO.md b/TODO.md index 3ebc4b1..9cac68e 100644 --- a/TODO.md +++ b/TODO.md @@ -40,8 +40,8 @@ Items marked ~~like this~~ were fixed in the initial implementation pass. ## HIGH – Security - [x] ~~**Enable minification/obfuscation for release builds**~~ – `app/build.gradle.kts`: `isMinifyEnabled = true` + `isShrinkResources = true` enabled for release. -- [ ] **Use `EncryptedSharedPreferences` for health data** – `DigestionRepository.kt`: weight, sex, and alcohol tolerance are stored plain. Use `androidx.security:security-crypto` for sensitive health data. -- [ ] **Review `allowBackup`** – `AndroidManifest.xml`: `android:allowBackup="true"`. Consider `android:allowBackup="false"` or a `fullBackupContent` XML to exclude SharedPreferences. +- [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. --- diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2336a22..ead8978 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -88,6 +88,9 @@ dependencies { implementation("androidx.room:room-runtime:$roomVersion") ksp("androidx.room:room-compiler:$roomVersion") implementation("androidx.room:room-ktx:$roomVersion") + + // Security – encrypted SharedPreferences + implementation("androidx.security:security-crypto:1.1.0-alpha06") } tasks.withType { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4d8f825..eedafda 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -4,6 +4,8 @@ + + + + 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 @@ + + + + + + + + + + From 0e25201be607efeab71827f5052f918448a1659f Mon Sep 17 00:00:00 2001 From: JohanPy Date: Wed, 11 Mar 2026 21:35:48 +0100 Subject: [PATCH 08/11] Data Integrity, DriveLawsTest (based on Wiki) , ViewModels, Koin --- .../canidrive/{ui => data}/repository/DigestionRepository.kt | 0 .../canidrive/{ui => data}/repository/DrinkRepository.kt | 0 .../canidrive/{ui => data}/repository/DriveLawRepository.kt | 0 .../com/vaudibert/canidrive/{ui => data}/repository/DriveLaws.kt | 0 .../vaudibert/canidrive/{ui => data}/repository/MainRepository.kt | 0 5 files changed, 0 insertions(+), 0 deletions(-) rename app/src/main/java/com/vaudibert/canidrive/{ui => data}/repository/DigestionRepository.kt (100%) rename app/src/main/java/com/vaudibert/canidrive/{ui => data}/repository/DrinkRepository.kt (100%) rename app/src/main/java/com/vaudibert/canidrive/{ui => data}/repository/DriveLawRepository.kt (100%) rename app/src/main/java/com/vaudibert/canidrive/{ui => data}/repository/DriveLaws.kt (100%) rename app/src/main/java/com/vaudibert/canidrive/{ui => data}/repository/MainRepository.kt (100%) diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DigestionRepository.kt b/app/src/main/java/com/vaudibert/canidrive/data/repository/DigestionRepository.kt similarity index 100% rename from app/src/main/java/com/vaudibert/canidrive/ui/repository/DigestionRepository.kt rename to app/src/main/java/com/vaudibert/canidrive/data/repository/DigestionRepository.kt 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 100% 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 diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DriveLawRepository.kt b/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLawRepository.kt similarity index 100% rename from app/src/main/java/com/vaudibert/canidrive/ui/repository/DriveLawRepository.kt rename to app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLawRepository.kt diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/repository/DriveLaws.kt b/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLaws.kt similarity index 100% rename from app/src/main/java/com/vaudibert/canidrive/ui/repository/DriveLaws.kt rename to app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLaws.kt diff --git a/app/src/main/java/com/vaudibert/canidrive/ui/repository/MainRepository.kt b/app/src/main/java/com/vaudibert/canidrive/data/repository/MainRepository.kt similarity index 100% rename from app/src/main/java/com/vaudibert/canidrive/ui/repository/MainRepository.kt rename to app/src/main/java/com/vaudibert/canidrive/data/repository/MainRepository.kt From 86cc2816c912b356a161e0c5ce4a09721095078b Mon Sep 17 00:00:00 2001 From: JohanPy Date: Wed, 11 Mar 2026 21:35:52 +0100 Subject: [PATCH 09/11] feat: Introduce settings screen with dark mode support and refactor ViewModel architecture and drive law explanation handling. --- app/build.gradle.kts | 7 +++ .../data/repository/DigestionRepository.kt | 2 +- .../data/repository/DrinkRepository.kt | 2 +- .../data/repository/DriveLawRepository.kt | 2 +- .../canidrive/data/repository/DriveLaws.kt | 46 +++++++-------- .../data/repository/MainRepository.kt | 2 +- .../com/vaudibert/canidrive/di/AppModule.kt | 22 ++++++++ .../canidrive/domain/drivelaw/DriveLaw.kt | 2 +- .../com/vaudibert/canidrive/ui/CanIDrive.kt | 20 ++++--- .../vaudibert/canidrive/ui/MainActivity.kt | 19 ++++++- .../ui/adapter/IngestedDrinksAdapter.kt | 8 +-- .../ui/adapter/PresetDrinksAdapter.kt | 2 +- .../canidrive/ui/fragment/AddDrinkFragment.kt | 6 +- .../ui/fragment/AddDrinkViewModel.kt | 13 +++++ .../canidrive/ui/fragment/DrinkerFragment.kt | 24 +++++--- .../canidrive/ui/fragment/DrinkerViewModel.kt | 20 +++++++ .../canidrive/ui/fragment/DriveFragment.kt | 17 +++--- .../canidrive/ui/fragment/DriveViewModel.kt | 16 ++++++ .../ui/fragment/EditPresetFragment.kt | 7 ++- .../ui/fragment/EditPresetViewModel.kt | 11 ++++ .../canidrive/ui/fragment/SettingsFragment.kt | 27 +++++++++ .../canidrive/ui/fragment/SplashFragment.kt | 6 +- app/src/main/res/layout/activity_main.xml | 3 + app/src/main/res/navigation/nav_graph.xml | 4 ++ app/src/main/res/values-fr-rFR/strings.xml | 7 +++ app/src/main/res/values-night/colors.xml | 18 ++++++ app/src/main/res/values/arrays.xml | 13 +++++ app/src/main/res/values/strings.xml | 7 +++ app/src/main/res/values/styles.xml | 4 +- app/src/main/res/xml/preferences.xml | 17 ++++++ .../canidrive/domain/DriveLawServiceTest.kt | 2 +- .../canidrive/ui/repository/DriveLawsTest.kt | 36 ++++++++++++ build_output.txt | 53 ++++++++++++++++++ build_output_dark_mode.txt | 53 ++++++++++++++++++ .../en-US/images/image_1024_500_playstore.png | Bin .../en-US/images/phoneScreenshots/1.png | Bin .../en-US/images/phoneScreenshots/2.png | Bin .../en-US/images/phoneScreenshots/3.png | Bin .../en-US/images/phoneScreenshots/4.png | Bin .../en-US/images/phoneScreenshots/5.png | Bin .../en-US/images/phoneScreenshots/6.png | Bin .../fr-FR/images/image_1024_500_playstore.png | Bin .../fr-FR/images/phoneScreenshots/1.png | Bin .../fr-FR/images/phoneScreenshots/2.png | Bin .../fr-FR/images/phoneScreenshots/3.png | Bin .../fr-FR/images/phoneScreenshots/4.png | Bin .../fr-FR/images/phoneScreenshots/5.png | Bin gradlew | 0 48 files changed, 433 insertions(+), 65 deletions(-) create mode 100644 app/src/main/java/com/vaudibert/canidrive/di/AppModule.kt create mode 100644 app/src/main/java/com/vaudibert/canidrive/ui/fragment/AddDrinkViewModel.kt create mode 100644 app/src/main/java/com/vaudibert/canidrive/ui/fragment/DrinkerViewModel.kt create mode 100644 app/src/main/java/com/vaudibert/canidrive/ui/fragment/DriveViewModel.kt create mode 100644 app/src/main/java/com/vaudibert/canidrive/ui/fragment/EditPresetViewModel.kt create mode 100644 app/src/main/java/com/vaudibert/canidrive/ui/fragment/SettingsFragment.kt create mode 100644 app/src/main/res/values-night/colors.xml create mode 100644 app/src/main/res/values/arrays.xml create mode 100644 app/src/main/res/xml/preferences.xml create mode 100644 app/src/test/java/com/vaudibert/canidrive/ui/repository/DriveLawsTest.kt create mode 100644 build_output.txt create mode 100644 build_output_dark_mode.txt mode change 100644 => 100755 fastlane/metadata/android/en-US/images/image_1024_500_playstore.png mode change 100644 => 100755 fastlane/metadata/android/en-US/images/phoneScreenshots/1.png mode change 100644 => 100755 fastlane/metadata/android/en-US/images/phoneScreenshots/2.png mode change 100644 => 100755 fastlane/metadata/android/en-US/images/phoneScreenshots/3.png mode change 100644 => 100755 fastlane/metadata/android/en-US/images/phoneScreenshots/4.png mode change 100644 => 100755 fastlane/metadata/android/en-US/images/phoneScreenshots/5.png mode change 100644 => 100755 fastlane/metadata/android/en-US/images/phoneScreenshots/6.png mode change 100644 => 100755 fastlane/metadata/android/fr-FR/images/image_1024_500_playstore.png mode change 100644 => 100755 fastlane/metadata/android/fr-FR/images/phoneScreenshots/1.png mode change 100644 => 100755 fastlane/metadata/android/fr-FR/images/phoneScreenshots/2.png mode change 100644 => 100755 fastlane/metadata/android/fr-FR/images/phoneScreenshots/3.png mode change 100644 => 100755 fastlane/metadata/android/fr-FR/images/phoneScreenshots/4.png mode change 100644 => 100755 fastlane/metadata/android/fr-FR/images/phoneScreenshots/5.png mode change 100644 => 100755 gradlew diff --git a/app/build.gradle.kts b/app/build.gradle.kts index ead8978..cd885be 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -89,8 +89,15 @@ dependencies { 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 { 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 index 30941a2..4d404b7 100644 --- a/app/src/main/java/com/vaudibert/canidrive/data/repository/DigestionRepository.kt +++ b/app/src/main/java/com/vaudibert/canidrive/data/repository/DigestionRepository.kt @@ -1,4 +1,4 @@ -package com.vaudibert.canidrive.ui.repository +package com.vaudibert.canidrive.data.repository import android.content.Context import androidx.lifecycle.LiveData diff --git a/app/src/main/java/com/vaudibert/canidrive/data/repository/DrinkRepository.kt b/app/src/main/java/com/vaudibert/canidrive/data/repository/DrinkRepository.kt index c274834..537f052 100644 --- a/app/src/main/java/com/vaudibert/canidrive/data/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 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 index e51bfab..0af47b9 100644 --- a/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLawRepository.kt +++ b/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLawRepository.kt @@ -1,4 +1,4 @@ -package com.vaudibert.canidrive.ui.repository +package com.vaudibert.canidrive.data.repository import android.content.Context import androidx.lifecycle.LiveData 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 index c0c0abb..30c72aa 100644 --- a/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLaws.kt +++ b/app/src/main/java/com/vaudibert/canidrive/data/repository/DriveLaws.kt @@ -1,4 +1,4 @@ -package com.vaudibert.canidrive.ui.repository +package com.vaudibert.canidrive.data.repository import com.vaudibert.canidrive.R import com.vaudibert.canidrive.domain.drivelaw.DriveLaw @@ -24,7 +24,7 @@ object DriveLaws { "AT", 0.5, YoungLimit( 0.1, - R.string.less_two_years_driving + "less_two_years_driving" ), professionalLimit = ProfessionalLimit( 0.2 @@ -46,7 +46,7 @@ object DriveLaws { // Bosnia & Herzegovina DriveLaw( "BA", 0.3, - YoungLimit(explanationId = R.string.twenty_one_young_or_less_three_years_driving), + YoungLimit(explanationName = "twenty_one_young_or_less_three_years_driving"), ProfessionalLimit() ), @@ -56,7 +56,7 @@ object DriveLaws { // Croatia DriveLaw( "HR", 0.5, - YoungLimit(explanationId = R.string.sixteen_to_twenty_four_years_old), + YoungLimit(explanationName = "sixteen_to_twenty_four_years_old"), ProfessionalLimit() ), @@ -80,7 +80,7 @@ object DriveLaws { "FR", 0.5, YoungLimit( 0.2, - R.string.less_two_years_driving + "less_two_years_driving" ), ProfessionalLimit(0.2) ), @@ -91,7 +91,7 @@ object DriveLaws { // Germany DriveLaw( "DE", 0.5, - YoungLimit(explanationId = R.string.twenty_one_young_or_less_two_years_driving), + YoungLimit(explanationName = "twenty_one_young_or_less_two_years_driving"), ProfessionalLimit(0.2) ), @@ -103,7 +103,7 @@ object DriveLaws { "GR", 0.5, YoungLimit( 0.2, - R.string.motor_cycle_or_less_two_years_driving + "motor_cycle_or_less_two_years_driving" ), ProfessionalLimit(0.2) ), @@ -120,7 +120,7 @@ object DriveLaws { "IE", 0.5, YoungLimit( 0.2, - R.string.motor_cycle_or_less_two_years_driving + "motor_cycle_or_less_two_years_driving" ), ProfessionalLimit(0.2) ), @@ -130,7 +130,7 @@ object DriveLaws { "IT", 0.5, YoungLimit( 0.01, - R.string.three_years_driving + "three_years_driving" ), ProfessionalLimit(0.01) ), @@ -140,14 +140,14 @@ object DriveLaws { "LV", 0.5, YoungLimit( 0.2, - R.string.less_two_years_driving + "less_two_years_driving" ) ), // Lithuania DriveLaw( "LT", 0.4, - YoungLimit(explanationId = R.string.motor_cycle_or_less_two_years_driving), + YoungLimit(explanationName = "motor_cycle_or_less_two_years_driving"), ProfessionalLimit() ), @@ -156,7 +156,7 @@ object DriveLaws { "LU", 0.5, YoungLimit( 0.2, - R.string.less_two_years_driving + "less_two_years_driving" ), ProfessionalLimit(0.2) ), @@ -176,7 +176,7 @@ object DriveLaws { "NL", 0.5, YoungLimit( 0.2, - R.string.five_years_driving + "five_years_driving" ) ), @@ -193,7 +193,7 @@ object DriveLaws { "PT", 0.5, YoungLimit( 0.2, - R.string.three_years_driving + "three_years_driving" ) ), @@ -206,7 +206,7 @@ object DriveLaws { // Serbia DriveLaw( "RS", 0.2, - YoungLimit(explanationId = R.string.motor_cycle_or_young_drivers), + YoungLimit(explanationName = "motor_cycle_or_young_drivers"), ProfessionalLimit() ), @@ -216,7 +216,7 @@ object DriveLaws { // Slovenia DriveLaw( "SI", 0.5, - YoungLimit(explanationId = R.string.three_years_driving), + YoungLimit(explanationName = "three_years_driving"), ProfessionalLimit() ), @@ -225,7 +225,7 @@ object DriveLaws { "ES", 0.5, YoungLimit( 0.3, - R.string.less_two_years_driving + "less_two_years_driving" ), ProfessionalLimit(0.2) ), @@ -236,7 +236,7 @@ object DriveLaws { // Switzland DriveLaw( "CH", 0.5, - YoungLimit(explanationId = R.string.three_years_driving) + YoungLimit(explanationName = "three_years_driving") ), // Ukraine @@ -253,17 +253,17 @@ object DriveLaws { // -------------------- Americas -------------------- // Canada - DriveLaw("CA", 0.08), + DriveLaw("CA", 0.8), // USA DriveLaw( "US", 0.8, - YoungLimit(explanationId = R.string.twenty_one_young) + YoungLimit(explanationName = "twenty_one_young") ), // -------------------- Asia -------------------- // China - DriveLaw("CN"), + DriveLaw("CN", 0.2), // Hong-Kong DriveLaw("HK", 0.5), @@ -303,7 +303,7 @@ object DriveLaws { "PH", 0.5, YoungLimit( 0.1, - R.string.motor_cycle_or_young_drivers + "motor_cycle_or_young_drivers" ), ProfessionalLimit(0.1) ), @@ -333,7 +333,7 @@ object DriveLaws { "IL", 0.24, YoungLimit( 0.05, - R.string.new_driver_or_twenty_four + "new_driver_or_twenty_four" ), ProfessionalLimit(0.05) ), 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 index ce18ae8..091699d 100644 --- a/app/src/main/java/com/vaudibert/canidrive/data/repository/MainRepository.kt +++ b/app/src/main/java/com/vaudibert/canidrive/data/repository/MainRepository.kt @@ -1,4 +1,4 @@ -package com.vaudibert.canidrive.ui.repository +package com.vaudibert.canidrive.data.repository import android.content.Context import androidx.room.Room 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..5010e2b --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/di/AppModule.kt @@ -0,0 +1,22 @@ +package com.vaudibert.canidrive.di + +import com.vaudibert.canidrive.data.repository.DigestionRepository +import com.vaudibert.canidrive.data.repository.DrinkRepository +import com.vaudibert.canidrive.data.repository.DriveLawRepository +import com.vaudibert.canidrive.data.repository.MainRepository +import org.koin.android.ext.koin.androidContext +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/drivelaw/DriveLaw.kt b/app/src/main/java/com/vaudibert/canidrive/domain/drivelaw/DriveLaw.kt index 1c41614..1d0f481 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 @@ -10,7 +10,7 @@ data class DriveLaw( fun isCustom() = countryCode.isEmpty() } -data class YoungLimit(val limit:Double = 0.0, val explanationId: Int) +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/ui/CanIDrive.kt b/app/src/main/java/com/vaudibert/canidrive/ui/CanIDrive.kt index 51b611e..d691439 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/CanIDrive.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/CanIDrive.kt @@ -2,21 +2,23 @@ package com.vaudibert.canidrive.ui import android.app.Application import com.vaudibert.canidrive.domain.ITimeService -import com.vaudibert.canidrive.ui.repository.MainRepository +import com.vaudibert.canidrive.data.repository.MainRepository +import com.vaudibert.canidrive.di.appModule +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 - 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 ed4c2a1..05ff607 100644 --- a/app/src/main/java/com/vaudibert/canidrive/ui/MainActivity.kt +++ b/app/src/main/java/com/vaudibert/canidrive/ui/MainActivity.kt @@ -6,15 +6,26 @@ import android.view.MenuItem import androidx.appcompat.app.AppCompatActivity import androidx.navigation.fragment.NavHostFragment import com.google.android.material.appbar.AppBarLayout +import androidx.appcompat.app.AppCompatDelegate +import androidx.navigation.findNavController +import androidx.preference.PreferenceManager import com.vaudibert.canidrive.R import com.vaudibert.canidrive.databinding.ActivityMainBinding class MainActivity : AppCompatActivity() { private lateinit var binding: ActivityMainBinding - val time = CanIDrive.instance.time override fun onCreate(savedInstanceState: Bundle?) { + 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) @@ -36,6 +47,7 @@ class MainActivity : AppCompatActivity() { 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 -> "" } } @@ -53,7 +65,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/adapter/IngestedDrinksAdapter.kt b/app/src/main/java/com/vaudibert/canidrive/ui/adapter/IngestedDrinksAdapter.kt index 7ff2b2f..245909a 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 @@ -15,8 +15,11 @@ import java.text.DateFormat import java.text.DecimalFormat import java.util.* +import com.vaudibert.canidrive.domain.drink.IngestionService + class IngestedDrinksAdapter( - val context: Context + val context: Context, + private val ingestionService: IngestionService ) : BaseAdapter() { private var ingestedDrinkList : List = emptyList() @@ -27,9 +30,6 @@ class IngestedDrinksAdapter( 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 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 9a5f479..f204fd8 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 @@ -12,7 +12,7 @@ import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.Observer import com.vaudibert.canidrive.R import com.vaudibert.canidrive.data.PresetDrinkEntity -import com.vaudibert.canidrive.ui.repository.DrinkRepository +import com.vaudibert.canidrive.data.repository.DrinkRepository import java.text.DecimalFormat class PresetDrinksAdapter( 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 8745f10..ab180cc 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 @@ -20,11 +20,15 @@ import com.vaudibert.canidrive.ui.CanIDrive import com.vaudibert.canidrive.ui.adapter.PresetDrinksAdapter import java.util.Date +import org.koin.androidx.viewmodel.ext.android.viewModel + /** * Fragment to add a drink. */ class AddDrinkFragment : Fragment() { + private val viewModel: AddDrinkViewModel by viewModel() + private var _binding: FragmentAddDrinkBinding? = null private val binding get() = _binding!! @@ -53,7 +57,7 @@ class AddDrinkFragment : Fragment() { seekBarIngestionDelay = view.findViewById(R.id.seekBarIngestionDelay) buttonValidateNewDrink = view.findViewById(R.id.buttonValidateNewDrink) - val drinkRepository = CanIDrive.instance.mainRepository.drinkRepository + val drinkRepository = viewModel.drinkRepository val presetService = drinkRepository.presetService setDelaySeekBar() 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..600968d --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/AddDrinkViewModel.kt @@ -0,0 +1,13 @@ +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 ead145a..5c727b7 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 @@ -25,16 +25,21 @@ import com.vaudibert.canidrive.databinding.FragmentDrinkerBinding 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 com.vaudibert.canidrive.data.repository.DigestionRepository +import com.vaudibert.canidrive.data.repository.MainRepository +import com.vaudibert.canidrive.domain.digestion.Sex import kotlin.math.roundToInt +import org.koin.androidx.viewmodel.ext.android.viewModel + /** * The drinker fragment to enter its details. */ // 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!! @@ -85,11 +90,11 @@ class DrinkerFragment : Fragment() { checkboxYoungDriver = view.findViewById(R.id.checkboxYoungDriver) checkboxProfessionalDriver = view.findViewById(R.id.checkboxProfessionalDriver) - mainRepository = CanIDrive.instance.mainRepository - digestionRepository = mainRepository.digestionRepository + mainRepository = viewModel.mainRepository + digestionRepository = viewModel.digestionRepository - val driveLawRepository = mainRepository.driveLawRepository - driveLawService = driveLawRepository.driveLawService + val driveLawRepository = viewModel.driveLawRepository + driveLawService = viewModel.driveLawService setupSpinnerCountry( driveLawService @@ -119,7 +124,12 @@ class DrinkerFragment : Fragment() { // 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 } 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..6b8d70d --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DrinkerViewModel.kt @@ -0,0 +1,20 @@ +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 9b66f31..5bbef4a 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 @@ -18,10 +18,12 @@ 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 com.vaudibert.canidrive.data.repository.DigestionRepository +import com.vaudibert.canidrive.data.repository.DrinkRepository import java.text.DateFormat +import org.koin.androidx.viewmodel.ext.android.viewModel + /** * The drive fragment that displays the drive status : * - can the user drive ? @@ -30,6 +32,8 @@ import java.text.DateFormat */ class DriveFragment : Fragment() { + private val viewModel: DriveViewModel by viewModel() + private var _binding: FragmentDriveStatusBinding? = null private val binding get() = _binding!! @@ -60,12 +64,11 @@ class DriveFragment : Fragment() { textViewPastDrinks = view.findViewById(R.id.textViewPastDrinks) listViewPastDrinks = view.findViewById(R.id.listViewPastDrinks) - val mainRepository = CanIDrive.instance.mainRepository - drinkerStatusService = mainRepository.drinkerStatusService - drinkRepository = mainRepository.drinkRepository - digestionRepository = mainRepository.digestionRepository + drinkerStatusService = viewModel.mainRepository.drinkerStatusService + drinkRepository = viewModel.drinkRepository + digestionRepository = viewModel.digestionRepository - ingestedDrinksAdapter = IngestedDrinksAdapter(requireContext()) + ingestedDrinksAdapter = IngestedDrinksAdapter(requireContext(), drinkRepository.ingestionService) listViewPastDrinks.adapter = ingestedDrinksAdapter drinkRepository.livePastDrinks.observe(viewLifecycleOwner) { 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..2beec13 --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/DriveViewModel.kt @@ -0,0 +1,16 @@ +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 timeLimitService = drinkRepository.timeLimitService + 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 69f7a04..a54e238 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 @@ -15,8 +15,12 @@ import com.vaudibert.canidrive.domain.drink.IngestedDrink import com.vaudibert.canidrive.ui.CanIDrive import java.text.DecimalFormat +import org.koin.androidx.viewmodel.ext.android.viewModel + class EditPresetFragment : Fragment() { + private val viewModel: EditPresetViewModel by viewModel() + private var _binding: FragmentAddPresetBinding? = null private val binding get() = _binding!! @@ -44,8 +48,7 @@ class EditPresetFragment : Fragment() { numberPickerVolume = view.findViewById(R.id.numberPickerVolume) numberPickerDegree = view.findViewById(R.id.numberPickerDegree) - val drinkRepository = CanIDrive.instance.mainRepository.drinkRepository - val presetService = drinkRepository.presetService + val presetService = viewModel.drinkRepository.presetService val selectedPreset = presetService.selectedPreset if (selectedPreset != 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..df4cd8d --- /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..6f1219f --- /dev/null +++ b/app/src/main/java/com/vaudibert/canidrive/ui/fragment/SettingsFragment.kt @@ -0,0 +1,27 @@ +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 63052e7..b80ae56 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 @@ -15,12 +15,16 @@ import com.vaudibert.canidrive.BuildConfig import com.vaudibert.canidrive.R import com.vaudibert.canidrive.databinding.FragmentSplashBinding import com.vaudibert.canidrive.ui.CanIDrive +import com.vaudibert.canidrive.data.repository.MainRepository +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!! @@ -80,7 +84,7 @@ class SplashFragment : Fragment() { private fun navigateToNext() { if (!isAdded) return - val init = CanIDrive.instance.mainRepository.init + val init = mainRepository.init val action = if (init) SplashFragmentDirections.actionSplashFragmentToDriveFragment() diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index 315d4f0..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" 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-fr-rFR/strings.xml b/app/src/main/res/values-fr-rFR/strings.xml index 602f4a8..a6573d1 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 ? 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 d642825..5ec9bcd 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 diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 4a1bd8e..e8f25d6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -1,7 +1,7 @@ - - + +