Compare commits

...

16 Commits
master ... csv

  1. 4
      app/build.gradle
  2. 2
      app/src/androidTest/java/net/pokeranalytics/android/components/SessionInstrumentedUnitTest.kt
  3. 2
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/BankrollInstrumentedUnitTest.kt
  4. 4
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/DeleteInstrumentedUnitTest.kt
  5. 2
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatPerformanceUnitTest.kt
  6. 8
      app/src/androidTest/java/net/pokeranalytics/android/unitTests/StatsInstrumentedUnitTest.kt
  7. 5
      app/src/main/AndroidManifest.xml
  8. 4
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  9. 67
      app/src/main/java/net/pokeranalytics/android/model/LiveData.kt
  10. 25
      app/src/main/java/net/pokeranalytics/android/model/TableSize.kt
  11. 22
      app/src/main/java/net/pokeranalytics/android/model/interfaces/Manageable.kt
  12. 23
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  13. 42
      app/src/main/java/net/pokeranalytics/android/model/realm/CustomField.kt
  14. 26
      app/src/main/java/net/pokeranalytics/android/model/realm/IdentifiableObject.kt
  15. 52
      app/src/main/java/net/pokeranalytics/android/model/realm/Import.kt
  16. 41
      app/src/main/java/net/pokeranalytics/android/model/realm/Result.kt
  17. 23
      app/src/main/java/net/pokeranalytics/android/model/realm/Session.kt
  18. 27
      app/src/main/java/net/pokeranalytics/android/ui/activity/ImportActivity.kt
  19. 33
      app/src/main/java/net/pokeranalytics/android/ui/activity/ImportsHistoryActivity.kt
  20. 21
      app/src/main/java/net/pokeranalytics/android/ui/extensions/UIExtensions.kt
  21. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/DataListFragment.kt
  22. 34
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportFragment.kt
  23. 123
      app/src/main/java/net/pokeranalytics/android/ui/fragment/ImportsHistoryFragment.kt
  24. 1
      app/src/main/java/net/pokeranalytics/android/ui/fragment/SettingsFragment.kt
  25. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/data/CustomFieldDataFragment.kt
  26. 2
      app/src/main/java/net/pokeranalytics/android/ui/fragment/data/DataManagerFragment.kt
  27. 5
      app/src/main/java/net/pokeranalytics/android/ui/view/RowRepresentable.kt
  28. 25
      app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt
  29. 7
      app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/SettingRow.kt
  30. 90
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVDescriptor.kt
  31. 69
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVField.kt
  32. 21
      app/src/main/java/net/pokeranalytics/android/util/csv/CSVImporter.kt
  33. 258
      app/src/main/java/net/pokeranalytics/android/util/csv/ProductCSVDescriptors.kt
  34. 24
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionCSVDescriptor.kt
  35. 48
      app/src/main/java/net/pokeranalytics/android/util/csv/SessionField.kt
  36. 8
      app/src/main/java/net/pokeranalytics/android/util/extensions/RealmExtensions.kt
  37. 15
      app/src/main/res/layout/activity_imports_history.xml
  38. 61
      app/src/main/res/layout/fragment_imports_history.xml
  39. 74
      app/src/main/res/layout/row_title_subtitle_action.xml
  40. 4
      app/src/main/res/layout/row_title_value_action.xml
  41. 2
      app/src/main/res/values/strings.xml
  42. 8
      app/src/test/java/net/pokeranalytics/android/BasicUnitTest.kt
  43. 9
      app/src/test/java/net/pokeranalytics/android/SavableEnumTest.kt

@ -29,8 +29,8 @@ android {
applicationId "net.pokeranalytics.android" applicationId "net.pokeranalytics.android"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 28 targetSdkVersion 28
versionCode 47 versionCode 51
versionName "2.1.1" versionName "2.1.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

@ -30,7 +30,7 @@ open class SessionInstrumentedUnitTest : RealmInstrumentedUnitTest() {
session.numberOfTables = numberOfTable session.numberOfTables = numberOfTable
session.tableSize = tableSize session.tableSize = tableSize
session.startDate = startDate session.startDate = startDate
session.result?.netResult = netResult session.result?.onlineNet = netResult
val cal = Calendar.getInstance() // creates calendar val cal = Calendar.getInstance() // creates calendar
cal.time = startDate // sets calendar time/date cal.time = startDate // sets calendar time/date
cal.add(Calendar.HOUR_OF_DAY, endDate) // adds one hour cal.add(Calendar.HOUR_OF_DAY, endDate) // adds one hour

@ -28,7 +28,7 @@ class BankrollInstrumentedUnitTest : SessionInstrumentedUnitTest() {
// convenience extension // convenience extension
fun Session.Companion.testInstance(netResult: Double, startDate: Date, endDate: Date?): Session { fun Session.Companion.testInstance(netResult: Double, startDate: Date, endDate: Date?): Session {
val session: Session = newInstance(super.mockRealm, false) val session: Session = newInstance(super.mockRealm, false)
session.result?.netResult = netResult session.result?.onlineNet = netResult
session.startDate = startDate session.startDate = startDate
session.endDate = endDate session.endDate = endDate
return session return session

@ -33,8 +33,8 @@ class DeleteInstrumentedUnitTest : RealmInstrumentedUnitTest() {
s1.bankroll = br1 s1.bankroll = br1
s2.bankroll = br2 s2.bankroll = br2
s1.result?.netResult = 100.0 s1.result?.onlineNet = 100.0
s2.result?.netResult = 200.0 s2.result?.onlineNet = 200.0
realm.commitTransaction() realm.commitTransaction()

@ -28,7 +28,7 @@ class StatPerformanceUnitTest : SessionInstrumentedUnitTest() {
realm.commitTransaction() realm.commitTransaction()
val d1 = Date() val d1 = Date()
realm.where(Result::class.java).sum("netResult") realm.where(Result::class.java).sum("onlineNet")
val d2 = Date() val d2 = Date()
val duration = (d2.time - d1.time) val duration = (d2.time - d1.time)

@ -593,8 +593,8 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
b2.currency = c2 b2.currency = c2
s1.bankroll = b1 s1.bankroll = b1
s2.bankroll = b2 s2.bankroll = b2
s1.result?.netResult = 100.0 s1.result?.onlineNet = 100.0
s2.result?.netResult = 200.0 s2.result?.onlineNet = 200.0
} }
@ -629,8 +629,8 @@ class StatsInstrumentedUnitTest : SessionInstrumentedUnitTest() {
b2.currency = c2 b2.currency = c2
s1.bankroll = b1 s1.bankroll = b1
s2.bankroll = b2 s2.bankroll = b2
s1.result?.netResult = 100.0 s1.result?.onlineNet = 100.0
s2.result?.netResult = 200.0 s2.result?.onlineNet = 200.0
} }

@ -129,6 +129,11 @@
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="portrait" /> android:screenOrientation="portrait" />
<activity
android:name="net.pokeranalytics.android.ui.activity.ImportsHistoryActivity"
android:launchMode="singleTop"
android:screenOrientation="portrait" />
<activity <activity
android:name="net.pokeranalytics.android.ui.activity.FiltersActivity" android:name="net.pokeranalytics.android.ui.activity.FiltersActivity"
android:launchMode="singleTop" android:launchMode="singleTop"

@ -33,7 +33,7 @@ class PokerAnalyticsApplication : Application() {
Realm.init(this) Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder() val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME) .name(Realm.DEFAULT_REALM_NAME)
.schemaVersion(7) .schemaVersion(8)
.migration(PokerAnalyticsMigration()) .migration(PokerAnalyticsMigration())
.initialData(Seed(this)) .initialData(Seed(this))
.build() .build()
@ -60,7 +60,7 @@ class PokerAnalyticsApplication : Application() {
if (BuildConfig.DEBUG) { if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}") Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
this.createFakeSessions() // this.createFakeSessions()
} }
Patcher.patchAll(this.applicationContext) Patcher.patchAll(this.applicationContext)

@ -3,46 +3,44 @@ package net.pokeranalytics.android.model
import android.content.Context import android.content.Context
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.interfaces.Deletable import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.realm.* import net.pokeranalytics.android.model.realm.*
import net.pokeranalytics.android.ui.view.Localizable import net.pokeranalytics.android.ui.view.Localizable
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
import net.pokeranalytics.android.util.extensions.findById import net.pokeranalytics.android.util.extensions.findById
/** /**
* An enum managing the business objects related to a realm results * An enum managing the business objects related to a realm results
*/ */
enum class LiveData : Localizable { enum class LiveData(override var uniqueIdentifier: Int, val relatedEntity: Class<out Identifiable>) : Localizable, IntIdentifiable {
BANKROLL, BANKROLL(0, Bankroll::class.java),
GAME, GAME(1, Game::class.java),
LOCATION, LOCATION(2, Location::class.java),
TOURNAMENT_NAME, TOURNAMENT_NAME(3, TournamentName::class.java),
TOURNAMENT_FEATURE, TOURNAMENT_FEATURE(4, TournamentFeature::class.java),
TRANSACTION, TRANSACTION(5, Transaction::class.java),
TRANSACTION_TYPE, TRANSACTION_TYPE(6, TransactionType::class.java),
FILTER, FILTER(7, Filter::class.java),
CUSTOM_FIELD, CUSTOM_FIELD(8, CustomField::class.java),
REPORT_SETUP; REPORT_SETUP(9, ReportSetup::class.java),
SESSION(10, Session::class.java);
var subType:Int? = null
companion object : IntSearchable<LiveData> {
override fun valuesInternal(): Array<LiveData> {
return values()
}
val relatedEntity: Class<out Deletable> fun valueFromClass(clazz: Class<out Identifiable>) : LiveData? {
get() { return values().firstOrNull { it.relatedEntity == clazz }
return when (this) {
BANKROLL -> Bankroll::class.java
GAME -> Game::class.java
LOCATION -> Location::class.java
TOURNAMENT_NAME -> TournamentName::class.java
TOURNAMENT_FEATURE -> TournamentFeature::class.java
TRANSACTION -> Transaction::class.java
TRANSACTION_TYPE -> TransactionType::class.java
FILTER -> Filter::class.java
CUSTOM_FIELD -> CustomField::class.java
REPORT_SETUP -> ReportSetup::class.java
}
} }
}
var subType:Int? = null
fun updateOrCreate(realm: Realm, primaryKey: String?): Deletable { fun updateOrCreate(realm: Realm, primaryKey: String?): Identifiable {
val proxyItem: Deletable? = this.getData(realm, primaryKey) val proxyItem: Identifiable? = this.getData(realm, primaryKey)
proxyItem?.let { proxyItem?.let {
return realm.copyFromRealm(it) return realm.copyFromRealm(it)
} ?: run { } ?: run {
@ -50,12 +48,12 @@ enum class LiveData : Localizable {
} }
} }
private fun newEntity(): Deletable { private fun newEntity(): Identifiable {
return this.relatedEntity.newInstance() return this.relatedEntity.newInstance()
} }
fun getData(realm: Realm, primaryKey: String?): Deletable? { fun getData(realm: Realm, primaryKey: String?): Identifiable? {
var proxyItem: Deletable? = null var proxyItem: Identifiable? = null
primaryKey?.let { primaryKey?.let {
val t = realm.findById(this.relatedEntity, it) val t = realm.findById(this.relatedEntity, it)
t?.let { t?.let {
@ -78,6 +76,7 @@ enum class LiveData : Localizable {
FILTER -> R.string.filter FILTER -> R.string.filter
CUSTOM_FIELD -> R.string.custom_field CUSTOM_FIELD -> R.string.custom_field
REPORT_SETUP -> R.string.custom REPORT_SETUP -> R.string.custom
SESSION -> R.string.session
} }
} }
@ -94,6 +93,7 @@ enum class LiveData : Localizable {
FILTER -> R.string.filters FILTER -> R.string.filters
CUSTOM_FIELD -> R.string.custom_fields CUSTOM_FIELD -> R.string.custom_fields
REPORT_SETUP -> R.string.custom REPORT_SETUP -> R.string.custom
SESSION -> R.string.sessions
} }
} }
@ -110,6 +110,7 @@ enum class LiveData : Localizable {
FILTER -> R.string.new_filter FILTER -> R.string.new_filter
CUSTOM_FIELD -> R.string.new_custom_field CUSTOM_FIELD -> R.string.new_custom_field
REPORT_SETUP -> R.string.new_report REPORT_SETUP -> R.string.new_report
else -> R.string.none
} }
} }

@ -4,6 +4,8 @@ import android.content.Context
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType import net.pokeranalytics.android.ui.view.RowViewType
import java.text.NumberFormat
import java.text.ParseException
class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITLE_GRID.ordinal) : RowRepresentable { class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITLE_GRID.ordinal) : RowRepresentable {
@ -15,12 +17,23 @@ class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITL
{ index -> TableSize(index + 2) }).toList() { index -> TableSize(index + 2) }).toList()
} }
fun valueForLabel(label: String) : Int? { /**
return when (label) { * Tries to parse a label into a Table Size Int value,
"Full Ring", "Full-Ring" -> 10 * using number parsing or label recognition
"Short-Handed", "Short Handed" -> 6 * A numberFormat can be passed when dealing with lots of volume
"Heads-Up", "Heads Up" -> 2 */
else -> null fun valueForLabel(label: String, numberFormat: NumberFormat? = null) : Int? {
return try {
val nf = numberFormat ?: NumberFormat.getInstance()
nf.parse(label).toInt()
} catch (e: ParseException) {
when (label) {
"Full Ring", "Full-Ring" -> 10
"Short-Handed", "Short Handed" -> 6
"Heads-Up", "Heads Up" -> 2
else -> null
}
} }
} }
} }

