diff --git a/app/build.gradle b/app/build.gradle index 2a8c09a..81bde06 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -9,8 +9,8 @@ android { applicationId "fr.corenting.convertisseureurofranc" minSdkVersion 17 targetSdkVersion 30 - versionCode 16 - versionName "2.7" + versionCode 17 + versionName "2.8" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } @@ -50,6 +50,8 @@ dependencies { implementation 'androidx.core:core-ktx:1.3.2' implementation 'androidx.preference:preference-ktx:1.1.1' implementation 'com.google.android.material:material:1.3.0' + implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.3.1" + // Tests testImplementation 'androidx.test:core-ktx:1.3.0' diff --git a/app/src/main/java/ConverterViewModel.kt b/app/src/main/java/ConverterViewModel.kt new file mode 100644 index 0000000..6f3d72c --- /dev/null +++ b/app/src/main/java/ConverterViewModel.kt @@ -0,0 +1,39 @@ +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider +import fr.corenting.convertisseureurofranc.converters.ConverterAbstract +import java.lang.IllegalStateException + +class ConverterViewModelFactory(private val initConverter: ConverterAbstract) : + ViewModelProvider.NewInstanceFactory() { + override fun create(modelClass: Class): T = + ConverterViewModel(initConverter) as T +} + +class ConverterViewModel(initConverter: ConverterAbstract) : ViewModel() { + private val converter = MutableLiveData().apply { + postValue(initConverter) + } + + fun getConverter(): LiveData { + return converter + } + + fun setConverter(newConverter: ConverterAbstract) { + converter.postValue(newConverter) + } + + fun doConversion(yearOfOrigin: Int, yearOfResult: Int, amount: Float): Float { + val currentConverter = converter.value + if (currentConverter != null) { + return currentConverter.convertFunction( + yearOfOrigin, + yearOfResult, + amount + ) + } else { + throw IllegalStateException() + } + } +} diff --git a/app/src/main/java/fr/corenting/convertisseureurofranc/AutoCompleteAdapter.kt b/app/src/main/java/fr/corenting/convertisseureurofranc/AutoCompleteAdapter.kt index a3541b8..c232b08 100644 --- a/app/src/main/java/fr/corenting/convertisseureurofranc/AutoCompleteAdapter.kt +++ b/app/src/main/java/fr/corenting/convertisseureurofranc/AutoCompleteAdapter.kt @@ -5,7 +5,7 @@ import android.widget.ArrayAdapter import android.widget.Filter -class AutocompleteAdapter(context: Context, resource: Int, objects: List) : +class AutoCompleteAdapter(context: Context, resource: Int, objects: List) : ArrayAdapter(context, resource, objects) { override fun getFilter(): Filter { diff --git a/app/src/main/java/fr/corenting/convertisseureurofranc/ConverterActivity.kt b/app/src/main/java/fr/corenting/convertisseureurofranc/ConverterActivity.kt index 780c103..9155d93 100644 --- a/app/src/main/java/fr/corenting/convertisseureurofranc/ConverterActivity.kt +++ b/app/src/main/java/fr/corenting/convertisseureurofranc/ConverterActivity.kt @@ -1,16 +1,16 @@ package fr.corenting.convertisseureurofranc +import ConverterViewModel +import ConverterViewModelFactory import android.os.Bundle import android.view.KeyEvent import android.view.MenuItem import android.view.View -import android.widget.AdapterView -import android.widget.AutoCompleteTextView -import android.widget.EditText import android.widget.Toast +import androidx.activity.viewModels import androidx.appcompat.app.AppCompatActivity import androidx.appcompat.app.AppCompatDelegate -import androidx.core.os.ConfigurationCompat +import androidx.core.widget.doOnTextChanged import fr.corenting.convertisseureurofranc.converters.ConverterAbstract import fr.corenting.convertisseureurofranc.converters.FranceConverter import fr.corenting.convertisseureurofranc.converters.USAConverter @@ -20,18 +20,8 @@ import fr.corenting.convertisseureurofranc.utils.Utils import java.util.* class ConverterActivity : AppCompatActivity() { - - companion object { - private const val converterBundleKey = "converter" - } - - private lateinit var converter: ConverterAbstract private lateinit var binding: ActivityConverterBinding - - // Save position for config change - private var currentConverter: Int = 0 - override fun onCreate(savedInstanceState: Bundle?) { // Theme and creation AppCompatDelegate.setDefaultNightMode(ThemeUtils.getThemeToUse(this)) @@ -42,50 +32,101 @@ class ConverterActivity : AppCompatActivity() { val view = binding.root setContentView(view) binding.topAppBar.setOnMenuItemClickListener(this::onMenuItemClickListener) + binding.topAppBar.menu.findItem(ThemeUtils.getMenuIdForCurrentTheme(this)).isChecked = true - //Set currency spinner content + // Common values val currenciesList = listOf( getString(R.string.usa_currencies), getString(R.string.france_currencies) ) - setAutoCompleteAdapter(binding.currencyAutoComplete, currenciesList) - binding.currencyAutoComplete.onItemClickListener = - AdapterView.OnItemClickListener { _, _, position, _ -> - onCurrencyItemClickListener(position) - } - // Set default currency - if (savedInstanceState == null) { - val currentLocale: Locale = - ConfigurationCompat.getLocales(resources.configuration).get(0) - if (currentLocale == Locale.FRANCE) { - binding.currencyAutoComplete.setText(getString(R.string.france_currencies), false) - onCurrencyItemClickListener(1) - } else { - binding.currencyAutoComplete.setText(getString(R.string.usa_currencies), false) - onCurrencyItemClickListener(0) + // Sum input + binding.sumToConvertText.doOnTextChanged { text, _, _, _ -> + try { + if (text.toString().toFloat() > 0) { + binding.sumToConvertInput.error = null + } + } catch (err: NumberFormatException) { } - } else { - onCurrencyItemClickListener(savedInstanceState.getInt(converterBundleKey)) } - binding.topAppBar.menu.findItem(ThemeUtils.getMenuIdForCurrentTheme(this)).isChecked = true - initButtons() + // Viewmodel and converter setup + val converterViewModel: ConverterViewModel by viewModels { + ConverterViewModelFactory(Utils.getConverterForCurrentLocale(applicationContext)) + } + converterViewModel.getConverter().observe(this, { + if (it != null) { + onConverterChange(it) + + // Update selected in list (used at app opening) + var selectedCurrencyPosition = 0 + if (it is FranceConverter) { + selectedCurrencyPosition = 1 + } + binding.currencyAutoComplete.setText(currenciesList[selectedCurrencyPosition]) + } + }) + + //Set currency spinner content + val adapter = AutoCompleteAdapter(this, R.layout.list_item, currenciesList) + binding.currencyAutoComplete.setAdapter(adapter) + binding.currencyAutoComplete.setOnItemClickListener { _, _, position, _ -> + when (position) { + 0 -> converterViewModel.setConverter(USAConverter(applicationContext)) + else -> converterViewModel.setConverter(FranceConverter(applicationContext)) + } + } + initButtons(converterViewModel) } - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) + private fun onConverterChange(newConverter: ConverterAbstract) { + val latestYear = newConverter.latestYear + val firstYear = newConverter.firstYear - outState.putInt(converterBundleKey, currentConverter) - } + binding.yearOfOriginAutoComplete.setText(latestYear.toString()) + binding.yearOfOriginInput.hint = getString(R.string.yearOfOrigin, firstYear, latestYear) + binding.yearOfOriginAutoComplete.doOnTextChanged { text, _, _, _ -> + YearInputTextHandler.doOnTextChanged( + applicationContext, + newConverter, + binding.yearOfOriginAutoComplete, + binding.yearOfOriginInput + ) + try { + val year = text.toString().toInt() + binding.sumToConvertInput.hint = getString( + R.string.sumToConvert, newConverter.getCurrencyFromYear(year) + ) + } catch (exc: NumberFormatException) { + } + } + binding.sumToConvertInput.hint = getString( + R.string.sumToConvert, + newConverter.getCurrencyFromYear(latestYear) + ) - private fun onCurrencyItemClickListener(position: Int) { - converter = when (position) { - 0 -> USAConverter(applicationContext) - else -> FranceConverter(applicationContext) + binding.yearOfResultAutoComplete.setText(latestYear.toString()) + binding.yearOfResultInput.hint = getString(R.string.yearOfOrigin, firstYear, latestYear) + binding.yearOfResultAutoComplete.doOnTextChanged { text, _, _, _ -> + YearInputTextHandler.doOnTextChanged( + applicationContext, + newConverter, + binding.yearOfResultAutoComplete, + binding.yearOfResultInput + ) + try { + val year = text.toString().toInt() + binding.resultInput.hint = getString( + R.string.resultText, + newConverter.getCurrencyFromYear(year) + ) + } catch (exc: NumberFormatException) { + } } - currentConverter = position - initInputFields() + binding.resultInput.hint = getString( + R.string.resultText, + newConverter.getCurrencyFromYear(latestYear) + ) } private fun onMenuItemClickListener(menuItem: MenuItem): Boolean { @@ -104,72 +145,7 @@ class ConverterActivity : AppCompatActivity() { } } - private fun initInputFields() { - // Set years inputs - val yearFields = listOf( - Triple( - binding.yearOfOriginAutoComplete, - binding.yearOfOriginInput, - R.string.yearOfOrigin - ), - Triple( - binding.yearOfResultAutoComplete, - binding.yearOfResultInput, - R.string.yearOfResult - ) - ) - - val yearsList = (converter.latestYear downTo converter.firstYear).toList().map { - it.toString() - } - for ((yearInputField, yearInputLayout, hintStringId) in yearFields) { - yearInputField.setText(yearsList.first().toString()) - yearInputLayout.hint = - getString(hintStringId, converter.firstYear, converter.latestYear) - yearInputField.onFocusChangeListener = View.OnFocusChangeListener { v, _ -> - val textContent = (v as EditText).text.toString() - val errorString = getString( - R.string.invalid_year_error - ) - if (textContent.isBlank()) { - yearInputLayout.error = errorString - } - - try { - val year = textContent.toInt() - when { - year < converter.firstYear || year > converter.latestYear -> { - yearInputLayout.error = errorString - } - else -> { - yearInputLayout.error = null - } - } - } catch (err: NumberFormatException) { - yearInputLayout.error = errorString - } - - // TODO: refresh corresponding currency input to handle currency change - } - } - - refreshCurrencyInputs(converter.latestYear, converter.latestYear - 2) - } - - private fun refreshCurrencyInputs(fromYear: Int, toYear: Int) { - val currencyFields = listOf( - Triple(binding.sumToConvertInput, R.string.sumToConvert, fromYear), - Triple(binding.resultInput, R.string.resultText, toYear) - ) - for ((sumInput, hintStringId, year) in currencyFields) { - sumInput.hint = getString( - hintStringId, - converter.getCurrencyFromYear(year) - ) - } - } - - private fun initButtons() { + private fun initButtons(converterViewModel: ConverterViewModel) { //Convert when the button is clicked binding.convertButton.setImeActionLabel( getString(R.string.convertButton), @@ -187,38 +163,34 @@ class ConverterActivity : AppCompatActivity() { //Setup listeners binding.convertButton.setOnClickListener { v -> - try { - binding.sumToConvertInput.error = null - Utils.hideSoftKeyboard(v) - val yearOfOrigin = - Integer.parseInt(binding.yearOfOriginAutoComplete.text.toString()) - val yearOfResult = - Integer.parseInt(binding.yearOfResultAutoComplete.text.toString()) - val amount = java.lang.Float.parseFloat(binding.sumToConvertText.text.toString()) - binding.resultText.setText( - Utils.formatNumber( - this, - converter.convertFunction(yearOfOrigin, yearOfResult, amount) - ) - ) - } catch (e: Exception) { - if (binding.sumToConvertText.text == null || binding.sumToConvertText.text.toString() == "") { - binding.sumToConvertInput.error = getString(R.string.no_amount_entered) - } - val errorToast = Toast.makeText( - this, getString(R.string.errorToast), - Toast.LENGTH_SHORT - ) - errorToast.show() - } + binding.convertButton.requestFocus() + binding.sumToConvertInput.clearFocus() + binding.yearOfOriginAutoComplete.clearFocus() + binding.yearOfResultAutoComplete.clearFocus() + Utils.hideSoftKeyboard(v) + doConversion(converterViewModel) } } - private fun setAutoCompleteAdapter( - autoCompleteTextView: AutoCompleteTextView, - items: List - ) { - val adapter = AutocompleteAdapter(this, R.layout.list_item, items) - autoCompleteTextView.setAdapter(adapter) + private fun doConversion(converterViewModel: ConverterViewModel) { + try { + binding.sumToConvertInput.error = null + val yearOfOrigin = + Integer.parseInt(binding.yearOfOriginAutoComplete.text.toString()) + val yearOfResult = + Integer.parseInt(binding.yearOfResultAutoComplete.text.toString()) + val amount = java.lang.Float.parseFloat(binding.sumToConvertText.text.toString()) + val convertedAmount = + converterViewModel.doConversion(yearOfOrigin, yearOfResult, amount) + binding.resultText.setText(Utils.formatNumber(this, convertedAmount)) + } catch (e: Exception) { + if (binding.sumToConvertText.text == null || binding.sumToConvertText.text.toString() == "") { + binding.sumToConvertInput.error = getString(R.string.no_amount_entered) + } + Toast.makeText( + this, getString(R.string.errorToast), + Toast.LENGTH_SHORT + ).show() + } } } diff --git a/app/src/main/java/fr/corenting/convertisseureurofranc/YearInputTextHandler.kt b/app/src/main/java/fr/corenting/convertisseureurofranc/YearInputTextHandler.kt new file mode 100644 index 0000000..375b62f --- /dev/null +++ b/app/src/main/java/fr/corenting/convertisseureurofranc/YearInputTextHandler.kt @@ -0,0 +1,37 @@ +package fr.corenting.convertisseureurofranc + +import android.content.Context +import android.widget.EditText +import com.google.android.material.textfield.TextInputLayout +import fr.corenting.convertisseureurofranc.converters.ConverterAbstract + +object YearInputTextHandler { + fun doOnTextChanged( + context: Context, + converter: ConverterAbstract, + yearInputField: EditText, + yearInputLayout: TextInputLayout + ) { + val textContent = yearInputField.text.toString() + val errorString = context.getString( + R.string.invalid_year_error + ) + if (textContent.isBlank()) { + yearInputLayout.error = errorString + } + + try { + val year = textContent.toInt() + when { + year < converter.firstYear || year > converter.latestYear -> { + yearInputLayout.error = errorString + } + else -> { + yearInputLayout.error = null + } + } + } catch (err: NumberFormatException) { + yearInputLayout.error = errorString + } + } +} \ No newline at end of file diff --git a/app/src/main/java/fr/corenting/convertisseureurofranc/utils/ThemeUtils.kt b/app/src/main/java/fr/corenting/convertisseureurofranc/utils/ThemeUtils.kt index e694636..82359df 100644 --- a/app/src/main/java/fr/corenting/convertisseureurofranc/utils/ThemeUtils.kt +++ b/app/src/main/java/fr/corenting/convertisseureurofranc/utils/ThemeUtils.kt @@ -7,7 +7,7 @@ import fr.corenting.convertisseureurofranc.R object ThemeUtils { - const val THEME_PREFERENCE_KEY = "theme" + private const val THEME_PREFERENCE_KEY = "theme" enum class ThemePreference(val preferenceValue: Int, val menuId: Int) { THEME_SYSTEM(1, R.id.action_theme_system), @@ -16,9 +16,9 @@ object ThemeUtils { companion object { fun fromPreferenceValue(value: Int) = - ThemePreference.values().first { it.preferenceValue == value } + values().first { it.preferenceValue == value } - fun fromMenuId(value: Int) = ThemePreference.values().first { it.menuId == value } + fun fromMenuId(value: Int) = values().first { it.menuId == value } } } diff --git a/app/src/main/java/fr/corenting/convertisseureurofranc/utils/Utils.kt b/app/src/main/java/fr/corenting/convertisseureurofranc/utils/Utils.kt index 7db1a74..401428c 100644 --- a/app/src/main/java/fr/corenting/convertisseureurofranc/utils/Utils.kt +++ b/app/src/main/java/fr/corenting/convertisseureurofranc/utils/Utils.kt @@ -10,8 +10,12 @@ import android.view.View import android.view.inputmethod.InputMethodManager import android.widget.TextView import androidx.appcompat.app.AlertDialog +import androidx.core.os.ConfigurationCompat import fr.corenting.convertisseureurofranc.BuildConfig import fr.corenting.convertisseureurofranc.R +import fr.corenting.convertisseureurofranc.converters.ConverterAbstract +import fr.corenting.convertisseureurofranc.converters.FranceConverter +import fr.corenting.convertisseureurofranc.converters.USAConverter import java.text.DecimalFormat import java.text.NumberFormat import java.util.* @@ -19,6 +23,17 @@ import java.util.* object Utils { + fun getConverterForCurrentLocale(context: Context): ConverterAbstract { + return when (ConfigurationCompat.getLocales(context.resources.configuration).get(0)) { + Locale.FRANCE -> { + FranceConverter(context) + } + else -> { + USAConverter(context) + } + } + } + fun hideSoftKeyboard(v: View): Boolean { val imm = v.context.getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager return imm.hideSoftInputFromWindow(v.applicationWindowToken, 0) @@ -44,7 +59,8 @@ object Utils { } fun formatNumber(c: Context, number: Float): String { - val formatter = NumberFormat.getInstance(getCurrentLocale(c)) as DecimalFormat + val currentLocale = ConfigurationCompat.getLocales(c.resources.configuration).get(0) + val formatter = NumberFormat.getInstance(currentLocale) as DecimalFormat return formatter.format(number) } @@ -57,12 +73,4 @@ object Utils { } } - private fun getCurrentLocale(ctx: Context): Locale { - return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { - ctx.resources.configuration.locales.get(0) - } else { - @Suppress("DEPRECATION") - ctx.resources.configuration.locale - } - } } diff --git a/app/src/main/res/layout/activity_converter.xml b/app/src/main/res/layout/activity_converter.xml index d0436bc..bb1fd0c 100644 --- a/app/src/main/res/layout/activity_converter.xml +++ b/app/src/main/res/layout/activity_converter.xml @@ -133,6 +133,10 @@ android:id="@+id/resultText" android:layout_width="match_parent" android:layout_height="wrap_content" + android:clickable="false" + android:cursorVisible="false" + android:focusable="false" + android:focusableInTouchMode="false" android:inputType="none" /> diff --git a/build.gradle b/build.gradle index f146d47..8134944 100644 --- a/build.gradle +++ b/build.gradle @@ -3,11 +3,11 @@ buildscript { ext.kotlin_version = '1.5.0' repositories { - jcenter() google() + mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:4.1.3' + classpath 'com.android.tools.build:gradle:4.2.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong @@ -17,7 +17,7 @@ buildscript { allprojects { repositories { - jcenter() google() + mavenCentral() } } diff --git a/fastlane/metadata/android/en-US/changelogs/17.txt b/fastlane/metadata/android/en-US/changelogs/17.txt new file mode 100644 index 0000000..0377a22 --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/17.txt @@ -0,0 +1 @@ +Internal changes \ No newline at end of file diff --git a/fastlane/metadata/android/fr-FR/changelogs/17.txt b/fastlane/metadata/android/fr-FR/changelogs/17.txt new file mode 100644 index 0000000..09d344d --- /dev/null +++ b/fastlane/metadata/android/fr-FR/changelogs/17.txt @@ -0,0 +1 @@ +Changements internes \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index e7f1210..0f3bfb0 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.5-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-6.7.1-all.zip