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. 63
      app/src/main/java/net/pokeranalytics/android/model/LiveData.kt
  10. 17
      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. 43
      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. 32
      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. 7
      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. 55
      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. 70
      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"
minSdkVersion 23
targetSdkVersion 28
versionCode 47
versionName "2.1.1"
versionCode 51
versionName "2.1.5"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

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

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

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

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

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

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

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

@ -3,46 +3,44 @@ package net.pokeranalytics.android.model
import android.content.Context
import io.realm.Realm
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.ui.view.Localizable
import net.pokeranalytics.android.util.enumerations.IntIdentifiable
import net.pokeranalytics.android.util.enumerations.IntSearchable
import net.pokeranalytics.android.util.extensions.findById
/**
* An enum managing the business objects related to a realm results
*/
enum class LiveData : Localizable {
BANKROLL,
GAME,
LOCATION,
TOURNAMENT_NAME,
TOURNAMENT_FEATURE,
TRANSACTION,
TRANSACTION_TYPE,
FILTER,
CUSTOM_FIELD,
REPORT_SETUP;
enum class LiveData(override var uniqueIdentifier: Int, val relatedEntity: Class<out Identifiable>) : Localizable, IntIdentifiable {
BANKROLL(0, Bankroll::class.java),
GAME(1, Game::class.java),
LOCATION(2, Location::class.java),
TOURNAMENT_NAME(3, TournamentName::class.java),
TOURNAMENT_FEATURE(4, TournamentFeature::class.java),
TRANSACTION(5, Transaction::class.java),
TRANSACTION_TYPE(6, TransactionType::class.java),
FILTER(7, Filter::class.java),
CUSTOM_FIELD(8, CustomField::class.java),
REPORT_SETUP(9, ReportSetup::class.java),
SESSION(10, Session::class.java);
var subType:Int? = null
companion object : IntSearchable<LiveData> {
val relatedEntity: Class<out Deletable>
get() {
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
override fun valuesInternal(): Array<LiveData> {
return values()
}
fun valueFromClass(clazz: Class<out Identifiable>) : LiveData? {
return values().firstOrNull { it.relatedEntity == clazz }
}
}
var subType:Int? = null
fun updateOrCreate(realm: Realm, primaryKey: String?): Deletable {
val proxyItem: Deletable? = this.getData(realm, primaryKey)
fun updateOrCreate(realm: Realm, primaryKey: String?): Identifiable {
val proxyItem: Identifiable? = this.getData(realm, primaryKey)
proxyItem?.let {
return realm.copyFromRealm(it)
} ?: run {
@ -50,12 +48,12 @@ enum class LiveData : Localizable {
}
}
private fun newEntity(): Deletable {
private fun newEntity(): Identifiable {
return this.relatedEntity.newInstance()
}
fun getData(realm: Realm, primaryKey: String?): Deletable? {
var proxyItem: Deletable? = null
fun getData(realm: Realm, primaryKey: String?): Identifiable? {
var proxyItem: Identifiable? = null
primaryKey?.let {
val t = realm.findById(this.relatedEntity, it)
t?.let {
@ -78,6 +76,7 @@ enum class LiveData : Localizable {
FILTER -> R.string.filter
CUSTOM_FIELD -> R.string.custom_field
REPORT_SETUP -> R.string.custom
SESSION -> R.string.session
}
}
@ -94,6 +93,7 @@ enum class LiveData : Localizable {
FILTER -> R.string.filters
CUSTOM_FIELD -> R.string.custom_fields
REPORT_SETUP -> R.string.custom
SESSION -> R.string.sessions
}
}
@ -110,6 +110,7 @@ enum class LiveData : Localizable {
FILTER -> R.string.new_filter
CUSTOM_FIELD -> R.string.new_custom_field
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.ui.view.RowRepresentable
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 {
@ -15,8 +17,18 @@ class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITL
{ index -> TableSize(index + 2) }).toList()
}
fun valueForLabel(label: String) : Int? {
return when (label) {
/**
* Tries to parse a label into a Table Size Int value,
* using number parsing or label recognition
* A numberFormat can be passed when dealing with lots of volume
*/
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
@ -24,6 +36,7 @@ class TableSize(var numberOfPlayer: Int, var rowViewType: Int = RowViewType.TITL
}
}
}
}
override fun getDisplayName(context: Context): String {
return if (this.numberOfPlayer == 2) {

@ -5,6 +5,9 @@ import io.realm.Realm
import io.realm.RealmModel
import net.pokeranalytics.android.R
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
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

@ -3,6 +3,7 @@ package net.pokeranalytics.android.model.migrations
import io.realm.DynamicRealm
import io.realm.RealmMigration
import timber.log.Timber
import java.util.*
class PokerAnalyticsMigration : RealmMigration {
@ -107,7 +108,6 @@ class PokerAnalyticsMigration : RealmMigration {
it.addPrimaryKey("id")
it.addField("value", String::class.java).setNullable("value", 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)
}
@ -151,6 +151,27 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("TransactionType")?.addField("useCount", Int::class.java)
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 {

@ -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.SimpleRow
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 kotlin.collections.ArrayList
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
override val realmObjectClass: Class<out Identifiable> = CustomField::class.java
@ -39,7 +56,13 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
IntIdentifiable {
LIST(0, R.string.enum_custom_field_type),
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
*/
fun addEntry(): CustomFieldEntry {
fun addEmptyEntry(): CustomFieldEntry {
val entry = CustomFieldEntry()
this.entries.add(entry)
sortEntries()
@ -275,7 +298,8 @@ open class CustomField : RealmObject(), NameManageable, StaticRowRepresentableDa
val realm = Realm.getDefaultInstance()
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()
}
}
@ -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.model.filter.Filterable
import net.pokeranalytics.android.model.filter.QueryCondition
import timber.log.Timber
@RealmClass
open class Result : RealmObject(), Filterable {
@ -48,21 +49,31 @@ open class Result : RealmObject(), Filterable {
/**
* The net result
*/
var netResult: Double? = null
var onlineNet: Double? = null
set(value) {
var errorMessage: String? = null
this.session?.bankroll?.let { bankroll ->
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 {
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
this.computeNet()
if (value != null) {
this.session.end()
this.session?.end()
}
}
@ -107,15 +118,23 @@ open class Result : RealmObject(), Filterable {
val transactionsSum = transactions.sumByDouble { it.amount }
val isLive = this.session?.isLive ?: true
if (isLive) {
this.onlineNet?.let {
this.net = it + transactionsSum
} ?: run {
val buyin = this.buyin ?: 0.0
val cashOut = this.cashout ?: 0.0
this.net = cashOut - buyin + transactionsSum
} else {
val netResult = this.netResult ?: 0.0
this.net = netResult + transactionsSum
}
val cashedOut = this.cashout ?: 0.0
this.net = cashedOut - buyin + 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
this.session?.computeStats()

@ -45,7 +45,7 @@ import kotlin.collections.ArrayList
typealias BB = Double
open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDataSource, RowRepresentable, Timed,
open class Session : RealmObject(), Manageable, StaticRowRepresentableDataSource, RowRepresentable, Timed,
TimeFilterable, Filterable, DatedBankrollGraphEntry {
enum class Type {
@ -314,6 +314,11 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
// The features of the tournament, like Knockout, Shootout, Turbo...
var tournamentFeatures: RealmList<TournamentFeature> = RealmList()
/**
* the prizepool of the tournament
*/
var tournamentPrizepool: Double? = null
// The custom fields values
var customFieldEntries: RealmList<CustomFieldEntry> = RealmList()
@ -471,7 +476,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
val hasNetResult: Boolean
get() {
return this.result?.netResult != null
return this.result?.onlineNet != null
}
// Manageable
@ -488,6 +493,14 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
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
/**
@ -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.BUY_IN -> this.result?.buyin?.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.END_DATE -> this.endDate?.shortDateTime() ?: NULL_TEXT
SessionRow.GAME -> getFormattedGame()
@ -898,7 +911,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
)
SessionRow.NET_RESULT -> row.editingDescriptors(
mapOf(
"defaultValue" to result?.netResult
"defaultValue" to result?.onlineNet
)
)
SessionRow.COMMENT -> row.editingDescriptors(
@ -993,7 +1006,7 @@ open class Session : RealmObject(), Savable, Editable, StaticRowRepresentableDat
}
SessionRow.NET_RESULT -> {
this.result?.let { result ->
result.netResult = value as Double?
result.onlineNet = value as Double?
}
}
SessionRow.COMMENT -> comment = value as String? ?: ""

@ -61,7 +61,7 @@ class ImportActivity : PokerAnalyticsActivity() {
val fis = contentResolver.openInputStream(fileURI)
Timber.d("Load fragment data with: $fis")
fis?.let {
fragment.setData(it)
fragment.setData(it, fileURI)
}
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?) {
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 net.pokeranalytics.android.BuildConfig
import net.pokeranalytics.android.R
import net.pokeranalytics.android.exceptions.PAIllegalStateException
import net.pokeranalytics.android.ui.activity.components.PokerAnalyticsActivity
import net.pokeranalytics.android.ui.fragment.components.PokerAnalyticsFragment
import net.pokeranalytics.android.util.DeviceUtils
@ -104,9 +105,13 @@ fun PokerAnalyticsActivity.showAlertDialog(title: Int? = null, message: Int? = n
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 {
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
*/
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, message: Int? = null, messageString: String? = null,
cancelButtonTitle: Int? = null, showCancelButton: Boolean = false,
positiveAction: (() -> Unit)? = null, negativeAction: (() -> Unit)? = null) {
val builder = AlertDialog.Builder(context)
title?.let {
builder.setTitle(title)
@ -124,7 +130,10 @@ fun showAlertDialog(
message?.let {
builder.setMessage(message)
}
builder.setPositiveButton(net.pokeranalytics.android.R.string.ok) { _, _ ->
messageString?.let {
builder.setMessage(it)
}
builder.setPositiveButton(R.string.ok) { _, _ ->
positiveAction?.invoke()
}

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

@ -1,5 +1,6 @@
package net.pokeranalytics.android.ui.fragment
import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
@ -10,6 +11,7 @@ import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.extensions.showAlertDialog
import net.pokeranalytics.android.ui.fragment.components.RealmFragment
import net.pokeranalytics.android.util.csv.CSVImporter
import net.pokeranalytics.android.util.csv.ImportDelegate
@ -25,7 +27,7 @@ class ImportFragment : RealmFragment(), ImportDelegate {
val coroutineContext: CoroutineContext
get() = Dispatchers.Main
private lateinit var filePath: String
private var filePath: String? = null
private lateinit var inputStream: InputStream
private lateinit var importer: CSVImporter
@ -33,8 +35,9 @@ class ImportFragment : RealmFragment(), ImportDelegate {
this.filePath = path
}
fun setData(inputStream: InputStream) {
fun setData(inputStream: InputStream, uri: Uri) {
this.inputStream = inputStream
this.filePath = uri.path
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
@ -69,9 +72,9 @@ class ImportFragment : RealmFragment(), ImportDelegate {
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
GlobalScope.launch(coroutineContext) {
@ -83,7 +86,7 @@ class ImportFragment : RealmFragment(), ImportDelegate {
try {
importer.start()
} catch (e: ImportException) {
// shouldDismissActivity = true
errorMessage = e.message
}
val e = Date()
val duration = (e.time - s.time) / 1000.0
@ -92,16 +95,17 @@ class ImportFragment : RealmFragment(), ImportDelegate {
}
test.await()
// if (shouldDismissActivity) {
//
// activity?.let {
// it.setResult(ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value)
// it.finish()
// }
//
// } else {
// }
if (errorMessage != null) {
showAlertDialog(
messageString = errorMessage,
positiveAction = {
activity?.finish()
})
} else {
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.BUG_REPORT -> parentActivity?.openContactMail(R.string.bug_report_subject, Realm.getDefaultInstance().path)
SettingRow.CURRENCY -> CurrenciesActivity.newInstanceForResult(this@SettingsFragment, RequestCode.CURRENCY.value)
SettingRow.IMPORTS_HISTORY -> ImportsHistoryActivity.newInstance(requireContext())
SettingRow.FOLLOW_US -> {
when (position) {
0 -> parentActivity?.openUrl(URL.BLOG.value)

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

@ -75,7 +75,7 @@ open class DataManagerFragment : RealmFragment() {
*/
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
}

@ -76,6 +76,11 @@ interface Displayable : Localizable {
get() {
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_ARROW(R.layout.row_title_value_arrow),
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_GRID(R.layout.row_bottom_sheet_grid_title),
DATA(R.layout.row_title),
@ -102,8 +103,8 @@ enum class RowViewType(private var layoutRes: Int) {
// Row View Holder
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,
TITLE_SWITCH, TITLE_CHECK, TITLE_VALUE_CHECK,
INFO, TITLE, TITLE_ARROW, TITLE_ICON_ARROW, TITLE_VALUE, TITLE_VALUE_ARROW, TITLE_VALUE_ACTION, TITLE_SUBTITLE_ACTION,
TITLE_GRID, TITLE_SWITCH, TITLE_CHECK, TITLE_VALUE_CHECK,
DATA, BOTTOM_SHEET_DATA, LOADER -> RowViewHolder(layout)
// Row Session
@ -244,6 +245,7 @@ enum class RowViewType(private var layoutRes: Int) {
}
// Listener
if (row.isClickable) {
val listener = View.OnClickListener {
itemView.findViewById<SwitchCompat?>(R.id.switchView)?.let {
if (adapter.dataSource.isEnabled(row)) {
@ -257,6 +259,7 @@ enum class RowViewType(private var layoutRes: Int) {
itemView.findViewById<View?>(R.id.container)?.setOnClickListener(listener)
}
}
}
// Switch
itemView.findViewById<SwitchCompat?>(R.id.switchView)?.let {

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

@ -1,18 +1,18 @@
package net.pokeranalytics.android.util.csv
import io.realm.Realm
import io.realm.kotlin.deleteFromRealm
import net.pokeranalytics.android.model.interfaces.Identifiable
import net.pokeranalytics.android.model.interfaces.ObjectIdentifier
import net.pokeranalytics.android.model.realm.Session
import net.pokeranalytics.android.util.extensions.findById
import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.Import
import org.apache.commons.csv.CSVRecord
import timber.log.Timber
import java.text.NumberFormat
/**
* The various sources of CSV
*/
enum class DataSource {
POKER_ANALYTICS,
POKER_INCOME,
POKER_BANKROLL_TRACKER,
RUNGOOD
@ -23,25 +23,19 @@ enum class DataSource {
*/
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?
override fun parse(realm: Realm, record: CSVRecord): Int {
val data = this.parseData(realm, record)
data?.let {
// Timber.d(">>>>>>> identifier added: ${it.id}")
this.realmModelIds.add(it.objectIdentifier)
this.import.identifiableObjects.add(it.objectIdentifier.identifiableObject)
}
return if (data != null) 1 else 0
}
protected fun addAdditionallyCreatedIdentifiable(identifiable: Identifiable) {
this.realmModelIds.add(identifiable.objectIdentifier)
this.import.identifiableObjects.add(identifiable.objectIdentifier.identifiableObject)
}
override fun cancel(realm: Realm) {
@ -57,20 +51,11 @@ abstract class DataCSVDescriptor<T : Identifiable>(source: DataSource, vararg el
}
private fun deleteInsertedFromRealm(realm: Realm) {
this.realmModelIds.forEach { identifier ->
val data = realm.findById(identifier.clazz, identifier.id)
if (data is Session) {
data.cleanup()
}
data?.deleteFromRealm()
}
this.realmModelIds.clear()
this.import.delete()
}
override fun save(realm: 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
*/
protected var fields: List<CSVField> = listOf()
protected var fields: MutableList<CSVField> = mutableListOf()
/**
* The mapping of CSVField with their index in the CSV file
*/
protected var fieldMapping: MutableMap<CSVField, Int> = mutableMapOf()
lateinit var import: Import
init {
if (elements.size > 0) {
this.fields = elements.toList()
if (elements.isNotEmpty()) {
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
*/
@ -135,9 +112,46 @@ abstract class CSVDescriptor(var source: DataSource, vararg elements: CSVField)
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")
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
import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.CustomFieldEntry
import timber.log.Timber
import java.text.DateFormat
import java.text.NumberFormat
@ -14,7 +16,9 @@ interface NumberCSVField: TypedCSVField<Double> {
val numberFormat: String?
override fun parse(value: String) : Double? {
companion object {
fun defaultParse(value: String) : Double? {
if (value.isEmpty()) {
return null
@ -25,12 +29,22 @@ interface NumberCSVField: TypedCSVField<Double> {
return try {
formatter.parse(value).toDouble()
} catch (e: ParseException) {
Timber.d("Field ${header} > Unparseable number: $value")
Timber.d("Field > Unparseable number: $value")
null
}
}
}
override fun parse(value: String) : Double? {
this.callback?.let { cb ->
return cb(value)
}
return defaultParse(value)
}
}
interface DataCSVField<T> : TypedCSVField<T> {
override fun parse(value: String): 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 {
fun parse(value: String) : T?
var callback: ((String) -> T?)?

@ -3,6 +3,7 @@ package net.pokeranalytics.android.util.csv
import android.os.Handler
import android.os.Looper
import io.realm.Realm
import net.pokeranalytics.android.model.realm.Import
import org.apache.commons.csv.CSVFormat
import org.apache.commons.csv.CSVParser
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
* to the CSVDescriptor
*/
open class CSVImporter(istream: InputStream) {
open class CSVImporter(istream: InputStream, filePath: String? = null) {
/**
* 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
*/
private val COMMIT_FREQUENCY = 100
/**
* The number of column indicating a valid record
*/
private val VALID_RECORD_COLUMNS = 4
/**
* 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
*/
private var path: String? = null
private var path: String? = filePath
/**
* The InputStream containing a file content
*/
@ -105,13 +108,18 @@ open class CSVImporter(istream: InputStream) {
realm.beginTransaction()
val import = realm.copyToRealm(Import())
this.path?.let {
import.fileName = it
}
parser.forEachIndexed { index, record ->
// Timber.d("line $index")
this.notifyDelegate()
if (this.currentDescriptor == null) { // find descriptor
this.currentDescriptor = this.findDescriptor(record)
this.currentDescriptor = this.findDescriptor(realm, record, import)
if (this.currentDescriptor == null) {
@ -174,10 +182,13 @@ open class CSVImporter(istream: InputStream) {
/**
* 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)) {
descriptor.import = import
descriptor.mapCustomField(record, realm)
this.currentDescriptor = descriptor
Timber.d("Identified source: ${descriptor.source}")
return descriptor

@ -4,7 +4,21 @@ class ProductCSVDescriptors {
companion object {
val pokerIncomeCash: CSVDescriptor = SessionCSVDescriptor(
/**
* The list of all managed CSVDescriptors
*/
val all: List<CSVDescriptor> =
listOf(
pokerIncomeCash,
pokerBankrollTracker,
runGoodCashGames,
runGoodTournaments,
iOSPokerAnalytics
)
private val pokerIncomeCash: CSVDescriptor
get() {
return SessionCSVDescriptor(
DataSource.POKER_INCOME,
false,
SessionField.Start("Start Time"),
@ -21,8 +35,11 @@ class ProductCSVDescriptors {
SessionField.Tips("Tips"),
SessionField.Blind("Stake")
)
}
val pokerBankrollTracker: CSVDescriptor = SessionCSVDescriptor(
private val pokerBankrollTracker: CSVDescriptor
get() {
return SessionCSVDescriptor(
DataSource.POKER_BANKROLL_TRACKER,
true,
SessionField.Start("starttime", dateFormat = "MM/dd/yy HH:mm"),
@ -50,8 +67,48 @@ class ProductCSVDescriptors {
SessionField.CurrencyRate("exchangerate"),
SessionField.TableSize("tablesize")
)
}
val runGoodTournaments: CSVDescriptor = SessionCSVDescriptor(
private val iOSPokerAnalytics: CSVDescriptor
get() {
return SessionCSVDescriptor(
DataSource.POKER_ANALYTICS,
true,
SessionField.Start("Start Date", dateFormat = "MM/dd/yy HH:mm:ss"),
SessionField.End("End Date", dateFormat = "MM/dd/yy HH:mm:ss"),
SessionField.Break("Break", callback = { string ->
val number = NumberCSVField.defaultParse(string)
return@Break number?.times(1000.0)
}),
SessionField.SessionType("Type"),
SessionField.Live("Live"),
SessionField.NumberOfTables("Tables"),
SessionField.Buyin("Buyin"),
SessionField.CashedOut("Cashed Out"),
SessionField.NetResult("Online Net"),
SessionField.Tips("Tips"),
SessionField.LimitType("Limit"),
SessionField.Game("Game"),
SessionField.TableSize("Table Size"),
SessionField.Location("Location"),
SessionField.Bankroll("Bankroll"),
SessionField.CurrencyCode("Currency Code"),
SessionField.CurrencyRate("Currency Rate"),
SessionField.SmallBlind("Small Blind"),
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"),
@ -74,10 +131,12 @@ class ProductCSVDescriptors {
SessionField.TournamentNumberOfPlayers("Total Players"),
SessionField.TournamentPosition("Finished Place"),
SessionField.TournamentType("Single-Table/Multi-Table")
)
}
val runGoodCashGames: CSVDescriptor = SessionCSVDescriptor(
private val runGoodCashGames: CSVDescriptor
get() {
return SessionCSVDescriptor(
DataSource.RUNGOOD,
false,
SessionField.Start("Start Date", dateFormat = "dd/MM/yyyy"),
@ -107,6 +166,7 @@ class ProductCSVDescriptors {
}
})
)
}
}

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

@ -1,7 +1,24 @@
package net.pokeranalytics.android.util.csv
import net.pokeranalytics.android.model.realm.CustomField
import net.pokeranalytics.android.model.realm.CustomFieldEntry
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 {
data class TransactionType(
@ -106,9 +123,16 @@ sealed class SessionField {
override val numberFormat: String? = null
) : 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) :
BlindCSVField
// data class Stakes(override var header: String) : CSVField
data class Game(override var header: String) : CSVField
data class Location(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 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(
override var header: String,
override var callback: ((String) -> Double?)? = null,
@ -133,9 +168,22 @@ sealed class SessionField {
override val numberFormat: String? = null
) : NumberCSVField
data class TournamentEntryFee(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : NumberCSVField
data class TournamentNumberOfPlayers(
override var header: String,
override var callback: ((String) -> Double?)? = null,
override val numberFormat: String? = null
) : 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)
}
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 {
val instance = this.where(clazz).equalTo("name", name).findFirst()
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_marginStart="16dp"
android:layout_marginEnd="8dp"
android:ellipsize="end"
style="@style/PokerAnalyticsTheme.TextView.RowValue"
android:gravity="end|center_vertical"
android:maxLines="1"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/action"
app:layout_constraintStart_toEndOf="@+id/title"

@ -32,12 +32,14 @@
<string name="progress">Progress</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_deletion">Do you want to delete this import?</string>
<string name="update_entity" formatted="false">Update %s</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="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="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="stacking_incoming">Stacking incoming</string>
<string name="stacking_outgoing">Stacking outgoing</string>

@ -1,5 +1,6 @@
package net.pokeranalytics.android
import net.pokeranalytics.android.model.TableSize
import net.pokeranalytics.android.util.extensions.kmbFormatted
import org.junit.Assert
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
import com.google.android.libraries.places.internal.it
import net.pokeranalytics.android.calculus.Stat
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 org.junit.Assert
import org.junit.Test
@ -21,6 +22,12 @@ class SavableEnumTest {
val transactionTypeValueIds = TransactionType.Value.valuesInternal().map { it.uniqueIdentifier }
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