@ -5,6 +5,9 @@ import io.realm.Realm
import io.realm.RealmModel import io.realm.RealmModel
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.ModelException import net.pokeranalytics.android.exceptions.ModelException
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.model.realm.IdentifiableObject
import net.pokeranalytics.android.ui.view.RowRepresentable import net.pokeranalytics.android.ui.view.RowRepresentable
enum class SaveValidityStatus { enum class SaveValidityStatus {
@ -45,7 +48,24 @@ interface NameManageable : Manageable {
} }
} }
class ObjectIdentifier(var id: String, var clazz: Class<out Identifiable>) class ObjectIdentifier(var id: String, var clazz: Class<out Identifiable>) {
val identifiableObject: IdentifiableObject
get() {
val livedata = LiveData.valueFromClass(this.clazz)
livedata?.let {
val io = IdentifiableObject()
io.id = this.id
io.classId = it.uniqueIdentifier
return io
} ?: run {
throw PAIllegalStateException("clazz $clazz has no corresponding LiveData value")
}
}
}
/** /**
* An interface associate a unique uniqueIdentifier to an object * An interface associate a unique uniqueIdentifier to an object

@ -3,6 +3,7 @@ package net.pokeranalytics.android.model.migrations
import io.realm.DynamicRealm import io.realm.DynamicRealm
import io.realm.RealmMigration import io.realm.RealmMigration
import timber.log.Timber import timber.log.Timber
import java.util.*
class PokerAnalyticsMigration : RealmMigration { class PokerAnalyticsMigration : RealmMigration {
@ -107,7 +108,6 @@ class PokerAnalyticsMigration : RealmMigration {
it.addPrimaryKey("id") it.addPrimaryKey("id")
it.addField("value", String::class.java).setNullable("value", false) it.addField("value", String::class.java).setNullable("value", false)
it.addField("order", Integer::class.java).setNullable("order", false) it.addField("order", Integer::class.java).setNullable("order", false)
// it.addRealmObjectField("customField", it).setNullable("customField", false)
it.addField("numericValue", Double::class.java).setNullable("numericValue", true) it.addField("numericValue", Double::class.java).setNullable("numericValue", true)
} }
@ -151,6 +151,27 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("TransactionType")?.addField("useCount", Int::class.java) schema.get("TransactionType")?.addField("useCount", Int::class.java)
currentVersion++ currentVersion++
} }
// Migrate to version 8
if (currentVersion == 7) {
Timber.d("*** Running migration ${currentVersion + 1}")
schema.create("Import")?.let { importSchema ->
importSchema.addField("date", Date::class.java).setRequired("date", true)
importSchema.addField("fileName", String::class.java).setRequired("fileName", true)
schema.get("Session")?.let {
importSchema.addRealmListField("sessions", it)
}
schema.get("Transaction")?.let {
importSchema.addRealmListField("transactions", it)
}
}
schema.get("Session")?.addField("tournamentPrizepool", Double::class.java)?.setNullable("tournamentPrizepool", true)
schema.get("Result")?.renameField("netResult", "onlineNet")
currentVersion++
}
} }
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {

@ -23,12 +23,29 @@ import net.pokeranalytics.android.ui.view.rowrepresentable.CustomFieldRow
import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable import net.pokeranalytics.android.ui.view.rowrepresentable.CustomizableRowRepresentable
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.util.enumerations.IntIdentifiable import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
import net.pokeranalytics.android.util.extensions.findByName
import java.util.* import java.util.*
import kotlin.collections.ArrayList import kotlin.collections.ArrayList
open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable { open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDataSource, RowRepresentable {
companion object {
fun getOrCreate(realm: Realm, name: String, type: Type): CustomField {
val cf = realm.findByName(CustomField::class.java, name)
cf?.let {
return it
}
val customField = CustomField()
customField.name = name
customField.type = type.uniqueIdentifier
return realm.copyToRealm(customField)
}
}
@Ignore @Ignore
override val realmObjectClass: Class<out Identifiable> = CustomField::class.java override val realmObjectClass: Class<out Identifiable> = CustomField::class.java
@ -39,7 +56,13 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
IntIdentifiable { IntIdentifiable {
LIST(0, R.string.enum_custom_field_type), LIST(0, R.string.enum_custom_field_type),
NUMBER(1, R.string.number), NUMBER(1, R.string.number),
AMOUNT(2, R.string.amount) AMOUNT(2, R.string.amount);
companion object : IntSearchable<Type> {
override fun valuesInternal(): Array<Type> {
return values()
}
}
} }
/** /**
@ -251,7 +274,7 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
/** /**
* Add an entry * Add an entry
*/ */
fun addEntry(): CustomFieldEntry { fun addEmptyEntry(): CustomFieldEntry {
val entry = CustomFieldEntry() val entry = CustomFieldEntry()
this.entries.add(entry) this.entries.add(entry)
sortEntries() sortEntries()
@ -275,7 +298,8 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
val realm = Realm.getDefaultInstance() val realm = Realm.getDefaultInstance()
realm.executeTransaction { realm.executeTransaction {
this.entriesToDelete.forEach { // entries are out of realm this.entriesToDelete.forEach {
// entries are out of realm
realm.where<CustomFieldEntry>().equalTo("id", it.id).findFirst()?.deleteFromRealm() realm.where<CustomFieldEntry>().equalTo("id", it.id).findFirst()?.deleteFromRealm()
} }
} }
@ -314,4 +338,16 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
} }
} }
fun getOrCreateListEntry(name: String): CustomFieldEntry {
this.entries.firstOrNull { it.name == name }?.let {
return it
}
val entry = this.realm.copyToRealm(CustomFieldEntry())
entry.value = name
this.entries.add(entry)
return entry
}
} }

@ -0,0 +1,26 @@
package net.pokeranalytics.android.model.realm
import io.realm.RealmModel
import io.realm.RealmObject
import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.util.extensions.findById
open class IdentifiableObject : RealmObject() {
/**
* The id of an Identifiable object
*/
var id: String = ""
/**
* The uniqueIdentifier of the LiveData object
*/
var classId: Int = 0
val realmModel: RealmModel?
get() {
val clazz = LiveData.valueByIdentifier(this.classId).relatedEntity
return this.realm.findById(clazz, id)
}
}

