diff --git a/app/build.gradle b/app/build.gradle index 4540e1c7..3e88ff26 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -6,7 +6,9 @@ apply plugin: 'realm-android' // Crashlytics apply plugin: 'com.google.gms.google-services' apply plugin: 'com.google.firebase.crashlytics' -////////////// +// Serialization +apply plugin: "kotlinx-serialization" + repositories { maven { url 'https://jitpack.io' } // required for MPAndroidChart @@ -99,7 +101,8 @@ dependencies { implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.3.6' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.6" implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version" - implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") // JVM dependency +// implementation("org.jetbrains.kotlinx:kotlinx-serialization-runtime:0.20.0") // JVM dependency + implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1" // Android implementation 'androidx.appcompat:appcompat:1.1.0' diff --git a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt index a57afe66..e481dd40 100644 --- a/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt +++ b/app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt @@ -39,7 +39,9 @@ class PokerAnalyticsApplication : Application() { override fun onCreate() { super.onCreate() - FirebaseApp.initializeApp(this) + if (!BuildConfig.DEBUG) { + FirebaseApp.initializeApp(this) + } UserDefaults.init(this) diff --git a/app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt b/app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt new file mode 100644 index 00000000..217aa88d --- /dev/null +++ b/app/src/main/java/net/pokeranalytics/android/api/CurrencyConverterApi.kt @@ -0,0 +1,61 @@ +package net.pokeranalytics.android.api + +import android.content.Context +import com.android.volley.VolleyError +import com.android.volley.toolbox.StringRequest +import com.android.volley.toolbox.Volley +import kotlinx.serialization.Serializable +import kotlinx.serialization.decodeFromString +import kotlinx.serialization.json.Json +import timber.log.Timber + +@Serializable +data class RateResponse(var info: RateInfo) +@Serializable +data class RateInfo(var rate: Double) + +class CurrencyConverterApi { + + companion object { + + val json = Json { ignoreUnknownKeys = true } + + fun currencyRate(fromCurrency: String, toCurrency: String, context: Context, callback: (Double?, VolleyError?) -> (Unit)) { + + val queue = Volley.newRequestQueue(context) +// val url = "https://free.currconv.com/api/v7/convert?q=${pair}&compact=ultra&apiKey=9b56e742a75392c8aeb7" + + val url = "https://api.apilayer.com/exchangerates_data/convert?to=$toCurrency&from=$fromCurrency&amount=1" + + // https://free.currconv.com/api/v7/convert?q=GBP_USD&compact=ultra&apiKey=5ba8d38995282fe8b1c8 + // { "USD_PHP": 44.1105, "PHP_USD": 0.0227 } + + Timber.d("Api call = $url") + + val stringRequest = object : StringRequest( + Method.GET, url, + { response -> + + val o = json.decodeFromString(response) + Timber.d("rate = ${o.info.rate}") + callback(o.info.rate, null) + }, + { + Timber.d("Api call failed: ${it.message}") + callback(null, it) + }) { + + override fun getHeaders(): MutableMap { + val headers = HashMap() + headers["apikey"] = "XnfeyID3PMKd3k4zTPW0XmZAbcZlZgqH" + return headers + } + + } + queue.add(stringRequest) + + } + + } + +} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/api/FreeConverterApi.kt b/app/src/main/java/net/pokeranalytics/android/api/FreeConverterApi.kt deleted file mode 100644 index 77a3f364..00000000 --- a/app/src/main/java/net/pokeranalytics/android/api/FreeConverterApi.kt +++ /dev/null @@ -1,46 +0,0 @@ -package net.pokeranalytics.android.api - -import android.content.Context -import com.android.volley.Request -import com.android.volley.toolbox.StringRequest -import com.android.volley.toolbox.Volley -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonConfiguration -import timber.log.Timber - -class FreeConverterApi { - - companion object { - - fun currencyRate(pair: String, context: Context, callback: (Double) -> (Unit)) { - - val queue = Volley.newRequestQueue(context) - val url = "https://free.currconv.com/api/v7/convert?q=${pair}&compact=ultra&apiKey=9b56e742a75392c8aeb7" - - // https://free.currconv.com/api/v7/convert?q=GBP_USD&compact=ultra&apiKey=5ba8d38995282fe8b1c8 - // { "USD_PHP": 44.1105, "PHP_USD": 0.0227 } - - val stringRequest = StringRequest( - Request.Method.GET, url, - { response -> - - val json = Json(JsonConfiguration.Stable) - val f = json.parseJson(response) - f.jsonObject[pair]?.primitive?.double?.let { rate -> - callback(rate) - } ?: run { - Timber.d("no rate: $response") - } - - }, - { - Timber.d("Api call failed: ${it.message}") - }) - - queue.add(stringRequest) - - } - - } - -} \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt b/app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt index 307c80ab..7c7c89bc 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/filter/Query.kt @@ -119,6 +119,7 @@ class Query { is QueryCondition.QueryDataCondition<*> -> { c.objectId?.let { return it } } + else -> {} } } return null diff --git a/app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt b/app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt index ec7318eb..e1d4b1b8 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/filter/QueryCondition.kt @@ -848,6 +848,7 @@ sealed class QueryCondition : RowRepresentable { } return realmQuery } + else -> {} } if (this is CustomFieldRelated) { diff --git a/app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt b/app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt index 89db8b65..fc154bae 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/migrations/Patcher.kt @@ -57,6 +57,10 @@ class Patcher { patchZeroTable() } + Preferences.executeOnce(Preferences.Keys.PATCH_RATED_AMOUNT, context) { + patchRatedAmounts() + } + patchPerformances(application) } @@ -215,5 +219,16 @@ class Patcher { realm.close() } + private fun patchRatedAmounts() { + val realm = Realm.getDefaultInstance() + val transactions = realm.where().findAll() + realm.executeTransaction { + transactions.forEach { t -> + t.computeRatedAmount() + } + } + realm.close() + } + } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt b/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt index 91d61f5e..93f7d03c 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt @@ -326,6 +326,12 @@ class PokerAnalyticsMigration : RealmMigration { ucs.addField("transactionTypeIds", String::class.java).setRequired("transactionTypeIds", true) } ?: throw PAIllegalStateException("UserConfig schema not found") + schema.get("Performance")?.let { ps -> + if (!ps.isPrimaryKey("id")) { + ps.addPrimaryKey("id") + } + } + currentVersion++ } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt index 3568edf1..b299ef32 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/FilterCondition.kt @@ -29,6 +29,7 @@ open class FilterCondition() : RealmObject() { is QueryCondition.ListOfDouble -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfDouble).listOfValues }) is QueryCondition.ListOfInt -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfInt).listOfValues }) is QueryCondition.ListOfString -> this.setValues(filterElementRows.flatMap { (it as QueryCondition.ListOfString).listOfValues }) + else -> {} } } diff --git a/app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt b/app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt index 094dca1d..05e5e5ac 100644 --- a/app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt +++ b/app/src/main/java/net/pokeranalytics/android/model/realm/Transaction.kt @@ -75,8 +75,7 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab override var amount: Double = 0.0 set(value) { field = value - val rate = this.bankroll?.currency?.rate ?: 1.0 - this.ratedAmount = rate * value + computeRatedAmount() } // The amount of the transaction @@ -114,6 +113,11 @@ open class Transaction : RealmObject(), RowRepresentable, RowUpdatable, Manageab RATED_AMOUNT("ratedAmount") } + fun computeRatedAmount() { + val rate = this.bankroll?.currency?.rate ?: 1.0 + this.ratedAmount = rate * this.amount + } + val displayAmount: Double get() { // for transfers we want to show a positive value (in the feed for instance) return if (this.destination == null) { this.amount } else { abs(this.amount) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt b/app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt index e406e7dc..61dc4b51 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt @@ -84,7 +84,7 @@ class ImportActivity : BaseActivity() { when (requestCode) { RequestCode.IMPORT.value -> { if (resultCode == ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) { - showAlertDialog(context = this, message = R.string.unknown_import_format_popup_message, positiveAction = { + showAlertDialog(context = this, messageResId = R.string.unknown_import_format_popup_message, positiveAction = { finish() }) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt b/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt index 478bb7e6..ca0b2b19 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt @@ -3,20 +3,17 @@ package net.pokeranalytics.android.ui.extensions import android.app.Activity import android.content.ActivityNotFoundException import android.content.Context -import android.content.DialogInterface import android.content.Intent import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Canvas import android.graphics.Color import android.net.Uri +import android.text.SpannableStringBuilder import android.util.TypedValue import android.view.View import android.view.inputmethod.InputMethodManager -import android.widget.ImageView -import android.widget.LinearLayout -import android.widget.TextView -import android.widget.Toast +import android.widget.* import androidx.appcompat.app.AlertDialog import androidx.appcompat.widget.SearchView import androidx.core.content.ContextCompat @@ -37,212 +34,245 @@ import java.io.File // Sizes val Int.dp: Int - get() = (this / Resources.getSystem().displayMetrics.density).toInt() + get() = (this / Resources.getSystem().displayMetrics.density).toInt() val Int.px: Int - get() = (this * Resources.getSystem().displayMetrics.density).toInt() + get() = (this * Resources.getSystem().displayMetrics.density).toInt() val Float.dp: Float - get() = (this / Resources.getSystem().displayMetrics.density) + get() = (this / Resources.getSystem().displayMetrics.density) val Float.px: Float - get() = (this * Resources.getSystem().displayMetrics.density) + get() = (this * Resources.getSystem().displayMetrics.density) // Toast fun Activity.toast(message: String) { - Toast.makeText(this, message, Toast.LENGTH_SHORT).show() + Toast.makeText(this, message, Toast.LENGTH_SHORT).show() } fun Fragment.toast(message: String) { - Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() + Toast.makeText(requireContext(), message, Toast.LENGTH_SHORT).show() } // Open Play Store for rating fun Activity.openPlayStorePage() { - val uri = Uri.parse("market://details?id=$packageName") - val goToMarket = Intent(Intent.ACTION_VIEW, uri) - goToMarket.addFlags( - Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT or - Intent.FLAG_ACTIVITY_MULTIPLE_TASK - ) - try { - startActivity(goToMarket) - } catch (e: ActivityNotFoundException) { - startActivity( - Intent( - Intent.ACTION_VIEW, Uri.parse( - "http://play.google.com/store/apps/details?id=$packageName" - ) - ) - ) - } + val uri = Uri.parse("market://details?id=$packageName") + val goToMarket = Intent(Intent.ACTION_VIEW, uri) + goToMarket.addFlags( + Intent.FLAG_ACTIVITY_NO_HISTORY or Intent.FLAG_ACTIVITY_NEW_DOCUMENT or + Intent.FLAG_ACTIVITY_MULTIPLE_TASK + ) + try { + startActivity(goToMarket) + } catch (e: ActivityNotFoundException) { + startActivity( + Intent( + Intent.ACTION_VIEW, Uri.parse( + "http://play.google.com/store/apps/details?id=$packageName" + ) + ) + ) + } } // Open email for "Contact us" fun BaseActivity.openContactMail(subjectStringRes: Int, filePath: String? = null) { - val info = - "v${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}) - ${AppGuard.isProUser}, Android ${android.os.Build.VERSION.SDK_INT}, ${DeviceUtils.getDeviceName()}" - - val emailIntent = Intent(Intent.ACTION_SEND) - - filePath?.let { - val databaseFile = File(it) - val contentUri = FileProvider.getUriForFile(this, "net.pokeranalytics.android.fileprovider", databaseFile) - if (contentUri != null) { - emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - emailIntent.setDataAndType(contentUri, contentResolver.getType(contentUri)) - emailIntent.putExtra(Intent.EXTRA_STREAM, contentUri) - } - } - - emailIntent.type = "message/rfc822" - emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(subjectStringRes)) - emailIntent.putExtra(Intent.EXTRA_TEXT, "\n\n$info") - emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(URL.SUPPORT_EMAIL.value)) - - startActivity(Intent.createChooser(emailIntent, getString(R.string.contact))) + val info = + "v${BuildConfig.VERSION_NAME}(${BuildConfig.VERSION_CODE}) - ${AppGuard.isProUser}, Android ${android.os.Build.VERSION.SDK_INT}, ${DeviceUtils.getDeviceName()}" + + val emailIntent = Intent(Intent.ACTION_SEND) + + filePath?.let { + val databaseFile = File(it) + val contentUri = FileProvider.getUriForFile( + this, + "net.pokeranalytics.android.fileprovider", + databaseFile + ) + if (contentUri != null) { + emailIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + emailIntent.setDataAndType(contentUri, contentResolver.getType(contentUri)) + emailIntent.putExtra(Intent.EXTRA_STREAM, contentUri) + } + } + + emailIntent.type = "message/rfc822" + emailIntent.putExtra(Intent.EXTRA_SUBJECT, getString(subjectStringRes)) + emailIntent.putExtra(Intent.EXTRA_TEXT, "\n\n$info") + emailIntent.putExtra(Intent.EXTRA_EMAIL, arrayOf(URL.SUPPORT_EMAIL.value)) + + startActivity(Intent.createChooser(emailIntent, getString(R.string.contact))) } // Open custom tab fun Context.openUrl(url: String) { - val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) - ContextCompat.startActivity(this, browserIntent, null) + val browserIntent = Intent(Intent.ACTION_VIEW, Uri.parse(url)) + ContextCompat.startActivity(this, browserIntent, null) } // Open custom tab -fun Context.areYouSure(title: Int? = null, message: Int? = null, positiveTitle: Int? = null, proceed: () -> Unit) { - - val builder: android.app.AlertDialog.Builder = android.app.AlertDialog.Builder(this) - - val messageResource = message ?: R.string.are_you_sure_you_want_to_do_this - builder.setMessage(messageResource) +fun Context.areYouSure( + title: Int? = null, + message: Int? = null, + positiveTitle: Int? = null, + proceed: () -> Unit +) { - title?.let { builder.setTitle(it) } + val builder: android.app.AlertDialog.Builder = android.app.AlertDialog.Builder(this) - val positiveButtonTitle = positiveTitle ?: R.string.yes - builder.setPositiveButton(positiveButtonTitle) { _, _ -> - proceed() - } - builder.setNegativeButton(R.string.cancel) { _, _ -> - // nothing - } + val messageResource = message ?: R.string.are_you_sure_you_want_to_do_this + builder.setMessage(messageResource) -// builder.setItems( -// arrayOf( -// getString(R.string.yes), -// getString(R.string.cancel) -// ) -// ) { _, index -> -// // The 'which' argument contains the index position -// // of the selected item -// when (index) { -// 0 -> proceed() -// 1 -> {} // nothing -// } -// } - builder.create().show() + title?.let { builder.setTitle(it) } + val positiveButtonTitle = positiveTitle ?: R.string.yes + builder.setPositiveButton(positiveButtonTitle) { _, _ -> + proceed() + } + builder.setNegativeButton(R.string.cancel) { _, _ -> + // nothing + } + builder.create().show() } // Display Alert Dialog fun Activity.showAlertDialog(title: Int? = null, message: Int? = null) { - showAlertDialog(this, title, message) + showAlertDialog(this, title, message) } -fun Fragment.showAlertDialog(title: Int? = null, message: Int? = null) { - context?.let { - showAlertDialog(it, title, message) - } +fun Fragment.showAlertDialog( + title: Int? = null, + messageResId: Int? = null, + message: String? = null +) { + context?.let { + showAlertDialog(it, title, messageResId, message) + } +} + +fun showEditTextAlertDialog( + context: Context, inputType: Int, title: Int? = null, messageResId: Int? = null, message: String? = null, + editTextText: String? = null, positiveAction: ((String) -> Unit)? = null +) { + val builder = AlertDialog.Builder(context) + title?.let { + builder.setTitle(title) + } + messageResId?.let { + builder.setMessage(messageResId) + } + message?.let { + builder.setMessage(it) + } + + val editText = EditText(context) + editTextText?.let { + editText.text = SpannableStringBuilder(it) + } + editText.setTextColor(ContextCompat.getColor(context, R.color.white)) + editText.inputType = inputType + + builder.setView(editText) + + builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ -> + positiveAction?.invoke(editText.text.toString()) + } + builder.show() } /** * Create and show an alert dialog */ fun showAlertDialog( - context: Context, title: Int? = null, message: Int? = null, cancelButtonTitle: Int? = null, showCancelButton: Boolean = false, - positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null + context: Context, title: Int? = null, messageResId: Int? = null, message: String? = null, + cancelButtonTitle: Int? = null, showCancelButton: Boolean = false, + positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null ) { - val builder = AlertDialog.Builder(context) - title?.let { - builder.setTitle(title) - } - message?.let { - builder.setMessage(message) - } - builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ -> - positiveAction?.invoke() - } - - if (cancelButtonTitle != null) { - builder.setNegativeButton(cancelButtonTitle) { _, _ -> - negativeAction?.invoke() - } - } else if (showCancelButton) { - builder.setNegativeButton(R.string.cancel) { _, _ -> - negativeAction?.invoke() - } - } - builder.show() + val builder = AlertDialog.Builder(context) + title?.let { + builder.setTitle(title) + } + messageResId?.let { + builder.setMessage(messageResId) + } + message?.let { + builder.setMessage(it) + } + + builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ -> + positiveAction?.invoke() + } + + if (cancelButtonTitle != null) { + builder.setNegativeButton(cancelButtonTitle) { _, _ -> + negativeAction?.invoke() + } + } else if (showCancelButton) { + builder.setNegativeButton(R.string.cancel) { _, _ -> + negativeAction?.invoke() + } + } + builder.show() } fun TextView.setTextFormat(textFormat: TextFormat, context: Context) { - this.setTextColor(textFormat.getColor(context)) - this.text = textFormat.text + this.setTextColor(textFormat.getColor(context)) + this.text = textFormat.text } fun View.hideWithAnimation() { - isVisible = true - animate().cancel() - animate().alpha(0f).withEndAction { isVisible = false }.start() + isVisible = true + animate().cancel() + animate().alpha(0f).withEndAction { isVisible = false }.start() } fun View.showWithAnimation() { - isVisible = true - animate().cancel() - animate().alpha(1f).start() + isVisible = true + animate().cancel() + animate().alpha(1f).start() } fun View.addCircleRipple() = with(TypedValue()) { - context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, this, true) - setBackgroundResource(resourceId) + context.theme.resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, this, true) + setBackgroundResource(resourceId) } fun SearchView.removeMargins() { - val searchEditFrame = findViewById(R.id.search_edit_frame) - val layoutParams = searchEditFrame?.layoutParams as LinearLayout.LayoutParams? - layoutParams?.leftMargin = 0 - layoutParams?.rightMargin = 0 - searchEditFrame?.layoutParams = layoutParams + val searchEditFrame = findViewById(R.id.search_edit_frame) + val layoutParams = searchEditFrame?.layoutParams as LinearLayout.LayoutParams? + layoutParams?.leftMargin = 0 + layoutParams?.rightMargin = 0 + searchEditFrame?.layoutParams = layoutParams } -fun View.toByteArray() : ByteArray { - return this.convertToBitmap().toByteArray() +fun View.toByteArray(): ByteArray { + return this.convertToBitmap().toByteArray() } fun View.convertToBitmap(): Bitmap { - val b = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888) - val canvas = Canvas(b) - val background = this.background - this.background?.let { - background.draw(canvas) - } ?: run { - canvas.drawColor(Color.WHITE) - } - this.draw(canvas) - return b + val b = Bitmap.createBitmap(this.width, this.height, Bitmap.Config.ARGB_8888) + val canvas = Canvas(b) + val background = this.background + this.background?.let { + background.draw(canvas) + } ?: run { + canvas.drawColor(Color.WHITE) + } + this.draw(canvas) + return b } -fun ImageView.toByteArray() : ByteArray { - return this.drawable.toBitmap().toByteArray() +fun ImageView.toByteArray(): ByteArray { + return this.drawable.toBitmap().toByteArray() } -fun Bitmap.toByteArray() : ByteArray { - val baos = ByteArrayOutputStream() - this.compress(Bitmap.CompressFormat.PNG, 100, baos) - return baos.toByteArray() +fun Bitmap.toByteArray(): ByteArray { + val baos = ByteArrayOutputStream() + this.compress(Bitmap.CompressFormat.PNG, 100, baos) + return baos.toByteArray() } fun Context.hideKeyboard(v: View) { - val imm = v.context.getSystemService(InputMethodManager::class.java) - imm?.hideSoftInputFromWindow(v.windowToken, 0) + val imm = v.context.getSystemService(InputMethodManager::class.java) + imm?.hideSoftInputFromWindow(v.windowToken, 0) } //fun Context.showKeyboard(view: View) { diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt index 4a9e5c5e..bbcff31e 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/CurrenciesFragment.kt @@ -65,8 +65,8 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row } var currencyCode: String = currency.currencyCode - var currencySymbole: String = currency.getSymbol(Locale.getDefault()) - var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbole})" + var currencySymbol: String = currency.getSymbol(Locale.getDefault()) + var currencyCodeAndSymbol: String = "${this.currencyCode} (${this.currencySymbol})" override val viewType: Int = RowViewType.TITLE_VALUE.ordinal } @@ -110,6 +110,9 @@ class CurrenciesFragment : BaseFragment(), StaticRowRepresentableDataSource, Row // RowRepresentableDelegate override fun onRowSelected(position: Int, row: RowRepresentable, tag: Int) { + + + val intent = Intent() intent.putExtra(INTENT_CURRENCY_CODE, (row as CurrencyRow).currency.currencyCode) this.activity?.setResult(Activity.RESULT_OK, intent) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt index df353214..2875020c 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt @@ -6,6 +6,7 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.os.Bundle +import android.text.InputType import android.view.LayoutInflater import android.view.View import android.view.ViewGroup @@ -17,6 +18,7 @@ import com.google.android.play.core.review.ReviewManagerFactory import io.realm.Realm import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.R +import net.pokeranalytics.android.api.CurrencyConverterApi import net.pokeranalytics.android.databinding.FragmentSettingsBinding import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.model.LiveData @@ -33,6 +35,7 @@ import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource import net.pokeranalytics.android.ui.extensions.openContactMail import net.pokeranalytics.android.ui.extensions.openUrl +import net.pokeranalytics.android.ui.extensions.showEditTextAlertDialog import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.modules.bankroll.BankrollActivity import net.pokeranalytics.android.ui.modules.datalist.DataListActivity @@ -141,19 +144,7 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep if (resultCode == Activity.RESULT_OK) { data?.let { val currencyCode = data.getStringExtra(CurrenciesFragment.INTENT_CURRENCY_CODE) ?: throw PAIllegalStateException("Missing currency code") - Preferences.setCurrencyCode(currencyCode, requireContext()) - val realm = Realm.getDefaultInstance() - realm.executeTransaction { - realm.where(Currency::class.java).isNull("code").or().equalTo("code", UserDefaults.currency.currencyCode).findAll().forEach { currency -> - currency.rate = Currency.DEFAULT_RATE - } - - realm.where(Session::class.java).isNull("bankroll.currency.code").findAll().forEach { session -> - session.bankrollHasBeenUpdated() - } - } - realm.close() - settingsAdapterRow.refreshRow(SettingsRow.CURRENCY) + updateMainCurrency(currencyCode) } } } @@ -163,6 +154,50 @@ class SettingsFragment : RealmFragment(), RowRepresentableDelegate, StaticRowRep } } + private fun updateMainCurrency(currencyCode: String) { + + Preferences.getDefaultCurrency(requireContext())?.currencyCode?.let { mainCurrencyCode -> + + if (mainCurrencyCode == currencyCode) { + return + } + showLoader(R.string.please_wait) + + CurrencyConverterApi.currencyRate(mainCurrencyCode, currencyCode, requireContext()) { apiRate, _ -> + hideLoader() + + val message = requireContext().getString(R.string.currency_rate_confirmation, mainCurrencyCode, currencyCode) + + +// val message = "Please enter the $mainCurrencyCode to $currencyCode rate to apply to all your bankrolls" + showEditTextAlertDialog(requireContext(), InputType.TYPE_CLASS_NUMBER or InputType.TYPE_NUMBER_FLAG_DECIMAL, + message = message, editTextText = apiRate?.toString()) { value -> + value.toDoubleOrNull()?.let { rate -> + updateMainCurrency(currencyCode, rate) + } + } + } + } + + } + + private fun updateMainCurrency(currencyCode: String, rate: Double) { + + Preferences.setCurrencyCode(currencyCode, requireContext()) + val realm = Realm.getDefaultInstance() + realm.executeTransaction { + realm.where(Currency::class.java).findAll().forEach { currency -> + currency.rate = (currency.rate ?: 1.0) * rate + } + + realm.where(Session::class.java).findAll().forEach { session -> + session.bankrollHasBeenUpdated() + } + } + realm.close() + settingsAdapterRow.refreshRow(SettingsRow.CURRENCY) + } + override fun adapterRows(): List { return rowRepresentation } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt index 608b1326..1608817f 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/BaseFragment.kt @@ -7,6 +7,7 @@ import android.os.Bundle import android.view.View import androidx.appcompat.widget.Toolbar import androidx.fragment.app.Fragment +import com.google.android.material.snackbar.Snackbar import net.pokeranalytics.android.PokerAnalyticsApplication import net.pokeranalytics.android.R import net.pokeranalytics.android.ui.activity.components.BaseActivity @@ -15,6 +16,7 @@ import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheet import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor import net.pokeranalytics.android.util.CrashLogging +import timber.log.Timber import java.io.File import java.util.* @@ -183,4 +185,13 @@ abstract class BaseFragment : Fragment() { return capability?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false } + fun showSnackBar(message: String) { + this.view?.let { view -> + val snackBar = Snackbar.make(view, message, Snackbar.LENGTH_INDEFINITE) + snackBar.show() + } ?: run { + Timber.d("No parent view for snackbar") + } + } + } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/LoaderDialogFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/LoaderDialogFragment.kt index 423fea32..6016700e7 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/LoaderDialogFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/components/LoaderDialogFragment.kt @@ -47,7 +47,7 @@ class LoaderDialogFragment: DialogFragment() { override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - arguments?.let {bundle -> + arguments?.let { bundle -> if (bundle.containsKey(ARGUMENT_MESSAGE_RES_ID)) { binding.loadingMessage.text = getString(bundle.getInt(ARGUMENT_MESSAGE_RES_ID)) } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt index 738e3889..0fbc1cc6 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/fragment/report/AbstractReportFragment.kt @@ -68,9 +68,9 @@ abstract class AbstractReportFragment : DataManagerFragment() { // Inflate and set the layout for the dialog // Pass null as the parent view because its going in the dialog layout - val view = inflater.inflate(net.pokeranalytics.android.R.layout.dialog_edit_text, null) + val view = inflater.inflate(R.layout.dialog_edit_text, null) val nameEditText = - view.findViewById(net.pokeranalytics.android.R.id.reportName) + view.findViewById(R.id.reportName) nameEditText.inputType = InputType.TYPE_TEXT_FLAG_CAP_SENTENCES this.model.primaryKey?.let { id -> @@ -81,7 +81,7 @@ abstract class AbstractReportFragment : DataManagerFragment() { builder.setView(view) // Add action buttons - .setPositiveButton(net.pokeranalytics.android.R.string.save) { dialog, _ -> + .setPositiveButton(R.string.save) { dialog, _ -> try { saveReport(nameEditText.text.toString()) dialog.dismiss() @@ -89,7 +89,7 @@ abstract class AbstractReportFragment : DataManagerFragment() { Toast.makeText(requireContext(), e.localizedMessage, Toast.LENGTH_SHORT).show() } } - .setNegativeButton(net.pokeranalytics.android.R.string.cancel) { dialog, _ -> + .setNegativeButton(R.string.cancel) { dialog, _ -> dialog.cancel() } diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt index 2e3a1dbb..210b26ba 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarDetailsFragment.kt @@ -177,6 +177,7 @@ class CalendarDetailsFragment : BaseFragment(), StaticRowRepresentableDataSource when (model.sessionTypeCondition) { QueryCondition.IsCash -> query.add(QueryCondition.IsCash) QueryCondition.IsTournament -> query.add(QueryCondition.IsTournament) + else -> {} } val requiredStats: List = listOf(Stat.LOCATIONS_PLAYED, Stat.LONGEST_STREAKS, Stat.DAYS_PLAYED, Stat.STANDARD_DEVIATION_HOURLY) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt index 09eecc5e..02591d28 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/calendar/CalendarFragment.kt @@ -431,6 +431,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable Calendar.MONTH, condition.listOfValues.first() ) + else -> {} } } @@ -478,6 +479,7 @@ class CalendarFragment : RealmFragment(), CoroutineScope, StaticRowRepresentable Calendar.YEAR, condition.listOfValues.first() ) + else -> {} } } yearlyReports[calendar.time] = computedResults diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/data/BankrollDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/BankrollDataFragment.kt index 996eac62..642787e4 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/data/BankrollDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/BankrollDataFragment.kt @@ -8,13 +8,14 @@ import android.view.View import androidx.lifecycle.MutableLiveData import androidx.lifecycle.ViewModelProvider import net.pokeranalytics.android.R -import net.pokeranalytics.android.api.FreeConverterApi +import net.pokeranalytics.android.api.CurrencyConverterApi import net.pokeranalytics.android.model.realm.Bankroll import net.pokeranalytics.android.model.realm.ResultCaptureType import net.pokeranalytics.android.ui.activity.CurrenciesActivity import net.pokeranalytics.android.ui.activity.components.RequestCode import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource +import net.pokeranalytics.android.ui.extensions.toast import net.pokeranalytics.android.ui.fragment.CurrenciesFragment import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor @@ -137,7 +138,7 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS override fun charSequenceForRow(row: RowRepresentable, context: Context, tag: Int): CharSequence { return when (row) { - SimpleRow.NAME -> if (bankroll.name.isNotEmpty()) bankroll.name else NULL_TEXT + SimpleRow.NAME -> bankroll.name.ifEmpty { NULL_TEXT } BankrollPropertiesRow.CURRENCY -> { bankroll.currency?.code?.let { code -> Currency.getInstance(code).currencyCode @@ -263,12 +264,24 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS } this.lastRefreshRateCall = System.currentTimeMillis() - val currenciesConverterValue = "${bankroll.currency?.code}_${defaultCurrency.currencyCode}" +// val currenciesConverterValue = "${bankroll.currency?.code}_${defaultCurrency.currencyCode}" - FreeConverterApi.currencyRate(currenciesConverterValue, requireContext()) { rate -> - onRowValueChanged(rate, BankrollPropertiesRow.RATE) - isRefreshingRate = false - rowRepresentableAdapter.refreshRow(BankrollPropertiesRow.REFRESH_RATE) + bankroll.currency?.code?.let { from -> + val to = defaultCurrency.currencyCode + CurrencyConverterApi.currencyRate(from, to, requireContext()) { rate, error -> + + rate?.let { + onRowValueChanged(rate, BankrollPropertiesRow.RATE) + } + + error?.localizedMessage?.let { message -> + toast(message) + } + +// onRowValueChanged(rate, BankrollPropertiesRow.RATE) + isRefreshingRate = false + rowRepresentableAdapter.refreshRow(BankrollPropertiesRow.REFRESH_RATE) + } } this.isRefreshingRate = true @@ -280,7 +293,6 @@ class BankrollDataFragment : EditableDataFragment(), StaticRowRepresentableDataS this.bankrollModel.selectedCaptureType.value?.let { Preferences.setResultCaptureType(this.bankroll, it, requireContext()) } - } } \ No newline at end of file diff --git a/app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt index 5a5d2684..bd1c5fff 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/modules/data/PlayerDataFragment.kt @@ -172,7 +172,7 @@ class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSou if (row.isValidForDelete(getRealm())) { GlobalScope.launch(Dispatchers.Main) { delay(300) - showAlertDialog(requireContext(), message = R.string.are_you_sure_you_want_to_delete, showCancelButton = true, positiveAction = { + showAlertDialog(requireContext(), messageResId = R.string.are_you_sure_you_want_to_delete, showCancelButton = true, positiveAction = { player.deleteComment(row) rowRepresentableAdapter.notifyDataSetChanged() }) diff --git a/app/src/main/java/net/pokeranalytics/android/ui/view/rows/FilterSectionRow.kt b/app/src/main/java/net/pokeranalytics/android/ui/view/rows/FilterSectionRow.kt index e09a04ad..4d62f22d 100644 --- a/app/src/main/java/net/pokeranalytics/android/ui/view/rows/FilterSectionRow.kt +++ b/app/src/main/java/net/pokeranalytics/android/ui/view/rows/FilterSectionRow.kt @@ -63,6 +63,7 @@ sealed class FilterSectionRow(override val resId: Int?) : RowRepresentable { is CustomField -> { return customField.name } + else -> {} } return name } diff --git a/app/src/main/java/net/pokeranalytics/android/util/Preferences.kt b/app/src/main/java/net/pokeranalytics/android/util/Preferences.kt index 853ffa13..bb78ed43 100644 --- a/app/src/main/java/net/pokeranalytics/android/util/Preferences.kt +++ b/app/src/main/java/net/pokeranalytics/android/util/Preferences.kt @@ -48,7 +48,8 @@ class Preferences { PATCH_STAKES("patchStakes"), CLEAN_BLINDS_FILTERS("deleteBlindsFilters"), SHOW_IN_APP_BADGES("showInAppBadges"), - LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate") + LAST_CALENDAR_BADGE_DATE("lastCalendarBadgeDate"), + PATCH_RATED_AMOUNT("patchRatedAmount[new field]") } enum class FeedMessage { @@ -101,17 +102,17 @@ class Preferences { companion object { - fun setStringSet(key: PreferenceKey, value: MutableSet, context: Context) { - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - val editor = preferences.edit() - editor.putStringSet(key.identifier, value) - editor.apply() - } - - fun getStringSet(key: Keys, context: Context): MutableSet? { - val preferences = PreferenceManager.getDefaultSharedPreferences(context) - return preferences.getStringSet(key.identifier, null) - } +// fun setStringSet(key: PreferenceKey, value: MutableSet, context: Context) { +// val preferences = PreferenceManager.getDefaultSharedPreferences(context) +// val editor = preferences.edit() +// editor.putStringSet(key.identifier, value) +// editor.apply() +// } +// +// fun getStringSet(key: Keys, context: Context): MutableSet? { +// val preferences = PreferenceManager.getDefaultSharedPreferences(context) +// return preferences.getStringSet(key.identifier, null) +// } fun setString(key: PreferenceKey, value: String, context: Context) { val preferences = PreferenceManager.getDefaultSharedPreferences(context) diff --git a/app/src/main/res/values-de/strings.xml b/app/src/main/res/values-de/strings.xml index aa31e9fa..b9e3a8b4 100644 --- a/app/src/main/res/values-de/strings.xml +++ b/app/src/main/res/values-de/strings.xml @@ -780,5 +780,7 @@ expense + Please wait… + Please enter the %1$s to %2$s rate to apply to all your bankrolls diff --git a/app/src/main/res/values-es/strings.xml b/app/src/main/res/values-es/strings.xml index 788f0034..207d3638 100644 --- a/app/src/main/res/values-es/strings.xml +++ b/app/src/main/res/values-es/strings.xml @@ -784,6 +784,8 @@ La aplicación funciona con una suscripción anual para uso ilimitado, pero obti expense + Please wait… + Please enter the %1$s to %2$s rate to apply to all your bankrolls diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 11c65a8e..b9ad5849 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -789,5 +789,7 @@ Filtre de transactions frais + Veuillez patienter… + Veuillez entrer le taux de %1$s vers %2$s pour l\'appliquer à toutes vos bankrolls diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index e3a79af2..e0422d56 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -779,5 +779,7 @@ expense + Please wait… + Please enter the %1$s to %2$s rate to apply to all your bankrolls diff --git a/app/src/main/res/values-it/strings.xml b/app/src/main/res/values-it/strings.xml index 794c114a..357687d5 100644 --- a/app/src/main/res/values-it/strings.xml +++ b/app/src/main/res/values-it/strings.xml @@ -779,5 +779,7 @@ expense + Please wait… + Please enter the %1$s to %2$s rate to apply to all your bankrolls diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index ace50d8d..2de4c802 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -783,5 +783,7 @@ expense + Please wait…... + Please enter the %1$s to %2$s rate to apply to all your bankrolls diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index a662b946..275a6332 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -778,5 +778,7 @@ expense + Please wait… + Please enter the %1$s to %2$s rate to apply to all your bankrolls diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml index 80489d4f..46cef7d4 100644 --- a/app/src/main/res/values-ru/strings.xml +++ b/app/src/main/res/values-ru/strings.xml @@ -780,5 +780,7 @@ expense + Please wait… + Please enter the %1$s to %2$s rate to apply to all your bankrolls diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml index 0fac0397..019fe926 100644 --- a/app/src/main/res/values-zh/strings.xml +++ b/app/src/main/res/values-zh/strings.xml @@ -773,5 +773,7 @@ expense + Please wait… + Please enter the %1$s to %2$s rate to apply to all your bankrolls diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index f88f0887..bfd71e18 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -833,5 +833,7 @@ This screen will show where you perform the best when you\'ll have more data. You also can create custom reports using the top right button. Transaction Filter expense + Please wait… + Please enter the %1$s to %2$s rate to apply to all your bankrolls diff --git a/build.gradle b/build.gradle index 6ce66563..c357114f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,7 +1,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { - ext.kotlin_version = '1.5.21' + ext.kotlin_version = '1.7.21' repositories { google() jcenter() @@ -15,6 +15,9 @@ buildscript { classpath 'com.google.gms:google-services:4.3.4' classpath 'com.google.firebase:firebase-crashlytics-gradle:2.4.1' + // serialization + classpath "org.jetbrains.kotlin:kotlin-serialization:$kotlin_version" + } }