@ -0,0 +1,52 @@
package net.pokeranalytics.android.model.realm
import android.content.Context
import io.realm.RealmList
import io.realm.RealmObject
import io.realm.annotations.Ignore
import io.realm.kotlin.deleteFromRealm
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import java.util.*
open class Import : RealmObject(), RowRepresentable {
var date: Date = Date()
var fileName: String = ""
var identifiableObjects: RealmList<IdentifiableObject> = RealmList()
@Ignore
override val isClickable: Boolean = false
@Ignore
override val viewType: Int = RowViewType.TITLE_SUBTITLE_ACTION.ordinal
@Ignore
override val imageRes: Int? = R.drawable.ic_outline_delete
@Ignore
override val imageClickable: Boolean? = true
override fun getDisplayName(context: Context): String {
return fileName
}
fun delete() {
this.realm.executeTransaction {
this.identifiableObjects.forEach {
val realmModel = it.realmModel
when (realmModel) {
is Session -> {
realmModel.cleanup()
}
else -> {}
}
realmModel?.deleteFromRealm()
}
}
}
}

@ -9,6 +9,7 @@ import io.realm.annotations.RealmClass
import net.pokeranalytics.android.exceptions.PAIllegalStateException import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.model.filter.Filterable import net.pokeranalytics.android.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition import net.pokeranalytics.android.model.filter.QueryCondition
import timber.log.Timber
@RealmClass @RealmClass
open class Result : RealmObject(), Filterable { open class Result : RealmObject(), Filterable {
@ -48,21 +49,31 @@ open class Result : RealmObject(), Filterable {
/** /**
* The net result * The net result
*/ */
var netResult: Double? = null var onlineNet: Double? = null
set(value) { set(value) {
var errorMessage: String? = null
this.session?.bankroll?.let { bankroll -> this.session?.bankroll?.let { bankroll ->
if (bankroll.live) { if (bankroll.live) {
throw PAIllegalStateException("Can't set net result on a live bankroll") errorMessage = "Can't set net result on a live bankroll"
} }
} ?: run { } ?: run {
throw PAIllegalStateException("Session doesn't have any bankroll") errorMessage = "Session doesn't have any bankroll"
}
errorMessage?.let {
if (isManaged) {
throw PAIllegalStateException(it)
} else {
Timber.w(it)
}
} }
field = value field = value
this.computeNet() this.computeNet()
if (value != null) { if (value != null) {
this.session.end() this.session?.end()
} }
} }
@ -107,16 +118,24 @@ open class Result : RealmObject(), Filterable {
val transactionsSum = transactions.sumByDouble { it.amount } val transactionsSum = transactions.sumByDouble { it.amount }
val isLive = this.session?.isLive ?: true this.onlineNet?.let {
if (isLive) { this.net = it + transactionsSum
} ?: run {
val buyin = this.buyin ?: 0.0 val buyin = this.buyin ?: 0.0
val cashOut = this.cashout ?: 0.0 val cashedOut = this.cashout ?: 0.0
this.net = cashOut - buyin + transactionsSum this.net = cashedOut - buyin + transactionsSum
} else {
val netResult = this.netResult ?: 0.0
this.net = netResult + transactionsSum
} }
// val isLive = this.session?.isLive ?: true
// if (isLive) {
// val buyin = this.buyin ?: 0.0
// val cashOut = this.cashout ?: 0.0
// this.net = cashOut - buyin + transactionsSum
// } else {
// val onlineNet = this.onlineNet ?: 0.0
// this.net = onlineNet + transactionsSum
// }
// Precompute results // Precompute results
this.session?.computeStats() this.session?.computeStats()
this.session?.sessionSet?.computeStats() this.session?.sessionSet?.computeStats()

@ -45,7 +45,7 @@ import kotlin.collections.ArrayList
typealias BB = Double typealias BB = Double
open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDataSource, RowRepresentable, Timed, open class Session : RealmObject(), Manageable, StaticRowRepresentableDataSource, RowRepresentable, Timed,
TimeFilterable, Filterable, DatedBankrollGraphEntry { TimeFilterable, Filterable, DatedBankrollGraphEntry {
enum class Type { enum class Type {
@ -314,6 +314,11 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
// The features of the tournament, like Knockout, Shootout, Turbo... // The features of the tournament, like Knockout, Shootout, Turbo...
var tournamentFeatures: RealmList<TournamentFeature> = RealmList() var tournamentFeatures: RealmList<TournamentFeature> = RealmList()
/**
* the prizepool of the tournament
*/
var tournamentPrizepool: Double? = null
// The custom fields values // The custom fields values
var customFieldEntries: RealmList<CustomFieldEntry> = RealmList() var customFieldEntries: RealmList<CustomFieldEntry> = RealmList()
@ -471,7 +476,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
val hasNetResult: Boolean val hasNetResult: Boolean
get() { get() {
return this.result?.netResult != null return this.result?.onlineNet != null
} }
// Manageable // Manageable
@ -488,6 +493,14 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
return R.string.no_br_popup_message return R.string.no_br_popup_message
} }
override fun getFailedDeleteMessage(status: DeleteValidityStatus): Int {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
override fun isValidForDelete(realm: Realm): Boolean {
return true
}
// States // States
/** /**
@ -783,7 +796,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
SessionRow.BREAK_TIME -> if (this.breakDuration > 0.0) this.breakDuration.toMinutes() else NULL_TEXT SessionRow.BREAK_TIME -> if (this.breakDuration > 0.0) this.breakDuration.toMinutes() else NULL_TEXT
SessionRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT SessionRow.BUY_IN -> this.result?.buyin?.toCurrency(currency) ?: NULL_TEXT
SessionRow.CASHED_OUT, SessionRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT SessionRow.CASHED_OUT, SessionRow.PRIZE -> this.result?.cashout?.toCurrency(currency) ?: NULL_TEXT
SessionRow.NET_RESULT -> this.result?.netResult?.toCurrency(currency) ?: NULL_TEXT SessionRow.NET_RESULT -> this.result?.onlineNet?.toCurrency(currency) ?: NULL_TEXT
SessionRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT SessionRow.COMMENT -> if (this.comment.isNotEmpty()) this.comment else NULL_TEXT
SessionRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT SessionRow.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT
SessionRow.GAME -> getFormattedGame() SessionRow.GAME -> getFormattedGame()
@ -898,7 +911,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
) )
SessionRow.NET_RESULT -> row.editingDescriptors( SessionRow.NET_RESULT -> row.editingDescriptors(
mapOf( mapOf(
"defaultValue" to result?.netResult "defaultValue" to result?.onlineNet
) )
) )
SessionRow.COMMENT -> row.editingDescriptors( SessionRow.COMMENT -> row.editingDescriptors(
@ -993,7 +1006,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
} }
SessionRow.NET_RESULT -> { SessionRow.NET_RESULT -> {
this.result?.let { result -> this.result?.let { result ->
result.netResult = value as Double? result.onlineNet = value as Double?
} }
} }
SessionRow.COMMENT -> comment = value as String? ?: "" SessionRow.COMMENT -> comment = value as String? ?: ""

@ -61,7 +61,7 @@ class ImportActivity : PokerAnalyticsActivity() {
val fis = contentResolver.openInputStream(fileURI) val fis = contentResolver.openInputStream(fileURI)
Timber.d("Load fragment data with: $fis") Timber.d("Load fragment data with: $fis")
fis?.let { fis?.let {
fragment.setData(it) fragment.setData(it, fileURI)
} }
fragmentTransaction.add(R.id.container, fragment) fragmentTransaction.add(R.id.container, fragment)
@ -69,31 +69,6 @@ class ImportActivity : PokerAnalyticsActivity() {
} }
// override fun onNewIntent(intent: Intent?) {
// super.onNewIntent(intent)
//
// Timber.d("++++++ data = ${intent?.data}")
//
// setIntent(intent)
// intent?.let {
//
// when (intent.action) {
// "android.intent.action.VIEW" -> { // import
// val data = it.data
// if (data != null) {
// this.requestImportConfirmation(data)
// } else {
// throw PAIllegalStateException("URI null on import")
// }
// }
// else -> {
// Timber.w("Intent ${intent.action} unmanaged")
// }
// }
// }
//
// }
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data) super.onActivityResult(requestCode, resultCode, data)

@ -0,0 +1,33 @@
package net.pokeranalytics.android.ui.activity
import android.content.Context
import android.content.Intent
import android.os.Bundle
import androidx.fragment.app.Fragment
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
class ImportsHistoryActivity : PokerAnalyticsActivity() {
companion object {
fun newInstance(context: Context) {
val intent = Intent(context, ImportsHistoryActivity::class.java)
context.startActivity(intent)
}
/**
* Create a new instance for result
*/
fun newInstanceForResult(fragment: Fragment, requestCode: Int) {
val intent = Intent(fragment.requireContext(), ImportsHistoryActivity::class.java)
fragment.startActivityForResult(intent, requestCode)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_imports_history)
}
}

@ -16,6 +16,7 @@ import androidx.core.content.FileProvider
import androidx.core.view.isVisible import androidx.core.view.isVisible
import net.pokeranalytics.android.BuildConfig import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.util.DeviceUtils import net.pokeranalytics.android.util.DeviceUtils
@ -104,9 +105,13 @@ fun PokerAnalyticsActivity.showAlertDialog(title: Int? = null, message: Int? = n
showAlertDialog(this, title, message) showAlertDialog(this, title, message)
} }
fun PokerAnalyticsFragment.showAlertDialog(title: Int? = null, message: Int? = null) { fun PokerAnalyticsFragment.showAlertDialog(title: Int? = null, message: Int? = null, messageString: String? = null,
cancelButtonTitle: Int? = null, showCancelButton: Boolean = false,
positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null) {
context?.let { context?.let {
showAlertDialog(it, title, message) showAlertDialog(it, title, message, messageString, cancelButtonTitle, showCancelButton, positiveAction, negativeAction)
} ?: run {
throw PAIllegalStateException("Fragment has no context")
} }
} }
@ -114,9 +119,10 @@ fun PokerAnalyticsFragment.showAlertDialog(title: Int? = null, message: Int? = n
* Create and show an alert dialog * Create and show an alert dialog
*/ */
fun showAlertDialog( fun showAlertDialog(
context: Context, title: Int? = null, message: Int? = null, cancelButtonTitle: Int? = null, showCancelButton: Boolean = false, context: Context, title: Int? = null, message: Int? = null, messageString: String? = null,
positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null cancelButtonTitle: Int? = null, showCancelButton: Boolean = false,
) { positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null) {
val builder = AlertDialog.Builder(context) val builder = AlertDialog.Builder(context)
title?.let { title?.let {
builder.setTitle(title) builder.setTitle(title)
@ -124,7 +130,10 @@ fun showAlertDialog(
message?.let { message?.let {
builder.setMessage(message) builder.setMessage(message)
} }
builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ -> messageString?.let {
builder.setMessage(it)
}
builder.setPositiveButton(R.string.ok) { _, _ ->
positiveAction?.invoke() positiveAction?.invoke()
} }

@ -47,7 +47,7 @@ open class DataListFragment : DeletableItemFragment(), LiveRowRepresentableDataS
open fun setData(dataType: Int) { open fun setData(dataType: Int) {
this.dataType = LiveData.values()[dataType] this.dataType = LiveData.values()[dataType]
this.identifiableClass = this.dataType.relatedEntity this.identifiableClass = this.dataType.relatedEntity as Class<out Deletable>
setToolbarTitle(this.dataType.pluralLocalizedTitle(requireContext())) setToolbarTitle(this.dataType.pluralLocalizedTitle(requireContext()))
this.items = this.retrieveItems(getRealm()) this.items = this.retrieveItems(getRealm())

@ -1,5 +1,6 @@
package net.pokeranalytics.android.ui.fragment package net.pokeranalytics.android.ui.fragment
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
@ -10,6 +11,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import net.pokeranalytics.android.R import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.extensions.showAlertDialog
import net.pokeranalytics.android.ui.fragment.components.RealmFragment import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.util.csv.CSVImporter import net.pokeranalytics.android.util.csv.CSVImporter
import net.pokeranalytics.android.util.csv.ImportDelegate import net.pokeranalytics.android.util.csv.ImportDelegate
@ -25,7 +27,7 @@ class ImportFragment : RealmFragment(), ImportDelegate {
val coroutineContext: CoroutineContext val coroutineContext: CoroutineContext
get() = Dispatchers.Main get() = Dispatchers.Main
private lateinit var filePath: String private var filePath: String? = null
private lateinit var inputStream: InputStream private lateinit var inputStream: InputStream
private lateinit var importer: CSVImporter private lateinit var importer: CSVImporter
@ -33,8 +35,9 @@ class ImportFragment : RealmFragment(), ImportDelegate {
this.filePath = path this.filePath = path
} }
fun setData(inputStream: InputStream) { fun setData(inputStream: InputStream, uri: Uri) {
this.inputStream = inputStream this.inputStream = inputStream
this.filePath = uri.path
} }
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? { override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -69,9 +72,9 @@ class ImportFragment : RealmFragment(), ImportDelegate {
private fun startImport() { private fun startImport() {
// var shouldDismissActivity = false var errorMessage: String? = null
this.importer = CSVImporter(inputStream) this.importer = CSVImporter(this.inputStream, this.filePath)
this.importer.delegate = this this.importer.delegate = this
GlobalScope.launch(coroutineContext) { GlobalScope.launch(coroutineContext) {
@ -83,7 +86,7 @@ class ImportFragment : RealmFragment(), ImportDelegate {
try { try {
importer.start() importer.start()
} catch (e: ImportException) { } catch (e: ImportException) {
// shouldDismissActivity = true errorMessage = e.message
} }
val e = Date() val e = Date()
val duration = (e.time - s.time) / 1000.0 val duration = (e.time - s.time) / 1000.0
@ -92,16 +95,17 @@ class ImportFragment : RealmFragment(), ImportDelegate {
} }
test.await() test.await()
// if (shouldDismissActivity) { if (errorMessage != null) {
//
// activity?.let { showAlertDialog(
// it.setResult(ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) messageString = errorMessage,
// it.finish() positiveAction = {
// } activity?.finish()
// })
// } else {
// } } else {
importDidFinish() importDidFinish()
}
} }

@ -0,0 +1,123 @@
package net.pokeranalytics.android.ui.fragment
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import io.realm.RealmResults
import io.realm.Sort
import io.realm.kotlin.where
import kotlinx.android.synthetic.main.fragment_data_list.*
import net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.Import
import net.pokeranalytics.android.ui.adapter.RowRepresentableAdapter
import net.pokeranalytics.android.ui.adapter.RowRepresentableDelegate
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.showAlertDialog
import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowViewType
import net.pokeranalytics.android.util.extensions.shortDateTime
import java.util.*
class ImportsHistoryFragment : RealmFragment(), StaticRowRepresentableDataSource, RowRepresentableDelegate {
companion object {
val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
//rows.addAll(mostUsedCurrencies)
//rows.add(SeparatorRow())
//rows.addAll(availableCurrencies)
rows
}
}
private lateinit var dataListAdapter: RowRepresentableAdapter
private lateinit var items: RealmResults<Import>
override fun rowRepresentableForPosition(position: Int): RowRepresentable? {
return this.items[position] as RowRepresentable
}
override fun numberOfRows(): Int {
return items.size
}
override fun viewTypeForPosition(position: Int): Int {
val viewType = (this.items[position] as RowRepresentable).viewType
return if (viewType != -1) viewType else RowViewType.DATA.ordinal
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.fragment_currencies, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initData()
initUI()
}
override fun adapterRows(): List<RowRepresentable>? {
return rowRepresentation
}
override fun stringForRow(row: RowRepresentable): String {
if (row is Import) {
return row.date.shortDateTime()
}
return super.stringForRow(row)
}
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) {
if (row is Import && fromAction) {
showAlertDialog(message = R.string.import_deletion, showCancelButton = true, positiveAction = {
//TODO: Check the deletion works correctly
row.delete()
dataListAdapter.notifyItemRemoved(position)
})
}
}
private fun initData() {
items = getRealm().where<Import>().findAll().sort("date", Sort.DESCENDING)
if (BuildConfig.DEBUG && items.size == 0) {
val calendar = Calendar.getInstance()
getRealm().executeTransaction {
for (i in 0..2) {
val import = Import()
import.fileName = "Import_${i}_Android.csv"
import.date = calendar.time
it.copyToRealm(import)
calendar.add(Calendar.DAY_OF_MONTH, -1)
}
}
}
}
/**
* Init UI
*/
private fun initUI() {
setDisplayHomeAsUpEnabled(true)
setToolbarTitle(getString(R.string.imports_history))
val viewManager = LinearLayoutManager(requireContext())
dataListAdapter = RowRepresentableAdapter(this, this)
recyclerView.apply {
setHasFixedSize(true)
layoutManager = viewManager
adapter = dataListAdapter
}
}
}

@ -126,6 +126,7 @@ class SettingsFragment : PokerAnalyticsFragment(), RowRepresentableDelegate, Sta
SettingRow.CONTACT_US -> parentActivity?.openContactMail(R.string.contact) SettingRow.CONTACT_US -> parentActivity?.openContactMail(R.string.contact)
SettingRow.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path) SettingRow.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path)
SettingRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value) SettingRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value)
SettingRow.IMPORTS_HISTORY -> ImportsHistoryActivity.newInstance(requireContext())
SettingRow.FOLLOW_US -> { SettingRow.FOLLOW_US -> {
when (position) { when (position) {
0 -> parentActivity?.openUrl(URL.BLOG.value) 0 -> parentActivity?.openUrl(URL.BLOG.value)

@ -216,7 +216,7 @@ class CustomFieldDataFragment : EditableDataFragment(), StaticRowRepresentableDa
} }
addItem.setOnClickListener { addItem.setOnClickListener {
val customFieldEntry = customField.addEntry() val customFieldEntry = customField.addEmptyEntry()
rowRepresentableAdapter.notifyDataSetChanged() rowRepresentableAdapter.notifyDataSetChanged()
onRowSelected(-1, customFieldEntry) onRowSelected(-1, customFieldEntry)
} }

@ -75,7 +75,7 @@ open class DataManagerFragment : RealmFragment() {
*/ */
private fun loadItem() { private fun loadItem() {
this.item = this.liveDataType.updateOrCreate(this.getRealm(), primaryKey) this.item = this.liveDataType.updateOrCreate(this.getRealm(), primaryKey) as Deletable
this.deleteButtonShouldAppear = this.primaryKey != null this.deleteButtonShouldAppear = this.primaryKey != null
} }

@ -76,6 +76,11 @@ interface Displayable : Localizable {
get() { get() {
return null return null
} }
val isClickable: Boolean
get() {
return true
}
} }
/** /**

@ -68,6 +68,7 @@ enum class RowViewType(private var layoutRes: Int) {
TITLE_VALUE(R.layout.row_title_value), TITLE_VALUE(R.layout.row_title_value),
TITLE_VALUE_ARROW(R.layout.row_title_value_arrow), TITLE_VALUE_ARROW(R.layout.row_title_value_arrow),
TITLE_VALUE_ACTION(R.layout.row_title_value_action), TITLE_VALUE_ACTION(R.layout.row_title_value_action),
TITLE_SUBTITLE_ACTION(R.layout.row_title_subtitle_action),
TITLE_SWITCH(R.layout.row_title_switch), TITLE_SWITCH(R.layout.row_title_switch),
TITLE_GRID(R.layout.row_bottom_sheet_grid_title), TITLE_GRID(R.layout.row_bottom_sheet_grid_title),
DATA(R.layout.row_title), DATA(R.layout.row_title),
@ -102,8 +103,8 @@ enum class RowViewType(private var layoutRes: Int) {
// Row View Holder // Row View Holder
HEADER_TITLE, HEADER_TITLE_VALUE, HEADER_TITLE_AMOUNT, HEADER_TITLE_AMOUNT_BIG, LOCATION_TITLE, HEADER_TITLE, HEADER_TITLE_VALUE, HEADER_TITLE_AMOUNT, HEADER_TITLE_AMOUNT_BIG, LOCATION_TITLE,
INFO, TITLE, TITLE_ARROW, TITLE_ICON_ARROW, TITLE_VALUE, TITLE_VALUE_ARROW, TITLE_VALUE_ACTION, TITLE_GRID, INFO, TITLE, TITLE_ARROW, TITLE_ICON_ARROW, TITLE_VALUE, TITLE_VALUE_ARROW, TITLE_VALUE_ACTION, TITLE_SUBTITLE_ACTION,
TITLE_SWITCH, TITLE_CHECK, TITLE_VALUE_CHECK, TITLE_GRID, TITLE_SWITCH, TITLE_CHECK, TITLE_VALUE_CHECK,
DATA, BOTTOM_SHEET_DATA, LOADER -> RowViewHolder(layout) DATA, BOTTOM_SHEET_DATA, LOADER -> RowViewHolder(layout)
// Row Session // Row Session
@ -244,18 +245,20 @@ enum class RowViewType(private var layoutRes: Int) {
} }
// Listener // Listener
val listener = View.OnClickListener { if (row.isClickable) {
itemView.findViewById<SwitchCompat?>(R.id.switchView)?.let { val listener = View.OnClickListener {
if (adapter.dataSource.isEnabled(row)) { itemView.findViewById<SwitchCompat?>(R.id.switchView)?.let {
it.isChecked = !it.isChecked if (adapter.dataSource.isEnabled(row)) {
it.isChecked = !it.isChecked
}
} ?: run {
adapter.delegate?.onRowSelected(position, row)
} }
} ?: run {
adapter.delegate?.onRowSelected(position, row)
} }
}
itemView.findViewById<View?>(R.id.container)?.setOnClickListener(listener) itemView.findViewById<View?>(R.id.container)?.setOnClickListener(listener)
} }
}
} }
// Switch // Switch

@ -26,6 +26,9 @@ enum class SettingRow : RowRepresentable {
LANGUAGE, LANGUAGE,
CURRENCY, CURRENCY,
// Import & Export
IMPORTS_HISTORY,
// Data management // Data management
CUSTOM_FIELD, CUSTOM_FIELD,
BANKROLL, BANKROLL,
@ -62,6 +65,9 @@ enum class SettingRow : RowRepresentable {
rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.preferences)) rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.preferences))
rows.addAll(arrayListOf(CURRENCY)) rows.addAll(arrayListOf(CURRENCY))
rows.add(CustomizableRowRepresentable(customViewType = RowViewType.HEADER_TITLE, resId = R.string.data))
rows.addAll(arrayListOf(IMPORTS_HISTORY))
rows.add( rows.add(
CustomizableRowRepresentable( CustomizableRowRepresentable(
customViewType = RowViewType.HEADER_TITLE, customViewType = RowViewType.HEADER_TITLE,
@ -95,6 +101,7 @@ enum class SettingRow : RowRepresentable {
TERMS_OF_USE -> R.string.terms_of_use TERMS_OF_USE -> R.string.terms_of_use
FOLLOW_US -> R.string.follow_us FOLLOW_US -> R.string.follow_us
LANGUAGE -> R.string.language LANGUAGE -> R.string.language
IMPORTS_HISTORY -> R.string.imports_history
CURRENCY -> R.string.currency CURRENCY -> R.string.currency
GDPR -> R.string.gdpr GDPR -> R.string.gdpr
else -> null else -> null

@ -1,18 +1,18 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import io.realm.Realm import io.realm.Realm
import io.realm.kotlin.deleteFromRealm
import net.pokeranalytics.android.model.interfaces.Identifiable import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.ObjectIdentifier import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.Session import net.pokeranalytics.android.model.realm.Import
import net.pokeranalytics.android.util.extensions.findById
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
import timber.log.Timber import timber.log.Timber
import java.text.NumberFormat
/** /**
* The various sources of CSV * The various sources of CSV
*/ */
enum class DataSource { enum class DataSource {
POKER_ANALYTICS,
POKER_INCOME, POKER_INCOME,
POKER_BANKROLL_TRACKER, POKER_BANKROLL_TRACKER,
RUNGOOD RUNGOOD
@ -23,25 +23,19 @@ enum class DataSource {
*/ */
abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg elements: CSVField) : CSVDescriptor(source, *elements) { abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg elements: CSVField) : CSVDescriptor(source, *elements) {
/**
* List of Realm object identificators
*/
val realmModelIds = mutableListOf<ObjectIdentifier>()
abstract fun parseData(realm: Realm, record: CSVRecord): T? abstract fun parseData(realm: Realm, record: CSVRecord): T?
override fun parse(realm: Realm, record: CSVRecord): Int { override fun parse(realm: Realm, record: CSVRecord): Int {
val data = this.parseData(realm, record) val data = this.parseData(realm, record)
data?.let { data?.let {
// Timber.d(">>>>>>> identifier added: ${it.id}") this.import.identifiableObjects.add(it.objectIdentifier.identifiableObject)
this.realmModelIds.add(it.objectIdentifier)
} }
return if (data != null) 1 else 0 return if (data != null) 1 else 0
} }
protected fun addAdditionallyCreatedIdentifiable(identifiable: Identifiable) { protected fun addAdditionallyCreatedIdentifiable(identifiable: Identifiable) {
this.realmModelIds.add(identifiable.objectIdentifier) this.import.identifiableObjects.add(identifiable.objectIdentifier.identifiableObject)
} }
override fun cancel(realm: Realm) { override fun cancel(realm: Realm) {
@ -57,20 +51,11 @@ abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg el
} }
private fun deleteInsertedFromRealm(realm: Realm) { private fun deleteInsertedFromRealm(realm: Realm) {
this.realmModelIds.forEach { identifier -> this.import.delete()
val data = realm.findById(identifier.clazz, identifier.id)
if (data is Session) {
data.cleanup()
}
data?.deleteFromRealm()
}
this.realmModelIds.clear()
} }
override fun save(realm: Realm) { override fun save(realm: Realm) {
super.save(realm) super.save(realm)
this.realmModelIds.clear()
} }
} }
@ -83,29 +68,21 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField)
/** /**
* The CSVField list describing the CSV header format * The CSVField list describing the CSV header format
*/ */
protected var fields: List<CSVField> = listOf() protected var fields: MutableList<CSVField> = mutableListOf()
/** /**
* The mapping of CSVField with their index in the CSV file * The mapping of CSVField with their index in the CSV file
*/ */
protected var fieldMapping: MutableMap<CSVField, Int> = mutableMapOf() protected var fieldMapping: MutableMap<CSVField, Int> = mutableMapOf()
lateinit var import: Import
init { init {
if (elements.size > 0) { if (elements.isNotEmpty()) {
this.fields = elements.toList() this.fields = elements.toMutableList()
} }
} }
companion object {
/**
* The list of all managed CSVDescriptors
*/
val all: List<CSVDescriptor> =
listOf(ProductCSVDescriptors.pokerIncomeCash,
ProductCSVDescriptors.pokerBankrollTracker,
ProductCSVDescriptors.runGoodCashGames,
ProductCSVDescriptors.runGoodTournaments)
}
/** /**
* Method called when iterating on a CSVRecord * Method called when iterating on a CSVRecord
*/ */
@ -135,9 +112,46 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField)
count++ count++
} }
} }
val mandatoryfields = this.fields.filter { it.optional == false } val mandatoryFields = this.fields.filter { it.optional == false }
Timber.d("source= ${this.source.name} > total fields = ${this.fields.size}, identified = $count") Timber.d("source= ${this.source.name} > total fields = ${this.fields.size}, identified = $count")
return count >= mandatoryfields.size return count >= mandatoryFields.size
}
fun mapCustomField(record: CSVRecord, realm: Realm) {
val customFields = realm.where(CustomField::class.java).findAll()
val headers = record.toSet()
headers.forEachIndexed { index, header ->
var name = header
// automatically creates custom field if necessary
if (source == DataSource.POKER_ANALYTICS) {
val splitter = "|"
if (header.contains(splitter)) {
val info = header.split(splitter)
name = info.first()
val typeIdentifier = NumberFormat.getInstance().parse(header.last().toString())
Timber.d("header = $header, info = $info, id = $typeIdentifier")
val type = CustomField.Type.valueByIdentifier(typeIdentifier.toInt())
CustomField.getOrCreate(realm, name, type)
}
}
// maps header with custom fields
val customField = customFields.firstOrNull { it.name == name }
customField?.let {
if (it.isListType) {
val f = MappedCustomCSVField.List(header, null, it)
this.fields.add(f)
this.fieldMapping[f] = index
} else {
val f = MappedCustomCSVField.Number(header, null, "", it)
this.fields.add(f)
this.fieldMapping[f] = index
}
}
}
} }
} }

@ -1,5 +1,7 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.CustomFieldEntry
import timber.log.Timber import timber.log.Timber
import java.text.DateFormat import java.text.DateFormat
import java.text.NumberFormat import java.text.NumberFormat
@ -14,21 +16,33 @@ interface NumberCSVField: TypedCSVField<Double> {
val numberFormat: String? val numberFormat: String?
override fun parse(value: String) : Double? { companion object {
fun defaultParse(value: String) : Double? {
if (value.isEmpty()) {
return null
}
if (value.isEmpty()) { val formatter = NumberFormat.getInstance()
return null
return try {
formatter.parse(value).toDouble()
} catch (e: ParseException) {
Timber.d("Field > Unparseable number: $value")
null
}
} }
}
val formatter = NumberFormat.getInstance() override fun parse(value: String) : Double? {
return try { this.callback?.let { cb ->
formatter.parse(value).toDouble() return cb(value)
} catch (e: ParseException) {
Timber.d("Field ${header} > Unparseable number: $value")
null
} }
return defaultParse(value)
} }
} }
interface DataCSVField<T> : TypedCSVField<T> { interface DataCSVField<T> : TypedCSVField<T> {
@ -82,6 +96,43 @@ interface BlindCSVField : TypedCSVField<Pair<Double, Double>> {
} }
interface CustomEntryCSVField : TypedCSVField<CustomFieldEntry>, CustomCSVField {
override fun parse(value: String): CustomFieldEntry? {
return if (value.isNotEmpty()) {
this.customField.getOrCreateListEntry(value)
} else {
null
}
}
}
interface CustomNumberCSVField : TypedCSVField<CustomFieldEntry>, CustomCSVField {
val numberFormat: String?
override fun parse(value: String): CustomFieldEntry? {
val formatter = NumberFormat.getInstance()
return try {
val number = formatter.parse(value).toDouble()
val entry = this.customField.realm.copyToRealm(CustomFieldEntry())
entry.numericValue = number
this.customField.entries.add(entry)
entry
} catch (e: ParseException) {
Timber.d("Field ${header} > Unparseable number: $value")
null
}
}
}
interface CustomCSVField {
var customField: CustomField
}
interface TypedCSVField<T> : CSVField { interface TypedCSVField<T> : CSVField {
fun parse(value: String) : T? fun parse(value: String) : T?
var callback: ((String) -> T?)? var callback: ((String) -> T?)?

@ -3,6 +3,7 @@ package net.pokeranalytics.android.util.csv
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import io.realm.Realm import io.realm.Realm
import net.pokeranalytics.android.model.realm.Import
import org.apache.commons.csv.CSVFormat import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVParser import org.apache.commons.csv.CSVParser
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
@ -25,7 +26,7 @@ interface ImportDelegate {
* When finding a descriptor, the CSVImporter then continue to parse the file and delegates the parsing of each row * When finding a descriptor, the CSVImporter then continue to parse the file and delegates the parsing of each row
* to the CSVDescriptor * to the CSVDescriptor
*/ */
open class CSVImporter(istream: InputStream) { open class CSVImporter(istream: InputStream, filePath: String? = null) {
/** /**
* The object being notified of the import progress * The object being notified of the import progress
@ -36,10 +37,12 @@ open class CSVImporter(istream: InputStream) {
* Number of commits required to commit a Realm transaction * Number of commits required to commit a Realm transaction
*/ */
private val COMMIT_FREQUENCY = 100 private val COMMIT_FREQUENCY = 100
/** /**
* The number of column indicating a valid record * The number of column indicating a valid record
*/ */
private val VALID_RECORD_COLUMNS = 4 private val VALID_RECORD_COLUMNS = 4
/** /**
* The number of valid record to test for descriptor before throwing a File Format Exception * The number of valid record to test for descriptor before throwing a File Format Exception
*/ */
@ -48,7 +51,7 @@ open class CSVImporter(istream: InputStream) {
/** /**
* The path of the CSV file * The path of the CSV file
*/ */
private var path: String? = null private var path: String? = filePath
/** /**
* The InputStream containing a file content * The InputStream containing a file content
*/ */
@ -105,13 +108,18 @@ open class CSVImporter(istream: InputStream) {
realm.beginTransaction() realm.beginTransaction()
val import = realm.copyToRealm(Import())
this.path?.let {
import.fileName = it
}
parser.forEachIndexed { index, record -> parser.forEachIndexed { index, record ->
// Timber.d("line $index") // Timber.d("line $index")
this.notifyDelegate() this.notifyDelegate()
if (this.currentDescriptor == null) { // find descriptor if (this.currentDescriptor == null) { // find descriptor
this.currentDescriptor = this.findDescriptor(record) this.currentDescriptor = this.findDescriptor(realm, record, import)
if (this.currentDescriptor == null) { if (this.currentDescriptor == null) {
@ -174,10 +182,13 @@ open class CSVImporter(istream: InputStream) {
/** /**
* Search for a descriptor in the list of managed formats * Search for a descriptor in the list of managed formats
*/ */
private fun findDescriptor(record: CSVRecord): CSVDescriptor? { private fun findDescriptor(realm: Realm, record: CSVRecord, import: Import): CSVDescriptor? {
CSVDescriptor.all.forEach { descriptor -> val allCSVDescriptors = ProductCSVDescriptors.all
allCSVDescriptors.forEach { descriptor ->
if (descriptor.matches(record)) { if (descriptor.matches(record)) {
descriptor.import = import
descriptor.mapCustomField(record, realm)
this.currentDescriptor = descriptor this.currentDescriptor = descriptor
Timber.d("Identified source: ${descriptor.source}") Timber.d("Identified source: ${descriptor.source}")
return descriptor return descriptor

@ -4,109 +4,169 @@ class ProductCSVDescriptors {
companion object { companion object {
val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor( /**
DataSource.POKER_INCOME, * The list of all managed CSVDescriptors
false, */
SessionField.Start("Start Time"), val all: List<CSVDescriptor> =
SessionField.End("End Time"), listOf(
SessionField.Buyin("Buy In"), pokerIncomeCash,
SessionField.CashedOut("Cashed Out"), pokerBankrollTracker,
SessionField.Break("Break Minutes"), runGoodCashGames,
SessionField.LimitType("Limit Type"), runGoodTournaments,
SessionField.Game("Game"), iOSPokerAnalytics
SessionField.Bankroll("Bankroll"), )
SessionField.Location("Location"),
SessionField.Location("Location Type"),
SessionField.Comment("Note"),
SessionField.Tips("Tips"),
SessionField.Blind("Stake")
)
val pokerBankrollTracker: CSVDescriptor = SessionCSVDescriptor( private val pokerIncomeCash: CSVDescriptor
DataSource.POKER_BANKROLL_TRACKER, get() {
true, return SessionCSVDescriptor(
SessionField.Start("starttime", dateFormat = "MM/dd/yy HH:mm"), DataSource.POKER_INCOME,
SessionField.End("endtime", dateFormat = "MM/dd/yy HH:mm"), false,
SessionField.SessionType("variant"), SessionField.Start("Start Time"),
SessionField.Buyin("buyin"), SessionField.End("End Time"),
SessionField.CashedOut("cashout"), SessionField.Buyin("Buy In"),
SessionField.Rebuy("rebuycosts"), SessionField.CashedOut("Cashed Out"),
SessionField.Addon("addoncosts"), SessionField.Break("Break Minutes"),
SessionField.Break("breakminutes"), SessionField.LimitType("Limit Type"),
SessionField.LimitType("limit"), SessionField.Game("Game"),
SessionField.Game("game"), SessionField.Bankroll("Bankroll"),
SessionField.Bankroll("currency"), // same as currency code SessionField.Location("Location"),
SessionField.Location("type"), SessionField.Location("Location Type"),
SessionField.Comment("comment", true), SessionField.Comment("Note"),
SessionField.Tips("expensesfromstack"), SessionField.Tips("Tips"),
SessionField.SmallBlind("smallblind"), SessionField.Blind("Stake")
SessionField.BigBlind("bigblind"), )
SessionField.TournamentNumberOfPlayers("player"), }
SessionField.TournamentPosition("place"),
SessionField.TournamentName("mttname"),
SessionField.CurrencyCode("currency"),
SessionField.StackingIn("sharesincomings"),
SessionField.StackingOut("sharesoutgoings"),
SessionField.CurrencyRate("exchangerate"),
SessionField.TableSize("tablesize")
)
val runGoodTournaments: CSVDescriptor = SessionCSVDescriptor( private val pokerBankrollTracker: CSVDescriptor
DataSource.RUNGOOD, get() {
true, return SessionCSVDescriptor(
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"), DataSource.POKER_BANKROLL_TRACKER,
SessionField.StartTime("Start Time"), true,
SessionField.End("End Date"), SessionField.Start("starttime", dateFormat = "MM/dd/yy HH:mm"),
SessionField.EndTime("End Time"), SessionField.End("endtime", dateFormat = "MM/dd/yy HH:mm"),
SessionField.Buyin("Total Buy-In"), SessionField.SessionType("variant"),
SessionField.CashedOut("Winnings"), SessionField.Buyin("buyin"),
SessionField.NetResult("Profit"), SessionField.CashedOut("cashout"),
SessionField.Break("Break"), SessionField.Rebuy("rebuycosts"),
SessionField.LimitType("Limit Type"), SessionField.Addon("addoncosts"),
SessionField.Game("Game"), SessionField.Break("breakminutes"),
SessionField.Bankroll("Bankroll"), SessionField.LimitType("limit"),
SessionField.TableSize("Table Type"), SessionField.Game("game"),
SessionField.Location("Location"), SessionField.Bankroll("currency"), // same as currency code
SessionField.LocationType("Location Type"), SessionField.Location("type"),
SessionField.Comment("Notes"), SessionField.Comment("comment", true),
SessionField.CurrencyCode("Currency"), SessionField.Tips("expensesfromstack"),
SessionField.TournamentName("Event Name"), SessionField.SmallBlind("smallblind"),
SessionField.TournamentNumberOfPlayers("Total Players"), SessionField.BigBlind("bigblind"),
SessionField.TournamentPosition("Finished Place"), SessionField.TournamentNumberOfPlayers("player"),
SessionField.TournamentType("Single-Table/Multi-Table") SessionField.TournamentPosition("place"),
SessionField.TournamentName("mttname"),
SessionField.CurrencyCode("currency"),
SessionField.StackingIn("sharesincomings"),
SessionField.StackingOut("sharesoutgoings"),
SessionField.CurrencyRate("exchangerate"),
SessionField.TableSize("tablesize")
)
}
)
val runGoodCashGames: CSVDescriptor = SessionCSVDescriptor( private val iOSPokerAnalytics: CSVDescriptor
DataSource.RUNGOOD, get() {
false, return SessionCSVDescriptor(
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"), DataSource.POKER_ANALYTICS,
SessionField.StartTime("Start Time", dateFormat = "HH:mm"), true,
SessionField.End("End Date", dateFormat = "dd/MM/yyyy"), SessionField.Start("Start Date", dateFormat = "MM/dd/yy HH:mm:ss"),
SessionField.EndTime("End Time", dateFormat = "HH:mm"), SessionField.End("End Date", dateFormat = "MM/dd/yy HH:mm:ss"),
SessionField.Buyin("Total Buy-In"), SessionField.Break("Break", callback = { string ->
SessionField.CashedOut("Cashed Out"), val number = NumberCSVField.defaultParse(string)
SessionField.NetResult("Profit"), return@Break number?.times(1000.0)
SessionField.Break("Break"), }),
SessionField.LimitType("Limit Type"), SessionField.SessionType("Type"),
SessionField.Game("Game"), SessionField.Live("Live"),
SessionField.Bankroll("Bankroll"), SessionField.NumberOfTables("Tables"),
SessionField.TableSize("Table Type"), SessionField.Buyin("Buyin"),
SessionField.Location("Location"), SessionField.CashedOut("Cashed Out"),
SessionField.LocationType("Location Type"), SessionField.NetResult("Online Net"),
SessionField.Comment("Notes"), SessionField.Tips("Tips"),
SessionField.CurrencyCode("Currency"), SessionField.LimitType("Limit"),
SessionField.Blind("Stakes", callback = { value -> SessionField.Game("Game"),
// $10/20 SessionField.TableSize("Table Size"),
value.drop(1) SessionField.Location("Location"),
val blinds = value.split("/") SessionField.Bankroll("Bankroll"),
if (blinds.size == 2) { SessionField.CurrencyCode("Currency Code"),
return@Blind Pair(blinds.first().toDouble(), blinds.last().toDouble()) SessionField.CurrencyRate("Currency Rate"),
} else { SessionField.SmallBlind("Small Blind"),
return@Blind null SessionField.BigBlind("Big Blind"),
} SessionField.TournamentType("Tournament Type"),
}) SessionField.TournamentEntryFee("Entry fee"),
) SessionField.TournamentNumberOfPlayers("Number of players"),
SessionField.TournamentPrizePool("Prize Pool"),
SessionField.TournamentPosition("Position"),
SessionField.Comment("Comment")
)
}
private val runGoodTournaments: CSVDescriptor
get() {
return SessionCSVDescriptor(
DataSource.RUNGOOD,
true,
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"),
SessionField.StartTime("Start Time"),
SessionField.End("End Date"),
SessionField.EndTime("End Time"),
SessionField.Buyin("Total Buy-In"),
SessionField.CashedOut("Winnings"),
SessionField.NetResult("Profit"),
SessionField.Break("Break"),
SessionField.LimitType("Limit Type"),
SessionField.Game("Game"),
SessionField.Bankroll("Bankroll"),
SessionField.TableSize("Table Type"),
SessionField.Location("Location"),
SessionField.LocationType("Location Type"),
SessionField.Comment("Notes"),
SessionField.CurrencyCode("Currency"),
SessionField.TournamentName("Event Name"),
SessionField.TournamentNumberOfPlayers("Total Players"),
SessionField.TournamentPosition("Finished Place"),
SessionField.TournamentType("Single-Table/Multi-Table")
)
}
private val runGoodCashGames: CSVDescriptor
get() {
return SessionCSVDescriptor(
DataSource.RUNGOOD,
false,
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"),
SessionField.StartTime("Start Time", dateFormat = "HH:mm"),
SessionField.End("End Date", dateFormat = "dd/MM/yyyy"),
SessionField.EndTime("End Time", dateFormat = "HH:mm"),
SessionField.Buyin("Total Buy-In"),
SessionField.CashedOut("Cashed Out"),
SessionField.NetResult("Profit"),
SessionField.Break("Break"),
SessionField.LimitType("Limit Type"),
SessionField.Game("Game"),
SessionField.Bankroll("Bankroll"),
SessionField.TableSize("Table Type"),
SessionField.Location("Location"),
SessionField.LocationType("Location Type"),
SessionField.Comment("Notes"),
SessionField.CurrencyCode("Currency"),
SessionField.Blind("Stakes", callback = { value ->
// $10/20
value.drop(1)
val blinds = value.split("/")
if (blinds.size == 2) {
return@Blind Pair(blinds.first().toDouble(), blinds.last().toDouble())
} else {
return@Blind null
}
})
)
}
} }

@ -14,6 +14,7 @@ import net.pokeranalytics.android.util.extensions.getOrCreate
import net.pokeranalytics.android.util.extensions.setHourMinutes import net.pokeranalytics.android.util.extensions.setHourMinutes
import org.apache.commons.csv.CSVRecord import org.apache.commons.csv.CSVRecord
import timber.log.Timber import timber.log.Timber
import java.text.NumberFormat
import java.util.* import java.util.*
/** /**
@ -124,12 +125,14 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
private fun parseSession(realm: Realm, record: CSVRecord): Session? { private fun parseSession(realm: Realm, record: CSVRecord): Session? {
val session = Session.newInstance(realm, this.isTournament, managed = false) val session = Session.newInstance(realm, this.isTournament, managed = false)
val intFormatter = NumberFormat.getInstance()
var startDate: Date? = null var startDate: Date? = null
var endDate: Date? = null var endDate: Date? = null
var isLive = true var isLive = true
var bankrollName = "" var bankrollName = ""
var netResult: Double? = null
var currencyCode: String? = null var currencyCode: String? = null
var currencyRate: Double? = null var currencyRate: Double? = null
var additionalBuyins = 0.0 // rebuy + addon var additionalBuyins = 0.0 // rebuy + addon
@ -155,6 +158,9 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
is SessionField.EndTime -> { is SessionField.EndTime -> {
endDate?.setHourMinutes(value) endDate?.setHourMinutes(value)
} }
is SessionField.Live -> {
isLive = field.parse(value) ?: true
}
is SessionField.Buyin -> { is SessionField.Buyin -> {
val buyin = field.parse(value) val buyin = field.parse(value)
session.result?.buyin = buyin session.result?.buyin = buyin
@ -164,6 +170,7 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
} }
} }
is SessionField.CashedOut -> session.result?.cashout = field.parse(value) is SessionField.CashedOut -> session.result?.cashout = field.parse(value)
is SessionField.NetResult -> netResult = field.parse(value)
is SessionField.SessionType -> { is SessionField.SessionType -> {
Session.Type.getValueFromString(value)?.let { type -> Session.Type.getValueFromString(value)?.let { type ->
session.type = type.ordinal session.type = type.ordinal
@ -199,7 +206,7 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
} }
is SessionField.SmallBlind -> session.cgSmallBlind = field.parse(value) is SessionField.SmallBlind -> session.cgSmallBlind = field.parse(value)
is SessionField.BigBlind -> session.cgBigBlind = field.parse(value) is SessionField.BigBlind -> session.cgBigBlind = field.parse(value)
is SessionField.TableSize -> session.tableSize = TableSize.valueForLabel(value) is SessionField.TableSize -> session.tableSize = TableSize.valueForLabel(value, intFormatter)
is SessionField.TournamentPosition -> session.result?.tournamentFinalPosition = is SessionField.TournamentPosition -> session.result?.tournamentFinalPosition =
field.parse(value)?.toInt() field.parse(value)?.toInt()
is SessionField.TournamentName -> { is SessionField.TournamentName -> {
@ -212,6 +219,7 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
TournamentType.getValueForLabel(value)?.ordinal TournamentType.getValueForLabel(value)?.ordinal
is SessionField.TournamentNumberOfPlayers -> session.tournamentNumberOfPlayers = is SessionField.TournamentNumberOfPlayers -> session.tournamentNumberOfPlayers =
field.parse(value)?.toInt() field.parse(value)?.toInt()
is SessionField.TournamentPrizePool -> session.tournamentPrizepool = field.parse(value)
is SessionField.CurrencyCode -> currencyCode = value is SessionField.CurrencyCode -> currencyCode = value
is SessionField.CurrencyRate -> currencyRate = field.parse(value) is SessionField.CurrencyRate -> currencyRate = field.parse(value)
is SessionField.StackingIn -> { is SessionField.StackingIn -> {
@ -220,6 +228,16 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
is SessionField.StackingOut -> { is SessionField.StackingOut -> {
stackingOut = field.parse(value) stackingOut = field.parse(value)
} }
is MappedCustomCSVField.Number -> {
field.parse(value)?.let {
session.customFieldEntries.add(it)
}
}
is MappedCustomCSVField.List -> {
field.parse(value)?.let {
session.customFieldEntries.add(it)
}
}
else -> { else -> {
} }
} }
@ -234,6 +252,10 @@ class SessionCSVDescriptor(source: DataSource, private var isTournament: Boolean
val bankroll = Bankroll.getOrCreate(realm, bankrollName, isLive, currencyCode, currencyRate) val bankroll = Bankroll.getOrCreate(realm, bankrollName, isLive, currencyCode, currencyRate)
session.bankroll = bankroll session.bankroll = bankroll
netResult?.let {
session.result?.onlineNet = it // need to be set after BR
}
session.result?.buyin?.let { session.result?.buyin?.let {
session.result?.buyin = it + additionalBuyins session.result?.buyin = it + additionalBuyins
} }

@ -1,7 +1,24 @@
package net.pokeranalytics.android.util.csv package net.pokeranalytics.android.util.csv
import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.CustomFieldEntry
import java.util.* import java.util.*
sealed class MappedCustomCSVField {
data class Number(
override var header: String,
override var callback: ((String) -> CustomFieldEntry?)? = null,
override val numberFormat: String?,
override var customField: CustomField) : CustomNumberCSVField
data class List(
override var header: String,
override var callback: ((String) -> CustomFieldEntry?)? = null,
override var customField: CustomField) : CustomEntryCSVField
}
sealed class TransactionField { sealed class TransactionField {
data class TransactionType( data class TransactionType(
@ -106,9 +123,16 @@ sealed class SessionField {
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberCSVField ) : NumberCSVField
data class NumberOfTables(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class Blind(override var header: String, override var callback: ((String) -> Pair<Double, Double>?)? = null) : data class Blind(override var header: String, override var callback: ((String) -> Pair<Double, Double>?)? = null) :
BlindCSVField BlindCSVField
// data class Stakes(override var header: String) : CSVField
data class Game(override var header: String) : CSVField data class Game(override var header: String) : CSVField
data class Location(override var header: String) : CSVField data class Location(override var header: String) : CSVField
data class LocationType(override var header: String) : CSVField data class LocationType(override var header: String) : CSVField
@ -121,6 +145,17 @@ sealed class SessionField {
data class TournamentName(override var header: String) : CSVField data class TournamentName(override var header: String) : CSVField
data class TournamentType(override var header: String) : CSVField data class TournamentType(override var header: String) : CSVField
data class Live(override var header: String,
override var callback: ((String) -> Boolean?)? = null) : TypedCSVField<Boolean> {
override fun parse(value: String): Boolean? {
return when (value) {
"Live", "1" -> true
else -> false
}
}
}
data class CurrencyRate( data class CurrencyRate(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Double?)? = null,
@ -133,9 +168,22 @@ sealed class SessionField {
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberCSVField ) : NumberCSVField
data class TournamentEntryFee(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class TournamentNumberOfPlayers( data class TournamentNumberOfPlayers(
override var header: String, override var header: String,
override var callback: ((String) -> Double?)? = null, override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null override val numberFormat: String? = null
) : NumberCSVField ) : NumberCSVField
data class TournamentPrizePool(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
} }

@ -24,6 +24,14 @@ inline fun <reified T: Identifiable> Realm.findById(id: String) : T? {
return this.findById(T::class.java, id) return this.findById(T::class.java, id)
} }
fun <T : NameManageable> Realm.findByName(clazz: Class<T>, name: String) : T? {
return this.where(clazz).equalTo("name", name).findFirst()
}
inline fun <reified T: NameManageable> Realm.findByName(name: String) : T? {
return this.findByName(T::class.java, name)
}
fun <T: NameManageable> Realm.getOrCreate(clazz: Class<T>, name: String) : T { fun <T: NameManageable> Realm.getOrCreate(clazz: Class<T>, name: String) : T {
val instance = this.where(clazz).equalTo("name", name).findFirst() val instance = this.where(clazz).equalTo("name", name).findFirst()
return if (instance != null) { return if (instance != null) {

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical">
<fragment
android:id="@+id/importsHistoryFragment"
android:name="net.pokeranalytics.android.ui.fragment.ImportsHistoryFragment"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:layout="@layout/fragment_imports_history" />
</LinearLayout>

@ -0,0 +1,61 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.coordinatorlayout.widget.CoordinatorLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.core.widget.NestedScrollView
android:id="@+id/nestedScrollView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:fillViewport="true"
app:layout_behavior="@string/appbar_scrolling_view_behavior">
<androidx.constraintlayout.widget.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
</androidx.core.widget.NestedScrollView>
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="match_parent"
android:layout_height="128dp"
android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar">
<com.google.android.material.appbar.CollapsingToolbarLayout
android:id="@+id/collapsingToolbar"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:collapsedTitleTextAppearance="@style/PokerAnalyticsTheme.Toolbar.CollapsedTitleAppearance"
app:contentScrim="?attr/colorPrimary"
app:expandedTitleGravity="bottom"
app:expandedTitleMarginStart="72dp"
app:expandedTitleTextAppearance="@style/PokerAnalyticsTheme.Toolbar.ExpandedTitleAppearance"
app:layout_scrollFlags="scroll|exitUntilCollapsed|snap">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_collapseMode="pin"
app:title="Poker Analytics"
app:titleTextColor="@color/white" />
</com.google.android.material.appbar.CollapsingToolbarLayout>
</com.google.android.material.appbar.AppBarLayout>
</androidx.coordinatorlayout.widget.CoordinatorLayout>

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/container"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<include layout="@layout/layout_swipe_to_delete" />
<androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/foreground"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/gray_dark"
android:foreground="?selectableItemBackground">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/title"
style="@style/PokerAnalyticsTheme.TextView.RowTitle"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginEnd="8dp"
app:layout_constraintEnd_toStartOf="@+id/action"
app:layout_constraintHorizontal_bias="0.0"
app:layout_constraintStart_toStartOf="@+id/guidelineStart"
app:layout_constraintTop_toTopOf="parent"
tools:text="Title" />
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/value"
style="@style/PokerAnalyticsTheme.TextView.RowValue"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:layout_marginBottom="16dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/action"
app:layout_constraintStart_toStartOf="@+id/guidelineStart"
app:layout_constraintTop_toBottomOf="@+id/title"
tools:text="Value" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/action"
android:layout_width="32dp"
android:layout_height="32dp"
android:background="?selectableItemBackgroundBorderless"
android:padding="4dp"
android:visibility="gone"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="@+id/guidelineEnd"
app:layout_constraintTop_toTopOf="parent"
tools:src="@drawable/ic_close"
tools:visibility="visible" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guidelineStart"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_begin="16dp" />
<androidx.constraintlayout.widget.Guideline
android:id="@+id/guidelineEnd"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="vertical"
app:layout_constraintGuide_end="16dp" />
</androidx.constraintlayout.widget.ConstraintLayout>
</FrameLayout>

@ -34,10 +34,8 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="16dp" android:layout_marginStart="16dp"
android:layout_marginEnd="8dp" android:layout_marginEnd="8dp"
android:ellipsize="end" style="@style/PokerAnalyticsTheme.TextView.RowValue"
android:gravity="end|center_vertical" android:gravity="end|center_vertical"
android:maxLines="1"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/action" app:layout_constraintEnd_toStartOf="@+id/action"
app:layout_constraintStart_toEndOf="@+id/title" app:layout_constraintStart_toEndOf="@+id/title"

@ -32,12 +32,14 @@
<string name="progress">Progress</string> <string name="progress">Progress</string>
<string name="save_report">Save Report</string> <string name="save_report">Save Report</string>
<string name="import_confirmation">Do you want to proceed with the file import?</string> <string name="import_confirmation">Do you want to proceed with the file import?</string>
<string name="import_deletion">Do you want to delete this import?</string>
<string name="update_entity" formatted="false">Update %s</string> <string name="update_entity" formatted="false">Update %s</string>
<string name="comparison_chart">Comparison chart</string> <string name="comparison_chart">Comparison chart</string>
<string name="filter_currently_selected">The filter cannot be deleted because it is currently selected.</string> <string name="filter_currently_selected">The filter cannot be deleted because it is currently selected.</string>
<string name="custom_field">Custom field</string> <string name="custom_field">Custom field</string>
<string name="transaction_relationship_error">The item is used in one or more transactions&#8230;Please delete the linked transactions first</string> <string name="transaction_relationship_error">The item is used in one or more transactions&#8230;Please delete the linked transactions first</string>
<string name="imported">Imported</string> <string name="imported">Imported</string>
<string name="imports_history">Imports history</string>
<string name="iap_session_message">You\'ve reached the maximum number of free sessions. Please subscribe for unlimited use and don\'t hesitate to tell us how you feel about your current experience!</string> <string name="iap_session_message">You\'ve reached the maximum number of free sessions. Please subscribe for unlimited use and don\'t hesitate to tell us how you feel about your current experience!</string>
<string name="stacking_incoming">Stacking incoming</string> <string name="stacking_incoming">Stacking incoming</string>
<string name="stacking_outgoing">Stacking outgoing</string> <string name="stacking_outgoing">Stacking outgoing</string>

@ -1,5 +1,6 @@
package net.pokeranalytics.android package net.pokeranalytics.android
import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.util.extensions.kmbFormatted import net.pokeranalytics.android.util.extensions.kmbFormatted
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
@ -33,4 +34,11 @@ class BasicUnitTest : RealmUnitTest() {
} }
@Test
fun testTableSizeParsing() {
Assert.assertEquals(TableSize.valueForLabel("1"), 1)
Assert.assertEquals(TableSize.valueForLabel("Full Ring"), 10)
Assert.assertNotEquals(TableSize.valueForLabel("pouet1"), 1)
}
} }

@ -1,8 +1,9 @@
package net.pokeranalytics.android package net.pokeranalytics.android
import com.google.android.libraries.places.internal.it
import net.pokeranalytics.android.calculus.Stat import net.pokeranalytics.android.calculus.Stat
import net.pokeranalytics.android.model.Criteria import net.pokeranalytics.android.model.Criteria
import net.pokeranalytics.android.model.LiveData
import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.TransactionType import net.pokeranalytics.android.model.realm.TransactionType
import org.junit.Assert import org.junit.Assert
import org.junit.Test import org.junit.Test
@ -21,6 +22,12 @@ class SavableEnumTest {
val transactionTypeValueIds = TransactionType.Value.valuesInternal().map { it.uniqueIdentifier } val transactionTypeValueIds = TransactionType.Value.valuesInternal().map { it.uniqueIdentifier }
Assert.assertEquals(transactionTypeValueIds.toSet().size, transactionTypeValueIds.size) Assert.assertEquals(transactionTypeValueIds.toSet().size, transactionTypeValueIds.size)
val liveDataIds = LiveData.valuesInternal().map { it.uniqueIdentifier }
Assert.assertEquals(liveDataIds.toSet().size, liveDataIds.size)
val customFieldTypeIds = CustomField.Type.valuesInternal().map { it.uniqueIdentifier }
Assert.assertEquals(customFieldTypeIds.toSet().size, customFieldTypeIds.size)
} }
} }
Loading…
Cancel
Save